diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..64fa418b --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +tmp/ +*.class +.vscode/ +.venv/ +docker.sh +Dockerfile +burp-extensions-montoya-api-examples/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +.idea +.gradle +.DS_Store +build/ +out/ +dist/ +bin/ +.prettierc + +*.pyc + +notes/ + +log*.txt + +__pycache__ + +generated/ +src/public/ +node_modules/ +# public/ +src/resources/_gen/ +.venv/ +build/ +bin/ +.gradle +.idea +notes/ +__pycache__ +old_deocs/ +TODO.md +install.md +note.md +burp.licence +TODO.MD +macosinstall.md +a.java +python3.8/ +python3-8/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..275655a6 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,85 @@ +stages: + - prepare + - build + - test + - build_docs_stage + - update_docs + +variables: + GRADLE_OPTS: "-Dorg.gradle.daemon=false" + +prepare_python_env: + stage: prepare + image: python:3.10 + script: + - python3 -m venv .venv + - source .venv/bin/activate + - pip install -r docs/requirements.txt + artifacts: + paths: + - .venv/ + +build_scalpel: + stage: build + image: python:3.10 + script: + - apt-get update -y + - apt-get install -y openjdk-17-jdk + - ./gradlew build + artifacts: + paths: + - scalpel/build/libs/*.jar + +build_javadoc: + stage: build + image: python:3.10 + script: + - apt-get update -y + - apt-get install -y openjdk-17-jdk + - ./gradlew javadoc + artifacts: + paths: + - scalpel/build/docs/javadoc/ + +build_docs: + stage: build_docs_stage + dependencies: + - prepare_python_env + - build_javadoc + image: python:3.10 + script: + - apt-get update -y + - apt-get install -y hugo + - source .venv/bin/activate + - rm -rf docs/public/javadoc/ + - cp -r scalpel/build/docs/javadoc/ docs/public/javadoc/ + - cd docs && ./build.py --no-javadoc + artifacts: + paths: + - docs/public/ + +run_tests: + stage: test + dependencies: + - prepare_python_env + image: python:3.10 + script: + - source .venv/bin/activate + - sh run_tests.sh + +update_docs: + stage: update_docs + only: + - main + image: debian + script: + - export GIT_SSL_NO_VERIFY=true + - apt-get update && apt-get install -y git + - git config --global user.email "ci@fakegitlab.com" + - git config --global user.name "GitLab CI" + - git remote set-url origin "${CI_REPOSITORY_URL}" + - git fetch origin main:main # Fetch main branch + - git checkout main # Switch to main branch + - git add docs/public/ + - git commit -m "Update generated docs [skip ci]" || echo "No changes to commit" + - git push origin main diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..d5153c7f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "trailingComma": "all", + "useTabs": true, + "tabWidth": 4, + "semi": true, + "singleQuote": false, + "quoteProps": "consistent", + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 00000000..9e6f7d07 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[MASTER] +init-hook='import sys; sys.path.append("./scalpel/src/main/resources/python")' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..9ac09f9e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,132 @@ +# Contributing to Scalpel + +Thank you for your interest in contributing to Scalpel! This document outlines the process and guidelines for contributing to the project. By following these guidelines, you can help ensure a smooth collaboration process and a consistent codebase. + +## Table of Content + +- [Setting Up Your Development Environment](#setting-up-your-development-environment) +- [Building the Project](#building-the-project) + - [Building Scalpel](#building-scalpel) + - [Building the documentation](#building-the-documentation) +- [Testing](#testing) +- [Commit and Branch Format](#commit-and-branch-format) + - [Commit Messages](#commit-messages) + - [Branch Naming](#branch-naming) +- [Submitting Changes](#submitting-changes) +- [Feedback and Reviews](#feedback-and-reviews) +- [Conclusion](#conclusion) + +## Setting Up Your Development Environment + +1. **Fork the Repository**: Start by forking the Scalpel repository to your own GitHub account. + +2. **Clone Your Fork**: Once done, clone your repository to your local machine: + + ```sh + git clone https://github.com/YOUR_USERNAME/scalpel.git + ``` + +3. **Set Up the Upstream Remote**: Add the original Scalpel repository as an "upstream" remote: + ```sh + git remote add upstream https://github.com/ORIGINAL_OWNER/scalpel.git + ``` + +## Building the Project + +### Building Scalpel + +1. Navigate to the project root directory. +2. Build the project: + ```sh + ./gradlew build + ``` +3. Upon successful build, the generated JAR file can be found in `./scalpel/build/libs/scalpel-*.jar`. + +### Building the documentation + +1. Navigate to the docs directory: + ```sh + cd docs/ + ``` +2. Create a virtual environment and install the requirements from `requirements.txt`: + ```sh + python3 -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt + ``` +3. Launch the build: + ```sh + ./build.py + ``` +4. The documentation HTML pages will be generated in `public/` + +## Testing + +Before submitting any changes, ensure that all tests pass. Run them as follows: + +```sh +./run_tests.sh +``` + +## Commit and Branch Format + +### Commit Messages + +Commit messages should be clear and descriptive. They should follow the format: + +``` +(): +``` + +- **type**: Describes the nature of the change (e.g., `fix`, `feature`, `docs`, `refactor`). +- **scope**: The part of the codebase the change affects (e.g., `editor`, `venv`, `framework`). +- **short description**: A brief description of the change. + +Example: + +``` +fix(editor): Resolve null reference issue +``` + +### Branch Naming + +Branch names should be descriptive and follow the format: + +``` +/ +``` + +- **type**: Describes the nature of the branch (e.g., `feature`, `fix`, `docs`, `refactor`). +- **short description**: A brief description of the branch's purpose, using kebab-case. + +Example: + +``` +feature/hex-editor +``` + +## Submitting Changes + +1. **Create a New Branch**: Based on the `main` branch, create a new branch following the branch naming convention mentioned above. + +2. **Make Your Changes**: Implement your changes, ensuring code quality and consistency. + +3. **Commit Your Changes**: Commit your changes following the commit message format. + +4. **Pull from Upstream**: Before pushing your changes, pull the latest changes from the `upstream` main branch: + + ```sh + git pull upstream main + ``` + +5. **Push to Your Fork**: Push your branch to your forked repository. + +6. **Open a Pull Request**: Go to the original Scalpel repository and open a pull request from your branch. Ensure that your PR is descriptive, mentioning the changes made and their purpose. + +## Feedback and Reviews + +Once your pull request is submitted, maintainers or contributors might provide feedback. Address any comments, make necessary changes, and push those updates to your branch. + +## Conclusion + +Your contributions are valuable in making Scalpel a robust and efficient tool. By adhering to these guidelines, you ensure a smooth and efficient collaboration process. Thank you for your contribution! diff --git a/LICENCE.md b/LICENCE.md new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/LICENCE.md @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..802e055e --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +# Scalpel + +Scalpel is a powerful **Burp Suite** extension that allows you to script Burp in order to intercept, rewrite HTTP traffic on the fly, and program custom Burp editors in Python 3. + +It provides an interactive way to edit encoded/encrypted data as plaintext and offers an easy-to-use Python library as an alternative to Burp's Java API. + +## Features + +- **Python Library**: Easy-to-use Python library, especially welcome for non-Java developers. + +- **Intercept and Rewrite HTTP Traffic**: Scalpel provides a set of predefined function names that can be implemented to intercept and modify HTTP requests and responses. + +- **Custom Burp Editors**: Program your own Burp editors in Python. Encoded/encrypted data can be handled as plaintext. + + - **Hex Editors**: Ability to create improved hex editors. + +## Usage + +Scalpel provides a Burp extension GUI for scripting and a set of predefined function names corresponding to specific actions. Simply write a Python script implementing the ones you need. + +Below is an example script: + +```py +from pyscalpel import Request, Response, Flow + +# Hook to determine whether an event should be handled by a hook +def match(flow: Flow) -> bool: + return flow.host_is("localhost") + +# Hook to intercept and rewrite a request +def request(req: Request) -> Request | None: + req.headers["X-Python-Intercept-Request"] = "request" + return req + +# Hook to intercept and rewrite a response +def response(res: Response) -> Response | None: + res.headers["X-Python-Intercept-Response"] = "response" + return res + +# Hook to create or update a request editor's content from a request +def req_edit_in(req: Request) -> bytes | None: + req.headers["X-Python-In-Request-Editor"] = "req_edit_in" + return bytes(req) + +# Hook to update a request from an editor's modified content +def req_edit_out(_: Request, text: bytes) -> Request | None: + req = Request.from_raw(text) + req.headers["X-Python-Out-Request-Editor"] = "req_edit_out" + return req + +# Hook to create or update a response editor's content from a response +def res_edit_in(res: Response) -> bytes | None: + res.headers["X-Python-In-Response-Editor"] = "res_edit_in" + return bytes(res) + +# Hook to update a response from an editor's modified content +def res_edit_out(_: Response, text: bytes) -> Response | None: + res = Response.from_raw(text) + res.headers["X-Python-Out-Response-Editor"] = "res_edit_out" + return res +``` + +## Documentation + +User documentation is available [**here**](https://ambionics.github.io/scalpel/public). + +## Examples + +Example scripts are available in the [`examples/`](scalpel/src/main/resources/python3-10/samples/) directory of the project. + +## Requirements + +Scalpel is compatible with Windows, Linux and MacOS. + +- OpenJDK >= `17` +- Python >= `3.8` +- pip +- python-virtualenv + +### Debian-based distributions + +The following packages are required: + +```sh +sudo apt install build-essential python3 python3-dev python3-venv openjdk-17-jdk +``` + +### Fedora / RHEL / CentOS + +The following packages are required: + +```sh +sudo dnf install @development-tools python3 python3-devel python3-virtualenv java-17-openjdk-devel +``` + +### Arch-based distributions + +The following packages are required: + +```sh +sudo pacman -S base-devel python python-pip python-virtualenv jdk-openjdk +``` + +### Windows + +Microsoft Visual C++ >=14.0 is required: +https://visualstudio.microsoft.com/visual-cpp-build-tools/ + +## Installation + +Download the latest JAR release of Scalpel from [GitHub](https://github.com/ambionics/scalpel/releases). + +The release file has to be added to Burp Suite as an extension. + +Learn more in the [documentation](https://ambionics.github.io/scalpel/public/overview-installation/). + +
+ +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). + +## License + +Scalpel is licensed under [Apache License 2.0](LICENCE.md). + +## Contact + +For any questions or feedback, please open an issue or contact the [maintainer](mailto:n.maccary@lexfo.fr). diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..2ffd566e --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +generated/ +node_modules/ +src/resources/_gen/ diff --git a/docs/.pylintrc b/docs/.pylintrc new file mode 100644 index 00000000..373761f4 --- /dev/null +++ b/docs/.pylintrc @@ -0,0 +1,2 @@ +[MASTER] +init-hook='import sys; sys.path.append("./../scalpel/src/main/resources/python")' diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..389969f1 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,13 @@ +# Documentation + +This directory houses the documentation available at https://ambionics.github.io/scalpel/public/ . + +## Prerequisites + +1. Install [hugo "extended"](https://gohugo.io/getting-started/installing/). + +## Editing docs locally + +1. Make sure the Python requirements are installed and the virtual python environment is activated. +2. Run `./build.py` to generate additional documentation source files. +3. Now you can change your working directory to `./src` and run `hugo server -D`. diff --git a/docs/build.py b/docs/build.py new file mode 100755 index 00000000..0e84b25f --- /dev/null +++ b/docs/build.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import argparse +import shutil +import subprocess +from pathlib import Path +import sys +import os +import pdoc + +parser = argparse.ArgumentParser(description="Build documentation.") +parser.add_argument( + "--no-javadoc", action="store_true", help="Disable Javadoc generation" +) + +args = parser.parse_args() + +here = Path(__file__).parent +pyscalpel_path = here.parent / "scalpel" / "src" / "main" / "resources" / "python3-10" + +os.environ["_DO_NOT_IMPORT_JAVA"] = "1" +os.environ["PYTHONPATH"] = f"{os.environ.get('PYTHONPATH') or ''}:{pyscalpel_path}" +sys.path.append(str(pyscalpel_path)) + +for script in sorted((here / "scripts").glob("*.py")): + print(f"Generating output for {script.name}...") + out = subprocess.check_output( + ["python3", script.absolute()], cwd=here, text=True, env=os.environ + ) + if out: + (here / "src" / "generated" / f"{script.stem}.html").write_text( + out, encoding="utf8" + ) + +if (here / "public").exists(): + shutil.rmtree(here / "public") + + +subprocess.run(["hugo"], cwd=here / "src", check=True) +if not args.no_javadoc: + subprocess.run(["./gradlew", "javadoc"], cwd=here.parent, check=True) + shutil.copytree( + here.parent / "scalpel" / "build" / "docs" / "javadoc", + here / "public" / "javadoc", + dirs_exist_ok=True, + ) + +pdoc.pdoc( + pyscalpel_path, + output_directory=here / "public" / "pdoc", +) diff --git a/docs/modd.conf b/docs/modd.conf new file mode 100644 index 00000000..f3fd350b --- /dev/null +++ b/docs/modd.conf @@ -0,0 +1,7 @@ +scripts/** { + prep: python3 build.py +} + +{ + daemon: cd src; hugo server -D +} diff --git a/docs/public/addons-debugging/index.html b/docs/public/addons-debugging/index.html new file mode 100644 index 00000000..d8e59fda --- /dev/null +++ b/docs/public/addons-debugging/index.html @@ -0,0 +1,237 @@ + + + + + + + + + Debugging + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  Debugging

+

Scalpel scripts can be hard to debug, as you cannot run them outside of Burp.

+

Also it is difficult to know if a bug is related to Scalpel/Burp context or to the user’s implementation.

+

Here are a few advices for debugging Scalpel errors.

+

#  Finding stacktraces

+

Errors that occur in scripts can be found in different places:

+

#  1. The Output tab

+

In the Scalpel tab, there is a sub-tab named Script Output, it shows all the standard output and error contents outputted by the current script +

+
+

+

#  2. The error popus

+

When an error happens in the hooks request() and response(), a popup is displayed. +

+
+

+

This popup can be disabled in the Settings tab

+

#  3. The dashboard event log

+

+
+ +The user may click on the events to get the full error message: +
+
+

+

#  4. The extensions logs

+
+
+ +

#  5. The command line output (best)

+
+
+ +
+

💡 When debugging, it is best to launch Burp in CLI, as the CLI output will contain absolutely all errors and logs, which is not always the case in the Burp GUI (e.g: In case of deadlocks, crashes and other tricky issues).

+
+ + +
+
+ + + diff --git a/docs/public/addons-events/index.html b/docs/public/addons-events/index.html new file mode 100644 index 00000000..fbe989be --- /dev/null +++ b/docs/public/addons-events/index.html @@ -0,0 +1,10 @@ + + + + /api/events.html + + + + + + diff --git a/docs/public/addons-examples/index.html b/docs/public/addons-examples/index.html new file mode 100644 index 00000000..49f6b5a0 --- /dev/null +++ b/docs/public/addons-examples/index.html @@ -0,0 +1,351 @@ + + + + + + + + + Examples + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  Script examples

+

This page provides example scripts to get familiar with Scalpel’s Python library. They are designed for real use cases.

+

#  Table of content

+ +

#  GZIP-ed API

+

Let’s assume you encountered an API using a custom protocol that gzips multiple form-data fields.

+

Quick-and-dirty Scalpel script to directly edit the unzipped data and find hidden secrets:

+
from pyscalpel import Request, Response, logger
+import gzip
+
+
+def unzip_bytes(data):
+    try:
+        # Create a GzipFile object with the input data
+        with gzip.GzipFile(fileobj=data) as gz_file:
+            # Read the uncompressed data
+            uncompressed_data = gz_file.read()
+        return uncompressed_data
+    except OSError as e:
+        logger.error(f"Error: Failed to unzip the data - {e}")
+
+
+def req_edit_in_fs(req: Request) -> bytes | None:
+    gz = req.multipart_form["fs"].content
+
+    # Decode utf-16 and re-encoding to get rid of null bytes in the editor
+    content = gzip.decompress(gz).decode("utf-16le").encode("latin-1")
+    return content
+
+
+def req_edit_out_fs(req: Request, text: bytes) -> Request | None:
+    data = text.decode("latin-1").encode("utf-16le")
+    content = gzip.compress(data, mtime=0)
+    req.multipart_form["fs"].content = content
+    return req
+
+
+def req_edit_in_filetosend(req: Request) -> bytes | None:
+    gz = req.multipart_form["filetosend"].content
+    content = gzip.decompress(gz)
+    return content
+
+
+def req_edit_out_filetosend(req: Request, text: bytes) -> Request | None:
+    data = text
+    content = gzip.compress(data, mtime=0)
+    req.multipart_form["filetosend"].content = content
+    return req
+
+
+def res_edit_in(res: Response) -> bytes | None:
+    gz = res.content
+    if not gz:
+        return
+
+    content = gzip.decompress(gz)
+    content.decode("utf-16le").encode("utf-8")
+    return content
+
+
+def res_edit_out(res: Response, text: bytes) -> Response | None:
+    res.content = text
+    return res
+

#  Cryptography using a session as a secret

+

In this case, the client encrypted its form data using a session token obtained upon authentication.

+

This script demonstrates that Scalpel can be easily used to deal with stateful behaviors:

+
+

💡 Find a mock API to test this case in Scalpel’s GitHub repository: test/server.js.

+
+
from pyscalpel import Request, Response, Flow
+from Crypto.Cipher import AES
+from Crypto.Hash import SHA256
+from Crypto.Util.Padding import pad, unpad
+from base64 import b64encode, b64decode
+
+
+session: bytes = b""
+
+
+def match(flow: Flow) -> bool:
+    return flow.path_is("/encrypt-session*") and bool(
+        session or flow.request.method != "POST"
+    )
+
+
+def get_cipher(secret: bytes, iv=bytes(16)):
+    hasher = SHA256.new()
+    hasher.update(secret)
+    derived_aes_key = hasher.digest()[:32]
+    cipher = AES.new(derived_aes_key, AES.MODE_CBC, iv)
+    return cipher
+
+
+def decrypt(secret: bytes, data: bytes) -> bytes:
+    data = b64decode(data)
+    cipher = get_cipher(secret)
+    decrypted = cipher.decrypt(data)
+    return unpad(decrypted, AES.block_size)
+
+
+def encrypt(secret: bytes, data: bytes) -> bytes:
+    cipher = get_cipher(secret)
+    padded_data = pad(data, AES.block_size)
+    encrypted = cipher.encrypt(padded_data)
+    return b64encode(encrypted)
+
+
+def response(res: Response) -> Response | None:
+    if res.request.method == "GET":
+        global session
+        session = res.content or b""
+        return
+
+
+def req_edit_in_encrypted(req: Request) -> bytes:
+    secret = session
+    encrypted = req.form[b"encrypted"]
+    if not encrypted:
+        return b""
+
+    return decrypt(secret, encrypted)
+
+
+def req_edit_out_encrypted(req: Request, text: bytes) -> Request:
+    secret = session
+    req.form[b"encrypted"] = encrypt(secret, text)
+    return req
+
+
+def res_edit_in_encrypted(res: Response) -> bytes:
+    secret = session
+    encrypted = res.content
+
+    if not encrypted:
+        return b""
+
+    return decrypt(secret, encrypted)
+
+
+def res_edit_out_encrypted(res: Response, text: bytes) -> Response:
+    secret = session
+    res.content = encrypt(secret, text)
+    return res
+

+
+

If you encountered an interesting case, feel free to contact us to add it!

+
+ + +
+
+ + + diff --git a/docs/public/addons-java/index.html b/docs/public/addons-java/index.html new file mode 100644 index 00000000..123100da --- /dev/null +++ b/docs/public/addons-java/index.html @@ -0,0 +1,246 @@ + + + + + + + + + Using the Burp API + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  Using the Burp API

+

Scalpel communicates with Burp through its Java API. Then, it provides the user with an execution context in which they should only use Python objects.

+

However, since Scalpel focuses on HTTP objects, it does not provide utilities for all the Burp API features (like the ability to generate Collaborator payloads).

+

If the user must deal with unhandled cases, they can directly access the MontoyaApi Java object to search for appropriate objects.

+

#  Examples

+

A script that spoofs the Host header with a collaborator payload:

+
from pyscalpel import Request, ctx
+
+# Spoof the Host header to a Burp collaborator payload to detect out-of-band interactions and HTTP SSRFs
+
+# Directly access the Montoya API Java object to generate a payload
+PAYLOAD = str(ctx["API"].collaborator().defaultPayloadGenerator().generatePayload())
+
+
+def request(req: Request) -> Request | None:
+    req.host_header = PAYLOAD
+    return req
+
+

💡 PortSwigger’s documentation for the Collaborator Generator.

+
+
+

A script that sends every request that has the cmd param to Burp Repeater:

+
from pyscalpel import Request, ctx
+from threading import Thread
+
+# Send every request that contains the "cmd" param to repeater
+
+# Ensure added request are unique by using a set
+seen = set()
+
+
+def request(req: Request) -> None:
+    cmd = req.query.get("cmd")
+    if cmd is not None and cmd not in seen:
+        # Convert request to Burp format
+        breq = req.to_burp()
+
+        # Directly access the Montoya API Java object to send the request to repeater
+        repeater = ctx["API"].repeater()
+
+        # Wait for sendToRepeater() while intercepting a request causes a Burp deadlock
+        Thread(target=lambda: repeater.sendToRepeater(breq, f"cmd={cmd}")).start()
+
+

💡 PortSwigger’s documentation for Burp repeater

+
+ + +
+
+ + + diff --git a/docs/public/api/events.html b/docs/public/api/events.html new file mode 100644 index 00000000..b0882c98 --- /dev/null +++ b/docs/public/api/events.html @@ -0,0 +1,868 @@ + + + + + + + + + Event Hooks & API + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  Available Hooks

+

The following list all available event hooks.

+

The full Python documentation is available here

+ + + +
+
+

+events

+ + + + + + +
 1from pyscalpel import Request, Response, Flow, MatchEvent
+ 2
+ 3
+ 4def match(flow: Flow, events: MatchEvent) -> bool:
+ 5    """- Determine whether an event should be handled by a hook.
+ 6
+ 7    - Args:
+ 8        - flow ([Flow](../pdoc/python3-10/pyscalpel.html#Flow)): The event context (contains request and optional response).
+ 9        - events ([MatchEvent](../pdoc/python3-10/pyscalpel.html#MatchEvent)): The hook type (request, response, req_edit_in, ...).
+10
+11    - Returns:
+12        - bool: True if the event must be handled. Otherwise, False.
+13    """
+14
+15
+16def request(req: Request) -> Request | None:
+17    """- Intercept and rewrite a request.
+18
+19    - Args:
+20        - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The intercepted request
+21
+22    - Returns:
+23        - [Request](../pdoc/python3-10/pyscalpel.html#Request) or None: The modified request. Otherwise, None to ignore the request.
+24    """
+25
+26
+27def response(res: Response) -> Response | None:
+28    """- Intercept and rewrite a response.
+29
+30    - Args:
+31        - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The intercepted response.
+32
+33    - Returns:
+34        - [Response](../pdoc/python3-10/pyscalpel.html#Response) or None: The modified response. Otherwise, None to ignore the response.
+35    """
+36
+37
+38def req_edit_in(req: Request) -> bytes | None:
+39    """- Create or update a request editor's content from a request.
+40       - May be used to decode a request to plaintext.
+41
+42    - Args:
+43        - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The HTTP request.
+44
+45    - Returns:
+46        - bytes or None: The editor's contents.
+47    """
+48
+49
+50def req_edit_out(req: Request, modified_content: bytes) -> Request | None:
+51    """- Update a request from an editor's modified content.
+52       - May be used to encode a request from plaintext (modified_content).
+53
+54    - Args:
+55        - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The original request.
+56        - modified_content (bytes): The editor's content.
+57
+58    - Returns:
+59        - [Request](../pdoc/python3-10/pyscalpel.html#Request) or None: The new request.
+60    """
+61
+62
+63def res_edit_in(res: Response) -> bytes | None:
+64    """- Create or update a response editor's content from a response.
+65       - May be used to decode a response to plaintext.
+66
+67    - Args:
+68        - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The HTTP response.
+69
+70    - Returns:
+71        - bytes or None: The editor contents.
+72    """
+73
+74
+75def res_edit_out(res: Response, modified_content: bytes) -> Response | None:
+76    """- Update a response from an editor's modified content.
+77       - May be used to encode a response from plaintext (modified_content).
+78
+79    - Args:
+80        - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The original response.
+81        - modified_content (bytes): The editor's content.
+82
+83    - Returns:
+84        - [Response](../pdoc/python3-10/pyscalpel.html#Response) or None: The new response.
+85    """
+
+ + +
+
+ +
+ + def + match( flow: pyscalpel.http.flow.Flow, events: Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out']) -> bool: + + + +
+ +
 5def match(flow: Flow, events: MatchEvent) -> bool:
+ 6    """- Determine whether an event should be handled by a hook.
+ 7
+ 8    - Args:
+ 9        - flow ([Flow](../pdoc/python3-10/pyscalpel.html#Flow)): The event context (contains request and optional response).
+10        - events ([MatchEvent](../pdoc/python3-10/pyscalpel.html#MatchEvent)): The hook type (request, response, req_edit_in, ...).
+11
+12    - Returns:
+13        - bool: True if the event must be handled. Otherwise, False.
+14    """
+
+ + +
    +
  • Determine whether an event should be handled by a hook.

  • +
  • Args:

    + +
      +
    • flow (Flow): The event context (contains request and optional response).
    • +
    • events (MatchEvent): The hook type (request, response, req_edit_in, ...).
    • +
  • +
  • Returns:

    + +
      +
    • bool: True if the event must be handled. Otherwise, False.
    • +
  • +
+
+ + +
+
+ +
+ + def + request( req: pyscalpel.http.request.Request) -> pyscalpel.http.request.Request | None: + + + +
+ +
17def request(req: Request) -> Request | None:
+18    """- Intercept and rewrite a request.
+19
+20    - Args:
+21        - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The intercepted request
+22
+23    - Returns:
+24        - [Request](../pdoc/python3-10/pyscalpel.html#Request) or None: The modified request. Otherwise, None to ignore the request.
+25    """
+
+ + +
    +
  • Intercept and rewrite a request.

  • +
  • Args:

    + +
      +
    • req (Request): The intercepted request
    • +
  • +
  • Returns:

    + +
      +
    • Request or None: The modified request. Otherwise, None to ignore the request.
    • +
  • +
+
+ + +
+
+ +
+ + def + response( res: pyscalpel.http.response.Response) -> pyscalpel.http.response.Response | None: + + + +
+ +
28def response(res: Response) -> Response | None:
+29    """- Intercept and rewrite a response.
+30
+31    - Args:
+32        - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The intercepted response.
+33
+34    - Returns:
+35        - [Response](../pdoc/python3-10/pyscalpel.html#Response) or None: The modified response. Otherwise, None to ignore the response.
+36    """
+
+ + +
    +
  • Intercept and rewrite a response.

  • +
  • Args:

    + +
      +
    • res (Response): The intercepted response.
    • +
  • +
  • Returns:

    + +
      +
    • Response or None: The modified response. Otherwise, None to ignore the response.
    • +
  • +
+
+ + +
+
+ +
+ + def + req_edit_in(req: pyscalpel.http.request.Request) -> bytes | None: + + + +
+ +
39def req_edit_in(req: Request) -> bytes | None:
+40    """- Create or update a request editor's content from a request.
+41       - May be used to decode a request to plaintext.
+42
+43    - Args:
+44        - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The HTTP request.
+45
+46    - Returns:
+47        - bytes or None: The editor's contents.
+48    """
+
+ + +
    +
  • Create or update a request editor's content from a request.

    + +
      +
    • May be used to decode a request to plaintext.
    • +
  • +
  • Args:

    + +
      +
    • req (Request): The HTTP request.
    • +
  • +
  • Returns:

    + +
      +
    • bytes or None: The editor's contents.
    • +
  • +
+
+ + +
+
+ +
+ + def + req_edit_out( req: pyscalpel.http.request.Request, modified_content: bytes) -> pyscalpel.http.request.Request | None: + + + +
+ +
51def req_edit_out(req: Request, modified_content: bytes) -> Request | None:
+52    """- Update a request from an editor's modified content.
+53       - May be used to encode a request from plaintext (modified_content).
+54
+55    - Args:
+56        - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The original request.
+57        - modified_content (bytes): The editor's content.
+58
+59    - Returns:
+60        - [Request](../pdoc/python3-10/pyscalpel.html#Request) or None: The new request.
+61    """
+
+ + +
    +
  • Update a request from an editor's modified content.

    + +
      +
    • May be used to encode a request from plaintext (modified_content).
    • +
  • +
  • Args:

    + +
      +
    • req (Request): The original request.
    • +
    • modified_content (bytes): The editor's content.
    • +
  • +
  • Returns:

    + +
      +
    • Request or None: The new request.
    • +
  • +
+
+ + +
+
+ +
+ + def + res_edit_in(res: pyscalpel.http.response.Response) -> bytes | None: + + + +
+ +
64def res_edit_in(res: Response) -> bytes | None:
+65    """- Create or update a response editor's content from a response.
+66       - May be used to decode a response to plaintext.
+67
+68    - Args:
+69        - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The HTTP response.
+70
+71    - Returns:
+72        - bytes or None: The editor contents.
+73    """
+
+ + +
    +
  • Create or update a response editor's content from a response.

    + +
      +
    • May be used to decode a response to plaintext.
    • +
  • +
  • Args:

    + +
  • +
  • Returns:

    + +
      +
    • bytes or None: The editor contents.
    • +
  • +
+
+ + +
+
+ +
+ + def + res_edit_out( res: pyscalpel.http.response.Response, modified_content: bytes) -> pyscalpel.http.response.Response | None: + + + +
+ +
76def res_edit_out(res: Response, modified_content: bytes) -> Response | None:
+77    """- Update a response from an editor's modified content.
+78       - May be used to encode a response from plaintext (modified_content).
+79
+80    - Args:
+81        - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The original response.
+82        - modified_content (bytes): The editor's content.
+83
+84    - Returns:
+85        - [Response](../pdoc/python3-10/pyscalpel.html#Response) or None: The new response.
+86    """
+
+ + +
    +
  • Update a response from an editor's modified content.

    + +
      +
    • May be used to encode a response from plaintext (modified_content).
    • +
  • +
  • Args:

    + +
      +
    • res (Response): The original response.
    • +
    • modified_content (bytes): The editor's content.
    • +
  • +
  • Returns:

    + +
  • +
+
+ + +
+
+ +

#  âš ï¸ Good to know

+
    +
  • +

    If your hooks return None, they will follow these behaviors:

    +
      +
    • request() or response(): The original request is be forwarded without any modifications.
    • +
    • req_edit_in() or res_edit_in(): The editor tab is not displayed.
    • +
    • req_edit_out() or res_edit_out(): The request is not modified.
    • +
    +
  • +
  • +

    If req_edit_out() or res_edit_out() isn’t declared but req_edit_in() or res_edit_in() is, the corresponding editor will be read-only.

    +
  • +
  • +

    You do not have to declare every hook if you don’t need them, if you only want to modify requests, you can declare the request() hook only.

    +
  • +
+

#  Further reading

+

Check out the Custom Burp Editors section.

+ + +
+
+ + + diff --git a/docs/public/api/index.html b/docs/public/api/index.html new file mode 100644 index 00000000..845e3bae --- /dev/null +++ b/docs/public/api/index.html @@ -0,0 +1,88 @@ + + + + + + + + + Apis + + + + + + + + + + + + + +
+
+
+
+
+
+ +

0001

+ + +
+
+
+
+
+
+ + + diff --git a/docs/public/api/index.xml b/docs/public/api/index.xml new file mode 100644 index 00000000..5239ed80 --- /dev/null +++ b/docs/public/api/index.xml @@ -0,0 +1,105 @@ + + + + Apis on scalpel.org docs + /api/ + Recent content in Apis on scalpel.org docs + Hugo -- gohugo.io + en-us + + pyscalpel.edit + /api/pyscalpel/edit.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/edit.html + pyscalpel.edit Scalpel allows choosing between normal and binary editors, to do so, the user can apply the editor decorator to the req_edit_in / res_edit_int hook: +View Source 1""" 2 Scalpel allows choosing between normal and binary editors, 3 to do so, the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_int` hook: 4""" 5from typing import Callable, Literal, get_args 6 7EditorMode = Literal["raw", "hex", "octal", "binary", "decimal"] 8EDITOR_MODES: set[EditorMode] = set(get_args(EditorMode)) 9 10 11def editor(mode: EditorMode): 12 """Decorator to specify the editor type for a given hook 13 14 This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp 15 16 Example: 17 ```py 18 @editor("hex") 19 def req_edit_in(req: Request) -> bytes | None: 20 return bytes(req) 21 ``` 22 This displays the request in an hex editor. + + + + pyscalpel.encoding + /api/pyscalpel/encoding.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/encoding.html + pyscalpel.encoding Utilities for encoding data. +View Source 1""" 2 Utilities for encoding data. 3""" 4 5from urllib.parse import unquote_to_bytes as urllibdecode 6from _internal_mitmproxy.utils import strutils 7 8 9# str/bytes conversion helpers from mitmproxy/http.py: 10# https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/http.py#:~:text=def-,_native,-(x%3A 11def always_bytes(data: str | bytes | int, encoding="latin-1") -> bytes: 12 """Convert data to bytes 13 14 Args: 15 data (str | bytes | int): The data to convert 16 17 Returns: 18 bytes: The converted bytes 19 """ 20 if isinstance(data, int): 21 data = str(data) 22 return strutils. + + + + pyscalpel.events + /api/pyscalpel/events.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/events.html + pyscalpel.events Events that can be passed to the match() hook +View Source 1"""Events that can be passed to the match() hook""" 2 3from typing import Literal, get_args 4 5MatchEvent = Literal[ 6 "request", 7 "response", 8 "req_edit_in", 9 "req_edit_out", 10 "res_edit_in", 11 "res_edit_out", 12] 13 14 15MATCH_EVENTS: set[MatchEvent] = set(get_args(MatchEvent)) MatchEvent = typing.Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out'] MATCH_EVENTS: set[typing.Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out']] = {'request', 'res_edit_out', 'req_edit_out', 'response', 'req_edit_in', 'res_edit_in'} + + + + pyscalpel.http + /api/pyscalpel/http.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/http.html + pyscalpel.http This module contains objects representing HTTP objects passed to the user's hooks +View Source 1""" 2 This module contains objects representing HTTP objects passed to the user's hooks 3""" 4 5from .request import Request, Headers 6from .response import Response 7from .flow import Flow 8from .utils import match_patterns, host_is 9from . import body 10 11__all__ = [ 12 "body", # <- pdoc shows a warning for this declaration but won't display it when absent 13 "Request", 14 "Response", 15 "Headers", 16 "Flow", 17 "host_is", 18 "match_patterns", 19] class Request: View Source 70class Request: 71 """A "Burp oriented" HTTP request class 72 73 74 This class allows to manipulate Burp requests in a Pythonic way. + + + + pyscalpel.http.body + /api/pyscalpel/http/body.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/http/body.html + pyscalpel.http.body Pentesters often have to manipulate form data in precise and extensive ways +This module contains implementations for the most common forms (multipart,urlencoded, JSON) +Users may be implement their own form by creating a Serializer, assigning the .serializer attribute in Request and using the "form" property +Forms are designed to be convertible from one to another. +For example, JSON forms may be converted to URL encoded forms by using the php query string syntax: + + + + pyscalpel.java + /api/pyscalpel/java.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/java.html + pyscalpel.java This module declares type definitions used for Java objects. +If you are a normal user, you should probably never have to manipulate these objects yourself. +View Source 1""" 2 This module declares type definitions used for Java objects. 3 4 If you are a normal user, you should probably never have to manipulate these objects yourself. 5""" 6from .bytes import JavaBytes 7from .import_java import import_java 8from .object import JavaClass, JavaObject 9from . + + + + pyscalpel.java.burp + /api/pyscalpel/java/burp.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/java/burp.html + pyscalpel.java.burp This module exposes Java objects from Burp's extensions API +If you are a normal user, you should probably never have to manipulate these objects yourself. +View Source 1""" 2 This module exposes Java objects from Burp's extensions API 3 4 If you are a normal user, you should probably never have to manipulate these objects yourself. 5""" 6from .byte_array import IByteArray, ByteArray 7from .http_header import IHttpHeader, HttpHeader 8from . + + + + pyscalpel.utils + /api/pyscalpel/utils.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/utils.html + pyscalpel.utils View Source 1import inspect 2from typing import TypeVar, Union 3from pyscalpel.burp_utils import ( 4 urldecode, 5 urlencode_all, 6) 7 8 9T = TypeVar("T", str, bytes) 10 11 12def removeprefix(s: T, prefix: Union[str, bytes]) -> T: 13 if isinstance(s, str) and isinstance(prefix, str): 14 if s.startswith(prefix): 15 return s[len(prefix) :] # type: ignore 16 elif isinstance(s, bytes) and isinstance(prefix, bytes): 17 if s.startswith(prefix): 18 return s[len(prefix) :] # type: ignore 19 return s 20 21 22def removesuffix(s: T, suffix: Union[str, bytes]) -> T: 23 if isinstance(s, str) and isinstance(suffix, str): 24 if s. + + + + pyscalpel.venv + /api/pyscalpel/venv.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/venv.html + pyscalpel.venv This module provides reimplementations of Python virtual environnements scripts +This is designed to be used internally, but in the case where the user desires to dynamically switch venvs using this, they should ensure the selected venv has the dependencies required by Scalpel. +View Source 1""" 2This module provides reimplementations of Python virtual environnements scripts 3 4This is designed to be used internally, 5but in the case where the user desires to dynamically switch venvs using this, 6they should ensure the selected venv has the dependencies required by Scalpel. + + + + diff --git a/docs/public/api/pyscalpel/edit.html b/docs/public/api/pyscalpel/edit.html new file mode 100644 index 00000000..32a39c93 --- /dev/null +++ b/docs/public/api/pyscalpel/edit.html @@ -0,0 +1,576 @@ + + + + + + + + + pyscalpel.edit + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.edit

+ +

Scalpel allows choosing between normal and binary editors, +to do so, the user can apply the editor decorator to the req_edit_in / res_edit_int hook:

+
+ + + + + +
 1"""
+ 2    Scalpel allows choosing between normal and binary editors,
+ 3    to do so, the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_int` hook:
+ 4"""
+ 5from typing import Callable, Literal, get_args
+ 6
+ 7EditorMode = Literal["raw", "hex", "octal", "binary", "decimal"]
+ 8EDITOR_MODES: set[EditorMode] = set(get_args(EditorMode))
+ 9
+10
+11def editor(mode: EditorMode):
+12    """Decorator to specify the editor type for a given hook
+13
+14    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp
+15
+16    Example:
+17    ```py
+18        @editor("hex")
+19        def req_edit_in(req: Request) -> bytes | None:
+20            return bytes(req)
+21    ```
+22    This displays the request in an hex editor.
+23
+24    Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`.
+25
+26
+27    Args:
+28        mode (EDITOR_MODE): The editor mode (raw, hex,...)
+29    """
+30
+31    if mode not in EDITOR_MODES:
+32        raise ValueError(f"Argument must be one of {EDITOR_MODES}")
+33
+34    def decorator(hook: Callable):
+35        hook.__annotations__["scalpel_editor_mode"] = mode
+36        return hook
+37
+38    return decorator
+
+ + +
+
+
+ EditorMode = +typing.Literal['raw', 'hex', 'octal', 'binary', 'decimal'] + + +
+ + + + +
+
+
+ EDITOR_MODES: set[typing.Literal['raw', 'hex', 'octal', 'binary', 'decimal']] = +{'raw', 'octal', 'binary', 'decimal', 'hex'} + + +
+ + + + +
+
+ +
+ + def + editor(mode: Literal['raw', 'hex', 'octal', 'binary', 'decimal']): + + + +
+ +
12def editor(mode: EditorMode):
+13    """Decorator to specify the editor type for a given hook
+14
+15    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp
+16
+17    Example:
+18    ```py
+19        @editor("hex")
+20        def req_edit_in(req: Request) -> bytes | None:
+21            return bytes(req)
+22    ```
+23    This displays the request in an hex editor.
+24
+25    Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`.
+26
+27
+28    Args:
+29        mode (EDITOR_MODE): The editor mode (raw, hex,...)
+30    """
+31
+32    if mode not in EDITOR_MODES:
+33        raise ValueError(f"Argument must be one of {EDITOR_MODES}")
+34
+35    def decorator(hook: Callable):
+36        hook.__annotations__["scalpel_editor_mode"] = mode
+37        return hook
+38
+39    return decorator
+
+ + +

Decorator to specify the editor type for a given hook

+ +

This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp

+ +

Example:

+ +
+
    @editor("hex")
+    def req_edit_in(req: Request) -> bytes | None:
+        return bytes(req)
+
+
+ +

This displays the request in an hex editor.

+ +

Currently, the only modes supported are "raw", "hex", "octal", "binary" and "decimal".

+ +

Args: + mode (EDITOR_MODE): The editor mode (raw, hex,...)

+
+ + +
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/encoding.html b/docs/public/api/pyscalpel/encoding.html new file mode 100644 index 00000000..492ae90e --- /dev/null +++ b/docs/public/api/pyscalpel/encoding.html @@ -0,0 +1,613 @@ + + + + + + + + + pyscalpel.encoding + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.encoding

+ +

Utilities for encoding data.

+
+ + + + + +
 1"""
+ 2    Utilities for encoding data.
+ 3"""
+ 4
+ 5from urllib.parse import unquote_to_bytes as urllibdecode
+ 6from _internal_mitmproxy.utils import strutils
+ 7
+ 8
+ 9# str/bytes conversion helpers from mitmproxy/http.py:
+10# https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/http.py#:~:text=def-,_native,-(x%3A
+11def always_bytes(data: str | bytes | int, encoding="latin-1") -> bytes:
+12    """Convert data to bytes
+13
+14    Args:
+15        data (str | bytes | int): The data to convert
+16
+17    Returns:
+18        bytes: The converted bytes
+19    """
+20    if isinstance(data, int):
+21        data = str(data)
+22    return strutils.always_bytes(data, encoding, "surrogateescape")
+23
+24
+25def always_str(data: str | bytes | int, encoding="latin-1") -> str:
+26    """Convert data to string
+27
+28    Args:
+29        data (str | bytes | int): The data to convert
+30
+31    Returns:
+32        str: The converted string
+33    """
+34    if isinstance(data, int):
+35        return str(data)
+36    return strutils.always_str(data, encoding, "surrogateescape")
+37
+38
+39
+40def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes:
+41    """URL Encode all bytes in the given bytes object"""
+42    return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding)
+43
+44
+45def urldecode(data: bytes | str, encoding="latin-1") -> bytes:
+46    """URL Decode all bytes in the given bytes object"""
+47    return urllibdecode(always_bytes(data, encoding))
+
+ + +
+
+ +
+ + def + always_bytes(data: str | bytes | int, encoding='latin-1') -> bytes: + + + +
+ +
12def always_bytes(data: str | bytes | int, encoding="latin-1") -> bytes:
+13    """Convert data to bytes
+14
+15    Args:
+16        data (str | bytes | int): The data to convert
+17
+18    Returns:
+19        bytes: The converted bytes
+20    """
+21    if isinstance(data, int):
+22        data = str(data)
+23    return strutils.always_bytes(data, encoding, "surrogateescape")
+
+ + +

Convert data to bytes

+ +

Args: + data (str | bytes | int): The data to convert

+ +

Returns: + bytes: The converted bytes

+
+ + +
+
+ +
+ + def + always_str(data: str | bytes | int, encoding='latin-1') -> str: + + + +
+ +
26def always_str(data: str | bytes | int, encoding="latin-1") -> str:
+27    """Convert data to string
+28
+29    Args:
+30        data (str | bytes | int): The data to convert
+31
+32    Returns:
+33        str: The converted string
+34    """
+35    if isinstance(data, int):
+36        return str(data)
+37    return strutils.always_str(data, encoding, "surrogateescape")
+
+ + +

Convert data to string

+ +

Args: + data (str | bytes | int): The data to convert

+ +

Returns: + str: The converted string

+
+ + +
+
+ +
+ + def + urlencode_all(data: bytes | str, encoding='latin-1') -> bytes: + + + +
+ +
41def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes:
+42    """URL Encode all bytes in the given bytes object"""
+43    return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding)
+
+ + +

URL Encode all bytes in the given bytes object

+
+ + +
+
+ +
+ + def + urldecode(data: bytes | str, encoding='latin-1') -> bytes: + + + +
+ +
46def urldecode(data: bytes | str, encoding="latin-1") -> bytes:
+47    """URL Decode all bytes in the given bytes object"""
+48    return urllibdecode(always_bytes(data, encoding))
+
+ + +

URL Decode all bytes in the given bytes object

+
+ + +
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/events.html b/docs/public/api/pyscalpel/events.html new file mode 100644 index 00000000..5e24f05d --- /dev/null +++ b/docs/public/api/pyscalpel/events.html @@ -0,0 +1,487 @@ + + + + + + + + + pyscalpel.events + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.events

+ +

Events that can be passed to the match() hook

+
+ + + + + +
 1"""Events that can be passed to the match() hook"""
+ 2
+ 3from typing import Literal, get_args
+ 4
+ 5MatchEvent = Literal[
+ 6    "request",
+ 7    "response",
+ 8    "req_edit_in",
+ 9    "req_edit_out",
+10    "res_edit_in",
+11    "res_edit_out",
+12]
+13
+14
+15MATCH_EVENTS: set[MatchEvent] = set(get_args(MatchEvent))
+
+ + +
+
+
+ MatchEvent = +typing.Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out'] + + +
+ + + + +
+
+
+ MATCH_EVENTS: set[typing.Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out']] = +{'request', 'res_edit_out', 'req_edit_out', 'response', 'req_edit_in', 'res_edit_in'} + + +
+ + + + +
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/http.html b/docs/public/api/pyscalpel/http.html new file mode 100644 index 00000000..3be8be4a --- /dev/null +++ b/docs/public/api/pyscalpel/http.html @@ -0,0 +1,3715 @@ + + + + + + + + + pyscalpel.http + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.http

+ +

This module contains objects representing HTTP objects passed to the user's hooks

+
+ + + + + +
 1"""
+ 2    This module contains objects representing HTTP objects passed to the user's hooks
+ 3"""
+ 4
+ 5from .request import Request, Headers
+ 6from .response import Response
+ 7from .flow import Flow
+ 8from .utils import match_patterns, host_is
+ 9from . import body
+10
+11__all__ = [
+12    "body",  # <- pdoc shows a warning for this declaration but won't display it when absent
+13    "Request",
+14    "Response",
+15    "Headers",
+16    "Flow",
+17    "host_is",
+18    "match_patterns",
+19]
+
+ + +
+
+ +
+ + class + Request: + + + +
+ +
 70class Request:
+ 71    """A "Burp oriented" HTTP request class
+ 72
+ 73
+ 74    This class allows to manipulate Burp requests in a Pythonic way.
+ 75    """
+ 76
+ 77    _Port = int
+ 78    _QueryParam = tuple[str, str]
+ 79    _ParsedQuery = tuple[_QueryParam, ...]
+ 80    _HttpVersion = str
+ 81    _HeaderKey = str
+ 82    _HeaderValue = str
+ 83    _Header = tuple[_HeaderKey, _HeaderValue]
+ 84    _Host = str
+ 85    _Method = str
+ 86    _Scheme = Literal["http", "https"]
+ 87    _Authority = str
+ 88    _Content = bytes
+ 89    _Path = str
+ 90
+ 91    host: _Host
+ 92    port: _Port
+ 93    method: _Method
+ 94    scheme: _Scheme
+ 95    authority: _Authority
+ 96
+ 97    # Path also includes URI parameters (;), query (?) and fragment (#)
+ 98    # Simply because it is more conveninent to manipulate that way in a pentensting context
+ 99    # It also mimics the way mitmproxy works.
+100    path: _Path
+101
+102    http_version: _HttpVersion
+103    _headers: Headers
+104    _serializer: FormSerializer | None = None
+105    _deserialized_content: Any = None
+106    _content: _Content | None = None
+107    _old_deserialized_content: Any = None
+108    _is_form_initialized: bool = False
+109    update_content_length: bool = True
+110
+111    def __init__(
+112        self,
+113        method: str,
+114        scheme: Literal["http", "https"],
+115        host: str,
+116        port: int,
+117        path: str,
+118        http_version: str,
+119        headers: (
+120            Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]]
+121        ),
+122        authority: str,
+123        content: bytes | None,
+124    ):
+125        self.scheme = scheme
+126        self.host = host
+127        self.port = port
+128        self.path = path
+129        self.method = method
+130        self.authority = authority
+131        self.http_version = http_version
+132        self.headers = headers if isinstance(headers, Headers) else Headers(headers)
+133        self._content = content
+134
+135        # Initialize the serializer (json,urlencoded,multipart)
+136        self.update_serializer_from_content_type(
+137            self.headers.get("Content-Type"), fail_silently=True
+138        )
+139
+140        # Initialize old deserialized content to avoid modifying content if it has not been modified
+141        # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py)
+142        self._old_deserialized_content = deepcopy(self._deserialized_content)
+143
+144    def _del_header(self, header: str) -> bool:
+145        if header in self._headers.keys():
+146            del self._headers[header]
+147            return True
+148
+149        return False
+150
+151    def _update_content_length(self) -> None:
+152        if self.update_content_length:
+153            if self._content is None:
+154                self._del_header("Content-Length")
+155            else:
+156                length = len(cast(bytes, self._content))
+157                self._headers["Content-Length"] = str(length)
+158
+159    @staticmethod
+160    def _parse_qs(query_string: str) -> _ParsedQuery:
+161        return tuple(urllib.parse.parse_qsl(query_string))
+162
+163    @staticmethod
+164    def _parse_url(
+165        url: str,
+166    ) -> tuple[_Scheme, _Host, _Port, _Path]:
+167        scheme, host, port, path = url_parse(url)
+168
+169        # This method is only used to create HTTP requests from URLs
+170        #   so we can ensure the scheme is valid for this usage
+171        if scheme not in (b"http", b"https"):
+172            scheme = b"http"
+173
+174        return cast(
+175            tuple[Literal["http", "https"], str, int, str],
+176            (scheme.decode("ascii"), host.decode("idna"), port, path.decode("ascii")),
+177        )
+178
+179    @staticmethod
+180    def _unparse_url(scheme: _Scheme, host: _Host, port: _Port, path: _Path) -> str:
+181        return url_unparse(scheme, host, port, path)
+182
+183    @classmethod
+184    def make(
+185        cls,
+186        method: str,
+187        url: str,
+188        content: bytes | str = "",
+189        headers: (
+190            Headers
+191            | dict[str | bytes, str | bytes]
+192            | dict[str, str]
+193            | dict[bytes, bytes]
+194            | Iterable[tuple[bytes, bytes]]
+195        ) = (),
+196    ) -> Request:
+197        """Create a request from an URL
+198
+199        Args:
+200            method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)
+201            url (str): The request URL
+202            content (bytes | str, optional): The request content. Defaults to "".
+203            headers (Headers, optional): The request headers. Defaults to ().
+204
+205        Returns:
+206            Request: The HTTP request
+207        """
+208        scalpel_headers: Headers
+209        match headers:
+210            case Headers():
+211                scalpel_headers = headers
+212            case dict():
+213                casted_headers = cast(dict[str | bytes, str | bytes], headers)
+214                scalpel_headers = Headers(
+215                    (
+216                        (always_bytes(key), always_bytes(val))
+217                        for key, val in casted_headers.items()
+218                    )
+219                )
+220            case _:
+221                scalpel_headers = Headers(headers)
+222
+223        scheme, host, port, path = Request._parse_url(url)
+224        http_version = "HTTP/1.1"
+225
+226        # Inferr missing Host header from URL
+227        host_header = scalpel_headers.get("Host")
+228        if host_header is None:
+229            match (scheme, port):
+230                case ("http", 80) | ("https", 443):
+231                    host_header = host
+232                case _:
+233                    host_header = f"{host}:{port}"
+234
+235            scalpel_headers["Host"] = host_header
+236
+237        authority: str = host_header
+238        encoded_content = always_bytes(content)
+239
+240        assert isinstance(host, str)
+241
+242        return cls(
+243            method=method,
+244            scheme=scheme,
+245            host=host,
+246            port=port,
+247            path=path,
+248            http_version=http_version,
+249            headers=scalpel_headers,
+250            authority=authority,
+251            content=encoded_content,
+252        )
+253
+254    @classmethod
+255    def from_burp(
+256        cls, request: IHttpRequest, service: IHttpService | None = None
+257    ) -> Request:  # pragma: no cover (uses Java API)
+258        """Construct an instance of the Request class from a Burp suite HttpRequest.
+259        :param request: The Burp suite HttpRequest to convert.
+260        :return: A Request with the same data as the Burp suite HttpRequest.
+261        """
+262        service = service or request.httpService()
+263        body = get_bytes(request.body())
+264
+265        # Burp will give you lowercased and pseudo headers when using HTTP/2.
+266        # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-.
+267        # https://blog.yaakov.online/http-2-header-casing/
+268        headers: Headers = Headers.from_burp(request.headers())
+269
+270        # Burp gives a 0 length byte array body even when it doesn't exist, instead of null.
+271        # Empty but existing bodies without a Content-Length header are lost in the process.
+272        if not body and not headers.get("Content-Length"):
+273            body = None
+274
+275        # request.url() gives a relative url for some reason
+276        # So we have to parse and unparse to get the full path
+277        #   (path + parameters + query + fragment)
+278        _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url())
+279
+280        # Concatenate the path components
+281        # Empty parameters,query and fragment are lost in the process
+282        # e.g.: http://example.com;?# becomes http://example.com
+283        # To use such an URL, the user must set the path directly
+284        # To fix this we would need to write our own URL parser, which is a bit overkill for now.
+285        path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment))
+286
+287        host = ""
+288        port = 0
+289        scheme = "http"
+290        if service:
+291            host = service.host()
+292            port = service.port()
+293            scheme = "https" if service.secure() else "http"
+294
+295        return cls(
+296            method=request.method(),
+297            scheme=scheme,
+298            host=host,
+299            port=port,
+300            path=path,
+301            http_version=request.httpVersion() or "HTTP/1.1",
+302            headers=headers,
+303            authority=headers.get(":authority") or headers.get("Host") or "",
+304            content=body,
+305        )
+306
+307    def __bytes__(self) -> bytes:
+308        """Convert the request to bytes
+309        :return: The request as bytes.
+310        """
+311        # Reserialize the request to bytes.
+312        first_line = (
+313            b" ".join(
+314                always_bytes(s) for s in (self.method, self.path, self.http_version)
+315            )
+316            + b"\r\n"
+317        )
+318
+319        # Strip HTTP/2 pseudo headers.
+320        # https://portswigger.net/burp/documentation/desktop/http2/http2-basics-for-burp-users#:~:text=HTTP/2%20specification.-,Pseudo%2Dheaders,-In%20HTTP/2
+321        mapped_headers = tuple(
+322            field for field in self.headers.fields if not field[0].startswith(b":")
+323        )
+324
+325        if self.headers.get(b"Host") is None and self.http_version == "HTTP/2":
+326            # Host header is not present in HTTP/2, but is required by Burp message editor.
+327            # So we have to add it back from the :authority pseudo-header.
+328            # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=pseudo%2Dheaders%20and-,derives,-the%20%3Aauthority%20from
+329            mapped_headers = (
+330                (b"Host", always_bytes(self.headers[":authority"])),
+331            ) + tuple(mapped_headers)
+332
+333        # Construct the request's headers part.
+334        headers_lines = b"".join(
+335            b"%s: %s\r\n" % (key, val) for key, val in mapped_headers
+336        )
+337
+338        # Set a default value for the request's body. (None -> b"")
+339        body = self.content or b""
+340
+341        # Construct the whole request and return it.
+342        return first_line + headers_lines + b"\r\n" + body
+343
+344    def to_burp(self) -> IHttpRequest:  # pragma: no cover
+345        """Convert the request to a Burp suite :class:`IHttpRequest`.
+346        :return: The request as a Burp suite :class:`IHttpRequest`.
+347        """
+348        # Convert the request to a Burp ByteArray.
+349        request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
+350
+351        if self.port == 0:
+352            # No networking information is available, so we build a plain network-less request.
+353            return HttpRequest.httpRequest(request_byte_array)
+354
+355        # Build the Burp HTTP networking service.
+356        service: IHttpService = HttpService.httpService(
+357            self.host, self.port, self.scheme == "https"
+358        )
+359
+360        # Instantiate and return a new Burp HTTP request.
+361        return HttpRequest.httpRequest(service, request_byte_array)
+362
+363    @classmethod
+364    def from_raw(
+365        cls,
+366        data: bytes | str,
+367        real_host: str = "",
+368        port: int = 0,
+369        scheme: Literal["http"] | Literal["https"] | str = "http",
+370    ) -> Request:  # pragma: no cover
+371        """Construct an instance of the Request class from raw bytes.
+372        :param data: The raw bytes to convert.
+373        :param real_host: The real host to connect to.
+374        :param port: The port of the request.
+375        :param scheme: The scheme of the request.
+376        :return: A :class:`Request` with the same data as the raw bytes.
+377        """
+378        # Convert the raw bytes to a Burp ByteArray.
+379        # We use the Burp API to trivialize the parsing of the request from raw bytes.
+380        str_or_byte_array: IByteArray | str = (
+381            data if isinstance(data, str) else PythonUtils.toByteArray(data)
+382        )
+383
+384        # Handle the case where the networking informations are not provided.
+385        if port == 0:
+386            # Instantiate and return a new Burp HTTP request without networking informations.
+387            burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array)
+388        else:
+389            # Build the Burp HTTP networking service.
+390            service: IHttpService = HttpService.httpService(
+391                real_host, port, scheme == "https"
+392            )
+393
+394            # Instantiate a new Burp HTTP request with networking informations.
+395            burp_request: IHttpRequest = HttpRequest.httpRequest(
+396                service, str_or_byte_array
+397            )
+398
+399        # Construct the request from the Burp.
+400        return cls.from_burp(burp_request)
+401
+402    @property
+403    def url(self) -> str:
+404        """
+405        The full URL string, constructed from `Request.scheme`,
+406            `Request.host`, `Request.port` and `Request.path`.
+407
+408        Setting this property updates these attributes as well.
+409        """
+410        return Request._unparse_url(self.scheme, self.host, self.port, self.path)
+411
+412    @url.setter
+413    def url(self, val: str | bytes) -> None:
+414        (self.scheme, self.host, self.port, self.path) = Request._parse_url(
+415            always_str(val)
+416        )
+417
+418    def _get_query(self) -> _ParsedQuery:
+419        query = urllib.parse.urlparse(self.url).query
+420        return tuple(url_decode(query))
+421
+422    def _set_query(self, query_data: Sequence[_QueryParam]):
+423        query = url_encode(query_data)
+424        _, _, path, params, _, fragment = urllib.parse.urlparse(self.url)
+425        self.path = urllib.parse.urlunparse(["", "", path, params, query, fragment])
+426
+427    @property
+428    def query(self) -> URLEncodedFormView:
+429        """The query string parameters as a dict-like object
+430
+431        Returns:
+432            QueryParamsView: The query string parameters
+433        """
+434        return URLEncodedFormView(
+435            multidict.MultiDictView(self._get_query, self._set_query)
+436        )
+437
+438    @query.setter
+439    def query(self, value: Sequence[tuple[str, str]]):
+440        self._set_query(value)
+441
+442    def _has_deserialized_content_changed(self) -> bool:
+443        return self._deserialized_content != self._old_deserialized_content
+444
+445    def _serialize_content(self):
+446        if self._serializer is None:
+447            return
+448
+449        if self._deserialized_content is None:
+450            self._content = None
+451            return
+452
+453        self._update_serialized_content(
+454            self._serializer.serialize(self._deserialized_content, req=self)
+455        )
+456
+457    def _update_serialized_content(self, serialized: bytes):
+458        if self._serializer is None:
+459            self._content = serialized
+460            return
+461
+462        # Update the parsed form
+463        self._deserialized_content = self._serializer.deserialize(serialized, self)
+464        self._old_deserialized_content = deepcopy(self._deserialized_content)
+465
+466        # Set the raw content directly
+467        self._content = serialized
+468
+469    def _deserialize_content(self):
+470        if self._serializer is None:
+471            return
+472
+473        if self._content:
+474            self._deserialized_content = self._serializer.deserialize(
+475                self._content, req=self
+476            )
+477
+478    def _update_deserialized_content(self, deserialized: Any):
+479        if self._serializer is None:
+480            return
+481
+482        if deserialized is None:
+483            self._deserialized_content = None
+484            self._old_deserialized_content = None
+485            return
+486
+487        self._deserialized_content = deserialized
+488        self._content = self._serializer.serialize(deserialized, self)
+489        self._update_content_length()
+490
+491    @property
+492    def content(self) -> bytes | None:
+493        """The request content / body as raw bytes
+494
+495        Returns:
+496            bytes | None: The content if it exists
+497        """
+498        if self._serializer and self._has_deserialized_content_changed():
+499            self._update_deserialized_content(self._deserialized_content)
+500            self._old_deserialized_content = deepcopy(self._deserialized_content)
+501
+502        self._update_content_length()
+503
+504        return self._content
+505
+506    @content.setter
+507    def content(self, value: bytes | str | None):
+508        match value:
+509            case None:
+510                self._content = None
+511                self._deserialized_content = None
+512                return
+513            case str():
+514                value = value.encode("latin-1")
+515
+516        self._update_content_length()
+517
+518        self._update_serialized_content(value)
+519
+520    @property
+521    def body(self) -> bytes | None:
+522        """Alias for content()
+523
+524        Returns:
+525            bytes | None: The request body / content
+526        """
+527        return self.content
+528
+529    @body.setter
+530    def body(self, value: bytes | str | None):
+531        self.content = value
+532
+533    def update_serializer_from_content_type(
+534        self,
+535        content_type: ImplementedContentType | str | None = None,
+536        fail_silently: bool = False,
+537    ):
+538        """Update the form parsing based on the given Content-Type
+539
+540        Args:
+541            content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.
+542            fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.
+543
+544        Raises:
+545            FormNotParsedException: Raised when the content-type is unknown.
+546        """
+547        # Strip the boundary param so we can use our content-type to serializer map
+548        _content_type: str = get_header_value_without_params(
+549            content_type or self.headers.get("Content-Type") or ""
+550        )
+551
+552        serializer = None
+553        if _content_type in IMPLEMENTED_CONTENT_TYPES:
+554            serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type)
+555
+556        if serializer is None:
+557            if fail_silently:
+558                serializer = self._serializer
+559            else:
+560                raise FormNotParsedException(
+561                    f"Unimplemented form content-type: {_content_type}"
+562                )
+563        self._set_serializer(serializer)
+564
+565    @property
+566    def content_type(self) -> str | None:
+567        """The Content-Type header value.
+568
+569        Returns:
+570            str | None: <=> self.headers.get("Content-Type")
+571        """
+572        return self.headers.get("Content-Type")
+573
+574    @content_type.setter
+575    def content_type(self, value: str) -> str | None:
+576        self.headers["Content-Type"] = value
+577
+578    def create_defaultform(
+579        self,
+580        content_type: ImplementedContentType | str | None = None,
+581        update_header: bool = True,
+582    ) -> MutableMapping[Any, Any]:
+583        """Creates the form if it doesn't exist, else returns the existing one
+584
+585        Args:
+586            content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.
+587            update_header (bool, optional): Whether to update the header. Defaults to True.
+588
+589        Raises:
+590            FormNotParsedException: Thrown when provided content-type has no implemented form-serializer
+591            FormNotParsedException: Thrown when the raw content could not be parsed.
+592
+593        Returns:
+594            MutableMapping[Any, Any]: The mapped form.
+595        """
+596        if not self._is_form_initialized or content_type:
+597            self.update_serializer_from_content_type(content_type)
+598
+599            # Set content-type if it does not exist
+600            if (content_type and update_header) or not self.headers.get_all(
+601                "Content-Type"
+602            ):
+603                self.headers["Content-Type"] = content_type
+604
+605        serializer = self._serializer
+606        if serializer is None:
+607            # This should probably never trigger here as it should already be raised by update_serializer_from_content_type
+608            raise FormNotParsedException(
+609                f"Form of content-type {self.content_type} not implemented."
+610            )
+611
+612        # Create default form.
+613        if not self.content:
+614            self._deserialized_content = serializer.get_empty_form(self)
+615        elif self._deserialized_content is None:
+616            self._deserialize_content()
+617
+618        if self._deserialized_content is None:
+619            raise FormNotParsedException(
+620                f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}"
+621            )
+622
+623        if not isinstance(self._deserialized_content, serializer.deserialized_type()):
+624            self._deserialized_content = serializer.get_empty_form(self)
+625
+626        self._is_form_initialized = True
+627        return self._deserialized_content
+628
+629    @property
+630    def form(self) -> MutableMapping[Any, Any]:
+631        """Mapping from content parsed accordingly to Content-Type
+632
+633        Raises:
+634            FormNotParsedException: The content could not be parsed accordingly to Content-Type
+635
+636        Returns:
+637            MutableMapping[Any, Any]: The mapped request form
+638        """
+639        if not self._is_form_initialized:
+640            self.update_serializer_from_content_type()
+641
+642        self.create_defaultform()
+643        if self._deserialized_content is None:
+644            raise FormNotParsedException()
+645
+646        self._is_form_initialized = True
+647        return self._deserialized_content
+648
+649    @form.setter
+650    def form(self, form: MutableMapping[Any, Any]):
+651        if not self._is_form_initialized:
+652            self.update_serializer_from_content_type()
+653            self._is_form_initialized = True
+654
+655        self._deserialized_content = form
+656
+657        # Update raw _content
+658        self._serialize_content()
+659
+660    def _set_serializer(self, serializer: FormSerializer | None):
+661        # Update the serializer
+662        old_serializer = self._serializer
+663        self._serializer = serializer
+664
+665        if serializer is None:
+666            self._deserialized_content = None
+667            return
+668
+669        if type(serializer) == type(old_serializer):
+670            return
+671
+672        if old_serializer is None:
+673            self._deserialize_content()
+674            return
+675
+676        old_form = self._deserialized_content
+677
+678        if old_form is None:
+679            self._deserialize_content()
+680            return
+681
+682        # Convert the form to an intermediate format for easier conversion
+683        exported_form = old_serializer.export_form(old_form)
+684
+685        # Parse the intermediate data to the new serializer format
+686        imported_form = serializer.import_form(exported_form, self)
+687        self._deserialized_content = imported_form
+688
+689    def _update_serializer_and_get_form(
+690        self, serializer: FormSerializer
+691    ) -> MutableMapping[Any, Any] | None:
+692        # Set the serializer and update the content
+693        self._set_serializer(serializer)
+694
+695        # Return the new form
+696        return self._deserialized_content
+697
+698    def _update_serializer_and_set_form(
+699        self, serializer: FormSerializer, form: MutableMapping[Any, Any]
+700    ) -> None:
+701        # NOOP when the serializer is the same
+702        self._set_serializer(serializer)
+703
+704        self._update_deserialized_content(form)
+705
+706    @property
+707    def urlencoded_form(self) -> URLEncodedForm:
+708        """The urlencoded form data
+709
+710        Converts the content to the urlencoded form format if needed.
+711        Modification to this object will update Request.content and vice versa
+712
+713        Returns:
+714            QueryParams: The urlencoded form data
+715        """
+716        self._is_form_initialized = True
+717        return cast(
+718            URLEncodedForm,
+719            self._update_serializer_and_get_form(URLEncodedFormSerializer()),
+720        )
+721
+722    @urlencoded_form.setter
+723    def urlencoded_form(self, form: URLEncodedForm):
+724        self._is_form_initialized = True
+725        self._update_serializer_and_set_form(URLEncodedFormSerializer(), form)
+726
+727    @property
+728    def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]:
+729        """The JSON form data
+730
+731        Converts the content to the JSON form format if needed.
+732        Modification to this object will update Request.content and vice versa
+733
+734        Returns:
+735          dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data
+736        """
+737        self._is_form_initialized = True
+738        if self._update_serializer_and_get_form(JSONFormSerializer()) is None:
+739            serializer = cast(JSONFormSerializer, self._serializer)
+740            self._deserialized_content = serializer.get_empty_form(self)
+741
+742        return self._deserialized_content
+743
+744    @json_form.setter
+745    def json_form(self, form: dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]):
+746        self._is_form_initialized = True
+747        self._update_serializer_and_set_form(JSONFormSerializer(), JSONForm(form))
+748
+749    def _ensure_multipart_content_type(self) -> str:
+750        content_types_headers = self.headers.get_all("Content-Type")
+751        pattern = re.compile(
+752            r"^multipart/form-data;\s*boundary=([^;\s]+)", re.IGNORECASE
+753        )
+754
+755        # Find a valid multipart content-type header with a valid boundary
+756        matched_content_type: str | None = None
+757        for content_type in content_types_headers:
+758            if pattern.match(content_type):
+759                matched_content_type = content_type
+760                break
+761
+762        # If no boundary was found, overwrite the Content-Type header
+763        # If an user wants to avoid this behaviour,they should manually create a MultiPartForm(), convert it to bytes
+764        #   and pass it as raw_form()
+765        if matched_content_type is None:
+766            # TODO: Randomly generate this? The boundary could be used to fingerprint Scalpel
+767            new_content_type = (
+768                "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI"
+769            )
+770            self.headers["Content-Type"] = new_content_type
+771            return new_content_type
+772
+773        return matched_content_type
+774
+775    @property
+776    def multipart_form(self) -> MultiPartForm:
+777        """The multipart form data
+778
+779        Converts the content to the multipart form format if needed.
+780        Modification to this object will update Request.content and vice versa
+781
+782        Returns:
+783            MultiPartForm
+784        """
+785        self._is_form_initialized = True
+786
+787        # Keep boundary even if content-type has changed
+788        if isinstance(self._deserialized_content, MultiPartForm):
+789            return self._deserialized_content
+790
+791        # We do not have an existing form, so we have to ensure we have a content-type header with a boundary
+792        self._ensure_multipart_content_type()
+793
+794        # Serialize the current form and try to parse it with the new serializer
+795        form = self._update_serializer_and_get_form(MultiPartFormSerializer())
+796        serializer = cast(MultiPartFormSerializer, self._serializer)
+797
+798        # Set a default value
+799        if not form:
+800            self._deserialized_content = serializer.get_empty_form(self)
+801
+802        # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary
+803        if self._deserialized_content is None:
+804            raise FormNotParsedException(
+805                f"Could not parse content to {serializer.deserialized_type()}"
+806            )
+807
+808        return self._deserialized_content
+809
+810    @multipart_form.setter
+811    def multipart_form(self, form: MultiPartForm):
+812        self._is_form_initialized = True
+813        if not isinstance(self._deserialized_content, MultiPartForm):
+814            # Generate a multipart header because we don't have any boundary to format the multipart.
+815            self._ensure_multipart_content_type()
+816
+817        return self._update_serializer_and_set_form(
+818            MultiPartFormSerializer(), cast(MutableMapping, form)
+819        )
+820
+821    @property
+822    def cookies(self) -> multidict.MultiDictView[str, str]:
+823        """
+824        The request cookies.
+825        For the most part, this behaves like a dictionary.
+826        Modifications to the MultiDictView update `Request.headers`, and vice versa.
+827        """
+828        return multidict.MultiDictView(self._get_cookies, self._set_cookies)
+829
+830    def _get_cookies(self) -> tuple[tuple[str, str], ...]:
+831        header = self.headers.get_all("Cookie")
+832        return tuple(cookies.parse_cookie_headers(header))
+833
+834    def _set_cookies(self, value: tuple[tuple[str, str], ...]):
+835        self.headers["cookie"] = cookies.format_cookie_header(value)
+836
+837    @cookies.setter
+838    def cookies(self, value: tuple[tuple[str, str], ...] | Mapping[str, str]):
+839        if hasattr(value, "items") and callable(getattr(value, "items")):
+840            value = tuple(cast(Mapping[str, str], value).items())
+841        self._set_cookies(cast(tuple[tuple[str, str], ...], value))
+842
+843    @property
+844    def host_header(self) -> str | None:
+845        """Host header value
+846
+847        Returns:
+848            str | None: The host header value
+849        """
+850        return self.headers.get("Host")
+851
+852    @host_header.setter
+853    def host_header(self, value: str | None):
+854        self.headers["Host"] = value
+855
+856    def text(self, encoding="utf-8") -> str:
+857        """The decoded content
+858
+859        Args:
+860            encoding (str, optional): encoding to use. Defaults to "utf-8".
+861
+862        Returns:
+863            str: The decoded content
+864        """
+865        if self.content is None:
+866            return ""
+867
+868        return self.content.decode(encoding)
+869
+870    @property
+871    def headers(self) -> Headers:
+872        """The request HTTP headers
+873
+874        Returns:
+875            Headers: a case insensitive dict containing the HTTP headers
+876        """
+877        self._update_content_length()
+878        return self._headers
+879
+880    @headers.setter
+881    def headers(self, value: Headers):
+882        self._headers = value
+883        self._update_content_length()
+884
+885    @property
+886    def content_length(self) -> int:
+887        """Returns the Content-Length header value
+888           Returns 0 if the header is absent
+889
+890        Args:
+891            value (int | str): The Content-Length value
+892
+893        Raises:
+894            RuntimeError: Throws RuntimeError when the value is invalid
+895        """
+896        content_length: str | None = self.headers.get("Content-Length")
+897        if content_length is None:
+898            return 0
+899
+900        trimmed = content_length.strip()
+901        if not trimmed.isdigit():
+902            raise ValueError("Content-Length does not contain only digits")
+903
+904        return int(trimmed)
+905
+906    @content_length.setter
+907    def content_length(self, value: int | str):
+908        if self.update_content_length:
+909            # It is useless to manually set content-length because the value will be erased.
+910            raise RuntimeError(
+911                "Cannot set content_length when self.update_content_length is True"
+912            )
+913
+914        if isinstance(value, int):
+915            value = str(value)
+916
+917        self._headers["Content-Length"] = value
+918
+919    @property
+920    def pretty_host(self) -> str:
+921        """Returns the most approriate host
+922        Returns self.host when it exists, else it returns self.host_header
+923
+924        Returns:
+925            str: The request target host
+926        """
+927        return self.host or self.headers.get("Host") or ""
+928
+929    def host_is(self, *patterns: str) -> bool:
+930        """Perform wildcard matching (fnmatch) on the target host.
+931
+932        Args:
+933            pattern (str): The pattern to use
+934
+935        Returns:
+936            bool: Whether the pattern matches
+937        """
+938        return host_is(self.pretty_host, *patterns)
+939
+940    def path_is(self, *patterns: str) -> bool:
+941        return match_patterns(self.path, *patterns)
+
+ + +

A "Burp oriented" HTTP request class

+ +

This class allows to manipulate Burp requests in a Pythonic way.

+
+ + +
+ +
+ + Request( method: str, scheme: Literal['http', 'https'], host: str, port: int, path: str, http_version: str, headers: Union[Headers, tuple[tuple[bytes, bytes], ...], Iterable[tuple[bytes, bytes]]], authority: str, content: bytes | None) + + + +
+ +
111    def __init__(
+112        self,
+113        method: str,
+114        scheme: Literal["http", "https"],
+115        host: str,
+116        port: int,
+117        path: str,
+118        http_version: str,
+119        headers: (
+120            Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]]
+121        ),
+122        authority: str,
+123        content: bytes | None,
+124    ):
+125        self.scheme = scheme
+126        self.host = host
+127        self.port = port
+128        self.path = path
+129        self.method = method
+130        self.authority = authority
+131        self.http_version = http_version
+132        self.headers = headers if isinstance(headers, Headers) else Headers(headers)
+133        self._content = content
+134
+135        # Initialize the serializer (json,urlencoded,multipart)
+136        self.update_serializer_from_content_type(
+137            self.headers.get("Content-Type"), fail_silently=True
+138        )
+139
+140        # Initialize old deserialized content to avoid modifying content if it has not been modified
+141        # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py)
+142        self._old_deserialized_content = deepcopy(self._deserialized_content)
+
+ + + + +
+
+
+ host: str + + +
+ + + + +
+
+
+ port: int + + +
+ + + + +
+
+
+ method: str + + +
+ + + + +
+
+
+ scheme: Literal['http', 'https'] + + +
+ + + + +
+
+
+ authority: str + + +
+ + + + +
+
+
+ path: str + + +
+ + + + +
+
+
+ http_version: str + + +
+ + + + +
+
+
+ update_content_length: bool = +True + + +
+ + + + +
+
+ +
+ headers: Headers + + + +
+ +
870    @property
+871    def headers(self) -> Headers:
+872        """The request HTTP headers
+873
+874        Returns:
+875            Headers: a case insensitive dict containing the HTTP headers
+876        """
+877        self._update_content_length()
+878        return self._headers
+
+ + +

The request HTTP headers

+ +

Returns: + Headers: a case insensitive dict containing the HTTP headers

+
+ + +
+
+ +
+
@classmethod
+ + def + make( cls, method: str, url: str, content: bytes | str = '', headers: Union[Headers, dict[str | bytes, str | bytes], dict[str, str], dict[bytes, bytes], Iterable[tuple[bytes, bytes]]] = ()) -> Request: + + + +
+ +
183    @classmethod
+184    def make(
+185        cls,
+186        method: str,
+187        url: str,
+188        content: bytes | str = "",
+189        headers: (
+190            Headers
+191            | dict[str | bytes, str | bytes]
+192            | dict[str, str]
+193            | dict[bytes, bytes]
+194            | Iterable[tuple[bytes, bytes]]
+195        ) = (),
+196    ) -> Request:
+197        """Create a request from an URL
+198
+199        Args:
+200            method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)
+201            url (str): The request URL
+202            content (bytes | str, optional): The request content. Defaults to "".
+203            headers (Headers, optional): The request headers. Defaults to ().
+204
+205        Returns:
+206            Request: The HTTP request
+207        """
+208        scalpel_headers: Headers
+209        match headers:
+210            case Headers():
+211                scalpel_headers = headers
+212            case dict():
+213                casted_headers = cast(dict[str | bytes, str | bytes], headers)
+214                scalpel_headers = Headers(
+215                    (
+216                        (always_bytes(key), always_bytes(val))
+217                        for key, val in casted_headers.items()
+218                    )
+219                )
+220            case _:
+221                scalpel_headers = Headers(headers)
+222
+223        scheme, host, port, path = Request._parse_url(url)
+224        http_version = "HTTP/1.1"
+225
+226        # Inferr missing Host header from URL
+227        host_header = scalpel_headers.get("Host")
+228        if host_header is None:
+229            match (scheme, port):
+230                case ("http", 80) | ("https", 443):
+231                    host_header = host
+232                case _:
+233                    host_header = f"{host}:{port}"
+234
+235            scalpel_headers["Host"] = host_header
+236
+237        authority: str = host_header
+238        encoded_content = always_bytes(content)
+239
+240        assert isinstance(host, str)
+241
+242        return cls(
+243            method=method,
+244            scheme=scheme,
+245            host=host,
+246            port=port,
+247            path=path,
+248            http_version=http_version,
+249            headers=scalpel_headers,
+250            authority=authority,
+251            content=encoded_content,
+252        )
+
+ + +

Create a request from an URL

+ +

Args: + method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...) + url (str): The request URL + content (bytes | str, optional): The request content. Defaults to "". + headers (Headers, optional): The request headers. Defaults to ().

+ +

Returns: + Request: The HTTP request

+
+ + +
+
+ +
+
@classmethod
+ + def + from_burp( cls, request: pyscalpel.java.burp.http_request.IHttpRequest, service: pyscalpel.java.burp.http_service.IHttpService | None = None) -> Request: + + + +
+ +
254    @classmethod
+255    def from_burp(
+256        cls, request: IHttpRequest, service: IHttpService | None = None
+257    ) -> Request:  # pragma: no cover (uses Java API)
+258        """Construct an instance of the Request class from a Burp suite HttpRequest.
+259        :param request: The Burp suite HttpRequest to convert.
+260        :return: A Request with the same data as the Burp suite HttpRequest.
+261        """
+262        service = service or request.httpService()
+263        body = get_bytes(request.body())
+264
+265        # Burp will give you lowercased and pseudo headers when using HTTP/2.
+266        # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-.
+267        # https://blog.yaakov.online/http-2-header-casing/
+268        headers: Headers = Headers.from_burp(request.headers())
+269
+270        # Burp gives a 0 length byte array body even when it doesn't exist, instead of null.
+271        # Empty but existing bodies without a Content-Length header are lost in the process.
+272        if not body and not headers.get("Content-Length"):
+273            body = None
+274
+275        # request.url() gives a relative url for some reason
+276        # So we have to parse and unparse to get the full path
+277        #   (path + parameters + query + fragment)
+278        _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url())
+279
+280        # Concatenate the path components
+281        # Empty parameters,query and fragment are lost in the process
+282        # e.g.: http://example.com;?# becomes http://example.com
+283        # To use such an URL, the user must set the path directly
+284        # To fix this we would need to write our own URL parser, which is a bit overkill for now.
+285        path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment))
+286
+287        host = ""
+288        port = 0
+289        scheme = "http"
+290        if service:
+291            host = service.host()
+292            port = service.port()
+293            scheme = "https" if service.secure() else "http"
+294
+295        return cls(
+296            method=request.method(),
+297            scheme=scheme,
+298            host=host,
+299            port=port,
+300            path=path,
+301            http_version=request.httpVersion() or "HTTP/1.1",
+302            headers=headers,
+303            authority=headers.get(":authority") or headers.get("Host") or "",
+304            content=body,
+305        )
+
+ + +

Construct an instance of the Request class from a Burp suite HttpRequest.

+ +
#  Parameters
+ +
    +
  • request: The Burp suite HttpRequest to convert.
  • +
+ +
#  Returns
+ +
+

A Request with the same data as the Burp suite HttpRequest.

+
+
+ + +
+
+ +
+ + def + to_burp(self) -> pyscalpel.java.burp.http_request.IHttpRequest: + + + +
+ +
344    def to_burp(self) -> IHttpRequest:  # pragma: no cover
+345        """Convert the request to a Burp suite :class:`IHttpRequest`.
+346        :return: The request as a Burp suite :class:`IHttpRequest`.
+347        """
+348        # Convert the request to a Burp ByteArray.
+349        request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
+350
+351        if self.port == 0:
+352            # No networking information is available, so we build a plain network-less request.
+353            return HttpRequest.httpRequest(request_byte_array)
+354
+355        # Build the Burp HTTP networking service.
+356        service: IHttpService = HttpService.httpService(
+357            self.host, self.port, self.scheme == "https"
+358        )
+359
+360        # Instantiate and return a new Burp HTTP request.
+361        return HttpRequest.httpRequest(service, request_byte_array)
+
+ + +

Convert the request to a Burp suite IHttpRequest.

+ +
#  Returns
+ +
+

The request as a Burp suite IHttpRequest.

+
+
+ + +
+
+ +
+
@classmethod
+ + def + from_raw( cls, data: bytes | str, real_host: str = '', port: int = 0, scheme: Union[Literal['http'], Literal['https'], str] = 'http') -> Request: + + + +
+ +
363    @classmethod
+364    def from_raw(
+365        cls,
+366        data: bytes | str,
+367        real_host: str = "",
+368        port: int = 0,
+369        scheme: Literal["http"] | Literal["https"] | str = "http",
+370    ) -> Request:  # pragma: no cover
+371        """Construct an instance of the Request class from raw bytes.
+372        :param data: The raw bytes to convert.
+373        :param real_host: The real host to connect to.
+374        :param port: The port of the request.
+375        :param scheme: The scheme of the request.
+376        :return: A :class:`Request` with the same data as the raw bytes.
+377        """
+378        # Convert the raw bytes to a Burp ByteArray.
+379        # We use the Burp API to trivialize the parsing of the request from raw bytes.
+380        str_or_byte_array: IByteArray | str = (
+381            data if isinstance(data, str) else PythonUtils.toByteArray(data)
+382        )
+383
+384        # Handle the case where the networking informations are not provided.
+385        if port == 0:
+386            # Instantiate and return a new Burp HTTP request without networking informations.
+387            burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array)
+388        else:
+389            # Build the Burp HTTP networking service.
+390            service: IHttpService = HttpService.httpService(
+391                real_host, port, scheme == "https"
+392            )
+393
+394            # Instantiate a new Burp HTTP request with networking informations.
+395            burp_request: IHttpRequest = HttpRequest.httpRequest(
+396                service, str_or_byte_array
+397            )
+398
+399        # Construct the request from the Burp.
+400        return cls.from_burp(burp_request)
+
+ + +

Construct an instance of the Request class from raw bytes.

+ +
#  Parameters
+ +
    +
  • data: The raw bytes to convert.
  • +
  • real_host: The real host to connect to.
  • +
  • port: The port of the request.
  • +
  • scheme: The scheme of the request.
  • +
+ +
#  Returns
+ +
+

A Request with the same data as the raw bytes.

+
+
+ + +
+
+ +
+ url: str + + + +
+ +
402    @property
+403    def url(self) -> str:
+404        """
+405        The full URL string, constructed from `Request.scheme`,
+406            `Request.host`, `Request.port` and `Request.path`.
+407
+408        Setting this property updates these attributes as well.
+409        """
+410        return Request._unparse_url(self.scheme, self.host, self.port, self.path)
+
+ + +

The full URL string, constructed from Request.scheme, + Request.host, Request.port and Request.path.

+ +

Setting this property updates these attributes as well.

+
+ + +
+
+ +
+ query: pyscalpel.http.body.urlencoded.URLEncodedFormView + + + +
+ +
427    @property
+428    def query(self) -> URLEncodedFormView:
+429        """The query string parameters as a dict-like object
+430
+431        Returns:
+432            QueryParamsView: The query string parameters
+433        """
+434        return URLEncodedFormView(
+435            multidict.MultiDictView(self._get_query, self._set_query)
+436        )
+
+ + +

The query string parameters as a dict-like object

+ +

Returns: + QueryParamsView: The query string parameters

+
+ + +
+
+ +
+ content: bytes | None + + + +
+ +
491    @property
+492    def content(self) -> bytes | None:
+493        """The request content / body as raw bytes
+494
+495        Returns:
+496            bytes | None: The content if it exists
+497        """
+498        if self._serializer and self._has_deserialized_content_changed():
+499            self._update_deserialized_content(self._deserialized_content)
+500            self._old_deserialized_content = deepcopy(self._deserialized_content)
+501
+502        self._update_content_length()
+503
+504        return self._content
+
+ + +

The request content / body as raw bytes

+ +

Returns: + bytes | None: The content if it exists

+
+ + +
+
+ +
+ body: bytes | None + + + +
+ +
520    @property
+521    def body(self) -> bytes | None:
+522        """Alias for content()
+523
+524        Returns:
+525            bytes | None: The request body / content
+526        """
+527        return self.content
+
+ + +

Alias for content()

+ +

Returns: + bytes | None: The request body / content

+
+ + +
+
+ +
+ + def + update_serializer_from_content_type( self, content_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None, fail_silently: bool = False): + + + +
+ +
533    def update_serializer_from_content_type(
+534        self,
+535        content_type: ImplementedContentType | str | None = None,
+536        fail_silently: bool = False,
+537    ):
+538        """Update the form parsing based on the given Content-Type
+539
+540        Args:
+541            content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.
+542            fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.
+543
+544        Raises:
+545            FormNotParsedException: Raised when the content-type is unknown.
+546        """
+547        # Strip the boundary param so we can use our content-type to serializer map
+548        _content_type: str = get_header_value_without_params(
+549            content_type or self.headers.get("Content-Type") or ""
+550        )
+551
+552        serializer = None
+553        if _content_type in IMPLEMENTED_CONTENT_TYPES:
+554            serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type)
+555
+556        if serializer is None:
+557            if fail_silently:
+558                serializer = self._serializer
+559            else:
+560                raise FormNotParsedException(
+561                    f"Unimplemented form content-type: {_content_type}"
+562                )
+563        self._set_serializer(serializer)
+
+ + +

Update the form parsing based on the given Content-Type

+ +

Args: + content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None. + fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.

+ +

Raises: + FormNotParsedException: Raised when the content-type is unknown.

+
+ + +
+
+ +
+ content_type: str | None + + + +
+ +
565    @property
+566    def content_type(self) -> str | None:
+567        """The Content-Type header value.
+568
+569        Returns:
+570            str | None: <=> self.headers.get("Content-Type")
+571        """
+572        return self.headers.get("Content-Type")
+
+ + +

The Content-Type header value.

+ +

Returns: + str | None: <=> self.headers.get("Content-Type")

+
+ + +
+
+ +
+ + def + create_defaultform( self, content_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None, update_header: bool = True) -> MutableMapping[Any, Any]: + + + +
+ +
578    def create_defaultform(
+579        self,
+580        content_type: ImplementedContentType | str | None = None,
+581        update_header: bool = True,
+582    ) -> MutableMapping[Any, Any]:
+583        """Creates the form if it doesn't exist, else returns the existing one
+584
+585        Args:
+586            content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.
+587            update_header (bool, optional): Whether to update the header. Defaults to True.
+588
+589        Raises:
+590            FormNotParsedException: Thrown when provided content-type has no implemented form-serializer
+591            FormNotParsedException: Thrown when the raw content could not be parsed.
+592
+593        Returns:
+594            MutableMapping[Any, Any]: The mapped form.
+595        """
+596        if not self._is_form_initialized or content_type:
+597            self.update_serializer_from_content_type(content_type)
+598
+599            # Set content-type if it does not exist
+600            if (content_type and update_header) or not self.headers.get_all(
+601                "Content-Type"
+602            ):
+603                self.headers["Content-Type"] = content_type
+604
+605        serializer = self._serializer
+606        if serializer is None:
+607            # This should probably never trigger here as it should already be raised by update_serializer_from_content_type
+608            raise FormNotParsedException(
+609                f"Form of content-type {self.content_type} not implemented."
+610            )
+611
+612        # Create default form.
+613        if not self.content:
+614            self._deserialized_content = serializer.get_empty_form(self)
+615        elif self._deserialized_content is None:
+616            self._deserialize_content()
+617
+618        if self._deserialized_content is None:
+619            raise FormNotParsedException(
+620                f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}"
+621            )
+622
+623        if not isinstance(self._deserialized_content, serializer.deserialized_type()):
+624            self._deserialized_content = serializer.get_empty_form(self)
+625
+626        self._is_form_initialized = True
+627        return self._deserialized_content
+
+ + +

Creates the form if it doesn't exist, else returns the existing one

+ +

Args: + content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None. + update_header (bool, optional): Whether to update the header. Defaults to True.

+ +

Raises: + FormNotParsedException: Thrown when provided content-type has no implemented form-serializer + FormNotParsedException: Thrown when the raw content could not be parsed.

+ +

Returns: + MutableMapping[Any, Any]: The mapped form.

+
+ + +
+
+ +
+ form: MutableMapping[Any, Any] + + + +
+ +
629    @property
+630    def form(self) -> MutableMapping[Any, Any]:
+631        """Mapping from content parsed accordingly to Content-Type
+632
+633        Raises:
+634            FormNotParsedException: The content could not be parsed accordingly to Content-Type
+635
+636        Returns:
+637            MutableMapping[Any, Any]: The mapped request form
+638        """
+639        if not self._is_form_initialized:
+640            self.update_serializer_from_content_type()
+641
+642        self.create_defaultform()
+643        if self._deserialized_content is None:
+644            raise FormNotParsedException()
+645
+646        self._is_form_initialized = True
+647        return self._deserialized_content
+
+ + +

Mapping from content parsed accordingly to Content-Type

+ +

Raises: + FormNotParsedException: The content could not be parsed accordingly to Content-Type

+ +

Returns: + MutableMapping[Any, Any]: The mapped request form

+
+ + +
+
+ +
+ urlencoded_form: pyscalpel.http.body.urlencoded.URLEncodedForm + + + +
+ +
706    @property
+707    def urlencoded_form(self) -> URLEncodedForm:
+708        """The urlencoded form data
+709
+710        Converts the content to the urlencoded form format if needed.
+711        Modification to this object will update Request.content and vice versa
+712
+713        Returns:
+714            QueryParams: The urlencoded form data
+715        """
+716        self._is_form_initialized = True
+717        return cast(
+718            URLEncodedForm,
+719            self._update_serializer_and_get_form(URLEncodedFormSerializer()),
+720        )
+
+ + +

The urlencoded form data

+ +

Converts the content to the urlencoded form format if needed. +Modification to this object will update Request.content and vice versa

+ +

Returns: + QueryParams: The urlencoded form data

+
+ + +
+
+ +
+ json_form: dict[str | int | float, str | int | float | bool | None | list[str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]] | dict[str | int | float, str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]]] + + + +
+ +
727    @property
+728    def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]:
+729        """The JSON form data
+730
+731        Converts the content to the JSON form format if needed.
+732        Modification to this object will update Request.content and vice versa
+733
+734        Returns:
+735          dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data
+736        """
+737        self._is_form_initialized = True
+738        if self._update_serializer_and_get_form(JSONFormSerializer()) is None:
+739            serializer = cast(JSONFormSerializer, self._serializer)
+740            self._deserialized_content = serializer.get_empty_form(self)
+741
+742        return self._deserialized_content
+
+ + +

The JSON form data

+ +

Converts the content to the JSON form format if needed. +Modification to this object will update Request.content and vice versa

+ +

Returns: + dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data

+
+ + +
+
+ +
+ multipart_form: pyscalpel.http.body.multipart.MultiPartForm + + + +
+ +
775    @property
+776    def multipart_form(self) -> MultiPartForm:
+777        """The multipart form data
+778
+779        Converts the content to the multipart form format if needed.
+780        Modification to this object will update Request.content and vice versa
+781
+782        Returns:
+783            MultiPartForm
+784        """
+785        self._is_form_initialized = True
+786
+787        # Keep boundary even if content-type has changed
+788        if isinstance(self._deserialized_content, MultiPartForm):
+789            return self._deserialized_content
+790
+791        # We do not have an existing form, so we have to ensure we have a content-type header with a boundary
+792        self._ensure_multipart_content_type()
+793
+794        # Serialize the current form and try to parse it with the new serializer
+795        form = self._update_serializer_and_get_form(MultiPartFormSerializer())
+796        serializer = cast(MultiPartFormSerializer, self._serializer)
+797
+798        # Set a default value
+799        if not form:
+800            self._deserialized_content = serializer.get_empty_form(self)
+801
+802        # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary
+803        if self._deserialized_content is None:
+804            raise FormNotParsedException(
+805                f"Could not parse content to {serializer.deserialized_type()}"
+806            )
+807
+808        return self._deserialized_content
+
+ + +

The multipart form data

+ +

Converts the content to the multipart form format if needed. +Modification to this object will update Request.content and vice versa

+ +

Returns: + MultiPartForm

+
+ + +
+
+ +
+ cookies: _internal_mitmproxy.coretypes.multidict.MultiDictView[str, str] + + + +
+ +
821    @property
+822    def cookies(self) -> multidict.MultiDictView[str, str]:
+823        """
+824        The request cookies.
+825        For the most part, this behaves like a dictionary.
+826        Modifications to the MultiDictView update `Request.headers`, and vice versa.
+827        """
+828        return multidict.MultiDictView(self._get_cookies, self._set_cookies)
+
+ + +

The request cookies. +For the most part, this behaves like a dictionary. +Modifications to the MultiDictView update Request.headers, and vice versa.

+
+ + +
+
+ +
+ host_header: str | None + + + +
+ +
843    @property
+844    def host_header(self) -> str | None:
+845        """Host header value
+846
+847        Returns:
+848            str | None: The host header value
+849        """
+850        return self.headers.get("Host")
+
+ + +

Host header value

+ +

Returns: + str | None: The host header value

+
+ + +
+
+ +
+ + def + text(self, encoding='utf-8') -> str: + + + +
+ +
856    def text(self, encoding="utf-8") -> str:
+857        """The decoded content
+858
+859        Args:
+860            encoding (str, optional): encoding to use. Defaults to "utf-8".
+861
+862        Returns:
+863            str: The decoded content
+864        """
+865        if self.content is None:
+866            return ""
+867
+868        return self.content.decode(encoding)
+
+ + +

The decoded content

+ +

Args: + encoding (str, optional): encoding to use. Defaults to "utf-8".

+ +

Returns: + str: The decoded content

+
+ + +
+
+ +
+ content_length: int + + + +
+ +
885    @property
+886    def content_length(self) -> int:
+887        """Returns the Content-Length header value
+888           Returns 0 if the header is absent
+889
+890        Args:
+891            value (int | str): The Content-Length value
+892
+893        Raises:
+894            RuntimeError: Throws RuntimeError when the value is invalid
+895        """
+896        content_length: str | None = self.headers.get("Content-Length")
+897        if content_length is None:
+898            return 0
+899
+900        trimmed = content_length.strip()
+901        if not trimmed.isdigit():
+902            raise ValueError("Content-Length does not contain only digits")
+903
+904        return int(trimmed)
+
+ + +

Returns the Content-Length header value + Returns 0 if the header is absent

+ +

Args: + value (int | str): The Content-Length value

+ +

Raises: + RuntimeError: Throws RuntimeError when the value is invalid

+
+ + +
+
+ +
+ pretty_host: str + + + +
+ +
919    @property
+920    def pretty_host(self) -> str:
+921        """Returns the most approriate host
+922        Returns self.host when it exists, else it returns self.host_header
+923
+924        Returns:
+925            str: The request target host
+926        """
+927        return self.host or self.headers.get("Host") or ""
+
+ + +

Returns the most approriate host +Returns self.host when it exists, else it returns self.host_header

+ +

Returns: + str: The request target host

+
+ + +
+
+ +
+ + def + host_is(self, *patterns: str) -> bool: + + + +
+ +
929    def host_is(self, *patterns: str) -> bool:
+930        """Perform wildcard matching (fnmatch) on the target host.
+931
+932        Args:
+933            pattern (str): The pattern to use
+934
+935        Returns:
+936            bool: Whether the pattern matches
+937        """
+938        return host_is(self.pretty_host, *patterns)
+
+ + +

Perform wildcard matching (fnmatch) on the target host.

+ +

Args: + pattern (str): The pattern to use

+ +

Returns: + bool: Whether the pattern matches

+
+ + +
+
+ +
+ + def + path_is(self, *patterns: str) -> bool: + + + +
+ +
940    def path_is(self, *patterns: str) -> bool:
+941        return match_patterns(self.path, *patterns)
+
+ + + + +
+
+
+ +
+ + class + Response(_internal_mitmproxy.http.Response): + + + +
+ +
 22class Response(MITMProxyResponse):
+ 23    """A "Burp oriented" HTTP response class
+ 24
+ 25
+ 26    This class allows to manipulate Burp responses in a Pythonic way.
+ 27
+ 28    Fields:
+ 29        scheme: http or https
+ 30        host: The initiating request target host
+ 31        port: The initiating request target port
+ 32        request: The initiating request.
+ 33    """
+ 34
+ 35    scheme: Literal["http", "https"] = "http"
+ 36    host: str = ""
+ 37    port: int = 0
+ 38    request: Request | None = None
+ 39
+ 40    def __init__(
+ 41        self,
+ 42        http_version: bytes,
+ 43        status_code: int,
+ 44        reason: bytes,
+ 45        headers: Headers | tuple[tuple[bytes, bytes], ...],
+ 46        content: bytes | None,
+ 47        trailers: Headers | tuple[tuple[bytes, bytes], ...] | None,
+ 48        scheme: Literal["http", "https"] = "http",
+ 49        host: str = "",
+ 50        port: int = 0,
+ 51    ):
+ 52        # Construct the base/inherited MITMProxy response.
+ 53        super().__init__(
+ 54            http_version,
+ 55            status_code,
+ 56            reason,
+ 57            headers,
+ 58            content,
+ 59            trailers,
+ 60            timestamp_start=time.time(),
+ 61            timestamp_end=time.time(),
+ 62        )
+ 63        self.scheme = scheme
+ 64        self.host = host
+ 65        self.port = port
+ 66
+ 67    @classmethod
+ 68    # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
+ 69    # link to mitmproxy documentation
+ 70    def from_mitmproxy(cls, response: MITMProxyResponse) -> Response:
+ 71        """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
+ 72        :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert.
+ 73        :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
+ 74        """
+ 75        return cls(
+ 76            always_bytes(response.http_version),
+ 77            response.status_code,
+ 78            always_bytes(response.reason),
+ 79            Headers.from_mitmproxy(response.headers),
+ 80            response.content,
+ 81            Headers.from_mitmproxy(response.trailers) if response.trailers else None,
+ 82        )
+ 83
+ 84    @classmethod
+ 85    def from_burp(
+ 86        cls,
+ 87        response: IHttpResponse,
+ 88        service: IHttpService | None = None,
+ 89        request: IHttpRequest | None = None,
+ 90    ) -> Response:
+ 91        """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`."""
+ 92        body = get_bytes(cast(IByteArray, response.body())) if response.body() else b""
+ 93        scalpel_response = cls(
+ 94            always_bytes(response.httpVersion() or "HTTP/1.1"),
+ 95            response.statusCode(),
+ 96            always_bytes(response.reasonPhrase() or b""),
+ 97            Headers.from_burp(response.headers()),
+ 98            body,
+ 99            None,
+100        )
+101
+102        burp_request: IHttpRequest | None = request
+103        if burp_request is None:
+104            try:
+105                # Some responses can have a "initiatingRequest" field.
+106                # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A
+107                burp_request = response.initiatingRequest()  # type: ignore
+108            except AttributeError:
+109                pass
+110
+111        if burp_request:
+112            scalpel_response.request = Request.from_burp(burp_request, service)
+113
+114        if not service and burp_request:
+115            # The only way to check if the Java method exist without writing Java is catching the error.
+116            service = burp_request.httpService()
+117
+118        if service:
+119            scalpel_response.scheme = "https" if service.secure() else "http"
+120            scalpel_response.host = service.host()
+121            scalpel_response.port = service.port()
+122
+123        return scalpel_response
+124
+125    def __bytes__(self) -> bytes:
+126        """Convert the response to raw bytes."""
+127        # Reserialize the response to bytes.
+128
+129        # Format the first line of the response. (e.g. "HTTP/1.1 200 OK\r\n")
+130        first_line = (
+131            b" ".join(
+132                always_bytes(s)
+133                for s in (self.http_version, str(self.status_code), self.reason)
+134            )
+135            + b"\r\n"
+136        )
+137
+138        # Format the response's headers part.
+139        headers_lines = b"".join(
+140            b"%s: %s\r\n" % (key, val) for key, val in self.headers.fields
+141        )
+142
+143        # Set a default value for the response's body. (None -> b"")
+144        body = self.content or b""
+145
+146        # Build the whole response and return it.
+147        return first_line + headers_lines + b"\r\n" + body
+148
+149    def to_burp(self) -> IHttpResponse:  # pragma: no cover (uses Java API)
+150        """Convert the response to a Burp suite :class:`IHttpResponse`."""
+151        response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
+152
+153        return HttpResponse.httpResponse(response_byte_array)
+154
+155    @classmethod
+156    def from_raw(
+157        cls, data: bytes | str
+158    ) -> Response:  # pragma: no cover (uses Java API)
+159        """Construct an instance of the Response class from raw bytes.
+160        :param data: The raw bytes to convert.
+161        :return: A :class:`Response` parsed from the raw bytes.
+162        """
+163        # Use the Burp API to trivialize the parsing of the response from raw bytes.
+164        # Convert the raw bytes to a Burp ByteArray.
+165        # Plain strings are OK too.
+166        str_or_byte_array: IByteArray | str = (
+167            data if isinstance(data, str) else PythonUtils.toByteArray(data)
+168        )
+169
+170        # Instantiate a new Burp HTTP response.
+171        burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array)
+172
+173        return cls.from_burp(burp_response)
+174
+175    @classmethod
+176    def make(
+177        cls,
+178        status_code: int = 200,
+179        content: bytes | str = b"",
+180        headers: Headers | tuple[tuple[bytes, bytes], ...] = (),
+181        host: str = "",
+182        port: int = 0,
+183        scheme: Literal["http", "https"] = "http",
+184    ) -> "Response":
+185        # Use the base/inherited make method to construct a MITMProxy response.
+186        mitmproxy_res = MITMProxyResponse.make(status_code, content, headers)
+187
+188        res = cls.from_mitmproxy(mitmproxy_res)
+189        res.host = host
+190        res.scheme = scheme
+191        res.port = port
+192
+193        return res
+194
+195    def host_is(self, *patterns: str) -> bool:
+196        """Matches the host against the provided patterns
+197
+198        Returns:
+199            bool: Whether at least one pattern matched
+200        """
+201        return host_is(self.host, *patterns)
+202
+203    @property
+204    def body(self) -> bytes | None:
+205        """Alias for content()
+206
+207        Returns:
+208            bytes | None: The request body / content
+209        """
+210        return self.content
+211
+212    @body.setter
+213    def body(self, val: bytes | None):
+214        self.content = val
+
+ + +

A "Burp oriented" HTTP response class

+ +

This class allows to manipulate Burp responses in a Pythonic way.

+ +

Fields: + scheme: http or https + host: The initiating request target host + port: The initiating request target port + request: The initiating request.

+
+ + +
+ +
+ + Response( http_version: bytes, status_code: int, reason: bytes, headers: Headers | tuple[tuple[bytes, bytes], ...], content: bytes | None, trailers: Headers | tuple[tuple[bytes, bytes], ...] | None, scheme: Literal['http', 'https'] = 'http', host: str = '', port: int = 0) + + + +
+ +
40    def __init__(
+41        self,
+42        http_version: bytes,
+43        status_code: int,
+44        reason: bytes,
+45        headers: Headers | tuple[tuple[bytes, bytes], ...],
+46        content: bytes | None,
+47        trailers: Headers | tuple[tuple[bytes, bytes], ...] | None,
+48        scheme: Literal["http", "https"] = "http",
+49        host: str = "",
+50        port: int = 0,
+51    ):
+52        # Construct the base/inherited MITMProxy response.
+53        super().__init__(
+54            http_version,
+55            status_code,
+56            reason,
+57            headers,
+58            content,
+59            trailers,
+60            timestamp_start=time.time(),
+61            timestamp_end=time.time(),
+62        )
+63        self.scheme = scheme
+64        self.host = host
+65        self.port = port
+
+ + + + +
+
+
+ scheme: Literal['http', 'https'] = +'http' + + +
+ + + + +
+
+
+ host: str = +'' + + +
+ + + + +
+
+
+ port: int = +0 + + +
+ + + + +
+
+
+ request: Request | None = +None + + +
+ + + + +
+
+ +
+
@classmethod
+ + def + from_mitmproxy( cls, response: _internal_mitmproxy.http.Response) -> Response: + + + +
+ +
67    @classmethod
+68    # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
+69    # link to mitmproxy documentation
+70    def from_mitmproxy(cls, response: MITMProxyResponse) -> Response:
+71        """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
+72        :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert.
+73        :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
+74        """
+75        return cls(
+76            always_bytes(response.http_version),
+77            response.status_code,
+78            always_bytes(response.reason),
+79            Headers.from_mitmproxy(response.headers),
+80            response.content,
+81            Headers.from_mitmproxy(response.trailers) if response.trailers else None,
+82        )
+
+ + +

Construct an instance of the Response class from a mitmproxy.http.HTTPResponse.

+ +
#  Parameters
+ + + +
#  Returns
+ +
+

A Response with the same data as the mitmproxy.http.HTTPResponse.

+
+
+ + +
+
+ +
+
@classmethod
+ + def + from_burp( cls, response: pyscalpel.java.burp.http_response.IHttpResponse, service: pyscalpel.java.burp.http_service.IHttpService | None = None, request: pyscalpel.java.burp.http_request.IHttpRequest | None = None) -> Response: + + + +
+ +
 84    @classmethod
+ 85    def from_burp(
+ 86        cls,
+ 87        response: IHttpResponse,
+ 88        service: IHttpService | None = None,
+ 89        request: IHttpRequest | None = None,
+ 90    ) -> Response:
+ 91        """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`."""
+ 92        body = get_bytes(cast(IByteArray, response.body())) if response.body() else b""
+ 93        scalpel_response = cls(
+ 94            always_bytes(response.httpVersion() or "HTTP/1.1"),
+ 95            response.statusCode(),
+ 96            always_bytes(response.reasonPhrase() or b""),
+ 97            Headers.from_burp(response.headers()),
+ 98            body,
+ 99            None,
+100        )
+101
+102        burp_request: IHttpRequest | None = request
+103        if burp_request is None:
+104            try:
+105                # Some responses can have a "initiatingRequest" field.
+106                # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A
+107                burp_request = response.initiatingRequest()  # type: ignore
+108            except AttributeError:
+109                pass
+110
+111        if burp_request:
+112            scalpel_response.request = Request.from_burp(burp_request, service)
+113
+114        if not service and burp_request:
+115            # The only way to check if the Java method exist without writing Java is catching the error.
+116            service = burp_request.httpService()
+117
+118        if service:
+119            scalpel_response.scheme = "https" if service.secure() else "http"
+120            scalpel_response.host = service.host()
+121            scalpel_response.port = service.port()
+122
+123        return scalpel_response
+
+ + +

Construct an instance of the Response class from a Burp suite IHttpResponse.

+
+ + +
+
+ +
+ + def + to_burp(self) -> pyscalpel.java.burp.http_response.IHttpResponse: + + + +
+ +
149    def to_burp(self) -> IHttpResponse:  # pragma: no cover (uses Java API)
+150        """Convert the response to a Burp suite :class:`IHttpResponse`."""
+151        response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
+152
+153        return HttpResponse.httpResponse(response_byte_array)
+
+ + +

Convert the response to a Burp suite IHttpResponse.

+
+ + +
+
+ +
+
@classmethod
+ + def + from_raw(cls, data: bytes | str) -> Response: + + + +
+ +
155    @classmethod
+156    def from_raw(
+157        cls, data: bytes | str
+158    ) -> Response:  # pragma: no cover (uses Java API)
+159        """Construct an instance of the Response class from raw bytes.
+160        :param data: The raw bytes to convert.
+161        :return: A :class:`Response` parsed from the raw bytes.
+162        """
+163        # Use the Burp API to trivialize the parsing of the response from raw bytes.
+164        # Convert the raw bytes to a Burp ByteArray.
+165        # Plain strings are OK too.
+166        str_or_byte_array: IByteArray | str = (
+167            data if isinstance(data, str) else PythonUtils.toByteArray(data)
+168        )
+169
+170        # Instantiate a new Burp HTTP response.
+171        burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array)
+172
+173        return cls.from_burp(burp_response)
+
+ + +

Construct an instance of the Response class from raw bytes.

+ +
#  Parameters
+ +
    +
  • data: The raw bytes to convert.
  • +
+ +
#  Returns
+ +
+

A Response parsed from the raw bytes.

+
+
+ + +
+
+ +
+
@classmethod
+ + def + make( cls, status_code: int = 200, content: bytes | str = b'', headers: Headers | tuple[tuple[bytes, bytes], ...] = (), host: str = '', port: int = 0, scheme: Literal['http', 'https'] = 'http') -> Response: + + + +
+ +
175    @classmethod
+176    def make(
+177        cls,
+178        status_code: int = 200,
+179        content: bytes | str = b"",
+180        headers: Headers | tuple[tuple[bytes, bytes], ...] = (),
+181        host: str = "",
+182        port: int = 0,
+183        scheme: Literal["http", "https"] = "http",
+184    ) -> "Response":
+185        # Use the base/inherited make method to construct a MITMProxy response.
+186        mitmproxy_res = MITMProxyResponse.make(status_code, content, headers)
+187
+188        res = cls.from_mitmproxy(mitmproxy_res)
+189        res.host = host
+190        res.scheme = scheme
+191        res.port = port
+192
+193        return res
+
+ + +

Simplified API for creating response objects.

+
+ + +
+
+ +
+ + def + host_is(self, *patterns: str) -> bool: + + + +
+ +
195    def host_is(self, *patterns: str) -> bool:
+196        """Matches the host against the provided patterns
+197
+198        Returns:
+199            bool: Whether at least one pattern matched
+200        """
+201        return host_is(self.host, *patterns)
+
+ + +

Matches the host against the provided patterns

+ +

Returns: + bool: Whether at least one pattern matched

+
+ + +
+
+ +
+ body: bytes | None + + + +
+ +
203    @property
+204    def body(self) -> bytes | None:
+205        """Alias for content()
+206
+207        Returns:
+208            bytes | None: The request body / content
+209        """
+210        return self.content
+
+ + +

Alias for content()

+ +

Returns: + bytes | None: The request body / content

+
+ + +
+
+
Inherited Members
+
+
_internal_mitmproxy.http.Response
+
data
+
status_code
+
reason
+
cookies
+
refresh
+ +
+
_internal_mitmproxy.http.Message
+
from_state
+
get_state
+
set_state
+
stream
+
http_version
+
is_http10
+
is_http11
+
is_http2
+
headers
+
trailers
+
raw_content
+
content
+
text
+
set_content
+
get_content
+
set_text
+
get_text
+
timestamp_start
+
timestamp_end
+
decode
+
encode
+
json
+ +
+
_internal_mitmproxy.coretypes.serializable.Serializable
+
copy
+ +
+
+
+
+
+ +
+ + class + Headers(_internal_mitmproxy.coretypes.multidict._MultiDict[~KT, ~VT], _internal_mitmproxy.coretypes.serializable.Serializable): + + + +
+ +
16class Headers(MITMProxyHeaders):
+17    """A wrapper around the MITMProxy Headers.
+18
+19    This class provides additional methods for converting headers between Burp suite and MITMProxy formats.
+20    """
+21
+22    def __init__(self, fields: Iterable[tuple[bytes, bytes]] | None = None, **headers):
+23        """
+24        :param fields: The headers to construct the from.
+25        :param headers: The headers to construct the from.
+26        """
+27        # Cannot safely use [] as default param because the reference will be shared with all __init__ calls
+28        fields = fields or []
+29
+30        # Construct the base/inherited MITMProxy headers.
+31        super().__init__(fields, **headers)
+32
+33    @classmethod
+34    def from_mitmproxy(cls, headers: MITMProxyHeaders) -> Headers:
+35        """
+36        Creates a `Headers` from a `mitmproxy.http.Headers`.
+37
+38        :param headers: The `mitmproxy.http.Headers` to convert.
+39        :type headers: :class Headers <https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Headers>`
+40        :return: A `Headers` with the same headers as the `mitmproxy.http.Headers`.
+41        """
+42
+43        # Construct from the raw MITMProxy headers data.
+44        return cls(headers.fields)
+45
+46    @classmethod
+47    def from_burp(cls, headers: list[IHttpHeader]) -> Headers:  # pragma: no cover
+48        """Construct an instance of the Headers class from a Burp suite HttpHeader array.
+49        :param headers: The Burp suite HttpHeader array to convert.
+50        :return: A Headers with the same headers as the Burp suite HttpHeader array.
+51        """
+52
+53        # print(f"burp: {headers}")
+54        # Convert the list of Burp IHttpHeaders to a list of tuples: (key, value)
+55        return cls(
+56            (
+57                (
+58                    always_bytes(header.name()),
+59                    always_bytes(header.value()),
+60                )
+61                for header in headers
+62            )
+63        )
+64
+65    def to_burp(self) -> list[IHttpHeader]:  # pragma: no cover
+66        """Convert the headers to a Burp suite HttpHeader array.
+67        :return: A Burp suite HttpHeader array.
+68        """
+69
+70        # Convert the list of tuples: (key, value) to a list of Burp IHttpHeaders
+71        return [
+72            HttpHeader.httpHeader(always_str(header[0]), always_str(header[1]))
+73            for header in self.fields
+74        ]
+
+ + +

A wrapper around the MITMProxy Headers.

+ +

This class provides additional methods for converting headers between Burp suite and MITMProxy formats.

+
+ + +
+ +
+ + Headers(fields: Optional[Iterable[tuple[bytes, bytes]]] = None, **headers) + + + +
+ +
22    def __init__(self, fields: Iterable[tuple[bytes, bytes]] | None = None, **headers):
+23        """
+24        :param fields: The headers to construct the from.
+25        :param headers: The headers to construct the from.
+26        """
+27        # Cannot safely use [] as default param because the reference will be shared with all __init__ calls
+28        fields = fields or []
+29
+30        # Construct the base/inherited MITMProxy headers.
+31        super().__init__(fields, **headers)
+
+ + +
#  Parameters
+ +
    +
  • fields: The headers to construct the from.
  • +
  • headers: The headers to construct the from.
  • +
+
+ + +
+
+ +
+
@classmethod
+ + def + from_mitmproxy( cls, headers: _internal_mitmproxy.http.Headers) -> Headers: + + + +
+ +
33    @classmethod
+34    def from_mitmproxy(cls, headers: MITMProxyHeaders) -> Headers:
+35        """
+36        Creates a `Headers` from a `mitmproxy.http.Headers`.
+37
+38        :param headers: The `mitmproxy.http.Headers` to convert.
+39        :type headers: :class Headers <https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Headers>`
+40        :return: A `Headers` with the same headers as the `mitmproxy.http.Headers`.
+41        """
+42
+43        # Construct from the raw MITMProxy headers data.
+44        return cls(headers.fields)
+
+ + +

Creates a Headers from a mitmproxy.http.Headers.

+ +
#  Parameters
+ +
    +
  • headers: The mitmproxy.http.Headers to convert.
  • +
+ +
#  Returns
+ +
+

A Headers with the same headers as the mitmproxy.http.Headers.

+
+
+ + +
+
+ +
+
@classmethod
+ + def + from_burp( cls, headers: list[pyscalpel.java.burp.http_header.IHttpHeader]) -> Headers: + + + +
+ +
46    @classmethod
+47    def from_burp(cls, headers: list[IHttpHeader]) -> Headers:  # pragma: no cover
+48        """Construct an instance of the Headers class from a Burp suite HttpHeader array.
+49        :param headers: The Burp suite HttpHeader array to convert.
+50        :return: A Headers with the same headers as the Burp suite HttpHeader array.
+51        """
+52
+53        # print(f"burp: {headers}")
+54        # Convert the list of Burp IHttpHeaders to a list of tuples: (key, value)
+55        return cls(
+56            (
+57                (
+58                    always_bytes(header.name()),
+59                    always_bytes(header.value()),
+60                )
+61                for header in headers
+62            )
+63        )
+
+ + +

Construct an instance of the Headers class from a Burp suite HttpHeader array.

+ +
#  Parameters
+ +
    +
  • headers: The Burp suite HttpHeader array to convert.
  • +
+ +
#  Returns
+ +
+

A Headers with the same headers as the Burp suite HttpHeader array.

+
+
+ + +
+
+ +
+ + def + to_burp(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]: + + + +
+ +
65    def to_burp(self) -> list[IHttpHeader]:  # pragma: no cover
+66        """Convert the headers to a Burp suite HttpHeader array.
+67        :return: A Burp suite HttpHeader array.
+68        """
+69
+70        # Convert the list of tuples: (key, value) to a list of Burp IHttpHeaders
+71        return [
+72            HttpHeader.httpHeader(always_str(header[0]), always_str(header[1]))
+73            for header in self.fields
+74        ]
+
+ + +

Convert the headers to a Burp suite HttpHeader array.

+ +
#  Returns
+ +
+

A Burp suite HttpHeader array.

+
+
+ + +
+
+
Inherited Members
+
+
_internal_mitmproxy.coretypes.multidict._MultiDict
+
fields
+
get_all
+
set_all
+
add
+
insert
+
keys
+
values
+
items
+ +
+
_internal_mitmproxy.coretypes.serializable.Serializable
+
from_state
+
get_state
+
set_state
+
copy
+ +
+
collections.abc.MutableMapping
+
pop
+
popitem
+
clear
+
update
+
setdefault
+ +
+
collections.abc.Mapping
+
get
+ +
+
+
+
+
+ +
+ + class + Flow: + + + +
+ +
10class Flow:
+11    """Contains request and response and some utilities for match()"""
+12
+13    def __init__(
+14        self,
+15        scheme: Literal["http", "https"] = "http",
+16        host: str = "",
+17        port: int = 0,
+18        request: Request | None = None,
+19        response: Response | None = None,
+20        text: bytes | None = None,
+21    ):
+22        self.scheme = scheme
+23        self.host = host
+24        self.port = port
+25        self.request = request
+26        self.response = response
+27        self.text = text
+28
+29    def host_is(self, *patterns: str) -> bool:
+30        """Matches a wildcard pattern against the target host
+31
+32        Returns:
+33            bool: True if at least one pattern matched
+34        """
+35        return host_is(self.host, *patterns)
+36
+37    def path_is(self, *patterns: str) -> bool:
+38        """Matches a wildcard pattern against the request path
+39
+40        Includes query string `?` and fragment `#`
+41
+42        Returns:
+43            bool: True if at least one pattern matched
+44        """
+45        req = self.request
+46        if req is None:
+47            return False
+48
+49        return req.path_is(*patterns)
+
+ + +

Contains request and response and some utilities for match()

+
+ + +
+ +
+ + Flow( scheme: Literal['http', 'https'] = 'http', host: str = '', port: int = 0, request: Request | None = None, response: Response | None = None, text: bytes | None = None) + + + +
+ +
13    def __init__(
+14        self,
+15        scheme: Literal["http", "https"] = "http",
+16        host: str = "",
+17        port: int = 0,
+18        request: Request | None = None,
+19        response: Response | None = None,
+20        text: bytes | None = None,
+21    ):
+22        self.scheme = scheme
+23        self.host = host
+24        self.port = port
+25        self.request = request
+26        self.response = response
+27        self.text = text
+
+ + + + +
+
+
+ scheme + + +
+ + + + +
+
+
+ host + + +
+ + + + +
+
+
+ port + + +
+ + + + +
+
+
+ request + + +
+ + + + +
+
+
+ response + + +
+ + + + +
+
+
+ text + + +
+ + + + +
+
+ +
+ + def + host_is(self, *patterns: str) -> bool: + + + +
+ +
29    def host_is(self, *patterns: str) -> bool:
+30        """Matches a wildcard pattern against the target host
+31
+32        Returns:
+33            bool: True if at least one pattern matched
+34        """
+35        return host_is(self.host, *patterns)
+
+ + +

Matches a wildcard pattern against the target host

+ +

Returns: + bool: True if at least one pattern matched

+
+ + +
+
+ +
+ + def + path_is(self, *patterns: str) -> bool: + + + +
+ +
37    def path_is(self, *patterns: str) -> bool:
+38        """Matches a wildcard pattern against the request path
+39
+40        Includes query string `?` and fragment `#`
+41
+42        Returns:
+43            bool: True if at least one pattern matched
+44        """
+45        req = self.request
+46        if req is None:
+47            return False
+48
+49        return req.path_is(*patterns)
+
+ + +

Matches a wildcard pattern against the request path

+ +

Includes query string ? and fragment #

+ +

Returns: + bool: True if at least one pattern matched

+
+ + +
+
+
+ +
+ + def + host_is(host: str, *patterns: str) -> bool: + + + +
+ +
21def host_is(host: str, *patterns: str) -> bool:
+22    """Matches a host using unix-like wildcard matching against multiple patterns
+23
+24    Args:
+25        host (str): The host to match against
+26        patterns (str): The patterns to use
+27
+28    Returns:
+29        bool: The match result (True if at least one pattern matches, else False)
+30    """
+31    return match_patterns(host, *patterns)
+
+ + +

Matches a host using unix-like wildcard matching against multiple patterns

+ +

Args: + host (str): The host to match against + patterns (str): The patterns to use

+ +

Returns: + bool: The match result (True if at least one pattern matches, else False)

+
+ + +
+
+ +
+ + def + match_patterns(to_match: str, *patterns: str) -> bool: + + + +
+ +
 5def match_patterns(to_match: str, *patterns: str) -> bool:
+ 6    """Matches a string using unix-like wildcard matching against multiple patterns
+ 7
+ 8    Args:
+ 9        to_match (str): The string to match against
+10        patterns (str): The patterns to use
+11
+12    Returns:
+13        bool: The match result (True if at least one pattern matches, else False)
+14    """
+15    for pattern in patterns:
+16        if fnmatch(to_match, pattern):
+17            return True
+18    return False
+
+ + +

Matches a string using unix-like wildcard matching against multiple patterns

+ +

Args: + to_match (str): The string to match against + patterns (str): The patterns to use

+ +

Returns: + bool: The match result (True if at least one pattern matches, else False)

+
+ + +
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/http/body.html b/docs/public/api/pyscalpel/http/body.html new file mode 100644 index 00000000..ecd90d33 --- /dev/null +++ b/docs/public/api/pyscalpel/http/body.html @@ -0,0 +1,2311 @@ + + + + + + + + + pyscalpel.http.body + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.http.body

+ +

Pentesters often have to manipulate form data in precise and extensive ways

+ +

This module contains implementations for the most common forms (multipart,urlencoded, JSON)

+ +

Users may be implement their own form by creating a Serializer, +assigning the .serializer attribute in Request and using the "form" property

+ +

Forms are designed to be convertible from one to another.

+ +

For example, JSON forms may be converted to URL encoded forms +by using the php query string syntax:

+ +

{"key1": {"key2" : {"key3" : "nested_value"}}} -> key1[key2][key3]=nested_value

+ +

And vice-versa.

+
+ + + + + +
 1"""
+ 2    Pentesters often have to manipulate form data in precise and extensive ways
+ 3
+ 4    This module contains implementations for the most common forms (multipart,urlencoded, JSON)
+ 5    
+ 6    Users may be implement their own form by creating a Serializer,
+ 7    assigning the .serializer attribute in `Request` and using the "form" property
+ 8    
+ 9    Forms are designed to be convertible from one to another.
+10    
+11    For example, JSON forms may be converted to URL encoded forms
+12    by using the php query string syntax:
+13    
+14    ```{"key1": {"key2" : {"key3" : "nested_value"}}} -> key1[key2][key3]=nested_value```
+15    
+16    And vice-versa.
+17"""
+18
+19from .form import *
+20
+21
+22__all__ = [
+23    "Form",
+24    "JSON_VALUE_TYPES",
+25    "JSONForm",
+26    "MultiPartForm",
+27    "MultiPartFormField",
+28    "URLEncodedForm",
+29    "FormSerializer",
+30    "json_unescape",
+31    "json_unescape_bytes",
+32    "json_escape_bytes",
+33]
+
+ + +
+
+ +
+ + class + Form(collections.abc.MutableMapping[~KT, ~VT]): + + + +
+ +
33class Form(MutableMapping[KT, VT], metaclass=ABCMeta):
+34    pass
+
+ + +

A MutableMapping is a generic container for associating +key/value pairs.

+ +

This class provides concrete generic implementations of all +methods except for __getitem__, __setitem__, __delitem__, +__iter__, and __len__.

+
+ + +
+
Inherited Members
+
+
collections.abc.MutableMapping
+
pop
+
popitem
+
clear
+
update
+
setdefault
+ +
+
collections.abc.Mapping
+
get
+
keys
+
items
+
values
+ +
+
+
+
+
+
+ JSON_VALUE_TYPES = + + str | int | float | bool | None | list['JSON_VALUE_TYPES'] | dict[str | int | float, 'JSON_VALUE_TYPES'] + + +
+ + + + +
+
+ +
+ + class + JSONForm(dict[str | int | float, str | int | float | bool | None | list['JSON_VALUE_TYPES'] | dict[str | int | float, 'JSON_VALUE_TYPES']]): + + + +
+ +
37class JSONForm(dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]):
+38    """Form representing a JSON object {}
+39
+40    Implemented by a plain dict
+41
+42    Args:
+43        dict (_type_): A dict containing JSON-compatible types.
+44    """
+45
+46    pass
+
+ + +

Form representing a JSON object {}

+ +

Implemented by a plain dict

+ +

Args: + dict (_type_): A dict containing JSON-compatible types.

+
+ + +
+
Inherited Members
+
+
builtins.dict
+
get
+
setdefault
+
pop
+
popitem
+
keys
+
items
+
values
+
update
+
fromkeys
+
clear
+
copy
+ +
+
+
+
+
+ +
+ + class + MultiPartForm(collections.abc.Mapping[str, pyscalpel.http.body.multipart.MultiPartFormField]): + + + +
+ +
341class MultiPartForm(Mapping[str, MultiPartFormField]):
+342    """
+343    This class represents a multipart/form-data request.
+344
+345    It contains a collection of MultiPartFormField objects, providing methods
+346    to add, get, and delete form fields.
+347
+348    The class also enables the conversion of the entire form
+349    into bytes for transmission.
+350
+351    - Args:
+352        - fields (Sequence[MultiPartFormField]): A sequence of MultiPartFormField objects that make up the form.
+353        - content_type (str): The content type of the form.
+354        - encoding (str): The encoding of the form.
+355
+356    - Raises:
+357        - TypeError: Raised when an incorrect type is passed to MultiPartForm.set.
+358        - KeyError: Raised when trying to access a field that does not exist in the form.
+359
+360    - Returns:
+361        - MultiPartForm: An instance of the class representing a multipart/form-data request.
+362
+363    - Yields:
+364        - Iterator[MultiPartFormField]: Yields each field in the form.
+365    """
+366
+367    fields: list[MultiPartFormField]
+368    content_type: str
+369    encoding: str
+370
+371    def __init__(
+372        self,
+373        fields: Sequence[MultiPartFormField],
+374        content_type: str,
+375        encoding: str = "utf-8",
+376    ):
+377        self.content_type = content_type
+378        self.encoding = encoding
+379        super().__init__()
+380        self.fields = list(fields)
+381
+382    @classmethod
+383    def from_bytes(
+384        cls, content: bytes, content_type: str, encoding: str = "utf-8"
+385    ) -> MultiPartForm:
+386        """Create a MultiPartForm by parsing a raw multipart form
+387
+388        - Args:
+389            - content (bytes): The multipart form as raw bytes
+390            - content_type (str): The Content-Type header with the corresponding boundary param (required).
+391            - encoding (str, optional): The encoding to use (not required). Defaults to "utf-8".
+392
+393        - Returns:
+394           - MultiPartForm: The parsed multipart form
+395        """
+396        decoder = MultipartDecoder(content, content_type, encoding=encoding)
+397        parts: tuple[BodyPart] = decoder.parts
+398        fields: tuple[MultiPartFormField, ...] = tuple(
+399            MultiPartFormField.from_body_part(body_part) for body_part in parts
+400        )
+401        return cls(fields, content_type, encoding)
+402
+403    @property
+404    def boundary(self) -> bytes:
+405        """Get the form multipart boundary
+406
+407        Returns:
+408            bytes: The multipart boundary
+409        """
+410        return extract_boundary(self.content_type, self.encoding)
+411
+412    def __bytes__(self) -> bytes:
+413        boundary = self.boundary
+414        serialized = b""
+415        encoding = self.encoding
+416        for field in self.fields:
+417            serialized += b"--" + boundary + b"\r\n"
+418
+419            # Format the headers
+420            for key, val in field.headers.items():
+421                serialized += (
+422                    key.encode(encoding) + b": " + val.encode(encoding) + b"\r\n"
+423                )
+424            serialized += b"\r\n" + field.content + b"\r\n"
+425
+426        # Format the final boundary
+427        serialized += b"--" + boundary + b"--\r\n\r\n"
+428        return serialized
+429
+430    # Override
+431    def get_all(self, key: str) -> list[MultiPartFormField]:
+432        """
+433        Return the list of all values for a given key.
+434        If that key is not in the MultiDict, the return value will be an empty list.
+435        """
+436        return [field for field in self.fields if key == field.name]
+437
+438    def get(
+439        self, key: str, default: MultiPartFormField | None = None
+440    ) -> MultiPartFormField | None:
+441        values = self.get_all(key)
+442        if not values:
+443            return default
+444
+445        return values[0]
+446
+447    def del_all(self, key: str):
+448        # Mutate object to avoid invalidating user references to fields
+449        for field in self.fields:
+450            if key == field.name:
+451                self.fields.remove(field)
+452
+453    def __delitem__(self, key: str):
+454        self.del_all(key)
+455
+456    def set(
+457        self,
+458        key: str,
+459        value: (
+460            TextIOWrapper
+461            | BufferedReader
+462            | IOBase
+463            | MultiPartFormField
+464            | bytes
+465            | str
+466            | int
+467            | float
+468            | None
+469        ),
+470    ) -> None:
+471        new_field: MultiPartFormField
+472        match value:
+473            case MultiPartFormField():
+474                new_field = value
+475            case int() | float():
+476                return self.set(key, str(value))
+477            case bytes() | str():
+478                new_field = MultiPartFormField.make(key)
+479                new_field.content = always_bytes(value)
+480            case IOBase():
+481                new_field = MultiPartFormField.from_file(key, value)
+482            case None:
+483                self.del_all(key)
+484                return
+485            case _:
+486                raise TypeError("Wrong type was passed to MultiPartForm.set")
+487
+488        for i, field in enumerate(self.fields):
+489            if field.name == key:
+490                self.fields[i] = new_field
+491                return
+492
+493        self.append(new_field)
+494
+495    def setdefault(
+496        self, key: str, default: MultiPartFormField | None = None
+497    ) -> MultiPartFormField:
+498        found = self.get(key)
+499        if found is None:
+500            default = default or MultiPartFormField.make(key)
+501            self[key] = default
+502            return default
+503
+504        return found
+505
+506    def __setitem__(
+507        self,
+508        key: str,
+509        value: (
+510            TextIOWrapper
+511            | BufferedReader
+512            | MultiPartFormField
+513            | IOBase
+514            | bytes
+515            | str
+516            | int
+517            | float
+518            | None
+519        ),
+520    ) -> None:
+521        self.set(key, value)
+522
+523    def __getitem__(self, key: str) -> MultiPartFormField:
+524        values = self.get_all(key)
+525        if not values:
+526            raise KeyError(key)
+527        return values[0]
+528
+529    def __len__(self) -> int:
+530        return len(self.fields)
+531
+532    def __eq__(self, other) -> bool:
+533        if isinstance(other, MultiPartForm):
+534            return self.fields == other.fields
+535        return False
+536
+537    def __iter__(self) -> Iterator[MultiPartFormField]:
+538        seen = set()
+539        for field in self.fields:
+540            if field not in seen:
+541                seen.add(field)
+542                yield field
+543
+544    def insert(self, index: int, value: MultiPartFormField) -> None:
+545        """
+546        Insert an additional value for the given key at the specified position.
+547        """
+548        self.fields.insert(index, value)
+549
+550    def append(self, value: MultiPartFormField) -> None:
+551        self.fields.append(value)
+552
+553    def __repr__(self):  # pragma: no cover
+554        fields = (repr(field) for field in self.fields)
+555        return f"{type(self).__name__}[{', '.join(fields)}]"
+556
+557    def items(self) -> tuple[tuple[str, MultiPartFormField], ...]:
+558        fields = self.fields
+559        items = ((i.name, i) for i in fields)
+560        return tuple(items)
+561
+562    def keys(self) -> tuple[str, ...]:
+563        return tuple(field.name for field in self.fields)
+564
+565    def values(self) -> tuple[MultiPartFormField, ...]:
+566        return tuple(self.fields)
+
+ + +

This class represents a multipart/form-data request.

+ +

It contains a collection of MultiPartFormField objects, providing methods +to add, get, and delete form fields.

+ +

The class also enables the conversion of the entire form +into bytes for transmission.

+ +
    +
  • Args:

    + +
      +
    • fields (Sequence[MultiPartFormField]): A sequence of MultiPartFormField objects that make up the form.
    • +
    • content_type (str): The content type of the form.
    • +
    • encoding (str): The encoding of the form.
    • +
  • +
  • Raises:

    + +
      +
    • TypeError: Raised when an incorrect type is passed to MultiPartForm.set.
    • +
    • KeyError: Raised when trying to access a field that does not exist in the form.
    • +
  • +
  • Returns:

    + +
      +
    • MultiPartForm: An instance of the class representing a multipart/form-data request.
    • +
  • +
  • Yields:

    + +
      +
    • Iterator[MultiPartFormField]: Yields each field in the form.
    • +
  • +
+
+ + +
+ +
+ + MultiPartForm( fields: Sequence[MultiPartFormField], content_type: str, encoding: str = 'utf-8') + + + +
+ +
371    def __init__(
+372        self,
+373        fields: Sequence[MultiPartFormField],
+374        content_type: str,
+375        encoding: str = "utf-8",
+376    ):
+377        self.content_type = content_type
+378        self.encoding = encoding
+379        super().__init__()
+380        self.fields = list(fields)
+
+ + + + +
+
+
+ fields: list[MultiPartFormField] + + +
+ + + + +
+
+
+ content_type: str + + +
+ + + + +
+
+
+ encoding: str + + +
+ + + + +
+
+ +
+
@classmethod
+ + def + from_bytes( cls, content: bytes, content_type: str, encoding: str = 'utf-8') -> MultiPartForm: + + + +
+ +
382    @classmethod
+383    def from_bytes(
+384        cls, content: bytes, content_type: str, encoding: str = "utf-8"
+385    ) -> MultiPartForm:
+386        """Create a MultiPartForm by parsing a raw multipart form
+387
+388        - Args:
+389            - content (bytes): The multipart form as raw bytes
+390            - content_type (str): The Content-Type header with the corresponding boundary param (required).
+391            - encoding (str, optional): The encoding to use (not required). Defaults to "utf-8".
+392
+393        - Returns:
+394           - MultiPartForm: The parsed multipart form
+395        """
+396        decoder = MultipartDecoder(content, content_type, encoding=encoding)
+397        parts: tuple[BodyPart] = decoder.parts
+398        fields: tuple[MultiPartFormField, ...] = tuple(
+399            MultiPartFormField.from_body_part(body_part) for body_part in parts
+400        )
+401        return cls(fields, content_type, encoding)
+
+ + +

Create a MultiPartForm by parsing a raw multipart form

+ +
    +
  • Args:

    + +
      +
    • content (bytes): The multipart form as raw bytes
    • +
    • content_type (str): The Content-Type header with the corresponding boundary param (required).
    • +
    • encoding (str, optional): The encoding to use (not required). Defaults to "utf-8".
    • +
  • +
  • Returns:

    + +
      +
    • MultiPartForm: The parsed multipart form
    • +
  • +
+
+ + +
+
+ +
+ boundary: bytes + + + +
+ +
403    @property
+404    def boundary(self) -> bytes:
+405        """Get the form multipart boundary
+406
+407        Returns:
+408            bytes: The multipart boundary
+409        """
+410        return extract_boundary(self.content_type, self.encoding)
+
+ + +

Get the form multipart boundary

+ +

Returns: + bytes: The multipart boundary

+
+ + +
+
+ +
+ + def + get_all(self, key: str) -> list[MultiPartFormField]: + + + +
+ +
431    def get_all(self, key: str) -> list[MultiPartFormField]:
+432        """
+433        Return the list of all values for a given key.
+434        If that key is not in the MultiDict, the return value will be an empty list.
+435        """
+436        return [field for field in self.fields if key == field.name]
+
+ + +

Return the list of all values for a given key. +If that key is not in the MultiDict, the return value will be an empty list.

+
+ + +
+
+ +
+ + def + del_all(self, key: str): + + + +
+ +
447    def del_all(self, key: str):
+448        # Mutate object to avoid invalidating user references to fields
+449        for field in self.fields:
+450            if key == field.name:
+451                self.fields.remove(field)
+
+ + + + +
+
+ +
+ + def + set( self, key: str, value: _io.TextIOWrapper | _io.BufferedReader | io.IOBase | MultiPartFormField | bytes | str | int | float | None) -> None: + + + +
+ +
456    def set(
+457        self,
+458        key: str,
+459        value: (
+460            TextIOWrapper
+461            | BufferedReader
+462            | IOBase
+463            | MultiPartFormField
+464            | bytes
+465            | str
+466            | int
+467            | float
+468            | None
+469        ),
+470    ) -> None:
+471        new_field: MultiPartFormField
+472        match value:
+473            case MultiPartFormField():
+474                new_field = value
+475            case int() | float():
+476                return self.set(key, str(value))
+477            case bytes() | str():
+478                new_field = MultiPartFormField.make(key)
+479                new_field.content = always_bytes(value)
+480            case IOBase():
+481                new_field = MultiPartFormField.from_file(key, value)
+482            case None:
+483                self.del_all(key)
+484                return
+485            case _:
+486                raise TypeError("Wrong type was passed to MultiPartForm.set")
+487
+488        for i, field in enumerate(self.fields):
+489            if field.name == key:
+490                self.fields[i] = new_field
+491                return
+492
+493        self.append(new_field)
+
+ + + + +
+
+ +
+ + def + setdefault( self, key: str, default: MultiPartFormField | None = None) -> MultiPartFormField: + + + +
+ +
495    def setdefault(
+496        self, key: str, default: MultiPartFormField | None = None
+497    ) -> MultiPartFormField:
+498        found = self.get(key)
+499        if found is None:
+500            default = default or MultiPartFormField.make(key)
+501            self[key] = default
+502            return default
+503
+504        return found
+
+ + + + +
+
+ +
+ + def + insert( self, index: int, value: MultiPartFormField) -> None: + + + +
+ +
544    def insert(self, index: int, value: MultiPartFormField) -> None:
+545        """
+546        Insert an additional value for the given key at the specified position.
+547        """
+548        self.fields.insert(index, value)
+
+ + +

Insert an additional value for the given key at the specified position.

+
+ + +
+
+ +
+ + def + append(self, value: MultiPartFormField) -> None: + + + +
+ +
550    def append(self, value: MultiPartFormField) -> None:
+551        self.fields.append(value)
+
+ + + + +
+
+
Inherited Members
+
+
collections.abc.Mapping
+
get
+
items
+
keys
+
values
+ +
+
+
+
+
+ +
+ + class + MultiPartFormField: + + + +
+ +
 86class MultiPartFormField:
+ 87    """
+ 88    This class represents a field in a multipart/form-data request.
+ 89
+ 90    It provides functionalities to create form fields from various inputs like raw body parts,
+ 91    files and manual construction with name, filename, body, and content type.
+ 92
+ 93    It also offers properties and methods to interact with the form field's headers and content.
+ 94
+ 95    Raises:
+ 96        StopIteration: Raised when the specified Content-Disposition header is not found or could not be parsed.
+ 97
+ 98    Returns:
+ 99        MultiPartFormField: An instance of the class representing a form field in a multipart/form-data request.
+100    """
+101
+102    headers: CaseInsensitiveDict[str]
+103    content: bytes
+104    encoding: str
+105
+106    def __init__(
+107        self,
+108        headers: CaseInsensitiveDict[str],
+109        content: bytes = b"",
+110        encoding: str = "utf-8",
+111    ):
+112        self.headers = headers
+113        self.content = content
+114        self.encoding = encoding
+115
+116    @classmethod
+117    def from_body_part(cls, body_part: BodyPart):
+118        headers = cls._fix_headers(cast(Mapping[bytes, bytes], body_part.headers))
+119        return cls(headers, body_part.content, body_part.encoding)
+120
+121    @classmethod
+122    def make(
+123        cls,
+124        name: str,
+125        filename: str | None = None,
+126        body: bytes = b"",
+127        content_type: str | None = None,
+128        encoding: str = "utf-8",
+129    ) -> MultiPartFormField:
+130        # Ensure the form won't break if someone includes quotes
+131        escaped_name: str = escape_parameter(name)
+132
+133        # rfc7578  4.2. specifies that urlencoding shouldn't be applied to filename
+134        #   But most browsers still replaces the " character by %22 to avoid breaking the parameters quotation.
+135        escaped_filename: str | None = filename and escape_parameter(filename)
+136
+137        if content_type is None:
+138            content_type = get_mime(filename)
+139
+140        urlencoded_content_type = urllibquote(content_type)
+141
+142        disposition = f'form-data; name="{escaped_name}"'
+143        if filename is not None:
+144            # When the param is a file, add a filename MIME param and a content-type header
+145            disposition += f'; filename="{escaped_filename}"'
+146            headers = CaseInsensitiveDict(
+147                {
+148                    CONTENT_DISPOSITION_KEY: disposition,
+149                    CONTENT_TYPE_KEY: urlencoded_content_type,
+150                }
+151            )
+152        else:
+153            headers = CaseInsensitiveDict(
+154                {
+155                    CONTENT_DISPOSITION_KEY: disposition,
+156                }
+157            )
+158
+159        return cls(headers, body, encoding)
+160
+161    # TODO: Rewrite request_toolbelt multipart parser to get rid of encoding.
+162    @staticmethod
+163    def from_file(
+164        name: str,
+165        file: TextIOWrapper | BufferedReader | str | IOBase,
+166        filename: str | None = None,
+167        content_type: str | None = None,
+168        encoding: str | None = None,
+169    ):
+170        if isinstance(file, str):
+171            file = open(file, mode="rb")
+172
+173        if filename is None:
+174            match file:
+175                case TextIOWrapper() | BufferedReader():
+176                    filename = os.path.basename(file.name)
+177                case _:
+178                    filename = name
+179
+180        # Guess the MIME content-type from the file extension
+181        if content_type is None:
+182            content_type = (
+183                mimetypes.guess_type(filename)[0] or "application/octet-stream"
+184            )
+185
+186        # Read the whole file into memory
+187        content: bytes
+188        match file:
+189            case TextIOWrapper():
+190                content = file.read().encode(file.encoding)
+191                # Override file.encoding if provided.
+192                encoding = encoding or file.encoding
+193            case BufferedReader() | IOBase():
+194                content = file.read()
+195
+196        instance = MultiPartFormField.make(
+197            name,
+198            filename=filename,
+199            body=content,
+200            content_type=content_type,
+201            encoding=encoding or "utf-8",
+202        )
+203
+204        file.close()
+205
+206        return instance
+207
+208    @staticmethod
+209    def __serialize_content(
+210        content: bytes, headers: Mapping[str | bytes, str | bytes]
+211    ) -> bytes:
+212        # Prepend content with headers
+213        merged_content: bytes = b""
+214        header_lines = (
+215            always_bytes(key) + b": " + always_bytes(value)
+216            for key, value in headers.items()
+217        )
+218        merged_content += b"\r\n".join(header_lines)
+219        merged_content += b"\r\n\r\n"
+220        merged_content += content
+221        return merged_content
+222
+223    def __bytes__(self) -> bytes:
+224        return self.__serialize_content(
+225            self.content,
+226            cast(Mapping[bytes | str, bytes | str], self.headers),
+227        )
+228    
+229    def __eq__(self, other) -> bool:
+230        match other:
+231            case MultiPartFormField() | bytes():
+232                return bytes(other) == bytes(self)
+233            case str():
+234                return other.encode("latin-1") == bytes(self)
+235        return False
+236
+237    def __hash__(self) -> int:
+238        return hash(bytes(self))
+239
+240    @staticmethod
+241    def _fix_headers(headers: Mapping[bytes, bytes]) -> CaseInsensitiveDict[str]:
+242        # Fix the headers key by converting them to strings
+243        # https://github.com/requests/toolbelt/pull/353
+244
+245        fixed_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict()
+246        for key, value in headers.items():
+247            fixed_headers[always_str(key)] = always_str(value.decode())
+248        return fixed_headers
+249
+250    # Unused for now
+251    # @staticmethod
+252    # def _unfix_headers(headers: Mapping[str, str]) -> CaseInsensitiveDict[bytes]:
+253    #     # Unfix the headers key by converting them to bytes
+254
+255    #     unfixed_headers: CaseInsensitiveDict[bytes] = CaseInsensitiveDict()
+256    #     for key, value in headers.items():
+257    #         unfixed_headers[always_bytes(key)] = always_bytes(value)  # type: ignore requests_toolbelt uses wrong types but it still works fine.
+258    #     return unfixed_headers
+259
+260    @property
+261    def text(self) -> str:
+262        return self.content.decode(self.encoding)
+263
+264    @property
+265    def content_type(self) -> str | None:
+266        return self.headers.get(CONTENT_TYPE_KEY)
+267
+268    @content_type.setter
+269    def content_type(self, content_type: str | None) -> None:
+270        headers = self.headers
+271        if content_type is None:
+272            del headers[CONTENT_TYPE_KEY]
+273        else:
+274            headers[CONTENT_TYPE_KEY] = content_type
+275
+276    def _parse_disposition(self) -> list[tuple[str, str]]:
+277        header_key = CONTENT_DISPOSITION_KEY
+278        header_value = self.headers[header_key]
+279        return parse_header(header_key, header_value)
+280
+281    def _unparse_disposition(self, parsed_header: list[tuple[str, str]]):
+282        unparsed = unparse_header_value(parsed_header)
+283        self.headers[CONTENT_DISPOSITION_KEY] = unparsed
+284
+285    def get_disposition_param(self, key: str) -> tuple[str, str | None] | None:
+286        """Get a param from the Content-Disposition header
+287
+288        Args:
+289            key (str): the param name
+290
+291        Raises:
+292            StopIteration: Raised when the param was not found.
+293
+294        Returns:
+295            tuple[str, str | None] | None: Returns the param as (key, value)
+296        """
+297        # Parse the Content-Disposition header
+298        parsed_disposition = self._parse_disposition()
+299        return find_header_param(parsed_disposition, key)
+300
+301    def set_disposition_param(self, key: str, value: str):
+302        """Set a Content-Type header parameter
+303
+304        Args:
+305            key (str): The parameter name
+306            value (str): The parameter value
+307        """
+308        parsed = self._parse_disposition()
+309        updated = update_header_param(parsed, key, value)
+310        self._unparse_disposition(cast(list[tuple[str, str]], updated))
+311
+312    @property
+313    def name(self) -> str:
+314        """Get the Content-Disposition header name parameter
+315
+316        Returns:
+317            str: The Content-Disposition header name parameter value
+318        """
+319        # Assume name is always present
+320        return cast(tuple[str, str], self.get_disposition_param("name"))[1]
+321
+322    @name.setter
+323    def name(self, value: str):
+324        self.set_disposition_param("name", value)
+325
+326    @property
+327    def filename(self) -> str | None:
+328        """Get the Content-Disposition header filename parameter
+329
+330        Returns:
+331            str | None: The Content-Disposition header filename parameter value
+332        """
+333        param = self.get_disposition_param("filename")
+334        return param and param[1]
+335
+336    @filename.setter
+337    def filename(self, value: str):
+338        self.set_disposition_param("filename", value)
+
+ + +

This class represents a field in a multipart/form-data request.

+ +

It provides functionalities to create form fields from various inputs like raw body parts, +files and manual construction with name, filename, body, and content type.

+ +

It also offers properties and methods to interact with the form field's headers and content.

+ +

Raises: + StopIteration: Raised when the specified Content-Disposition header is not found or could not be parsed.

+ +

Returns: + MultiPartFormField: An instance of the class representing a form field in a multipart/form-data request.

+
+ + +
+ +
+ + MultiPartFormField( headers: requests.structures.CaseInsensitiveDict[str], content: bytes = b'', encoding: str = 'utf-8') + + + +
+ +
106    def __init__(
+107        self,
+108        headers: CaseInsensitiveDict[str],
+109        content: bytes = b"",
+110        encoding: str = "utf-8",
+111    ):
+112        self.headers = headers
+113        self.content = content
+114        self.encoding = encoding
+
+ + + + +
+
+
+ headers: requests.structures.CaseInsensitiveDict[str] + + +
+ + + + +
+
+
+ content: bytes + + +
+ + + + +
+
+
+ encoding: str + + +
+ + + + +
+
+ +
+
@classmethod
+ + def + from_body_part(cls, body_part: requests_toolbelt.multipart.decoder.BodyPart): + + + +
+ +
116    @classmethod
+117    def from_body_part(cls, body_part: BodyPart):
+118        headers = cls._fix_headers(cast(Mapping[bytes, bytes], body_part.headers))
+119        return cls(headers, body_part.content, body_part.encoding)
+
+ + + + +
+
+ +
+
@classmethod
+ + def + make( cls, name: str, filename: str | None = None, body: bytes = b'', content_type: str | None = None, encoding: str = 'utf-8') -> MultiPartFormField: + + + +
+ +
121    @classmethod
+122    def make(
+123        cls,
+124        name: str,
+125        filename: str | None = None,
+126        body: bytes = b"",
+127        content_type: str | None = None,
+128        encoding: str = "utf-8",
+129    ) -> MultiPartFormField:
+130        # Ensure the form won't break if someone includes quotes
+131        escaped_name: str = escape_parameter(name)
+132
+133        # rfc7578  4.2. specifies that urlencoding shouldn't be applied to filename
+134        #   But most browsers still replaces the " character by %22 to avoid breaking the parameters quotation.
+135        escaped_filename: str | None = filename and escape_parameter(filename)
+136
+137        if content_type is None:
+138            content_type = get_mime(filename)
+139
+140        urlencoded_content_type = urllibquote(content_type)
+141
+142        disposition = f'form-data; name="{escaped_name}"'
+143        if filename is not None:
+144            # When the param is a file, add a filename MIME param and a content-type header
+145            disposition += f'; filename="{escaped_filename}"'
+146            headers = CaseInsensitiveDict(
+147                {
+148                    CONTENT_DISPOSITION_KEY: disposition,
+149                    CONTENT_TYPE_KEY: urlencoded_content_type,
+150                }
+151            )
+152        else:
+153            headers = CaseInsensitiveDict(
+154                {
+155                    CONTENT_DISPOSITION_KEY: disposition,
+156                }
+157            )
+158
+159        return cls(headers, body, encoding)
+
+ + + + +
+
+ +
+
@staticmethod
+ + def + from_file( name: str, file: _io.TextIOWrapper | _io.BufferedReader | str | io.IOBase, filename: str | None = None, content_type: str | None = None, encoding: str | None = None): + + + +
+ +
162    @staticmethod
+163    def from_file(
+164        name: str,
+165        file: TextIOWrapper | BufferedReader | str | IOBase,
+166        filename: str | None = None,
+167        content_type: str | None = None,
+168        encoding: str | None = None,
+169    ):
+170        if isinstance(file, str):
+171            file = open(file, mode="rb")
+172
+173        if filename is None:
+174            match file:
+175                case TextIOWrapper() | BufferedReader():
+176                    filename = os.path.basename(file.name)
+177                case _:
+178                    filename = name
+179
+180        # Guess the MIME content-type from the file extension
+181        if content_type is None:
+182            content_type = (
+183                mimetypes.guess_type(filename)[0] or "application/octet-stream"
+184            )
+185
+186        # Read the whole file into memory
+187        content: bytes
+188        match file:
+189            case TextIOWrapper():
+190                content = file.read().encode(file.encoding)
+191                # Override file.encoding if provided.
+192                encoding = encoding or file.encoding
+193            case BufferedReader() | IOBase():
+194                content = file.read()
+195
+196        instance = MultiPartFormField.make(
+197            name,
+198            filename=filename,
+199            body=content,
+200            content_type=content_type,
+201            encoding=encoding or "utf-8",
+202        )
+203
+204        file.close()
+205
+206        return instance
+
+ + + + +
+
+ +
+ text: str + + + +
+ +
260    @property
+261    def text(self) -> str:
+262        return self.content.decode(self.encoding)
+
+ + + + +
+
+ +
+ content_type: str | None + + + +
+ +
264    @property
+265    def content_type(self) -> str | None:
+266        return self.headers.get(CONTENT_TYPE_KEY)
+
+ + + + +
+
+ +
+ + def + get_disposition_param(self, key: str) -> tuple[str, str | None] | None: + + + +
+ +
285    def get_disposition_param(self, key: str) -> tuple[str, str | None] | None:
+286        """Get a param from the Content-Disposition header
+287
+288        Args:
+289            key (str): the param name
+290
+291        Raises:
+292            StopIteration: Raised when the param was not found.
+293
+294        Returns:
+295            tuple[str, str | None] | None: Returns the param as (key, value)
+296        """
+297        # Parse the Content-Disposition header
+298        parsed_disposition = self._parse_disposition()
+299        return find_header_param(parsed_disposition, key)
+
+ + +

Get a param from the Content-Disposition header

+ +

Args: + key (str): the param name

+ +

Raises: + StopIteration: Raised when the param was not found.

+ +

Returns: + tuple[str, str | None] | None: Returns the param as (key, value)

+
+ + +
+
+ +
+ + def + set_disposition_param(self, key: str, value: str): + + + +
+ +
301    def set_disposition_param(self, key: str, value: str):
+302        """Set a Content-Type header parameter
+303
+304        Args:
+305            key (str): The parameter name
+306            value (str): The parameter value
+307        """
+308        parsed = self._parse_disposition()
+309        updated = update_header_param(parsed, key, value)
+310        self._unparse_disposition(cast(list[tuple[str, str]], updated))
+
+ + +

Set a Content-Type header parameter

+ +

Args: + key (str): The parameter name + value (str): The parameter value

+
+ + +
+
+ +
+ name: str + + + +
+ +
312    @property
+313    def name(self) -> str:
+314        """Get the Content-Disposition header name parameter
+315
+316        Returns:
+317            str: The Content-Disposition header name parameter value
+318        """
+319        # Assume name is always present
+320        return cast(tuple[str, str], self.get_disposition_param("name"))[1]
+
+ + +

Get the Content-Disposition header name parameter

+ +

Returns: + str: The Content-Disposition header name parameter value

+
+ + +
+
+ +
+ filename: str | None + + + +
+ +
326    @property
+327    def filename(self) -> str | None:
+328        """Get the Content-Disposition header filename parameter
+329
+330        Returns:
+331            str | None: The Content-Disposition header filename parameter value
+332        """
+333        param = self.get_disposition_param("filename")
+334        return param and param[1]
+
+ + +

Get the Content-Disposition header filename parameter

+ +

Returns: + str | None: The Content-Disposition header filename parameter value

+
+ + +
+
+
+ +
+ + class + URLEncodedForm(_internal_mitmproxy.coretypes.multidict.MultiDict[bytes, bytes]): + + + +
+ +
27class URLEncodedForm(multidict.MultiDict[bytes, bytes]):
+28    def __init__(self, fields: Iterable[tuple[str | bytes, str | bytes]]) -> None:
+29        fields_converted_to_bytes: Iterable[tuple[bytes, bytes]] = (
+30            (
+31                always_bytes(key),
+32                always_bytes(val),
+33            )
+34            for (key, val) in fields
+35        )
+36        super().__init__(fields_converted_to_bytes)
+37
+38    def __setitem__(self, key: int | str | bytes, value: int | str | bytes) -> None:
+39        super().__setitem__(always_bytes(key), always_bytes(value))
+40
+41    def __getitem__(self, key: int | bytes | str) -> bytes:
+42        return super().__getitem__(always_bytes(key))
+
+ + +

A concrete MultiDict, storing its own data.

+
+ + +
+
Inherited Members
+
+
_internal_mitmproxy.coretypes.multidict.MultiDict
+
MultiDict
+
fields
+
get_state
+
set_state
+
from_state
+ +
+
_internal_mitmproxy.coretypes.multidict._MultiDict
+
get_all
+
set_all
+
add
+
insert
+
keys
+
values
+
items
+ +
+
collections.abc.MutableMapping
+
pop
+
popitem
+
clear
+
update
+
setdefault
+ +
+
_internal_mitmproxy.coretypes.serializable.Serializable
+
copy
+ +
+
collections.abc.Mapping
+
get
+ +
+
+
+
+
+ +
+ + class + FormSerializer(abc.ABC): + + + +
+ +
 49class FormSerializer(ABC):
+ 50    @abstractmethod
+ 51    def serialize(self, deserialized_body: Form, req: ObjectWithHeaders) -> bytes:
+ 52        """Serialize a parsed form to raw bytes
+ 53
+ 54        Args:
+ 55            deserialized_body (Form): The parsed form
+ 56            req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)
+ 57
+ 58        Returns:
+ 59            bytes: Form's raw bytes representation
+ 60        """
+ 61
+ 62    @abstractmethod
+ 63    def deserialize(self, body: bytes, req: ObjectWithHeaders) -> Form | None:
+ 64        """Parses the form from its raw bytes representation
+ 65
+ 66        Args:
+ 67            body (bytes): The form as bytes
+ 68            req (ObjectWithHeaders): The originating request  (used for multipart to get an up to date boundary from content-type)
+ 69
+ 70        Returns:
+ 71            Form | None: The parsed form
+ 72        """
+ 73
+ 74    @abstractmethod
+ 75    def get_empty_form(self, req: ObjectWithHeaders) -> Form:
+ 76        """Get an empty parsed form object
+ 77
+ 78        Args:
+ 79            req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)
+ 80
+ 81        Returns:
+ 82            Form: The empty form
+ 83        """
+ 84
+ 85    @abstractmethod
+ 86    def deserialized_type(self) -> type[Form]:
+ 87        """Gets the form concrete type
+ 88
+ 89        Returns:
+ 90            type[Form]: The form concrete type
+ 91        """
+ 92
+ 93    @abstractmethod
+ 94    def import_form(self, exported: ExportedForm, req: ObjectWithHeaders) -> Form:
+ 95        """Imports a form exported by a serializer
+ 96            Used to convert a form from a Content-Type to another
+ 97            Information may be lost in the process
+ 98
+ 99        Args:
+100            exported (ExportedForm): The exported form
+101            req: (ObjectWithHeaders): Used to get multipart boundary
+102
+103        Returns:
+104            Form: The form converted to this serializer's format
+105        """
+106
+107    @abstractmethod
+108    def export_form(self, source: Form) -> TupleExportedForm:
+109        """Formats a form so it can be imported by another serializer
+110            Information may be lost in the process
+111
+112        Args:
+113            form (Form): The form to export
+114
+115        Returns:
+116            ExportedForm: The exported form
+117        """
+
+ + +

Helper class that provides a standard way to create an ABC using +inheritance.

+
+ + +
+ +
+
@abstractmethod
+ + def + serialize( self, deserialized_body: Form, req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> bytes: + + + +
+ +
50    @abstractmethod
+51    def serialize(self, deserialized_body: Form, req: ObjectWithHeaders) -> bytes:
+52        """Serialize a parsed form to raw bytes
+53
+54        Args:
+55            deserialized_body (Form): The parsed form
+56            req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)
+57
+58        Returns:
+59            bytes: Form's raw bytes representation
+60        """
+
+ + +

Serialize a parsed form to raw bytes

+ +

Args: + deserialized_body (Form): The parsed form + req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)

+ +

Returns: + bytes: Form's raw bytes representation

+
+ + +
+
+ +
+
@abstractmethod
+ + def + deserialize( self, body: bytes, req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> Form | None: + + + +
+ +
62    @abstractmethod
+63    def deserialize(self, body: bytes, req: ObjectWithHeaders) -> Form | None:
+64        """Parses the form from its raw bytes representation
+65
+66        Args:
+67            body (bytes): The form as bytes
+68            req (ObjectWithHeaders): The originating request  (used for multipart to get an up to date boundary from content-type)
+69
+70        Returns:
+71            Form | None: The parsed form
+72        """
+
+ + +

Parses the form from its raw bytes representation

+ +

Args: + body (bytes): The form as bytes + req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)

+ +

Returns: + Form | None: The parsed form

+
+ + +
+
+ +
+
@abstractmethod
+ + def + get_empty_form( self, req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> Form: + + + +
+ +
74    @abstractmethod
+75    def get_empty_form(self, req: ObjectWithHeaders) -> Form:
+76        """Get an empty parsed form object
+77
+78        Args:
+79            req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)
+80
+81        Returns:
+82            Form: The empty form
+83        """
+
+ + +

Get an empty parsed form object

+ +

Args: + req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)

+ +

Returns: + Form: The empty form

+
+ + +
+
+ +
+
@abstractmethod
+ + def + deserialized_type(self) -> type[Form]: + + + +
+ +
85    @abstractmethod
+86    def deserialized_type(self) -> type[Form]:
+87        """Gets the form concrete type
+88
+89        Returns:
+90            type[Form]: The form concrete type
+91        """
+
+ + +

Gets the form concrete type

+ +

Returns: + type[Form]: The form concrete type

+
+ + +
+
+ +
+
@abstractmethod
+ + def + import_form( self, exported: tuple[tuple[bytes, bytes | None], ...], req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> Form: + + + +
+ +
 93    @abstractmethod
+ 94    def import_form(self, exported: ExportedForm, req: ObjectWithHeaders) -> Form:
+ 95        """Imports a form exported by a serializer
+ 96            Used to convert a form from a Content-Type to another
+ 97            Information may be lost in the process
+ 98
+ 99        Args:
+100            exported (ExportedForm): The exported form
+101            req: (ObjectWithHeaders): Used to get multipart boundary
+102
+103        Returns:
+104            Form: The form converted to this serializer's format
+105        """
+
+ + +

Imports a form exported by a serializer + Used to convert a form from a Content-Type to another + Information may be lost in the process

+ +

Args: + exported (ExportedForm): The exported form + req: (ObjectWithHeaders): Used to get multipart boundary

+ +

Returns: + Form: The form converted to this serializer's format

+
+ + +
+
+ +
+
@abstractmethod
+ + def + export_form( self, source: Form) -> tuple[tuple[bytes, bytes | None], ...]: + + + +
+ +
107    @abstractmethod
+108    def export_form(self, source: Form) -> TupleExportedForm:
+109        """Formats a form so it can be imported by another serializer
+110            Information may be lost in the process
+111
+112        Args:
+113            form (Form): The form to export
+114
+115        Returns:
+116            ExportedForm: The exported form
+117        """
+
+ + +

Formats a form so it can be imported by another serializer + Information may be lost in the process

+ +

Args: + form (Form): The form to export

+ +

Returns: + ExportedForm: The exported form

+
+ + +
+
+
+ +
+ + def + json_unescape(escaped: str) -> str: + + + +
+ +
55def json_unescape(escaped: str) -> str:
+56    def decode_match(match):
+57        return chr(int(match.group(1), 16))
+58
+59    return re.sub(r"\\u([0-9a-fA-F]{4})", decode_match, escaped)
+
+ + + + +
+
+ +
+ + def + json_unescape_bytes(escaped: str) -> bytes: + + + +
+ +
62def json_unescape_bytes(escaped: str) -> bytes:
+63    return json_unescape(escaped).encode("latin-1")
+
+ + + + +
+
+ +
+ + def + json_escape_bytes(data: bytes) -> str: + + + +
+ +
49def json_escape_bytes(data: bytes) -> str:
+50    printable = string.printable.encode("utf-8")
+51
+52    return "".join(chr(ch) if ch in printable else f"\\u{ch:04x}" for ch in data)
+
+ + + + +
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/java.html b/docs/public/api/pyscalpel/java.html new file mode 100644 index 00000000..2cd8cc85 --- /dev/null +++ b/docs/public/api/pyscalpel/java.html @@ -0,0 +1,859 @@ + + + + + + + + + pyscalpel.java + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.java

+ +

This module declares type definitions used for Java objects.

+ +

If you are a normal user, you should probably never have to manipulate these objects yourself.

+
+ + + + + +
 1"""
+ 2    This module declares type definitions used for Java objects.
+ 3    
+ 4    If you are a normal user, you should probably never have to manipulate these objects yourself.
+ 5"""
+ 6from .bytes import JavaBytes
+ 7from .import_java import import_java
+ 8from .object import JavaClass, JavaObject
+ 9from . import burp
+10from . import scalpel_types
+11
+12__all__ = [
+13    "burp",
+14    "scalpel_types",
+15    "import_java",
+16    "JavaObject",
+17    "JavaBytes",
+18    "JavaClass",
+19]
+
+ + +
+
+ +
+ + def + import_java( module: str, name: str, expected_type: Type[~ExpectedObject] = <class 'JavaObject'>) -> ~ExpectedObject: + + + +
+ +
19def import_java(
+20    module: str, name: str, expected_type: Type[ExpectedObject] = JavaObject
+21) -> ExpectedObject:
+22    """Import a Java class using Python's import mechanism.
+23
+24    :param module: The module to import from. (e.g. "java.lang")
+25    :param name: The name of the class to import. (e.g. "String")
+26    :param expected_type: The expected type of the class. (e.g. JavaObject)
+27    :return: The imported class.
+28    """
+29    if _is_pdoc() or os.environ.get("_DO_NOT_IMPORT_JAVA") is not None:
+30        return None  # type: ignore
+31    try:  # pragma: no cover
+32        module = __import__(module, fromlist=[name])
+33        return getattr(module, name)
+34    except ImportError as exc:  # pragma: no cover
+35        raise ImportError(f"Could not import Java class {name}") from exc
+
+ + +

Import a Java class using Python's import mechanism.

+ +
#  Parameters
+ +
    +
  • module: The module to import from. (e.g. "java.lang")
  • +
  • name: The name of the class to import. (e.g. "String")
  • +
  • expected_type: The expected type of the class. (e.g. JavaObject)
  • +
+ +
#  Returns
+ +
+

The imported class.

+
+
+ + +
+
+ +
+ + class + JavaObject(typing.Protocol): + + + +
+ +
10class JavaObject(Protocol, metaclass=ABCMeta):
+11    """generated source for class Object"""
+12
+13    @abstractmethod
+14    def __init__(self):
+15        """generated source for method __init__"""
+16
+17    @abstractmethod
+18    def getClass(self) -> JavaClass:
+19        """generated source for method getClass"""
+20
+21    @abstractmethod
+22    def hashCode(self) -> int:
+23        """generated source for method hashCode"""
+24
+25    @abstractmethod
+26    def equals(self, obj) -> bool:
+27        """generated source for method equals"""
+28
+29    @abstractmethod
+30    def clone(self) -> JavaObject:
+31        """generated source for method clone"""
+32
+33    @abstractmethod
+34    def __str__(self) -> str:
+35        """generated source for method toString"""
+36
+37    @abstractmethod
+38    def notify(self) -> None:
+39        """generated source for method notify"""
+40
+41    @abstractmethod
+42    def notifyAll(self) -> None:
+43        """generated source for method notifyAll"""
+44
+45    @abstractmethod
+46    @overload
+47    def wait(self) -> None:
+48        """generated source for method wait"""
+49
+50    @abstractmethod
+51    @overload
+52    def wait(self, arg0: int) -> None:
+53        """generated source for method wait_0"""
+54
+55    @abstractmethod
+56    @overload
+57    def wait(self, timeoutMillis: int, nanos: int) -> None:
+58        """generated source for method wait_1"""
+59
+60    @abstractmethod
+61    def finalize(self) -> None:
+62        """generated source for method finalize"""
+
+ + +

generated source for class Object

+
+ + +
+ +
+
@abstractmethod
+ + def + getClass(self) -> JavaClass: + + + +
+ +
17    @abstractmethod
+18    def getClass(self) -> JavaClass:
+19        """generated source for method getClass"""
+
+ + +

generated source for method getClass

+
+ + +
+
+ +
+
@abstractmethod
+ + def + hashCode(self) -> int: + + + +
+ +
21    @abstractmethod
+22    def hashCode(self) -> int:
+23        """generated source for method hashCode"""
+
+ + +

generated source for method hashCode

+
+ + +
+
+ +
+
@abstractmethod
+ + def + equals(self, obj) -> bool: + + + +
+ +
25    @abstractmethod
+26    def equals(self, obj) -> bool:
+27        """generated source for method equals"""
+
+ + +

generated source for method equals

+
+ + +
+
+ +
+
@abstractmethod
+ + def + clone(self) -> JavaObject: + + + +
+ +
29    @abstractmethod
+30    def clone(self) -> JavaObject:
+31        """generated source for method clone"""
+
+ + +

generated source for method clone

+
+ + +
+
+ +
+
@abstractmethod
+ + def + notify(self) -> None: + + + +
+ +
37    @abstractmethod
+38    def notify(self) -> None:
+39        """generated source for method notify"""
+
+ + +

generated source for method notify

+
+ + +
+
+ +
+
@abstractmethod
+ + def + notifyAll(self) -> None: + + + +
+ +
41    @abstractmethod
+42    def notifyAll(self) -> None:
+43        """generated source for method notifyAll"""
+
+ + +

generated source for method notifyAll

+
+ + +
+
+ +
+ + def + wait(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + finalize(self) -> None: + + + +
+ +
60    @abstractmethod
+61    def finalize(self) -> None:
+62        """generated source for method finalize"""
+
+ + +

generated source for method finalize

+
+ + +
+
+
+ +
+ + class + JavaBytes(list[int]): + + + +
+ +
5class JavaBytes(list[int]):
+6    __metaclass__ = ABCMeta
+
+ + +

Built-in mutable sequence.

+ +

If no argument is given, the constructor creates a new empty list. +The argument must be an iterable if specified.

+
+ + +
+
Inherited Members
+
+
builtins.list
+
list
+
clear
+
copy
+
append
+
insert
+
extend
+
pop
+
remove
+
index
+
count
+
reverse
+
sort
+ +
+
+
+
+
+ +
+ + class + JavaClass(pyscalpel.java.JavaObject): + + + +
+ +
65class JavaClass(JavaObject, metaclass=ABCMeta):
+66    pass
+
+ + +

generated source for class Object

+
+ + +
+
Inherited Members
+
+ +
+
+
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/java/burp.html b/docs/public/api/pyscalpel/java/burp.html new file mode 100644 index 00000000..15b4d05e --- /dev/null +++ b/docs/public/api/pyscalpel/java/burp.html @@ -0,0 +1,4780 @@ + + + + + + + + + pyscalpel.java.burp + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.java.burp

+ +

This module exposes Java objects from Burp's extensions API

+ +

If you are a normal user, you should probably never have to manipulate these objects yourself.

+
+ + + + + +
 1"""
+ 2    This module exposes Java objects from Burp's extensions API
+ 3    
+ 4    If you are a normal user, you should probably never have to manipulate these objects yourself.
+ 5"""
+ 6from .byte_array import IByteArray, ByteArray
+ 7from .http_header import IHttpHeader, HttpHeader
+ 8from .http_message import IHttpMessage
+ 9from .http_request import IHttpRequest, HttpRequest
+10from .http_response import IHttpResponse, HttpResponse
+11from .http_parameter import IHttpParameter, HttpParameter
+12from .http_service import IHttpService, HttpService
+13from .http_request_response import IHttpRequestResponse
+14from .http import IHttp
+15from .logging import Logging
+16
+17__all__ = [
+18    "IHttp",
+19    "IHttpRequest",
+20    "HttpRequest",
+21    "IHttpResponse",
+22    "HttpResponse",
+23    "IHttpRequestResponse",
+24    "IHttpHeader",
+25    "HttpHeader",
+26    "IHttpMessage",
+27    "IHttpParameter",
+28    "HttpParameter",
+29    "IHttpService",
+30    "HttpService",
+31    "IByteArray",
+32    "ByteArray",
+33    "Logging",
+34]
+
+ + +
+
+ +
+ + class + IHttp(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
+ +
12class IHttp(JavaObject, Protocol, metaclass=ABCMeta):  # pragma: no cover
+13    """generated source for interface Http"""
+14
+15    __metaclass__ = ABCMeta
+16
+17    @abstractmethod
+18    def sendRequest(self, request: IHttpRequest) -> IHttpRequestResponse:
+19        ...
+
+ + +

generated source for interface Http

+
+ + +
+ +
+
@abstractmethod
+ + def + sendRequest( self, request: IHttpRequest) -> IHttpRequestResponse: + + + +
+ +
17    @abstractmethod
+18    def sendRequest(self, request: IHttpRequest) -> IHttpRequestResponse:
+19        ...
+
+ + + + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+ +
+ + class + IHttpRequest(pyscalpel.java.burp.IHttpMessage, typing.Protocol): + + + +
+ +
 20class IHttpRequest(IHttpMessage, Protocol):  # pragma: no cover
+ 21    """generated source for interface HttpRequest"""
+ 22
+ 23    #      * HTTP service for the request.
+ 24    #      *
+ 25    #      * @return An {@link HttpService} object containing details of the HTTP service.
+ 26    #
+ 27
+ 28    @abstractmethod
+ 29    def httpService(self) -> IHttpService:
+ 30        """generated source for method httpService"""
+ 31
+ 32    #
+ 33    #      * URL for the request.
+ 34    #      * If the request is malformed, then a {@link MalformedRequestException} is thrown.
+ 35    #      *
+ 36    #      * @return The URL in the request.
+ 37    #      * @throws MalformedRequestException if request is malformed.
+ 38    #
+ 39
+ 40    @abstractmethod
+ 41    def url(self) -> str:
+ 42        """generated source for method url"""
+ 43
+ 44    #
+ 45    #      * HTTP method for the request.
+ 46    #      * If the request is malformed, then a {@link MalformedRequestException} is thrown.
+ 47    #      *
+ 48    #      * @return The HTTP method used in the request.
+ 49    #      * @throws MalformedRequestException if request is malformed.
+ 50    #
+ 51
+ 52    @abstractmethod
+ 53    def method(self) -> str:
+ 54        """generated source for method method"""
+ 55
+ 56    #
+ 57    #      * Path and File for the request.
+ 58    #      * If the request is malformed, then a {@link MalformedRequestException} is thrown.
+ 59    #      *
+ 60    #      * @return the path and file in the request
+ 61    #      * @throws MalformedRequestException if request is malformed.
+ 62    #
+ 63
+ 64    @abstractmethod
+ 65    def path(self) -> str:
+ 66        """generated source for method path"""
+ 67
+ 68    #
+ 69    #      * HTTP Version text parsed from the request line for HTTP 1 messages.
+ 70    #      * HTTP 2 messages will return "HTTP/2"
+ 71    #      *
+ 72    #      * @return Version string
+ 73    #
+ 74
+ 75    @abstractmethod
+ 76    def httpVersion(self) -> str | None:
+ 77        """generated source for method httpVersion"""
+ 78
+ 79    #
+ 80    #      * {@inheritDoc}
+ 81    #
+ 82
+ 83    @abstractmethod
+ 84    def headers(self) -> list[IHttpHeader]:
+ 85        """generated source for method headers"""
+ 86
+ 87    #
+ 88    #      * @return The detected content type of the request.
+ 89    #
+ 90
+ 91    @abstractmethod
+ 92    def contentType(self) -> JavaObject:
+ 93        """generated source for method contentType"""
+ 94
+ 95    #
+ 96    #      * @return The parameters contained in the request.
+ 97    #
+ 98
+ 99    @abstractmethod
+100    def parameters(self) -> list[IHttpParameter]:
+101        """generated source for method parameters"""
+102
+103    #
+104    #      * {@inheritDoc}
+105    #
+106
+107    @abstractmethod
+108    def body(self) -> IByteArray:
+109        """generated source for method body"""
+110
+111    #
+112    #      * {@inheritDoc}
+113    #
+114
+115    @abstractmethod
+116    def bodyToString(self) -> str:
+117        """generated source for method bodyToString"""
+118
+119    #
+120    #      * {@inheritDoc}
+121    #
+122
+123    @abstractmethod
+124    def bodyOffset(self) -> int:
+125        """generated source for method bodyOffset"""
+126
+127    #
+128    #      * {@inheritDoc}
+129    #
+130
+131    @abstractmethod
+132    def markers(self):
+133        """generated source for method markers"""
+134
+135    #
+136    #      * {@inheritDoc}
+137    #
+138
+139    @abstractmethod
+140    def toByteArray(self) -> IByteArray:
+141        """generated source for method toByteArray"""
+142
+143    #
+144    #      * {@inheritDoc}
+145    #
+146
+147    @abstractmethod
+148    def __str__(self) -> str:
+149        """generated source for method toString"""
+150
+151    #
+152    #      * Create a copy of the {@code HttpRequest} in temporary file.<br>
+153    #      * This method is used to save the {@code HttpRequest} object to a temporary file,
+154    #      * so that it is no longer held in memory. Extensions can use this method to convert
+155    #      * {@code HttpRequest} objects into a form suitable for long-term usage.
+156    #      *
+157    #      * @return A new {@code ByteArray} instance stored in temporary file.
+158    #
+159
+160    @abstractmethod
+161    def copyToTempFile(self) -> IHttpRequest:
+162        """generated source for method copyToTempFile"""
+163
+164    #
+165    #      * Create a copy of the {@code HttpRequest} with the new service.
+166    #      *
+167    #      * @param service An {@link HttpService} reference to add.
+168    #      *
+169    #      * @return A new {@code HttpRequest} instance.
+170    #
+171
+172    @abstractmethod
+173    def withService(self, service: IHttpService) -> IHttpRequest:
+174        """generated source for method withService"""
+175
+176    #
+177    #      * Create a copy of the {@code HttpRequest} with the new path.
+178    #      *
+179    #      * @param path The path to use.
+180    #      *
+181    #      * @return A new {@code HttpRequest} instance with updated path.
+182    #
+183
+184    @abstractmethod
+185    def withPath(self, path: str) -> IHttpRequest:
+186        """generated source for method withPath"""
+187
+188    #
+189    #      * Create a copy of the {@code HttpRequest} with the new method.
+190    #      *
+191    #      * @param method the method to use
+192    #      *
+193    #      * @return a new {@code HttpRequest} instance with updated method.
+194    #
+195
+196    @abstractmethod
+197    def withMethod(self, method: str) -> IHttpRequest:
+198        """generated source for method withMethod"""
+199
+200    #
+201    #      * Create a copy of the {@code HttpRequest} with the added HTTP parameters.
+202    #      *
+203    #      * @param parameters HTTP parameters to add.
+204    #      *
+205    #      * @return A new {@code HttpRequest} instance.
+206    #
+207
+208    @abstractmethod
+209    def withAddedParameters(self, parameters: Iterable[IHttpParameter]) -> IHttpRequest:
+210        """generated source for method withAddedParameters"""
+211
+212    #
+213    #      * Create a copy of the {@code HttpRequest} with the added HTTP parameters.
+214    #      *
+215    #      * @param parameters HTTP parameters to add.
+216    #      *
+217    #      * @return A new {@code HttpRequest} instance.
+218    #
+219
+220    @abstractmethod
+221    def withAddedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
+222        """generated source for method withAddedParameters_0"""
+223
+224    #
+225    #      * Create a copy of the {@code HttpRequest} with the removed HTTP parameters.
+226    #      *
+227    #      * @param parameters HTTP parameters to remove.
+228    #      *
+229    #      * @return A new {@code HttpRequest} instance.
+230    #
+231
+232    @abstractmethod
+233    def withRemovedParameters(
+234        self, parameters: Iterable[IHttpParameter]
+235    ) -> IHttpRequest:
+236        """generated source for method withRemovedParameters"""
+237
+238    #
+239    #      * Create a copy of the {@code HttpRequest} with the removed HTTP parameters.
+240    #      *
+241    #      * @param parameters HTTP parameters to remove.
+242    #      *
+243    #      * @return A new {@code HttpRequest} instance.
+244    #
+245
+246    @abstractmethod
+247    def withRemovedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
+248        """generated source for method withRemovedParameters_0"""
+249
+250    #
+251    #      * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.<br>
+252    #      * If a parameter does not exist in the request, a new one will be added.
+253    #      *
+254    #      * @param parameters HTTP parameters to update.
+255    #      *
+256    #      * @return A new {@code HttpRequest} instance.
+257    #
+258
+259    @abstractmethod
+260    def withUpdatedParameters(self, parameters: list[IHttpParameter]) -> IHttpRequest:
+261        """generated source for method withUpdatedParameters"""
+262
+263    #
+264    #      * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.<br>
+265    #      * If a parameter does not exist in the request, a new one will be added.
+266    #      *
+267    #      * @param parameters HTTP parameters to update.
+268    #      *
+269    #      * @return A new {@code HttpRequest} instance.
+270    #
+271
+272    @abstractmethod
+273    def withUpdatedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
+274        """generated source for method withUpdatedParameters_0"""
+275
+276    #
+277    #      * Create a copy of the {@code HttpRequest} with the transformation applied.
+278    #      *
+279    #      * @param transformation Transformation to apply.
+280    #      *
+281    #      * @return A new {@code HttpRequest} instance.
+282    #
+283
+284    @abstractmethod
+285    def withTransformationApplied(self, transformation) -> IHttpRequest:
+286        """generated source for method withTransformationApplied"""
+287
+288    #
+289    #      * Create a copy of the {@code HttpRequest} with the updated body.<br>
+290    #      * Updates Content-Length header.
+291    #      *
+292    #      * @param body the new body for the request
+293    #      *
+294    #      * @return A new {@code HttpRequest} instance.
+295    #
+296
+297    @abstractmethod
+298    def withBody(self, body) -> IHttpRequest:
+299        """generated source for method withBody"""
+300
+301    #
+302    #      * Create a copy of the {@code HttpRequest} with the updated body.<br>
+303    #      * Updates Content-Length header.
+304    #      *
+305    #      * @param body the new body for the request
+306    #      *
+307    #      * @return A new {@code HttpRequest} instance.
+308    #
+309
+310    @abstractmethod
+311    def withBody_0(self, body: IByteArray) -> IHttpRequest:
+312        """generated source for method withBody_0"""
+313
+314    #
+315    #      * Create a copy of the {@code HttpRequest} with the added header.
+316    #      *
+317    #      * @param name  The name of the header.
+318    #      * @param value The value of the header.
+319    #      *
+320    #      * @return The updated HTTP request with the added header.
+321    #
+322
+323    @abstractmethod
+324    def withAddedHeader(self, name: str, value: str) -> IHttpRequest:
+325        """generated source for method withAddedHeader"""
+326
+327    #
+328    #      * Create a copy of the {@code HttpRequest} with the added header.
+329    #      *
+330    #      * @param header The {@link HttpHeader} to add to the HTTP request.
+331    #      *
+332    #      * @return The updated HTTP request with the added header.
+333    #
+334
+335    @abstractmethod
+336    def withAddedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
+337        """generated source for method withAddedHeader_0"""
+338
+339    #
+340    #      * Create a copy of the {@code HttpRequest} with the updated header.
+341    #      *
+342    #      * @param name  The name of the header to update the value of.
+343    #      * @param value The new value of the specified HTTP header.
+344    #      *
+345    #      * @return The updated request containing the updated header.
+346    #
+347
+348    @abstractmethod
+349    def withUpdatedHeader(self, name: str, value: str) -> IHttpRequest:
+350        """generated source for method withUpdatedHeader"""
+351
+352    #
+353    #      * Create a copy of the {@code HttpRequest} with the updated header.
+354    #      *
+355    #      * @param header The {@link HttpHeader} to update containing the new value.
+356    #      *
+357    #      * @return The updated request containing the updated header.
+358    #
+359
+360    @abstractmethod
+361    def withUpdatedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
+362        """generated source for method withUpdatedHeader_0"""
+363
+364    #
+365    #      * Removes an existing HTTP header from the current request.
+366    #      *
+367    #      * @param name The name of the HTTP header to remove from the request.
+368    #      *
+369    #      * @return The updated request containing the removed header.
+370    #
+371
+372    @abstractmethod
+373    def withRemovedHeader(self, name: str) -> IHttpRequest:
+374        """generated source for method withRemovedHeader"""
+375
+376    #
+377    #      * Removes an existing HTTP header from the current request.
+378    #      *
+379    #      * @param header The {@link HttpHeader} to remove from the request.
+380    #      *
+381    #      * @return The updated request containing the removed header.
+382    #
+383
+384    @abstractmethod
+385    def withRemovedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
+386        """generated source for method withRemovedHeader_0"""
+387
+388    #
+389    #      * Create a copy of the {@code HttpRequest} with the added markers.
+390    #      *
+391    #      * @param markers Request markers to add.
+392    #      *
+393    #      * @return A new {@code MarkedHttpRequestResponse} instance.
+394    #
+395
+396    @abstractmethod
+397    def withMarkers(self, markers) -> IHttpRequest:
+398        """generated source for method withMarkers"""
+399
+400    #
+401    #      * Create a copy of the {@code HttpRequest} with the added markers.
+402    #      *
+403    #      * @param markers Request markers to add.
+404    #      *
+405    #      * @return A new {@code MarkedHttpRequestResponse} instance.
+406    #
+407
+408    @abstractmethod
+409    def withMarkers_0(self, *markers) -> IHttpRequest:
+410        """generated source for method withMarkers_0"""
+411
+412    #
+413    #      * Create a copy of the {@code HttpRequest} with added default headers.
+414    #      *
+415    #      * @return a new (@code HttpRequest) with added default headers
+416    #
+417
+418    @abstractmethod
+419    def withDefaultHeaders(self) -> IHttpRequest:
+420        """generated source for method withDefaultHeaders"""
+421
+422    #
+423    #      * Create a new empty instance of {@link HttpRequest}.<br>
+424    #      *
+425    #      * @².
+426    #
+427
+428    @abstractmethod
+429    @overload
+430    def httpRequest(self, request: IByteArray | str) -> IHttpRequest:
+431        """generated source for method httpRequest"""
+432
+433    @abstractmethod
+434    @overload
+435    def httpRequest(self, service: IHttpService, req: IByteArray | str) -> IHttpRequest:
+436        """generated source for method httpRequest"""
+437
+438    #
+439    #      * Create a new instance of {@link HttpRequest}.<br>
+440    #      *
+441    #      *
+442    #      * @².
+443    #
+444    #
+445
+446    @abstractmethod
+447    def httpRequestFromUrl(self, url: str) -> IHttpRequest:
+448        """generated source for method httpRequestFromUrl"""
+449
+450    #
+451    #      * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.<br>
+452    #      *
+453    #      * @param service An HTTP service for the request.
+454    #      * @param headers A list of HTTP 2 headers.
+455    #      * @param body    A body of the HTTP 2 request.
+456    #      *
+457    #      * @².
+458    #
+459
+460    @abstractmethod
+461    def http2Request(
+462        self, service: IHttpService, headers: Iterable[IHttpHeader], body: IByteArray
+463    ) -> IHttpRequest:
+464        """generated source for method http2Request"""
+465
+466    #
+467    #      * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.<br>
+468    #      *
+469    #      * @param service An HTTP service for the request.
+470    #      * @param headers A list of HTTP 2 headers.
+471    #      * @param body    A body of the HTTP 2 request.
+472    #      *
+473    #      * @².
+474    #
+
+ + +

generated source for interface HttpRequest

+
+ + +
+ +
+
@abstractmethod
+ + def + httpService(self) -> IHttpService: + + + +
+ +
28    @abstractmethod
+29    def httpService(self) -> IHttpService:
+30        """generated source for method httpService"""
+
+ + +

generated source for method httpService

+
+ + +
+
+ +
+
@abstractmethod
+ + def + url(self) -> str: + + + +
+ +
40    @abstractmethod
+41    def url(self) -> str:
+42        """generated source for method url"""
+
+ + +

generated source for method url

+
+ + +
+
+ +
+
@abstractmethod
+ + def + method(self) -> str: + + + +
+ +
52    @abstractmethod
+53    def method(self) -> str:
+54        """generated source for method method"""
+
+ + +

generated source for method method

+
+ + +
+
+ +
+
@abstractmethod
+ + def + path(self) -> str: + + + +
+ +
64    @abstractmethod
+65    def path(self) -> str:
+66        """generated source for method path"""
+
+ + +

generated source for method path

+
+ + +
+
+ +
+
@abstractmethod
+ + def + httpVersion(self) -> str | None: + + + +
+ +
75    @abstractmethod
+76    def httpVersion(self) -> str | None:
+77        """generated source for method httpVersion"""
+
+ + +

generated source for method httpVersion

+
+ + +
+
+ +
+
@abstractmethod
+ + def + headers(self) -> list[IHttpHeader]: + + + +
+ +
83    @abstractmethod
+84    def headers(self) -> list[IHttpHeader]:
+85        """generated source for method headers"""
+
+ + +

generated source for method headers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + contentType(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
91    @abstractmethod
+92    def contentType(self) -> JavaObject:
+93        """generated source for method contentType"""
+
+ + +

generated source for method contentType

+
+ + +
+
+ +
+
@abstractmethod
+ + def + parameters(self) -> list[IHttpParameter]: + + + +
+ +
 99    @abstractmethod
+100    def parameters(self) -> list[IHttpParameter]:
+101        """generated source for method parameters"""
+
+ + +

generated source for method parameters

+
+ + +
+
+ +
+
@abstractmethod
+ + def + body(self) -> IByteArray: + + + +
+ +
107    @abstractmethod
+108    def body(self) -> IByteArray:
+109        """generated source for method body"""
+
+ + +

generated source for method body

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyToString(self) -> str: + + + +
+ +
115    @abstractmethod
+116    def bodyToString(self) -> str:
+117        """generated source for method bodyToString"""
+
+ + +

generated source for method bodyToString

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyOffset(self) -> int: + + + +
+ +
123    @abstractmethod
+124    def bodyOffset(self) -> int:
+125        """generated source for method bodyOffset"""
+
+ + +

generated source for method bodyOffset

+
+ + +
+
+ +
+
@abstractmethod
+ + def + markers(self): + + + +
+ +
131    @abstractmethod
+132    def markers(self):
+133        """generated source for method markers"""
+
+ + +

generated source for method markers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + toByteArray(self) -> IByteArray: + + + +
+ +
139    @abstractmethod
+140    def toByteArray(self) -> IByteArray:
+141        """generated source for method toByteArray"""
+
+ + +

generated source for method toByteArray

+
+ + +
+
+ +
+
@abstractmethod
+ + def + copyToTempFile(self) -> IHttpRequest: + + + +
+ +
160    @abstractmethod
+161    def copyToTempFile(self) -> IHttpRequest:
+162        """generated source for method copyToTempFile"""
+
+ + +

generated source for method copyToTempFile

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withService( self, service: IHttpService) -> IHttpRequest: + + + +
+ +
172    @abstractmethod
+173    def withService(self, service: IHttpService) -> IHttpRequest:
+174        """generated source for method withService"""
+
+ + +

generated source for method withService

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withPath(self, path: str) -> IHttpRequest: + + + +
+ +
184    @abstractmethod
+185    def withPath(self, path: str) -> IHttpRequest:
+186        """generated source for method withPath"""
+
+ + +

generated source for method withPath

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withMethod(self, method: str) -> IHttpRequest: + + + +
+ +
196    @abstractmethod
+197    def withMethod(self, method: str) -> IHttpRequest:
+198        """generated source for method withMethod"""
+
+ + +

generated source for method withMethod

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withAddedParameters( self, parameters: Iterable[IHttpParameter]) -> IHttpRequest: + + + +
+ +
208    @abstractmethod
+209    def withAddedParameters(self, parameters: Iterable[IHttpParameter]) -> IHttpRequest:
+210        """generated source for method withAddedParameters"""
+
+ + +

generated source for method withAddedParameters

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withAddedParameters_0( self, *parameters: IHttpParameter) -> IHttpRequest: + + + +
+ +
220    @abstractmethod
+221    def withAddedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
+222        """generated source for method withAddedParameters_0"""
+
+ + +

generated source for method withAddedParameters_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withRemovedParameters( self, parameters: Iterable[IHttpParameter]) -> IHttpRequest: + + + +
+ +
232    @abstractmethod
+233    def withRemovedParameters(
+234        self, parameters: Iterable[IHttpParameter]
+235    ) -> IHttpRequest:
+236        """generated source for method withRemovedParameters"""
+
+ + +

generated source for method withRemovedParameters

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withRemovedParameters_0( self, *parameters: IHttpParameter) -> IHttpRequest: + + + +
+ +
246    @abstractmethod
+247    def withRemovedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
+248        """generated source for method withRemovedParameters_0"""
+
+ + +

generated source for method withRemovedParameters_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withUpdatedParameters( self, parameters: list[IHttpParameter]) -> IHttpRequest: + + + +
+ +
259    @abstractmethod
+260    def withUpdatedParameters(self, parameters: list[IHttpParameter]) -> IHttpRequest:
+261        """generated source for method withUpdatedParameters"""
+
+ + +

generated source for method withUpdatedParameters

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withUpdatedParameters_0( self, *parameters: IHttpParameter) -> IHttpRequest: + + + +
+ +
272    @abstractmethod
+273    def withUpdatedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
+274        """generated source for method withUpdatedParameters_0"""
+
+ + +

generated source for method withUpdatedParameters_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withTransformationApplied(self, transformation) -> IHttpRequest: + + + +
+ +
284    @abstractmethod
+285    def withTransformationApplied(self, transformation) -> IHttpRequest:
+286        """generated source for method withTransformationApplied"""
+
+ + +

generated source for method withTransformationApplied

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withBody(self, body) -> IHttpRequest: + + + +
+ +
297    @abstractmethod
+298    def withBody(self, body) -> IHttpRequest:
+299        """generated source for method withBody"""
+
+ + +

generated source for method withBody

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withBody_0( self, body: IByteArray) -> IHttpRequest: + + + +
+ +
310    @abstractmethod
+311    def withBody_0(self, body: IByteArray) -> IHttpRequest:
+312        """generated source for method withBody_0"""
+
+ + +

generated source for method withBody_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withAddedHeader( self, name: str, value: str) -> IHttpRequest: + + + +
+ +
323    @abstractmethod
+324    def withAddedHeader(self, name: str, value: str) -> IHttpRequest:
+325        """generated source for method withAddedHeader"""
+
+ + +

generated source for method withAddedHeader

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withAddedHeader_0( self, header: IHttpHeader) -> IHttpRequest: + + + +
+ +
335    @abstractmethod
+336    def withAddedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
+337        """generated source for method withAddedHeader_0"""
+
+ + +

generated source for method withAddedHeader_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withUpdatedHeader( self, name: str, value: str) -> IHttpRequest: + + + +
+ +
348    @abstractmethod
+349    def withUpdatedHeader(self, name: str, value: str) -> IHttpRequest:
+350        """generated source for method withUpdatedHeader"""
+
+ + +

generated source for method withUpdatedHeader

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withUpdatedHeader_0( self, header: IHttpHeader) -> IHttpRequest: + + + +
+ +
360    @abstractmethod
+361    def withUpdatedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
+362        """generated source for method withUpdatedHeader_0"""
+
+ + +

generated source for method withUpdatedHeader_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withRemovedHeader(self, name: str) -> IHttpRequest: + + + +
+ +
372    @abstractmethod
+373    def withRemovedHeader(self, name: str) -> IHttpRequest:
+374        """generated source for method withRemovedHeader"""
+
+ + +

generated source for method withRemovedHeader

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withRemovedHeader_0( self, header: IHttpHeader) -> IHttpRequest: + + + +
+ +
384    @abstractmethod
+385    def withRemovedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
+386        """generated source for method withRemovedHeader_0"""
+
+ + +

generated source for method withRemovedHeader_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withMarkers(self, markers) -> IHttpRequest: + + + +
+ +
396    @abstractmethod
+397    def withMarkers(self, markers) -> IHttpRequest:
+398        """generated source for method withMarkers"""
+
+ + +

generated source for method withMarkers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withMarkers_0(self, *markers) -> IHttpRequest: + + + +
+ +
408    @abstractmethod
+409    def withMarkers_0(self, *markers) -> IHttpRequest:
+410        """generated source for method withMarkers_0"""
+
+ + +

generated source for method withMarkers_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withDefaultHeaders(self) -> IHttpRequest: + + + +
+ +
418    @abstractmethod
+419    def withDefaultHeaders(self) -> IHttpRequest:
+420        """generated source for method withDefaultHeaders"""
+
+ + +

generated source for method withDefaultHeaders

+
+ + +
+
+ +
+ + def + httpRequest(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + httpRequestFromUrl(self, url: str) -> IHttpRequest: + + + +
+ +
446    @abstractmethod
+447    def httpRequestFromUrl(self, url: str) -> IHttpRequest:
+448        """generated source for method httpRequestFromUrl"""
+
+ + +

generated source for method httpRequestFromUrl

+
+ + +
+
+ +
+
@abstractmethod
+ + def + http2Request( self, service: IHttpService, headers: Iterable[IHttpHeader], body: IByteArray) -> IHttpRequest: + + + +
+ +
460    @abstractmethod
+461    def http2Request(
+462        self, service: IHttpService, headers: Iterable[IHttpHeader], body: IByteArray
+463    ) -> IHttpRequest:
+464        """generated source for method http2Request"""
+
+ + +

generated source for method http2Request

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+
+ HttpRequest = +None + + +
+ + + + +
+
+ +
+ + class + IHttpResponse(pyscalpel.java.burp.IHttpMessage, typing.Protocol): + + + +
+ +
 18class IHttpResponse(IHttpMessage, Protocol):  # pragma: no cover
+ 19    """generated source for interface HttpResponse"""
+ 20
+ 21    #
+ 22    #      * Obtain the HTTP status code contained in the response.
+ 23    #      *
+ 24    #      * @return HTTP status code.
+ 25    #
+ 26    @abstractmethod
+ 27    def statusCode(self) -> int:
+ 28        """generated source for method statusCode"""
+ 29
+ 30    #
+ 31    #      * Obtain the HTTP reason phrase contained in the response for HTTP 1 messages.
+ 32    #      * HTTP 2 messages will return a mapped phrase based on the status code.
+ 33    #      *
+ 34    #      * @return HTTP Reason phrase.
+ 35    #
+ 36    @abstractmethod
+ 37    def reasonPhrase(self) -> str | None:
+ 38        """generated source for method reasonPhrase"""
+ 39
+ 40    #
+ 41    #      * Return the HTTP Version text parsed from the response line for HTTP 1 messages.
+ 42    #      * HTTP 2 messages will return "HTTP/2"
+ 43    #      *
+ 44    #      * @return Version string
+ 45    #
+ 46    @abstractmethod
+ 47    def httpVersion(self) -> str | None:
+ 48        """generated source for method httpVersion"""
+ 49
+ 50    #
+ 51    #      * {@inheritDoc}
+ 52    #
+ 53    @abstractmethod
+ 54    def headers(self) -> list[IHttpHeader]:
+ 55        """generated source for method headers"""
+ 56
+ 57    #
+ 58    #      * {@inheritDoc}
+ 59    #
+ 60    @abstractmethod
+ 61    def body(self) -> IByteArray | None:
+ 62        """generated source for method body"""
+ 63
+ 64    #
+ 65    #      * {@inheritDoc}
+ 66    #
+ 67    @abstractmethod
+ 68    def bodyToString(self) -> str:
+ 69        """generated source for method bodyToString"""
+ 70
+ 71    #
+ 72    #      * {@inheritDoc}
+ 73    #
+ 74    @abstractmethod
+ 75    def bodyOffset(self) -> int:
+ 76        """generated source for method bodyOffset"""
+ 77
+ 78    #
+ 79    #      * {@inheritDoc}
+ 80    #
+ 81    @abstractmethod
+ 82    def markers(self) -> JavaObject:
+ 83        """generated source for method markers"""
+ 84
+ 85    #
+ 86    #      * Obtain details of the HTTP cookies set in the response.
+ 87    #      *
+ 88    #      * @return A list of {@link Cookie} objects representing the cookies set in the response, if any.
+ 89    #
+ 90    @abstractmethod
+ 91    def cookies(self) -> JavaObject:
+ 92        """generated source for method cookies"""
+ 93
+ 94    #
+ 95    #      * Obtain the MIME type of the response, as stated in the HTTP headers.
+ 96    #      *
+ 97    #      * @return The stated MIME type.
+ 98    #
+ 99    @abstractmethod
+100    def statedMimeType(self) -> JavaObject:
+101        """generated source for method statedMimeType"""
+102
+103    #
+104    #      * Obtain the MIME type of the response, as inferred from the contents of the HTTP message body.
+105    #      *
+106    #      * @return The inferred MIME type.
+107    #
+108    @abstractmethod
+109    def inferredMimeType(self) -> JavaObject:
+110        """generated source for method inferredMimeType"""
+111
+112    #
+113    #      * Retrieve the number of types given keywords appear in the response.
+114    #      *
+115    #      * @param keywords Keywords to count.
+116    #      *
+117    #      * @return List of keyword counts in the order they were provided.
+118    #
+119    @abstractmethod
+120    def keywordCounts(self, *keywords) -> int:
+121        """generated source for method keywordCounts"""
+122
+123    #
+124    #      * Retrieve the values of response attributes.
+125    #      *
+126    #      * @param types Response attributes to retrieve values for.
+127    #      *
+128    #      * @return List of {@link Attribute} objects.
+129    #
+130    @abstractmethod
+131    def attributes(self, *types) -> JavaObject:
+132        """generated source for method attributes"""
+133
+134    #
+135    #      * {@inheritDoc}
+136    #
+137    @abstractmethod
+138    def toByteArray(self) -> IByteArray:
+139        """generated source for method toByteArray"""
+140
+141    #
+142    #      * {@inheritDoc}
+143    #
+144    @abstractmethod
+145    def __str__(self) -> str:
+146        """generated source for method toString"""
+147
+148    #
+149    #      * Create a copy of the {@code HttpResponse} in temporary file.<br>
+150    #      * This method is used to save the {@code HttpResponse} object to a temporary file,
+151    #      * so that it is no longer held in memory. Extensions can use this method to convert
+152    #      * {@code HttpResponse} objects into a form suitable for long-term usage.
+153    #      *
+154    #      * @return A new {@code HttpResponse} instance stored in temporary file.
+155    #
+156    @abstractmethod
+157    def copyToTempFile(self) -> IHttpResponse:
+158        """generated source for method copyToTempFile"""
+159
+160    #
+161    #      * Create a copy of the {@code HttpResponse} with the provided status code.
+162    #      *
+163    #      * @param statusCode the new status code for response
+164    #      *
+165    #      * @return A new {@code HttpResponse} instance.
+166    #
+167    @abstractmethod
+168    def withStatusCode(self, statusCode: int) -> IHttpResponse:
+169        """generated source for method withStatusCode"""
+170
+171    #
+172    #      * Create a copy of the {@code HttpResponse} with the new reason phrase.
+173    #      *
+174    #      * @param reasonPhrase the new reason phrase for response
+175    #      *
+176    #      * @return A new {@code HttpResponse} instance.
+177    #
+178    @abstractmethod
+179    def withReasonPhrase(self, reasonPhrase: str) -> IHttpResponse:
+180        """generated source for method withReasonPhrase"""
+181
+182    #
+183    #      * Create a copy of the {@code HttpResponse} with the new http version.
+184    #      *
+185    #      * @param httpVersion the new http version for response
+186    #      *
+187    #      * @return A new {@code HttpResponse} instance.
+188    #
+189    @abstractmethod
+190    def withHttpVersion(self, httpVersion: str) -> IHttpResponse:
+191        """generated source for method withHttpVersion"""
+192
+193    #
+194    #      * Create a copy of the {@code HttpResponse} with the updated body.<br>
+195    #      * Updates Content-Length header.
+196    #      *
+197    #      * @param body the new body for the response
+198    #      *
+199    #      * @return A new {@code HttpResponse} instance.
+200    #
+201    @abstractmethod
+202    def withBody(self, body: IByteArray | str) -> IHttpResponse:
+203        """generated source for method withBody"""
+204
+205    #
+206    #      * Create a copy of the {@code HttpResponse} with the added header.
+207    #      *
+208    #      * @param header The {@link HttpHeader} to add to the response.
+209    #      *
+210    #      * @return The updated response containing the added header.
+211    #
+212    # @abstractmethod
+213    # def withAddedHeader(self, header) -> 'IHttpResponse':
+214    #     """ generated source for method withAddedHeader """
+215
+216    # #
+217    # #      * Create a copy of the {@code HttpResponse}  with the added header.
+218    # #      *
+219    # #      * @param name  The name of the header.
+220    # #      * @param value The value of the header.
+221    # #      *
+222    # #      * @return The updated response containing the added header.
+223    # #
+224    @abstractmethod
+225    def withAddedHeader(self, name: str, value: str) -> IHttpResponse:
+226        """generated source for method withAddedHeader_0"""
+227
+228    #
+229    #      * Create a copy of the {@code HttpResponse}  with the updated header.
+230    #      *
+231    #      * @param header The {@link HttpHeader} to update containing the new value.
+232    #      *
+233    #      * @return The updated response containing the updated header.
+234    #
+235    # @abstractmethod
+236    # def withUpdatedHeader(self, header) -> 'IHttpResponse':
+237    #     """ generated source for method withUpdatedHeader """
+238
+239    # #
+240    # #      * Create a copy of the {@code HttpResponse}  with the updated header.
+241    # #      *
+242    # #      * @param name  The name of the header to update the value of.
+243    # #      * @param value The new value of the specified HTTP header.
+244    # #      *
+245    # #      * @return The updated response containing the updated header.
+246    # #
+247    @abstractmethod
+248    def withUpdatedHeader(self, name: str, value: str) -> IHttpResponse:
+249        """generated source for method withUpdatedHeader_0"""
+250
+251    #
+252    #      * Create a copy of the {@code HttpResponse}  with the removed header.
+253    #      *
+254    #      * @param header The {@link HttpHeader} to remove from the response.
+255    #      *
+256    #      * @return The updated response containing the removed header.
+257    #
+258    # @abstractmethod
+259    # def withRemovedHeader(self, header) -> 'IHttpResponse':
+260    #     """ generated source for method withRemovedHeader """
+261
+262    # #
+263    # #      * Create a copy of the {@code HttpResponse}  with the removed header.
+264    # #      *
+265    # #      * @param name The name of the HTTP header to remove from the response.
+266    # #      *
+267    # #      * @return The updated response containing the removed header.
+268    # #
+269    @abstractmethod
+270    def withRemovedHeader(self, name: str) -> IHttpResponse:
+271        """generated source for method withRemovedHeader_0"""
+272
+273    #
+274    #      * Create a copy of the {@code HttpResponse} with the added markers.
+275    #      *
+276    #      * @param markers Request markers to add.
+277    #      *
+278    #      * @return A new {@code MarkedHttpRequestResponse} instance.
+279    #
+280    @abstractmethod
+281    @overload
+282    def withMarkers(self, markers: JavaObject) -> IHttpResponse:
+283        """generated source for method withMarkers"""
+284
+285    #
+286    #      * Create a copy of the {@code HttpResponse} with the added markers.
+287    #      *
+288    #      * @param markers Request markers to add.
+289    #      *
+290    #      * @return A new {@code MarkedHttpRequestResponse} instance.
+291    #
+292    @abstractmethod
+293    @overload
+294    def withMarkers(self, *markers: JavaObject) -> IHttpResponse:
+295        """generated source for method withMarkers_0"""
+296
+297    #
+298    #      * Create a new empty instance of {@link HttpResponse}.<br>
+299    #      *
+300    #      * @return A new {@link HttpResponse} instance.
+301    #
+302    @abstractmethod
+303    def httpResponse(self, response: IByteArray | str) -> IHttpResponse:
+304        """generated source for method httpResponse"""
+
+ + +

generated source for interface HttpResponse

+
+ + +
+ +
+
@abstractmethod
+ + def + statusCode(self) -> int: + + + +
+ +
26    @abstractmethod
+27    def statusCode(self) -> int:
+28        """generated source for method statusCode"""
+
+ + +

generated source for method statusCode

+
+ + +
+
+ +
+
@abstractmethod
+ + def + reasonPhrase(self) -> str | None: + + + +
+ +
36    @abstractmethod
+37    def reasonPhrase(self) -> str | None:
+38        """generated source for method reasonPhrase"""
+
+ + +

generated source for method reasonPhrase

+
+ + +
+
+ +
+
@abstractmethod
+ + def + httpVersion(self) -> str | None: + + + +
+ +
46    @abstractmethod
+47    def httpVersion(self) -> str | None:
+48        """generated source for method httpVersion"""
+
+ + +

generated source for method httpVersion

+
+ + +
+
+ +
+
@abstractmethod
+ + def + headers(self) -> list[IHttpHeader]: + + + +
+ +
53    @abstractmethod
+54    def headers(self) -> list[IHttpHeader]:
+55        """generated source for method headers"""
+
+ + +

generated source for method headers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + body(self) -> IByteArray | None: + + + +
+ +
60    @abstractmethod
+61    def body(self) -> IByteArray | None:
+62        """generated source for method body"""
+
+ + +

generated source for method body

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyToString(self) -> str: + + + +
+ +
67    @abstractmethod
+68    def bodyToString(self) -> str:
+69        """generated source for method bodyToString"""
+
+ + +

generated source for method bodyToString

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyOffset(self) -> int: + + + +
+ +
74    @abstractmethod
+75    def bodyOffset(self) -> int:
+76        """generated source for method bodyOffset"""
+
+ + +

generated source for method bodyOffset

+
+ + +
+
+ +
+
@abstractmethod
+ + def + markers(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
81    @abstractmethod
+82    def markers(self) -> JavaObject:
+83        """generated source for method markers"""
+
+ + +

generated source for method markers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + cookies(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
90    @abstractmethod
+91    def cookies(self) -> JavaObject:
+92        """generated source for method cookies"""
+
+ + +

generated source for method cookies

+
+ + +
+
+ +
+
@abstractmethod
+ + def + statedMimeType(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
 99    @abstractmethod
+100    def statedMimeType(self) -> JavaObject:
+101        """generated source for method statedMimeType"""
+
+ + +

generated source for method statedMimeType

+
+ + +
+
+ +
+
@abstractmethod
+ + def + inferredMimeType(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
108    @abstractmethod
+109    def inferredMimeType(self) -> JavaObject:
+110        """generated source for method inferredMimeType"""
+
+ + +

generated source for method inferredMimeType

+
+ + +
+
+ +
+
@abstractmethod
+ + def + keywordCounts(self, *keywords) -> int: + + + +
+ +
119    @abstractmethod
+120    def keywordCounts(self, *keywords) -> int:
+121        """generated source for method keywordCounts"""
+
+ + +

generated source for method keywordCounts

+
+ + +
+
+ +
+
@abstractmethod
+ + def + attributes(self, *types) -> pyscalpel.java.object.JavaObject: + + + +
+ +
130    @abstractmethod
+131    def attributes(self, *types) -> JavaObject:
+132        """generated source for method attributes"""
+
+ + +

generated source for method attributes

+
+ + +
+
+ +
+
@abstractmethod
+ + def + toByteArray(self) -> IByteArray: + + + +
+ +
137    @abstractmethod
+138    def toByteArray(self) -> IByteArray:
+139        """generated source for method toByteArray"""
+
+ + +

generated source for method toByteArray

+
+ + +
+
+ +
+
@abstractmethod
+ + def + copyToTempFile(self) -> IHttpResponse: + + + +
+ +
156    @abstractmethod
+157    def copyToTempFile(self) -> IHttpResponse:
+158        """generated source for method copyToTempFile"""
+
+ + +

generated source for method copyToTempFile

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withStatusCode(self, statusCode: int) -> IHttpResponse: + + + +
+ +
167    @abstractmethod
+168    def withStatusCode(self, statusCode: int) -> IHttpResponse:
+169        """generated source for method withStatusCode"""
+
+ + +

generated source for method withStatusCode

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withReasonPhrase( self, reasonPhrase: str) -> IHttpResponse: + + + +
+ +
178    @abstractmethod
+179    def withReasonPhrase(self, reasonPhrase: str) -> IHttpResponse:
+180        """generated source for method withReasonPhrase"""
+
+ + +

generated source for method withReasonPhrase

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withHttpVersion( self, httpVersion: str) -> IHttpResponse: + + + +
+ +
189    @abstractmethod
+190    def withHttpVersion(self, httpVersion: str) -> IHttpResponse:
+191        """generated source for method withHttpVersion"""
+
+ + +

generated source for method withHttpVersion

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withBody( self, body: IByteArray | str) -> IHttpResponse: + + + +
+ +
201    @abstractmethod
+202    def withBody(self, body: IByteArray | str) -> IHttpResponse:
+203        """generated source for method withBody"""
+
+ + +

generated source for method withBody

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withAddedHeader( self, name: str, value: str) -> IHttpResponse: + + + +
+ +
224    @abstractmethod
+225    def withAddedHeader(self, name: str, value: str) -> IHttpResponse:
+226        """generated source for method withAddedHeader_0"""
+
+ + +

generated source for method withAddedHeader_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withUpdatedHeader( self, name: str, value: str) -> IHttpResponse: + + + +
+ +
247    @abstractmethod
+248    def withUpdatedHeader(self, name: str, value: str) -> IHttpResponse:
+249        """generated source for method withUpdatedHeader_0"""
+
+ + +

generated source for method withUpdatedHeader_0

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withRemovedHeader(self, name: str) -> IHttpResponse: + + + +
+ +
269    @abstractmethod
+270    def withRemovedHeader(self, name: str) -> IHttpResponse:
+271        """generated source for method withRemovedHeader_0"""
+
+ + +

generated source for method withRemovedHeader_0

+
+ + +
+
+ +
+ + def + withMarkers(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + httpResponse( self, response: IByteArray | str) -> IHttpResponse: + + + +
+ +
302    @abstractmethod
+303    def httpResponse(self, response: IByteArray | str) -> IHttpResponse:
+304        """generated source for method httpResponse"""
+
+ + +

generated source for method httpResponse

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+
+ HttpResponse = +None + + +
+ + + + +
+
+ +
+ + class + IHttpRequestResponse(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
+ +
12class IHttpRequestResponse(JavaObject, Protocol, metaclass=ABCMeta):  # pragma: no cover
+13    """generated source for interface HttpRequestResponse"""
+14
+15    __metaclass__ = ABCMeta
+16
+17    @abstractmethod
+18    def request(self) -> IHttpRequest | None:
+19        ...
+20
+21    @abstractmethod
+22    def response(self) -> IHttpResponse | None:
+23        ...
+
+ + +

generated source for interface HttpRequestResponse

+
+ + +
+ +
+
@abstractmethod
+ + def + request(self) -> IHttpRequest | None: + + + +
+ +
17    @abstractmethod
+18    def request(self) -> IHttpRequest | None:
+19        ...
+
+ + + + +
+
+ +
+
@abstractmethod
+ + def + response(self) -> IHttpResponse | None: + + + +
+ +
21    @abstractmethod
+22    def response(self) -> IHttpResponse | None:
+23        ...
+
+ + + + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+ +
+ + class + IHttpHeader(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
+ +
16class IHttpHeader(JavaObject, Protocol, metaclass=ABCMeta):  # pragma: no cover
+17    """generated source for interface HttpHeader"""
+18
+19    __metaclass__ = ABCMeta
+20    #
+21    #      * @return The name of the header.
+22    #
+23
+24    @abstractmethod
+25    def name(self) -> str:
+26        """generated source for method name"""
+27
+28    #
+29    #      * @return The value of the header.
+30    #
+31    @abstractmethod
+32    def value(self) -> str:
+33        """generated source for method value"""
+34
+35    #
+36    #      * @return The {@code String} representation of the header.
+37    #
+38    @abstractmethod
+39    def __str__(self):
+40        """generated source for method toString"""
+41
+42    #
+43    #      * Create a new instance of {@code HttpHeader} from name and value.
+44    #      *
+45    #      * @param name  The name of the header.
+46    #      * @param value The value of the header.
+47    #      *
+48    #      * @return A new {@code HttpHeader} instance.
+49    #
+50    @abstractmethod
+51    @overload
+52    def httpHeader(self, name: str, value: str) -> IHttpHeader:
+53        """generated source for method httpHeader"""
+54
+55    #
+56    #      * Create a new instance of HttpHeader from a {@code String} header representation.
+57    #      * It will be parsed according to the HTTP/1.1 specification for headers.
+58    #      *
+59    #      * @param header The {@code String} header representation.
+60    #      *
+61    #      * @return A new {@code HttpHeader} instance.
+62    #
+63    @abstractmethod
+64    @overload
+65    def httpHeader(self, header: str) -> IHttpHeader:
+66        """generated source for method httpHeader_0"""
+
+ + +

generated source for interface HttpHeader

+
+ + +
+ +
+
@abstractmethod
+ + def + name(self) -> str: + + + +
+ +
24    @abstractmethod
+25    def name(self) -> str:
+26        """generated source for method name"""
+
+ + +

generated source for method name

+
+ + +
+
+ +
+
@abstractmethod
+ + def + value(self) -> str: + + + +
+ +
31    @abstractmethod
+32    def value(self) -> str:
+33        """generated source for method value"""
+
+ + +

generated source for method value

+
+ + +
+
+ +
+ + def + httpHeader(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+
+ HttpHeader = +None + + +
+ + + + +
+
+ +
+ + class + IHttpMessage(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
+ +
15class IHttpMessage(JavaObject, Protocol):  # pragma: no cover
+16    """generated source for interface HttpMessage"""
+17
+18    #
+19    #      * HTTP headers contained in the message.
+20    #      *
+21    #      * @return A list of HTTP headers.
+22    #
+23    @abstractmethod
+24    def headers(self) -> IHttpHeader:
+25        """generated source for method headers"""
+26
+27    #
+28    #      * Offset within the message where the message body begins.
+29    #      *
+30    #      * @return The message body offset.
+31    #
+32    @abstractmethod
+33    def bodyOffset(self) -> int:
+34        """generated source for method bodyOffset"""
+35
+36    #
+37    #      * Body of a message as a byte array.
+38    #      *
+39    #      * @return The body of a message as a byte array.
+40    #
+41    @abstractmethod
+42    def body(self) -> IByteArray:
+43        """generated source for method body"""
+44
+45    #
+46    #      * Body of a message as a {@code String}.
+47    #      *
+48    #      * @return The body of a message as a {@code String}.
+49    #
+50    @abstractmethod
+51    def bodyToString(self) -> str:
+52        """generated source for method bodyToString"""
+53
+54    #
+55    #      * Markers for the message.
+56    #      *
+57    #      * @return A list of markers.
+58    #
+59    @abstractmethod
+60    def markers(self) -> JavaObject:
+61        """generated source for method markers"""
+62
+63    #
+64    #      * Message as a byte array.
+65    #      *
+66    #      * @return The message as a byte array.
+67    #
+68    @abstractmethod
+69    def toByteArray(self) -> IByteArray:
+70        """generated source for method toByteArray"""
+71
+72    #
+73    #      * Message as a {@code String}.
+74    #      *
+75    #      * @return The message as a {@code String}.
+76    #
+77    @abstractmethod
+78    def __str__(self) -> str:
+79        """generated source for method toString"""
+
+ + +

generated source for interface HttpMessage

+
+ + +
+ +
+
@abstractmethod
+ + def + headers(self) -> IHttpHeader: + + + +
+ +
23    @abstractmethod
+24    def headers(self) -> IHttpHeader:
+25        """generated source for method headers"""
+
+ + +

generated source for method headers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyOffset(self) -> int: + + + +
+ +
32    @abstractmethod
+33    def bodyOffset(self) -> int:
+34        """generated source for method bodyOffset"""
+
+ + +

generated source for method bodyOffset

+
+ + +
+
+ +
+
@abstractmethod
+ + def + body(self) -> IByteArray: + + + +
+ +
41    @abstractmethod
+42    def body(self) -> IByteArray:
+43        """generated source for method body"""
+
+ + +

generated source for method body

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyToString(self) -> str: + + + +
+ +
50    @abstractmethod
+51    def bodyToString(self) -> str:
+52        """generated source for method bodyToString"""
+
+ + +

generated source for method bodyToString

+
+ + +
+
+ +
+
@abstractmethod
+ + def + markers(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
59    @abstractmethod
+60    def markers(self) -> JavaObject:
+61        """generated source for method markers"""
+
+ + +

generated source for method markers

+
+ + +
+
+ +
+
@abstractmethod
+ + def + toByteArray(self) -> IByteArray: + + + +
+ +
68    @abstractmethod
+69    def toByteArray(self) -> IByteArray:
+70        """generated source for method toByteArray"""
+
+ + +

generated source for method toByteArray

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+ +
+ + class + IHttpParameter(pyscalpel.java.object.JavaObject): + + + +
+ +
15class IHttpParameter(JavaObject):  # pragma: no cover
+16    """generated source for interface HttpParameter"""
+17
+18    __metaclass__ = ABCMeta
+19    #
+20    #      * @return The parameter type.
+21    #
+22
+23    @abstractmethod
+24    def type_(self) -> JavaObject:
+25        """generated source for method type_"""
+26
+27    #
+28    #      * @return The parameter name.
+29    #
+30    @abstractmethod
+31    def name(self) -> str:
+32        """generated source for method name"""
+33
+34    #
+35    #      * @return The parameter value.
+36    #
+37    @abstractmethod
+38    def value(self) -> str:
+39        """generated source for method value"""
+40
+41    #
+42    #      * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#URL} type.
+43    #      *
+44    #      * @param name  The parameter name.
+45    #      * @param value The parameter value.
+46    #      *
+47    #      * @return A new {@code HttpParameter} instance.
+48    #
+49    @abstractmethod
+50    def urlParameter(self, name: str, value: str) -> IHttpParameter:
+51        """generated source for method urlParameter"""
+52
+53    #
+54    #      * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#BODY} type.
+55    #      *
+56    #      * @param name  The parameter name.
+57    #      * @param value The parameter value.
+58    #      *
+59    #      * @return A new {@code HttpParameter} instance.
+60    #
+61    @abstractmethod
+62    def bodyParameter(self, name: str, value: str) -> IHttpParameter:
+63        """generated source for method bodyParameter"""
+64
+65    #
+66    #      * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#COOKIE} type.
+67    #      *
+68    #      * @param name  The parameter name.
+69    #      * @param value The parameter value.
+70    #      *
+71    #      * @return A new {@code HttpParameter} instance.
+72    #
+73    @abstractmethod
+74    def cookieParameter(self, name: str, value: str) -> IHttpParameter:
+75        """generated source for method cookieParameter"""
+76
+77    #
+78    #      * Create a new Instance of {@code HttpParameter} with the specified type.
+79    #      *
+80    #      * @param name  The parameter name.
+81    #      * @param value The parameter value.
+82    #      * @param type  The header type.
+83    #      *
+84    #      * @return A new {@code HttpParameter} instance.
+85    #
+86    @abstractmethod
+87    def parameter(self, name: str, value: str, type_: JavaObject) -> IHttpParameter:
+88        """generated source for method parameter"""
+
+ + +

generated source for interface HttpParameter

+
+ + +
+ +
+
@abstractmethod
+ + def + type_(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
23    @abstractmethod
+24    def type_(self) -> JavaObject:
+25        """generated source for method type_"""
+
+ + +

generated source for method type_

+
+ + +
+
+ +
+
@abstractmethod
+ + def + name(self) -> str: + + + +
+ +
30    @abstractmethod
+31    def name(self) -> str:
+32        """generated source for method name"""
+
+ + +

generated source for method name

+
+ + +
+
+ +
+
@abstractmethod
+ + def + value(self) -> str: + + + +
+ +
37    @abstractmethod
+38    def value(self) -> str:
+39        """generated source for method value"""
+
+ + +

generated source for method value

+
+ + +
+
+ +
+
@abstractmethod
+ + def + urlParameter( self, name: str, value: str) -> IHttpParameter: + + + +
+ +
49    @abstractmethod
+50    def urlParameter(self, name: str, value: str) -> IHttpParameter:
+51        """generated source for method urlParameter"""
+
+ + +

generated source for method urlParameter

+
+ + +
+
+ +
+
@abstractmethod
+ + def + bodyParameter( self, name: str, value: str) -> IHttpParameter: + + + +
+ +
61    @abstractmethod
+62    def bodyParameter(self, name: str, value: str) -> IHttpParameter:
+63        """generated source for method bodyParameter"""
+
+ + +

generated source for method bodyParameter

+
+ + +
+
+ +
+
@abstractmethod
+ + def + cookieParameter( self, name: str, value: str) -> IHttpParameter: + + + +
+ +
73    @abstractmethod
+74    def cookieParameter(self, name: str, value: str) -> IHttpParameter:
+75        """generated source for method cookieParameter"""
+
+ + +

generated source for method cookieParameter

+
+ + +
+
+ +
+
@abstractmethod
+ + def + parameter( self, name: str, value: str, type_: pyscalpel.java.object.JavaObject) -> IHttpParameter: + + + +
+ +
86    @abstractmethod
+87    def parameter(self, name: str, value: str, type_: JavaObject) -> IHttpParameter:
+88        """generated source for method parameter"""
+
+ + +

generated source for method parameter

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+
+ HttpParameter = +None + + +
+ + + + +
+
+ +
+ + class + IHttpService(pyscalpel.java.object.JavaObject): + + + +
+ +
12class IHttpService(JavaObject, metaclass=ABCMeta):  # pragma: no cover
+13    @abstractmethod
+14    def host(self) -> str:
+15        """The hostname or IP address for the service."""
+16
+17    @abstractmethod
+18    @overload
+19    def httpService(self, baseUrl: str) -> IHttpService:
+20        """Create a new instance of {@code HttpService} from a base URL."""
+21
+22    @abstractmethod
+23    @overload
+24    def httpService(self, baseUrl: str, secure: bool) -> IHttpService:
+25        """Create a new instance of {@code HttpService} from a base URL and a protocol."""
+26
+27    @abstractmethod
+28    @overload
+29    def httpService(self, host: str, port: int, secure: bool) -> IHttpService:
+30        """Create a new instance of {@code HttpService} from a host, a port and a protocol."""
+31
+32    @abstractmethod
+33    def httpService(self, *args, **kwargs) -> IHttpService:
+34        """Create a new instance of {@code HttpService} from a host, a port and a protocol."""
+35
+36    @abstractmethod
+37    def port(self) -> int:
+38        """The port number for the service."""
+39
+40    @abstractmethod
+41    def secure(self) -> bool:
+42        """True if a secure protocol is used for the connection, false otherwise."""
+43
+44    @abstractmethod
+45    def __str__(self) -> str:
+46        """The {@code String} representation of the service."""
+
+ + +

generated source for class Object

+
+ + +
+ +
+
@abstractmethod
+ + def + host(self) -> str: + + + +
+ +
13    @abstractmethod
+14    def host(self) -> str:
+15        """The hostname or IP address for the service."""
+
+ + +

The hostname or IP address for the service.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + httpService(self, *args, **kwargs) -> IHttpService: + + + +
+ +
32    @abstractmethod
+33    def httpService(self, *args, **kwargs) -> IHttpService:
+34        """Create a new instance of {@code HttpService} from a host, a port and a protocol."""
+
+ + +

Create a new instance of {@code HttpService} from a host, a port and a protocol.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + port(self) -> int: + + + +
+ +
36    @abstractmethod
+37    def port(self) -> int:
+38        """The port number for the service."""
+
+ + +

The port number for the service.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + secure(self) -> bool: + + + +
+ +
40    @abstractmethod
+41    def secure(self) -> bool:
+42        """True if a secure protocol is used for the connection, false otherwise."""
+
+ + +

True if a secure protocol is used for the connection, false otherwise.

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+
+ HttpService = +None + + +
+ + + + +
+
+ +
+ + class + IByteArray(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
+ +
 13class IByteArray(JavaObject, Protocol):  # pragma: no cover
+ 14    __metaclass__ = ABCMeta
+ 15
+ 16    """ generated source for interface ByteArray """
+ 17
+ 18    #
+ 19    #      * Access the byte stored at the provided index.
+ 20    #      *
+ 21    #      * @param index Index of the byte to be retrieved.
+ 22    #      *
+ 23    #      * @return The byte at the index.
+ 24    #
+ 25    @abstractmethod
+ 26    def getByte(self, index: int) -> int:
+ 27        """generated source for method getByte"""
+ 28
+ 29    #
+ 30    #      * Sets the byte at the provided index to the provided byte.
+ 31    #      *
+ 32    #      * @param index Index of the byte to be set.
+ 33    #      * @param value The byte to be set.
+ 34    #
+ 35    @abstractmethod
+ 36    @overload
+ 37    def setByte(self, index: int, value: int) -> None:
+ 38        """generated source for method setByte"""
+ 39
+ 40    #
+ 41    #      * Sets the byte at the provided index to the provided narrowed integer value.
+ 42    #      *
+ 43    #      * @param index Index of the byte to be set.
+ 44    #      * @param value The integer value to be set after a narrowing primitive conversion to a byte.
+ 45    #
+ 46    @abstractmethod
+ 47    @overload
+ 48    def setByte(self, index: int, value: int) -> None:
+ 49        """generated source for method setByte_0"""
+ 50
+ 51    #
+ 52    #      * Sets bytes starting at the specified index to the provided bytes.
+ 53    #      *
+ 54    #      * @param index The index of the first byte to set.
+ 55    #      * @param data  The byte[] or sequence of bytes to be set.
+ 56    #
+ 57    @abstractmethod
+ 58    @overload
+ 59    def setBytes(self, index: int, *data: int) -> None:
+ 60        """generated source for method setBytes"""
+ 61
+ 62    #
+ 63    #      * Sets bytes starting at the specified index to the provided integers after narrowing primitive conversion to bytes.
+ 64    #      *
+ 65    #      * @param index The index of the first byte to set.
+ 66    #      * @param data  The int[] or the sequence of integers to be set after a narrowing primitive conversion to bytes.
+ 67    #
+ 68
+ 69    @abstractmethod
+ 70    @overload
+ 71    def setBytes(self, index: int, byteArray: IByteArray) -> None:
+ 72        """generated source for method setBytes_1"""
+ 73
+ 74    #
+ 75    #      * Number of bytes stored in the {@code ByteArray}.
+ 76    #      *
+ 77    #      * @return Length of the {@code ByteArray}.
+ 78    #
+ 79    @abstractmethod
+ 80    def length(self) -> int:
+ 81        """generated source for method length"""
+ 82
+ 83    #
+ 84    #      * Copy of all bytes
+ 85    #      *
+ 86    #      * @return Copy of all bytes.
+ 87    #
+ 88    @abstractmethod
+ 89    def getBytes(self) -> JavaBytes:
+ 90        """generated source for method getBytes"""
+ 91
+ 92    #
+ 93    #      * New ByteArray with all bytes between the start index (inclusive) and the end index (exclusive).
+ 94    #      *
+ 95    #      * @param startIndexInclusive The inclusive start index of retrieved range.
+ 96    #      * @param endIndexExclusive   The exclusive end index of retrieved range.
+ 97    #      *
+ 98    #      * @return ByteArray containing all bytes in the specified range.
+ 99    #
+100    @abstractmethod
+101    @overload
+102    def subArray(
+103        self, startIndexInclusive: int, endIndexExclusive: int
+104    ) -> "IByteArray":
+105        """generated source for method subArray"""
+106
+107    #
+108    #      * New ByteArray with all bytes in the specified range.
+109    #      *
+110    #      * @param range The {@link Range} of bytes to be returned.
+111    #      *
+112    #      * @return ByteArray containing all bytes in the specified range.
+113    #
+114    @abstractmethod
+115    @overload
+116    def subArray(self, _range) -> IByteArray:
+117        """generated source for method subArray_0"""
+118
+119    #
+120    #      * Create a copy of the {@code ByteArray}
+121    #      *
+122    #      * @return New {@code ByteArray} with a copy of the wrapped bytes.
+123    #
+124    @abstractmethod
+125    def copy(self) -> IByteArray:
+126        """generated source for method copy"""
+127
+128    #
+129    #      * Create a copy of the {@code ByteArray} in temporary file.<br>
+130    #      * This method is used to save the {@code ByteArray} object to a temporary file,
+131    #      * so that it is no longer held in memory. Extensions can use this method to convert
+132    #      * {@code ByteArray} objects into a form suitable for long-term usage.
+133    #      *
+134    #      * @return A new {@code ByteArray} instance stored in temporary file.
+135    #
+136    @abstractmethod
+137    def copyToTempFile(self) -> IByteArray:
+138        """generated source for method copyToTempFile"""
+139
+140    #
+141    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
+142    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
+143    #      *
+144    #      * @param searchTerm The value to be searched for.
+145    #      *
+146    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
+147    #
+148    @abstractmethod
+149    @overload
+150    def indexOf(self, searchTerm: IByteArray) -> int:
+151        """generated source for method indexOf"""
+152
+153    #
+154    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
+155    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
+156    #      *
+157    #      * @param searchTerm The value to be searched for.
+158    #      *
+159    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
+160    #
+161    @abstractmethod
+162    @overload
+163    def indexOf(self, searchTerm: str) -> int:
+164        """generated source for method indexOf_0"""
+165
+166    #
+167    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
+168    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
+169    #      *
+170    #      * @param searchTerm    The value to be searched for.
+171    #      * @param caseSensitive Flags whether the search is case-sensitive.
+172    #      *
+173    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
+174    #
+175    @abstractmethod
+176    @overload
+177    def indexOf(self, searchTerm: IByteArray, caseSensitive: bool) -> int:
+178        """generated source for method indexOf_1"""
+179
+180    #
+181    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
+182    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
+183    #      *
+184    #      * @param searchTerm    The value to be searched for.
+185    #      * @param caseSensitive Flags whether the search is case-sensitive.
+186    #      *
+187    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
+188    #
+189    @abstractmethod
+190    @overload
+191    def indexOf(self, searchTerm: str, caseSensitive: bool) -> int:
+192        """generated source for method indexOf_2"""
+193
+194    #
+195    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
+196    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
+197    #      *
+198    #      * @param searchTerm          The value to be searched for.
+199    #      * @param caseSensitive       Flags whether the search is case-sensitive.
+200    #      * @param startIndexInclusive The inclusive start index for the search.
+201    #      * @param endIndexExclusive   The exclusive end index for the search.
+202    #      *
+203    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
+204    #
+205    @abstractmethod
+206    @overload
+207    def indexOf(
+208        self,
+209        searchTerm: IByteArray,
+210        caseSensitive: bool,
+211        startIndexInclusive: int,
+212        endIndexExclusive: int,
+213    ) -> int:
+214        """generated source for method indexOf_3"""
+215
+216    #
+217    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
+218    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
+219    #      *
+220    #      * @param searchTerm          The value to be searched for.
+221    #      * @param caseSensitive       Flags whether the search is case-sensitive.
+222    #      * @param startIndexInclusive The inclusive start index for the search.
+223    #      * @param endIndexExclusive   The exclusive end index for the search.
+224    #      *
+225    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
+226    #
+227    @abstractmethod
+228    @overload
+229    def indexOf(
+230        self,
+231        searchTerm: str,
+232        caseSensitive: bool,
+233        startIndexInclusive: int,
+234        endIndexExclusive: int,
+235    ) -> int:
+236        """generated source for method indexOf_4"""
+237
+238    #
+239    #      * Searches the data in the ByteArray and counts all matches for a specified term.
+240    #      *
+241    #      * @param searchTerm The value to be searched for.
+242    #      *
+243    #      * @return The count of all matches of the pattern
+244    #
+245    @abstractmethod
+246    @overload
+247    def countMatches(self, searchTerm: IByteArray) -> int:
+248        """generated source for method countMatches"""
+249
+250    #
+251    #      * Searches the data in the ByteArray and counts all matches for a specified term.
+252    #      *
+253    #      * @param searchTerm The value to be searched for.
+254    #      *
+255    #      * @return The count of all matches of the pattern
+256    #
+257    @abstractmethod
+258    @overload
+259    def countMatches(self, searchTerm: str) -> int:
+260        """generated source for method countMatches_0"""
+261
+262    #
+263    #      * Searches the data in the ByteArray and counts all matches for a specified term.
+264    #      *
+265    #      * @param searchTerm    The value to be searched for.
+266    #      * @param caseSensitive Flags whether the search is case-sensitive.
+267    #      *
+268    #      * @return The count of all matches of the pattern
+269    #
+270    @abstractmethod
+271    @overload
+272    def countMatches(self, searchTerm: IByteArray, caseSensitive: bool) -> int:
+273        """generated source for method countMatches_1"""
+274
+275    #
+276    #      * Searches the data in the ByteArray and counts all matches for a specified term.
+277    #      *
+278    #      * @param searchTerm    The value to be searched for.
+279    #      * @param caseSensitive Flags whether the search is case-sensitive.
+280    #      *
+281    #      * @return The count of all matches of the pattern
+282    #
+283    @abstractmethod
+284    @overload
+285    def countMatches(self, searchTerm: str, caseSensitive: bool) -> int:
+286        """generated source for method countMatches_2"""
+287
+288    #
+289    #      * Searches the data in the ByteArray and counts all matches for a specified term.
+290    #      *
+291    #      * @param searchTerm          The value to be searched for.
+292    #      * @param caseSensitive       Flags whether the search is case-sensitive.
+293    #      * @param startIndexInclusive The inclusive start index for the search.
+294    #      * @param endIndexExclusive   The exclusive end index for the search.
+295    #      *
+296    #      * @return The count of all matches of the pattern within the specified bounds
+297    #
+298    @abstractmethod
+299    @overload
+300    def countMatches(
+301        self,
+302        searchTerm: IByteArray,
+303        caseSensitive: bool,
+304        startIndexInclusive: int,
+305        endIndexExclusive: int,
+306    ) -> int:
+307        """generated source for method countMatches_3"""
+308
+309    #
+310    #      * Searches the data in the ByteArray and counts all matches for a specified term.
+311    #      *
+312    #      * @param searchTerm          The value to be searched for.
+313    #      * @param caseSensitive       Flags whether the search is case-sensitive.
+314    #      * @param startIndexInclusive The inclusive start index for the search.
+315    #      * @param endIndexExclusive   The exclusive end index for the search.
+316    #      *
+317    #      * @return The count of all matches of the pattern within the specified bounds
+318    #
+319    @abstractmethod
+320    @overload
+321    def countMatches(
+322        self,
+323        searchTerm: str,
+324        caseSensitive: bool,
+325        startIndexInclusive: int,
+326        endIndexExclusive: int,
+327    ) -> int:
+328        """generated source for method countMatches_4"""
+329
+330    #
+331    #      * Convert the bytes of the ByteArray into String form using the encoding specified by Burp Suite.
+332    #      *
+333    #      * @return The converted data in String form.
+334    #
+335    @abstractmethod
+336    def __str__(self) -> str:
+337        """generated source for method toString"""
+338
+339    #
+340    #      * Create a copy of the {@code ByteArray} appended with the provided bytes.
+341    #      *
+342    #      * @param data The byte[] or sequence of bytes to append.
+343    #
+344    @abstractmethod
+345    def withAppended(self, *data: int) -> IByteArray:
+346        """generated source for method withAppended"""
+347
+348    #
+349    #      * Create a copy of the {@code ByteArray} appended with the provided integers after narrowing primitive conversion to bytes.
+350    #      *
+351    #      * @param data The int[] or sequence of integers to append after narrowing primitive conversion to bytes.
+352    #
+353
+354    #
+355    @abstractmethod
+356    def byteArrayOfLength(self, length: int) -> IByteArray:
+357        """generated source for method byteArrayOfLength"""
+358
+359    #
+360    #      * Create a new {@code ByteArray} with the provided byte data.<br>
+361    #      *
+362    #      * @param data byte[] to wrap, or sequence of bytes to wrap.
+363    #      *
+364    #      * @return New {@code ByteArray} wrapping the provided byte array.
+365    #
+366    # @abstractmethod
+367    @abstractmethod
+368    def byteArray(self, data: bytes | JavaBytes | list[int] | str) -> IByteArray:
+369        """generated source for method byteArray"""
+370
+371    #
+372    #      * Create a new {@code ByteArray} with the provided integers after a narrowing primitive conversion to bytes.<br>
+373    #      *
+374    #      * @param data bytes.
+375    #      *
+376    #      * @return New {@code ByteArray} wrapping the provided data after a narrowing primitive conversion to bytes.
+377    #
+
+ + +

generated source for class Object

+
+ + +
+ +
+
@abstractmethod
+ + def + getByte(self, index: int) -> int: + + + +
+ +
25    @abstractmethod
+26    def getByte(self, index: int) -> int:
+27        """generated source for method getByte"""
+
+ + +

generated source for method getByte

+
+ + +
+
+ +
+ + def + setByte(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+ + def + setBytes(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + length(self) -> int: + + + +
+ +
79    @abstractmethod
+80    def length(self) -> int:
+81        """generated source for method length"""
+
+ + +

generated source for method length

+
+ + +
+
+ +
+
@abstractmethod
+ + def + getBytes(self) -> pyscalpel.java.bytes.JavaBytes: + + + +
+ +
88    @abstractmethod
+89    def getBytes(self) -> JavaBytes:
+90        """generated source for method getBytes"""
+
+ + +

generated source for method getBytes

+
+ + +
+
+ +
+ + def + subArray(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + copy(self) -> IByteArray: + + + +
+ +
124    @abstractmethod
+125    def copy(self) -> IByteArray:
+126        """generated source for method copy"""
+
+ + +

generated source for method copy

+
+ + +
+
+ +
+
@abstractmethod
+ + def + copyToTempFile(self) -> IByteArray: + + + +
+ +
136    @abstractmethod
+137    def copyToTempFile(self) -> IByteArray:
+138        """generated source for method copyToTempFile"""
+
+ + +

generated source for method copyToTempFile

+
+ + +
+
+ +
+ + def + indexOf(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+ + def + countMatches(*args, **kwds): + + + +
+ +
2010def _overload_dummy(*args, **kwds):
+2011    """Helper for @overload to raise when called."""
+2012    raise NotImplementedError(
+2013        "You should not call an overloaded function. "
+2014        "A series of @overload-decorated functions "
+2015        "outside a stub module should always be followed "
+2016        "by an implementation that is not @overload-ed.")
+
+ + +

Helper for @overload to raise when called.

+
+ + +
+
+ +
+
@abstractmethod
+ + def + withAppended(self, *data: int) -> IByteArray: + + + +
+ +
344    @abstractmethod
+345    def withAppended(self, *data: int) -> IByteArray:
+346        """generated source for method withAppended"""
+
+ + +

generated source for method withAppended

+
+ + +
+
+ +
+
@abstractmethod
+ + def + byteArrayOfLength(self, length: int) -> IByteArray: + + + +
+ +
355    @abstractmethod
+356    def byteArrayOfLength(self, length: int) -> IByteArray:
+357        """generated source for method byteArrayOfLength"""
+
+ + +

generated source for method byteArrayOfLength

+
+ + +
+
+ +
+
@abstractmethod
+ + def + byteArray( self, data: bytes | pyscalpel.java.bytes.JavaBytes | list[int] | str) -> IByteArray: + + + +
+ +
367    @abstractmethod
+368    def byteArray(self, data: bytes | JavaBytes | list[int] | str) -> IByteArray:
+369        """generated source for method byteArray"""
+
+ + +

generated source for method byteArray

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+
+ ByteArray = +None + + +
+ + + + +
+
+ +
+ + class + Logging(pyscalpel.java.object.JavaObject): + + + +
+ +
12class Logging(JavaObject):  # pragma: no cover
+13    """generated source for interface Logging"""
+14
+15    #
+16    #       * Obtain the current extension's standard output
+17    #       * stream. Extensions should write all output to this stream, allowing the
+18    #       * Burp user to configure how that output is handled from within the UI.
+19    #       *
+20    #       * @return The extension's standard output stream.
+21    #
+22    @abstractmethod
+23    def output(self) -> JavaObject:
+24        """generated source for method output"""
+25
+26    #
+27    #       * Obtain the current extension's standard error
+28    #       * stream. Extensions should write all error messages to this stream,
+29    #       * allowing the Burp user to configure how that output is handled from
+30    #       * within the UI.
+31    #       *
+32    #       * @return The extension's standard error stream.
+33    #
+34    @abstractmethod
+35    def error(self) -> JavaObject:
+36        """generated source for method error"""
+37
+38    #
+39    #       * This method prints a line of output to the current extension's standard
+40    #       * output stream.
+41    #       *
+42    #       * @param message The message to print.
+43    #
+44    @abstractmethod
+45    def logToOutput(self, message: str) -> None:
+46        """generated source for method logToOutput"""
+47
+48    #
+49    #       * This method prints a line of output to the current extension's standard
+50    #       * error stream.
+51    #       *
+52    #       * @param message The message to print.
+53    #
+54    @abstractmethod
+55    def error(self, message: str) -> None:
+56        """generated source for method error"""
+57
+58    #
+59    #       * This method can be used to display a debug event in the Burp Suite
+60    #       * event log.
+61    #       *
+62    #       * @param message The debug message to display.
+63    #
+64    @abstractmethod
+65    def raiseDebugEvent(self, message: str) -> None:
+66        """generated source for method raiseDebugEvent"""
+67
+68    #
+69    #       * This method can be used to display an informational event in the Burp
+70    #       * Suite event log.
+71    #       *
+72    #       * @param message The informational message to display.
+73    #
+74    @abstractmethod
+75    def raiseInfoEvent(self, message: str) -> None:
+76        """generated source for method raiseInfoEvent"""
+77
+78    #
+79    #       * This method can be used to display an error event in the Burp Suite
+80    #       * event log.
+81    #       *
+82    #       * @param message The error message to display.
+83    #
+84    @abstractmethod
+85    def raiseErrorEvent(self, message: str) -> None:
+86        """generated source for method raiseErrorEvent"""
+87
+88    #
+89    #       * This method can be used to display a critical event in the Burp Suite
+90    #       * event log.
+91    #       *
+92    #       * @param message The critical message to display.
+93    #
+94    @abstractmethod
+95    def raiseCriticalEvent(self, message: str) -> None:
+96        """generated source for method raiseCriticalEvent"""
+
+ + +

generated source for interface Logging

+
+ + +
+ +
+
@abstractmethod
+ + def + output(self) -> pyscalpel.java.object.JavaObject: + + + +
+ +
22    @abstractmethod
+23    def output(self) -> JavaObject:
+24        """generated source for method output"""
+
+ + +

generated source for method output

+
+ + +
+
+ +
+
@abstractmethod
+ + def + error(self, message: str) -> None: + + + +
+ +
54    @abstractmethod
+55    def error(self, message: str) -> None:
+56        """generated source for method error"""
+
+ + +

generated source for method error

+
+ + +
+
+ +
+
@abstractmethod
+ + def + logToOutput(self, message: str) -> None: + + + +
+ +
44    @abstractmethod
+45    def logToOutput(self, message: str) -> None:
+46        """generated source for method logToOutput"""
+
+ + +

generated source for method logToOutput

+
+ + +
+
+ +
+
@abstractmethod
+ + def + raiseDebugEvent(self, message: str) -> None: + + + +
+ +
64    @abstractmethod
+65    def raiseDebugEvent(self, message: str) -> None:
+66        """generated source for method raiseDebugEvent"""
+
+ + +

generated source for method raiseDebugEvent

+
+ + +
+
+ +
+
@abstractmethod
+ + def + raiseInfoEvent(self, message: str) -> None: + + + +
+ +
74    @abstractmethod
+75    def raiseInfoEvent(self, message: str) -> None:
+76        """generated source for method raiseInfoEvent"""
+
+ + +

generated source for method raiseInfoEvent

+
+ + +
+
+ +
+
@abstractmethod
+ + def + raiseErrorEvent(self, message: str) -> None: + + + +
+ +
84    @abstractmethod
+85    def raiseErrorEvent(self, message: str) -> None:
+86        """generated source for method raiseErrorEvent"""
+
+ + +

generated source for method raiseErrorEvent

+
+ + +
+
+ +
+
@abstractmethod
+ + def + raiseCriticalEvent(self, message: str) -> None: + + + +
+ +
94    @abstractmethod
+95    def raiseCriticalEvent(self, message: str) -> None:
+96        """generated source for method raiseCriticalEvent"""
+
+ + +

generated source for method raiseCriticalEvent

+
+ + +
+
+
Inherited Members
+
+
pyscalpel.java.object.JavaObject
+
getClass
+
hashCode
+
equals
+
clone
+
notify
+
notifyAll
+
wait
+
finalize
+ +
+
+
+
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/utils.html b/docs/public/api/pyscalpel/utils.html new file mode 100644 index 00000000..48d9ccb6 --- /dev/null +++ b/docs/public/api/pyscalpel/utils.html @@ -0,0 +1,601 @@ + + + + + + + + + pyscalpel.utils + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.utils

+ + + + + + +
 1import inspect
+ 2from typing import TypeVar, Union
+ 3from pyscalpel.burp_utils import (
+ 4    urldecode,
+ 5    urlencode_all,
+ 6)
+ 7
+ 8
+ 9T = TypeVar("T", str, bytes)
+10
+11
+12def removeprefix(s: T, prefix: Union[str, bytes]) -> T:
+13    if isinstance(s, str) and isinstance(prefix, str):
+14        if s.startswith(prefix):
+15            return s[len(prefix) :]  # type: ignore
+16    elif isinstance(s, bytes) and isinstance(prefix, bytes):
+17        if s.startswith(prefix):
+18            return s[len(prefix) :]  # type: ignore
+19    return s
+20
+21
+22def removesuffix(s: T, suffix: Union[str, bytes]) -> T:
+23    if isinstance(s, str) and isinstance(suffix, str):
+24        if s.endswith(suffix):
+25            return s[: -len(suffix)]
+26    elif isinstance(s, bytes) and isinstance(suffix, bytes):
+27        if s.endswith(suffix):
+28            return s[: -len(suffix)]
+29    return s
+30
+31
+32def current_function_name() -> str:
+33    """Get current function name
+34
+35    Returns:
+36        str: The function name
+37    """
+38    frame = inspect.currentframe()
+39    if frame is None:
+40        return ""
+41
+42    caller_frame = frame.f_back
+43    if caller_frame is None:
+44        return ""
+45
+46    return caller_frame.f_code.co_name
+47
+48
+49def get_tab_name() -> str:
+50    """Get current editor tab name
+51
+52    Returns:
+53        str: The tab name
+54    """
+55    frame = inspect.currentframe()
+56    prefixes = ("req_edit_in", "req_edit_out")
+57
+58    # Go to previous frame till the editor name is found
+59    while frame is not None:
+60        frame_name = frame.f_code.co_name
+61        for prefix in prefixes:
+62            if frame_name.startswith(prefix):
+63                return removeprefix(removeprefix(frame_name, prefix), "_")
+64
+65        frame = frame.f_back
+66
+67    raise RuntimeError("get_tab_name() wasn't called from an editor callback.")
+68
+69
+70__all__ = [
+71    "urldecode",
+72    "urlencode_all",
+73    "current_function_name",
+74]
+
+ + +
+
+ +
+ + def + urldecode(data: bytes | str, encoding='latin-1') -> bytes: + + + +
+ +
46def urldecode(data: bytes | str, encoding="latin-1") -> bytes:
+47    """URL Decode all bytes in the given bytes object"""
+48    return urllibdecode(always_bytes(data, encoding))
+
+ + +

URL Decode all bytes in the given bytes object

+
+ + +
+
+ +
+ + def + urlencode_all(data: bytes | str, encoding='latin-1') -> bytes: + + + +
+ +
41def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes:
+42    """URL Encode all bytes in the given bytes object"""
+43    return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding)
+
+ + +

URL Encode all bytes in the given bytes object

+
+ + +
+
+ +
+ + def + current_function_name() -> str: + + + +
+ +
33def current_function_name() -> str:
+34    """Get current function name
+35
+36    Returns:
+37        str: The function name
+38    """
+39    frame = inspect.currentframe()
+40    if frame is None:
+41        return ""
+42
+43    caller_frame = frame.f_back
+44    if caller_frame is None:
+45        return ""
+46
+47    return caller_frame.f_code.co_name
+
+ + +

Get current function name

+ +

Returns: + str: The function name

+
+ + +
+
+ + + +
+
+ + + diff --git a/docs/public/api/pyscalpel/venv.html b/docs/public/api/pyscalpel/venv.html new file mode 100644 index 00000000..94670eb3 --- /dev/null +++ b/docs/public/api/pyscalpel/venv.html @@ -0,0 +1,775 @@ + + + + + + + + + pyscalpel.venv + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+

+pyscalpel.venv

+ +

This module provides reimplementations of Python virtual environnements scripts

+ +

This is designed to be used internally, +but in the case where the user desires to dynamically switch venvs using this, +they should ensure the selected venv has the dependencies required by Scalpel.

+
+ + + + + +
  1"""
+  2This module provides reimplementations of Python virtual environnements scripts
+  3
+  4This is designed to be used internally, 
+  5but in the case where the user desires to dynamically switch venvs using this,
+  6they should ensure the selected venv has the dependencies required by Scalpel.
+  7"""
+  8
+  9import os
+ 10import sys
+ 11import glob
+ 12import subprocess
+ 13
+ 14_old_prefix = sys.prefix
+ 15_old_exec_prefix = sys.exec_prefix
+ 16
+ 17# Python's virtualenv's activate/deactivate ported from the bash script to Python code.
+ 18# https://docs.python.org/3/library/venv.html#:~:text=each%20provided%20path.-,How%20venvs%20work%C2%B6,-When%20a%20Python
+ 19
+ 20# pragma: no cover
+ 21
+ 22
+ 23def deactivate() -> None:  # pragma: no cover
+ 24    """Deactivates the current virtual environment."""
+ 25    if "_OLD_VIRTUAL_PATH" in os.environ:
+ 26        os.environ["PATH"] = os.environ["_OLD_VIRTUAL_PATH"]
+ 27        del os.environ["_OLD_VIRTUAL_PATH"]
+ 28    if "_OLD_VIRTUAL_PYTHONHOME" in os.environ:
+ 29        os.environ["PYTHONHOME"] = os.environ["_OLD_VIRTUAL_PYTHONHOME"]
+ 30        del os.environ["_OLD_VIRTUAL_PYTHONHOME"]
+ 31    if "VIRTUAL_ENV" in os.environ:
+ 32        del os.environ["VIRTUAL_ENV"]
+ 33
+ 34    sys.prefix = _old_prefix
+ 35    sys.exec_prefix = _old_exec_prefix
+ 36
+ 37
+ 38def activate(path: str | None) -> None:  # pragma: no cover
+ 39    """Activates the virtual environment at the given path."""
+ 40    deactivate()
+ 41
+ 42    if path is None:
+ 43        return
+ 44
+ 45    virtual_env = os.path.abspath(path)
+ 46    os.environ["_OLD_VIRTUAL_PATH"] = os.environ.get("PATH", "")
+ 47    os.environ["VIRTUAL_ENV"] = virtual_env
+ 48
+ 49    old_pythonhome = os.environ.pop("PYTHONHOME", None)
+ 50    if old_pythonhome:
+ 51        os.environ["_OLD_VIRTUAL_PYTHONHOME"] = old_pythonhome
+ 52
+ 53    if os.name == "nt":
+ 54        site_packages_paths = os.path.join(virtual_env, "Lib", "site-packages")
+ 55    else:
+ 56        site_packages_paths = glob.glob(
+ 57            os.path.join(virtual_env, "lib", "python*", "site-packages")
+ 58        )
+ 59
+ 60    if not site_packages_paths:
+ 61        raise RuntimeError(
+ 62            f"No 'site-packages' directory found in virtual environment at {virtual_env}"
+ 63        )
+ 64
+ 65    site_packages = site_packages_paths[0]
+ 66    sys.path.insert(0, site_packages)
+ 67    sys.prefix = virtual_env
+ 68    sys.exec_prefix = virtual_env
+ 69
+ 70
+ 71def install(*packages: str) -> int:  # pragma: no cover
+ 72    """Install a Python package in the current venv.
+ 73
+ 74    Returns:
+ 75        int: The pip install command exit code.
+ 76    """
+ 77    pip = os.path.join(sys.prefix, "bin", "pip")
+ 78    return subprocess.call([pip, "install", "--require-virtualenv", "--", *packages])
+ 79
+ 80
+ 81def uninstall(*packages: str) -> int:  # pragma: no cover
+ 82    """Uninstall a Python package from the current venv.
+ 83
+ 84    Returns:
+ 85        int: The pip uninstall command exit code.
+ 86    """
+ 87    pip = os.path.join(sys.prefix, "bin", "pip")
+ 88    return subprocess.call(
+ 89        [pip, "uninstall", "--require-virtualenv", "-y", "--", *packages]
+ 90    )
+ 91
+ 92
+ 93def create(path: str) -> int:  # pragma: no cover
+ 94    """Creates a Python venv on the given path
+ 95
+ 96    Returns:
+ 97        int: The `python3 -m venv` command exit code.
+ 98    """
+ 99    return subprocess.call(["python3", "-m", "venv", "--", path])
+100
+101
+102def create_default() -> str:  # pragma: no cover
+103    """Creates a default venv in the user's home directory
+104        Only creates it if the directory doesn't already exist
+105
+106    Returns:
+107        str: The venv directory path.
+108    """
+109    scalpel_venv = os.path.join(os.path.expanduser("~"), ".scalpel", "venv_default")
+110    # Don't recreate the venv if it alreay exists
+111    if not os.path.exists(scalpel_venv):
+112        os.makedirs(scalpel_venv, exist_ok=True)
+113        create(scalpel_venv)
+114    return scalpel_venv
+
+ + +
+
+ +
+ + def + deactivate() -> None: + + + +
+ +
24def deactivate() -> None:  # pragma: no cover
+25    """Deactivates the current virtual environment."""
+26    if "_OLD_VIRTUAL_PATH" in os.environ:
+27        os.environ["PATH"] = os.environ["_OLD_VIRTUAL_PATH"]
+28        del os.environ["_OLD_VIRTUAL_PATH"]
+29    if "_OLD_VIRTUAL_PYTHONHOME" in os.environ:
+30        os.environ["PYTHONHOME"] = os.environ["_OLD_VIRTUAL_PYTHONHOME"]
+31        del os.environ["_OLD_VIRTUAL_PYTHONHOME"]
+32    if "VIRTUAL_ENV" in os.environ:
+33        del os.environ["VIRTUAL_ENV"]
+34
+35    sys.prefix = _old_prefix
+36    sys.exec_prefix = _old_exec_prefix
+
+ + +

Deactivates the current virtual environment.

+
+ + +
+
+ +
+ + def + activate(path: str | None) -> None: + + + +
+ +
39def activate(path: str | None) -> None:  # pragma: no cover
+40    """Activates the virtual environment at the given path."""
+41    deactivate()
+42
+43    if path is None:
+44        return
+45
+46    virtual_env = os.path.abspath(path)
+47    os.environ["_OLD_VIRTUAL_PATH"] = os.environ.get("PATH", "")
+48    os.environ["VIRTUAL_ENV"] = virtual_env
+49
+50    old_pythonhome = os.environ.pop("PYTHONHOME", None)
+51    if old_pythonhome:
+52        os.environ["_OLD_VIRTUAL_PYTHONHOME"] = old_pythonhome
+53
+54    if os.name == "nt":
+55        site_packages_paths = os.path.join(virtual_env, "Lib", "site-packages")
+56    else:
+57        site_packages_paths = glob.glob(
+58            os.path.join(virtual_env, "lib", "python*", "site-packages")
+59        )
+60
+61    if not site_packages_paths:
+62        raise RuntimeError(
+63            f"No 'site-packages' directory found in virtual environment at {virtual_env}"
+64        )
+65
+66    site_packages = site_packages_paths[0]
+67    sys.path.insert(0, site_packages)
+68    sys.prefix = virtual_env
+69    sys.exec_prefix = virtual_env
+
+ + +

Activates the virtual environment at the given path.

+
+ + +
+
+ +
+ + def + install(*packages: str) -> int: + + + +
+ +
72def install(*packages: str) -> int:  # pragma: no cover
+73    """Install a Python package in the current venv.
+74
+75    Returns:
+76        int: The pip install command exit code.
+77    """
+78    pip = os.path.join(sys.prefix, "bin", "pip")
+79    return subprocess.call([pip, "install", "--require-virtualenv", "--", *packages])
+
+ + +

Install a Python package in the current venv.

+ +

Returns: + int: The pip install command exit code.

+
+ + +
+
+ +
+ + def + uninstall(*packages: str) -> int: + + + +
+ +
82def uninstall(*packages: str) -> int:  # pragma: no cover
+83    """Uninstall a Python package from the current venv.
+84
+85    Returns:
+86        int: The pip uninstall command exit code.
+87    """
+88    pip = os.path.join(sys.prefix, "bin", "pip")
+89    return subprocess.call(
+90        [pip, "uninstall", "--require-virtualenv", "-y", "--", *packages]
+91    )
+
+ + +

Uninstall a Python package from the current venv.

+ +

Returns: + int: The pip uninstall command exit code.

+
+ + +
+
+ +
+ + def + create(path: str) -> int: + + + +
+ +
 94def create(path: str) -> int:  # pragma: no cover
+ 95    """Creates a Python venv on the given path
+ 96
+ 97    Returns:
+ 98        int: The `python3 -m venv` command exit code.
+ 99    """
+100    return subprocess.call(["python3", "-m", "venv", "--", path])
+
+ + +

Creates a Python venv on the given path

+ +

Returns: + int: The python3 -m venv command exit code.

+
+ + +
+
+ +
+ + def + create_default() -> str: + + + +
+ +
103def create_default() -> str:  # pragma: no cover
+104    """Creates a default venv in the user's home directory
+105        Only creates it if the directory doesn't already exist
+106
+107    Returns:
+108        str: The venv directory path.
+109    """
+110    scalpel_venv = os.path.join(os.path.expanduser("~"), ".scalpel", "venv_default")
+111    # Don't recreate the venv if it alreay exists
+112    if not os.path.exists(scalpel_venv):
+113        os.makedirs(scalpel_venv, exist_ok=True)
+114        create(scalpel_venv)
+115    return scalpel_venv
+
+ + +

Creates a default venv in the user's home directory + Only creates it if the directory doesn't already exist

+ +

Returns: + str: The venv directory path.

+
+ + +
+
+ + + +
+
+ + + diff --git a/docs/public/categories/index.html b/docs/public/categories/index.html new file mode 100644 index 00000000..ebb7bc29 --- /dev/null +++ b/docs/public/categories/index.html @@ -0,0 +1,38 @@ + + + + + + + + + Categories + + + + + + + + + + + + + +
+
+
+
+
+
+ +
+
+
+
+
+
+ + + diff --git a/docs/public/categories/index.xml b/docs/public/categories/index.xml new file mode 100644 index 00000000..03982652 --- /dev/null +++ b/docs/public/categories/index.xml @@ -0,0 +1,10 @@ + + + + Categories on scalpel.org docs + /categories/ + Recent content in Categories on scalpel.org docs + Hugo -- gohugo.io + en-us + + diff --git a/docs/public/concepts-howscalpelworks/index.html b/docs/public/concepts-howscalpelworks/index.html new file mode 100644 index 00000000..23865838 --- /dev/null +++ b/docs/public/concepts-howscalpelworks/index.html @@ -0,0 +1,241 @@ + + + + + + + + + How scalpel works + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  How Scalpel works

+

#  Table of content

+ +

#  Dependencies

+
    +
  • Scalpel’s Python library is embedded in a JAR file and is unzipped when Burp loads the extension.
  • +
  • Scalpel requires external dependencies and will install them using pip when needed.
  • +
  • Scalpel will always use a virtual environment for every action. Hence, it will never modify the user’s global Python installation.
  • +
  • Scalpel relies on Jep to communicate with Python. It requires to have a JDK installed on your machine.
  • +
  • User scripts are executed in a virtual environment selected from the Scalpel tab.
  • +
  • Scalpel provides a terminal with a shell running in the selected virtual environment to easily install packages.
  • +
  • Creating new virtual environments or adding existing ones can be done via the dedicated GUI.
  • +
  • All data is stored in the ~/.scalpel directory.
  • +
+

#  Behavior

+
    +
  • Scalpel uses the Java Burp Montoya API to interact with Burp.
  • +
  • Scalpel uses Java to handle the dependencies installation, HTTP and GUI for Burp, and communication with Python.
  • +
  • Scalpel uses Jep to execute Python from Java.
  • +
  • Python execution is handled through a task queue in a dedicated thread that will execute one Python task at a time in a thread-safe way.
  • +
  • All Python hooks are executed through a _framework.py file that will activate the selected venv, load the user script file, look for callable objects matching the hooks names (match, request, response, req_edit_in, res_edit_in, req_edit_out, res_edit_out, req_edit_in_<tab_name>, res_edit_in_<tab_name>, req_edit_out_<tab_name>, res_edit_out_<tab_name>).
  • +
  • The _framework.py declares callbacks that receive Java objects, convert them to custom easy-to-use Python objects, pass the Python objects to the corresponding user hook, get back the modified Python objects and convert them back to Java objects.
  • +
  • Java code receives the hook’s result and interact with Burp to apply its effects.
  • +
  • At each task, Scalpel checks whether the user script file changed. If so, it reloads and restarts the interpreter.
  • +
+

#  Python scripting

+
    +
  • Scalpel uses a single shared interpreter. Then, if any global variables are changed in a hook, their values remain changed in the next hook calls.
  • +
  • For easy Python scripting, Scalpel provides many utilities described in the Event Hooks & API section.
  • +
+

#  Diagram

+

Here is a diagram illustating the points above: +

+
+

+ + +
+
+ + + diff --git a/docs/public/css/style.css b/docs/public/css/style.css new file mode 100644 index 00000000..d42e307e --- /dev/null +++ b/docs/public/css/style.css @@ -0,0 +1,6786 @@ +/* Background */ +.chroma { + color: #f8f8f2; + background-color: #272822; } + +/* Error */ +.chroma .err { + color: #960050; + background-color: #1e0010; } + +/* LineTableTD */ +.chroma .lntd { + vertical-align: top; + padding: 0; + margin: 0; + border: 0; } + +/* LineTable */ +.chroma .lntable { + border-spacing: 0; + padding: 0; + margin: 0; + border: 0; + width: 100%; + overflow: auto; + display: block; } + +/* LineHighlight */ +.chroma .hl { + display: block; + width: 100%; + background-color: #ffffcc; } + +/* LineNumbersTable */ +.chroma .lnt { + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + display: block; } + +/* LineNumbers */ +.chroma .ln { + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; } + +/* Keyword */ +.chroma .k { + color: #66d9ef; } + +/* KeywordConstant */ +.chroma .kc { + color: #66d9ef; } + +/* KeywordDeclaration */ +.chroma .kd { + color: #66d9ef; } + +/* KeywordNamespace */ +.chroma .kn { + color: #f92672; } + +/* KeywordPseudo */ +.chroma .kp { + color: #66d9ef; } + +/* KeywordReserved */ +.chroma .kr { + color: #66d9ef; } + +/* KeywordType */ +.chroma .kt { + color: #66d9ef; } + +/* NameAttribute */ +.chroma .na { + color: #a6e22e; } + +/* NameClass */ +.chroma .nc { + color: #a6e22e; } + +/* NameConstant */ +.chroma .no { + color: #66d9ef; } + +/* NameDecorator */ +.chroma .nd { + color: #a6e22e; } + +/* NameException */ +.chroma .ne { + color: #a6e22e; } + +/* NameFunction */ +.chroma .nf { + color: #a6e22e; } + +/* NameOther */ +.chroma .nx { + color: #a6e22e; } + +/* NameTag */ +.chroma .nt { + color: #f92672; } + +/* Literal */ +.chroma .l { + color: #ae81ff; } + +/* LiteralDate */ +.chroma .ld { + color: #e6db74; } + +/* LiteralString */ +.chroma .s { + color: #e6db74; } + +/* LiteralStringAffix */ +.chroma .sa { + color: #e6db74; } + +/* LiteralStringBacktick */ +.chroma .sb { + color: #e6db74; } + +/* LiteralStringChar */ +.chroma .sc { + color: #e6db74; } + +/* LiteralStringDelimiter */ +.chroma .dl { + color: #e6db74; } + +/* LiteralStringDoc */ +.chroma .sd { + color: #e6db74; } + +/* LiteralStringDouble */ +.chroma .s2 { + color: #e6db74; } + +/* LiteralStringEscape */ +.chroma .se { + color: #ae81ff; } + +/* LiteralStringHeredoc */ +.chroma .sh { + color: #e6db74; } + +/* LiteralStringInterpol */ +.chroma .si { + color: #e6db74; } + +/* LiteralStringOther */ +.chroma .sx { + color: #e6db74; } + +/* LiteralStringRegex */ +.chroma .sr { + color: #e6db74; } + +/* LiteralStringSingle */ +.chroma .s1 { + color: #e6db74; } + +/* LiteralStringSymbol */ +.chroma .ss { + color: #e6db74; } + +/* LiteralNumber */ +.chroma .m { + color: #ae81ff; } + +/* LiteralNumberBin */ +.chroma .mb { + color: #ae81ff; } + +/* LiteralNumberFloat */ +.chroma .mf { + color: #ae81ff; } + +/* LiteralNumberHex */ +.chroma .mh { + color: #ae81ff; } + +/* LiteralNumberInteger */ +.chroma .mi { + color: #ae81ff; } + +/* LiteralNumberIntegerLong */ +.chroma .il { + color: #ae81ff; } + +/* LiteralNumberOct */ +.chroma .mo { + color: #ae81ff; } + +/* Operator */ +.chroma .o { + color: #f92672; } + +/* OperatorWord */ +.chroma .ow { + color: #f92672; } + +/* Comment */ +.chroma .c { + color: #75715e; } + +/* CommentHashbang */ +.chroma .ch { + color: #75715e; } + +/* CommentMultiline */ +.chroma .cm { + color: #75715e; } + +/* CommentSingle */ +.chroma .c1 { + color: #75715e; } + +/* CommentSpecial */ +.chroma .cs { + color: #75715e; } + +/* CommentPreproc */ +.chroma .cp { + color: #75715e; } + +/* CommentPreprocFile */ +.chroma .cpf { + color: #75715e; } + +/* GenericDeleted */ +.chroma .gd { + color: #f92672; } + +/* GenericEmph */ +.chroma .ge { + font-style: italic; } + +/* GenericInserted */ +.chroma .gi { + color: #a6e22e; } + +/* GenericStrong */ +.chroma .gs { + font-weight: bold; } + +/* GenericSubheading */ +.chroma .gu { + color: #75715e; } + +.badge { + color: #fff; + background-color: #6c757d; + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 1; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; } + .badge:empty { + display: none; } + +@keyframes spinAround { + from { + transform: rotate(0deg); } + to { + transform: rotate(359deg); } } + +/*! minireset.css v0.0.2 | MIT License | github.com/jgthms/minireset.css */ +html, +body, +p, +ol, +ul, +li, +dl, +dt, +dd, +blockquote, +figure, +fieldset, +legend, +textarea, +pre, +iframe, +hr, +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + padding: 0; } + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: 100%; + font-weight: normal; } + +ul { + list-style: none; } + +button, +input, +select, +textarea { + margin: 0; } + +html { + box-sizing: border-box; } + +* { + box-sizing: inherit; } + *:before, *:after { + box-sizing: inherit; } + +img, +embed, +object, +audio, +video { + max-width: 100%; } + +iframe { + border: 0; } + +table { + border-collapse: collapse; + border-spacing: 0; } + +td, +th { + padding: 0; + text-align: left; } + +html { + background-color: white; + font-size: 16px; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + min-width: 300px; + overflow-x: hidden; + overflow-y: scroll; + text-rendering: optimizeLegibility; + text-size-adjust: 100%; } + +article, +aside, +figure, +footer, +header, +hgroup, +section { + display: block; } + +body, +button, +input, +select, +textarea { + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif, "Font Awesome 5 Free", "Font Awesome 5 Brands"; } + +code, +pre { + -moz-osx-font-smoothing: auto; + -webkit-font-smoothing: auto; + font-family: monospace; } + +body { + color: #4a4a4a; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; } + +a { + color: #3273dc; + cursor: pointer; + text-decoration: none; } + a strong { + color: currentColor; } + a:hover { + color: #363636; } + +code { + background-color: whitesmoke; + color: #ff3860; + font-size: 0.875em; + font-weight: normal; + padding: 0.25em 0.5em 0.25em; } + +hr { + background-color: #dbdbdb; + border: none; + display: block; + height: 1px; + margin: 1.5rem 0; } + +img { + height: auto; + max-width: 100%; } + +input[type="checkbox"], +input[type="radio"] { + vertical-align: baseline; } + +small { + font-size: 0.875em; } + +span { + font-style: inherit; + font-weight: inherit; } + +strong { + color: #363636; + font-weight: 700; } + +pre { + -webkit-overflow-scrolling: touch; + background-color: whitesmoke; + color: #4a4a4a; + font-size: 0.875em; + overflow-x: auto; + padding: 1.25rem 1.5rem; + white-space: pre; + word-wrap: normal; } + pre code { + background-color: transparent; + color: currentColor; + font-size: 1em; + padding: 0; } + +table td, +table th { + text-align: left; + vertical-align: top; } + +table th { + color: #363636; } + +.is-clearfix:after { + clear: both; + content: " "; + display: table; } + +.is-pulled-left { + float: left !important; } + +.is-pulled-right { + float: right !important; } + +.is-clipped { + overflow: hidden !important; } + +.is-overlay { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; } + +.is-size-1 { + font-size: 3rem !important; } + +.is-size-2 { + font-size: 2.5rem !important; } + +.is-size-3 { + font-size: 2rem !important; } + +.is-size-4 { + font-size: 1.5rem !important; } + +.is-size-5 { + font-size: 1.25rem !important; } + +.is-size-6 { + font-size: 1rem !important; } + +.is-size-7 { + font-size: 0.75rem !important; } + +@media screen and (max-width: 768px) { + .is-size-1-mobile { + font-size: 3rem !important; } + .is-size-2-mobile { + font-size: 2.5rem !important; } + .is-size-3-mobile { + font-size: 2rem !important; } + .is-size-4-mobile { + font-size: 1.5rem !important; } + .is-size-5-mobile { + font-size: 1.25rem !important; } + .is-size-6-mobile { + font-size: 1rem !important; } + .is-size-7-mobile { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 769px), print { + .is-size-1-tablet { + font-size: 3rem !important; } + .is-size-2-tablet { + font-size: 2.5rem !important; } + .is-size-3-tablet { + font-size: 2rem !important; } + .is-size-4-tablet { + font-size: 1.5rem !important; } + .is-size-5-tablet { + font-size: 1.25rem !important; } + .is-size-6-tablet { + font-size: 1rem !important; } + .is-size-7-tablet { + font-size: 0.75rem !important; } } + +@media screen and (max-width: 1023px) { + .is-size-1-touch { + font-size: 3rem !important; } + .is-size-2-touch { + font-size: 2.5rem !important; } + .is-size-3-touch { + font-size: 2rem !important; } + .is-size-4-touch { + font-size: 1.5rem !important; } + .is-size-5-touch { + font-size: 1.25rem !important; } + .is-size-6-touch { + font-size: 1rem !important; } + .is-size-7-touch { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 1024px) { + .is-size-1-desktop { + font-size: 3rem !important; } + .is-size-2-desktop { + font-size: 2.5rem !important; } + .is-size-3-desktop { + font-size: 2rem !important; } + .is-size-4-desktop { + font-size: 1.5rem !important; } + .is-size-5-desktop { + font-size: 1.25rem !important; } + .is-size-6-desktop { + font-size: 1rem !important; } + .is-size-7-desktop { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 1216px) { + .is-size-1-widescreen { + font-size: 3rem !important; } + .is-size-2-widescreen { + font-size: 2.5rem !important; } + .is-size-3-widescreen { + font-size: 2rem !important; } + .is-size-4-widescreen { + font-size: 1.5rem !important; } + .is-size-5-widescreen { + font-size: 1.25rem !important; } + .is-size-6-widescreen { + font-size: 1rem !important; } + .is-size-7-widescreen { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 1408px) { + .is-size-1-fullhd { + font-size: 3rem !important; } + .is-size-2-fullhd { + font-size: 2.5rem !important; } + .is-size-3-fullhd { + font-size: 2rem !important; } + .is-size-4-fullhd { + font-size: 1.5rem !important; } + .is-size-5-fullhd { + font-size: 1.25rem !important; } + .is-size-6-fullhd { + font-size: 1rem !important; } + .is-size-7-fullhd { + font-size: 0.75rem !important; } } + +.has-text-centered { + text-align: center !important; } + +@media screen and (max-width: 768px) { + .has-text-centered-mobile { + text-align: center !important; } } + +@media screen and (min-width: 769px), print { + .has-text-centered-tablet { + text-align: center !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-centered-tablet-only { + text-align: center !important; } } + +@media screen and (max-width: 1023px) { + .has-text-centered-touch { + text-align: center !important; } } + +@media screen and (min-width: 1024px) { + .has-text-centered-desktop { + text-align: center !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-centered-desktop-only { + text-align: center !important; } } + +@media screen and (min-width: 1216px) { + .has-text-centered-widescreen { + text-align: center !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-centered-widescreen-only { + text-align: center !important; } } + +@media screen and (min-width: 1408px) { + .has-text-centered-fullhd { + text-align: center !important; } } + +.has-text-justified { + text-align: justify !important; } + +@media screen and (max-width: 768px) { + .has-text-justified-mobile { + text-align: justify !important; } } + +@media screen and (min-width: 769px), print { + .has-text-justified-tablet { + text-align: justify !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-justified-tablet-only { + text-align: justify !important; } } + +@media screen and (max-width: 1023px) { + .has-text-justified-touch { + text-align: justify !important; } } + +@media screen and (min-width: 1024px) { + .has-text-justified-desktop { + text-align: justify !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-justified-desktop-only { + text-align: justify !important; } } + +@media screen and (min-width: 1216px) { + .has-text-justified-widescreen { + text-align: justify !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-justified-widescreen-only { + text-align: justify !important; } } + +@media screen and (min-width: 1408px) { + .has-text-justified-fullhd { + text-align: justify !important; } } + +.has-text-left { + text-align: left !important; } + +@media screen and (max-width: 768px) { + .has-text-left-mobile { + text-align: left !important; } } + +@media screen and (min-width: 769px), print { + .has-text-left-tablet { + text-align: left !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-left-tablet-only { + text-align: left !important; } } + +@media screen and (max-width: 1023px) { + .has-text-left-touch { + text-align: left !important; } } + +@media screen and (min-width: 1024px) { + .has-text-left-desktop { + text-align: left !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-left-desktop-only { + text-align: left !important; } } + +@media screen and (min-width: 1216px) { + .has-text-left-widescreen { + text-align: left !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-left-widescreen-only { + text-align: left !important; } } + +@media screen and (min-width: 1408px) { + .has-text-left-fullhd { + text-align: left !important; } } + +.has-text-right { + text-align: right !important; } + +@media screen and (max-width: 768px) { + .has-text-right-mobile { + text-align: right !important; } } + +@media screen and (min-width: 769px), print { + .has-text-right-tablet { + text-align: right !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-right-tablet-only { + text-align: right !important; } } + +@media screen and (max-width: 1023px) { + .has-text-right-touch { + text-align: right !important; } } + +@media screen and (min-width: 1024px) { + .has-text-right-desktop { + text-align: right !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-right-desktop-only { + text-align: right !important; } } + +@media screen and (min-width: 1216px) { + .has-text-right-widescreen { + text-align: right !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-right-widescreen-only { + text-align: right !important; } } + +@media screen and (min-width: 1408px) { + .has-text-right-fullhd { + text-align: right !important; } } + +.is-capitalized { + text-transform: capitalize !important; } + +.is-lowercase { + text-transform: lowercase !important; } + +.is-uppercase { + text-transform: uppercase !important; } + +.has-text-white { + color: white !important; } + +a.has-text-white:hover, a.has-text-white:focus { + color: #e6e6e6 !important; } + +.has-text-black { + color: #0a0a0a !important; } + +a.has-text-black:hover, a.has-text-black:focus { + color: black !important; } + +.has-text-light { + color: whitesmoke !important; } + +a.has-text-light:hover, a.has-text-light:focus { + color: #dbdbdb !important; } + +.has-text-dark { + color: #363636 !important; } + +a.has-text-dark:hover, a.has-text-dark:focus { + color: #1c1c1c !important; } + +.has-text-primary { + color: #C93312 !important; } + +a.has-text-primary:hover, a.has-text-primary:focus { + color: #9a270e !important; } + +.has-text-link { + color: #3273dc !important; } + +a.has-text-link:hover, a.has-text-link:focus { + color: #205bbc !important; } + +.has-text-info { + color: #209cee !important; } + +a.has-text-info:hover, a.has-text-info:focus { + color: #0f81cc !important; } + +.has-text-success { + color: #23d160 !important; } + +a.has-text-success:hover, a.has-text-success:focus { + color: #1ca64c !important; } + +.has-text-warning { + color: #ffdd57 !important; } + +a.has-text-warning:hover, a.has-text-warning:focus { + color: #ffd324 !important; } + +.has-text-danger { + color: #ff3860 !important; } + +a.has-text-danger:hover, a.has-text-danger:focus { + color: #ff0537 !important; } + +.has-text-black-bis { + color: #121212 !important; } + +.has-text-black-ter { + color: #242424 !important; } + +.has-text-grey-darker { + color: #363636 !important; } + +.has-text-grey-dark { + color: #4a4a4a !important; } + +.has-text-grey { + color: #7a7a7a !important; } + +.has-text-grey-light { + color: #b5b5b5 !important; } + +.has-text-grey-lighter { + color: #dbdbdb !important; } + +.has-text-white-ter { + color: whitesmoke !important; } + +.has-text-white-bis { + color: #fafafa !important; } + +.has-text-weight-light { + font-weight: 300 !important; } + +.has-text-weight-normal { + font-weight: 400 !important; } + +.has-text-weight-semibold { + font-weight: 600 !important; } + +.has-text-weight-bold { + font-weight: 700 !important; } + +.is-block { + display: block !important; } + +@media screen and (max-width: 768px) { + .is-block-mobile { + display: block !important; } } + +@media screen and (min-width: 769px), print { + .is-block-tablet { + display: block !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-block-tablet-only { + display: block !important; } } + +@media screen and (max-width: 1023px) { + .is-block-touch { + display: block !important; } } + +@media screen and (min-width: 1024px) { + .is-block-desktop { + display: block !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-block-desktop-only { + display: block !important; } } + +@media screen and (min-width: 1216px) { + .is-block-widescreen { + display: block !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-block-widescreen-only { + display: block !important; } } + +@media screen and (min-width: 1408px) { + .is-block-fullhd { + display: block !important; } } + +.is-flex { + display: flex !important; } + +@media screen and (max-width: 768px) { + .is-flex-mobile { + display: flex !important; } } + +@media screen and (min-width: 769px), print { + .is-flex-tablet { + display: flex !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-flex-tablet-only { + display: flex !important; } } + +@media screen and (max-width: 1023px) { + .is-flex-touch { + display: flex !important; } } + +@media screen and (min-width: 1024px) { + .is-flex-desktop { + display: flex !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-flex-desktop-only { + display: flex !important; } } + +@media screen and (min-width: 1216px) { + .is-flex-widescreen { + display: flex !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-flex-widescreen-only { + display: flex !important; } } + +@media screen and (min-width: 1408px) { + .is-flex-fullhd { + display: flex !important; } } + +.is-inline { + display: inline !important; } + +@media screen and (max-width: 768px) { + .is-inline-mobile { + display: inline !important; } } + +@media screen and (min-width: 769px), print { + .is-inline-tablet { + display: inline !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-inline-tablet-only { + display: inline !important; } } + +@media screen and (max-width: 1023px) { + .is-inline-touch { + display: inline !important; } } + +@media screen and (min-width: 1024px) { + .is-inline-desktop { + display: inline !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-inline-desktop-only { + display: inline !important; } } + +@media screen and (min-width: 1216px) { + .is-inline-widescreen { + display: inline !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-inline-widescreen-only { + display: inline !important; } } + +@media screen and (min-width: 1408px) { + .is-inline-fullhd { + display: inline !important; } } + +.is-inline-block { + display: inline-block !important; } + +@media screen and (max-width: 768px) { + .is-inline-block-mobile { + display: inline-block !important; } } + +@media screen and (min-width: 769px), print { + .is-inline-block-tablet { + display: inline-block !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-inline-block-tablet-only { + display: inline-block !important; } } + +@media screen and (max-width: 1023px) { + .is-inline-block-touch { + display: inline-block !important; } } + +@media screen and (min-width: 1024px) { + .is-inline-block-desktop { + display: inline-block !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-inline-block-desktop-only { + display: inline-block !important; } } + +@media screen and (min-width: 1216px) { + .is-inline-block-widescreen { + display: inline-block !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-inline-block-widescreen-only { + display: inline-block !important; } } + +@media screen and (min-width: 1408px) { + .is-inline-block-fullhd { + display: inline-block !important; } } + +.is-inline-flex { + display: inline-flex !important; } + +@media screen and (max-width: 768px) { + .is-inline-flex-mobile { + display: inline-flex !important; } } + +@media screen and (min-width: 769px), print { + .is-inline-flex-tablet { + display: inline-flex !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-inline-flex-tablet-only { + display: inline-flex !important; } } + +@media screen and (max-width: 1023px) { + .is-inline-flex-touch { + display: inline-flex !important; } } + +@media screen and (min-width: 1024px) { + .is-inline-flex-desktop { + display: inline-flex !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-inline-flex-desktop-only { + display: inline-flex !important; } } + +@media screen and (min-width: 1216px) { + .is-inline-flex-widescreen { + display: inline-flex !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-inline-flex-widescreen-only { + display: inline-flex !important; } } + +@media screen and (min-width: 1408px) { + .is-inline-flex-fullhd { + display: inline-flex !important; } } + +.is-hidden { + display: none !important; } + +@media screen and (max-width: 768px) { + .is-hidden-mobile { + display: none !important; } } + +@media screen and (min-width: 769px), print { + .is-hidden-tablet { + display: none !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-hidden-tablet-only { + display: none !important; } } + +@media screen and (max-width: 1023px) { + .is-hidden-touch { + display: none !important; } } + +@media screen and (min-width: 1024px) { + .is-hidden-desktop { + display: none !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-hidden-desktop-only { + display: none !important; } } + +@media screen and (min-width: 1216px) { + .is-hidden-widescreen { + display: none !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-hidden-widescreen-only { + display: none !important; } } + +@media screen and (min-width: 1408px) { + .is-hidden-fullhd { + display: none !important; } } + +.is-invisible { + visibility: hidden !important; } + +@media screen and (max-width: 768px) { + .is-invisible-mobile { + visibility: hidden !important; } } + +@media screen and (min-width: 769px), print { + .is-invisible-tablet { + visibility: hidden !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-invisible-tablet-only { + visibility: hidden !important; } } + +@media screen and (max-width: 1023px) { + .is-invisible-touch { + visibility: hidden !important; } } + +@media screen and (min-width: 1024px) { + .is-invisible-desktop { + visibility: hidden !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-invisible-desktop-only { + visibility: hidden !important; } } + +@media screen and (min-width: 1216px) { + .is-invisible-widescreen { + visibility: hidden !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-invisible-widescreen-only { + visibility: hidden !important; } } + +@media screen and (min-width: 1408px) { + .is-invisible-fullhd { + visibility: hidden !important; } } + +.is-marginless { + margin: 0 !important; } + +.is-paddingless { + padding: 0 !important; } + +.is-radiusless { + border-radius: 0 !important; } + +.is-shadowless { + box-shadow: none !important; } + +.is-unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +.column { + display: block; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 1; + padding: 0.75rem; } + .columns.is-mobile > .column.is-narrow { + flex: none; } + .columns.is-mobile > .column.is-full { + flex: none; + width: 100%; } + .columns.is-mobile > .column.is-three-quarters { + flex: none; + width: 75%; } + .columns.is-mobile > .column.is-two-thirds { + flex: none; + width: 66.6666%; } + .columns.is-mobile > .column.is-half { + flex: none; + width: 50%; } + .columns.is-mobile > .column.is-one-third { + flex: none; + width: 33.3333%; } + .columns.is-mobile > .column.is-one-quarter { + flex: none; + width: 25%; } + .columns.is-mobile > .column.is-one-fifth { + flex: none; + width: 20%; } + .columns.is-mobile > .column.is-two-fifths { + flex: none; + width: 40%; } + .columns.is-mobile > .column.is-three-fifths { + flex: none; + width: 60%; } + .columns.is-mobile > .column.is-four-fifths { + flex: none; + width: 80%; } + .columns.is-mobile > .column.is-offset-three-quarters { + margin-left: 75%; } + .columns.is-mobile > .column.is-offset-two-thirds { + margin-left: 66.6666%; } + .columns.is-mobile > .column.is-offset-half { + margin-left: 50%; } + .columns.is-mobile > .column.is-offset-one-third { + margin-left: 33.3333%; } + .columns.is-mobile > .column.is-offset-one-quarter { + margin-left: 25%; } + .columns.is-mobile > .column.is-offset-one-fifth { + margin-left: 20%; } + .columns.is-mobile > .column.is-offset-two-fifths { + margin-left: 40%; } + .columns.is-mobile > .column.is-offset-three-fifths { + margin-left: 60%; } + .columns.is-mobile > .column.is-offset-four-fifths { + margin-left: 80%; } + .columns.is-mobile > .column.is-1 { + flex: none; + width: 8.33333%; } + .columns.is-mobile > .column.is-offset-1 { + margin-left: 8.33333%; } + .columns.is-mobile > .column.is-2 { + flex: none; + width: 16.66667%; } + .columns.is-mobile > .column.is-offset-2 { + margin-left: 16.66667%; } + .columns.is-mobile > .column.is-3 { + flex: none; + width: 25%; } + .columns.is-mobile > .column.is-offset-3 { + margin-left: 25%; } + .columns.is-mobile > .column.is-4 { + flex: none; + width: 33.33333%; } + .columns.is-mobile > .column.is-offset-4 { + margin-left: 33.33333%; } + .columns.is-mobile > .column.is-5 { + flex: none; + width: 41.66667%; } + .columns.is-mobile > .column.is-offset-5 { + margin-left: 41.66667%; } + .columns.is-mobile > .column.is-6 { + flex: none; + width: 50%; } + .columns.is-mobile > .column.is-offset-6 { + margin-left: 50%; } + .columns.is-mobile > .column.is-7 { + flex: none; + width: 58.33333%; } + .columns.is-mobile > .column.is-offset-7 { + margin-left: 58.33333%; } + .columns.is-mobile > .column.is-8 { + flex: none; + width: 66.66667%; } + .columns.is-mobile > .column.is-offset-8 { + margin-left: 66.66667%; } + .columns.is-mobile > .column.is-9 { + flex: none; + width: 75%; } + .columns.is-mobile > .column.is-offset-9 { + margin-left: 75%; } + .columns.is-mobile > .column.is-10 { + flex: none; + width: 83.33333%; } + .columns.is-mobile > .column.is-offset-10 { + margin-left: 83.33333%; } + .columns.is-mobile > .column.is-11 { + flex: none; + width: 91.66667%; } + .columns.is-mobile > .column.is-offset-11 { + margin-left: 91.66667%; } + .columns.is-mobile > .column.is-12 { + flex: none; + width: 100%; } + .columns.is-mobile > .column.is-offset-12 { + margin-left: 100%; } + @media screen and (max-width: 768px) { + .column.is-narrow-mobile { + flex: none; } + .column.is-full-mobile { + flex: none; + width: 100%; } + .column.is-three-quarters-mobile { + flex: none; + width: 75%; } + .column.is-two-thirds-mobile { + flex: none; + width: 66.6666%; } + .column.is-half-mobile { + flex: none; + width: 50%; } + .column.is-one-third-mobile { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-mobile { + flex: none; + width: 25%; } + .column.is-one-fifth-mobile { + flex: none; + width: 20%; } + .column.is-two-fifths-mobile { + flex: none; + width: 40%; } + .column.is-three-fifths-mobile { + flex: none; + width: 60%; } + .column.is-four-fifths-mobile { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-mobile { + margin-left: 75%; } + .column.is-offset-two-thirds-mobile { + margin-left: 66.6666%; } + .column.is-offset-half-mobile { + margin-left: 50%; } + .column.is-offset-one-third-mobile { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-mobile { + margin-left: 25%; } + .column.is-offset-one-fifth-mobile { + margin-left: 20%; } + .column.is-offset-two-fifths-mobile { + margin-left: 40%; } + .column.is-offset-three-fifths-mobile { + margin-left: 60%; } + .column.is-offset-four-fifths-mobile { + margin-left: 80%; } + .column.is-1-mobile { + flex: none; + width: 8.33333%; } + .column.is-offset-1-mobile { + margin-left: 8.33333%; } + .column.is-2-mobile { + flex: none; + width: 16.66667%; } + .column.is-offset-2-mobile { + margin-left: 16.66667%; } + .column.is-3-mobile { + flex: none; + width: 25%; } + .column.is-offset-3-mobile { + margin-left: 25%; } + .column.is-4-mobile { + flex: none; + width: 33.33333%; } + .column.is-offset-4-mobile { + margin-left: 33.33333%; } + .column.is-5-mobile { + flex: none; + width: 41.66667%; } + .column.is-offset-5-mobile { + margin-left: 41.66667%; } + .column.is-6-mobile { + flex: none; + width: 50%; } + .column.is-offset-6-mobile { + margin-left: 50%; } + .column.is-7-mobile { + flex: none; + width: 58.33333%; } + .column.is-offset-7-mobile { + margin-left: 58.33333%; } + .column.is-8-mobile { + flex: none; + width: 66.66667%; } + .column.is-offset-8-mobile { + margin-left: 66.66667%; } + .column.is-9-mobile { + flex: none; + width: 75%; } + .column.is-offset-9-mobile { + margin-left: 75%; } + .column.is-10-mobile { + flex: none; + width: 83.33333%; } + .column.is-offset-10-mobile { + margin-left: 83.33333%; } + .column.is-11-mobile { + flex: none; + width: 91.66667%; } + .column.is-offset-11-mobile { + margin-left: 91.66667%; } + .column.is-12-mobile { + flex: none; + width: 100%; } + .column.is-offset-12-mobile { + margin-left: 100%; } } + @media screen and (min-width: 769px), print { + .column.is-narrow, .column.is-narrow-tablet { + flex: none; } + .column.is-full, .column.is-full-tablet { + flex: none; + width: 100%; } + .column.is-three-quarters, .column.is-three-quarters-tablet { + flex: none; + width: 75%; } + .column.is-two-thirds, .column.is-two-thirds-tablet { + flex: none; + width: 66.6666%; } + .column.is-half, .column.is-half-tablet { + flex: none; + width: 50%; } + .column.is-one-third, .column.is-one-third-tablet { + flex: none; + width: 33.3333%; } + .column.is-one-quarter, .column.is-one-quarter-tablet { + flex: none; + width: 25%; } + .column.is-one-fifth, .column.is-one-fifth-tablet { + flex: none; + width: 20%; } + .column.is-two-fifths, .column.is-two-fifths-tablet { + flex: none; + width: 40%; } + .column.is-three-fifths, .column.is-three-fifths-tablet { + flex: none; + width: 60%; } + .column.is-four-fifths, .column.is-four-fifths-tablet { + flex: none; + width: 80%; } + .column.is-offset-three-quarters, .column.is-offset-three-quarters-tablet { + margin-left: 75%; } + .column.is-offset-two-thirds, .column.is-offset-two-thirds-tablet { + margin-left: 66.6666%; } + .column.is-offset-half, .column.is-offset-half-tablet { + margin-left: 50%; } + .column.is-offset-one-third, .column.is-offset-one-third-tablet { + margin-left: 33.3333%; } + .column.is-offset-one-quarter, .column.is-offset-one-quarter-tablet { + margin-left: 25%; } + .column.is-offset-one-fifth, .column.is-offset-one-fifth-tablet { + margin-left: 20%; } + .column.is-offset-two-fifths, .column.is-offset-two-fifths-tablet { + margin-left: 40%; } + .column.is-offset-three-fifths, .column.is-offset-three-fifths-tablet { + margin-left: 60%; } + .column.is-offset-four-fifths, .column.is-offset-four-fifths-tablet { + margin-left: 80%; } + .column.is-1, .column.is-1-tablet { + flex: none; + width: 8.33333%; } + .column.is-offset-1, .column.is-offset-1-tablet { + margin-left: 8.33333%; } + .column.is-2, .column.is-2-tablet { + flex: none; + width: 16.66667%; } + .column.is-offset-2, .column.is-offset-2-tablet { + margin-left: 16.66667%; } + .column.is-3, .column.is-3-tablet { + flex: none; + width: 25%; } + .column.is-offset-3, .column.is-offset-3-tablet { + margin-left: 25%; } + .column.is-4, .column.is-4-tablet { + flex: none; + width: 33.33333%; } + .column.is-offset-4, .column.is-offset-4-tablet { + margin-left: 33.33333%; } + .column.is-5, .column.is-5-tablet { + flex: none; + width: 41.66667%; } + .column.is-offset-5, .column.is-offset-5-tablet { + margin-left: 41.66667%; } + .column.is-6, .column.is-6-tablet { + flex: none; + width: 50%; } + .column.is-offset-6, .column.is-offset-6-tablet { + margin-left: 50%; } + .column.is-7, .column.is-7-tablet { + flex: none; + width: 58.33333%; } + .column.is-offset-7, .column.is-offset-7-tablet { + margin-left: 58.33333%; } + .column.is-8, .column.is-8-tablet { + flex: none; + width: 66.66667%; } + .column.is-offset-8, .column.is-offset-8-tablet { + margin-left: 66.66667%; } + .column.is-9, .column.is-9-tablet { + flex: none; + width: 75%; } + .column.is-offset-9, .column.is-offset-9-tablet { + margin-left: 75%; } + .column.is-10, .column.is-10-tablet { + flex: none; + width: 83.33333%; } + .column.is-offset-10, .column.is-offset-10-tablet { + margin-left: 83.33333%; } + .column.is-11, .column.is-11-tablet { + flex: none; + width: 91.66667%; } + .column.is-offset-11, .column.is-offset-11-tablet { + margin-left: 91.66667%; } + .column.is-12, .column.is-12-tablet { + flex: none; + width: 100%; } + .column.is-offset-12, .column.is-offset-12-tablet { + margin-left: 100%; } } + @media screen and (max-width: 1023px) { + .column.is-narrow-touch { + flex: none; } + .column.is-full-touch { + flex: none; + width: 100%; } + .column.is-three-quarters-touch { + flex: none; + width: 75%; } + .column.is-two-thirds-touch { + flex: none; + width: 66.6666%; } + .column.is-half-touch { + flex: none; + width: 50%; } + .column.is-one-third-touch { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-touch { + flex: none; + width: 25%; } + .column.is-one-fifth-touch { + flex: none; + width: 20%; } + .column.is-two-fifths-touch { + flex: none; + width: 40%; } + .column.is-three-fifths-touch { + flex: none; + width: 60%; } + .column.is-four-fifths-touch { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-touch { + margin-left: 75%; } + .column.is-offset-two-thirds-touch { + margin-left: 66.6666%; } + .column.is-offset-half-touch { + margin-left: 50%; } + .column.is-offset-one-third-touch { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-touch { + margin-left: 25%; } + .column.is-offset-one-fifth-touch { + margin-left: 20%; } + .column.is-offset-two-fifths-touch { + margin-left: 40%; } + .column.is-offset-three-fifths-touch { + margin-left: 60%; } + .column.is-offset-four-fifths-touch { + margin-left: 80%; } + .column.is-1-touch { + flex: none; + width: 8.33333%; } + .column.is-offset-1-touch { + margin-left: 8.33333%; } + .column.is-2-touch { + flex: none; + width: 16.66667%; } + .column.is-offset-2-touch { + margin-left: 16.66667%; } + .column.is-3-touch { + flex: none; + width: 25%; } + .column.is-offset-3-touch { + margin-left: 25%; } + .column.is-4-touch { + flex: none; + width: 33.33333%; } + .column.is-offset-4-touch { + margin-left: 33.33333%; } + .column.is-5-touch { + flex: none; + width: 41.66667%; } + .column.is-offset-5-touch { + margin-left: 41.66667%; } + .column.is-6-touch { + flex: none; + width: 50%; } + .column.is-offset-6-touch { + margin-left: 50%; } + .column.is-7-touch { + flex: none; + width: 58.33333%; } + .column.is-offset-7-touch { + margin-left: 58.33333%; } + .column.is-8-touch { + flex: none; + width: 66.66667%; } + .column.is-offset-8-touch { + margin-left: 66.66667%; } + .column.is-9-touch { + flex: none; + width: 75%; } + .column.is-offset-9-touch { + margin-left: 75%; } + .column.is-10-touch { + flex: none; + width: 83.33333%; } + .column.is-offset-10-touch { + margin-left: 83.33333%; } + .column.is-11-touch { + flex: none; + width: 91.66667%; } + .column.is-offset-11-touch { + margin-left: 91.66667%; } + .column.is-12-touch { + flex: none; + width: 100%; } + .column.is-offset-12-touch { + margin-left: 100%; } } + @media screen and (min-width: 1024px) { + .column.is-narrow-desktop { + flex: none; } + .column.is-full-desktop { + flex: none; + width: 100%; } + .column.is-three-quarters-desktop { + flex: none; + width: 75%; } + .column.is-two-thirds-desktop { + flex: none; + width: 66.6666%; } + .column.is-half-desktop { + flex: none; + width: 50%; } + .column.is-one-third-desktop { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-desktop { + flex: none; + width: 25%; } + .column.is-one-fifth-desktop { + flex: none; + width: 20%; } + .column.is-two-fifths-desktop { + flex: none; + width: 40%; } + .column.is-three-fifths-desktop { + flex: none; + width: 60%; } + .column.is-four-fifths-desktop { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-desktop { + margin-left: 75%; } + .column.is-offset-two-thirds-desktop { + margin-left: 66.6666%; } + .column.is-offset-half-desktop { + margin-left: 50%; } + .column.is-offset-one-third-desktop { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-desktop { + margin-left: 25%; } + .column.is-offset-one-fifth-desktop { + margin-left: 20%; } + .column.is-offset-two-fifths-desktop { + margin-left: 40%; } + .column.is-offset-three-fifths-desktop { + margin-left: 60%; } + .column.is-offset-four-fifths-desktop { + margin-left: 80%; } + .column.is-1-desktop { + flex: none; + width: 8.33333%; } + .column.is-offset-1-desktop { + margin-left: 8.33333%; } + .column.is-2-desktop { + flex: none; + width: 16.66667%; } + .column.is-offset-2-desktop { + margin-left: 16.66667%; } + .column.is-3-desktop { + flex: none; + width: 25%; } + .column.is-offset-3-desktop { + margin-left: 25%; } + .column.is-4-desktop { + flex: none; + width: 33.33333%; } + .column.is-offset-4-desktop { + margin-left: 33.33333%; } + .column.is-5-desktop { + flex: none; + width: 41.66667%; } + .column.is-offset-5-desktop { + margin-left: 41.66667%; } + .column.is-6-desktop { + flex: none; + width: 50%; } + .column.is-offset-6-desktop { + margin-left: 50%; } + .column.is-7-desktop { + flex: none; + width: 58.33333%; } + .column.is-offset-7-desktop { + margin-left: 58.33333%; } + .column.is-8-desktop { + flex: none; + width: 66.66667%; } + .column.is-offset-8-desktop { + margin-left: 66.66667%; } + .column.is-9-desktop { + flex: none; + width: 75%; } + .column.is-offset-9-desktop { + margin-left: 75%; } + .column.is-10-desktop { + flex: none; + width: 83.33333%; } + .column.is-offset-10-desktop { + margin-left: 83.33333%; } + .column.is-11-desktop { + flex: none; + width: 91.66667%; } + .column.is-offset-11-desktop { + margin-left: 91.66667%; } + .column.is-12-desktop { + flex: none; + width: 100%; } + .column.is-offset-12-desktop { + margin-left: 100%; } } + @media screen and (min-width: 1216px) { + .column.is-narrow-widescreen { + flex: none; } + .column.is-full-widescreen { + flex: none; + width: 100%; } + .column.is-three-quarters-widescreen { + flex: none; + width: 75%; } + .column.is-two-thirds-widescreen { + flex: none; + width: 66.6666%; } + .column.is-half-widescreen { + flex: none; + width: 50%; } + .column.is-one-third-widescreen { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-widescreen { + flex: none; + width: 25%; } + .column.is-one-fifth-widescreen { + flex: none; + width: 20%; } + .column.is-two-fifths-widescreen { + flex: none; + width: 40%; } + .column.is-three-fifths-widescreen { + flex: none; + width: 60%; } + .column.is-four-fifths-widescreen { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-widescreen { + margin-left: 75%; } + .column.is-offset-two-thirds-widescreen { + margin-left: 66.6666%; } + .column.is-offset-half-widescreen { + margin-left: 50%; } + .column.is-offset-one-third-widescreen { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-widescreen { + margin-left: 25%; } + .column.is-offset-one-fifth-widescreen { + margin-left: 20%; } + .column.is-offset-two-fifths-widescreen { + margin-left: 40%; } + .column.is-offset-three-fifths-widescreen { + margin-left: 60%; } + .column.is-offset-four-fifths-widescreen { + margin-left: 80%; } + .column.is-1-widescreen { + flex: none; + width: 8.33333%; } + .column.is-offset-1-widescreen { + margin-left: 8.33333%; } + .column.is-2-widescreen { + flex: none; + width: 16.66667%; } + .column.is-offset-2-widescreen { + margin-left: 16.66667%; } + .column.is-3-widescreen { + flex: none; + width: 25%; } + .column.is-offset-3-widescreen { + margin-left: 25%; } + .column.is-4-widescreen { + flex: none; + width: 33.33333%; } + .column.is-offset-4-widescreen { + margin-left: 33.33333%; } + .column.is-5-widescreen { + flex: none; + width: 41.66667%; } + .column.is-offset-5-widescreen { + margin-left: 41.66667%; } + .column.is-6-widescreen { + flex: none; + width: 50%; } + .column.is-offset-6-widescreen { + margin-left: 50%; } + .column.is-7-widescreen { + flex: none; + width: 58.33333%; } + .column.is-offset-7-widescreen { + margin-left: 58.33333%; } + .column.is-8-widescreen { + flex: none; + width: 66.66667%; } + .column.is-offset-8-widescreen { + margin-left: 66.66667%; } + .column.is-9-widescreen { + flex: none; + width: 75%; } + .column.is-offset-9-widescreen { + margin-left: 75%; } + .column.is-10-widescreen { + flex: none; + width: 83.33333%; } + .column.is-offset-10-widescreen { + margin-left: 83.33333%; } + .column.is-11-widescreen { + flex: none; + width: 91.66667%; } + .column.is-offset-11-widescreen { + margin-left: 91.66667%; } + .column.is-12-widescreen { + flex: none; + width: 100%; } + .column.is-offset-12-widescreen { + margin-left: 100%; } } + @media screen and (min-width: 1408px) { + .column.is-narrow-fullhd { + flex: none; } + .column.is-full-fullhd { + flex: none; + width: 100%; } + .column.is-three-quarters-fullhd { + flex: none; + width: 75%; } + .column.is-two-thirds-fullhd { + flex: none; + width: 66.6666%; } + .column.is-half-fullhd { + flex: none; + width: 50%; } + .column.is-one-third-fullhd { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-fullhd { + flex: none; + width: 25%; } + .column.is-one-fifth-fullhd { + flex: none; + width: 20%; } + .column.is-two-fifths-fullhd { + flex: none; + width: 40%; } + .column.is-three-fifths-fullhd { + flex: none; + width: 60%; } + .column.is-four-fifths-fullhd { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-fullhd { + margin-left: 75%; } + .column.is-offset-two-thirds-fullhd { + margin-left: 66.6666%; } + .column.is-offset-half-fullhd { + margin-left: 50%; } + .column.is-offset-one-third-fullhd { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-fullhd { + margin-left: 25%; } + .column.is-offset-one-fifth-fullhd { + margin-left: 20%; } + .column.is-offset-two-fifths-fullhd { + margin-left: 40%; } + .column.is-offset-three-fifths-fullhd { + margin-left: 60%; } + .column.is-offset-four-fifths-fullhd { + margin-left: 80%; } + .column.is-1-fullhd { + flex: none; + width: 8.33333%; } + .column.is-offset-1-fullhd { + margin-left: 8.33333%; } + .column.is-2-fullhd { + flex: none; + width: 16.66667%; } + .column.is-offset-2-fullhd { + margin-left: 16.66667%; } + .column.is-3-fullhd { + flex: none; + width: 25%; } + .column.is-offset-3-fullhd { + margin-left: 25%; } + .column.is-4-fullhd { + flex: none; + width: 33.33333%; } + .column.is-offset-4-fullhd { + margin-left: 33.33333%; } + .column.is-5-fullhd { + flex: none; + width: 41.66667%; } + .column.is-offset-5-fullhd { + margin-left: 41.66667%; } + .column.is-6-fullhd { + flex: none; + width: 50%; } + .column.is-offset-6-fullhd { + margin-left: 50%; } + .column.is-7-fullhd { + flex: none; + width: 58.33333%; } + .column.is-offset-7-fullhd { + margin-left: 58.33333%; } + .column.is-8-fullhd { + flex: none; + width: 66.66667%; } + .column.is-offset-8-fullhd { + margin-left: 66.66667%; } + .column.is-9-fullhd { + flex: none; + width: 75%; } + .column.is-offset-9-fullhd { + margin-left: 75%; } + .column.is-10-fullhd { + flex: none; + width: 83.33333%; } + .column.is-offset-10-fullhd { + margin-left: 83.33333%; } + .column.is-11-fullhd { + flex: none; + width: 91.66667%; } + .column.is-offset-11-fullhd { + margin-left: 91.66667%; } + .column.is-12-fullhd { + flex: none; + width: 100%; } + .column.is-offset-12-fullhd { + margin-left: 100%; } } + +.columns { + margin-left: -0.75rem; + margin-right: -0.75rem; + margin-top: -0.75rem; } + .columns:last-child { + margin-bottom: -0.75rem; } + .columns:not(:last-child) { + margin-bottom: calc(1.5rem - 0.75rem); } + .columns.is-centered { + justify-content: center; } + .columns.is-gapless { + margin-left: 0; + margin-right: 0; + margin-top: 0; } + .columns.is-gapless > .column { + margin: 0; + padding: 0 !important; } + .columns.is-gapless:not(:last-child) { + margin-bottom: 1.5rem; } + .columns.is-gapless:last-child { + margin-bottom: 0; } + .columns.is-mobile { + display: flex; } + .columns.is-multiline { + flex-wrap: wrap; } + .columns.is-vcentered { + align-items: center; } + @media screen and (min-width: 769px), print { + .columns:not(.is-desktop) { + display: flex; } } + @media screen and (min-width: 1024px) { + .columns.is-desktop { + display: flex; } } + +.columns.is-variable { + --columnGap: 0.75rem; + margin-left: calc(-1 * var(--columnGap)); + margin-right: calc(-1 * var(--columnGap)); } + .columns.is-variable .column { + padding-left: var(--columnGap); + padding-right: var(--columnGap); } + .columns.is-variable.is-0 { + --columnGap: 0rem; } + .columns.is-variable.is-1 { + --columnGap: 0.25rem; } + .columns.is-variable.is-2 { + --columnGap: 0.5rem; } + .columns.is-variable.is-3 { + --columnGap: 0.75rem; } + .columns.is-variable.is-4 { + --columnGap: 1rem; } + .columns.is-variable.is-5 { + --columnGap: 1.25rem; } + .columns.is-variable.is-6 { + --columnGap: 1.5rem; } + .columns.is-variable.is-7 { + --columnGap: 1.75rem; } + .columns.is-variable.is-8 { + --columnGap: 2rem; } + +.tile { + align-items: stretch; + display: block; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 1; + min-height: min-content; } + .tile.is-ancestor { + margin-left: -0.75rem; + margin-right: -0.75rem; + margin-top: -0.75rem; } + .tile.is-ancestor:last-child { + margin-bottom: -0.75rem; } + .tile.is-ancestor:not(:last-child) { + margin-bottom: 0.75rem; } + .tile.is-child { + margin: 0 !important; } + .tile.is-parent { + padding: 0.75rem; } + .tile.is-vertical { + flex-direction: column; } + .tile.is-vertical > .tile.is-child:not(:last-child) { + margin-bottom: 1.5rem !important; } + @media screen and (min-width: 769px), print { + .tile:not(.is-child) { + display: flex; } + .tile.is-1 { + flex: none; + width: 8.33333%; } + .tile.is-2 { + flex: none; + width: 16.66667%; } + .tile.is-3 { + flex: none; + width: 25%; } + .tile.is-4 { + flex: none; + width: 33.33333%; } + .tile.is-5 { + flex: none; + width: 41.66667%; } + .tile.is-6 { + flex: none; + width: 50%; } + .tile.is-7 { + flex: none; + width: 58.33333%; } + .tile.is-8 { + flex: none; + width: 66.66667%; } + .tile.is-9 { + flex: none; + width: 75%; } + .tile.is-10 { + flex: none; + width: 83.33333%; } + .tile.is-11 { + flex: none; + width: 91.66667%; } + .tile.is-12 { + flex: none; + width: 100%; } } + +.box { + background-color: white; + border-radius: 5px; + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + color: #4a4a4a; + display: block; + padding: 1.25rem; } + .box:not(:last-child) { + margin-bottom: 1.5rem; } + +a.box:hover, a.box:focus { + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px #3273dc; } + +a.box:active { + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.2), 0 0 0 1px #3273dc; } + +.button { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: white; + border-color: #dbdbdb; + color: #363636; + cursor: pointer; + justify-content: center; + padding-left: 0.75em; + padding-right: 0.75em; + text-align: center; + white-space: nowrap; } + .button:focus, .button.is-focused, .button:active, .button.is-active { + outline: none; } + .button[disabled] { + cursor: not-allowed; } + .button strong { + color: inherit; } + .button .icon, .button .icon.is-small, .button .icon.is-medium, .button .icon.is-large { + height: 1.5em; + width: 1.5em; } + .button .icon:first-child:not(:last-child) { + margin-left: calc(-0.375em - 1px); + margin-right: 0.1875em; } + .button .icon:last-child:not(:first-child) { + margin-left: 0.1875em; + margin-right: calc(-0.375em - 1px); } + .button .icon:first-child:last-child { + margin-left: calc(-0.375em - 1px); + margin-right: calc(-0.375em - 1px); } + .button:hover, .button.is-hovered { + border-color: #b5b5b5; + color: #363636; } + .button:focus, .button.is-focused { + border-color: #3273dc; + color: #363636; } + .button:focus:not(:active), .button.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .button:active, .button.is-active { + border-color: #4a4a4a; + color: #363636; } + .button.is-text { + background-color: transparent; + border-color: transparent; + color: #4a4a4a; + text-decoration: underline; } + .button.is-text:hover, .button.is-text.is-hovered, .button.is-text:focus, .button.is-text.is-focused { + background-color: whitesmoke; + color: #363636; } + .button.is-text:active, .button.is-text.is-active { + background-color: #e8e8e8; + color: #363636; } + .button.is-text[disabled] { + background-color: transparent; + border-color: transparent; + box-shadow: none; } + .button.is-white { + background-color: white; + border-color: transparent; + color: #0a0a0a; } + .button.is-white:hover, .button.is-white.is-hovered { + background-color: #f9f9f9; + border-color: transparent; + color: #0a0a0a; } + .button.is-white:focus, .button.is-white.is-focused { + border-color: transparent; + color: #0a0a0a; } + .button.is-white:focus:not(:active), .button.is-white.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); } + .button.is-white:active, .button.is-white.is-active { + background-color: #f2f2f2; + border-color: transparent; + color: #0a0a0a; } + .button.is-white[disabled] { + background-color: white; + border-color: transparent; + box-shadow: none; } + .button.is-white.is-inverted { + background-color: #0a0a0a; + color: white; } + .button.is-white.is-inverted:hover { + background-color: black; } + .button.is-white.is-inverted[disabled] { + background-color: #0a0a0a; + border-color: transparent; + box-shadow: none; + color: white; } + .button.is-white.is-loading:after { + border-color: transparent transparent #0a0a0a #0a0a0a !important; } + .button.is-white.is-outlined { + background-color: transparent; + border-color: white; + color: white; } + .button.is-white.is-outlined:hover, .button.is-white.is-outlined:focus { + background-color: white; + border-color: white; + color: #0a0a0a; } + .button.is-white.is-outlined.is-loading:after { + border-color: transparent transparent white white !important; } + .button.is-white.is-outlined[disabled] { + background-color: transparent; + border-color: white; + box-shadow: none; + color: white; } + .button.is-white.is-inverted.is-outlined { + background-color: transparent; + border-color: #0a0a0a; + color: #0a0a0a; } + .button.is-white.is-inverted.is-outlined:hover, .button.is-white.is-inverted.is-outlined:focus { + background-color: #0a0a0a; + color: white; } + .button.is-white.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #0a0a0a; + box-shadow: none; + color: #0a0a0a; } + .button.is-black { + background-color: #0a0a0a; + border-color: transparent; + color: white; } + .button.is-black:hover, .button.is-black.is-hovered { + background-color: #040404; + border-color: transparent; + color: white; } + .button.is-black:focus, .button.is-black.is-focused { + border-color: transparent; + color: white; } + .button.is-black:focus:not(:active), .button.is-black.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); } + .button.is-black:active, .button.is-black.is-active { + background-color: black; + border-color: transparent; + color: white; } + .button.is-black[disabled] { + background-color: #0a0a0a; + border-color: transparent; + box-shadow: none; } + .button.is-black.is-inverted { + background-color: white; + color: #0a0a0a; } + .button.is-black.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-black.is-inverted[disabled] { + background-color: white; + border-color: transparent; + box-shadow: none; + color: #0a0a0a; } + .button.is-black.is-loading:after { + border-color: transparent transparent white white !important; } + .button.is-black.is-outlined { + background-color: transparent; + border-color: #0a0a0a; + color: #0a0a0a; } + .button.is-black.is-outlined:hover, .button.is-black.is-outlined:focus { + background-color: #0a0a0a; + border-color: #0a0a0a; + color: white; } + .button.is-black.is-outlined.is-loading:after { + border-color: transparent transparent #0a0a0a #0a0a0a !important; } + .button.is-black.is-outlined[disabled] { + background-color: transparent; + border-color: #0a0a0a; + box-shadow: none; + color: #0a0a0a; } + .button.is-black.is-inverted.is-outlined { + background-color: transparent; + border-color: white; + color: white; } + .button.is-black.is-inverted.is-outlined:hover, .button.is-black.is-inverted.is-outlined:focus { + background-color: white; + color: #0a0a0a; } + .button.is-black.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: white; + box-shadow: none; + color: white; } + .button.is-light { + background-color: whitesmoke; + border-color: transparent; + color: #363636; } + .button.is-light:hover, .button.is-light.is-hovered { + background-color: #eeeeee; + border-color: transparent; + color: #363636; } + .button.is-light:focus, .button.is-light.is-focused { + border-color: transparent; + color: #363636; } + .button.is-light:focus:not(:active), .button.is-light.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); } + .button.is-light:active, .button.is-light.is-active { + background-color: #e8e8e8; + border-color: transparent; + color: #363636; } + .button.is-light[disabled] { + background-color: whitesmoke; + border-color: transparent; + box-shadow: none; } + .button.is-light.is-inverted { + background-color: #363636; + color: whitesmoke; } + .button.is-light.is-inverted:hover { + background-color: #292929; } + .button.is-light.is-inverted[disabled] { + background-color: #363636; + border-color: transparent; + box-shadow: none; + color: whitesmoke; } + .button.is-light.is-loading:after { + border-color: transparent transparent #363636 #363636 !important; } + .button.is-light.is-outlined { + background-color: transparent; + border-color: whitesmoke; + color: whitesmoke; } + .button.is-light.is-outlined:hover, .button.is-light.is-outlined:focus { + background-color: whitesmoke; + border-color: whitesmoke; + color: #363636; } + .button.is-light.is-outlined.is-loading:after { + border-color: transparent transparent whitesmoke whitesmoke !important; } + .button.is-light.is-outlined[disabled] { + background-color: transparent; + border-color: whitesmoke; + box-shadow: none; + color: whitesmoke; } + .button.is-light.is-inverted.is-outlined { + background-color: transparent; + border-color: #363636; + color: #363636; } + .button.is-light.is-inverted.is-outlined:hover, .button.is-light.is-inverted.is-outlined:focus { + background-color: #363636; + color: whitesmoke; } + .button.is-light.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #363636; + box-shadow: none; + color: #363636; } + .button.is-dark { + background-color: #363636; + border-color: transparent; + color: whitesmoke; } + .button.is-dark:hover, .button.is-dark.is-hovered { + background-color: #2f2f2f; + border-color: transparent; + color: whitesmoke; } + .button.is-dark:focus, .button.is-dark.is-focused { + border-color: transparent; + color: whitesmoke; } + .button.is-dark:focus:not(:active), .button.is-dark.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); } + .button.is-dark:active, .button.is-dark.is-active { + background-color: #292929; + border-color: transparent; + color: whitesmoke; } + .button.is-dark[disabled] { + background-color: #363636; + border-color: transparent; + box-shadow: none; } + .button.is-dark.is-inverted { + background-color: whitesmoke; + color: #363636; } + .button.is-dark.is-inverted:hover { + background-color: #e8e8e8; } + .button.is-dark.is-inverted[disabled] { + background-color: whitesmoke; + border-color: transparent; + box-shadow: none; + color: #363636; } + .button.is-dark.is-loading:after { + border-color: transparent transparent whitesmoke whitesmoke !important; } + .button.is-dark.is-outlined { + background-color: transparent; + border-color: #363636; + color: #363636; } + .button.is-dark.is-outlined:hover, .button.is-dark.is-outlined:focus { + background-color: #363636; + border-color: #363636; + color: whitesmoke; } + .button.is-dark.is-outlined.is-loading:after { + border-color: transparent transparent #363636 #363636 !important; } + .button.is-dark.is-outlined[disabled] { + background-color: transparent; + border-color: #363636; + box-shadow: none; + color: #363636; } + .button.is-dark.is-inverted.is-outlined { + background-color: transparent; + border-color: whitesmoke; + color: whitesmoke; } + .button.is-dark.is-inverted.is-outlined:hover, .button.is-dark.is-inverted.is-outlined:focus { + background-color: whitesmoke; + color: #363636; } + .button.is-dark.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: whitesmoke; + box-shadow: none; + color: whitesmoke; } + .button.is-primary { + background-color: #C93312; + border-color: transparent; + color: #fff; } + .button.is-primary:hover, .button.is-primary.is-hovered { + background-color: #bd3011; + border-color: transparent; + color: #fff; } + .button.is-primary:focus, .button.is-primary.is-focused { + border-color: transparent; + color: #fff; } + .button.is-primary:focus:not(:active), .button.is-primary.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(201, 51, 18, 0.25); } + .button.is-primary:active, .button.is-primary.is-active { + background-color: #b22d10; + border-color: transparent; + color: #fff; } + .button.is-primary[disabled] { + background-color: #C93312; + border-color: transparent; + box-shadow: none; } + .button.is-primary.is-inverted { + background-color: #fff; + color: #C93312; } + .button.is-primary.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-primary.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #C93312; } + .button.is-primary.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-primary.is-outlined { + background-color: transparent; + border-color: #C93312; + color: #C93312; } + .button.is-primary.is-outlined:hover, .button.is-primary.is-outlined:focus { + background-color: #C93312; + border-color: #C93312; + color: #fff; } + .button.is-primary.is-outlined.is-loading:after { + border-color: transparent transparent #C93312 #C93312 !important; } + .button.is-primary.is-outlined[disabled] { + background-color: transparent; + border-color: #C93312; + box-shadow: none; + color: #C93312; } + .button.is-primary.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-primary.is-inverted.is-outlined:hover, .button.is-primary.is-inverted.is-outlined:focus { + background-color: #fff; + color: #C93312; } + .button.is-primary.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-link { + background-color: #3273dc; + border-color: transparent; + color: #fff; } + .button.is-link:hover, .button.is-link.is-hovered { + background-color: #276cda; + border-color: transparent; + color: #fff; } + .button.is-link:focus, .button.is-link.is-focused { + border-color: transparent; + color: #fff; } + .button.is-link:focus:not(:active), .button.is-link.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .button.is-link:active, .button.is-link.is-active { + background-color: #2366d1; + border-color: transparent; + color: #fff; } + .button.is-link[disabled] { + background-color: #3273dc; + border-color: transparent; + box-shadow: none; } + .button.is-link.is-inverted { + background-color: #fff; + color: #3273dc; } + .button.is-link.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-link.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #3273dc; } + .button.is-link.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-link.is-outlined { + background-color: transparent; + border-color: #3273dc; + color: #3273dc; } + .button.is-link.is-outlined:hover, .button.is-link.is-outlined:focus { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; } + .button.is-link.is-outlined.is-loading:after { + border-color: transparent transparent #3273dc #3273dc !important; } + .button.is-link.is-outlined[disabled] { + background-color: transparent; + border-color: #3273dc; + box-shadow: none; + color: #3273dc; } + .button.is-link.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-link.is-inverted.is-outlined:hover, .button.is-link.is-inverted.is-outlined:focus { + background-color: #fff; + color: #3273dc; } + .button.is-link.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-info { + background-color: #209cee; + border-color: transparent; + color: #fff; } + .button.is-info:hover, .button.is-info.is-hovered { + background-color: #1496ed; + border-color: transparent; + color: #fff; } + .button.is-info:focus, .button.is-info.is-focused { + border-color: transparent; + color: #fff; } + .button.is-info:focus:not(:active), .button.is-info.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(32, 156, 238, 0.25); } + .button.is-info:active, .button.is-info.is-active { + background-color: #118fe4; + border-color: transparent; + color: #fff; } + .button.is-info[disabled] { + background-color: #209cee; + border-color: transparent; + box-shadow: none; } + .button.is-info.is-inverted { + background-color: #fff; + color: #209cee; } + .button.is-info.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-info.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #209cee; } + .button.is-info.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-info.is-outlined { + background-color: transparent; + border-color: #209cee; + color: #209cee; } + .button.is-info.is-outlined:hover, .button.is-info.is-outlined:focus { + background-color: #209cee; + border-color: #209cee; + color: #fff; } + .button.is-info.is-outlined.is-loading:after { + border-color: transparent transparent #209cee #209cee !important; } + .button.is-info.is-outlined[disabled] { + background-color: transparent; + border-color: #209cee; + box-shadow: none; + color: #209cee; } + .button.is-info.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-info.is-inverted.is-outlined:hover, .button.is-info.is-inverted.is-outlined:focus { + background-color: #fff; + color: #209cee; } + .button.is-info.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-success { + background-color: #23d160; + border-color: transparent; + color: #fff; } + .button.is-success:hover, .button.is-success.is-hovered { + background-color: #22c65b; + border-color: transparent; + color: #fff; } + .button.is-success:focus, .button.is-success.is-focused { + border-color: transparent; + color: #fff; } + .button.is-success:focus:not(:active), .button.is-success.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(35, 209, 96, 0.25); } + .button.is-success:active, .button.is-success.is-active { + background-color: #20bc56; + border-color: transparent; + color: #fff; } + .button.is-success[disabled] { + background-color: #23d160; + border-color: transparent; + box-shadow: none; } + .button.is-success.is-inverted { + background-color: #fff; + color: #23d160; } + .button.is-success.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-success.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #23d160; } + .button.is-success.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-success.is-outlined { + background-color: transparent; + border-color: #23d160; + color: #23d160; } + .button.is-success.is-outlined:hover, .button.is-success.is-outlined:focus { + background-color: #23d160; + border-color: #23d160; + color: #fff; } + .button.is-success.is-outlined.is-loading:after { + border-color: transparent transparent #23d160 #23d160 !important; } + .button.is-success.is-outlined[disabled] { + background-color: transparent; + border-color: #23d160; + box-shadow: none; + color: #23d160; } + .button.is-success.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-success.is-inverted.is-outlined:hover, .button.is-success.is-inverted.is-outlined:focus { + background-color: #fff; + color: #23d160; } + .button.is-success.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-warning { + background-color: #ffdd57; + border-color: transparent; + color: #FFFFFF; } + .button.is-warning:hover, .button.is-warning.is-hovered { + background-color: #ffdb4a; + border-color: transparent; + color: #FFFFFF; } + .button.is-warning:focus, .button.is-warning.is-focused { + border-color: transparent; + color: #FFFFFF; } + .button.is-warning:focus:not(:active), .button.is-warning.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(255, 221, 87, 0.25); } + .button.is-warning:active, .button.is-warning.is-active { + background-color: #ffd83d; + border-color: transparent; + color: #FFFFFF; } + .button.is-warning[disabled] { + background-color: #ffdd57; + border-color: transparent; + box-shadow: none; } + .button.is-warning.is-inverted { + background-color: #FFFFFF; + color: #ffdd57; } + .button.is-warning.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-warning.is-inverted[disabled] { + background-color: #FFFFFF; + border-color: transparent; + box-shadow: none; + color: #ffdd57; } + .button.is-warning.is-loading:after { + border-color: transparent transparent #FFFFFF #FFFFFF !important; } + .button.is-warning.is-outlined { + background-color: transparent; + border-color: #ffdd57; + color: #ffdd57; } + .button.is-warning.is-outlined:hover, .button.is-warning.is-outlined:focus { + background-color: #ffdd57; + border-color: #ffdd57; + color: #FFFFFF; } + .button.is-warning.is-outlined.is-loading:after { + border-color: transparent transparent #ffdd57 #ffdd57 !important; } + .button.is-warning.is-outlined[disabled] { + background-color: transparent; + border-color: #ffdd57; + box-shadow: none; + color: #ffdd57; } + .button.is-warning.is-inverted.is-outlined { + background-color: transparent; + border-color: #FFFFFF; + color: #FFFFFF; } + .button.is-warning.is-inverted.is-outlined:hover, .button.is-warning.is-inverted.is-outlined:focus { + background-color: #FFFFFF; + color: #ffdd57; } + .button.is-warning.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #FFFFFF; + box-shadow: none; + color: #FFFFFF; } + .button.is-danger { + background-color: #ff3860; + border-color: transparent; + color: #fff; } + .button.is-danger:hover, .button.is-danger.is-hovered { + background-color: #ff2b56; + border-color: transparent; + color: #fff; } + .button.is-danger:focus, .button.is-danger.is-focused { + border-color: transparent; + color: #fff; } + .button.is-danger:focus:not(:active), .button.is-danger.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(255, 56, 96, 0.25); } + .button.is-danger:active, .button.is-danger.is-active { + background-color: #ff1f4b; + border-color: transparent; + color: #fff; } + .button.is-danger[disabled] { + background-color: #ff3860; + border-color: transparent; + box-shadow: none; } + .button.is-danger.is-inverted { + background-color: #fff; + color: #ff3860; } + .button.is-danger.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-danger.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #ff3860; } + .button.is-danger.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-danger.is-outlined { + background-color: transparent; + border-color: #ff3860; + color: #ff3860; } + .button.is-danger.is-outlined:hover, .button.is-danger.is-outlined:focus { + background-color: #ff3860; + border-color: #ff3860; + color: #fff; } + .button.is-danger.is-outlined.is-loading:after { + border-color: transparent transparent #ff3860 #ff3860 !important; } + .button.is-danger.is-outlined[disabled] { + background-color: transparent; + border-color: #ff3860; + box-shadow: none; + color: #ff3860; } + .button.is-danger.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-danger.is-inverted.is-outlined:hover, .button.is-danger.is-inverted.is-outlined:focus { + background-color: #fff; + color: #ff3860; } + .button.is-danger.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-small { + border-radius: 2px; + font-size: 0.75rem; } + .button.is-medium { + font-size: 1.25rem; } + .button.is-large { + font-size: 1.5rem; } + .button[disabled] { + background-color: white; + border-color: #dbdbdb; + box-shadow: none; + opacity: 0.5; } + .button.is-fullwidth { + display: flex; + width: 100%; } + .button.is-loading { + color: transparent !important; + pointer-events: none; } + .button.is-loading:after { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; + position: absolute; + left: calc(50% - (1em / 2)); + top: calc(50% - (1em / 2)); + position: absolute !important; } + .button.is-static { + background-color: whitesmoke; + border-color: #dbdbdb; + color: #7a7a7a; + box-shadow: none; + pointer-events: none; } + +.buttons { + align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; } + .buttons .button { + margin-bottom: 0.5rem; } + .buttons .button:not(:last-child) { + margin-right: 0.5rem; } + .buttons:last-child { + margin-bottom: -0.5rem; } + .buttons:not(:last-child) { + margin-bottom: 1rem; } + .buttons.has-addons .button:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .buttons.has-addons .button:not(:last-child) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + margin-right: -1px; } + .buttons.has-addons .button:last-child { + margin-right: 0; } + .buttons.has-addons .button:hover, .buttons.has-addons .button.is-hovered { + z-index: 2; } + .buttons.has-addons .button:focus, .buttons.has-addons .button.is-focused, .buttons.has-addons .button:active, .buttons.has-addons .button.is-active, .buttons.has-addons .button.is-selected { + z-index: 3; } + .buttons.has-addons .button:focus:hover, .buttons.has-addons .button.is-focused:hover, .buttons.has-addons .button:active:hover, .buttons.has-addons .button.is-active:hover, .buttons.has-addons .button.is-selected:hover { + z-index: 4; } + .buttons.is-centered { + justify-content: center; } + .buttons.is-right { + justify-content: flex-end; } + +.container { + margin: 0 auto; + position: relative; } + @media screen and (min-width: 1024px) { + .container { + max-width: 960px; + width: 960px; } + .container.is-fluid { + margin-left: 32px; + margin-right: 32px; + max-width: none; + width: auto; } } + @media screen and (max-width: 1215px) { + .container.is-widescreen { + max-width: 1152px; + width: auto; } } + @media screen and (max-width: 1407px) { + .container.is-fullhd { + max-width: 1344px; + width: auto; } } + @media screen and (min-width: 1216px) { + .container { + max-width: 1152px; + width: 1152px; } } + @media screen and (min-width: 1408px) { + .container { + max-width: 1344px; + width: 1344px; } } + +.content:not(:last-child) { + margin-bottom: 1.5rem; } + +.content li + li { + margin-top: 0.25em; } + +.content p:not(:last-child), +.content dl:not(:last-child), +.content ol:not(:last-child), +.content ul:not(:last-child), +.content blockquote:not(:last-child), +.content pre:not(:last-child), +.content table:not(:last-child) { + margin-bottom: 1em; } + +.content h1, +.content h2, +.content h3, +.content h4, +.content h5, +.content h6 { + color: #363636; + font-weight: 400; + line-height: 1.125; } + +.content h1 { + font-size: 2em; + margin-bottom: 0.5em; } + .content h1:not(:first-child) { + margin-top: 1em; } + +.content h2 { + font-size: 1.75em; + margin-bottom: 0.5714em; } + .content h2:not(:first-child) { + margin-top: 1.1428em; } + +.content h3 { + font-size: 1.5em; + margin-bottom: 0.6666em; } + .content h3:not(:first-child) { + margin-top: 1.3333em; } + +.content h4 { + font-size: 1.25em; + margin-bottom: 0.8em; } + +.content h5 { + font-size: 1.125em; + margin-bottom: 0.8888em; } + +.content h6 { + font-size: 1em; + margin-bottom: 1em; } + +.content blockquote { + background-color: whitesmoke; + border-left: 5px solid #dbdbdb; + padding: 1.25em 1.5em; } + +.content ol { + list-style: decimal outside; + margin-left: 2em; + margin-top: 1em; } + +.content ul { + list-style: disc outside; + margin-left: 2em; + margin-top: 1em; } + .content ul ul { + list-style-type: circle; + margin-top: 0.5em; } + .content ul ul ul { + list-style-type: square; } + +.content dd { + margin-left: 2em; } + +.content figure { + margin-left: 2em; + margin-right: 2em; + text-align: center; } + .content figure:not(:first-child) { + margin-top: 2em; } + .content figure:not(:last-child) { + margin-bottom: 2em; } + .content figure img { + display: inline-block; } + .content figure figcaption { + font-style: italic; } + +.content pre { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + padding: 1.25em 1.5em; + white-space: pre; + word-wrap: normal; } + +.content sup, +.content sub { + font-size: 75%; } + +.content table { + width: 100%; } + .content table td, + .content table th { + border: 1px solid #dbdbdb; + border-width: 0 0 1px; + padding: 0.5em 0.75em; + vertical-align: top; } + .content table th { + color: #363636; + text-align: left; } + .content table tr:hover { + background-color: whitesmoke; } + .content table thead td, + .content table thead th { + border-width: 0 0 2px; + color: #363636; } + .content table tfoot td, + .content table tfoot th { + border-width: 2px 0 0; + color: #363636; } + .content table tbody tr:last-child td, + .content table tbody tr:last-child th { + border-bottom-width: 0; } + +.content.is-small { + font-size: 0.75rem; } + +.content.is-medium { + font-size: 1.25rem; } + +.content.is-large { + font-size: 1.5rem; } + +.input, +.textarea { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + background-color: white; + border-color: #dbdbdb; + color: #363636; + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); + max-width: 100%; + width: 100%; } + .input:focus, .input.is-focused, .input:active, .input.is-active, + .textarea:focus, + .textarea.is-focused, + .textarea:active, + .textarea.is-active { + outline: none; } + .input[disabled], + .textarea[disabled] { + cursor: not-allowed; } + .input::-moz-placeholder, + .textarea::-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input::-webkit-input-placeholder, + .textarea::-webkit-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input:-moz-placeholder, + .textarea:-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input:-ms-input-placeholder, + .textarea:-ms-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input:hover, .input.is-hovered, + .textarea:hover, + .textarea.is-hovered { + border-color: #b5b5b5; } + .input:focus, .input.is-focused, .input:active, .input.is-active, + .textarea:focus, + .textarea.is-focused, + .textarea:active, + .textarea.is-active { + border-color: #3273dc; + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .input[disabled], + .textarea[disabled] { + background-color: whitesmoke; + border-color: whitesmoke; + box-shadow: none; + color: #7a7a7a; } + .input[disabled]::-moz-placeholder, + .textarea[disabled]::-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[disabled]::-webkit-input-placeholder, + .textarea[disabled]::-webkit-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[disabled]:-moz-placeholder, + .textarea[disabled]:-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[disabled]:-ms-input-placeholder, + .textarea[disabled]:-ms-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[type="search"], + .textarea[type="search"] { + border-radius: 290486px; } + .input[readonly], + .textarea[readonly] { + box-shadow: none; } + .input.is-white, + .textarea.is-white { + border-color: white; } + .input.is-white:focus, .input.is-white.is-focused, .input.is-white:active, .input.is-white.is-active, + .textarea.is-white:focus, + .textarea.is-white.is-focused, + .textarea.is-white:active, + .textarea.is-white.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); } + .input.is-black, + .textarea.is-black { + border-color: #0a0a0a; } + .input.is-black:focus, .input.is-black.is-focused, .input.is-black:active, .input.is-black.is-active, + .textarea.is-black:focus, + .textarea.is-black.is-focused, + .textarea.is-black:active, + .textarea.is-black.is-active { + box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); } + .input.is-light, + .textarea.is-light { + border-color: whitesmoke; } + .input.is-light:focus, .input.is-light.is-focused, .input.is-light:active, .input.is-light.is-active, + .textarea.is-light:focus, + .textarea.is-light.is-focused, + .textarea.is-light:active, + .textarea.is-light.is-active { + box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); } + .input.is-dark, + .textarea.is-dark { + border-color: #363636; } + .input.is-dark:focus, .input.is-dark.is-focused, .input.is-dark:active, .input.is-dark.is-active, + .textarea.is-dark:focus, + .textarea.is-dark.is-focused, + .textarea.is-dark:active, + .textarea.is-dark.is-active { + box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); } + .input.is-primary, + .textarea.is-primary { + border-color: #C93312; } + .input.is-primary:focus, .input.is-primary.is-focused, .input.is-primary:active, .input.is-primary.is-active, + .textarea.is-primary:focus, + .textarea.is-primary.is-focused, + .textarea.is-primary:active, + .textarea.is-primary.is-active { + box-shadow: 0 0 0 0.125em rgba(201, 51, 18, 0.25); } + .input.is-link, + .textarea.is-link { + border-color: #3273dc; } + .input.is-link:focus, .input.is-link.is-focused, .input.is-link:active, .input.is-link.is-active, + .textarea.is-link:focus, + .textarea.is-link.is-focused, + .textarea.is-link:active, + .textarea.is-link.is-active { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .input.is-info, + .textarea.is-info { + border-color: #209cee; } + .input.is-info:focus, .input.is-info.is-focused, .input.is-info:active, .input.is-info.is-active, + .textarea.is-info:focus, + .textarea.is-info.is-focused, + .textarea.is-info:active, + .textarea.is-info.is-active { + box-shadow: 0 0 0 0.125em rgba(32, 156, 238, 0.25); } + .input.is-success, + .textarea.is-success { + border-color: #23d160; } + .input.is-success:focus, .input.is-success.is-focused, .input.is-success:active, .input.is-success.is-active, + .textarea.is-success:focus, + .textarea.is-success.is-focused, + .textarea.is-success:active, + .textarea.is-success.is-active { + box-shadow: 0 0 0 0.125em rgba(35, 209, 96, 0.25); } + .input.is-warning, + .textarea.is-warning { + border-color: #ffdd57; } + .input.is-warning:focus, .input.is-warning.is-focused, .input.is-warning:active, .input.is-warning.is-active, + .textarea.is-warning:focus, + .textarea.is-warning.is-focused, + .textarea.is-warning:active, + .textarea.is-warning.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 221, 87, 0.25); } + .input.is-danger, + .textarea.is-danger { + border-color: #ff3860; } + .input.is-danger:focus, .input.is-danger.is-focused, .input.is-danger:active, .input.is-danger.is-active, + .textarea.is-danger:focus, + .textarea.is-danger.is-focused, + .textarea.is-danger:active, + .textarea.is-danger.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 56, 96, 0.25); } + .input.is-small, + .textarea.is-small { + border-radius: 2px; + font-size: 0.75rem; } + .input.is-medium, + .textarea.is-medium { + font-size: 1.25rem; } + .input.is-large, + .textarea.is-large { + font-size: 1.5rem; } + .input.is-fullwidth, + .textarea.is-fullwidth { + display: block; + width: 100%; } + .input.is-inline, + .textarea.is-inline { + display: inline; + width: auto; } + +.input.is-static { + background-color: transparent; + border-color: transparent; + box-shadow: none; + padding-left: 0; + padding-right: 0; } + +.textarea { + display: block; + max-width: 100%; + min-width: 100%; + padding: 0.625em; + resize: vertical; } + .textarea:not([rows]) { + max-height: 600px; + min-height: 120px; } + .textarea[rows] { + height: unset; } + .textarea.has-fixed-size { + resize: none; } + +.checkbox, +.radio { + cursor: pointer; + display: inline-block; + line-height: 1.25; + position: relative; } + .checkbox input, + .radio input { + cursor: pointer; } + .checkbox:hover, + .radio:hover { + color: #363636; } + .checkbox[disabled], + .radio[disabled] { + color: #7a7a7a; + cursor: not-allowed; } + +.radio + .radio { + margin-left: 0.5em; } + +.select { + display: inline-block; + max-width: 100%; + position: relative; + vertical-align: top; } + .select:not(.is-multiple) { + height: 2.25em; } + .select:not(.is-multiple)::after { + border: 1px solid #3273dc; + border-right: 0; + border-top: 0; + content: " "; + display: block; + height: 0.5em; + pointer-events: none; + position: absolute; + transform: rotate(-45deg); + transform-origin: center; + width: 0.5em; + margin-top: -0.375em; + right: 1.125em; + top: 50%; + z-index: 4; } + .select select { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + background-color: white; + border-color: #dbdbdb; + color: #363636; + cursor: pointer; + display: block; + font-size: 1em; + max-width: 100%; + outline: none; } + .select select:focus, .select select.is-focused, .select select:active, .select select.is-active { + outline: none; } + .select select[disabled] { + cursor: not-allowed; } + .select select::-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select::-webkit-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select:-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select:-ms-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select:hover, .select select.is-hovered { + border-color: #b5b5b5; } + .select select:focus, .select select.is-focused, .select select:active, .select select.is-active { + border-color: #3273dc; + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .select select[disabled] { + background-color: whitesmoke; + border-color: whitesmoke; + box-shadow: none; + color: #7a7a7a; } + .select select[disabled]::-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select[disabled]::-webkit-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select[disabled]:-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select[disabled]:-ms-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select::-ms-expand { + display: none; } + .select select[disabled]:hover { + border-color: whitesmoke; } + .select select:not([multiple]) { + padding-right: 2.5em; } + .select select[multiple] { + height: unset; + padding: 0; } + .select select[multiple] option { + padding: 0.5em 1em; } + .select:hover::after { + border-color: #363636; } + .select.is-white select { + border-color: white; } + .select.is-white select:focus, .select.is-white select.is-focused, .select.is-white select:active, .select.is-white select.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); } + .select.is-black select { + border-color: #0a0a0a; } + .select.is-black select:focus, .select.is-black select.is-focused, .select.is-black select:active, .select.is-black select.is-active { + box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); } + .select.is-light select { + border-color: whitesmoke; } + .select.is-light select:focus, .select.is-light select.is-focused, .select.is-light select:active, .select.is-light select.is-active { + box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); } + .select.is-dark select { + border-color: #363636; } + .select.is-dark select:focus, .select.is-dark select.is-focused, .select.is-dark select:active, .select.is-dark select.is-active { + box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); } + .select.is-primary select { + border-color: #C93312; } + .select.is-primary select:focus, .select.is-primary select.is-focused, .select.is-primary select:active, .select.is-primary select.is-active { + box-shadow: 0 0 0 0.125em rgba(201, 51, 18, 0.25); } + .select.is-link select { + border-color: #3273dc; } + .select.is-link select:focus, .select.is-link select.is-focused, .select.is-link select:active, .select.is-link select.is-active { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .select.is-info select { + border-color: #209cee; } + .select.is-info select:focus, .select.is-info select.is-focused, .select.is-info select:active, .select.is-info select.is-active { + box-shadow: 0 0 0 0.125em rgba(32, 156, 238, 0.25); } + .select.is-success select { + border-color: #23d160; } + .select.is-success select:focus, .select.is-success select.is-focused, .select.is-success select:active, .select.is-success select.is-active { + box-shadow: 0 0 0 0.125em rgba(35, 209, 96, 0.25); } + .select.is-warning select { + border-color: #ffdd57; } + .select.is-warning select:focus, .select.is-warning select.is-focused, .select.is-warning select:active, .select.is-warning select.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 221, 87, 0.25); } + .select.is-danger select { + border-color: #ff3860; } + .select.is-danger select:focus, .select.is-danger select.is-focused, .select.is-danger select:active, .select.is-danger select.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 56, 96, 0.25); } + .select.is-small { + border-radius: 2px; + font-size: 0.75rem; } + .select.is-medium { + font-size: 1.25rem; } + .select.is-large { + font-size: 1.5rem; } + .select.is-disabled::after { + border-color: #7a7a7a; } + .select.is-fullwidth { + width: 100%; } + .select.is-fullwidth select { + width: 100%; } + .select.is-loading::after { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; + margin-top: 0; + position: absolute; + right: 0.625em; + top: 0.625em; + transform: none; } + .select.is-loading.is-small:after { + font-size: 0.75rem; } + .select.is-loading.is-medium:after { + font-size: 1.25rem; } + .select.is-loading.is-large:after { + font-size: 1.5rem; } + +.file { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: stretch; + display: flex; + justify-content: flex-start; + position: relative; } + .file.is-white .file-cta { + background-color: white; + border-color: transparent; + color: #0a0a0a; } + .file.is-white:hover .file-cta, .file.is-white.is-hovered .file-cta { + background-color: #f9f9f9; + border-color: transparent; + color: #0a0a0a; } + .file.is-white:focus .file-cta, .file.is-white.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.25); + color: #0a0a0a; } + .file.is-white:active .file-cta, .file.is-white.is-active .file-cta { + background-color: #f2f2f2; + border-color: transparent; + color: #0a0a0a; } + .file.is-black .file-cta { + background-color: #0a0a0a; + border-color: transparent; + color: white; } + .file.is-black:hover .file-cta, .file.is-black.is-hovered .file-cta { + background-color: #040404; + border-color: transparent; + color: white; } + .file.is-black:focus .file-cta, .file.is-black.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(10, 10, 10, 0.25); + color: white; } + .file.is-black:active .file-cta, .file.is-black.is-active .file-cta { + background-color: black; + border-color: transparent; + color: white; } + .file.is-light .file-cta { + background-color: whitesmoke; + border-color: transparent; + color: #363636; } + .file.is-light:hover .file-cta, .file.is-light.is-hovered .file-cta { + background-color: #eeeeee; + border-color: transparent; + color: #363636; } + .file.is-light:focus .file-cta, .file.is-light.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(245, 245, 245, 0.25); + color: #363636; } + .file.is-light:active .file-cta, .file.is-light.is-active .file-cta { + background-color: #e8e8e8; + border-color: transparent; + color: #363636; } + .file.is-dark .file-cta { + background-color: #363636; + border-color: transparent; + color: whitesmoke; } + .file.is-dark:hover .file-cta, .file.is-dark.is-hovered .file-cta { + background-color: #2f2f2f; + border-color: transparent; + color: whitesmoke; } + .file.is-dark:focus .file-cta, .file.is-dark.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(54, 54, 54, 0.25); + color: whitesmoke; } + .file.is-dark:active .file-cta, .file.is-dark.is-active .file-cta { + background-color: #292929; + border-color: transparent; + color: whitesmoke; } + .file.is-primary .file-cta { + background-color: #C93312; + border-color: transparent; + color: #fff; } + .file.is-primary:hover .file-cta, .file.is-primary.is-hovered .file-cta { + background-color: #bd3011; + border-color: transparent; + color: #fff; } + .file.is-primary:focus .file-cta, .file.is-primary.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(201, 51, 18, 0.25); + color: #fff; } + .file.is-primary:active .file-cta, .file.is-primary.is-active .file-cta { + background-color: #b22d10; + border-color: transparent; + color: #fff; } + .file.is-link .file-cta { + background-color: #3273dc; + border-color: transparent; + color: #fff; } + .file.is-link:hover .file-cta, .file.is-link.is-hovered .file-cta { + background-color: #276cda; + border-color: transparent; + color: #fff; } + .file.is-link:focus .file-cta, .file.is-link.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(50, 115, 220, 0.25); + color: #fff; } + .file.is-link:active .file-cta, .file.is-link.is-active .file-cta { + background-color: #2366d1; + border-color: transparent; + color: #fff; } + .file.is-info .file-cta { + background-color: #209cee; + border-color: transparent; + color: #fff; } + .file.is-info:hover .file-cta, .file.is-info.is-hovered .file-cta { + background-color: #1496ed; + border-color: transparent; + color: #fff; } + .file.is-info:focus .file-cta, .file.is-info.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(32, 156, 238, 0.25); + color: #fff; } + .file.is-info:active .file-cta, .file.is-info.is-active .file-cta { + background-color: #118fe4; + border-color: transparent; + color: #fff; } + .file.is-success .file-cta { + background-color: #23d160; + border-color: transparent; + color: #fff; } + .file.is-success:hover .file-cta, .file.is-success.is-hovered .file-cta { + background-color: #22c65b; + border-color: transparent; + color: #fff; } + .file.is-success:focus .file-cta, .file.is-success.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(35, 209, 96, 0.25); + color: #fff; } + .file.is-success:active .file-cta, .file.is-success.is-active .file-cta { + background-color: #20bc56; + border-color: transparent; + color: #fff; } + .file.is-warning .file-cta { + background-color: #ffdd57; + border-color: transparent; + color: #FFFFFF; } + .file.is-warning:hover .file-cta, .file.is-warning.is-hovered .file-cta { + background-color: #ffdb4a; + border-color: transparent; + color: #FFFFFF; } + .file.is-warning:focus .file-cta, .file.is-warning.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(255, 221, 87, 0.25); + color: #FFFFFF; } + .file.is-warning:active .file-cta, .file.is-warning.is-active .file-cta { + background-color: #ffd83d; + border-color: transparent; + color: #FFFFFF; } + .file.is-danger .file-cta { + background-color: #ff3860; + border-color: transparent; + color: #fff; } + .file.is-danger:hover .file-cta, .file.is-danger.is-hovered .file-cta { + background-color: #ff2b56; + border-color: transparent; + color: #fff; } + .file.is-danger:focus .file-cta, .file.is-danger.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(255, 56, 96, 0.25); + color: #fff; } + .file.is-danger:active .file-cta, .file.is-danger.is-active .file-cta { + background-color: #ff1f4b; + border-color: transparent; + color: #fff; } + .file.is-small { + font-size: 0.75rem; } + .file.is-medium { + font-size: 1.25rem; } + .file.is-medium .file-icon .fa { + font-size: 21px; } + .file.is-large { + font-size: 1.5rem; } + .file.is-large .file-icon .fa { + font-size: 28px; } + .file.has-name .file-cta { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + .file.has-name .file-name { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .file.has-name.is-empty .file-cta { + border-radius: 3px; } + .file.has-name.is-empty .file-name { + display: none; } + .file.is-centered { + justify-content: center; } + .file.is-right { + justify-content: flex-end; } + .file.is-boxed .file-label { + flex-direction: column; } + .file.is-boxed .file-cta { + flex-direction: column; + height: auto; + padding: 1em 3em; } + .file.is-boxed .file-name { + border-width: 0 1px 1px; } + .file.is-boxed .file-icon { + height: 1.5em; + width: 1.5em; } + .file.is-boxed .file-icon .fa { + font-size: 21px; } + .file.is-boxed.is-small .file-icon .fa { + font-size: 14px; } + .file.is-boxed.is-medium .file-icon .fa { + font-size: 28px; } + .file.is-boxed.is-large .file-icon .fa { + font-size: 35px; } + .file.is-boxed.has-name .file-cta { + border-radius: 3px 3px 0 0; } + .file.is-boxed.has-name .file-name { + border-radius: 0 0 3px 3px; + border-width: 0 1px 1px; } + .file.is-right .file-cta { + border-radius: 0 3px 3px 0; } + .file.is-right .file-name { + border-radius: 3px 0 0 3px; + border-width: 1px 0 1px 1px; + order: -1; } + .file.is-fullwidth .file-label { + width: 100%; } + .file.is-fullwidth .file-name { + flex-grow: 1; + max-width: none; } + +.file-label { + align-items: stretch; + display: flex; + cursor: pointer; + justify-content: flex-start; + overflow: hidden; + position: relative; } + .file-label:hover .file-cta { + background-color: #eeeeee; + color: #363636; } + .file-label:hover .file-name { + border-color: #d5d5d5; } + .file-label:active .file-cta { + background-color: #e8e8e8; + color: #363636; } + .file-label:active .file-name { + border-color: #cfcfcf; } + +.file-input { + height: 0.01em; + left: 0; + outline: none; + position: absolute; + top: 0; + width: 0.01em; } + +.file-cta, +.file-name { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + border-color: #dbdbdb; + border-radius: 3px; + font-size: 1em; + padding-left: 1em; + padding-right: 1em; + white-space: nowrap; } + .file-cta:focus, .file-cta.is-focused, .file-cta:active, .file-cta.is-active, + .file-name:focus, + .file-name.is-focused, + .file-name:active, + .file-name.is-active { + outline: none; } + .file-cta[disabled], + .file-name[disabled] { + cursor: not-allowed; } + +.file-cta { + background-color: whitesmoke; + color: #4a4a4a; } + +.file-name { + border-color: #dbdbdb; + border-style: solid; + border-width: 1px 1px 1px 0; + display: block; + max-width: 16em; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; } + +.file-icon { + align-items: center; + display: flex; + height: 1em; + justify-content: center; + margin-right: 0.5em; + width: 1em; } + .file-icon .fa { + font-size: 14px; } + +.label { + color: #363636; + display: block; + font-size: 1rem; + font-weight: 700; } + .label:not(:last-child) { + margin-bottom: 0.5em; } + .label.is-small { + font-size: 0.75rem; } + .label.is-medium { + font-size: 1.25rem; } + .label.is-large { + font-size: 1.5rem; } + +.help { + display: block; + font-size: 0.75rem; + margin-top: 0.25rem; } + .help.is-white { + color: white; } + .help.is-black { + color: #0a0a0a; } + .help.is-light { + color: whitesmoke; } + .help.is-dark { + color: #363636; } + .help.is-primary { + color: #C93312; } + .help.is-link { + color: #3273dc; } + .help.is-info { + color: #209cee; } + .help.is-success { + color: #23d160; } + .help.is-warning { + color: #ffdd57; } + .help.is-danger { + color: #ff3860; } + +.field:not(:last-child) { + margin-bottom: 0.75rem; } + +.field.has-addons { + display: flex; + justify-content: flex-start; } + .field.has-addons .control:not(:last-child) { + margin-right: -1px; } + .field.has-addons .control:first-child .button, + .field.has-addons .control:first-child .input, + .field.has-addons .control:first-child .select select { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; } + .field.has-addons .control:last-child .button, + .field.has-addons .control:last-child .input, + .field.has-addons .control:last-child .select select { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; } + .field.has-addons .control .button, + .field.has-addons .control .input, + .field.has-addons .control .select select { + border-radius: 0; } + .field.has-addons .control .button:hover, .field.has-addons .control .button.is-hovered, + .field.has-addons .control .input:hover, + .field.has-addons .control .input.is-hovered, + .field.has-addons .control .select select:hover, + .field.has-addons .control .select select.is-hovered { + z-index: 2; } + .field.has-addons .control .button:focus, .field.has-addons .control .button.is-focused, .field.has-addons .control .button:active, .field.has-addons .control .button.is-active, + .field.has-addons .control .input:focus, + .field.has-addons .control .input.is-focused, + .field.has-addons .control .input:active, + .field.has-addons .control .input.is-active, + .field.has-addons .control .select select:focus, + .field.has-addons .control .select select.is-focused, + .field.has-addons .control .select select:active, + .field.has-addons .control .select select.is-active { + z-index: 3; } + .field.has-addons .control .button:focus:hover, .field.has-addons .control .button.is-focused:hover, .field.has-addons .control .button:active:hover, .field.has-addons .control .button.is-active:hover, + .field.has-addons .control .input:focus:hover, + .field.has-addons .control .input.is-focused:hover, + .field.has-addons .control .input:active:hover, + .field.has-addons .control .input.is-active:hover, + .field.has-addons .control .select select:focus:hover, + .field.has-addons .control .select select.is-focused:hover, + .field.has-addons .control .select select:active:hover, + .field.has-addons .control .select select.is-active:hover { + z-index: 4; } + .field.has-addons .control.is-expanded { + flex-grow: 1; } + .field.has-addons.has-addons-centered { + justify-content: center; } + .field.has-addons.has-addons-right { + justify-content: flex-end; } + .field.has-addons.has-addons-fullwidth .control { + flex-grow: 1; + flex-shrink: 0; } + +.field.is-grouped { + display: flex; + justify-content: flex-start; } + .field.is-grouped > .control { + flex-shrink: 0; } + .field.is-grouped > .control:not(:last-child) { + margin-bottom: 0; + margin-right: 0.75rem; } + .field.is-grouped > .control.is-expanded { + flex-grow: 1; + flex-shrink: 1; } + .field.is-grouped.is-grouped-centered { + justify-content: center; } + .field.is-grouped.is-grouped-right { + justify-content: flex-end; } + .field.is-grouped.is-grouped-multiline { + flex-wrap: wrap; } + .field.is-grouped.is-grouped-multiline > .control:last-child, .field.is-grouped.is-grouped-multiline > .control:not(:last-child) { + margin-bottom: 0.75rem; } + .field.is-grouped.is-grouped-multiline:last-child { + margin-bottom: -0.75rem; } + .field.is-grouped.is-grouped-multiline:not(:last-child) { + margin-bottom: 0; } + +@media screen and (min-width: 769px), print { + .field.is-horizontal { + display: flex; } } + +.field-label .label { + font-size: inherit; } + +@media screen and (max-width: 768px) { + .field-label { + margin-bottom: 0.5rem; } } + +@media screen and (min-width: 769px), print { + .field-label { + flex-basis: 0; + flex-grow: 1; + flex-shrink: 0; + margin-right: 1.5rem; + text-align: right; } + .field-label.is-small { + font-size: 0.75rem; + padding-top: 0.375em; } + .field-label.is-normal { + padding-top: 0.375em; } + .field-label.is-medium { + font-size: 1.25rem; + padding-top: 0.375em; } + .field-label.is-large { + font-size: 1.5rem; + padding-top: 0.375em; } } + +.field-body .field .field { + margin-bottom: 0; } + +@media screen and (min-width: 769px), print { + .field-body { + display: flex; + flex-basis: 0; + flex-grow: 5; + flex-shrink: 1; } + .field-body .field { + margin-bottom: 0; } + .field-body > .field { + flex-shrink: 1; } + .field-body > .field:not(.is-narrow) { + flex-grow: 1; } + .field-body > .field:not(:last-child) { + margin-right: 0.75rem; } } + +.control { + font-size: 1rem; + position: relative; + text-align: left; } + .control.has-icon .icon { + color: #dbdbdb; + height: 2.25em; + pointer-events: none; + position: absolute; + top: 0; + width: 2.25em; + z-index: 4; } + .control.has-icon .input:focus + .icon { + color: #7a7a7a; } + .control.has-icon .input.is-small + .icon { + font-size: 0.75rem; } + .control.has-icon .input.is-medium + .icon { + font-size: 1.25rem; } + .control.has-icon .input.is-large + .icon { + font-size: 1.5rem; } + .control.has-icon:not(.has-icon-right) .icon { + left: 0; } + .control.has-icon:not(.has-icon-right) .input { + padding-left: 2.25em; } + .control.has-icon.has-icon-right .icon { + right: 0; } + .control.has-icon.has-icon-right .input { + padding-right: 2.25em; } + .control.has-icons-left .input:focus ~ .icon, + .control.has-icons-left .select:focus ~ .icon, .control.has-icons-right .input:focus ~ .icon, + .control.has-icons-right .select:focus ~ .icon { + color: #7a7a7a; } + .control.has-icons-left .input.is-small ~ .icon, + .control.has-icons-left .select.is-small ~ .icon, .control.has-icons-right .input.is-small ~ .icon, + .control.has-icons-right .select.is-small ~ .icon { + font-size: 0.75rem; } + .control.has-icons-left .input.is-medium ~ .icon, + .control.has-icons-left .select.is-medium ~ .icon, .control.has-icons-right .input.is-medium ~ .icon, + .control.has-icons-right .select.is-medium ~ .icon { + font-size: 1.25rem; } + .control.has-icons-left .input.is-large ~ .icon, + .control.has-icons-left .select.is-large ~ .icon, .control.has-icons-right .input.is-large ~ .icon, + .control.has-icons-right .select.is-large ~ .icon { + font-size: 1.5rem; } + .control.has-icons-left .icon, .control.has-icons-right .icon { + color: #dbdbdb; + height: 2.25em; + pointer-events: none; + position: absolute; + top: 0; + width: 2.25em; + z-index: 4; } + .control.has-icons-left .input, + .control.has-icons-left .select select { + padding-left: 2.25em; } + .control.has-icons-left .icon.is-left { + left: 0; } + .control.has-icons-right .input, + .control.has-icons-right .select select { + padding-right: 2.25em; } + .control.has-icons-right .icon.is-right { + right: 0; } + .control.is-loading::after { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; + position: absolute !important; + right: 0.625em; + top: 0.625em; } + .control.is-loading.is-small:after { + font-size: 0.75rem; } + .control.is-loading.is-medium:after { + font-size: 1.25rem; } + .control.is-loading.is-large:after { + font-size: 1.5rem; } + +.icon { + align-items: center; + display: inline-flex; + justify-content: center; + height: 1.5rem; + width: 1.5rem; } + .icon.is-small { + height: 1rem; + width: 1rem; } + .icon.is-medium { + height: 2rem; + width: 2rem; } + .icon.is-large { + height: 3rem; + width: 3rem; } + +.image { + display: block; + position: relative; } + .image img { + display: block; + height: auto; + width: 100%; } + .image.is-square img, .image.is-1by1 img, .image.is-4by3 img, .image.is-3by2 img, .image.is-16by9 img, .image.is-2by1 img { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 100%; } + .image.is-square, .image.is-1by1 { + padding-top: 100%; } + .image.is-4by3 { + padding-top: 75%; } + .image.is-3by2 { + padding-top: 66.6666%; } + .image.is-16by9 { + padding-top: 56.25%; } + .image.is-2by1 { + padding-top: 50%; } + .image.is-16x16 { + height: 16px; + width: 16px; } + .image.is-24x24 { + height: 24px; + width: 24px; } + .image.is-32x32 { + height: 32px; + width: 32px; } + .image.is-48x48 { + height: 48px; + width: 48px; } + .image.is-64x64 { + height: 64px; + width: 64px; } + .image.is-96x96 { + height: 96px; + width: 96px; } + .image.is-128x128 { + height: 128px; + width: 128px; } + +.notification { + background-color: whitesmoke; + border-radius: 3px; + padding: 1.25rem 2.5rem 1.25rem 1.5rem; + position: relative; } + .notification:not(:last-child) { + margin-bottom: 1.5rem; } + .notification a:not(.button) { + color: currentColor; + text-decoration: underline; } + .notification strong { + color: currentColor; } + .notification code, + .notification pre { + background: white; } + .notification pre code { + background: transparent; } + .notification > .delete { + position: absolute; + right: 0.5em; + top: 0.5em; } + .notification .title, + .notification .subtitle, + .notification .content { + color: currentColor; } + .notification.is-white { + background-color: white; + color: #0a0a0a; } + .notification.is-black { + background-color: #0a0a0a; + color: white; } + .notification.is-light { + background-color: whitesmoke; + color: #363636; } + .notification.is-dark { + background-color: #363636; + color: whitesmoke; } + .notification.is-primary { + background-color: #C93312; + color: #fff; } + .notification.is-link { + background-color: #3273dc; + color: #fff; } + .notification.is-info { + background-color: #209cee; + color: #fff; } + .notification.is-success { + background-color: #23d160; + color: #fff; } + .notification.is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .notification.is-danger { + background-color: #ff3860; + color: #fff; } + +.progress { + -moz-appearance: none; + -webkit-appearance: none; + border: none; + border-radius: 290486px; + display: block; + height: 1rem; + overflow: hidden; + padding: 0; + width: 100%; } + .progress:not(:last-child) { + margin-bottom: 1.5rem; } + .progress::-webkit-progress-bar { + background-color: #dbdbdb; } + .progress::-webkit-progress-value { + background-color: #4a4a4a; } + .progress::-moz-progress-bar { + background-color: #4a4a4a; } + .progress::-ms-fill { + background-color: #4a4a4a; + border: none; } + .progress.is-white::-webkit-progress-value { + background-color: white; } + .progress.is-white::-moz-progress-bar { + background-color: white; } + .progress.is-white::-ms-fill { + background-color: white; } + .progress.is-black::-webkit-progress-value { + background-color: #0a0a0a; } + .progress.is-black::-moz-progress-bar { + background-color: #0a0a0a; } + .progress.is-black::-ms-fill { + background-color: #0a0a0a; } + .progress.is-light::-webkit-progress-value { + background-color: whitesmoke; } + .progress.is-light::-moz-progress-bar { + background-color: whitesmoke; } + .progress.is-light::-ms-fill { + background-color: whitesmoke; } + .progress.is-dark::-webkit-progress-value { + background-color: #363636; } + .progress.is-dark::-moz-progress-bar { + background-color: #363636; } + .progress.is-dark::-ms-fill { + background-color: #363636; } + .progress.is-primary::-webkit-progress-value { + background-color: #C93312; } + .progress.is-primary::-moz-progress-bar { + background-color: #C93312; } + .progress.is-primary::-ms-fill { + background-color: #C93312; } + .progress.is-link::-webkit-progress-value { + background-color: #3273dc; } + .progress.is-link::-moz-progress-bar { + background-color: #3273dc; } + .progress.is-link::-ms-fill { + background-color: #3273dc; } + .progress.is-info::-webkit-progress-value { + background-color: #209cee; } + .progress.is-info::-moz-progress-bar { + background-color: #209cee; } + .progress.is-info::-ms-fill { + background-color: #209cee; } + .progress.is-success::-webkit-progress-value { + background-color: #23d160; } + .progress.is-success::-moz-progress-bar { + background-color: #23d160; } + .progress.is-success::-ms-fill { + background-color: #23d160; } + .progress.is-warning::-webkit-progress-value { + background-color: #ffdd57; } + .progress.is-warning::-moz-progress-bar { + background-color: #ffdd57; } + .progress.is-warning::-ms-fill { + background-color: #ffdd57; } + .progress.is-danger::-webkit-progress-value { + background-color: #ff3860; } + .progress.is-danger::-moz-progress-bar { + background-color: #ff3860; } + .progress.is-danger::-ms-fill { + background-color: #ff3860; } + .progress.is-small { + height: 0.75rem; } + .progress.is-medium { + height: 1.25rem; } + .progress.is-large { + height: 1.5rem; } + +.table { + background-color: white; + color: #363636; + margin-bottom: 1.5rem; } + .table td, + .table th { + border: 1px solid #dbdbdb; + border-width: 0 0 1px; + padding: 0.5em 0.75em; + vertical-align: top; } + .table td.is-white, + .table th.is-white { + background-color: white; + border-color: white; + color: #0a0a0a; } + .table td.is-black, + .table th.is-black { + background-color: #0a0a0a; + border-color: #0a0a0a; + color: white; } + .table td.is-light, + .table th.is-light { + background-color: whitesmoke; + border-color: whitesmoke; + color: #363636; } + .table td.is-dark, + .table th.is-dark { + background-color: #363636; + border-color: #363636; + color: whitesmoke; } + .table td.is-primary, + .table th.is-primary { + background-color: #C93312; + border-color: #C93312; + color: #fff; } + .table td.is-link, + .table th.is-link { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; } + .table td.is-info, + .table th.is-info { + background-color: #209cee; + border-color: #209cee; + color: #fff; } + .table td.is-success, + .table th.is-success { + background-color: #23d160; + border-color: #23d160; + color: #fff; } + .table td.is-warning, + .table th.is-warning { + background-color: #ffdd57; + border-color: #ffdd57; + color: #FFFFFF; } + .table td.is-danger, + .table th.is-danger { + background-color: #ff3860; + border-color: #ff3860; + color: #fff; } + .table td.is-narrow, + .table th.is-narrow { + white-space: nowrap; + width: 1%; } + .table td.is-selected, + .table th.is-selected { + background-color: #C93312; + color: #fff; } + .table td.is-selected a, + .table td.is-selected strong, + .table th.is-selected a, + .table th.is-selected strong { + color: currentColor; } + .table th { + color: #363636; + text-align: left; } + .table tr.is-selected { + background-color: #C93312; + color: #fff; } + .table tr.is-selected a, + .table tr.is-selected strong { + color: currentColor; } + .table tr.is-selected td, + .table tr.is-selected th { + border-color: #fff; + color: currentColor; } + .table thead td, + .table thead th { + border-width: 0 0 2px; + color: #363636; } + .table tfoot td, + .table tfoot th { + border-width: 2px 0 0; + color: #363636; } + .table tbody tr:last-child td, + .table tbody tr:last-child th { + border-bottom-width: 0; } + .table.is-bordered td, + .table.is-bordered th { + border-width: 1px; } + .table.is-bordered tr:last-child td, + .table.is-bordered tr:last-child th { + border-bottom-width: 1px; } + .table.is-fullwidth { + width: 100%; } + .table.is-hoverable tbody tr:not(.is-selected):hover { + background-color: #fafafa; } + .table.is-hoverable.is-striped tbody tr:not(.is-selected):hover { + background-color: whitesmoke; } + .table.is-narrow td, + .table.is-narrow th { + padding: 0.25em 0.5em; } + .table.is-striped tbody tr:not(.is-selected):nth-child(even) { + background-color: #fafafa; } + +.tags { + align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; } + .tags .tag { + margin-bottom: 0.5rem; } + .tags .tag:not(:last-child) { + margin-right: 0.5rem; } + .tags:last-child { + margin-bottom: -0.5rem; } + .tags:not(:last-child) { + margin-bottom: 1rem; } + .tags.has-addons .tag { + margin-right: 0; } + .tags.has-addons .tag:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .tags.has-addons .tag:not(:last-child) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + .tags.is-centered { + justify-content: center; } + .tags.is-centered .tag { + margin-right: 0.25rem; + margin-left: 0.25rem; } + .tags.is-right { + justify-content: flex-end; } + .tags.is-right .tag:not(:first-child) { + margin-left: 0.5rem; } + .tags.is-right .tag:not(:last-child) { + margin-right: 0; } + +.tag:not(body) { + align-items: center; + background-color: whitesmoke; + border-radius: 3px; + color: #4a4a4a; + display: inline-flex; + font-size: 0.75rem; + height: 2em; + justify-content: center; + line-height: 1.5; + padding-left: 0.75em; + padding-right: 0.75em; + white-space: nowrap; } + .tag:not(body) .delete { + margin-left: 0.25em; + margin-right: -0.375em; } + .tag:not(body).is-white { + background-color: white; + color: #0a0a0a; } + .tag:not(body).is-black { + background-color: #0a0a0a; + color: white; } + .tag:not(body).is-light { + background-color: whitesmoke; + color: #363636; } + .tag:not(body).is-dark { + background-color: #363636; + color: whitesmoke; } + .tag:not(body).is-primary { + background-color: #C93312; + color: #fff; } + .tag:not(body).is-link { + background-color: #3273dc; + color: #fff; } + .tag:not(body).is-info { + background-color: #209cee; + color: #fff; } + .tag:not(body).is-success { + background-color: #23d160; + color: #fff; } + .tag:not(body).is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .tag:not(body).is-danger { + background-color: #ff3860; + color: #fff; } + .tag:not(body).is-medium { + font-size: 1rem; } + .tag:not(body).is-large { + font-size: 1.25rem; } + .tag:not(body) .icon:first-child:not(:last-child) { + margin-left: -0.375em; + margin-right: 0.1875em; } + .tag:not(body) .icon:last-child:not(:first-child) { + margin-left: 0.1875em; + margin-right: -0.375em; } + .tag:not(body) .icon:first-child:last-child { + margin-left: -0.375em; + margin-right: -0.375em; } + .tag:not(body).is-delete { + margin-left: 1px; + padding: 0; + position: relative; + width: 2em; } + .tag:not(body).is-delete:before, .tag:not(body).is-delete:after { + background-color: currentColor; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } + .tag:not(body).is-delete:before { + height: 1px; + width: 50%; } + .tag:not(body).is-delete:after { + height: 50%; + width: 1px; } + .tag:not(body).is-delete:hover, .tag:not(body).is-delete:focus { + background-color: #e8e8e8; } + .tag:not(body).is-delete:active { + background-color: #dbdbdb; } + .tag:not(body).is-rounded { + border-radius: 290486px; } + +a.tag:hover { + text-decoration: underline; } + +.title, +.subtitle { + word-break: break-word; } + .title:not(:last-child), + .subtitle:not(:last-child) { + margin-bottom: 1.5rem; } + .title em, + .title span, + .subtitle em, + .subtitle span { + font-weight: inherit; } + .title .tag, + .subtitle .tag { + vertical-align: middle; } + +.title { + color: #363636; + font-size: 2rem; + font-weight: 600; + line-height: 1.125; } + .title strong { + color: inherit; + font-weight: inherit; } + .title + .highlight { + margin-top: -0.75rem; } + .title:not(.is-spaced) + .subtitle { + margin-top: -1.5rem; } + .title.is-1 { + font-size: 3rem; } + .title.is-2 { + font-size: 2.5rem; } + .title.is-3 { + font-size: 2rem; } + .title.is-4 { + font-size: 1.5rem; } + .title.is-5 { + font-size: 1.25rem; } + .title.is-6 { + font-size: 1rem; } + .title.is-7 { + font-size: 0.75rem; } + +.subtitle { + color: #4a4a4a; + font-size: 1.25rem; + font-weight: 400; + line-height: 1.25; } + .subtitle strong { + color: #363636; + font-weight: 600; } + .subtitle:not(.is-spaced) + .title { + margin-top: -1.5rem; } + .subtitle.is-1 { + font-size: 3rem; } + .subtitle.is-2 { + font-size: 2.5rem; } + .subtitle.is-3 { + font-size: 2rem; } + .subtitle.is-4 { + font-size: 1.5rem; } + .subtitle.is-5 { + font-size: 1.25rem; } + .subtitle.is-6 { + font-size: 1rem; } + .subtitle.is-7 { + font-size: 0.75rem; } + +.block:not(:last-child) { + margin-bottom: 1.5rem; } + +.delete { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -moz-appearance: none; + -webkit-appearance: none; + background-color: rgba(10, 10, 10, 0.2); + border: none; + border-radius: 290486px; + cursor: pointer; + display: inline-block; + flex-grow: 0; + flex-shrink: 0; + font-size: 0; + height: 20px; + max-height: 20px; + max-width: 20px; + min-height: 20px; + min-width: 20px; + outline: none; + position: relative; + vertical-align: top; + width: 20px; } + .delete:before, .delete:after { + background-color: white; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } + .delete:before { + height: 2px; + width: 50%; } + .delete:after { + height: 50%; + width: 2px; } + .delete:hover, .delete:focus { + background-color: rgba(10, 10, 10, 0.3); } + .delete:active { + background-color: rgba(10, 10, 10, 0.4); } + .delete.is-small { + height: 16px; + max-height: 16px; + max-width: 16px; + min-height: 16px; + min-width: 16px; + width: 16px; } + .delete.is-medium { + height: 24px; + max-height: 24px; + max-width: 24px; + min-height: 24px; + min-width: 24px; + width: 24px; } + .delete.is-large { + height: 32px; + max-height: 32px; + max-width: 32px; + min-height: 32px; + min-width: 32px; + width: 32px; } + +.heading { + display: block; + font-size: 11px; + letter-spacing: 1px; + margin-bottom: 5px; + text-transform: uppercase; } + +.highlight { + font-weight: 400; + max-width: 100%; + overflow: hidden; + padding: 0; } + .highlight:not(:last-child) { + margin-bottom: 1.5rem; } + .highlight pre { + overflow: auto; + max-width: 100%; } + +.loader { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; } + +.number { + align-items: center; + background-color: whitesmoke; + border-radius: 290486px; + display: inline-flex; + font-size: 1.25rem; + height: 2em; + justify-content: center; + margin-right: 1.5rem; + min-width: 2.5em; + padding: 0.25rem 0.5rem; + text-align: center; + vertical-align: top; } + +.breadcrumb { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: stretch; + display: flex; + font-size: 1rem; + overflow: hidden; + overflow-x: auto; + white-space: nowrap; } + .breadcrumb:not(:last-child) { + margin-bottom: 1.5rem; } + .breadcrumb a { + align-items: center; + color: #3273dc; + display: flex; + justify-content: center; + padding: 0.5em 0.75em; } + .breadcrumb a:hover { + color: #363636; } + .breadcrumb li { + align-items: center; + display: flex; } + .breadcrumb li:first-child a { + padding-left: 0; } + .breadcrumb li.is-active a { + color: #363636; + cursor: default; + pointer-events: none; } + .breadcrumb li + li::before { + color: #4a4a4a; + content: "\0002f"; } + .breadcrumb ul, .breadcrumb ol { + align-items: center; + display: flex; + flex-grow: 1; + flex-shrink: 0; + justify-content: flex-start; } + .breadcrumb .icon:first-child { + margin-right: 0.5em; } + .breadcrumb .icon:last-child { + margin-left: 0.5em; } + .breadcrumb.is-centered ol, .breadcrumb.is-centered ul { + justify-content: center; } + .breadcrumb.is-right ol, .breadcrumb.is-right ul { + justify-content: flex-end; } + .breadcrumb.is-small { + font-size: 0.75rem; } + .breadcrumb.is-medium { + font-size: 1.25rem; } + .breadcrumb.is-large { + font-size: 1.5rem; } + .breadcrumb.has-arrow-separator li + li::before { + content: "\02192"; } + .breadcrumb.has-bullet-separator li + li::before { + content: "\02022"; } + .breadcrumb.has-dot-separator li + li::before { + content: "\000b7"; } + .breadcrumb.has-succeeds-separator li + li::before { + content: "\0227B"; } + +.card { + background-color: white; + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + color: #4a4a4a; + max-width: 100%; + position: relative; } + +.card-header { + align-items: stretch; + box-shadow: 0 1px 2px rgba(10, 10, 10, 0.1); + display: flex; } + +.card-header-title { + align-items: center; + color: #363636; + display: flex; + flex-grow: 1; + font-weight: 700; + padding: 0.75rem; } + .card-header-title.is-centered { + justify-content: center; } + +.card-header-icon { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + padding: 0.75rem; } + +.card-image { + display: block; + position: relative; } + +.card-content { + padding: 1.5rem; } + +.card-footer { + border-top: 1px solid #dbdbdb; + align-items: stretch; + display: flex; } + +.card-footer-item { + align-items: center; + display: flex; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + padding: 0.75rem; } + .card-footer-item:not(:last-child) { + border-right: 1px solid #dbdbdb; } + +.card .media:not(:last-child) { + margin-bottom: 0.75rem; } + +.dropdown { + display: inline-flex; + position: relative; + vertical-align: top; } + .dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu { + display: block; } + .dropdown.is-right .dropdown-menu { + left: auto; + right: 0; } + .dropdown.is-up .dropdown-menu { + bottom: 100%; + padding-bottom: 4px; + padding-top: unset; + top: auto; } + +.dropdown-menu { + display: none; + left: 0; + min-width: 12rem; + padding-top: 4px; + position: absolute; + top: 100%; + z-index: 20; } + +.dropdown-content { + background-color: white; + border-radius: 3px; + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + padding-bottom: 0.5rem; + padding-top: 0.5rem; } + +.dropdown-item { + color: #4a4a4a; + display: block; + font-size: 0.875rem; + line-height: 1.5; + padding: 0.375rem 1rem; + position: relative; } + +a.dropdown-item { + padding-right: 3rem; + white-space: nowrap; } + a.dropdown-item:hover { + background-color: whitesmoke; + color: #0a0a0a; } + a.dropdown-item.is-active { + background-color: #3273dc; + color: #fff; } + +.dropdown-divider { + background-color: #dbdbdb; + border: none; + display: block; + height: 1px; + margin: 0.5rem 0; } + +.level { + align-items: center; + justify-content: space-between; } + .level:not(:last-child) { + margin-bottom: 1.5rem; } + .level code { + border-radius: 3px; } + .level img { + display: inline-block; + vertical-align: top; } + .level.is-mobile { + display: flex; } + .level.is-mobile .level-left, + .level.is-mobile .level-right { + display: flex; } + .level.is-mobile .level-left + .level-right { + margin-top: 0; } + .level.is-mobile .level-item { + margin-right: 0.75rem; } + .level.is-mobile .level-item:not(:last-child) { + margin-bottom: 0; } + .level.is-mobile .level-item:not(.is-narrow) { + flex-grow: 1; } + @media screen and (min-width: 769px), print { + .level { + display: flex; } + .level > .level-item:not(.is-narrow) { + flex-grow: 1; } } + +.level-item { + align-items: center; + display: flex; + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; + justify-content: center; } + .level-item .title, + .level-item .subtitle { + margin-bottom: 0; } + @media screen and (max-width: 768px) { + .level-item:not(:last-child) { + margin-bottom: 0.75rem; } } + +.level-left, +.level-right { + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; } + .level-left .level-item.is-flexible, + .level-right .level-item.is-flexible { + flex-grow: 1; } + @media screen and (min-width: 769px), print { + .level-left .level-item:not(:last-child), + .level-right .level-item:not(:last-child) { + margin-right: 0.75rem; } } + +.level-left { + align-items: center; + justify-content: flex-start; } + @media screen and (max-width: 768px) { + .level-left + .level-right { + margin-top: 1.5rem; } } + @media screen and (min-width: 769px), print { + .level-left { + display: flex; } } + +.level-right { + align-items: center; + justify-content: flex-end; } + @media screen and (min-width: 769px), print { + .level-right { + display: flex; } } + +.media { + align-items: flex-start; + display: flex; + text-align: left; } + .media .content:not(:last-child) { + margin-bottom: 0.75rem; } + .media .media { + border-top: 1px solid rgba(219, 219, 219, 0.5); + display: flex; + padding-top: 0.75rem; } + .media .media .content:not(:last-child), + .media .media .control:not(:last-child) { + margin-bottom: 0.5rem; } + .media .media .media { + padding-top: 0.5rem; } + .media .media .media + .media { + margin-top: 0.5rem; } + .media + .media { + border-top: 1px solid rgba(219, 219, 219, 0.5); + margin-top: 1rem; + padding-top: 1rem; } + .media.is-large + .media { + margin-top: 1.5rem; + padding-top: 1.5rem; } + +.media-left, +.media-right { + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; } + +.media-left { + margin-right: 1rem; } + +.media-right { + margin-left: 1rem; } + +.media-content { + flex-basis: auto; + flex-grow: 1; + flex-shrink: 1; + text-align: left; } + +.menu { + font-size: 1rem; } + .menu.is-small { + font-size: 0.75rem; } + .menu.is-medium { + font-size: 1.25rem; } + .menu.is-large { + font-size: 1.5rem; } + +.menu-list { + line-height: 1.25; } + .menu-list a { + border-radius: 2px; + color: #4a4a4a; + display: block; + padding: 0.5em 0.75em; } + .menu-list a:hover { + background-color: whitesmoke; + color: #363636; } + .menu-list a.is-active { + background-color: #3273dc; + color: #fff; } + .menu-list li ul { + border-left: 1px solid #dbdbdb; + margin: 0.75em; + padding-left: 0.75em; } + +.menu-label { + color: #7a7a7a; + font-size: 0.75em; + letter-spacing: 0.1em; + text-transform: uppercase; } + .menu-label:not(:first-child) { + margin-top: 1em; } + .menu-label:not(:last-child) { + margin-bottom: 1em; } + +.message { + background-color: whitesmoke; + border-radius: 3px; + font-size: 1rem; } + .message:not(:last-child) { + margin-bottom: 1.5rem; } + .message strong { + color: currentColor; } + .message a:not(.button):not(.tag) { + color: currentColor; + text-decoration: underline; } + .message.is-small { + font-size: 0.75rem; } + .message.is-medium { + font-size: 1.25rem; } + .message.is-large { + font-size: 1.5rem; } + .message.is-white { + background-color: white; } + .message.is-white .message-header { + background-color: white; + color: #0a0a0a; } + .message.is-white .message-body { + border-color: white; + color: #4d4d4d; } + .message.is-black { + background-color: #fafafa; } + .message.is-black .message-header { + background-color: #0a0a0a; + color: white; } + .message.is-black .message-body { + border-color: #0a0a0a; + color: #090909; } + .message.is-light { + background-color: #fafafa; } + .message.is-light .message-header { + background-color: whitesmoke; + color: #363636; } + .message.is-light .message-body { + border-color: whitesmoke; + color: #505050; } + .message.is-dark { + background-color: #fafafa; } + .message.is-dark .message-header { + background-color: #363636; + color: whitesmoke; } + .message.is-dark .message-body { + border-color: #363636; + color: #2a2a2a; } + .message.is-primary { + background-color: #fef7f6; } + .message.is-primary .message-header { + background-color: #C93312; + color: #fff; } + .message.is-primary .message-body { + border-color: #C93312; + color: #8a2711; } + .message.is-link { + background-color: #f6f9fe; } + .message.is-link .message-header { + background-color: #3273dc; + color: #fff; } + .message.is-link .message-body { + border-color: #3273dc; + color: #22509a; } + .message.is-info { + background-color: #f6fbfe; } + .message.is-info .message-header { + background-color: #209cee; + color: #fff; } + .message.is-info .message-body { + border-color: #209cee; + color: #12537e; } + .message.is-success { + background-color: #f6fef9; } + .message.is-success .message-header { + background-color: #23d160; + color: #fff; } + .message.is-success .message-body { + border-color: #23d160; + color: #0e301a; } + .message.is-warning { + background-color: #fffdf5; } + .message.is-warning .message-header { + background-color: #ffdd57; + color: #FFFFFF; } + .message.is-warning .message-body { + border-color: #ffdd57; + color: #3b3108; } + .message.is-danger { + background-color: #fff5f7; } + .message.is-danger .message-header { + background-color: #ff3860; + color: #fff; } + .message.is-danger .message-body { + border-color: #ff3860; + color: #cd0930; } + +.message-header { + align-items: center; + background-color: #4a4a4a; + border-radius: 3px 3px 0 0; + color: #fff; + display: flex; + justify-content: space-between; + line-height: 1.25; + padding: 0.5em 0.75em; + position: relative; } + .message-header .delete { + flex-grow: 0; + flex-shrink: 0; + margin-left: 0.75em; } + .message-header + .message-body { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: none; } + +.message-body { + border: 1px solid #dbdbdb; + border-radius: 3px; + color: #4a4a4a; + padding: 1em 1.25em; } + .message-body code, + .message-body pre { + background-color: white; } + .message-body pre code { + background-color: transparent; } + +.modal { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + align-items: center; + display: none; + justify-content: center; + overflow: hidden; + position: fixed; + z-index: 20; } + .modal.is-active { + display: flex; } + +.modal-background { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + background-color: rgba(10, 10, 10, 0.86); } + +.modal-content, +.modal-card { + margin: 0 20px; + max-height: calc(100vh - 160px); + overflow: auto; + position: relative; + width: 100%; } + @media screen and (min-width: 769px), print { + .modal-content, + .modal-card { + margin: 0 auto; + max-height: calc(100vh - 40px); + width: 640px; } } + +.modal-close { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -moz-appearance: none; + -webkit-appearance: none; + background-color: rgba(10, 10, 10, 0.2); + border: none; + border-radius: 290486px; + cursor: pointer; + display: inline-block; + flex-grow: 0; + flex-shrink: 0; + font-size: 0; + height: 20px; + max-height: 20px; + max-width: 20px; + min-height: 20px; + min-width: 20px; + outline: none; + position: relative; + vertical-align: top; + width: 20px; + background: none; + height: 40px; + position: fixed; + right: 20px; + top: 20px; + width: 40px; } + .modal-close:before, .modal-close:after { + background-color: white; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } + .modal-close:before { + height: 2px; + width: 50%; } + .modal-close:after { + height: 50%; + width: 2px; } + .modal-close:hover, .modal-close:focus { + background-color: rgba(10, 10, 10, 0.3); } + .modal-close:active { + background-color: rgba(10, 10, 10, 0.4); } + .modal-close.is-small { + height: 16px; + max-height: 16px; + max-width: 16px; + min-height: 16px; + min-width: 16px; + width: 16px; } + .modal-close.is-medium { + height: 24px; + max-height: 24px; + max-width: 24px; + min-height: 24px; + min-width: 24px; + width: 24px; } + .modal-close.is-large { + height: 32px; + max-height: 32px; + max-width: 32px; + min-height: 32px; + min-width: 32px; + width: 32px; } + +.modal-card { + display: flex; + flex-direction: column; + max-height: calc(100vh - 40px); + overflow: hidden; } + +.modal-card-head, +.modal-card-foot { + align-items: center; + background-color: whitesmoke; + display: flex; + flex-shrink: 0; + justify-content: flex-start; + padding: 20px; + position: relative; } + +.modal-card-head { + border-bottom: 1px solid #dbdbdb; + border-top-left-radius: 5px; + border-top-right-radius: 5px; } + +.modal-card-title { + color: #363636; + flex-grow: 1; + flex-shrink: 0; + font-size: 1.5rem; + line-height: 1; } + +.modal-card-foot { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-top: 1px solid #dbdbdb; } + .modal-card-foot .button:not(:last-child) { + margin-right: 10px; } + +.modal-card-body { + -webkit-overflow-scrolling: touch; + background-color: white; + flex-grow: 1; + flex-shrink: 1; + overflow: auto; + padding: 20px; } + +.navbar { + background-color: white; + min-height: 3.25rem; + position: relative; } + .navbar.is-white { + background-color: white; + color: #0a0a0a; } + .navbar.is-white .navbar-brand > .navbar-item, + .navbar.is-white .navbar-brand .navbar-link { + color: #0a0a0a; } + .navbar.is-white .navbar-brand > a.navbar-item:hover, .navbar.is-white .navbar-brand > a.navbar-item.is-active, + .navbar.is-white .navbar-brand .navbar-link:hover, + .navbar.is-white .navbar-brand .navbar-link.is-active { + background-color: #f2f2f2; + color: #0a0a0a; } + .navbar.is-white .navbar-brand .navbar-link::after { + border-color: #0a0a0a; } + @media screen and (min-width: 1024px) { + .navbar.is-white .navbar-start > .navbar-item, + .navbar.is-white .navbar-start .navbar-link, + .navbar.is-white .navbar-end > .navbar-item, + .navbar.is-white .navbar-end .navbar-link { + color: #0a0a0a; } + .navbar.is-white .navbar-start > a.navbar-item:hover, .navbar.is-white .navbar-start > a.navbar-item.is-active, + .navbar.is-white .navbar-start .navbar-link:hover, + .navbar.is-white .navbar-start .navbar-link.is-active, + .navbar.is-white .navbar-end > a.navbar-item:hover, + .navbar.is-white .navbar-end > a.navbar-item.is-active, + .navbar.is-white .navbar-end .navbar-link:hover, + .navbar.is-white .navbar-end .navbar-link.is-active { + background-color: #f2f2f2; + color: #0a0a0a; } + .navbar.is-white .navbar-start .navbar-link::after, + .navbar.is-white .navbar-end .navbar-link::after { + border-color: #0a0a0a; } + .navbar.is-white .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #f2f2f2; + color: #0a0a0a; } + .navbar.is-white .navbar-dropdown a.navbar-item.is-active { + background-color: white; + color: #0a0a0a; } } + .navbar.is-black { + background-color: #0a0a0a; + color: white; } + .navbar.is-black .navbar-brand > .navbar-item, + .navbar.is-black .navbar-brand .navbar-link { + color: white; } + .navbar.is-black .navbar-brand > a.navbar-item:hover, .navbar.is-black .navbar-brand > a.navbar-item.is-active, + .navbar.is-black .navbar-brand .navbar-link:hover, + .navbar.is-black .navbar-brand .navbar-link.is-active { + background-color: black; + color: white; } + .navbar.is-black .navbar-brand .navbar-link::after { + border-color: white; } + @media screen and (min-width: 1024px) { + .navbar.is-black .navbar-start > .navbar-item, + .navbar.is-black .navbar-start .navbar-link, + .navbar.is-black .navbar-end > .navbar-item, + .navbar.is-black .navbar-end .navbar-link { + color: white; } + .navbar.is-black .navbar-start > a.navbar-item:hover, .navbar.is-black .navbar-start > a.navbar-item.is-active, + .navbar.is-black .navbar-start .navbar-link:hover, + .navbar.is-black .navbar-start .navbar-link.is-active, + .navbar.is-black .navbar-end > a.navbar-item:hover, + .navbar.is-black .navbar-end > a.navbar-item.is-active, + .navbar.is-black .navbar-end .navbar-link:hover, + .navbar.is-black .navbar-end .navbar-link.is-active { + background-color: black; + color: white; } + .navbar.is-black .navbar-start .navbar-link::after, + .navbar.is-black .navbar-end .navbar-link::after { + border-color: white; } + .navbar.is-black .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link { + background-color: black; + color: white; } + .navbar.is-black .navbar-dropdown a.navbar-item.is-active { + background-color: #0a0a0a; + color: white; } } + .navbar.is-light { + background-color: whitesmoke; + color: #363636; } + .navbar.is-light .navbar-brand > .navbar-item, + .navbar.is-light .navbar-brand .navbar-link { + color: #363636; } + .navbar.is-light .navbar-brand > a.navbar-item:hover, .navbar.is-light .navbar-brand > a.navbar-item.is-active, + .navbar.is-light .navbar-brand .navbar-link:hover, + .navbar.is-light .navbar-brand .navbar-link.is-active { + background-color: #e8e8e8; + color: #363636; } + .navbar.is-light .navbar-brand .navbar-link::after { + border-color: #363636; } + @media screen and (min-width: 1024px) { + .navbar.is-light .navbar-start > .navbar-item, + .navbar.is-light .navbar-start .navbar-link, + .navbar.is-light .navbar-end > .navbar-item, + .navbar.is-light .navbar-end .navbar-link { + color: #363636; } + .navbar.is-light .navbar-start > a.navbar-item:hover, .navbar.is-light .navbar-start > a.navbar-item.is-active, + .navbar.is-light .navbar-start .navbar-link:hover, + .navbar.is-light .navbar-start .navbar-link.is-active, + .navbar.is-light .navbar-end > a.navbar-item:hover, + .navbar.is-light .navbar-end > a.navbar-item.is-active, + .navbar.is-light .navbar-end .navbar-link:hover, + .navbar.is-light .navbar-end .navbar-link.is-active { + background-color: #e8e8e8; + color: #363636; } + .navbar.is-light .navbar-start .navbar-link::after, + .navbar.is-light .navbar-end .navbar-link::after { + border-color: #363636; } + .navbar.is-light .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #e8e8e8; + color: #363636; } + .navbar.is-light .navbar-dropdown a.navbar-item.is-active { + background-color: whitesmoke; + color: #363636; } } + .navbar.is-dark { + background-color: #363636; + color: whitesmoke; } + .navbar.is-dark .navbar-brand > .navbar-item, + .navbar.is-dark .navbar-brand .navbar-link { + color: whitesmoke; } + .navbar.is-dark .navbar-brand > a.navbar-item:hover, .navbar.is-dark .navbar-brand > a.navbar-item.is-active, + .navbar.is-dark .navbar-brand .navbar-link:hover, + .navbar.is-dark .navbar-brand .navbar-link.is-active { + background-color: #292929; + color: whitesmoke; } + .navbar.is-dark .navbar-brand .navbar-link::after { + border-color: whitesmoke; } + @media screen and (min-width: 1024px) { + .navbar.is-dark .navbar-start > .navbar-item, + .navbar.is-dark .navbar-start .navbar-link, + .navbar.is-dark .navbar-end > .navbar-item, + .navbar.is-dark .navbar-end .navbar-link { + color: whitesmoke; } + .navbar.is-dark .navbar-start > a.navbar-item:hover, .navbar.is-dark .navbar-start > a.navbar-item.is-active, + .navbar.is-dark .navbar-start .navbar-link:hover, + .navbar.is-dark .navbar-start .navbar-link.is-active, + .navbar.is-dark .navbar-end > a.navbar-item:hover, + .navbar.is-dark .navbar-end > a.navbar-item.is-active, + .navbar.is-dark .navbar-end .navbar-link:hover, + .navbar.is-dark .navbar-end .navbar-link.is-active { + background-color: #292929; + color: whitesmoke; } + .navbar.is-dark .navbar-start .navbar-link::after, + .navbar.is-dark .navbar-end .navbar-link::after { + border-color: whitesmoke; } + .navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #292929; + color: whitesmoke; } + .navbar.is-dark .navbar-dropdown a.navbar-item.is-active { + background-color: #363636; + color: whitesmoke; } } + .navbar.is-primary { + background-color: #C93312; + color: #fff; } + .navbar.is-primary .navbar-brand > .navbar-item, + .navbar.is-primary .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-primary .navbar-brand > a.navbar-item:hover, .navbar.is-primary .navbar-brand > a.navbar-item.is-active, + .navbar.is-primary .navbar-brand .navbar-link:hover, + .navbar.is-primary .navbar-brand .navbar-link.is-active { + background-color: #b22d10; + color: #fff; } + .navbar.is-primary .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-primary .navbar-start > .navbar-item, + .navbar.is-primary .navbar-start .navbar-link, + .navbar.is-primary .navbar-end > .navbar-item, + .navbar.is-primary .navbar-end .navbar-link { + color: #fff; } + .navbar.is-primary .navbar-start > a.navbar-item:hover, .navbar.is-primary .navbar-start > a.navbar-item.is-active, + .navbar.is-primary .navbar-start .navbar-link:hover, + .navbar.is-primary .navbar-start .navbar-link.is-active, + .navbar.is-primary .navbar-end > a.navbar-item:hover, + .navbar.is-primary .navbar-end > a.navbar-item.is-active, + .navbar.is-primary .navbar-end .navbar-link:hover, + .navbar.is-primary .navbar-end .navbar-link.is-active { + background-color: #b22d10; + color: #fff; } + .navbar.is-primary .navbar-start .navbar-link::after, + .navbar.is-primary .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #b22d10; + color: #fff; } + .navbar.is-primary .navbar-dropdown a.navbar-item.is-active { + background-color: #C93312; + color: #fff; } } + .navbar.is-link { + background-color: #3273dc; + color: #fff; } + .navbar.is-link .navbar-brand > .navbar-item, + .navbar.is-link .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-link .navbar-brand > a.navbar-item:hover, .navbar.is-link .navbar-brand > a.navbar-item.is-active, + .navbar.is-link .navbar-brand .navbar-link:hover, + .navbar.is-link .navbar-brand .navbar-link.is-active { + background-color: #2366d1; + color: #fff; } + .navbar.is-link .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-link .navbar-start > .navbar-item, + .navbar.is-link .navbar-start .navbar-link, + .navbar.is-link .navbar-end > .navbar-item, + .navbar.is-link .navbar-end .navbar-link { + color: #fff; } + .navbar.is-link .navbar-start > a.navbar-item:hover, .navbar.is-link .navbar-start > a.navbar-item.is-active, + .navbar.is-link .navbar-start .navbar-link:hover, + .navbar.is-link .navbar-start .navbar-link.is-active, + .navbar.is-link .navbar-end > a.navbar-item:hover, + .navbar.is-link .navbar-end > a.navbar-item.is-active, + .navbar.is-link .navbar-end .navbar-link:hover, + .navbar.is-link .navbar-end .navbar-link.is-active { + background-color: #2366d1; + color: #fff; } + .navbar.is-link .navbar-start .navbar-link::after, + .navbar.is-link .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-link .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #2366d1; + color: #fff; } + .navbar.is-link .navbar-dropdown a.navbar-item.is-active { + background-color: #3273dc; + color: #fff; } } + .navbar.is-info { + background-color: #209cee; + color: #fff; } + .navbar.is-info .navbar-brand > .navbar-item, + .navbar.is-info .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-info .navbar-brand > a.navbar-item:hover, .navbar.is-info .navbar-brand > a.navbar-item.is-active, + .navbar.is-info .navbar-brand .navbar-link:hover, + .navbar.is-info .navbar-brand .navbar-link.is-active { + background-color: #118fe4; + color: #fff; } + .navbar.is-info .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-info .navbar-start > .navbar-item, + .navbar.is-info .navbar-start .navbar-link, + .navbar.is-info .navbar-end > .navbar-item, + .navbar.is-info .navbar-end .navbar-link { + color: #fff; } + .navbar.is-info .navbar-start > a.navbar-item:hover, .navbar.is-info .navbar-start > a.navbar-item.is-active, + .navbar.is-info .navbar-start .navbar-link:hover, + .navbar.is-info .navbar-start .navbar-link.is-active, + .navbar.is-info .navbar-end > a.navbar-item:hover, + .navbar.is-info .navbar-end > a.navbar-item.is-active, + .navbar.is-info .navbar-end .navbar-link:hover, + .navbar.is-info .navbar-end .navbar-link.is-active { + background-color: #118fe4; + color: #fff; } + .navbar.is-info .navbar-start .navbar-link::after, + .navbar.is-info .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-info .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #118fe4; + color: #fff; } + .navbar.is-info .navbar-dropdown a.navbar-item.is-active { + background-color: #209cee; + color: #fff; } } + .navbar.is-success { + background-color: #23d160; + color: #fff; } + .navbar.is-success .navbar-brand > .navbar-item, + .navbar.is-success .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-success .navbar-brand > a.navbar-item:hover, .navbar.is-success .navbar-brand > a.navbar-item.is-active, + .navbar.is-success .navbar-brand .navbar-link:hover, + .navbar.is-success .navbar-brand .navbar-link.is-active { + background-color: #20bc56; + color: #fff; } + .navbar.is-success .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-success .navbar-start > .navbar-item, + .navbar.is-success .navbar-start .navbar-link, + .navbar.is-success .navbar-end > .navbar-item, + .navbar.is-success .navbar-end .navbar-link { + color: #fff; } + .navbar.is-success .navbar-start > a.navbar-item:hover, .navbar.is-success .navbar-start > a.navbar-item.is-active, + .navbar.is-success .navbar-start .navbar-link:hover, + .navbar.is-success .navbar-start .navbar-link.is-active, + .navbar.is-success .navbar-end > a.navbar-item:hover, + .navbar.is-success .navbar-end > a.navbar-item.is-active, + .navbar.is-success .navbar-end .navbar-link:hover, + .navbar.is-success .navbar-end .navbar-link.is-active { + background-color: #20bc56; + color: #fff; } + .navbar.is-success .navbar-start .navbar-link::after, + .navbar.is-success .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-success .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #20bc56; + color: #fff; } + .navbar.is-success .navbar-dropdown a.navbar-item.is-active { + background-color: #23d160; + color: #fff; } } + .navbar.is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .navbar.is-warning .navbar-brand > .navbar-item, + .navbar.is-warning .navbar-brand .navbar-link { + color: #FFFFFF; } + .navbar.is-warning .navbar-brand > a.navbar-item:hover, .navbar.is-warning .navbar-brand > a.navbar-item.is-active, + .navbar.is-warning .navbar-brand .navbar-link:hover, + .navbar.is-warning .navbar-brand .navbar-link.is-active { + background-color: #ffd83d; + color: #FFFFFF; } + .navbar.is-warning .navbar-brand .navbar-link::after { + border-color: #FFFFFF; } + @media screen and (min-width: 1024px) { + .navbar.is-warning .navbar-start > .navbar-item, + .navbar.is-warning .navbar-start .navbar-link, + .navbar.is-warning .navbar-end > .navbar-item, + .navbar.is-warning .navbar-end .navbar-link { + color: #FFFFFF; } + .navbar.is-warning .navbar-start > a.navbar-item:hover, .navbar.is-warning .navbar-start > a.navbar-item.is-active, + .navbar.is-warning .navbar-start .navbar-link:hover, + .navbar.is-warning .navbar-start .navbar-link.is-active, + .navbar.is-warning .navbar-end > a.navbar-item:hover, + .navbar.is-warning .navbar-end > a.navbar-item.is-active, + .navbar.is-warning .navbar-end .navbar-link:hover, + .navbar.is-warning .navbar-end .navbar-link.is-active { + background-color: #ffd83d; + color: #FFFFFF; } + .navbar.is-warning .navbar-start .navbar-link::after, + .navbar.is-warning .navbar-end .navbar-link::after { + border-color: #FFFFFF; } + .navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #ffd83d; + color: #FFFFFF; } + .navbar.is-warning .navbar-dropdown a.navbar-item.is-active { + background-color: #ffdd57; + color: #FFFFFF; } } + .navbar.is-danger { + background-color: #ff3860; + color: #fff; } + .navbar.is-danger .navbar-brand > .navbar-item, + .navbar.is-danger .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-danger .navbar-brand > a.navbar-item:hover, .navbar.is-danger .navbar-brand > a.navbar-item.is-active, + .navbar.is-danger .navbar-brand .navbar-link:hover, + .navbar.is-danger .navbar-brand .navbar-link.is-active { + background-color: #ff1f4b; + color: #fff; } + .navbar.is-danger .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-danger .navbar-start > .navbar-item, + .navbar.is-danger .navbar-start .navbar-link, + .navbar.is-danger .navbar-end > .navbar-item, + .navbar.is-danger .navbar-end .navbar-link { + color: #fff; } + .navbar.is-danger .navbar-start > a.navbar-item:hover, .navbar.is-danger .navbar-start > a.navbar-item.is-active, + .navbar.is-danger .navbar-start .navbar-link:hover, + .navbar.is-danger .navbar-start .navbar-link.is-active, + .navbar.is-danger .navbar-end > a.navbar-item:hover, + .navbar.is-danger .navbar-end > a.navbar-item.is-active, + .navbar.is-danger .navbar-end .navbar-link:hover, + .navbar.is-danger .navbar-end .navbar-link.is-active { + background-color: #ff1f4b; + color: #fff; } + .navbar.is-danger .navbar-start .navbar-link::after, + .navbar.is-danger .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #ff1f4b; + color: #fff; } + .navbar.is-danger .navbar-dropdown a.navbar-item.is-active { + background-color: #ff3860; + color: #fff; } } + .navbar > .container { + align-items: stretch; + display: flex; + min-height: 3.25rem; + width: 100%; } + .navbar.has-shadow { + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-bottom, .navbar.is-fixed-top { + left: 0; + position: fixed; + right: 0; + z-index: 30; } + .navbar.is-fixed-bottom { + bottom: 0; } + .navbar.is-fixed-bottom.has-shadow { + box-shadow: 0 -2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-top { + top: 0; } + +html.has-navbar-fixed-top { + padding-top: 3.25rem; } + +html.has-navbar-fixed-bottom { + padding-bottom: 3.25rem; } + +.navbar-brand, +.navbar-tabs { + align-items: stretch; + display: flex; + flex-shrink: 0; + min-height: 3.25rem; } + +.navbar-tabs { + -webkit-overflow-scrolling: touch; + max-width: 100vw; + overflow-x: auto; + overflow-y: hidden; } + +.navbar-burger { + cursor: pointer; + display: block; + height: 3.25rem; + position: relative; + width: 3.25rem; + margin-left: auto; } + .navbar-burger span { + background-color: currentColor; + display: block; + height: 1px; + left: calc(50% - 8px); + position: absolute; + transform-origin: center; + transition-duration: 86ms; + transition-property: background-color, opacity, transform; + transition-timing-function: ease-out; + width: 16px; } + .navbar-burger span:nth-child(1) { + top: calc(50% - 6px); } + .navbar-burger span:nth-child(2) { + top: calc(50% - 1px); } + .navbar-burger span:nth-child(3) { + top: calc(50% + 4px); } + .navbar-burger:hover { + background-color: rgba(0, 0, 0, 0.05); } + .navbar-burger.is-active span:nth-child(1) { + transform: translateY(5px) rotate(45deg); } + .navbar-burger.is-active span:nth-child(2) { + opacity: 0; } + .navbar-burger.is-active span:nth-child(3) { + transform: translateY(-5px) rotate(-45deg); } + +.navbar-menu { + display: none; } + +.navbar-item, +.navbar-link { + color: #4a4a4a; + display: block; + line-height: 1.5; + padding: 0.5rem 1rem; + position: relative; } + +a.navbar-item:hover, a.navbar-item.is-active, +a.navbar-link:hover, +a.navbar-link.is-active { + background-color: whitesmoke; + color: #3273dc; } + +.navbar-item { + flex-grow: 0; + flex-shrink: 0; } + .navbar-item img { + max-height: 1.75rem; } + .navbar-item.has-dropdown { + padding: 0; } + .navbar-item.is-expanded { + flex-grow: 1; + flex-shrink: 1; } + .navbar-item.is-tab { + border-bottom: 1px solid transparent; + min-height: 3.25rem; + padding-bottom: calc(0.5rem - 1px); } + .navbar-item.is-tab:hover { + background-color: transparent; + border-bottom-color: #3273dc; } + .navbar-item.is-tab.is-active { + background-color: transparent; + border-bottom-color: #3273dc; + border-bottom-style: solid; + border-bottom-width: 3px; + color: #3273dc; + padding-bottom: calc(0.5rem - 3px); } + +.navbar-content { + flex-grow: 1; + flex-shrink: 1; } + +.navbar-link { + padding-right: 2.5em; } + +.navbar-dropdown { + font-size: 0.875rem; + padding-bottom: 0.5rem; + padding-top: 0.5rem; } + .navbar-dropdown .navbar-item { + padding-left: 1.5rem; + padding-right: 1.5rem; } + +.navbar-divider { + background-color: #dbdbdb; + border: none; + display: none; + height: 1px; + margin: 0.5rem 0; } + +@media screen and (max-width: 1023px) { + .navbar > .container { + display: block; } + .navbar-brand .navbar-item, + .navbar-tabs .navbar-item { + align-items: center; + display: flex; } + .navbar-menu { + background-color: white; + box-shadow: 0 8px 16px rgba(10, 10, 10, 0.1); + padding: 0.5rem 0; } + .navbar-menu.is-active { + display: block; } + .navbar.is-fixed-bottom-touch, .navbar.is-fixed-top-touch { + left: 0; + position: fixed; + right: 0; + z-index: 30; } + .navbar.is-fixed-bottom-touch { + bottom: 0; } + .navbar.is-fixed-bottom-touch.has-shadow { + box-shadow: 0 -2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-top-touch { + top: 0; } + .navbar.is-fixed-top .navbar-menu, .navbar.is-fixed-top-touch .navbar-menu { + -webkit-overflow-scrolling: touch; + max-height: calc(100vh - 3.25rem); + overflow: auto; } + html.has-navbar-fixed-top-touch { + padding-top: 3.25rem; } + html.has-navbar-fixed-bottom-touch { + padding-bottom: 3.25rem; } } + +@media screen and (min-width: 1024px) { + .navbar, + .navbar-menu, + .navbar-start, + .navbar-end { + align-items: stretch; + display: flex; } + .navbar { + min-height: 3.25rem; } + .navbar.is-transparent a.navbar-item:hover, .navbar.is-transparent a.navbar-item.is-active, + .navbar.is-transparent a.navbar-link:hover, + .navbar.is-transparent a.navbar-link.is-active { + background-color: transparent !important; } + .navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link, .navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link { + background-color: transparent !important; } + .navbar.is-transparent .navbar-dropdown a.navbar-item:hover { + background-color: whitesmoke; + color: #0a0a0a; } + .navbar.is-transparent .navbar-dropdown a.navbar-item.is-active { + background-color: whitesmoke; + color: #3273dc; } + .navbar-burger { + display: none; } + .navbar-item, + .navbar-link { + align-items: center; + display: flex; } + .navbar-item.has-dropdown { + align-items: stretch; } + .navbar-item.has-dropdown-up .navbar-link::after { + transform: rotate(135deg) translate(0.25em, -0.25em); } + .navbar-item.has-dropdown-up .navbar-dropdown { + border-bottom: 1px solid #dbdbdb; + border-radius: 5px 5px 0 0; + border-top: none; + bottom: 100%; + box-shadow: 0 -8px 8px rgba(10, 10, 10, 0.1); + top: auto; } + .navbar-item.is-active .navbar-dropdown, .navbar-item.is-hoverable:hover .navbar-dropdown { + display: block; } + .navbar-item.is-active .navbar-dropdown.is-boxed, .navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed { + opacity: 1; + pointer-events: auto; + transform: translateY(0); } + .navbar-link::after { + border: 1px solid #3273dc; + border-right: 0; + border-top: 0; + content: " "; + display: block; + height: 0.5em; + pointer-events: none; + position: absolute; + transform: rotate(-45deg); + transform-origin: center; + width: 0.5em; + margin-top: -0.375em; + right: 1.125em; + top: 50%; } + .navbar-menu { + flex-grow: 1; + flex-shrink: 0; } + .navbar-start { + justify-content: flex-start; + margin-right: auto; } + .navbar-end { + justify-content: flex-end; + margin-left: auto; } + .navbar-dropdown { + background-color: white; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-top: 1px solid #dbdbdb; + box-shadow: 0 8px 8px rgba(10, 10, 10, 0.1); + display: none; + font-size: 0.875rem; + left: 0; + min-width: 100%; + position: absolute; + top: 100%; + z-index: 20; } + .navbar-dropdown .navbar-item { + padding: 0.375rem 1rem; + white-space: nowrap; } + .navbar-dropdown a.navbar-item { + padding-right: 3rem; } + .navbar-dropdown a.navbar-item:hover { + background-color: whitesmoke; + color: #0a0a0a; } + .navbar-dropdown a.navbar-item.is-active { + background-color: whitesmoke; + color: #3273dc; } + .navbar-dropdown.is-boxed { + border-radius: 5px; + border-top: none; + box-shadow: 0 8px 8px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + display: block; + opacity: 0; + pointer-events: none; + top: calc(100% + (-4px)); + transform: translateY(-5px); + transition-duration: 86ms; + transition-property: opacity, transform; } + .navbar-dropdown.is-right { + left: auto; + right: 0; } + .navbar-divider { + display: block; } + .navbar > .container .navbar-brand, + .container > .navbar .navbar-brand { + margin-left: -1rem; } + .navbar > .container .navbar-menu, + .container > .navbar .navbar-menu { + margin-right: -1rem; } + .navbar.is-fixed-bottom-desktop, .navbar.is-fixed-top-desktop { + left: 0; + position: fixed; + right: 0; + z-index: 30; } + .navbar.is-fixed-bottom-desktop { + bottom: 0; } + .navbar.is-fixed-bottom-desktop.has-shadow { + box-shadow: 0 -2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-top-desktop { + top: 0; } + html.has-navbar-fixed-top-desktop { + padding-top: 3.25rem; } + html.has-navbar-fixed-bottom-desktop { + padding-bottom: 3.25rem; } + a.navbar-item.is-active, + a.navbar-link.is-active { + color: #0a0a0a; } + a.navbar-item.is-active:not(:hover), + a.navbar-link.is-active:not(:hover) { + background-color: transparent; } + .navbar-item.has-dropdown:hover .navbar-link, .navbar-item.has-dropdown.is-active .navbar-link { + background-color: whitesmoke; } } + +.pagination { + font-size: 1rem; + margin: -0.25rem; } + .pagination.is-small { + font-size: 0.75rem; } + .pagination.is-medium { + font-size: 1.25rem; } + .pagination.is-large { + font-size: 1.5rem; } + +.pagination, +.pagination-list { + align-items: center; + display: flex; + justify-content: center; + text-align: center; } + +.pagination-previous, +.pagination-next, +.pagination-link, +.pagination-ellipsis { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-size: 1em; + padding-left: 0.5em; + padding-right: 0.5em; + justify-content: center; + margin: 0.25rem; + text-align: center; } + .pagination-previous:focus, .pagination-previous.is-focused, .pagination-previous:active, .pagination-previous.is-active, + .pagination-next:focus, + .pagination-next.is-focused, + .pagination-next:active, + .pagination-next.is-active, + .pagination-link:focus, + .pagination-link.is-focused, + .pagination-link:active, + .pagination-link.is-active, + .pagination-ellipsis:focus, + .pagination-ellipsis.is-focused, + .pagination-ellipsis:active, + .pagination-ellipsis.is-active { + outline: none; } + .pagination-previous[disabled], + .pagination-next[disabled], + .pagination-link[disabled], + .pagination-ellipsis[disabled] { + cursor: not-allowed; } + +.pagination-previous, +.pagination-next, +.pagination-link { + border-color: #dbdbdb; + min-width: 2.25em; } + .pagination-previous:hover, + .pagination-next:hover, + .pagination-link:hover { + border-color: #b5b5b5; + color: #363636; } + .pagination-previous:focus, + .pagination-next:focus, + .pagination-link:focus { + border-color: #3273dc; } + .pagination-previous:active, + .pagination-next:active, + .pagination-link:active { + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.2); } + .pagination-previous[disabled], + .pagination-next[disabled], + .pagination-link[disabled] { + background-color: #dbdbdb; + border-color: #dbdbdb; + box-shadow: none; + color: #7a7a7a; + opacity: 0.5; } + +.pagination-previous, +.pagination-next { + padding-left: 0.75em; + padding-right: 0.75em; + white-space: nowrap; } + +.pagination-link.is-current { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; } + +.pagination-ellipsis { + color: #b5b5b5; + pointer-events: none; } + +.pagination-list { + flex-wrap: wrap; } + +@media screen and (max-width: 768px) { + .pagination { + flex-wrap: wrap; } + .pagination-previous, + .pagination-next { + flex-grow: 1; + flex-shrink: 1; } + .pagination-list li { + flex-grow: 1; + flex-shrink: 1; } } + +@media screen and (min-width: 769px), print { + .pagination-list { + flex-grow: 1; + flex-shrink: 1; + justify-content: flex-start; + order: 1; } + .pagination-previous { + order: 2; } + .pagination-next { + order: 3; } + .pagination { + justify-content: space-between; } + .pagination.is-centered .pagination-previous { + order: 1; } + .pagination.is-centered .pagination-list { + justify-content: center; + order: 2; } + .pagination.is-centered .pagination-next { + order: 3; } + .pagination.is-right .pagination-previous { + order: 1; } + .pagination.is-right .pagination-next { + order: 2; } + .pagination.is-right .pagination-list { + justify-content: flex-end; + order: 3; } } + +.panel { + font-size: 1rem; } + .panel:not(:last-child) { + margin-bottom: 1.5rem; } + +.panel-heading, +.panel-tabs, +.panel-block { + border-bottom: 1px solid #dbdbdb; + border-left: 1px solid #dbdbdb; + border-right: 1px solid #dbdbdb; } + .panel-heading:first-child, + .panel-tabs:first-child, + .panel-block:first-child { + border-top: 1px solid #dbdbdb; } + +.panel-heading { + background-color: whitesmoke; + border-radius: 3px 3px 0 0; + color: #363636; + font-size: 1.25em; + font-weight: 300; + line-height: 1.25; + padding: 0.5em 0.75em; } + +.panel-tabs { + align-items: flex-end; + display: flex; + font-size: 0.875em; + justify-content: center; } + .panel-tabs a { + border-bottom: 1px solid #dbdbdb; + margin-bottom: -1px; + padding: 0.5em; } + .panel-tabs a.is-active { + border-bottom-color: #4a4a4a; + color: #363636; } + +.panel-list a { + color: #4a4a4a; } + .panel-list a:hover { + color: #3273dc; } + +.panel-block { + align-items: center; + color: #363636; + display: flex; + justify-content: flex-start; + padding: 0.5em 0.75em; } + .panel-block input[type="checkbox"] { + margin-right: 0.75em; } + .panel-block > .control { + flex-grow: 1; + flex-shrink: 1; + width: 100%; } + .panel-block.is-wrapped { + flex-wrap: wrap; } + .panel-block.is-active { + border-left-color: #3273dc; + color: #363636; } + .panel-block.is-active .panel-icon { + color: #3273dc; } + +a.panel-block, +label.panel-block { + cursor: pointer; } + a.panel-block:hover, + label.panel-block:hover { + background-color: whitesmoke; } + +.panel-icon { + display: inline-block; + font-size: 14px; + height: 1em; + line-height: 1em; + text-align: center; + vertical-align: top; + width: 1em; + color: #7a7a7a; + margin-right: 0.75em; } + .panel-icon .fa { + font-size: inherit; + line-height: inherit; } + +.tabs { + -webkit-overflow-scrolling: touch; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: stretch; + display: flex; + font-size: 1rem; + justify-content: space-between; + overflow: hidden; + overflow-x: auto; + white-space: nowrap; } + .tabs:not(:last-child) { + margin-bottom: 1.5rem; } + .tabs a { + align-items: center; + border-bottom-color: #dbdbdb; + border-bottom-style: solid; + border-bottom-width: 1px; + color: #4a4a4a; + display: flex; + justify-content: center; + margin-bottom: -1px; + padding: 0.5em 1em; + vertical-align: top; } + .tabs a:hover { + border-bottom-color: #363636; + color: #363636; } + .tabs li { + display: block; } + .tabs li.is-active a { + border-bottom-color: #3273dc; + color: #3273dc; } + .tabs ul { + align-items: center; + border-bottom-color: #dbdbdb; + border-bottom-style: solid; + border-bottom-width: 1px; + display: flex; + flex-grow: 1; + flex-shrink: 0; + justify-content: flex-start; } + .tabs ul.is-left { + padding-right: 0.75em; } + .tabs ul.is-center { + flex: none; + justify-content: center; + padding-left: 0.75em; + padding-right: 0.75em; } + .tabs ul.is-right { + justify-content: flex-end; + padding-left: 0.75em; } + .tabs .icon:first-child { + margin-right: 0.5em; } + .tabs .icon:last-child { + margin-left: 0.5em; } + .tabs.is-centered ul { + justify-content: center; } + .tabs.is-right ul { + justify-content: flex-end; } + .tabs.is-boxed a { + border: 1px solid transparent; + border-radius: 3px 3px 0 0; } + .tabs.is-boxed a:hover { + background-color: whitesmoke; + border-bottom-color: #dbdbdb; } + .tabs.is-boxed li.is-active a { + background-color: white; + border-color: #dbdbdb; + border-bottom-color: transparent !important; } + .tabs.is-fullwidth li { + flex-grow: 1; + flex-shrink: 0; } + .tabs.is-toggle a { + border-color: #dbdbdb; + border-style: solid; + border-width: 1px; + margin-bottom: 0; + position: relative; } + .tabs.is-toggle a:hover { + background-color: whitesmoke; + border-color: #b5b5b5; + z-index: 2; } + .tabs.is-toggle li + li { + margin-left: -1px; } + .tabs.is-toggle li:first-child a { + border-radius: 3px 0 0 3px; } + .tabs.is-toggle li:last-child a { + border-radius: 0 3px 3px 0; } + .tabs.is-toggle li.is-active a { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; + z-index: 1; } + .tabs.is-toggle ul { + border-bottom: none; } + .tabs.is-small { + font-size: 0.75rem; } + .tabs.is-medium { + font-size: 1.25rem; } + .tabs.is-large { + font-size: 1.5rem; } + +.hero { + align-items: stretch; + display: flex; + flex-direction: column; + justify-content: space-between; } + .hero .navbar { + background: none; } + .hero .tabs ul { + border-bottom: none; } + .hero.is-white { + background-color: white; + color: #0a0a0a; } + .hero.is-white a:not(.button), + .hero.is-white strong { + color: inherit; } + .hero.is-white .title { + color: #0a0a0a; } + .hero.is-white .subtitle { + color: rgba(10, 10, 10, 0.9); } + .hero.is-white .subtitle a:not(.button), + .hero.is-white .subtitle strong { + color: #0a0a0a; } + @media screen and (max-width: 1023px) { + .hero.is-white .navbar-menu { + background-color: white; } } + .hero.is-white .navbar-item, + .hero.is-white .navbar-link { + color: rgba(10, 10, 10, 0.7); } + .hero.is-white a.navbar-item:hover, .hero.is-white a.navbar-item.is-active, + .hero.is-white .navbar-link:hover, + .hero.is-white .navbar-link.is-active { + background-color: #f2f2f2; + color: #0a0a0a; } + .hero.is-white .tabs a { + color: #0a0a0a; + opacity: 0.9; } + .hero.is-white .tabs a:hover { + opacity: 1; } + .hero.is-white .tabs li.is-active a { + opacity: 1; } + .hero.is-white .tabs.is-boxed a, .hero.is-white .tabs.is-toggle a { + color: #0a0a0a; } + .hero.is-white .tabs.is-boxed a:hover, .hero.is-white .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-white .tabs.is-boxed li.is-active a, .hero.is-white .tabs.is-boxed li.is-active a:hover, .hero.is-white .tabs.is-toggle li.is-active a, .hero.is-white .tabs.is-toggle li.is-active a:hover { + background-color: #0a0a0a; + border-color: #0a0a0a; + color: white; } + .hero.is-white.is-bold { + background-image: linear-gradient(141deg, #e6e6e6 0%, white 71%, white 100%); } + @media screen and (max-width: 768px) { + .hero.is-white.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #e6e6e6 0%, white 71%, white 100%); } } + .hero.is-black { + background-color: #0a0a0a; + color: white; } + .hero.is-black a:not(.button), + .hero.is-black strong { + color: inherit; } + .hero.is-black .title { + color: white; } + .hero.is-black .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-black .subtitle a:not(.button), + .hero.is-black .subtitle strong { + color: white; } + @media screen and (max-width: 1023px) { + .hero.is-black .navbar-menu { + background-color: #0a0a0a; } } + .hero.is-black .navbar-item, + .hero.is-black .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-black a.navbar-item:hover, .hero.is-black a.navbar-item.is-active, + .hero.is-black .navbar-link:hover, + .hero.is-black .navbar-link.is-active { + background-color: black; + color: white; } + .hero.is-black .tabs a { + color: white; + opacity: 0.9; } + .hero.is-black .tabs a:hover { + opacity: 1; } + .hero.is-black .tabs li.is-active a { + opacity: 1; } + .hero.is-black .tabs.is-boxed a, .hero.is-black .tabs.is-toggle a { + color: white; } + .hero.is-black .tabs.is-boxed a:hover, .hero.is-black .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-black .tabs.is-boxed li.is-active a, .hero.is-black .tabs.is-boxed li.is-active a:hover, .hero.is-black .tabs.is-toggle li.is-active a, .hero.is-black .tabs.is-toggle li.is-active a:hover { + background-color: white; + border-color: white; + color: #0a0a0a; } + .hero.is-black.is-bold { + background-image: linear-gradient(141deg, black 0%, #0a0a0a 71%, #181616 100%); } + @media screen and (max-width: 768px) { + .hero.is-black.is-bold .navbar-menu { + background-image: linear-gradient(141deg, black 0%, #0a0a0a 71%, #181616 100%); } } + .hero.is-light { + background-color: whitesmoke; + color: #363636; } + .hero.is-light a:not(.button), + .hero.is-light strong { + color: inherit; } + .hero.is-light .title { + color: #363636; } + .hero.is-light .subtitle { + color: rgba(54, 54, 54, 0.9); } + .hero.is-light .subtitle a:not(.button), + .hero.is-light .subtitle strong { + color: #363636; } + @media screen and (max-width: 1023px) { + .hero.is-light .navbar-menu { + background-color: whitesmoke; } } + .hero.is-light .navbar-item, + .hero.is-light .navbar-link { + color: rgba(54, 54, 54, 0.7); } + .hero.is-light a.navbar-item:hover, .hero.is-light a.navbar-item.is-active, + .hero.is-light .navbar-link:hover, + .hero.is-light .navbar-link.is-active { + background-color: #e8e8e8; + color: #363636; } + .hero.is-light .tabs a { + color: #363636; + opacity: 0.9; } + .hero.is-light .tabs a:hover { + opacity: 1; } + .hero.is-light .tabs li.is-active a { + opacity: 1; } + .hero.is-light .tabs.is-boxed a, .hero.is-light .tabs.is-toggle a { + color: #363636; } + .hero.is-light .tabs.is-boxed a:hover, .hero.is-light .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-light .tabs.is-boxed li.is-active a, .hero.is-light .tabs.is-boxed li.is-active a:hover, .hero.is-light .tabs.is-toggle li.is-active a, .hero.is-light .tabs.is-toggle li.is-active a:hover { + background-color: #363636; + border-color: #363636; + color: whitesmoke; } + .hero.is-light.is-bold { + background-image: linear-gradient(141deg, #dfd8d9 0%, whitesmoke 71%, white 100%); } + @media screen and (max-width: 768px) { + .hero.is-light.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #dfd8d9 0%, whitesmoke 71%, white 100%); } } + .hero.is-dark { + background-color: #363636; + color: whitesmoke; } + .hero.is-dark a:not(.button), + .hero.is-dark strong { + color: inherit; } + .hero.is-dark .title { + color: whitesmoke; } + .hero.is-dark .subtitle { + color: rgba(245, 245, 245, 0.9); } + .hero.is-dark .subtitle a:not(.button), + .hero.is-dark .subtitle strong { + color: whitesmoke; } + @media screen and (max-width: 1023px) { + .hero.is-dark .navbar-menu { + background-color: #363636; } } + .hero.is-dark .navbar-item, + .hero.is-dark .navbar-link { + color: rgba(245, 245, 245, 0.7); } + .hero.is-dark a.navbar-item:hover, .hero.is-dark a.navbar-item.is-active, + .hero.is-dark .navbar-link:hover, + .hero.is-dark .navbar-link.is-active { + background-color: #292929; + color: whitesmoke; } + .hero.is-dark .tabs a { + color: whitesmoke; + opacity: 0.9; } + .hero.is-dark .tabs a:hover { + opacity: 1; } + .hero.is-dark .tabs li.is-active a { + opacity: 1; } + .hero.is-dark .tabs.is-boxed a, .hero.is-dark .tabs.is-toggle a { + color: whitesmoke; } + .hero.is-dark .tabs.is-boxed a:hover, .hero.is-dark .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-dark .tabs.is-boxed li.is-active a, .hero.is-dark .tabs.is-boxed li.is-active a:hover, .hero.is-dark .tabs.is-toggle li.is-active a, .hero.is-dark .tabs.is-toggle li.is-active a:hover { + background-color: whitesmoke; + border-color: whitesmoke; + color: #363636; } + .hero.is-dark.is-bold { + background-image: linear-gradient(141deg, #1f191a 0%, #363636 71%, #46403f 100%); } + @media screen and (max-width: 768px) { + .hero.is-dark.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #1f191a 0%, #363636 71%, #46403f 100%); } } + .hero.is-primary { + background-color: #C93312; + color: #fff; } + .hero.is-primary a:not(.button), + .hero.is-primary strong { + color: inherit; } + .hero.is-primary .title { + color: #fff; } + .hero.is-primary .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-primary .subtitle a:not(.button), + .hero.is-primary .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-primary .navbar-menu { + background-color: #C93312; } } + .hero.is-primary .navbar-item, + .hero.is-primary .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-primary a.navbar-item:hover, .hero.is-primary a.navbar-item.is-active, + .hero.is-primary .navbar-link:hover, + .hero.is-primary .navbar-link.is-active { + background-color: #b22d10; + color: #fff; } + .hero.is-primary .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-primary .tabs a:hover { + opacity: 1; } + .hero.is-primary .tabs li.is-active a { + opacity: 1; } + .hero.is-primary .tabs.is-boxed a, .hero.is-primary .tabs.is-toggle a { + color: #fff; } + .hero.is-primary .tabs.is-boxed a:hover, .hero.is-primary .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-primary .tabs.is-boxed li.is-active a, .hero.is-primary .tabs.is-boxed li.is-active a:hover, .hero.is-primary .tabs.is-toggle li.is-active a, .hero.is-primary .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #C93312; } + .hero.is-primary.is-bold { + background-image: linear-gradient(141deg, #a30805 0%, #C93312 71%, #e7590e 100%); } + @media screen and (max-width: 768px) { + .hero.is-primary.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #a30805 0%, #C93312 71%, #e7590e 100%); } } + .hero.is-link { + background-color: #3273dc; + color: #fff; } + .hero.is-link a:not(.button), + .hero.is-link strong { + color: inherit; } + .hero.is-link .title { + color: #fff; } + .hero.is-link .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-link .subtitle a:not(.button), + .hero.is-link .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-link .navbar-menu { + background-color: #3273dc; } } + .hero.is-link .navbar-item, + .hero.is-link .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-link a.navbar-item:hover, .hero.is-link a.navbar-item.is-active, + .hero.is-link .navbar-link:hover, + .hero.is-link .navbar-link.is-active { + background-color: #2366d1; + color: #fff; } + .hero.is-link .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-link .tabs a:hover { + opacity: 1; } + .hero.is-link .tabs li.is-active a { + opacity: 1; } + .hero.is-link .tabs.is-boxed a, .hero.is-link .tabs.is-toggle a { + color: #fff; } + .hero.is-link .tabs.is-boxed a:hover, .hero.is-link .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-link .tabs.is-boxed li.is-active a, .hero.is-link .tabs.is-boxed li.is-active a:hover, .hero.is-link .tabs.is-toggle li.is-active a, .hero.is-link .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #3273dc; } + .hero.is-link.is-bold { + background-image: linear-gradient(141deg, #1577c6 0%, #3273dc 71%, #4366e5 100%); } + @media screen and (max-width: 768px) { + .hero.is-link.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #1577c6 0%, #3273dc 71%, #4366e5 100%); } } + .hero.is-info { + background-color: #209cee; + color: #fff; } + .hero.is-info a:not(.button), + .hero.is-info strong { + color: inherit; } + .hero.is-info .title { + color: #fff; } + .hero.is-info .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-info .subtitle a:not(.button), + .hero.is-info .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-info .navbar-menu { + background-color: #209cee; } } + .hero.is-info .navbar-item, + .hero.is-info .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-info a.navbar-item:hover, .hero.is-info a.navbar-item.is-active, + .hero.is-info .navbar-link:hover, + .hero.is-info .navbar-link.is-active { + background-color: #118fe4; + color: #fff; } + .hero.is-info .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-info .tabs a:hover { + opacity: 1; } + .hero.is-info .tabs li.is-active a { + opacity: 1; } + .hero.is-info .tabs.is-boxed a, .hero.is-info .tabs.is-toggle a { + color: #fff; } + .hero.is-info .tabs.is-boxed a:hover, .hero.is-info .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-info .tabs.is-boxed li.is-active a, .hero.is-info .tabs.is-boxed li.is-active a:hover, .hero.is-info .tabs.is-toggle li.is-active a, .hero.is-info .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #209cee; } + .hero.is-info.is-bold { + background-image: linear-gradient(141deg, #04a6d7 0%, #209cee 71%, #3287f5 100%); } + @media screen and (max-width: 768px) { + .hero.is-info.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #04a6d7 0%, #209cee 71%, #3287f5 100%); } } + .hero.is-success { + background-color: #23d160; + color: #fff; } + .hero.is-success a:not(.button), + .hero.is-success strong { + color: inherit; } + .hero.is-success .title { + color: #fff; } + .hero.is-success .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-success .subtitle a:not(.button), + .hero.is-success .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-success .navbar-menu { + background-color: #23d160; } } + .hero.is-success .navbar-item, + .hero.is-success .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-success a.navbar-item:hover, .hero.is-success a.navbar-item.is-active, + .hero.is-success .navbar-link:hover, + .hero.is-success .navbar-link.is-active { + background-color: #20bc56; + color: #fff; } + .hero.is-success .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-success .tabs a:hover { + opacity: 1; } + .hero.is-success .tabs li.is-active a { + opacity: 1; } + .hero.is-success .tabs.is-boxed a, .hero.is-success .tabs.is-toggle a { + color: #fff; } + .hero.is-success .tabs.is-boxed a:hover, .hero.is-success .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-success .tabs.is-boxed li.is-active a, .hero.is-success .tabs.is-boxed li.is-active a:hover, .hero.is-success .tabs.is-toggle li.is-active a, .hero.is-success .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #23d160; } + .hero.is-success.is-bold { + background-image: linear-gradient(141deg, #12af2f 0%, #23d160 71%, #2ce28a 100%); } + @media screen and (max-width: 768px) { + .hero.is-success.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #12af2f 0%, #23d160 71%, #2ce28a 100%); } } + .hero.is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .hero.is-warning a:not(.button), + .hero.is-warning strong { + color: inherit; } + .hero.is-warning .title { + color: #FFFFFF; } + .hero.is-warning .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-warning .subtitle a:not(.button), + .hero.is-warning .subtitle strong { + color: #FFFFFF; } + @media screen and (max-width: 1023px) { + .hero.is-warning .navbar-menu { + background-color: #ffdd57; } } + .hero.is-warning .navbar-item, + .hero.is-warning .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-warning a.navbar-item:hover, .hero.is-warning a.navbar-item.is-active, + .hero.is-warning .navbar-link:hover, + .hero.is-warning .navbar-link.is-active { + background-color: #ffd83d; + color: #FFFFFF; } + .hero.is-warning .tabs a { + color: #FFFFFF; + opacity: 0.9; } + .hero.is-warning .tabs a:hover { + opacity: 1; } + .hero.is-warning .tabs li.is-active a { + opacity: 1; } + .hero.is-warning .tabs.is-boxed a, .hero.is-warning .tabs.is-toggle a { + color: #FFFFFF; } + .hero.is-warning .tabs.is-boxed a:hover, .hero.is-warning .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-warning .tabs.is-boxed li.is-active a, .hero.is-warning .tabs.is-boxed li.is-active a:hover, .hero.is-warning .tabs.is-toggle li.is-active a, .hero.is-warning .tabs.is-toggle li.is-active a:hover { + background-color: #FFFFFF; + border-color: #FFFFFF; + color: #ffdd57; } + .hero.is-warning.is-bold { + background-image: linear-gradient(141deg, #ffaf24 0%, #ffdd57 71%, #fffa70 100%); } + @media screen and (max-width: 768px) { + .hero.is-warning.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #ffaf24 0%, #ffdd57 71%, #fffa70 100%); } } + .hero.is-danger { + background-color: #ff3860; + color: #fff; } + .hero.is-danger a:not(.button), + .hero.is-danger strong { + color: inherit; } + .hero.is-danger .title { + color: #fff; } + .hero.is-danger .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-danger .subtitle a:not(.button), + .hero.is-danger .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-danger .navbar-menu { + background-color: #ff3860; } } + .hero.is-danger .navbar-item, + .hero.is-danger .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-danger a.navbar-item:hover, .hero.is-danger a.navbar-item.is-active, + .hero.is-danger .navbar-link:hover, + .hero.is-danger .navbar-link.is-active { + background-color: #ff1f4b; + color: #fff; } + .hero.is-danger .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-danger .tabs a:hover { + opacity: 1; } + .hero.is-danger .tabs li.is-active a { + opacity: 1; } + .hero.is-danger .tabs.is-boxed a, .hero.is-danger .tabs.is-toggle a { + color: #fff; } + .hero.is-danger .tabs.is-boxed a:hover, .hero.is-danger .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-danger .tabs.is-boxed li.is-active a, .hero.is-danger .tabs.is-boxed li.is-active a:hover, .hero.is-danger .tabs.is-toggle li.is-active a, .hero.is-danger .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #ff3860; } + .hero.is-danger.is-bold { + background-image: linear-gradient(141deg, #ff0561 0%, #ff3860 71%, #ff5257 100%); } + @media screen and (max-width: 768px) { + .hero.is-danger.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #ff0561 0%, #ff3860 71%, #ff5257 100%); } } + .hero.is-small .hero-body { + padding-bottom: 1.5rem; + padding-top: 1.5rem; } + @media screen and (min-width: 769px), print { + .hero.is-medium .hero-body { + padding-bottom: 9rem; + padding-top: 9rem; } } + @media screen and (min-width: 769px), print { + .hero.is-large .hero-body { + padding-bottom: 18rem; + padding-top: 18rem; } } + .hero.is-halfheight .hero-body, .hero.is-fullheight .hero-body { + align-items: center; + display: flex; } + .hero.is-halfheight .hero-body > .container, .hero.is-fullheight .hero-body > .container { + flex-grow: 1; + flex-shrink: 1; } + .hero.is-halfheight { + min-height: 50vh; } + .hero.is-fullheight { + min-height: 100vh; } + +.hero-video { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + overflow: hidden; } + .hero-video video { + left: 50%; + min-height: 100%; + min-width: 100%; + position: absolute; + top: 50%; + transform: translate3d(-50%, -50%, 0); } + .hero-video.is-transparent { + opacity: 0.3; } + @media screen and (max-width: 768px) { + .hero-video { + display: none; } } + +.hero-buttons { + margin-top: 1.5rem; } + @media screen and (max-width: 768px) { + .hero-buttons .button { + display: flex; } + .hero-buttons .button:not(:last-child) { + margin-bottom: 0.75rem; } } + @media screen and (min-width: 769px), print { + .hero-buttons { + display: flex; + justify-content: center; } + .hero-buttons .button:not(:last-child) { + margin-right: 1.5rem; } } + +.hero-head, +.hero-foot { + flex-grow: 0; + flex-shrink: 0; } + +.hero-body { + flex-grow: 1; + flex-shrink: 0; + padding: 3rem 1.5rem; } + +.section { + padding: 3rem 1.5rem; } + @media screen and (min-width: 1024px) { + .section.is-medium { + padding: 9rem 1.5rem; } + .section.is-large { + padding: 18rem 1.5rem; } } + +.footer { + background-color: whitesmoke; + padding: 3rem 1.5rem 6rem; } + +#sidebar { + background-color: #eee; + border-right: 1px solid #c1c1c1; + box-shadow: 0 0 20px rgba(50, 50, 50, 0.2) inset; + padding: 1.75rem; } + #sidebar .brand { + padding: 1rem 0; + text-align: center; } + +#main { + padding: 3rem; } + +.example { + margin-bottom: 1em; } + .example .highlight { + margin: 0; } + .example .path { + font-style: italic; + width: 100%; + text-align: right; } + +code { + color: #1a9f1a; + font-size: 0.875em; + font-weight: normal; } + +.content h2 { + padding-top: 1em; + border-top: 1px solid #c0c0c0; } + +h1 .anchor, h2 .anchor, h3 .anchor, h4 .anchor, h5 .anchor, h6 .anchor { + display: inline-block; + width: 0; + margin-left: -1.5rem; + margin-right: 1.5rem; + transition: all 100ms ease-in-out; + opacity: 0; } + +h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { + opacity: 1; } + +h1:target, h2:target, h3:target, h4:target, h5:target, h6:target { + color: #C93312; } + h1:target .anchor, h2:target .anchor, h3:target .anchor, h4:target .anchor, h5:target .anchor, h6:target .anchor { + opacity: 1; + color: #C93312; } + +.footnotes p { + display: inline; } + +figure.has-border img { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); } diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 00000000..425913c3 Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/public/feature-editors/index.html b/docs/public/feature-editors/index.html new file mode 100644 index 00000000..5d4745ca --- /dev/null +++ b/docs/public/feature-editors/index.html @@ -0,0 +1,599 @@ + + + + + + + + + Custom Burp editors + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  Custom Burp Editors

+

Scalpel’s main killer feature is the ability to program your own editors using Python.

+

#  Table of content

+ +

#  Event hooks

+

#  1. Edit a request

+

E.g: A simple script to edit a fully URL encoded query string parameter in a request:

+
from pyscalpel import Request
+from pyscalpel.utils import urldecode, urlencode_all
+
+
+# Hook to initialize the editor's content
+def req_edit_in(req: Request) -> bytes | None:
+    param = req.query.get("filename")
+    if param is not None:
+        return urldecode(param)
+
+    # Do not modify the request
+    return None
+
+# Hook to update the request from the editor's modified content
+def req_edit_out(req: Request, modified_content: bytes) -> Request:
+    req.query["filename"] = urlencode_all(modified_content)
+    return req
+
    +
  • If you open a request with a filename query parameter, a Scalpel tab should appear in the editor like shown below:
    +
    +
  • +
  • Once your req_edit_in() Python hook is invoked, the tab should contain the filename parameter’s URL decoded content.
    +
    +
  • +
  • You can modify it to update the request and thus, include anything you want (e.g: path traversal sequences).
    +
    +
  • +
  • When you send the request or switch to another editor tab, your Python hook req_edit_out() will be invoked to update the parameter.
    +
    +
  • +
+

#  2. Edit a response

+

It is the same process for editing responses:

+
def res_edit_in(res: Response) -> bytes | None:
+    # Displays an additional header in the editor
+    res.headers["X-Python-In-Response-Editor"] = "res_edit_in"
+    return bytes(res)
+
+
+def res_edit_out(_: Response, text: bytes) -> Response | None:
+    # Recreate a response from the editor's content
+    res = Response.from_raw(text)
+    return res
+

+

#  3. Multiple tabs example

+

You can have multiple tabs open at the same time. Just suffix your function names:

+

E.g: Same script as above but for two parameters: “filename” and “directory”.

+
from pyscalpel import Request
+from pyscalpel.utils import urldecode, urlencode_all
+
+def req_edit_in_filename(req: Request):
+    param = req.query.get("filename")
+    if param is not None:
+        return urldecode(param)
+
+def req_edit_out_filename(req: Request, text: bytes):
+    req.query["filename"] = urlencode_all(text)
+    return req
+
+
+def req_edit_in_directory(req: Request):
+    param = req.query.get("directory")
+    if param is not None:
+        return urldecode(param)
+
+
+def req_edit_out_directory(req: Request, text: bytes):
+    req.query["directory"] = urlencode_all(text)
+    return req
+

This will result in two open tabs. One for the filename parameter and one for the directory parameter (see the second image below). +

+
+ +
+
+

+
+

#  Binary editors

+ + + +
+
+

+editors

+ +

To display the contents of your tab in a hexadecimal, binary, octal or decimal editor, +the user can apply the editor decorator to the req_edit_in / res_edit_in hook:

+
+ + + + + +
1"""
+2    To display the contents of your tab in a hexadecimal, binary, octal or decimal editor,
+3    the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_in` hook:
+4"""
+5from pyscalpel.edit import editor
+6
+7
+8__all__ = ["editor"]
+
+ + +
+
+ +
+ + def + editor(mode: Literal['raw', 'hex', 'octal', 'binary', 'decimal']): + + + +
+ +
12def editor(mode: EditorMode):
+13    """Decorator to specify the editor type for a given hook
+14
+15    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp
+16
+17    Example:
+18    ```py
+19        @editor("hex")
+20        def req_edit_in(req: Request) -> bytes | None:
+21            return bytes(req)
+22    ```
+23    This displays the request in an hex editor.
+24
+25    Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`.
+26
+27
+28    Args:
+29        mode (EDITOR_MODE): The editor mode (raw, hex,...)
+30    """
+31
+32    if mode not in EDITOR_MODES:
+33        raise ValueError(f"Argument must be one of {EDITOR_MODES}")
+34
+35    def decorator(hook: Callable):
+36        hook.__annotations__["scalpel_editor_mode"] = mode
+37        return hook
+38
+39    return decorator
+
+ + +

Decorator to specify the editor type for a given hook

+ +

This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp

+ +

Example:

+ +
+
    @editor("hex")
+    def req_edit_in(req: Request) -> bytes | None:
+        return bytes(req)
+
+
+ +

This displays the request in an hex editor.

+ +

Currently, the only modes supported are "raw", "hex", "octal", "binary" and "decimal".

+ +

Args: + mode (EDITOR_MODE): The editor mode (raw, hex,...)

+
+ + +
+
+ +

#  Example

+

E.g.: A simple script displaying requests in a hexadecimal editor and responses in a binary editor:

+
from pyscalpel import Request, Response, editor
+
+
+@editor("hex")
+def req_edit_in(req: Request) -> bytes | None:
+    return bytes(req)
+
+@editor("binary")
+def res_edit_in(res: Response) -> bytes | None:
+    return bytes(res)
+

The hexadecimal editor: +

+
+

+

The binary editor: +

+
+

+
+

#  Further reading

+

Learn more about the available hooks in the technical documentation’s Event Hooks & API section.

+ + +
+
+ + + diff --git a/docs/public/feature-http/index.html b/docs/public/feature-http/index.html new file mode 100644 index 00000000..69620aa8 --- /dev/null +++ b/docs/public/feature-http/index.html @@ -0,0 +1,250 @@ + + + + + + + + + Intercept and rewrite HTTP traffic + + + + + + + + + + + + +
+ +
+ + + + + Edit on GitHub + + + +

#  Event Hooks

+

Scalpel scripts hook into Burps’s internal mechanisms through event hooks.

+

These are implemented as methods with a set of well-known names. +Events receive Request, Response, Flow and bytes objects as arguments. By modifying these objects, scripts can +change traffic on the fly and program custom request/response editors.

+

For instance, here is an script that adds a response +header with the number of seen responses:

+
from pyscalpel import Response
+
+count = 0
+
+def response(res: Response) -> Response:
+    global count
+
+    count += 1
+    res.headers["count"] = count
+    return res
+

#  Intercept and Rewrite HTTP Traffic

+

#  Request / Response

+

To intercept requests/responses, implement the request() and response() functions in your script:

+

E.g: Hooks that add an arbitrary header to every request and response:

+
from pyscalpel import Request, Response
+
+# Intercept the request
+def request(req: Request) -> Request:
+    # Add an header
+    req.headers["X-Python-Intercept-Request"] = "request"
+    # Return the modified request
+    return req
+
+# Same for response
+def response(res: Response) -> Response:
+    res.headers["X-Python-Intercept-Response"] = "response"
+    return res
+

+

#  Match

+

Decide whether to intercept an HTTP message with the match() function:

+

E.g: A match intercepting requests to localhost and 127.0.0.1 only:

+
from pyscalpel import Flow
+
+# If match() returns true, request(), response(), req_edit_in(), [...] callbacks will be used.
+def match(flow: Flow) -> bool:
+    # True if host is localhost or 127.0.0.1
+    return flow.host_is("localhost", "127.0.0.1")
+

#  Further reading

+ + + +
+
+ + + diff --git a/docs/public/github.svg b/docs/public/github.svg new file mode 100644 index 00000000..a8d11740 --- /dev/null +++ b/docs/public/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/public/index.html b/docs/public/index.html new file mode 100644 index 00000000..593e8371 --- /dev/null +++ b/docs/public/index.html @@ -0,0 +1,225 @@ + + + + + + + + + Introduction + + + + + + + + + + + + + +
+ +
+ + + +

#  Introduction

+

Scalpel is a powerful Burp Suite extension that allows you to script Burp in order to intercept, rewrite HTTP traffic on the fly, and program custom Burp editors in Python 3.

+

It provides an interactive way to edit encoded/encrypted data as plaintext and offers an easy-to-use Python library as an alternative to Burp’s Java API.

+

#  Index

+ +

#  Features

+
    +
  • Python Library: Easy-to-use Python library, especially welcome for non-Java developers.
  • +
  • Intercept and Rewrite HTTP Traffic: Scalpel provides a set of predefined function names that can be implemented to intercept and modify HTTP requests and responses.
  • +
  • Custom Burp Editors: Program your own Burp editors in Python. Encoded/encrypted data can be handled as plaintext. +
      +
    • Hex Editors: Ability to create improved hex editors.
    • +
    +
  • +
+

#  Use cases

+ +
+

Note: One might think existing Burp extensions like Piper can handle such cases. But actually they can’t.
+For example, when intercepting a response, Piper cannot get information from the initiating request, which is required in the above use cases. Scalpel generally allows you to manage complex cases that are not handled by other Burp extensions like Piper or Hackvertor.

+
+ + +
+
+ + + diff --git a/docs/public/index.xml b/docs/public/index.xml new file mode 100644 index 00000000..b8a882f4 --- /dev/null +++ b/docs/public/index.xml @@ -0,0 +1,239 @@ + + + + Introduction on scalpel.org docs + / + Recent content in Introduction on scalpel.org docs + Hugo -- gohugo.io + en-us + + Debugging + /addons-debugging/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /addons-debugging/ + Debugging Scalpel scripts can be hard to debug, as you cannot run them outside of Burp. +Also it is difficult to know if a bug is related to Scalpel/Burp context or to the user&rsquo;s implementation. +Here are a few advices for debugging Scalpel errors. +Finding stacktraces Errors that occur in scripts can be found in different places: +1. The Output tab In the Scalpel tab, there is a sub-tab named Script Output, it shows all the standard output and error contents outputted by the current script 2. + + + + Custom Burp editors + /feature-editors/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /feature-editors/ + Custom Burp Editors Scalpel&rsquo;s main killer feature is the ability to program your own editors using Python. +Table of content Event hooks Edit a request Edit a response Multiple tabs example Binary editors Event hooks 1. Edit a request E.g: A simple script to edit a fully URL encoded query string parameter in a request: +from pyscalpel import Request from pyscalpel.utils import urldecode, urlencode_all # Hook to initialize the editor&#39;s content def req_edit_in(req: Request) -&gt; bytes | None: param = req. + + + + Decrypting custom encryption + /tute-aes/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /tute-aes/ + Decrypting custom encryption Context An IOT appliance adds an obfuscation layer to its HTTP communications by encrypting the body of its requests and responses with a key. +On every HTTP request, the program sends two POST parameters: +secret (the encryption key) encrypted (the ciphertext). Let&rsquo;s solve this problem by using Scalpel! +It will provide an additional tab in the Repeater which displays the plaintext for every request and response. The plaintext can also be edited. + + + + Event Hooks & API + /api/events.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/events.html + Available Hooks The following list all available event hooks. +The full Python documentation is available here +events View Source 1from pyscalpel import Request, Response, Flow, MatchEvent 2 3 4def match(flow: Flow, events: MatchEvent) -&gt; bool: 5 &quot;&quot;&quot;- Determine whether an event should be handled by a hook. 6 7 - Args: 8 - flow ([Flow](../pdoc/python3-10/pyscalpel.html#Flow)): The event context (contains request and optional response). 9 - events ([MatchEvent](../pdoc/python3-10/pyscalpel.html#MatchEvent)): The hook type (request, response, req_edit_in, . + + + + Examples + /addons-examples/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /addons-examples/ + Script examples This page provides example scripts to get familiar with Scalpel&rsquo;s Python library. They are designed for real use cases. +Table of content GZIP-ed API Cryptography using a session as a secret GZIP-ed API Let&rsquo;s assume you encountered an API using a custom protocol that gzips multiple form-data fields. +Quick-and-dirty Scalpel script to directly edit the unzipped data and find hidden secrets: +from pyscalpel import Request, Response, logger import gzip def unzip_bytes(data): try: # Create a GzipFile object with the input data with gzip. + + + + FAQ + /overview-faq/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /overview-faq/ + FAQ Table of Contents Why does Scalpel depend on JDK whereas Burp comes with its own JRE? Why using Java with Jep to execute Python whereas Burp already supports Python extensions with Jython? Once the .jar is loaded, no additional request shows up in the editor My distribution/OS comes with an outdated python. Configuring my editor for Python I installed Python using the Microsoft Store and Scalpel doesn&rsquo;t work. Why does Scalpel depend on JDK whereas Burp comes with its own JRE? + + + + First steps + /tute-first-steps/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /tute-first-steps/ + First Steps with Scalpel Introduction Welcome to your first steps with Scalpel! This beginner-friendly tutorial will walk you through basic steps to automatically and interactively modify HTTP headers using Scalpel. By the end of this tutorial, you’ll be able to edit the content of the User-Agent and Accept-Language headers using Scalpel’s hooks and custom editors. +Table of content Setting up Scalpel Inspecting a GET request Create a new script Manipulating headers Creating custom editors Conclusion 1. + + + + How scalpel works + /concepts-howscalpelworks/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /concepts-howscalpelworks/ + How Scalpel works Table of content Dependencies Behavior Python scripting Diagram Dependencies Scalpel&rsquo;s Python library is embedded in a JAR file and is unzipped when Burp loads the extension. Scalpel requires external dependencies and will install them using pip when needed. Scalpel will always use a virtual environment for every action. Hence, it will never modify the user&rsquo;s global Python installation. Scalpel relies on Jep to communicate with Python. It requires to have a JDK installed on your machine. + + + + Installation + /overview-installation/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /overview-installation/ + Installation Requirements OpenJDK &gt;= 17 Python &gt;= 3.8 pip python-virtualenv Debian-based distributions The following packages are required: +sudo apt install build-essential python3 python3-dev python3-venv openjdk-17-jdk Fedora / RHEL / CentOS The following packages are required: +sudo dnf install @development-tools python3 python3-devel python3-virtualenv java-17-openjdk-devel Arch-based distributions The following packages are required: +sudo pacman -S base-devel python python-pip python-virtualenv jdk-openjdk Windows Microsoft Visual C++ &gt;=14.0 is required: https://visualstudio.microsoft.com/visual-cpp-build-tools/ +Step-by-step instructions Download the latest JAR release. + + + + Intercept and rewrite HTTP traffic + /feature-http/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /feature-http/ + Event Hooks Scalpel scripts hook into Burps&rsquo;s internal mechanisms through event hooks. +These are implemented as methods with a set of well-known names. Events receive Request, Response, Flow and bytes objects as arguments. By modifying these objects, scripts can change traffic on the fly and program custom request/response editors. +For instance, here is an script that adds a response header with the number of seen responses: +from pyscalpel import Response count = 0 def response(res: Response) -&gt; Response: global count count += 1 res. + + + + pyscalpel.edit + /api/pyscalpel/edit.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/edit.html + pyscalpel.edit Scalpel allows choosing between normal and binary editors, to do so, the user can apply the editor decorator to the req_edit_in / res_edit_int hook: +View Source 1&quot;&quot;&quot; 2 Scalpel allows choosing between normal and binary editors, 3 to do so, the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_int` hook: 4&quot;&quot;&quot; 5from typing import Callable, Literal, get_args 6 7EditorMode = Literal[&quot;raw&quot;, &quot;hex&quot;, &quot;octal&quot;, &quot;binary&quot;, &quot;decimal&quot;] 8EDITOR_MODES: set[EditorMode] = set(get_args(EditorMode)) 9 10 11def editor(mode: EditorMode): 12 &quot;&quot;&quot;Decorator to specify the editor type for a given hook 13 14 This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp 15 16 Example: 17 ```py 18 @editor(&quot;hex&quot;) 19 def req_edit_in(req: Request) -&gt; bytes | None: 20 return bytes(req) 21 ``` 22 This displays the request in an hex editor. + + + + pyscalpel.encoding + /api/pyscalpel/encoding.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/encoding.html + pyscalpel.encoding Utilities for encoding data. +View Source 1&quot;&quot;&quot; 2 Utilities for encoding data. 3&quot;&quot;&quot; 4 5from urllib.parse import unquote_to_bytes as urllibdecode 6from _internal_mitmproxy.utils import strutils 7 8 9# str/bytes conversion helpers from mitmproxy/http.py: 10# https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/http.py#:~:text=def-,_native,-(x%3A 11def always_bytes(data: str | bytes | int, encoding=&quot;latin-1&quot;) -&gt; bytes: 12 &quot;&quot;&quot;Convert data to bytes 13 14 Args: 15 data (str | bytes | int): The data to convert 16 17 Returns: 18 bytes: The converted bytes 19 &quot;&quot;&quot; 20 if isinstance(data, int): 21 data = str(data) 22 return strutils. + + + + pyscalpel.events + /api/pyscalpel/events.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/events.html + pyscalpel.events Events that can be passed to the match() hook +View Source 1&quot;&quot;&quot;Events that can be passed to the match() hook&quot;&quot;&quot; 2 3from typing import Literal, get_args 4 5MatchEvent = Literal[ 6 &quot;request&quot;, 7 &quot;response&quot;, 8 &quot;req_edit_in&quot;, 9 &quot;req_edit_out&quot;, 10 &quot;res_edit_in&quot;, 11 &quot;res_edit_out&quot;, 12] 13 14 15MATCH_EVENTS: set[MatchEvent] = set(get_args(MatchEvent)) MatchEvent = typing.Literal[&#39;request&#39;, &#39;response&#39;, &#39;req_edit_in&#39;, &#39;req_edit_out&#39;, &#39;res_edit_in&#39;, &#39;res_edit_out&#39;] MATCH_EVENTS: set[typing.Literal[&#39;request&#39;, &#39;response&#39;, &#39;req_edit_in&#39;, &#39;req_edit_out&#39;, &#39;res_edit_in&#39;, &#39;res_edit_out&#39;]] = {&#39;request&#39;, &#39;res_edit_out&#39;, &#39;req_edit_out&#39;, &#39;response&#39;, &#39;req_edit_in&#39;, &#39;res_edit_in&#39;} + + + + pyscalpel.http + /api/pyscalpel/http.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/http.html + pyscalpel.http This module contains objects representing HTTP objects passed to the user's hooks +View Source 1&quot;&quot;&quot; 2 This module contains objects representing HTTP objects passed to the user&#39;s hooks 3&quot;&quot;&quot; 4 5from .request import Request, Headers 6from .response import Response 7from .flow import Flow 8from .utils import match_patterns, host_is 9from . import body 10 11__all__ = [ 12 &quot;body&quot;, # &lt;- pdoc shows a warning for this declaration but won&#39;t display it when absent 13 &quot;Request&quot;, 14 &quot;Response&quot;, 15 &quot;Headers&quot;, 16 &quot;Flow&quot;, 17 &quot;host_is&quot;, 18 &quot;match_patterns&quot;, 19] class Request: View Source 70class Request: 71 &quot;&quot;&quot;A &quot;Burp oriented&quot; HTTP request class 72 73 74 This class allows to manipulate Burp requests in a Pythonic way. + + + + pyscalpel.http.body + /api/pyscalpel/http/body.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/http/body.html + pyscalpel.http.body Pentesters often have to manipulate form data in precise and extensive ways +This module contains implementations for the most common forms (multipart,urlencoded, JSON) +Users may be implement their own form by creating a Serializer, assigning the .serializer attribute in Request and using the "form" property +Forms are designed to be convertible from one to another. +For example, JSON forms may be converted to URL encoded forms by using the php query string syntax: + + + + pyscalpel.java + /api/pyscalpel/java.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/java.html + pyscalpel.java This module declares type definitions used for Java objects. +If you are a normal user, you should probably never have to manipulate these objects yourself. +View Source 1&quot;&quot;&quot; 2 This module declares type definitions used for Java objects. 3 4 If you are a normal user, you should probably never have to manipulate these objects yourself. 5&quot;&quot;&quot; 6from .bytes import JavaBytes 7from .import_java import import_java 8from .object import JavaClass, JavaObject 9from . + + + + pyscalpel.java.burp + /api/pyscalpel/java/burp.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/java/burp.html + pyscalpel.java.burp This module exposes Java objects from Burp's extensions API +If you are a normal user, you should probably never have to manipulate these objects yourself. +View Source 1&quot;&quot;&quot; 2 This module exposes Java objects from Burp&#39;s extensions API 3 4 If you are a normal user, you should probably never have to manipulate these objects yourself. 5&quot;&quot;&quot; 6from .byte_array import IByteArray, ByteArray 7from .http_header import IHttpHeader, HttpHeader 8from . + + + + pyscalpel.utils + /api/pyscalpel/utils.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/utils.html + pyscalpel.utils View Source 1import inspect 2from typing import TypeVar, Union 3from pyscalpel.burp_utils import ( 4 urldecode, 5 urlencode_all, 6) 7 8 9T = TypeVar(&quot;T&quot;, str, bytes) 10 11 12def removeprefix(s: T, prefix: Union[str, bytes]) -&gt; T: 13 if isinstance(s, str) and isinstance(prefix, str): 14 if s.startswith(prefix): 15 return s[len(prefix) :] # type: ignore 16 elif isinstance(s, bytes) and isinstance(prefix, bytes): 17 if s.startswith(prefix): 18 return s[len(prefix) :] # type: ignore 19 return s 20 21 22def removesuffix(s: T, suffix: Union[str, bytes]) -&gt; T: 23 if isinstance(s, str) and isinstance(suffix, str): 24 if s. + + + + pyscalpel.venv + /api/pyscalpel/venv.html + Mon, 01 Jan 0001 00:00:00 +0000 + + /api/pyscalpel/venv.html + pyscalpel.venv This module provides reimplementations of Python virtual environnements scripts +This is designed to be used internally, but in the case where the user desires to dynamically switch venvs using this, they should ensure the selected venv has the dependencies required by Scalpel. +View Source 1&quot;&quot;&quot; 2This module provides reimplementations of Python virtual environnements scripts 3 4This is designed to be used internally, 5but in the case where the user desires to dynamically switch venvs using this, 6they should ensure the selected venv has the dependencies required by Scalpel. + + + + Usage + /overview-usage/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /overview-usage/ + Usage Scalpel allows you to programmatically intercept and modify HTTP requests/responses going through Burp, as well as creating custom request/response editors with Python. +To do so, Scalpel provides a Burp extension GUI for scripting and a set of predefined function names corresponding to specific actions: +match: Determine whether an event should be handled by a hook. request: Intercept and rewrite a request. response: Intercept and rewrite a response. req_edit_in: Create or update a request editor&rsquo;s content from a request. + + + + Using the Burp API + /addons-java/ + Mon, 01 Jan 0001 00:00:00 +0000 + + /addons-java/ + Using the Burp API Scalpel communicates with Burp through its Java API. Then, it provides the user with an execution context in which they should only use Python objects. +However, since Scalpel focuses on HTTP objects, it does not provide utilities for all the Burp API features (like the ability to generate Collaborator payloads). +If the user must deal with unhandled cases, they can directly access the MontoyaApi Java object to search for appropriate objects. + + + + diff --git a/docs/public/javadoc/allclasses-index.html b/docs/public/javadoc/allclasses-index.html new file mode 100644 index 00000000..0840e176 --- /dev/null +++ b/docs/public/javadoc/allclasses-index.html @@ -0,0 +1,243 @@ + + + + +All Classes and Interfaces (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

All Classes and Interfaces

+
+
+
+
+
+
Class
+
Description
+ +
+
Base class for implementing Scalpel editors + It handles all the Python stuff and only leaves the content setter/getter, modification checker and selection parts abstract + That way, if you wish to implement you own editor, you only have to add logic specific to it (get/set, selected data, has content been modified by user ?)
+
+ +
 
+ +
+
Provides utilities to get default commands.
+
+ +
+
Scalpel configuration.
+
+ +
+
Global configuration.
+
+ +
 
+ +
+
Burp tab handling Scalpel configuration + IntelliJ's GUI designer is needed to edit most components.
+
+ +
+
Contains constants used by the extension.
+
+ +
 
+ +
+
Enum used by editors to identify themselves
+
+ +
 
+ +
 
+ +
+
Interface declaring all the necessary methods to implement a Scalpel editor + If you wish to implement your own type of editor, you should use the AbstractEditor class as a base.
+
+ +
+
Utilities to perform IO utilities conveniently
+
+ +
 
+ +
 
+ +
+
Color palette for the embedded terminal + Contains colors for both light and dark theme
+
+ +
 
+ +
+
Utilities to initialize Java Embedded Python (jep)
+
+ +
+
Utility class for Python scripts.
+
+ +
+
Provides methods for unpacking the Scalpel resources.
+
+
Result<T,E extends Throwable>
+
+
Optional-style class for handling python task results + + A completed python task can have multiple outcomes: + - The task completes successfully and returns a value + - The task completes successfully but returns no value + - The task throws an exception + + Result allows us to handle returned values and errors uniformly to handle them when needed.
+
+ +
+
The main class of the extension.
+
+ +
 
+ +
 
+ +
+
Provides a new ScalpelProvidedEditor object for editing HTTP requests or responses.
+
+ +
+
Provides an UI text editor component for editing HTTP requests or responses.
+
+ +
+
This stores all the informations required to create a tab.
+
+ +
+
A tab can be associated with at most two hooks + (e.g req_edit_in and req_edit_out) + + This stores the informations related to only one hook and is later merged with the second hook information into a HookTabInfo
+
+ +
+
Responds to requested Python tasks from multiple threads through a task queue handled in a single sepearate thread.
+
+ +
 
+ +
+
Hexadecimal editor implementation for a Scalpel editor + Users can press their keyboard's INSER key to enter insertion mode + (which is impossible in Burp's native hex editor)
+
+ +
 
+ +
+
Handles HTTP requests and responses.
+
+ +
+
Provides methods for logging messages to the Burp Suite output and standard streams.
+
+ +
+
Log levels used to filtrate logs by weight + Useful for debugging.
+
+ +
 
+ +
+
Provides an UI text editor component for editing HTTP requests or responses.
+
+ +
 
+ +
 
+ +
+
Provides methods for constructing the Burp Suite UI.
+
+ +
 
+ +
 
+ +
+
Manage Python virtual environments.
+
+ +
 
+ +
 
+ +
 
+ +
+
Provides a blocking wait dialog GUI popup.
+
+ +
+
A workspace is a folder containing a venv and the associated scripts.
+
+
+
+
+
+
+
+ + diff --git a/docs/public/javadoc/allpackages-index.html b/docs/public/javadoc/allpackages-index.html new file mode 100644 index 00000000..8321674f --- /dev/null +++ b/docs/public/javadoc/allpackages-index.html @@ -0,0 +1,67 @@ + + + + +All Packages (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

All Packages

+
+
Package Summary
+
+
Package
+
Description
+ +
 
+ +
 
+ +
 
+
+
+
+
+ + diff --git a/docs/public/javadoc/constant-values.html b/docs/public/javadoc/constant-values.html new file mode 100644 index 00000000..cc598296 --- /dev/null +++ b/docs/public/javadoc/constant-values.html @@ -0,0 +1,213 @@ + + + + +Constant Field Values (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Constant Field Values

+
+

Contents

+ +
+
+
+

lexfo.scalpel.*

+ +
+
+
+
+ + diff --git a/docs/public/javadoc/element-list b/docs/public/javadoc/element-list new file mode 100644 index 00000000..5f3c57d1 --- /dev/null +++ b/docs/public/javadoc/element-list @@ -0,0 +1,3 @@ +lexfo.scalpel +lexfo.scalpel.components +lexfo.scalpel.editors diff --git a/docs/public/javadoc/help-doc.html b/docs/public/javadoc/help-doc.html new file mode 100644 index 00000000..f3bc1b5c --- /dev/null +++ b/docs/public/javadoc/help-doc.html @@ -0,0 +1,185 @@ + + + + +API Help (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+

JavaDoc Help

+ +
+
+

Navigation

+Starting from the Overview page, you can browse the documentation using the links in each page, and in the navigation bar at the top of each page. The Index and Search box allow you to navigate to specific declarations and summary pages, including: All Packages, All Classes and Interfaces + +
+
+
+

Kinds of Pages

+The following sections describe the different kinds of pages in this collection. +
+

Overview

+

The Overview page is the front page of this API document and provides a list of all packages with a summary for each. This page can also contain an overall description of the set of packages.

+
+
+

Package

+

Each package has a page that contains a list of its classes and interfaces, with a summary for each. These pages may contain the following categories:

+
    +
  • Interfaces
  • +
  • Classes
  • +
  • Enum Classes
  • +
  • Exceptions
  • +
  • Errors
  • +
  • Annotation Interfaces
  • +
+
+
+

Class or Interface

+

Each class, interface, nested class and nested interface has its own separate page. Each of these pages has three sections consisting of a declaration and description, member summary tables, and detailed member descriptions. Entries in each of these sections are omitted if they are empty or not applicable.

+
    +
  • Class Inheritance Diagram
  • +
  • Direct Subclasses
  • +
  • All Known Subinterfaces
  • +
  • All Known Implementing Classes
  • +
  • Class or Interface Declaration
  • +
  • Class or Interface Description
  • +
+
+
    +
  • Nested Class Summary
  • +
  • Enum Constant Summary
  • +
  • Field Summary
  • +
  • Property Summary
  • +
  • Constructor Summary
  • +
  • Method Summary
  • +
  • Required Element Summary
  • +
  • Optional Element Summary
  • +
+
+
    +
  • Enum Constant Details
  • +
  • Field Details
  • +
  • Property Details
  • +
  • Constructor Details
  • +
  • Method Details
  • +
  • Element Details
  • +
+

Note: Annotation interfaces have required and optional elements, but not methods. Only enum classes have enum constants. The components of a record class are displayed as part of the declaration of the record class. Properties are a feature of JavaFX.

+

The summary entries are alphabetical, while the detailed descriptions are in the order they appear in the source code. This preserves the logical groupings established by the programmer.

+
+
+

Other Files

+

Packages and modules may contain pages with additional information related to the declarations nearby.

+
+
+

Tree (Class Hierarchy)

+

There is a Class Hierarchy page for all packages, plus a hierarchy for each package. Each hierarchy page contains a list of classes and a list of interfaces. Classes are organized by inheritance structure starting with java.lang.Object. Interfaces do not inherit from java.lang.Object.

+
    +
  • When viewing the Overview page, clicking on TREE displays the hierarchy for all packages.
  • +
  • When viewing a particular package, class or interface page, clicking on TREE displays the hierarchy for only that package.
  • +
+
+
+

Constant Field Values

+

The Constant Field Values page lists the static final fields and their values.

+
+
+

Serialized Form

+

Each serializable or externalizable class has a description of its serialization fields and methods. This information is of interest to those who implement rather than use the API. While there is no link in the navigation bar, you can get to this information by going to any serialized class and clicking "Serialized Form" in the "See Also" section of the class description.

+
+
+

All Packages

+

The All Packages page contains an alphabetic index of all packages contained in the documentation.

+
+
+

All Classes and Interfaces

+

The All Classes and Interfaces page contains an alphabetic index of all classes and interfaces contained in the documentation, including annotation interfaces, enum classes, and record classes.

+
+
+

Index

+

The Index contains an alphabetic index of all classes, interfaces, constructors, methods, and fields in the documentation, as well as summary pages such as All Packages, All Classes and Interfaces.

+
+
+
+This help file applies to API documentation generated by the standard doclet.
+
+
+ + diff --git a/docs/public/javadoc/index-all.html b/docs/public/javadoc/index-all.html new file mode 100644 index 00000000..89b972ef --- /dev/null +++ b/docs/public/javadoc/index-all.html @@ -0,0 +1,2068 @@ + + + + +Index (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Index

+
+$ A B C D E F G H I J K L M N O P R S T U V W _ 
All Classes and Interfaces|All Packages|Constant Field Values|Serialized Form +

$

+
+
$$$getRootComponent$$$() - Method in class lexfo.scalpel.ConfigTab
+
 
+
$$$setupUI$$$() - Method in class lexfo.scalpel.ConfigTab
+
+
Method generated by IntelliJ IDEA GUI Designer + >>> IMPORTANT!! <<< + DO NOT edit this method OR call it in your code!
+
+
+

A

+
+
AbstractEditor - Class in lexfo.scalpel.editors
+
+
Base class for implementing Scalpel editors + It handles all the Python stuff and only leaves the content setter/getter, modification checker and selection parts abstract + That way, if you wish to implement you own editor, you only have to add logic specific to it (get/set, selected data, has content been modified by user ?)
+
+
AbstractEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor) - Constructor for class lexfo.scalpel.editors.AbstractEditor
+
+
Constructs a new Scalpel editor.
+
+
addCheckboxSetting(String, String, boolean) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
addDropdownSetting(String, String, String[], String) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
addEditorToDisplayedTabs(IMessageEditor) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Adds the editor to the tabbed pane + If the editor caption is blank, the displayed name will be the tab index.
+
+
addError(String) - Method in class lexfo.scalpel.components.ErrorPopup
+
 
+
addInformationText(String) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
addListDoubleClickListener(JList<T>, Consumer<ListSelectionEvent>) - Method in class lexfo.scalpel.ConfigTab
+
+
JList doesn't natively support double click events, so we implment it + ourselves.
+
+
addListener(Consumer<Map<String, String>>) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
addSettingComponent(String, String, JComponent) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
addTask(String, Object[], Map<String, Object>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Adds a new task to the queue of tasks to be executed by the script.
+
+
addTask(String, Object[], Map<String, Object>, boolean) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Adds a new task to the queue of tasks to be executed by the script.
+
+
addTextFieldSetting(String, String, String) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
addVentText - Variable in class lexfo.scalpel.ConfigTab
+
 
+
addVenvButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
addVenvPath(Path) - Method in class lexfo.scalpel.Config
+
 
+
adjustTabBarVisibility() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
all(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
 
+
ALL - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
annotations - Variable in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
The field for the annotations record component.
+
+
annotations() - Method in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
Returns the value of the annotations record component.
+
+
API - Variable in class lexfo.scalpel.ConfigTab
+
 
+
API - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The Montoya API object.
+
+
API - Variable in class lexfo.scalpel.Scalpel
+
+
The MontoyaApi object used to interact with Burp Suite.
+
+
API - Variable in class lexfo.scalpel.ScalpelEditorProvider
+
+
The MontoyaApi object used to interact with Burp Suite.
+
+
API - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The Montoya API object.
+
+
API - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The MontoyaApi object to use for sending and receiving HTTP messages.
+
+
API - Variable in class lexfo.scalpel.ScalpelHttpRequestHandler
+
 
+
appendToDebugInfo(String) - Static method in class lexfo.scalpel.ConfigTab
+
 
+
args - Variable in class lexfo.scalpel.ScalpelExecutor.Task
+
+
The arguments passed to the task.
+
+
Async - Class in lexfo.scalpel
+
 
+
Async() - Constructor for class lexfo.scalpel.Async
+
 
+
await() - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
+
Add the task to the queue and wait for it to be completed by the task thread.
+
+
awaitTask(String, Object[], Map<String, Object>, Class<T>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Awaits the result of a task.
+
+
+

B

+
+
base - Variable in class lexfo.scalpel.ScalpelExecutor.CustomEnquirer
+
+
The base ClassEnquirer to use.
+
+
BASH_INIT_FILE_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
binaryDataToByteArray(BinaryData) - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
Convert from BinEd format to Burp format
+
+
browsePanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
burpFrame - Variable in class lexfo.scalpel.ConfigTab
+
 
+
byteArrayToBinaryData(ByteArray) - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
Convert from Burp format to BinEd format
+
+
+

C

+
+
call() - Method in interface lexfo.scalpel.IO.IOSupplier
+
 
+
CallableData(String, HashMap<String, String>) - Constructor for record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
Creates an instance of a CallableData record class.
+
+
callEditorHook(HttpMessage, HttpService, ByteArray, Boolean, String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHook(HttpMessage, HttpService, Boolean, String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHook(Object[], Boolean, Boolean, String, Class<T>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHook(Object, HttpService, Boolean, Boolean, String, Class<T>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHookInRequest(HttpRequest, HttpService, String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHookInResponse(HttpResponse, HttpRequest, HttpService, String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHookOutRequest(HttpRequest, HttpService, ByteArray, String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callEditorHookOutResponse(HttpResponse, HttpRequest, HttpService, ByteArray, String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given tab.
+
+
callIntercepterHook(T, HttpService) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the corresponding Python callback for the given message intercepted by Proxy.
+
+
caption() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the name of the tab.
+
+
caption() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the name of the tab.
+
+
changeListeners - Variable in class lexfo.scalpel.components.SettingsPanel
+
 
+
clearOutputs(String) - Static method in class lexfo.scalpel.ConfigTab
+
 
+
clearPipCache(Path) - Static method in class lexfo.scalpel.Venv
+
 
+
cmdFormat(String, Object, Object) - Method in class lexfo.scalpel.ConfigTab
+
 
+
CommandChecker - Class in lexfo.scalpel
+
+
Provides utilities to get default commands.
+
+
CommandChecker() - Constructor for class lexfo.scalpel.CommandChecker
+
 
+
config - Variable in class lexfo.scalpel.ConfigTab
+
 
+
config - Variable in class lexfo.scalpel.Scalpel
+
 
+
config - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
Config - Class in lexfo.scalpel
+
+
Scalpel configuration.
+
+
Config(MontoyaApi) - Constructor for class lexfo.scalpel.Config
+
 
+
CONFIG_EXT - Static variable in class lexfo.scalpel.Config
+
 
+
Config._GlobalData - Class in lexfo.scalpel
+
+
Global configuration.
+
+
Config._ProjectData - Class in lexfo.scalpel
+
 
+
ConfigTab - Class in lexfo.scalpel
+
+
Burp tab handling Scalpel configuration + IntelliJ's GUI designer is needed to edit most components.
+
+
ConfigTab(MontoyaApi, ScalpelExecutor, Config, Theme) - Constructor for class lexfo.scalpel.ConfigTab
+
 
+
Constants - Class in lexfo.scalpel
+
+
Contains constants used by the extension.
+
+
Constants() - Constructor for class lexfo.scalpel.Constants
+
 
+
constructConfigTab(MontoyaApi, ScalpelExecutor, Config, Theme) - Static method in class lexfo.scalpel.UIBuilder
+
+
Constructs the configuration Burp tab.
+
+
constructScalpelInterpreterTab(Config, ScalpelExecutor) - Static method in class lexfo.scalpel.UIBuilder
+
+
Constructs the debug Python testing Burp tab.
+
+
contains(Charset) - Method in class lexfo.scalpel.editors.DisplayableWhiteSpaceCharset
+
 
+
copyScriptToWorkspace(Path, Path) - Static method in class lexfo.scalpel.Workspace
+
+
Copy the script to the selected workspace
+
+
copyToClipboardButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
copyWorkspaceFiles(Path) - Static method in class lexfo.scalpel.Workspace
+
 
+
create(Path) - Static method in class lexfo.scalpel.Venv
+
+
Create a virtual environment.
+
+
createAndInitWorkspace(Path, Optional<Path>, Optional<Terminal>) - Static method in class lexfo.scalpel.Workspace
+
 
+
createAndInstallDefaults(Path) - Static method in class lexfo.scalpel.Venv
+
+
Create a virtual environment and install the default packages.
+
+
createButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
createExceptionFromProcess(Process, String, String) - Static method in class lexfo.scalpel.Workspace
+
 
+
createSettingsProvider(Theme) - Static method in class lexfo.scalpel.Terminal
+
 
+
createTerminal(Theme, String) - Static method in class lexfo.scalpel.Terminal
+
+
Creates a JediTermWidget that will run a shell in the virtualenv.
+
+
createTerminal(Theme, String, String, String) - Static method in class lexfo.scalpel.Terminal
+
+
Creates a JediTermWidget that will run a shell in the virtualenv.
+
+
createTerminalWidget(Theme, String, Optional<String>, Optional<String>) - Static method in class lexfo.scalpel.Terminal
+
 
+
createTtyConnector(String) - Static method in class lexfo.scalpel.Terminal
+
+
Creates a TtyConnector that will run a shell in the virtualenv.
+
+
createTtyConnector(String, Optional<Dimension>, Optional<String>, Optional<String>) - Static method in class lexfo.scalpel.Terminal
+
+
Creates a TtyConnector that will run a shell in the virtualenv.
+
+
createUIComponents() - Method in class lexfo.scalpel.ConfigTab
+
 
+
ctx - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The editor creation context.
+
+
ctx - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The editor creation context.
+
+
CustomEnquirer() - Constructor for class lexfo.scalpel.ScalpelExecutor.CustomEnquirer
+
+
Constructs a new CustomEnquirer object.
+
+
+

D

+
+
DARK_COLORS - Static variable in class lexfo.scalpel.Palette
+
 
+
DARK_PALETTE - Static variable in class lexfo.scalpel.Palette
+
 
+
DATA_DIR_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
DATA_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
DATA_PREFIX - Static variable in class lexfo.scalpel.Config
+
 
+
DATA_PROJECT_ID_KEY - Static variable in class lexfo.scalpel.Config
+
 
+
debug(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output at the DEBUG level.
+
+
DEBUG - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
debugInfoTextPane - Variable in class lexfo.scalpel.ConfigTab
+
 
+
decodeLoop(ByteBuffer, CharBuffer) - Method in class lexfo.scalpel.editors.WhitspaceCharsetDecoder
+
 
+
DEFAULT_EDITOR_MODE - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_LINUX_OPEN_DIR_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_LINUX_OPEN_FILE_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_LINUX_TERM_EDIT_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_OPEN_DIR_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_OPEN_FILE_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_SCRIPT_FILENAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
DEFAULT_SCRIPT_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
DEFAULT_TERM_EDIT_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_TERMINAL_EDITOR - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_UNIX_SHELL - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_VENV_DEPENDENCIES - Static variable in class lexfo.scalpel.Constants
+
+
Required python packages
+
+
DEFAULT_VENV_NAME - Static variable in class lexfo.scalpel.Workspace
+
 
+
DEFAULT_WINDOWS_EDITOR - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_WINDOWS_OPEN_DIR_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_WINDOWS_OPEN_FILE_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
DEFAULT_WINDOWS_TERM_EDIT_CMD - Static variable in class lexfo.scalpel.Constants
+
 
+
defaultScriptPath - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
defaultWorkspacePath - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
delete(Path) - Static method in class lexfo.scalpel.Venv
+
+
Delete a virtual environment.
+
+
direction - Variable in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
The field for the direction record component.
+
+
direction() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Returns the value of the direction record component.
+
+
directions - Variable in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
The field for the directions record component.
+
+
directions() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Returns the value of the directions record component.
+
+
disable() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
DisplayableWhiteSpaceCharset - Class in lexfo.scalpel.editors
+
 
+
DisplayableWhiteSpaceCharset() - Constructor for class lexfo.scalpel.editors.DisplayableWhiteSpaceCharset
+
 
+
displayErrors() - Method in class lexfo.scalpel.components.ErrorPopup
+
 
+
displayProxyErrorPopup - Variable in class lexfo.scalpel.Config._ProjectData
+
 
+
dumpConfig() - Method in class lexfo.scalpel.Config
+
 
+
dumps(Object) - Static method in class lexfo.scalpel.Terminal
+
 
+
+

E

+
+
editor - Variable in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
 
+
editor - Variable in class lexfo.scalpel.editors.ScalpelRawEditor
+
 
+
EDITOR_MODE_ANNOTATION_KEY - Static variable in class lexfo.scalpel.Constants
+
 
+
editorProvider - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
editors - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
editorsRefs - Variable in class lexfo.scalpel.ScalpelEditorProvider
+
 
+
EditorType - Enum Class in lexfo.scalpel
+
+
Enum used by editors to identify themselves
+
+
EditorType() - Constructor for enum class lexfo.scalpel.EditorType
+
 
+
editScriptCommand - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
empty() - Static method in class lexfo.scalpel.Result
+
 
+
enable() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
encodeLoop(CharBuffer, ByteBuffer) - Method in class lexfo.scalpel.editors.WhitspaceCharsetEncoder
+
 
+
equals(Object) - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Indicates whether some other object is "equal to" this one.
+
+
equals(Object) - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Indicates whether some other object is "equal to" this one.
+
+
equals(Object) - Method in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
Indicates whether some other object is "equal to" this one.
+
+
error - Variable in class lexfo.scalpel.Result
+
 
+
error(E) - Static method in class lexfo.scalpel.Result
+
 
+
error(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite error output and standard error.
+
+
ERROR - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
errorArea - Variable in class lexfo.scalpel.components.ErrorPopup
+
 
+
ErrorDialog - Class in lexfo.scalpel.components
+
 
+
ErrorDialog() - Constructor for class lexfo.scalpel.components.ErrorDialog
+
 
+
errorMessages - Static variable in class lexfo.scalpel.components.ErrorPopup
+
 
+
errorPane - Variable in class lexfo.scalpel.editors.AbstractEditor
+
 
+
ErrorPopup - Class in lexfo.scalpel.components
+
 
+
ErrorPopup(MontoyaApi) - Constructor for class lexfo.scalpel.components.ErrorPopup
+
 
+
escapeshellarg(String) - Static method in class lexfo.scalpel.Terminal
+
 
+
evalAndCaptureOutput(String) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Evaluates the given script and returns the output.
+
+
exceptionToErrorMsg(Throwable, String) - Static method in class lexfo.scalpel.ScalpelLogger
+
 
+
executeHook(HttpRequestResponse) - Method in class lexfo.scalpel.editors.AbstractEditor
+
 
+
executeHook(HttpRequestResponse) - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
executePythonCommand(String) - Static method in class lexfo.scalpel.PythonSetup
+
 
+
executor - Static variable in class lexfo.scalpel.Async
+
 
+
executor - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The executor responsible for interacting with Python.
+
+
executor - Variable in class lexfo.scalpel.Scalpel
+
+
The ScalpelExecutor object used to execute Python scripts.
+
+
executor - Variable in class lexfo.scalpel.ScalpelEditorProvider
+
+
The ScalpelExecutor object used to execute Python scripts.
+
+
executor - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The executor responsible for interacting with Python.
+
+
executor - Variable in class lexfo.scalpel.ScalpelHttpRequestHandler
+
+
The ScalpelExecutor object used to execute Python scripts.
+
+
extractBinary(String) - Static method in class lexfo.scalpel.CommandChecker
+
 
+
extractRessources(String, String, Set<String>) - Static method in class lexfo.scalpel.RessourcesUnpacker
+
+
Extracts the Scalpel python resources from the Scalpel JAR file.
+
+
extractRessourcesToHome() - Static method in class lexfo.scalpel.RessourcesUnpacker
+
+
Initializes the Scalpel resources directory.
+
+
+

F

+
+
fatal(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output at the FATAL level.
+
+
FATAL - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
filterEditorHooks(List<ScalpelExecutor.CallableData>) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Retain hooks for editing a request / response and parses them.
+
+
findBinaryInPath(String) - Static method in class lexfo.scalpel.Config
+
 
+
findJdkPath() - Method in class lexfo.scalpel.Config
+
+
Tries to get the JDK path from PATH, usual install locations, or by prompting the user.
+
+
findMontoyaInterface(Class<?>, HashSet<Class<?>>) - Static method in class lexfo.scalpel.UnObfuscator
+
 
+
finished - Variable in class lexfo.scalpel.ScalpelExecutor.Task
+
+
Whether the task has been completed.
+
+
flatMap(Function<? super T, Result<U, E>>) - Method in class lexfo.scalpel.Result
+
 
+
forceGarbageCollection() - Method in class lexfo.scalpel.ScalpelEditorProvider
+
 
+
framework - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The path of the Scalpel framework that will be used to execute the script.
+
+
FRAMEWORK_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
FRAMEWORK_REQ_CB_NAME - Static variable in class lexfo.scalpel.Constants
+
+
Callback prefix for request intercepters.
+
+
FRAMEWORK_REQ_EDIT_PREFIX - Static variable in class lexfo.scalpel.Constants
+
+
Callback prefix for request editors.
+
+
FRAMEWORK_RES_CB_NAME - Static variable in class lexfo.scalpel.Constants
+
+
Callback prefix for response intercepters.
+
+
FRAMEWORK_RES_EDIT_PREFIX - Static variable in class lexfo.scalpel.Constants
+
+
Callback prefix for response editors.
+
+
frameworkBrowseButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
frameworkConfigPanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
frameworkPathField - Variable in class lexfo.scalpel.ConfigTab
+
 
+
frameworkPathTextArea - Variable in class lexfo.scalpel.ConfigTab
+
 
+
+

G

+
+
gbc - Variable in class lexfo.scalpel.components.SettingsPanel
+
 
+
GET_CB_NAME - Static variable in class lexfo.scalpel.Constants
+
 
+
getAvailableCommand(String...) - Static method in class lexfo.scalpel.CommandChecker
+
 
+
getBackgroundByColorIndex(int) - Method in class lexfo.scalpel.Palette
+
 
+
getCallables() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
getCallables() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
getClassName(Object) - Static method in class lexfo.scalpel.UnObfuscator
+
+
Finds a Montoya interface in the specified class, its superclasses or + interfaces, and return its name.
+
+
getClassNames(String) - Method in class lexfo.scalpel.ScalpelExecutor.CustomEnquirer
+
+
Gets the names of all the classes in a package.
+
+
getCtx() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the editor's creation context.
+
+
getCtx() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
getCtx() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the editor's creation context.
+
+
getDefaultGlobalData() - Method in class lexfo.scalpel.Config
+
+
Get the global configuration.
+
+
getDefaultIncludePath() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
getDefaultProjectData() - Method in class lexfo.scalpel.Config
+
+
Get the project configuration.
+
+
getDefaultWorkspace() - Static method in class lexfo.scalpel.Workspace
+
 
+
getDisplayProxyErrorPopup() - Method in class lexfo.scalpel.Config
+
 
+
getEditorCallbackName(Boolean, Boolean) - Static method in class lexfo.scalpel.ScalpelExecutor
+
+
Returns the name of the corresponding Python callback for the given tab.
+
+
getEditorContent() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Get the editor's content
+
+
getEditorContent() - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
 
+
getEditorContent() - Method in class lexfo.scalpel.editors.ScalpelRawEditor
+
 
+
getEditorType() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the editor type (REQUEST or RESPONSE).
+
+
getEditorType() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
getEditorType() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the editor type (REQUEST or RESPONSE).
+
+
getEditScriptCommand() - Method in class lexfo.scalpel.Config
+
 
+
getError() - Method in class lexfo.scalpel.Result
+
 
+
getExecutablePath(Path, String) - Static method in class lexfo.scalpel.Venv
+
 
+
getForegroundByColorIndex(int) - Method in class lexfo.scalpel.Palette
+
 
+
getFrameworkPath() - Method in class lexfo.scalpel.Config
+
 
+
getGlobalConfigFile() - Static method in class lexfo.scalpel.Config
+
+
Get the global configuration file.
+
+
getHookPrefix(String) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
getHookSuffix(String) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
getHttpService() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Get the network informations associated with the editor + + Gets the HttpService from requestResponse and falls back to request if it is null
+
+
getHttpService() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
getHttpService() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Get the network informations associated with the editor + + Gets the HttpService from requestResponse and falls back to request if it is null
+
+
getId() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the editor's unique ID.
+
+
getId() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
getId() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the editor's unique ID.
+
+
getInstalledPackages(Path) - Static method in class lexfo.scalpel.Venv
+
+
Get the list of installed packages in a virtual environment.
+
+
getInstance() - Static method in class lexfo.scalpel.Config
+
+
Provides access to the singleton instance of the Config class.
+
+
getInstance() - Static method in class lexfo.scalpel.ConfigTab
+
 
+
getInstance(MontoyaApi) - Static method in class lexfo.scalpel.Config
+
+
Provides access to the singleton instance of the Config class.
+
+
getInstance(Optional<MontoyaApi>) - Static method in class lexfo.scalpel.Config
+
+
Provides access to the singleton instance of the Config class.
+
+
getJdkPath() - Method in class lexfo.scalpel.Config
+
 
+
getLastModified() - Method in class lexfo.scalpel.Config
+
+
Get the last modification time of the project configuration file.
+
+
getLogLevel() - Method in class lexfo.scalpel.Config
+
 
+
getMessage() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the HTTP message being edited.
+
+
getMessage() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
getMessage() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the HTTP message being edited.
+
+
getMessageCbName(T) - Static method in class lexfo.scalpel.ScalpelExecutor
+
+
Returns the name of the corresponding Python callback for the given message intercepted by Proxy.
+
+
getOpenFolderCommand() - Method in class lexfo.scalpel.Config
+
 
+
getOpenScriptCommand() - Method in class lexfo.scalpel.Config
+
 
+
getOrCreateDefaultWorkspace(Path) - Static method in class lexfo.scalpel.Workspace
+
+
Get the default workspace path.
+
+
getPane() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the Burp editor object.
+
+
getPipPath(Path) - Static method in class lexfo.scalpel.Venv
+
 
+
getPythonVersion() - Static method in class lexfo.scalpel.PythonSetup
+
 
+
getRequest() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Creates a new HTTP request by passing the editor's contents through a Python callback.
+
+
getRequest() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Creates a new HTTP request by passing the editor's contents through a Python callback.
+
+
getRequestResponse() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the stored HttpRequestResponse.
+
+
getRequestResponse() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
getRequestResponse() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the stored HttpRequestResponse.
+
+
getResponse() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Creates a new HTTP response by passing the editor's contents through a Python callback.
+
+
getResponse() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Creates a new HTTP response by passing the editor's contents through a Python callback.
+
+
getRunningJarPath() - Static method in class lexfo.scalpel.RessourcesUnpacker
+
+
Returns the path to the Scalpel JAR file.
+
+
getScalpelDir() - Static method in class lexfo.scalpel.Workspace
+
+
Get the scalpel configuration directory.
+
+
getSelectedWorkspacePath() - Method in class lexfo.scalpel.Config
+
 
+
getSettingsValues() - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
getSitePackagesPath(Path) - Static method in class lexfo.scalpel.Venv
+
 
+
getSubPackages(String) - Method in class lexfo.scalpel.ScalpelExecutor.CustomEnquirer
+
+
Gets the names of all the sub-packages of a package.
+
+
getTabNameOffsetInHookName(String) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
getUiComponent() - Method in class lexfo.scalpel.editors.AbstractEditor
+
 
+
getUiComponent() - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
Returns the underlying UI component.
+
+
getUiComponent() - Method in class lexfo.scalpel.editors.ScalpelRawEditor
+
+
Returns the underlying UI component.
+
+
getUsedPythonBin() - Static method in class lexfo.scalpel.PythonSetup
+
 
+
getUserScriptPath() - Method in class lexfo.scalpel.Config
+
 
+
getValue() - Method in class lexfo.scalpel.Result
+
 
+
getVenvDir(Path) - Static method in class lexfo.scalpel.Workspace
+
 
+
getVenvPaths() - Method in class lexfo.scalpel.Config
+
 
+
getWorkspacesDir() - Static method in class lexfo.scalpel.Workspace
+
+
Get the default venvs directory.
+
+
globalConfig - Variable in class lexfo.scalpel.Config
+
 
+
guessJdkPath() - Static method in class lexfo.scalpel.Config
+
 
+
+

H

+
+
handleBrowseButtonClick(Supplier<Path>, Consumer<Path>) - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleEnableButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleHttpRequestToBeSent(HttpRequestToBeSent) - Method in class lexfo.scalpel.ScalpelHttpRequestHandler
+
+
Handles HTTP requests.
+
+
handleHttpResponseReceived(HttpResponseReceived) - Method in class lexfo.scalpel.ScalpelHttpRequestHandler
+
+
Handles HTTP responses.
+
+
handleNewScriptButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleOpenScriptButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleOpenScriptFolderButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleScriptListSelectionEvent() - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleVenvButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
handleVenvListSelectionEvent(ListSelectionEvent) - Method in class lexfo.scalpel.ConfigTab
+
 
+
hasConfigChanged() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
hasFrameworkChanged() - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Checks if the framework file has been modified since the last check.
+
+
hashCode() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Returns a hash code value for this object.
+
+
hashCode() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Returns a hash code value for this object.
+
+
hashCode() - Method in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
Returns a hash code value for this object.
+
+
hasIncludeDir(Path) - Static method in class lexfo.scalpel.Config
+
 
+
hasScriptChanged() - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Checks if the script file has been modified since the last check.
+
+
hasValue() - Method in class lexfo.scalpel.Result
+
 
+
helpTextPane - Variable in class lexfo.scalpel.ConfigTab
+
 
+
HEX_EDITOR_MODE - Static variable in class lexfo.scalpel.Constants
+
 
+
hookInPrefix - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
req_edit_in_ or res_edit_in_
+
+
hookOutPrefix - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
req_edit_out_ or res_edit_out_
+
+
hookPrefix - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
req_edit_ or res_edit
+
+
HookTabInfo(String, String, Set<String>) - Constructor for record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Creates an instance of a HookTabInfo record class.
+
+
+

I

+
+
id - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The editor ID.
+
+
id - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The editor ID.
+
+
ifEmpty(Runnable) - Method in class lexfo.scalpel.Result
+
 
+
ifError(Consumer<E>) - Method in class lexfo.scalpel.Result
+
 
+
ifSuccess(Consumer<T>) - Method in class lexfo.scalpel.Result
+
 
+
IMessageEditor - Interface in lexfo.scalpel.editors
+
+
Interface declaring all the necessary methods to implement a Scalpel editor + If you wish to implement your own type of editor, you should use the AbstractEditor class as a base.
+
+
IN_SUFFIX - Static variable in class lexfo.scalpel.Constants
+
+
Callback suffix for HttpMessage-to-bytes convertion.
+
+
inError - Variable in class lexfo.scalpel.editors.AbstractEditor
+
 
+
info(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output at the INFO level.
+
+
INFO - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
initGlobalConfig() - Method in class lexfo.scalpel.Config
+
 
+
initialize(MontoyaApi) - Method in class lexfo.scalpel.Scalpel
+
+
Initializes the extension.
+
+
initInterpreter() - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Initializes the interpreter.
+
+
initProjectConfig() - Method in class lexfo.scalpel.Config
+
 
+
install(Path, String...) - Static method in class lexfo.scalpel.Venv
+
+
Install a package in a virtual environment.
+
+
install(Path, Map<String, String>, String...) - Static method in class lexfo.scalpel.Venv
+
+
Install a package in a virtual environment.
+
+
install_background(Path, String...) - Static method in class lexfo.scalpel.Venv
+
+
Install a package in a virtual environment in a new thread.
+
+
install_background(Path, Map<String, String>, String...) - Static method in class lexfo.scalpel.Venv
+
+
Install a package in a virtual environment.
+
+
installDefaults(Path) - Static method in class lexfo.scalpel.Venv
+
 
+
installDefaults(Path, Map<String, String>, Boolean) - Static method in class lexfo.scalpel.Venv
+
 
+
instance - Static variable in class lexfo.scalpel.Config
+
 
+
instance - Static variable in class lexfo.scalpel.ConfigTab
+
 
+
IO - Class in lexfo.scalpel
+
+
Utilities to perform IO utilities conveniently
+
+
IO() - Constructor for class lexfo.scalpel.IO
+
 
+
IO.IORunnable - Interface in lexfo.scalpel
+
 
+
IO.IOSupplier<T> - Interface in lexfo.scalpel
+
 
+
ioWrap(IO.IOSupplier<T>) - Static method in class lexfo.scalpel.IO
+
 
+
ioWrap(IO.IOSupplier<T>, Supplier<T>) - Static method in class lexfo.scalpel.IO
+
 
+
isCommandAvailable(String) - Static method in class lexfo.scalpel.CommandChecker
+
 
+
isEmpty - Variable in class lexfo.scalpel.Result
+
 
+
isEmpty() - Method in class lexfo.scalpel.Result
+
 
+
isEnabled - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
isEnabled() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
isEnabledFor(HttpRequestResponse) - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Determines whether the editor should be enabled for the provided HttpRequestResponse.
+
+
isEnabledFor(HttpRequestResponse) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Determines whether the editor should be enabled for the provided HttpRequestResponse.
+
+
isFinished() - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
 
+
isJavaPackage(String) - Method in class lexfo.scalpel.ScalpelExecutor.CustomEnquirer
+
+
Determines whether a string represents a valid Java package.
+
+
isJepInstalled(Path) - Static method in class lexfo.scalpel.Workspace
+
+
If a previous install failed because python dependencies were not installed, + this will be false, in this case, we just try to resume the install.
+
+
isModified() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns whether the editor has been modified since the last time it was programatically set + (called by Burp)
+
+
isModified() - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
Returns whether the editor has been modified.
+
+
isModified() - Method in class lexfo.scalpel.editors.ScalpelRawEditor
+
+
Returns whether the editor has been modified.
+
+
isModified() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns whether the editor has been modified.
+
+
isRunnerAlive - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
Flag indicating whether the task runner loop is running.
+
+
isRunnerStarting - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
isRunning() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
isStarting() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
isSuccess() - Method in class lexfo.scalpel.Result
+
 
+
+

J

+
+
jdkPath - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
+

K

+
+
keyToLabel - Variable in class lexfo.scalpel.components.SettingsPanel
+
 
+
kwargs - Variable in class lexfo.scalpel.ScalpelExecutor.Task
+
+
The keyword arguments passed to the task.
+
+
+

L

+
+
lastConfigModificationTimestamp - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
lastFrameworkModificationTimestamp - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The timestamp of the last recorded modification to the framework file.
+
+
lastModified - Variable in class lexfo.scalpel.Config
+
 
+
lastScriptModificationTimestamp - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The timestamp of the last recorded modification to the script file.
+
+
launchOpenScriptCommand(Path) - Method in class lexfo.scalpel.ConfigTab
+
 
+
launchTaskRunner() - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Launches the task runner thread.
+
+
Level(int) - Constructor for enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
lexfo.scalpel - package lexfo.scalpel
+
 
+
lexfo.scalpel.components - package lexfo.scalpel.components
+
 
+
lexfo.scalpel.editors - package lexfo.scalpel.editors
+
 
+
LIGHT_COLORS - Static variable in class lexfo.scalpel.Palette
+
 
+
LIGHT_PALETTE - Static variable in class lexfo.scalpel.Palette
+
 
+
linkifyURLs(String) - Static method in class lexfo.scalpel.components.ErrorDialog
+
 
+
listPannel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
loadLibPython3() - Static method in class lexfo.scalpel.PythonSetup
+
 
+
log(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output at the TRACE level.
+
+
log(ScalpelLogger.Level, String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output.
+
+
logConfig(Config) - Static method in class lexfo.scalpel.Scalpel
+
 
+
logFatalStackTrace(Throwable) - Static method in class lexfo.scalpel.ScalpelLogger
+
 
+
logger - Static variable in class lexfo.scalpel.ScalpelLogger
+
 
+
loggerLevel - Static variable in class lexfo.scalpel.ScalpelLogger
+
+
Configured log level
+
+
logLevel - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
logStackTrace() - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the current thread stack trace to the Burp Suite error output and standard error.
+
+
logStackTrace(Boolean) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the current thread stack trace to either the Burp Suite output and standard output or the Burp Suite error output and standard error.
+
+
logStackTrace(String, Throwable) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified throwable stack trace to the Burp Suite error output and standard error.
+
+
logStackTrace(Throwable) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified throwable stack trace to the Burp Suite error output and standard error.
+
+
+

M

+
+
main(String[]) - Static method in class lexfo.scalpel.components.PlaceholderTextField
+
 
+
map(Function<? super T, ? extends U>) - Method in class lexfo.scalpel.Result
+
 
+
mapper - Static variable in class lexfo.scalpel.IO
+
 
+
mergeHookTabInfo(Stream<ScalpelEditorTabbedPane.PartialHookTabInfo>) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Takes all the hooks infos and merge the corresponding ones + E.g: + Given the hook req_edit_in_tab1 + To create a tab, we need to know if req_edit_in_tab1 has a corresponding req_edit_out_tab1 + The editor mode (raw or hex) must be taken from the req_edit_in_tab1 annotations (@edit("hex"))
+
+
MIN_SUPPORTED_PYTHON_VERSION - Static variable in class lexfo.scalpel.Constants
+
 
+
mode - Variable in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
The field for the mode record component.
+
+
mode - Variable in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
The field for the mode record component.
+
+
mode() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Returns the value of the mode record component.
+
+
mode() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Returns the value of the mode record component.
+
+
modeToEditorMap - Static variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
 
+
mustReload() - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Checks if either the framework or user script file has been modified since the last check.
+
+
myColors - Variable in class lexfo.scalpel.Palette
+
 
+
+

N

+
+
name - Variable in class lexfo.scalpel.editors.AbstractEditor
+
 
+
name - Variable in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
The field for the name record component.
+
+
name - Variable in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
The field for the name record component.
+
+
name - Variable in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
The field for the name record component.
+
+
name - Variable in class lexfo.scalpel.ScalpelExecutor.Task
+
+
The name of the task.
+
+
name - Variable in class lexfo.scalpel.Venv.PackageInfo
+
 
+
name() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Returns the value of the name record component.
+
+
name() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Returns the value of the name record component.
+
+
name() - Method in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
Returns the value of the name record component.
+
+
names - Static variable in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
nameToLevel - Static variable in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
NATIVE_LIBJEP_FILE - Static variable in class lexfo.scalpel.Constants
+
+
JEP native library filename
+
+
newDecoder() - Method in class lexfo.scalpel.editors.DisplayableWhiteSpaceCharset
+
 
+
newEncoder() - Method in class lexfo.scalpel.editors.DisplayableWhiteSpaceCharset
+
 
+
notifyChangeListeners() - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
notifyEventLoop() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
+

O

+
+
oldContent - Variable in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
 
+
openEditorInTerminal(Path) - Method in class lexfo.scalpel.ConfigTab
+
+
Opens the script in a terminal editor
+
+
openFolderButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
openFolderCommand - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
openIssueOnGitHubButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
openScriptButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
openScriptCommand - Variable in class lexfo.scalpel.Config._GlobalData
+
 
+
or(Result<T, E>) - Method in class lexfo.scalpel.Result
+
 
+
orElse(T) - Method in class lexfo.scalpel.Result
+
 
+
orElseGet(Supplier<? extends T>) - Method in class lexfo.scalpel.Result
+
 
+
originalDecoder - Variable in class lexfo.scalpel.editors.WhitspaceCharsetDecoder
+
 
+
originalEncoder - Variable in class lexfo.scalpel.editors.WhitspaceCharsetEncoder
+
 
+
OUT_SUFFIX - Static variable in class lexfo.scalpel.Constants
+
+
Callback suffix for bytes to HttpMessage convertion.
+
+
outError - Variable in class lexfo.scalpel.editors.AbstractEditor
+
 
+
outputTabPanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
+

P

+
+
PackageInfo() - Constructor for class lexfo.scalpel.Venv.PackageInfo
+
 
+
packagesTable - Variable in class lexfo.scalpel.ConfigTab
+
 
+
paintComponent(Graphics) - Method in class lexfo.scalpel.components.PlaceholderTextField
+
 
+
Palette - Class in lexfo.scalpel
+
+
Color palette for the embedded terminal + Contains colors for both light and dark theme
+
+
Palette(Color[]) - Constructor for class lexfo.scalpel.Palette
+
 
+
pane - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The editor swing UI component.
+
+
PartialHookTabInfo(String, String, String) - Constructor for record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Creates an instance of a PartialHookTabInfo record class.
+
+
PERSISTED_FRAMEWORK - Static variable in class lexfo.scalpel.Constants
+
+
Persistence key for the cached framework path.
+
+
PERSISTED_SCRIPT - Static variable in class lexfo.scalpel.Constants
+
+
Persistence key for the cached user script path.
+
+
PERSISTENCE_PREFIX - Static variable in class lexfo.scalpel.Constants
+
+
Scalpel prefix for the persistence databases.
+
+
PIP_BIN - Static variable in class lexfo.scalpel.Constants
+
 
+
placeholder - Variable in class lexfo.scalpel.components.PlaceholderTextField
+
 
+
PlaceholderTextField - Class in lexfo.scalpel.components
+
 
+
PlaceholderTextField(String) - Constructor for class lexfo.scalpel.components.PlaceholderTextField
+
 
+
popup - Variable in class lexfo.scalpel.ScalpelHttpRequestHandler
+
 
+
PREFERRED_PYTHON_VERSION - Static variable in class lexfo.scalpel.Constants
+
 
+
println(Terminal, String) - Static method in class lexfo.scalpel.Workspace
+
 
+
processOutboundMessage() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Creates a new HTTP message by passing the editor's contents through a Python callback.
+
+
processOutboundMessage() - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
processOutboundMessage() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Creates a new HTTP message by passing the editor's contents through a Python callback.
+
+
processTask(SubInterpreter, ScalpelExecutor.Task) - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
projectConfig - Variable in class lexfo.scalpel.Config
+
 
+
projectID - Variable in class lexfo.scalpel.Config
+
 
+
projectScalpelConfig - Variable in class lexfo.scalpel.Config
+
 
+
provideHttpRequestEditor(EditorCreationContext) - Method in class lexfo.scalpel.ScalpelEditorProvider
+
+
Provides a new ExtensionProvidedHttpRequestEditor object for editing an HTTP request.
+
+
provideHttpResponseEditor(EditorCreationContext) - Method in class lexfo.scalpel.ScalpelEditorProvider
+
+
Provides a new ExtensionProvidedHttpResponseEditor object for editing an HTTP response.
+
+
provider - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The editor provider that instantiated this editor.
+
+
provider - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The editor provider that instantiated this editor.
+
+
pushCharToOutput(int, boolean) - Static method in class lexfo.scalpel.ConfigTab
+
+
Push a character to a stdout or stderr text area.
+
+
putStringToOutput(String, boolean) - Static method in class lexfo.scalpel.ConfigTab
+
 
+
PYSCALPEL_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
PYTHON_BIN - Static variable in class lexfo.scalpel.Constants
+
+
Python 3 executable filename
+
+
PYTHON_DEPENDENCIES - Static variable in class lexfo.scalpel.Constants
+
 
+
PYTHON_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
PYTHON_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
PythonSetup - Class in lexfo.scalpel
+
+
Utilities to initialize Java Embedded Python (jep)
+
+
PythonSetup() - Constructor for class lexfo.scalpel.PythonSetup
+
 
+
pythonStderr - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
pythonStdout - Variable in class lexfo.scalpel.ScalpelExecutor
+
 
+
PythonUtils - Class in lexfo.scalpel
+
+
Utility class for Python scripts.
+
+
PythonUtils() - Constructor for class lexfo.scalpel.PythonUtils
+
 
+
+

R

+
+
RAW_EDITOR_MODE - Static variable in class lexfo.scalpel.Constants
+
 
+
readConfigFile(File, Class<T>) - Static method in class lexfo.scalpel.Config
+
 
+
readJSON(File, Class<T>) - Static method in class lexfo.scalpel.IO
+
 
+
readJSON(File, Class<T>, Consumer<IOException>) - Static method in class lexfo.scalpel.IO
+
 
+
readJSON(String, Class<T>) - Static method in class lexfo.scalpel.IO
+
 
+
recreateEditors() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Recreates the editors tabs.
+
+
recreateEditorsAsync() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Recreates the editors tabs asynchronously.
+
+
reject() - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
 
+
reject(Throwable) - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
 
+
reject(Optional<Throwable>) - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
 
+
rejectAllTasks() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
removeVenvPath(Path) - Method in class lexfo.scalpel.Config
+
 
+
REQ_CB_NAME - Static variable in class lexfo.scalpel.Constants
+
 
+
REQ_EDIT_PREFIX - Static variable in class lexfo.scalpel.Constants
+
 
+
REQUEST - Enum constant in enum class lexfo.scalpel.EditorType
+
+
Indicates an editor for an HTTP request.
+
+
RES_CB_NAME - Static variable in class lexfo.scalpel.Constants
+
 
+
RES_EDIT_PREFIX - Static variable in class lexfo.scalpel.Constants
+
 
+
resetChangeIndicators() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
resetEditors() - Method in class lexfo.scalpel.ScalpelEditorProvider
+
 
+
resetEditorsAsync() - Method in class lexfo.scalpel.ScalpelEditorProvider
+
 
+
resetTerminalButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
resolve(Object) - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
 
+
RESPONSE - Enum constant in enum class lexfo.scalpel.EditorType
+
+
Indicates an editor for an HTTP response.
+
+
RESSOURCES_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
RESSOURCES_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
RESSOURCES_TO_COPY - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
RessourcesUnpacker - Class in lexfo.scalpel
+
+
Provides methods for unpacking the Scalpel resources.
+
+
RessourcesUnpacker() - Constructor for class lexfo.scalpel.RessourcesUnpacker
+
 
+
result - Variable in class lexfo.scalpel.ScalpelExecutor.Task
+
+
An optional object containing the result of the task, if it has been completed.
+
+
Result<T,E extends Throwable> - Class in lexfo.scalpel
+
+
Optional-style class for handling python task results + + A completed python task can have multiple outcomes: + - The task completes successfully and returns a value + - The task completes successfully but returns no value + - The task throws an exception + + Result allows us to handle returned values and errors uniformly to handle them when needed.
+
+
Result(T, E, boolean) - Constructor for class lexfo.scalpel.Result
+
 
+
rootPanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
rootPanel - Variable in class lexfo.scalpel.editors.AbstractEditor
+
 
+
run() - Method in interface lexfo.scalpel.IO.IORunnable
+
 
+
run(Runnable) - Static method in class lexfo.scalpel.Async
+
 
+
run(IO.IORunnable) - Static method in class lexfo.scalpel.IO
+
 
+
runner - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The task runner thread.
+
+
+

S

+
+
safeCloseInterpreter(SubInterpreter) - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
safeJepInvoke(String, Class<T>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the given Python function without any argument.
+
+
safeJepInvoke(String, Object[], Map<String, Object>, Class<T>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the given Python function with the given arguments and keyword arguments.
+
+
safeJepInvoke(String, Object, Class<T>) - Method in class lexfo.scalpel.ScalpelExecutor
+
+
Calls the given Python function with the given argument.
+
+
SAMPLES_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
SAMPLES_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
sanitizeHTML(String) - Static method in class lexfo.scalpel.components.ErrorDialog
+
 
+
saveAllConfig() - Method in class lexfo.scalpel.Config
+
+
Write the global and project configuration to their respective files.
+
+
saveGlobalConfig() - Method in class lexfo.scalpel.Config
+
+
Write the global configuration to the global configuration file.
+
+
saveProjectConfig() - Method in class lexfo.scalpel.Config
+
+
Write the project configuration to the project configuration file.
+
+
Scalpel - Class in lexfo.scalpel
+
+
The main class of the extension.
+
+
Scalpel() - Constructor for class lexfo.scalpel.Scalpel
+
 
+
ScalpelBinaryEditor - Class in lexfo.scalpel.editors
+
 
+
ScalpelBinaryEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor) - Constructor for class lexfo.scalpel.editors.ScalpelBinaryEditor
+
 
+
ScalpelDecimalEditor - Class in lexfo.scalpel.editors
+
 
+
ScalpelDecimalEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor) - Constructor for class lexfo.scalpel.editors.ScalpelDecimalEditor
+
 
+
ScalpelEditorProvider - Class in lexfo.scalpel
+
+
Provides a new ScalpelProvidedEditor object for editing HTTP requests or responses.
+
+
ScalpelEditorProvider(MontoyaApi, ScalpelExecutor) - Constructor for class lexfo.scalpel.ScalpelEditorProvider
+
+
Constructs a new ScalpelEditorProvider object with the specified MontoyaApi object and ScalpelExecutor object.
+
+
ScalpelEditorTabbedPane - Class in lexfo.scalpel
+
+
Provides an UI text editor component for editing HTTP requests or responses.
+
+
ScalpelEditorTabbedPane(MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorProvider, ScalpelExecutor) - Constructor for class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Constructs a new Scalpel editor.
+
+
ScalpelEditorTabbedPane.HookTabInfo - Record Class in lexfo.scalpel
+
+
This stores all the informations required to create a tab.
+
+
ScalpelEditorTabbedPane.PartialHookTabInfo - Record Class in lexfo.scalpel
+
+
A tab can be associated with at most two hooks + (e.g req_edit_in and req_edit_out) + + This stores the informations related to only one hook and is later merged with the second hook information into a HookTabInfo
+
+
scalpelExecutor - Variable in class lexfo.scalpel.ConfigTab
+
 
+
ScalpelExecutor - Class in lexfo.scalpel
+
+
Responds to requested Python tasks from multiple threads through a task queue handled in a single sepearate thread.
+
+
ScalpelExecutor(MontoyaApi, Config) - Constructor for class lexfo.scalpel.ScalpelExecutor
+
+
Constructs a new ScalpelExecutor object.
+
+
ScalpelExecutor.CallableData - Record Class in lexfo.scalpel
+
 
+
ScalpelExecutor.CustomEnquirer - Class in lexfo.scalpel
+
+
A custom ClassEnquirer for the Jep interpreter used by the script executor.
+
+
ScalpelExecutor.Task - Class in lexfo.scalpel
+
+
A class representing a task to be executed by the Scalpel script.
+
+
ScalpelGenericBinaryEditor - Class in lexfo.scalpel.editors
+
+
Hexadecimal editor implementation for a Scalpel editor + Users can press their keyboard's INSER key to enter insertion mode + (which is impossible in Burp's native hex editor)
+
+
ScalpelGenericBinaryEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor, CodeType) - Constructor for class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
Constructs a new Scalpel editor.
+
+
ScalpelHexEditor - Class in lexfo.scalpel.editors
+
 
+
ScalpelHexEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor) - Constructor for class lexfo.scalpel.editors.ScalpelHexEditor
+
 
+
ScalpelHttpRequestHandler - Class in lexfo.scalpel
+
+
Handles HTTP requests and responses.
+
+
ScalpelHttpRequestHandler(MontoyaApi, ScalpelEditorProvider, ScalpelExecutor) - Constructor for class lexfo.scalpel.ScalpelHttpRequestHandler
+
+
Constructs a new ScalpelHttpRequestHandler object with the specified MontoyaApi object and ScalpelExecutor object.
+
+
scalpelIsENABLEDButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
ScalpelLogger - Class in lexfo.scalpel
+
+
Provides methods for logging messages to the Burp Suite output and standard streams.
+
+
ScalpelLogger() - Constructor for class lexfo.scalpel.ScalpelLogger
+
 
+
ScalpelLogger.Level - Enum Class in lexfo.scalpel
+
+
Log levels used to filtrate logs by weight + Useful for debugging.
+
+
ScalpelOctalEditor - Class in lexfo.scalpel.editors
+
 
+
ScalpelOctalEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor) - Constructor for class lexfo.scalpel.editors.ScalpelOctalEditor
+
 
+
ScalpelRawEditor - Class in lexfo.scalpel.editors
+
+
Provides an UI text editor component for editing HTTP requests or responses.
+
+
ScalpelRawEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor) - Constructor for class lexfo.scalpel.editors.ScalpelRawEditor
+
+
Constructs a new Scalpel editor.
+
+
script - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The path of the Scalpel script that will be passed to the framework.
+
+
scriptBrowseButton - Variable in class lexfo.scalpel.ConfigTab
+
 
+
scriptConfigPanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
scriptPathTextArea - Variable in class lexfo.scalpel.ConfigTab
+
 
+
selectedData() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the selected data.
+
+
selectedData() - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
Returns the selected data.
+
+
selectedData() - Method in class lexfo.scalpel.editors.ScalpelRawEditor
+
+
Returns the selected data.
+
+
selectedData() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the selected data.
+
+
selectEditor() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Select the most suited editor for updating Burp message data.
+
+
selectedScriptLabel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
selectScript(Path) - Method in class lexfo.scalpel.ConfigTab
+
 
+
setAndStoreScript(Path) - Method in class lexfo.scalpel.ConfigTab
+
 
+
setDisplayProxyErrorPopup(String) - Method in class lexfo.scalpel.Config
+
 
+
setEditorContent(ByteArray) - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Set the editor's content + + Note: This should update isModified()
+
+
setEditorContent(ByteArray) - Method in class lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
 
+
setEditorContent(ByteArray) - Method in class lexfo.scalpel.editors.ScalpelRawEditor
+
 
+
setEditorError(Throwable) - Method in class lexfo.scalpel.editors.AbstractEditor
+
 
+
setEditorsProvider(ScalpelEditorProvider) - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
setEditScriptCommand(String) - Method in class lexfo.scalpel.Config
+
 
+
setJdkPath(Path) - Method in class lexfo.scalpel.Config
+
 
+
setLogger(Logging) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Set the Burp logger instance to use.
+
+
setLogLevel(String) - Method in class lexfo.scalpel.Config
+
 
+
setLogLevel(ScalpelLogger.Level) - Static method in class lexfo.scalpel.ScalpelLogger
+
 
+
setOpenFolderCommand(String) - Method in class lexfo.scalpel.Config
+
 
+
setOpenScriptCommand(String) - Method in class lexfo.scalpel.Config
+
 
+
setRequestResponse(HttpRequestResponse) - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Sets the HttpRequestResponse to be edited.
+
+
setRequestResponse(HttpRequestResponse) - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Sets the HttpRequestResponse to be edited.
+
+
setRequestResponseInternal(HttpRequestResponse) - Method in class lexfo.scalpel.editors.AbstractEditor
+
 
+
setRequestResponseInternal(HttpRequestResponse) - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
setSelectedVenvPath(Path) - Method in class lexfo.scalpel.Config
+
 
+
setSettings(Map<String, String>) - Method in class lexfo.scalpel.ConfigTab
+
 
+
setSettingsValues(Map<String, String>) - Method in class lexfo.scalpel.components.SettingsPanel
+
 
+
settingsComponentsByKey - Variable in class lexfo.scalpel.components.SettingsPanel
+
 
+
settingsPanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
SettingsPanel - Class in lexfo.scalpel.components
+
 
+
SettingsPanel() - Constructor for class lexfo.scalpel.components.SettingsPanel
+
 
+
settingsTab - Variable in class lexfo.scalpel.ConfigTab
+
 
+
setupAutoScroll(JScrollPane, JTextArea) - Static method in class lexfo.scalpel.UIUtils
+
+
Set up auto-scrolling for a script output text area.
+
+
setupCopyButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setupDebugInfoTab() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setupGitHubIssueButton() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setupHelpTab() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setupJepFromConfig(Config) - Static method in class lexfo.scalpel.Scalpel
+
 
+
setupLogsTab() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setupSettingsTab() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setupVenvTab() - Method in class lexfo.scalpel.ConfigTab
+
 
+
setUserScriptPath(Path) - Method in class lexfo.scalpel.Config
+
 
+
setVenvPaths(ArrayList<String>) - Method in class lexfo.scalpel.Config
+
 
+
SHELL_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
showBlockingWaitDialog(String, Consumer<JLabel>) - Static method in class lexfo.scalpel.components.WorkingPopup
+
+
Shows a blocking wait dialog.
+
+
showErrorDialog(Frame, String) - Static method in class lexfo.scalpel.components.ErrorDialog
+
 
+
sleep(Integer) - Static method in class lexfo.scalpel.IO
+
 
+
stackTraceToString(StackTraceElement[]) - Static method in class lexfo.scalpel.ScalpelLogger
+
 
+
stderrScrollPane - Variable in class lexfo.scalpel.ConfigTab
+
 
+
stderrTextArea - Variable in class lexfo.scalpel.ConfigTab
+
 
+
stdoutScrollPane - Variable in class lexfo.scalpel.ConfigTab
+
 
+
stdoutTextArea - Variable in class lexfo.scalpel.ConfigTab
+
 
+
success(T) - Static method in class lexfo.scalpel.Result
+
 
+
suppressCheckBox - Variable in class lexfo.scalpel.components.ErrorPopup
+
 
+
+

T

+
+
tabs - Variable in class lexfo.scalpel.editors.AbstractEditor
+
 
+
Task(String, Object[], Map<String, Object>) - Constructor for class lexfo.scalpel.ScalpelExecutor.Task
+
+
Constructs a new Task object.
+
+
taskLoop() - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
tasks - Variable in class lexfo.scalpel.ScalpelExecutor
+
+
The Python task queue.
+
+
TEMPLATES_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
Terminal - Class in lexfo.scalpel
+
 
+
Terminal() - Constructor for class lexfo.scalpel.Terminal
+
 
+
terminalForVenvConfig - Variable in class lexfo.scalpel.ConfigTab
+
 
+
theme - Variable in class lexfo.scalpel.ConfigTab
+
 
+
then(Consumer<Object>) - Method in class lexfo.scalpel.ScalpelExecutor.Task
+
 
+
toByteArray(byte[]) - Static method in class lexfo.scalpel.PythonUtils
+
+
Convert Python bytes to a Burp ByteArray
+
+
toJavaBytes(byte[]) - Static method in class lexfo.scalpel.PythonUtils
+
+
Convert Python bytes to Java bytes + + It is not possible to explicitely convert to Java bytes Python side without a Java helper like this one, + because Jep doesn't natively support the convertion: + https://github.com/ninia/jep/wiki/How-Jep-Works#objects + + When returning byte[], + Python receives a PyJArray of integer-like objects which will be mapped back to byte[] by Jep.
+
+
toPythonBytes(byte[]) - Static method in class lexfo.scalpel.PythonUtils
+
+
Convert Java signed bytes to corresponding unsigned values + Convertions issues occur when passing Java bytes to Python because Java's are signed and Python's are unsigned.
+
+
toString() - Method in class lexfo.scalpel.Result
+
 
+
toString() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
Returns a string representation of this record class.
+
+
toString() - Method in record class lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
Returns a string representation of this record class.
+
+
toString() - Method in record class lexfo.scalpel.ScalpelExecutor.CallableData
+
+
Returns a string representation of this record class.
+
+
trace(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output at the TRACE level.
+
+
TRACE - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
type - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The editor type (REQUEST or RESPONSE).
+
+
type - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The editor type (REQUEST or RESPONSE).
+
+
+

U

+
+
UIBuilder - Class in lexfo.scalpel
+
+
Provides methods for constructing the Burp Suite UI.
+
+
UIBuilder() - Constructor for class lexfo.scalpel.UIBuilder
+
 
+
uiComponent() - Method in class lexfo.scalpel.ConfigTab
+
+
Returns the UI component to display.
+
+
uiComponent() - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Returns the underlying UI component.
+
+
uiComponent() - Method in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
Returns the underlying UI component.
+
+
UIUtils - Class in lexfo.scalpel
+
 
+
UIUtils() - Constructor for class lexfo.scalpel.UIUtils
+
 
+
UnObfuscator - Class in lexfo.scalpel
+
 
+
UnObfuscator() - Constructor for class lexfo.scalpel.UnObfuscator
+
 
+
updateContent(HttpRequestResponse) - Method in class lexfo.scalpel.editors.AbstractEditor
+
+
Initializes the editor with Python callbacks output of the inputted HTTP message.
+
+
updateContent(HttpRequestResponse) - Method in interface lexfo.scalpel.editors.IMessageEditor
+
 
+
updateHeader(T, String, String) - Static method in class lexfo.scalpel.PythonUtils
+
+
Updates the specified HttpMessage object's header with the specified name and value.
+
+
updatePackagesTable() - Method in class lexfo.scalpel.ConfigTab
+
 
+
updatePackagesTable(Consumer<JTable>) - Method in class lexfo.scalpel.ConfigTab
+
 
+
updatePackagesTable(Consumer<JTable>, Runnable) - Method in class lexfo.scalpel.ConfigTab
+
 
+
updateRootPanel() - Method in class lexfo.scalpel.editors.AbstractEditor
+
 
+
updateScriptList() - Method in class lexfo.scalpel.ConfigTab
+
 
+
updateTerminal(String) - Method in class lexfo.scalpel.ConfigTab
+
 
+
updateTerminal(String, String, String) - Method in class lexfo.scalpel.ConfigTab
+
 
+
userScriptPath - Variable in class lexfo.scalpel.Config._ProjectData
+
 
+
utf8 - Variable in class lexfo.scalpel.editors.DisplayableWhiteSpaceCharset
+
 
+
+

V

+
+
VALID_HOOK_PREFIXES - Static variable in class lexfo.scalpel.Constants
+
 
+
value - Variable in class lexfo.scalpel.Result
+
 
+
value - Variable in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
value() - Method in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
valueOf(String) - Static method in enum class lexfo.scalpel.EditorType
+
+
Returns the enum constant of this class with the specified name.
+
+
valueOf(String) - Static method in enum class lexfo.scalpel.ScalpelLogger.Level
+
+
Returns the enum constant of this class with the specified name.
+
+
values() - Static method in enum class lexfo.scalpel.EditorType
+
+
Returns an array containing the constants of this enum class, in +the order they are declared.
+
+
values() - Static method in enum class lexfo.scalpel.ScalpelLogger.Level
+
+
Returns an array containing the constants of this enum class, in +the order they are declared.
+
+
Venv - Class in lexfo.scalpel
+
+
Manage Python virtual environments.
+
+
Venv() - Constructor for class lexfo.scalpel.Venv
+
 
+
VENV_BIN_DIR - Static variable in class lexfo.scalpel.Constants
+
 
+
VENV_DIR - Static variable in class lexfo.scalpel.Workspace
+
 
+
VENV_LIB_DIR - Static variable in class lexfo.scalpel.Constants
+
+
Venv dir containing site-packages
+
+
Venv.PackageInfo - Class in lexfo.scalpel
+
 
+
venvListComponent - Variable in class lexfo.scalpel.ConfigTab
+
 
+
venvScriptList - Variable in class lexfo.scalpel.ConfigTab
+
 
+
venvSelectPanel - Variable in class lexfo.scalpel.ConfigTab
+
 
+
version - Variable in class lexfo.scalpel.Venv.PackageInfo
+
 
+
+

W

+
+
waitForExecutor(MontoyaApi, ScalpelEditorProvider, ScalpelExecutor) - Static method in class lexfo.scalpel.Scalpel
+
 
+
warn(String) - Static method in class lexfo.scalpel.ScalpelLogger
+
+
Logs the specified message to the Burp Suite output and standard output at the WARN level.
+
+
WARN - Enum constant in enum class lexfo.scalpel.ScalpelLogger.Level
+
 
+
WhitspaceCharsetDecoder - Class in lexfo.scalpel.editors
+
 
+
WhitspaceCharsetDecoder(Charset, CharsetDecoder) - Constructor for class lexfo.scalpel.editors.WhitspaceCharsetDecoder
+
 
+
WhitspaceCharsetEncoder - Class in lexfo.scalpel.editors
+
 
+
WhitspaceCharsetEncoder(Charset, CharsetEncoder) - Constructor for class lexfo.scalpel.editors.WhitspaceCharsetEncoder
+
 
+
WINDOWS_COLORS - Static variable in class lexfo.scalpel.Palette
+
 
+
WINDOWS_PALETTE - Static variable in class lexfo.scalpel.Palette
+
 
+
WorkingPopup - Class in lexfo.scalpel.components
+
+
Provides a blocking wait dialog GUI popup.
+
+
WorkingPopup() - Constructor for class lexfo.scalpel.components.WorkingPopup
+
 
+
Workspace - Class in lexfo.scalpel
+
+
A workspace is a folder containing a venv and the associated scripts.
+
+
Workspace() - Constructor for class lexfo.scalpel.Workspace
+
 
+
WORKSPACE_DIRNAME - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
WORKSPACE_PATH - Static variable in class lexfo.scalpel.RessourcesUnpacker
+
 
+
workspacePath - Variable in class lexfo.scalpel.Config._ProjectData
+
 
+
workspacePaths - Variable in class lexfo.scalpel.Config._GlobalData
+
+
List of registered venv paths.
+
+
writeFile(String, String) - Static method in class lexfo.scalpel.IO
+
 
+
writeJSON(File, Object) - Static method in class lexfo.scalpel.IO
+
 
+
writer - Static variable in class lexfo.scalpel.IO
+
 
+
+

_

+
+
_GlobalData() - Constructor for class lexfo.scalpel.Config._GlobalData
+
 
+
_innerTaskLoop(SubInterpreter) - Method in class lexfo.scalpel.ScalpelExecutor
+
 
+
_jdkPath - Variable in class lexfo.scalpel.Config
+
 
+
_ProjectData() - Constructor for class lexfo.scalpel.Config._ProjectData
+
 
+
_requestResponse - Variable in class lexfo.scalpel.editors.AbstractEditor
+
+
The HTTP request or response being edited.
+
+
_requestResponse - Variable in class lexfo.scalpel.ScalpelEditorTabbedPane
+
+
The HTTP request or response being edited.
+
+
+$ A B C D E F G H I J K L M N O P R S T U V W _ 
All Classes and Interfaces|All Packages|Constant Field Values|Serialized Form
+
+
+ + diff --git a/docs/public/javadoc/index.html b/docs/public/javadoc/index.html new file mode 100644 index 00000000..e1e6e113 --- /dev/null +++ b/docs/public/javadoc/index.html @@ -0,0 +1,69 @@ + + + + +Overview (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

scalpel 1.0.0 API

+
+
+
Packages
+
+
Package
+
Description
+ +
 
+ +
 
+ +
 
+
+
+
+
+
+ + diff --git a/docs/public/javadoc/jquery-ui.overrides.css b/docs/public/javadoc/jquery-ui.overrides.css new file mode 100644 index 00000000..03c010ba --- /dev/null +++ b/docs/public/javadoc/jquery-ui.overrides.css @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + /* Overrides the color of selection used in jQuery UI */ + background: #F8981D; + border: 1px solid #F8981D; +} diff --git a/docs/public/javadoc/legal/COPYRIGHT b/docs/public/javadoc/legal/COPYRIGHT new file mode 100644 index 00000000..945e19c1 --- /dev/null +++ b/docs/public/javadoc/legal/COPYRIGHT @@ -0,0 +1,69 @@ +Copyright © 1993, 2018, Oracle and/or its affiliates. +All rights reserved. + +This software and related documentation are provided under a +license agreement containing restrictions on use and +disclosure and are protected by intellectual property laws. +Except as expressly permitted in your license agreement or +allowed by law, you may not use, copy, reproduce, translate, +broadcast, modify, license, transmit, distribute, exhibit, +perform, publish, or display any part, in any form, or by +any means. Reverse engineering, disassembly, or +decompilation of this software, unless required by law for +interoperability, is prohibited. + +The information contained herein is subject to change +without notice and is not warranted to be error-free. If you +find any errors, please report them to us in writing. + +If this is software or related documentation that is +delivered to the U.S. Government or anyone licensing it on +behalf of the U.S. Government, the following notice is +applicable: + +U.S. GOVERNMENT END USERS: Oracle programs, including any +operating system, integrated software, any programs +installed on the hardware, and/or documentation, delivered +to U.S. Government end users are "commercial computer +software" pursuant to the applicable Federal Acquisition +Regulation and agency-specific supplemental regulations. As +such, use, duplication, disclosure, modification, and +adaptation of the programs, including any operating system, +integrated software, any programs installed on the hardware, +and/or documentation, shall be subject to license terms and +license restrictions applicable to the programs. No other +rights are granted to the U.S. Government. + +This software or hardware is developed for general use in a +variety of information management applications. It is not +developed or intended for use in any inherently dangerous +applications, including applications that may create a risk +of personal injury. If you use this software or hardware in +dangerous applications, then you shall be responsible to +take all appropriate fail-safe, backup, redundancy, and +other measures to ensure its safe use. Oracle Corporation +and its affiliates disclaim any liability for any damages +caused by use of this software or hardware in dangerous +applications. + +Oracle and Java are registered trademarks of Oracle and/or +its affiliates. Other names may be trademarks of their +respective owners. + +Intel and Intel Xeon are trademarks or registered trademarks +of Intel Corporation. All SPARC trademarks are used under +license and are trademarks or registered trademarks of SPARC +International, Inc. AMD, Opteron, the AMD logo, and the AMD +Opteron logo are trademarks or registered trademarks of +Advanced Micro Devices. UNIX is a registered trademark of +The Open Group. + +This software or hardware and documentation may provide +access to or information on content, products, and services +from third parties. Oracle Corporation and its affiliates +are not responsible for and expressly disclaim all +warranties of any kind with respect to third-party content, +products, and services. Oracle Corporation and its +affiliates will not be responsible for any loss, costs, or +damages incurred due to your access to or use of third-party +content, products, or services. diff --git a/docs/public/javadoc/legal/LICENSE b/docs/public/javadoc/legal/LICENSE new file mode 100644 index 00000000..ee860d38 --- /dev/null +++ b/docs/public/javadoc/legal/LICENSE @@ -0,0 +1,118 @@ +Your use of this Program is governed by the No-Fee Terms and Conditions set +forth below, unless you have received this Program (alone or as part of another +Oracle product) under an Oracle license agreement (including but not limited to +the Oracle Master Agreement), in which case your use of this Program is governed +solely by such license agreement with Oracle. + +Oracle No-Fee Terms and Conditions (NFTC) + +Definitions + +"Oracle" refers to Oracle America, Inc. "You" and "Your" refers to (a) a company +or organization (each an "Entity") accessing the Programs, if use of the +Programs will be on behalf of such Entity; or (b) an individual accessing the +Programs, if use of the Programs will not be on behalf of an Entity. +"Program(s)" refers to Oracle software provided by Oracle pursuant to the +following terms and any updates, error corrections, and/or Program Documentation +provided by Oracle. "Program Documentation" refers to Program user manuals and +Program installation manuals, if any. If available, Program Documentation may be +delivered with the Programs and/or may be accessed from +www.oracle.com/documentation. "Separate Terms" refers to separate license terms +that are specified in the Program Documentation, readmes or notice files and +that apply to Separately Licensed Technology. "Separately Licensed Technology" +refers to Oracle or third party technology that is licensed under Separate Terms +and not under the terms of this license. + +Separately Licensed Technology + +Oracle may provide certain notices to You in Program Documentation, readmes or +notice files in connection with Oracle or third party technology provided as or +with the Programs. If specified in the Program Documentation, readmes or notice +files, such technology will be licensed to You under Separate Terms. Your rights +to use Separately Licensed Technology under Separate Terms are not restricted in +any way by the terms herein. For clarity, notwithstanding the existence of a +notice, third party technology that is not Separately Licensed Technology shall +be deemed part of the Programs licensed to You under the terms of this license. + +Source Code for Open Source Software + +For software that You receive from Oracle in binary form that is licensed under +an open source license that gives You the right to receive the source code for +that binary, You can obtain a copy of the applicable source code from +https://oss.oracle.com/sources/ or http://www.oracle.com/goto/opensourcecode. If +the source code for such software was not provided to You with the binary, You +can also receive a copy of the source code on physical media by submitting a +written request pursuant to the instructions in the "Written Offer for Source +Code" section of the latter website. + +------------------------------------------------------------------------------- + +The following license terms apply to those Programs that are not provided to You +under Separate Terms. + +License Rights and Restrictions + +Oracle grants to You, as a recipient of this Program, subject to the conditions +stated herein, a nonexclusive, nontransferable, limited license to: + +(a) internally use the unmodified Programs for the purposes of developing, +testing, prototyping and demonstrating your applications, and running the +Program for Your own personal use or internal business operations; and + +(b) redistribute the unmodified Program and Program Documentation, under the +terms of this License, provided that You do not charge Your licensees any fees +associated with such distribution or use of the Program, including, without +limitation, fees for products that include or are bundled with a copy of the +Program or for services that involve the use of the distributed Program. + +You may make copies of the Programs to the extent reasonably necessary for +exercising the license rights granted herein and for backup purposes. You are +granted the right to use the Programs to provide third party training in the use +of the Programs and associated Separately Licensed Technology only if there is +express authorization of such use by Oracle on the Program's download page or in +the Program Documentation. + +Your license is contingent on compliance with the following conditions: + +- You do not remove markings or notices of either Oracle's or a licensor's + proprietary rights from the Programs or Program Documentation; + +- You comply with all U.S. and applicable export control and economic sanctions + laws and regulations that govern Your use of the Programs (including technical + data); + +- You do not cause or permit reverse engineering, disassembly or decompilation + of the Programs (except as allowed by law) by You nor allow an associated + party to do so. + +For clarity, any source code that may be included in the distribution with the +Programs is provided solely for reference purposes and may not be modified, +unless such source code is under Separate Terms permitting modification. + +Ownership + +Oracle or its licensors retain all ownership and intellectual property rights to +the Programs. + +Information Collection + +The Programs' installation and/or auto-update processes, if any, may transmit a +limited amount of data to Oracle or its service provider about those processes +to help Oracle understand and optimize them. Oracle does not associate the data +with personally identifiable information. Refer to Oracle's Privacy Policy at +www.oracle.com/privacy. + +Disclaimer of Warranties; Limitation of Liability + +THE PROGRAMS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. ORACLE FURTHER +DISCLAIMS ALL WARRANTIES, EXPRESS AND IMPLIED, INCLUDING WITHOUT LIMITATION, ANY +IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +NONINFRINGEMENT. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW WILL ORACLE BE LIABLE TO YOU FOR +DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT +LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. diff --git a/docs/public/javadoc/legal/jquery.md b/docs/public/javadoc/legal/jquery.md new file mode 100644 index 00000000..d468b318 --- /dev/null +++ b/docs/public/javadoc/legal/jquery.md @@ -0,0 +1,72 @@ +## jQuery v3.6.1 + +### jQuery License +``` +jQuery v 3.6.1 +Copyright OpenJS Foundation and other contributors, https://openjsf.org/ + +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 jQuery JavaScript Library v3.6.1 also includes Sizzle.js + +Sizzle.js includes the following license: + +Copyright JS Foundation and other contributors, https://js.foundation/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/sizzle + +The following license applies to all parts of this software except as +documented below: + +==== + +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. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + +********************* + +``` diff --git a/docs/public/javadoc/legal/jqueryUI.md b/docs/public/javadoc/legal/jqueryUI.md new file mode 100644 index 00000000..8bda9d7a --- /dev/null +++ b/docs/public/javadoc/legal/jqueryUI.md @@ -0,0 +1,49 @@ +## jQuery UI v1.13.2 + +### jQuery UI License +``` +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-ui + +The following license applies to all parts of this software except as +documented below: + +==== + +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. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code contained within the demos directory. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + +``` diff --git a/docs/public/javadoc/lexfo/scalpel/Async.html b/docs/public/javadoc/lexfo/scalpel/Async.html new file mode 100644 index 00000000..012abadf --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Async.html @@ -0,0 +1,186 @@ + + + + +Async (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Async

+
+
java.lang.Object +
lexfo.scalpel.Async
+
+
+
+
public class Async +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      executor

      +
      private static final Executor executor
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Async

      +
      public Async()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    + +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/CommandChecker.html b/docs/public/javadoc/lexfo/scalpel/CommandChecker.html new file mode 100644 index 00000000..4efc9f28 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/CommandChecker.html @@ -0,0 +1,176 @@ + + + + +CommandChecker (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class CommandChecker

+
+
java.lang.Object +
lexfo.scalpel.CommandChecker
+
+
+
+
public class CommandChecker +extends Object
+
Provides utilities to get default commands.
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      CommandChecker

      +
      public CommandChecker()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getAvailableCommand

      +
      public static String getAvailableCommand(String... commands)
      +
      +
    • +
    • +
      +

      extractBinary

      +
      private static String extractBinary(String command)
      +
      +
    • +
    • +
      +

      isCommandAvailable

      +
      private static boolean isCommandAvailable(String command)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Config._GlobalData.html b/docs/public/javadoc/lexfo/scalpel/Config._GlobalData.html new file mode 100644 index 00000000..36f539e0 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Config._GlobalData.html @@ -0,0 +1,235 @@ + + + + +Config._GlobalData (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Config._GlobalData

+
+
java.lang.Object +
lexfo.scalpel.Config._GlobalData
+
+
+
+
Enclosing class:
+
Config
+
+
+
private static class Config._GlobalData +extends Object
+
Global configuration. + + This is the configuration that is shared between all projects. + It contains the list of venvs and the default values. + + The default values are inferred from the user behavior. + For a new project, the default venv, script and framework paths are laste ones selected by the user in any different project. + If the user has never selected a venv, script or framework, the default values are set to default values.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      workspacePaths

      +
      public ArrayList<String> workspacePaths
      +
      List of registered venv paths.
      +
      +
    • +
    • +
      +

      defaultWorkspacePath

      +
      public String defaultWorkspacePath
      +
      +
    • +
    • +
      +

      defaultScriptPath

      +
      public String defaultScriptPath
      +
      +
    • +
    • +
      +

      jdkPath

      +
      public String jdkPath
      +
      +
    • +
    • +
      +

      logLevel

      +
      public String logLevel
      +
      +
    • +
    • +
      +

      openScriptCommand

      +
      public String openScriptCommand
      +
      +
    • +
    • +
      +

      editScriptCommand

      +
      public String editScriptCommand
      +
      +
    • +
    • +
      +

      openFolderCommand

      +
      public String openFolderCommand
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      _GlobalData

      +
      private _GlobalData()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Config._ProjectData.html b/docs/public/javadoc/lexfo/scalpel/Config._ProjectData.html new file mode 100644 index 00000000..97a809e7 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Config._ProjectData.html @@ -0,0 +1,179 @@ + + + + +Config._ProjectData (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Config._ProjectData

+
+
java.lang.Object +
lexfo.scalpel.Config._ProjectData
+
+
+
+
Enclosing class:
+
Config
+
+
+
private static class Config._ProjectData +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      workspacePath

      +
      public String workspacePath
      +
      +
    • +
    • +
      +

      userScriptPath

      +
      public String userScriptPath
      +
      +
    • +
    • +
      +

      displayProxyErrorPopup

      +
      public String displayProxyErrorPopup
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      _ProjectData

      +
      private _ProjectData()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Config.html b/docs/public/javadoc/lexfo/scalpel/Config.html new file mode 100644 index 00000000..a6a830f2 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Config.html @@ -0,0 +1,768 @@ + + + + +Config (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Config

+
+
java.lang.Object +
lexfo.scalpel.Config
+
+
+
+
public class Config +extends Object
+
Scalpel configuration. + + + By default, the project configuration file is located in the $HOME/.scalpel directory. + + The file name is the project id with the .json extension. + The project ID is an UUID stored in the extension data: + https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/persistence/Persistence.html#extensionData() + + The configuration file looks something like this: + { + "workspacePaths": [ + "/path/to/workspace1", + "/path/to/workspace2" + ], + "scriptPath": "/path/to/script.py", + } + + The file is not really designed to be directly edited by the user, but rather by the extension itself. + + A configuration file is needed because we need to store global persistent data arrays. (e.g. workspacePaths) + Which can't be done with the Java Preferences API. + Furthermore, it's simply more convenient to store as JSON and we already have a scalpel directory to store 'ad-hoc' python workspaces.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      globalConfig

      +
      private final Config._GlobalData globalConfig
      +
      +
    • +
    • +
      +

      projectConfig

      +
      private final Config._ProjectData projectConfig
      +
      +
    • +
    • +
      +

      lastModified

      +
      private long lastModified
      +
      +
    • +
    • +
      +

      CONFIG_EXT

      +
      private static final String CONFIG_EXT
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      projectID

      +
      public final String projectID
      +
      +
    • +
    • +
      +

      projectScalpelConfig

      +
      private final File projectScalpelConfig
      +
      +
    • +
    • +
      +

      DATA_PREFIX

      +
      private static final String DATA_PREFIX
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DATA_PROJECT_ID_KEY

      +
      private static final String DATA_PROJECT_ID_KEY
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      _jdkPath

      +
      private Path _jdkPath
      +
      +
    • +
    • +
      +

      instance

      +
      private static Config instance
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    + +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getInstance

      +
      private static Config getInstance(Optional<MontoyaApi> API)
      +
      Provides access to the singleton instance of the Config class.
      +
      +
      Returns:
      +
      The single instance of the Config class.
      +
      +
      +
    • +
    • +
      +

      getInstance

      +
      public static Config getInstance()
      +
      Provides access to the singleton instance of the Config class.
      +
      +
      Returns:
      +
      The single instance of the Config class.
      +
      +
      +
    • +
    • +
      +

      getInstance

      +
      public static Config getInstance(MontoyaApi API)
      +
      Provides access to the singleton instance of the Config class.
      +
      +
      Returns:
      +
      The single instance of the Config class.
      +
      +
      +
    • +
    • +
      +

      readConfigFile

      +
      private static <T> T readConfigFile(File file, + Class<T> clazz)
      +
      +
    • +
    • +
      +

      initGlobalConfig

      +
      private Config._GlobalData initGlobalConfig() + throws IOException
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      initProjectConfig

      +
      private Config._ProjectData initProjectConfig()
      +
      +
    • +
    • +
      +

      saveGlobalConfig

      +
      private void saveGlobalConfig()
      +
      Write the global configuration to the global configuration file.
      +
      +
    • +
    • +
      +

      saveProjectConfig

      +
      private void saveProjectConfig()
      +
      Write the project configuration to the project configuration file.
      +
      +
    • +
    • +
      +

      saveAllConfig

      +
      private void saveAllConfig()
      +
      Write the global and project configuration to their respective files.
      +
      +
    • +
    • +
      +

      getLastModified

      +
      public long getLastModified()
      +
      Get the last modification time of the project configuration file. + + This is used to reload the execution configuration when the project configuration file is modified.
      +
      +
      Returns:
      +
      The last modification time of the project configuration file.
      +
      +
      +
    • +
    • +
      +

      getGlobalConfigFile

      +
      public static File getGlobalConfigFile()
      +
      Get the global configuration file.
      +
      +
      Returns:
      +
      The global configuration file. (default: $HOME/.scalpel/global.json)
      +
      +
      +
    • +
    • +
      +

      hasIncludeDir

      +
      private static boolean hasIncludeDir(Path jdkPath)
      +
      +
    • +
    • +
      +

      guessJdkPath

      +
      private static Optional<Path> guessJdkPath() + throws IOException
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      findJdkPath

      +
      public Path findJdkPath() + throws IOException
      +
      Tries to get the JDK path from PATH, usual install locations, or by prompting the user.
      +
      +
      Returns:
      +
      The JDK path.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      findBinaryInPath

      +
      private static Stream<Path> findBinaryInPath(String binaryName)
      +
      +
    • +
    • +
      +

      getDefaultGlobalData

      +
      private Config._GlobalData getDefaultGlobalData() + throws IOException
      +
      Get the global configuration.
      +
      +
      Returns:
      +
      The global configuration.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getDefaultProjectData

      +
      private Config._ProjectData getDefaultProjectData()
      +
      Get the project configuration.
      +
      +
      Returns:
      +
      The project configuration.
      +
      +
      +
    • +
    • +
      +

      getVenvPaths

      +
      public String[] getVenvPaths()
      +
      +
    • +
    • +
      +

      getUserScriptPath

      +
      public Path getUserScriptPath()
      +
      +
    • +
    • +
      +

      getFrameworkPath

      +
      public Path getFrameworkPath()
      +
      +
    • +
    • +
      +

      getJdkPath

      +
      public Path getJdkPath()
      +
      +
    • +
    • +
      +

      getSelectedWorkspacePath

      +
      public Path getSelectedWorkspacePath()
      +
      +
    • +
    • +
      +

      setJdkPath

      +
      public void setJdkPath(Path path)
      +
      +
    • +
    • +
      +

      setVenvPaths

      +
      public void setVenvPaths(ArrayList<String> venvPaths)
      +
      +
    • +
    • +
      +

      setUserScriptPath

      +
      public void setUserScriptPath(Path scriptPath)
      +
      +
    • +
    • +
      +

      setSelectedVenvPath

      +
      public void setSelectedVenvPath(Path venvPath)
      +
      +
    • +
    • +
      +

      addVenvPath

      +
      public void addVenvPath(Path venvPath)
      +
      +
    • +
    • +
      +

      removeVenvPath

      +
      public void removeVenvPath(Path venvPath)
      +
      +
    • +
    • +
      +

      getLogLevel

      +
      public String getLogLevel()
      +
      +
    • +
    • +
      +

      setLogLevel

      +
      public void setLogLevel(String logLevel)
      +
      +
    • +
    • +
      +

      getOpenScriptCommand

      +
      public String getOpenScriptCommand()
      +
      +
    • +
    • +
      +

      setOpenScriptCommand

      +
      public void setOpenScriptCommand(String openScriptCommand)
      +
      +
    • +
    • +
      +

      getEditScriptCommand

      +
      public String getEditScriptCommand()
      +
      +
    • +
    • +
      +

      setEditScriptCommand

      +
      public void setEditScriptCommand(String editScriptCommand)
      +
      +
    • +
    • +
      +

      getOpenFolderCommand

      +
      public String getOpenFolderCommand()
      +
      +
    • +
    • +
      +

      setOpenFolderCommand

      +
      public void setOpenFolderCommand(String openFolderCommand)
      +
      +
    • +
    • +
      +

      getDisplayProxyErrorPopup

      +
      public String getDisplayProxyErrorPopup()
      +
      +
    • +
    • +
      +

      setDisplayProxyErrorPopup

      +
      public void setDisplayProxyErrorPopup(String displayProxyErrorPopup)
      +
      +
    • +
    • +
      +

      dumpConfig

      +
      public String dumpConfig()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ConfigTab.html b/docs/public/javadoc/lexfo/scalpel/ConfigTab.html new file mode 100644 index 00000000..35779f62 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ConfigTab.html @@ -0,0 +1,1007 @@ + + + + +ConfigTab (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ConfigTab

+
+ +
+
+
All Implemented Interfaces:
+
ImageObserver, MenuContainer, Serializable, Accessible, RootPaneContainer, WindowConstants
+
+
+
public class ConfigTab +extends JFrame
+
Burp tab handling Scalpel configuration + IntelliJ's GUI designer is needed to edit most components.
+
+
See Also:
+
+ +
+
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      rootPanel

      +
      private JPanel rootPanel
      +
      +
    • +
    • +
      +

      frameworkBrowseButton

      +
      private JButton frameworkBrowseButton
      +
      +
    • +
    • +
      +

      frameworkPathField

      +
      private JTextField frameworkPathField
      +
      +
    • +
    • +
      +

      browsePanel

      +
      private JPanel browsePanel
      +
      +
    • +
    • +
      +

      frameworkConfigPanel

      +
      private JPanel frameworkConfigPanel
      +
      +
    • +
    • +
      +

      frameworkPathTextArea

      +
      private JTextArea frameworkPathTextArea
      +
      +
    • +
    • +
      +

      scriptConfigPanel

      +
      private JPanel scriptConfigPanel
      +
      +
    • +
    • +
      +

      scriptBrowseButton

      +
      private JButton scriptBrowseButton
      +
      +
    • +
    • +
      +

      scriptPathTextArea

      +
      private JLabel scriptPathTextArea
      +
      +
    • +
    • +
      +

      terminalForVenvConfig

      +
      private com.jediterm.terminal.ui.JediTermWidget terminalForVenvConfig
      +
      +
    • +
    • +
      +

      venvListComponent

      +
      private JList<String> venvListComponent
      +
      +
    • +
    • +
      +

      packagesTable

      +
      private JTable packagesTable
      +
      +
    • +
    • +
      +

      addVentText

      +
      private PlaceholderTextField addVentText
      +
      +
    • +
    • +
      +

      addVenvButton

      +
      private JButton addVenvButton
      +
      +
    • +
    • +
      +

      venvSelectPanel

      +
      private JPanel venvSelectPanel
      +
      +
    • +
    • +
      +

      openScriptButton

      +
      private JButton openScriptButton
      +
      +
    • +
    • +
      +

      createButton

      +
      private JButton createButton
      +
      +
    • +
    • +
      +

      venvScriptList

      +
      private JList<String> venvScriptList
      +
      +
    • +
    • +
      +

      listPannel

      +
      private JPanel listPannel
      +
      +
    • +
    • +
      +

      openFolderButton

      +
      private JButton openFolderButton
      +
      +
    • +
    • +
      +

      scalpelIsENABLEDButton

      +
      private JButton scalpelIsENABLEDButton
      +
      +
    • +
    • +
      +

      stderrTextArea

      +
      private JTextArea stderrTextArea
      +
      +
    • +
    • +
      +

      stdoutTextArea

      +
      private JTextArea stdoutTextArea
      +
      +
    • +
    • +
      +

      helpTextPane

      +
      private JTextPane helpTextPane
      +
      +
    • +
    • +
      +

      outputTabPanel

      +
      private JPanel outputTabPanel
      +
      +
    • +
    • +
      +

      stdoutScrollPane

      +
      private JScrollPane stdoutScrollPane
      +
      +
    • +
    • +
      +

      stderrScrollPane

      +
      private JScrollPane stderrScrollPane
      +
      +
    • +
    • +
      +

      debugInfoTextPane

      +
      private JTextPane debugInfoTextPane
      +
      +
    • +
    • +
      +

      selectedScriptLabel

      +
      private JLabel selectedScriptLabel
      +
      +
    • +
    • +
      +

      resetTerminalButton

      +
      private JButton resetTerminalButton
      +
      +
    • +
    • +
      +

      copyToClipboardButton

      +
      private JButton copyToClipboardButton
      +
      +
    • +
    • +
      +

      settingsTab

      +
      private JPanel settingsTab
      +
      +
    • +
    • +
      +

      openIssueOnGitHubButton

      +
      private JButton openIssueOnGitHubButton
      +
      +
    • +
    • +
      +

      scalpelExecutor

      +
      private final transient ScalpelExecutor scalpelExecutor
      +
      +
    • +
    • +
      +

      config

      +
      private final transient Config config
      +
      +
    • +
    • +
      +

      API

      +
      private final transient MontoyaApi API
      +
      +
    • +
    • +
      +

      theme

      +
      private final Theme theme
      +
      +
    • +
    • +
      +

      burpFrame

      +
      private final Frame burpFrame
      +
      +
    • +
    • +
      +

      settingsPanel

      +
      private final SettingsPanel settingsPanel
      +
      +
    • +
    • +
      +

      instance

      +
      private static ConfigTab instance
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    + +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getInstance

      +
      public static ConfigTab getInstance()
      +
      +
    • +
    • +
      +

      setupVenvTab

      +
      private void setupVenvTab()
      +
      +
    • +
    • +
      +

      setupHelpTab

      +
      private void setupHelpTab()
      +
      +
    • +
    • +
      +

      setupLogsTab

      +
      private void setupLogsTab()
      +
      +
    • +
    • +
      +

      setupCopyButton

      +
      private void setupCopyButton()
      +
      +
    • +
    • +
      +

      setSettings

      +
      public void setSettings(Map<String,String> settings)
      +
      +
    • +
    • +
      +

      setupSettingsTab

      +
      private void setupSettingsTab()
      +
      +
    • +
    • +
      +

      setupGitHubIssueButton

      +
      private void setupGitHubIssueButton()
      +
      +
    • +
    • +
      +

      setupDebugInfoTab

      +
      private void setupDebugInfoTab()
      +
      +
    • +
    • +
      +

      appendToDebugInfo

      +
      public static void appendToDebugInfo(String info)
      +
      +
    • +
    • +
      +

      pushCharToOutput

      +
      public static void pushCharToOutput(int c, + boolean isStdout)
      +
      Push a character to a stdout or stderr text area.
      +
      +
      Parameters:
      +
      c - The character to push.
      +
      isStdout - Whether the character is from stdout or stderr.
      +
      +
      +
    • +
    • +
      +

      putStringToOutput

      +
      public static void putStringToOutput(String s, + boolean isStdout)
      +
      +
    • +
    • +
      +

      clearOutputs

      +
      public static void clearOutputs(String msg)
      +
      +
    • +
    • +
      +

      addListDoubleClickListener

      +
      private <T> void addListDoubleClickListener(JList<T> list, + Consumer<ListSelectionEvent> handler)
      +
      JList doesn't natively support double click events, so we implment it + ourselves.
      +
      +
      Type Parameters:
      +
      T -
      +
      Parameters:
      +
      list - The list to add the listener to.
      +
      handler - The listener handler callback.
      +
      +
      +
    • +
    • +
      +

      handleOpenScriptFolderButton

      +
      private void handleOpenScriptFolderButton()
      +
      +
    • +
    • +
      +

      handleScriptListSelectionEvent

      +
      private void handleScriptListSelectionEvent()
      +
      +
    • +
    • +
      +

      updateScriptList

      +
      private void updateScriptList()
      +
      +
    • +
    • +
      +

      selectScript

      +
      private void selectScript(Path path)
      +
      +
    • +
    • +
      +

      handleNewScriptButton

      +
      private void handleNewScriptButton()
      +
      +
    • +
    • +
      +

      openEditorInTerminal

      +
      private void openEditorInTerminal(Path fileToEdit)
      +
      Opens the script in a terminal editor +

      + Tries to use the EDITOR env var + Falls back to vi if EDITOR is missing

      +
      +
      Parameters:
      +
      fileToEdit -
      +
      +
      +
    • +
    • +
      +

      cmdFormat

      +
      private String cmdFormat(String fmt, + Object dir, + Object file)
      +
      +
    • +
    • +
      +

      handleOpenScriptButton

      +
      private void handleOpenScriptButton()
      +
      +
    • +
    • +
      +

      launchOpenScriptCommand

      +
      private void launchOpenScriptCommand(Path script)
      +
      +
    • +
    • +
      +

      handleEnableButton

      +
      private void handleEnableButton()
      +
      +
    • +
    • +
      +

      handleVenvButton

      +
      private void handleVenvButton()
      +
      +
    • +
    • +
      +

      updateTerminal

      +
      private void updateTerminal(String selectedVenvPath, + String cwd, + String cmd)
      +
      +
    • +
    • +
      +

      updateTerminal

      +
      private void updateTerminal(String selectedVenvPath)
      +
      +
    • +
    • +
      +

      handleVenvListSelectionEvent

      +
      private void handleVenvListSelectionEvent(ListSelectionEvent e)
      +
      +
    • +
    • +
      +

      handleBrowseButtonClick

      +
      private CompletableFuture<Void> handleBrowseButtonClick(Supplier<Path> getter, + Consumer<Path> setter)
      +
      +
    • +
    • +
      +

      updatePackagesTable

      +
      private CompletableFuture<Void> updatePackagesTable(Consumer<JTable> onSuccess, + Runnable onFail)
      +
      +
    • +
    • +
      +

      updatePackagesTable

      +
      private void updatePackagesTable(Consumer<JTable> onSuccess)
      +
      +
    • +
    • +
      +

      updatePackagesTable

      +
      private void updatePackagesTable()
      +
      +
    • +
    • +
      +

      setAndStoreScript

      +
      private void setAndStoreScript(Path path)
      +
      +
    • +
    • +
      +

      uiComponent

      +
      public Component uiComponent()
      +
      Returns the UI component to display.
      +
      +
      Returns:
      +
      the UI component to display
      +
      +
      +
    • +
    • +
      +

      createUIComponents

      +
      private void createUIComponents()
      +
      +
    • +
    • +
      +

      $$$setupUI$$$

      +
      private void $$$setupUI$$$()
      +
      Method generated by IntelliJ IDEA GUI Designer + >>> IMPORTANT!! <<< + DO NOT edit this method OR call it in your code!
      +
      +
    • +
    • +
      +

      $$$getRootComponent$$$

      +
      public JComponent $$$getRootComponent$$$()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Constants.html b/docs/public/javadoc/lexfo/scalpel/Constants.html new file mode 100644 index 00000000..d93deade --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Constants.html @@ -0,0 +1,755 @@ + + + + +Constants (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Constants

+
+
java.lang.Object +
lexfo.scalpel.Constants
+
+
+
+
public class Constants +extends Object
+
Contains constants used by the extension.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      REQ_EDIT_PREFIX

      +
      public static final String REQ_EDIT_PREFIX
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      FRAMEWORK_REQ_EDIT_PREFIX

      +
      public static final String FRAMEWORK_REQ_EDIT_PREFIX
      +
      Callback prefix for request editors.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      RES_EDIT_PREFIX

      +
      public static final String RES_EDIT_PREFIX
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      FRAMEWORK_RES_EDIT_PREFIX

      +
      public static final String FRAMEWORK_RES_EDIT_PREFIX
      +
      Callback prefix for response editors.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      IN_SUFFIX

      +
      public static final String IN_SUFFIX
      +
      Callback suffix for HttpMessage-to-bytes convertion.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      OUT_SUFFIX

      +
      public static final String OUT_SUFFIX
      +
      Callback suffix for bytes to HttpMessage convertion.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      REQ_CB_NAME

      +
      public static final String REQ_CB_NAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      FRAMEWORK_REQ_CB_NAME

      +
      public static final String FRAMEWORK_REQ_CB_NAME
      +
      Callback prefix for request intercepters.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      RES_CB_NAME

      +
      public static final String RES_CB_NAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      VALID_HOOK_PREFIXES

      +
      public static final com.google.common.collect.ImmutableSet<String> VALID_HOOK_PREFIXES
      +
      +
    • +
    • +
      +

      FRAMEWORK_RES_CB_NAME

      +
      public static final String FRAMEWORK_RES_CB_NAME
      +
      Callback prefix for response intercepters.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      PERSISTENCE_PREFIX

      +
      public static final String PERSISTENCE_PREFIX
      +
      Scalpel prefix for the persistence databases.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      PERSISTED_SCRIPT

      +
      public static final String PERSISTED_SCRIPT
      +
      Persistence key for the cached user script path.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      PERSISTED_FRAMEWORK

      +
      public static final String PERSISTED_FRAMEWORK
      +
      Persistence key for the cached framework path.
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      GET_CB_NAME

      +
      public static final String GET_CB_NAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_VENV_DEPENDENCIES

      +
      public static final String[] DEFAULT_VENV_DEPENDENCIES
      +
      Required python packages
      +
      +
    • +
    • +
      +

      PYTHON_DEPENDENCIES

      +
      public static final String[] PYTHON_DEPENDENCIES
      +
      +
    • +
    • +
      +

      VENV_LIB_DIR

      +
      public static final String VENV_LIB_DIR
      +
      Venv dir containing site-packages
      +
      +
    • +
    • +
      +

      NATIVE_LIBJEP_FILE

      +
      public static final String NATIVE_LIBJEP_FILE
      +
      JEP native library filename
      +
      +
    • +
    • +
      +

      PYTHON_BIN

      +
      public static final String PYTHON_BIN
      +
      Python 3 executable filename
      +
      +
    • +
    • +
      +

      PIP_BIN

      +
      public static final String PIP_BIN
      +
      +
    • +
    • +
      +

      VENV_BIN_DIR

      +
      public static final String VENV_BIN_DIR
      +
      +
    • +
    • +
      +

      DEFAULT_TERMINAL_EDITOR

      +
      public static final String DEFAULT_TERMINAL_EDITOR
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_WINDOWS_EDITOR

      +
      public static final String DEFAULT_WINDOWS_EDITOR
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      EDITOR_MODE_ANNOTATION_KEY

      +
      public static final String EDITOR_MODE_ANNOTATION_KEY
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      HEX_EDITOR_MODE

      +
      public static final String HEX_EDITOR_MODE
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      RAW_EDITOR_MODE

      +
      public static final String RAW_EDITOR_MODE
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_EDITOR_MODE

      +
      public static final String DEFAULT_EDITOR_MODE
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      MIN_SUPPORTED_PYTHON_VERSION

      +
      public static final int MIN_SUPPORTED_PYTHON_VERSION
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      PREFERRED_PYTHON_VERSION

      +
      public static final int PREFERRED_PYTHON_VERSION
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_UNIX_SHELL

      +
      public static final String DEFAULT_UNIX_SHELL
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_LINUX_TERM_EDIT_CMD

      +
      public static final String DEFAULT_LINUX_TERM_EDIT_CMD
      +
      +
    • +
    • +
      +

      DEFAULT_LINUX_OPEN_FILE_CMD

      +
      public static final String DEFAULT_LINUX_OPEN_FILE_CMD
      +
      +
    • +
    • +
      +

      DEFAULT_LINUX_OPEN_DIR_CMD

      +
      public static final String DEFAULT_LINUX_OPEN_DIR_CMD
      +
      +
    • +
    • +
      +

      DEFAULT_WINDOWS_TERM_EDIT_CMD

      +
      public static final String DEFAULT_WINDOWS_TERM_EDIT_CMD
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_WINDOWS_OPEN_FILE_CMD

      +
      public static final String DEFAULT_WINDOWS_OPEN_FILE_CMD
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_WINDOWS_OPEN_DIR_CMD

      +
      public static final String DEFAULT_WINDOWS_OPEN_DIR_CMD
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_TERM_EDIT_CMD

      +
      public static final String DEFAULT_TERM_EDIT_CMD
      +
      +
    • +
    • +
      +

      DEFAULT_OPEN_FILE_CMD

      +
      public static final String DEFAULT_OPEN_FILE_CMD
      +
      +
    • +
    • +
      +

      DEFAULT_OPEN_DIR_CMD

      +
      public static final String DEFAULT_OPEN_DIR_CMD
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Constants

      +
      public Constants()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/EditorType.html b/docs/public/javadoc/lexfo/scalpel/EditorType.html new file mode 100644 index 00000000..47c99abd --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/EditorType.html @@ -0,0 +1,252 @@ + + + + +EditorType (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Enum Class EditorType

+
+
java.lang.Object +
java.lang.Enum<EditorType> +
lexfo.scalpel.EditorType
+
+
+
+
+
All Implemented Interfaces:
+
Serializable, Comparable<EditorType>, Constable
+
+
+
public enum EditorType +extends Enum<EditorType>
+
Enum used by editors to identify themselves
+
+
+ +
+
+
    + +
  • +
    +

    Enum Constant Details

    +
      +
    • +
      +

      REQUEST

      +
      public static final EditorType REQUEST
      +
      Indicates an editor for an HTTP request.
      +
      +
    • +
    • +
      +

      RESPONSE

      +
      public static final EditorType RESPONSE
      +
      Indicates an editor for an HTTP response.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      EditorType

      +
      private EditorType()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      values

      +
      public static EditorType[] values()
      +
      Returns an array containing the constants of this enum class, in +the order they are declared.
      +
      +
      Returns:
      +
      an array containing the constants of this enum class, in the order they are declared
      +
      +
      +
    • +
    • +
      +

      valueOf

      +
      public static EditorType valueOf(String name)
      +
      Returns the enum constant of this class with the specified name. +The string must match exactly an identifier used to declare an +enum constant in this class. (Extraneous whitespace characters are +not permitted.)
      +
      +
      Parameters:
      +
      name - the name of the enum constant to be returned.
      +
      Returns:
      +
      the enum constant with the specified name
      +
      Throws:
      +
      IllegalArgumentException - if this enum class has no constant with the specified name
      +
      NullPointerException - if the argument is null
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/IO.IORunnable.html b/docs/public/javadoc/lexfo/scalpel/IO.IORunnable.html new file mode 100644 index 00000000..20d762ea --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/IO.IORunnable.html @@ -0,0 +1,141 @@ + + + + +IO.IORunnable (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Interface IO.IORunnable

+
+
+
+
Enclosing class:
+
IO
+
+
+
Functional Interface:
+
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
+
+
+
@FunctionalInterface +public static interface IO.IORunnable
+
+
+
    + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    +
    void
    +
    run()
    +
     
    +
    +
    +
    +
    +
  • +
+
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/IO.IOSupplier.html b/docs/public/javadoc/lexfo/scalpel/IO.IOSupplier.html new file mode 100644 index 00000000..985f7ada --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/IO.IOSupplier.html @@ -0,0 +1,141 @@ + + + + +IO.IOSupplier (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Interface IO.IOSupplier<T>

+
+
+
+
Enclosing class:
+
IO
+
+
+
Functional Interface:
+
This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
+
+
+
@FunctionalInterface +public static interface IO.IOSupplier<T>
+
+
+
    + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    + + +
     
    +
    +
    +
    +
    +
  • +
+
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/IO.html b/docs/public/javadoc/lexfo/scalpel/IO.html new file mode 100644 index 00000000..36b57daa --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/IO.html @@ -0,0 +1,300 @@ + + + + +IO (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class IO

+
+
java.lang.Object +
lexfo.scalpel.IO
+
+
+
+
public class IO +extends Object
+
Utilities to perform IO utilities conveniently
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      mapper

      +
      private static final com.fasterxml.jackson.databind.ObjectMapper mapper
      +
      +
    • +
    • +
      +

      writer

      +
      private static final com.fasterxml.jackson.databind.ObjectWriter writer
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      IO

      +
      public IO()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      ioWrap

      +
      public static final <T> T ioWrap(IO.IOSupplier<T> supplier)
      +
      +
    • +
    • +
      +

      run

      +
      public static final void run(IO.IORunnable supplier)
      +
      +
    • +
    • +
      +

      ioWrap

      +
      public static final <T> T ioWrap(IO.IOSupplier<T> supplier, + com.google.common.base.Supplier<T> defaultSupplier)
      +
      +
    • +
    • +
      +

      readJSON

      +
      public static <T> T readJSON(File file, + Class<T> clazz)
      +
      +
    • +
    • +
      +

      readJSON

      +
      public static <T> T readJSON(File file, + Class<T> clazz, + Consumer<IOException> errorHandler)
      +
      +
    • +
    • +
      +

      readJSON

      +
      public static <T> T readJSON(String value, + Class<T> clazz)
      +
      +
    • +
    • +
      +

      writeJSON

      +
      public static void writeJSON(File file, + Object obj)
      +
      +
    • +
    • +
      +

      writeFile

      +
      public static void writeFile(String path, + String content)
      +
      +
    • +
    • +
      +

      sleep

      +
      public static void sleep(Integer ms)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Palette.html b/docs/public/javadoc/lexfo/scalpel/Palette.html new file mode 100644 index 00000000..486c070b --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Palette.html @@ -0,0 +1,266 @@ + + + + +Palette (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Palette

+
+
java.lang.Object +
com.jediterm.terminal.emulator.ColorPalette +
lexfo.scalpel.Palette
+
+
+
+
+
public class Palette +extends com.jediterm.terminal.emulator.ColorPalette
+
Color palette for the embedded terminal + Contains colors for both light and dark theme
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      DARK_COLORS

      +
      private static final Color[] DARK_COLORS
      +
      +
    • +
    • +
      +

      DARK_PALETTE

      +
      public static final com.jediterm.terminal.emulator.ColorPalette DARK_PALETTE
      +
      +
    • +
    • +
      +

      LIGHT_COLORS

      +
      private static final Color[] LIGHT_COLORS
      +
      +
    • +
    • +
      +

      LIGHT_PALETTE

      +
      public static final com.jediterm.terminal.emulator.ColorPalette LIGHT_PALETTE
      +
      +
    • +
    • +
      +

      WINDOWS_COLORS

      +
      private static final Color[] WINDOWS_COLORS
      +
      +
    • +
    • +
      +

      WINDOWS_PALETTE

      +
      public static final com.jediterm.terminal.emulator.ColorPalette WINDOWS_PALETTE
      +
      +
    • +
    • +
      +

      myColors

      +
      private final Color[] myColors
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Palette

      +
      private Palette(Color[] colors)
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getForegroundByColorIndex

      +
      public Color getForegroundByColorIndex(int colorIndex)
      +
      +
      Specified by:
      +
      getForegroundByColorIndex in class com.jediterm.terminal.emulator.ColorPalette
      +
      +
      +
    • +
    • +
      +

      getBackgroundByColorIndex

      +
      protected Color getBackgroundByColorIndex(int colorIndex)
      +
      +
      Specified by:
      +
      getBackgroundByColorIndex in class com.jediterm.terminal.emulator.ColorPalette
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/PythonSetup.html b/docs/public/javadoc/lexfo/scalpel/PythonSetup.html new file mode 100644 index 00000000..d6b88355 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/PythonSetup.html @@ -0,0 +1,185 @@ + + + + +PythonSetup (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class PythonSetup

+
+
java.lang.Object +
lexfo.scalpel.PythonSetup
+
+
+
+
public class PythonSetup +extends Object
+
Utilities to initialize Java Embedded Python (jep)
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      PythonSetup

      +
      public PythonSetup()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      loadLibPython3

      +
      public static void loadLibPython3()
      +
      +
    • +
    • +
      +

      getPythonVersion

      +
      public static int getPythonVersion()
      +
      +
    • +
    • +
      +

      getUsedPythonBin

      +
      public static String getUsedPythonBin()
      +
      +
    • +
    • +
      +

      executePythonCommand

      +
      public static String executePythonCommand(String command)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/PythonUtils.html b/docs/public/javadoc/lexfo/scalpel/PythonUtils.html new file mode 100644 index 00000000..f07596e4 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/PythonUtils.html @@ -0,0 +1,251 @@ + + + + +PythonUtils (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class PythonUtils

+
+
java.lang.Object +
lexfo.scalpel.PythonUtils
+
+
+
+
public class PythonUtils +extends Object
+
Utility class for Python scripts.
+
+
+
    + +
  • +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Constructor
    +
    Description
    + +
     
    +
    +
    +
  • + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    +
    static ByteArray
    +
    toByteArray(byte[] pythonBytes)
    +
    +
    Convert Python bytes to a Burp ByteArray
    +
    +
    static byte[]
    +
    toJavaBytes(byte[] pythonBytes)
    +
    +
    Convert Python bytes to Java bytes + + It is not possible to explicitely convert to Java bytes Python side without a Java helper like this one, + because Jep doesn't natively support the convertion: + https://github.com/ninia/jep/wiki/How-Jep-Works#objects + + When returning byte[], + Python receives a PyJArray of integer-like objects which will be mapped back to byte[] by Jep.
    +
    +
    static int[]
    +
    toPythonBytes(byte[] javaBytes)
    +
    +
    Convert Java signed bytes to corresponding unsigned values + Convertions issues occur when passing Java bytes to Python because Java's are signed and Python's are unsigned.
    +
    +
    static <T extends HttpMessage>
    T
    +
    updateHeader(T msg, + String name, + String value)
    +
    +
    Updates the specified HttpMessage object's header with the specified name and value.
    +
    +
    +
    +
    +
    +

    Methods inherited from class java.lang.Object

    +clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    +
    +
  • +
+
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      PythonUtils

      +
      public PythonUtils()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      toPythonBytes

      +
      public static int[] toPythonBytes(byte[] javaBytes)
      +
      Convert Java signed bytes to corresponding unsigned values + Convertions issues occur when passing Java bytes to Python because Java's are signed and Python's are unsigned. + Passing an unsigned int array solves this problem.
      +
      +
      Parameters:
      +
      javaBytes - the bytes to convert
      +
      Returns:
      +
      the corresponding unsigned values as int
      +
      +
      +
    • +
    • +
      +

      toJavaBytes

      +
      public static byte[] toJavaBytes(byte[] pythonBytes)
      +
      Convert Python bytes to Java bytes + + It is not possible to explicitely convert to Java bytes Python side without a Java helper like this one, + because Jep doesn't natively support the convertion: + https://github.com/ninia/jep/wiki/How-Jep-Works#objects + + When returning byte[], + Python receives a PyJArray of integer-like objects which will be mapped back to byte[] by Jep. + + Some errors this solves are for example when there is both an overload for byte[] and int[] and Jep chooses the wrong one. + This can be used to avoid type errors by avoding Jep's conversion by passing a native Java object.
      +
      +
      Parameters:
      +
      pythonBytes - the unsigned values to convert
      +
      Returns:
      +
      the corresponding signed bytes
      +
      +
      +
    • +
    • +
      +

      toByteArray

      +
      public static ByteArray toByteArray(byte[] pythonBytes)
      +
      Convert Python bytes to a Burp ByteArray
      +
      +
      Parameters:
      +
      pythonBytes - the unsigned values to convert
      +
      Returns:
      +
      the corresponding Burp ByteArray
      +
      +
      +
    • +
    • +
      +

      updateHeader

      +
      public static <T extends HttpMessage> T updateHeader(T msg, + String name, + String value)
      +
      Updates the specified HttpMessage object's header with the specified name and value. + Creates the header when it doesn't exist. +

      (Burp's withUpdatedHeader() method does not create the header.)

      +
      +
      Type Parameters:
      +
      T - The type of the HttpMessage object.
      +
      Parameters:
      +
      msg - The HttpMessage object to update.
      +
      name - The name of the header to update.
      +
      value - The value of the header to update.
      +
      Returns:
      +
      The updated HttpMessage object.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/RessourcesUnpacker.html b/docs/public/javadoc/lexfo/scalpel/RessourcesUnpacker.html new file mode 100644 index 00000000..a5bdf480 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/RessourcesUnpacker.html @@ -0,0 +1,436 @@ + + + + +RessourcesUnpacker (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class RessourcesUnpacker

+
+
java.lang.Object +
lexfo.scalpel.RessourcesUnpacker
+
+
+
+
public class RessourcesUnpacker +extends Object
+
Provides methods for unpacking the Scalpel resources.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      DATA_DIRNAME

      +
      public static final String DATA_DIRNAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DATA_DIR_PATH

      +
      public static final Path DATA_DIR_PATH
      +
      +
    • +
    • +
      +

      RESSOURCES_DIRNAME

      +
      public static final String RESSOURCES_DIRNAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      PYTHON_DIRNAME

      +
      public static final String PYTHON_DIRNAME
      +
      +
    • +
    • +
      +

      SHELL_DIRNAME

      +
      public static final String SHELL_DIRNAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      TEMPLATES_DIRNAME

      +
      public static final String TEMPLATES_DIRNAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      WORKSPACE_DIRNAME

      +
      public static final String WORKSPACE_DIRNAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      RESSOURCES_TO_COPY

      +
      private static final Set<String> RESSOURCES_TO_COPY
      +
      +
    • +
    • +
      +

      SAMPLES_DIRNAME

      +
      public static final String SAMPLES_DIRNAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      DEFAULT_SCRIPT_FILENAME

      +
      public static final String DEFAULT_SCRIPT_FILENAME
      +
      +
      See Also:
      +
      + +
      +
      +
      +
    • +
    • +
      +

      RESSOURCES_PATH

      +
      public static final Path RESSOURCES_PATH
      +
      +
    • +
    • +
      +

      PYTHON_PATH

      +
      public static final Path PYTHON_PATH
      +
      +
    • +
    • +
      +

      WORKSPACE_PATH

      +
      public static final Path WORKSPACE_PATH
      +
      +
    • +
    • +
      +

      PYSCALPEL_PATH

      +
      public static final Path PYSCALPEL_PATH
      +
      +
    • +
    • +
      +

      FRAMEWORK_PATH

      +
      public static final Path FRAMEWORK_PATH
      +
      +
    • +
    • +
      +

      SAMPLES_PATH

      +
      public static final Path SAMPLES_PATH
      +
      +
    • +
    • +
      +

      DEFAULT_SCRIPT_PATH

      +
      public static final Path DEFAULT_SCRIPT_PATH
      +
      +
    • +
    • +
      +

      BASH_INIT_FILE_PATH

      +
      public static final Path BASH_INIT_FILE_PATH
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      RessourcesUnpacker

      +
      public RessourcesUnpacker()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getRunningJarPath

      +
      private static String getRunningJarPath()
      +
      Returns the path to the Scalpel JAR file.
      +
      +
      Returns:
      +
      The path to the Scalpel JAR file.
      +
      +
      +
    • +
    • +
      +

      extractRessources

      +
      private static void extractRessources(String zipFile, + String extractFolder, + Set<String> entriesWhitelist)
      +
      Extracts the Scalpel python resources from the Scalpel JAR file.
      +
      +
      Parameters:
      +
      zipFile - The path to the Scalpel JAR file.
      +
      extractFolder - The path to the Scalpel resources directory.
      +
      +
      +
    • +
    • +
      +

      extractRessourcesToHome

      +
      public static void extractRessourcesToHome()
      +
      Initializes the Scalpel resources directory.
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Result.html b/docs/public/javadoc/lexfo/scalpel/Result.html new file mode 100644 index 00000000..4bcd9854 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Result.html @@ -0,0 +1,372 @@ + + + + +Result (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Result<T,E extends Throwable>

+
+
java.lang.Object +
lexfo.scalpel.Result<T,E>
+
+
+
+
public class Result<T,E extends Throwable> +extends Object
+
Optional-style class for handling python task results + + A completed python task can have multiple outcomes: + - The task completes successfully and returns a value + - The task completes successfully but returns no value + - The task throws an exception + + Result allows us to handle returned values and errors uniformly to handle them when needed.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      value

      +
      private final T value
      +
      +
    • +
    • +
      +

      error

      +
      private final E extends Throwable error
      +
      +
    • +
    • +
      +

      isEmpty

      +
      private final boolean isEmpty
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Result

      +
      private Result(T value, + E error, + boolean isEmpty)
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      success

      +
      public static <T, +E extends Throwable> Result<T,E> success(T value)
      +
      +
    • +
    • +
      +

      empty

      +
      public static <T, +E extends Throwable> Result<T,E> empty()
      +
      +
    • +
    • +
      +

      error

      +
      public static <T, +E extends Throwable> Result<T,E> error(E error)
      +
      +
    • +
    • +
      +

      isSuccess

      +
      public boolean isSuccess()
      +
      +
    • +
    • +
      +

      hasValue

      +
      public boolean hasValue()
      +
      +
    • +
    • +
      +

      getValue

      +
      public T getValue()
      +
      +
    • +
    • +
      +

      getError

      +
      public E getError()
      +
      +
    • +
    • +
      +

      isEmpty

      +
      public boolean isEmpty()
      +
      +
    • +
    • +
      +

      toString

      +
      public String toString()
      +
      +
      Overrides:
      +
      toString in class Object
      +
      +
      +
    • +
    • +
      +

      map

      +
      public <U> Result<U,E> map(Function<? super T,? extends U> mapper)
      +
      +
    • +
    • +
      +

      flatMap

      +
      public <U> Result<U,E> flatMap(Function<? super T,Result<U,E>> mapper)
      +
      +
    • +
    • +
      +

      or

      +
      public Result<T,E> or(Result<T,E> other)
      +
      +
    • +
    • +
      +

      orElse

      +
      public T orElse(T other)
      +
      +
    • +
    • +
      +

      orElseGet

      +
      public T orElseGet(Supplier<? extends T> other)
      +
      +
    • +
    • +
      +

      ifSuccess

      +
      public void ifSuccess(Consumer<T> action)
      +
      +
    • +
    • +
      +

      ifError

      +
      public void ifError(Consumer<E> action)
      +
      +
    • +
    • +
      +

      ifEmpty

      +
      public void ifEmpty(Runnable action)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Scalpel.html b/docs/public/javadoc/lexfo/scalpel/Scalpel.html new file mode 100644 index 00000000..eb8dcb5f --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Scalpel.html @@ -0,0 +1,262 @@ + + + + +Scalpel (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Scalpel

+
+
java.lang.Object +
lexfo.scalpel.Scalpel
+
+
+
+
All Implemented Interfaces:
+
BurpExtension
+
+
+
public class Scalpel +extends Object +implements BurpExtension
+
The main class of the extension. + This class is instantiated by Burp Suite and is used to initialize the extension.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      executor

      +
      private ScalpelExecutor executor
      +
      The ScalpelExecutor object used to execute Python scripts.
      +
      +
    • +
    • +
      +

      API

      +
      private MontoyaApi API
      +
      The MontoyaApi object used to interact with Burp Suite.
      +
      +
    • +
    • +
      +

      config

      +
      private Config config
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Scalpel

      +
      public Scalpel()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    + +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelEditorProvider.html b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorProvider.html new file mode 100644 index 00000000..96b7eb4e --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorProvider.html @@ -0,0 +1,285 @@ + + + + +ScalpelEditorProvider (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelEditorProvider

+
+
java.lang.Object +
lexfo.scalpel.ScalpelEditorProvider
+
+
+
+
All Implemented Interfaces:
+
HttpRequestEditorProvider, HttpResponseEditorProvider
+
+
+
public class ScalpelEditorProvider +extends Object +implements HttpRequestEditorProvider, HttpResponseEditorProvider
+
Provides a new ScalpelProvidedEditor object for editing HTTP requests or responses. +

Calls Python scripts to initialize the editor and update the requests or responses.

+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    + +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ScalpelEditorProvider

      +
      public ScalpelEditorProvider(MontoyaApi API, + ScalpelExecutor executor)
      +
      Constructs a new ScalpelEditorProvider object with the specified MontoyaApi object and ScalpelExecutor object.
      +
      +
      Parameters:
      +
      API - The MontoyaApi object to use.
      +
      executor - The ScalpelExecutor object to use.
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      provideHttpRequestEditor

      +
      public ExtensionProvidedHttpRequestEditor provideHttpRequestEditor(EditorCreationContext creationContext)
      +
      Provides a new ExtensionProvidedHttpRequestEditor object for editing an HTTP request.
      +
      +
      Specified by:
      +
      provideHttpRequestEditor in interface HttpRequestEditorProvider
      +
      Parameters:
      +
      creationContext - The EditorCreationContext object containing information about the request editor.
      +
      Returns:
      +
      A new ScalpelProvidedEditor object for editing the HTTP request.
      +
      +
      +
    • +
    • +
      +

      provideHttpResponseEditor

      +
      public ExtensionProvidedHttpResponseEditor provideHttpResponseEditor(EditorCreationContext creationContext)
      +
      Provides a new ExtensionProvidedHttpResponseEditor object for editing an HTTP response.
      +
      +
      Specified by:
      +
      provideHttpResponseEditor in interface HttpResponseEditorProvider
      +
      Parameters:
      +
      creationContext - The EditorCreationContext object containing information about the response editor.
      +
      Returns:
      +
      A new ScalpelProvidedEditor object for editing the HTTP response.
      +
      +
      +
    • +
    • +
      +

      forceGarbageCollection

      +
      private void forceGarbageCollection()
      +
      +
    • +
    • +
      +

      resetEditors

      +
      public void resetEditors()
      +
      +
    • +
    • +
      +

      resetEditorsAsync

      +
      public CompletableFuture<Void> resetEditorsAsync()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.HookTabInfo.html b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.HookTabInfo.html new file mode 100644 index 00000000..84b23e3e --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.HookTabInfo.html @@ -0,0 +1,331 @@ + + + + +ScalpelEditorTabbedPane.HookTabInfo (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Record Class ScalpelEditorTabbedPane.HookTabInfo

+
+
java.lang.Object +
java.lang.Record +
lexfo.scalpel.ScalpelEditorTabbedPane.HookTabInfo
+
+
+
+
+
Enclosing class:
+
ScalpelEditorTabbedPane
+
+
+
private static record ScalpelEditorTabbedPane.HookTabInfo(String name, String mode, Set<String> directions) +extends Record
+
This stores all the informations required to create a tab. + .directions contains the whole prefix and not just "in" or "out"
+
+
+
    + +
  • +
    +

    Field Summary

    +
    Fields
    +
    +
    Modifier and Type
    +
    Field
    +
    Description
    +
    private final Set<String>
    + +
    +
    The field for the directions record component.
    +
    +
    private final String
    + +
    +
    The field for the mode record component.
    +
    +
    private final String
    + +
    +
    The field for the name record component.
    +
    +
    +
    +
  • + +
  • +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Modifier
    +
    Constructor
    +
    Description
    +
    private
    +
    HookTabInfo(String name, + String mode, + Set<String> directions)
    +
    +
    Creates an instance of a HookTabInfo record class.
    +
    +
    +
    +
  • + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    + + +
    +
    Returns the value of the directions record component.
    +
    +
    final boolean
    + +
    +
    Indicates whether some other object is "equal to" this one.
    +
    +
    final int
    + +
    +
    Returns a hash code value for this object.
    +
    + + +
    +
    Returns the value of the mode record component.
    +
    + + +
    +
    Returns the value of the name record component.
    +
    +
    final String
    + +
    +
    Returns a string representation of this record class.
    +
    +
    +
    +
    +
    +

    Methods inherited from class java.lang.Object

    +clone, finalize, getClass, notify, notifyAll, wait, wait, wait
    +
    +
  • +
+
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      name

      +
      private final String name
      +
      The field for the name record component.
      +
      +
    • +
    • +
      +

      mode

      +
      private final String mode
      +
      The field for the mode record component.
      +
      +
    • +
    • +
      +

      directions

      +
      private final Set<String> directions
      +
      The field for the directions record component.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      HookTabInfo

      +
      private HookTabInfo(String name, + String mode, + Set<String> directions)
      +
      Creates an instance of a HookTabInfo record class.
      +
      +
      Parameters:
      +
      name - the value for the name record component
      +
      mode - the value for the mode record component
      +
      directions - the value for the directions record component
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      toString

      +
      public final String toString()
      +
      Returns a string representation of this record class. The representation contains the name of the class, followed by the name and value of each of the record components.
      +
      +
      Specified by:
      +
      toString in class Record
      +
      Returns:
      +
      a string representation of this object
      +
      +
      +
    • +
    • +
      +

      hashCode

      +
      public final int hashCode()
      +
      Returns a hash code value for this object. The value is derived from the hash code of each of the record components.
      +
      +
      Specified by:
      +
      hashCode in class Record
      +
      Returns:
      +
      a hash code value for this object
      +
      +
      +
    • +
    • +
      +

      equals

      +
      public final boolean equals(Object o)
      +
      Indicates whether some other object is "equal to" this one. The objects are equal if the other object is of the same class and if all the record components are equal. All components in this record class are compared with Objects::equals(Object,Object).
      +
      +
      Specified by:
      +
      equals in class Record
      +
      Parameters:
      +
      o - the object with which to compare
      +
      Returns:
      +
      true if this object is the same as the o argument; false otherwise.
      +
      +
      +
    • +
    • +
      +

      name

      +
      public String name()
      +
      Returns the value of the name record component.
      +
      +
      Returns:
      +
      the value of the name record component
      +
      +
      +
    • +
    • +
      +

      mode

      +
      public String mode()
      +
      Returns the value of the mode record component.
      +
      +
      Returns:
      +
      the value of the mode record component
      +
      +
      +
    • +
    • +
      +

      directions

      +
      public Set<String> directions()
      +
      Returns the value of the directions record component.
      +
      +
      Returns:
      +
      the value of the directions record component
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.PartialHookTabInfo.html b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.PartialHookTabInfo.html new file mode 100644 index 00000000..09969038 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.PartialHookTabInfo.html @@ -0,0 +1,333 @@ + + + + +ScalpelEditorTabbedPane.PartialHookTabInfo (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Record Class ScalpelEditorTabbedPane.PartialHookTabInfo

+
+
java.lang.Object +
java.lang.Record +
lexfo.scalpel.ScalpelEditorTabbedPane.PartialHookTabInfo
+
+
+
+
+
Enclosing class:
+
ScalpelEditorTabbedPane
+
+
+
private static record ScalpelEditorTabbedPane.PartialHookTabInfo(String name, String mode, String direction) +extends Record
+
A tab can be associated with at most two hooks + (e.g req_edit_in and req_edit_out) + + This stores the informations related to only one hook and is later merged with the second hook information into a HookTabInfo
+
+
+
    + +
  • +
    +

    Field Summary

    +
    Fields
    +
    +
    Modifier and Type
    +
    Field
    +
    Description
    +
    private final String
    + +
    +
    The field for the direction record component.
    +
    +
    private final String
    + +
    +
    The field for the mode record component.
    +
    +
    private final String
    + +
    +
    The field for the name record component.
    +
    +
    +
    +
  • + +
  • +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Modifier
    +
    Constructor
    +
    Description
    +
    private
    +
    PartialHookTabInfo(String name, + String mode, + String direction)
    +
    +
    Creates an instance of a PartialHookTabInfo record class.
    +
    +
    +
    +
  • + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    + + +
    +
    Returns the value of the direction record component.
    +
    +
    final boolean
    + +
    +
    Indicates whether some other object is "equal to" this one.
    +
    +
    final int
    + +
    +
    Returns a hash code value for this object.
    +
    + + +
    +
    Returns the value of the mode record component.
    +
    + + +
    +
    Returns the value of the name record component.
    +
    +
    final String
    + +
    +
    Returns a string representation of this record class.
    +
    +
    +
    +
    +
    +

    Methods inherited from class java.lang.Object

    +clone, finalize, getClass, notify, notifyAll, wait, wait, wait
    +
    +
  • +
+
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      name

      +
      private final String name
      +
      The field for the name record component.
      +
      +
    • +
    • +
      +

      mode

      +
      private final String mode
      +
      The field for the mode record component.
      +
      +
    • +
    • +
      +

      direction

      +
      private final String direction
      +
      The field for the direction record component.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      PartialHookTabInfo

      +
      private PartialHookTabInfo(String name, + String mode, + String direction)
      +
      Creates an instance of a PartialHookTabInfo record class.
      +
      +
      Parameters:
      +
      name - the value for the name record component
      +
      mode - the value for the mode record component
      +
      direction - the value for the direction record component
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      toString

      +
      public final String toString()
      +
      Returns a string representation of this record class. The representation contains the name of the class, followed by the name and value of each of the record components.
      +
      +
      Specified by:
      +
      toString in class Record
      +
      Returns:
      +
      a string representation of this object
      +
      +
      +
    • +
    • +
      +

      hashCode

      +
      public final int hashCode()
      +
      Returns a hash code value for this object. The value is derived from the hash code of each of the record components.
      +
      +
      Specified by:
      +
      hashCode in class Record
      +
      Returns:
      +
      a hash code value for this object
      +
      +
      +
    • +
    • +
      +

      equals

      +
      public final boolean equals(Object o)
      +
      Indicates whether some other object is "equal to" this one. The objects are equal if the other object is of the same class and if all the record components are equal. All components in this record class are compared with Objects::equals(Object,Object).
      +
      +
      Specified by:
      +
      equals in class Record
      +
      Parameters:
      +
      o - the object with which to compare
      +
      Returns:
      +
      true if this object is the same as the o argument; false otherwise.
      +
      +
      +
    • +
    • +
      +

      name

      +
      public String name()
      +
      Returns the value of the name record component.
      +
      +
      Returns:
      +
      the value of the name record component
      +
      +
      +
    • +
    • +
      +

      mode

      +
      public String mode()
      +
      Returns the value of the mode record component.
      +
      +
      Returns:
      +
      the value of the mode record component
      +
      +
      +
    • +
    • +
      +

      direction

      +
      public String direction()
      +
      Returns the value of the direction record component.
      +
      +
      Returns:
      +
      the value of the direction record component
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.html b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.html new file mode 100644 index 00000000..f94bfae4 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelEditorTabbedPane.html @@ -0,0 +1,830 @@ + + + + +ScalpelEditorTabbedPane (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelEditorTabbedPane

+
+
java.lang.Object +
lexfo.scalpel.ScalpelEditorTabbedPane
+
+
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor
+
+
+
public class ScalpelEditorTabbedPane +extends Object +implements ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor
+
Provides an UI text editor component for editing HTTP requests or responses. + Calls Python scripts to initialize the editor and update the requests or responses.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      pane

      +
      private final JTabbedPane pane
      +
      The editor swing UI component.
      +
      +
    • +
    • +
      +

      _requestResponse

      +
      private HttpRequestResponse _requestResponse
      +
      The HTTP request or response being edited.
      +
      +
    • +
    • +
      +

      API

      +
      private final MontoyaApi API
      +
      The Montoya API object.
      +
      +
    • +
    • +
      +

      ctx

      +
      private final EditorCreationContext ctx
      +
      The editor creation context.
      +
      +
    • +
    • +
      +

      type

      +
      private final EditorType type
      +
      The editor type (REQUEST or RESPONSE).
      +
      +
    • +
    • +
      +

      id

      +
      private final String id
      +
      The editor ID. (unused)
      +
      +
    • +
    • +
      +

      provider

      +
      private final ScalpelEditorProvider provider
      +
      The editor provider that instantiated this editor. (unused)
      +
      +
    • +
    • +
      +

      executor

      +
      private final ScalpelExecutor executor
      +
      The executor responsible for interacting with Python.
      +
      +
    • +
    • +
      +

      editors

      +
      private final ArrayList<IMessageEditor> editors
      +
      +
    • +
    • +
      +

      hookPrefix

      +
      private final String hookPrefix
      +
      req_edit_ or res_edit
      +
      +
    • +
    • +
      +

      hookInPrefix

      +
      private final String hookInPrefix
      +
      req_edit_in_ or res_edit_in_
      +
      +
    • +
    • +
      +

      hookOutPrefix

      +
      private final String hookOutPrefix
      +
      req_edit_out_ or res_edit_out_
      +
      +
    • +
    • +
      +

      modeToEditorMap

      +
      public static final Map<String,Class<? extends AbstractEditor>> modeToEditorMap
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ScalpelEditorTabbedPane

      +
      ScalpelEditorTabbedPane(MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorProvider provider, + ScalpelExecutor executor)
      +
      Constructs a new Scalpel editor.
      +
      +
      Parameters:
      +
      API - The Montoya API object.
      +
      creationContext - The EditorCreationContext object containing information about the editor.
      +
      type - The editor type (REQUEST or RESPONSE).
      +
      provider - The ScalpelEditorProvider object that instantiated this editor.
      +
      executor - The executor to use.
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    + +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.CallableData.html b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.CallableData.html new file mode 100644 index 00000000..b872011c --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.CallableData.html @@ -0,0 +1,296 @@ + + + + +ScalpelExecutor.CallableData (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Record Class ScalpelExecutor.CallableData

+
+
java.lang.Object +
java.lang.Record +
lexfo.scalpel.ScalpelExecutor.CallableData
+
+
+
+
+
Enclosing class:
+
ScalpelExecutor
+
+
+
public static record ScalpelExecutor.CallableData(String name, HashMap<String,String> annotations) +extends Record
+
+
+
    + +
  • +
    +

    Field Summary

    +
    Fields
    +
    +
    Modifier and Type
    +
    Field
    +
    Description
    +
    private final HashMap<String,String>
    + +
    +
    The field for the annotations record component.
    +
    +
    private final String
    + +
    +
    The field for the name record component.
    +
    +
    +
    +
  • + +
  • +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Constructor
    +
    Description
    +
    CallableData(String name, + HashMap<String,String> annotations)
    +
    +
    Creates an instance of a CallableData record class.
    +
    +
    +
    +
  • + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    + + +
    +
    Returns the value of the annotations record component.
    +
    +
    final boolean
    + +
    +
    Indicates whether some other object is "equal to" this one.
    +
    +
    final int
    + +
    +
    Returns a hash code value for this object.
    +
    + + +
    +
    Returns the value of the name record component.
    +
    +
    final String
    + +
    +
    Returns a string representation of this record class.
    +
    +
    +
    +
    +
    +

    Methods inherited from class java.lang.Object

    +clone, finalize, getClass, notify, notifyAll, wait, wait, wait
    +
    +
  • +
+
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      name

      +
      private final String name
      +
      The field for the name record component.
      +
      +
    • +
    • +
      +

      annotations

      +
      private final HashMap<String,String> annotations
      +
      The field for the annotations record component.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      CallableData

      +
      public CallableData(String name, + HashMap<String,String> annotations)
      +
      Creates an instance of a CallableData record class.
      +
      +
      Parameters:
      +
      name - the value for the name record component
      +
      annotations - the value for the annotations record component
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      toString

      +
      public final String toString()
      +
      Returns a string representation of this record class. The representation contains the name of the class, followed by the name and value of each of the record components.
      +
      +
      Specified by:
      +
      toString in class Record
      +
      Returns:
      +
      a string representation of this object
      +
      +
      +
    • +
    • +
      +

      hashCode

      +
      public final int hashCode()
      +
      Returns a hash code value for this object. The value is derived from the hash code of each of the record components.
      +
      +
      Specified by:
      +
      hashCode in class Record
      +
      Returns:
      +
      a hash code value for this object
      +
      +
      +
    • +
    • +
      +

      equals

      +
      public final boolean equals(Object o)
      +
      Indicates whether some other object is "equal to" this one. The objects are equal if the other object is of the same class and if all the record components are equal. All components in this record class are compared with Objects::equals(Object,Object).
      +
      +
      Specified by:
      +
      equals in class Record
      +
      Parameters:
      +
      o - the object with which to compare
      +
      Returns:
      +
      true if this object is the same as the o argument; false otherwise.
      +
      +
      +
    • +
    • +
      +

      name

      +
      public String name()
      +
      Returns the value of the name record component.
      +
      +
      Returns:
      +
      the value of the name record component
      +
      +
      +
    • +
    • +
      +

      annotations

      +
      public HashMap<String,String> annotations()
      +
      Returns the value of the annotations record component.
      +
      +
      Returns:
      +
      the value of the annotations record component
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.CustomEnquirer.html b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.CustomEnquirer.html new file mode 100644 index 00000000..4cdcb223 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.CustomEnquirer.html @@ -0,0 +1,256 @@ + + + + +ScalpelExecutor.CustomEnquirer (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelExecutor.CustomEnquirer

+
+
java.lang.Object +
lexfo.scalpel.ScalpelExecutor.CustomEnquirer
+
+
+
+
All Implemented Interfaces:
+
ClassEnquirer
+
+
+
Enclosing class:
+
ScalpelExecutor
+
+
+
private class ScalpelExecutor.CustomEnquirer +extends Object +implements ClassEnquirer
+
A custom ClassEnquirer for the Jep interpreter used by the script executor.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      base

      +
      private ClassList base
      +
      The base ClassEnquirer to use.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      CustomEnquirer

      +
      CustomEnquirer()
      +
      Constructs a new CustomEnquirer object.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getClassNames

      +
      public String[] getClassNames(String pkg)
      +
      Gets the names of all the classes in a package.
      +
      +
      Specified by:
      +
      getClassNames in interface ClassEnquirer
      +
      Parameters:
      +
      pkg - the name of the package.
      +
      Returns:
      +
      an array of the names of the classes in the package.
      +
      +
      +
    • +
    • +
      +

      getSubPackages

      +
      public String[] getSubPackages(String p)
      +
      Gets the names of all the sub-packages of a package.
      +
      +
      Specified by:
      +
      getSubPackages in interface ClassEnquirer
      +
      Parameters:
      +
      p - the name of the package.
      +
      Returns:
      +
      an array of the names of the sub-packages of the package.
      +
      +
      +
    • +
    • +
      +

      isJavaPackage

      +
      public boolean isJavaPackage(String s)
      +
      Determines whether a string represents a valid Java package.
      +
      +
      Specified by:
      +
      isJavaPackage in interface ClassEnquirer
      +
      Parameters:
      +
      s - the string to check.
      +
      Returns:
      +
      true if the string represents a valid Java package, false otherwise.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.Task.html b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.Task.html new file mode 100644 index 00000000..27e15dc6 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.Task.html @@ -0,0 +1,316 @@ + + + + +ScalpelExecutor.Task (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelExecutor.Task

+
+
java.lang.Object +
lexfo.scalpel.ScalpelExecutor.Task
+
+
+
+
Enclosing class:
+
ScalpelExecutor
+
+
+
private class ScalpelExecutor.Task +extends Object
+
A class representing a task to be executed by the Scalpel script.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      name

      +
      private String name
      +
      The name of the task.
      +
      +
    • +
    • +
      +

      args

      +
      private Object[] args
      +
      The arguments passed to the task.
      +
      +
    • +
    • +
      +

      finished

      +
      private Boolean finished
      +
      Whether the task has been completed. (Used to break out of the awaitResult() loop in case of failure.)
      +
      +
    • +
    • +
      +

      kwargs

      +
      private Map<String,Object> kwargs
      +
      The keyword arguments passed to the task.
      +
      +
    • +
    • +
      +

      result

      +
      private Result<Object,Throwable> result
      +
      An optional object containing the result of the task, if it has been completed.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Task

      +
      public Task(String name, + Object[] args, + Map<String,Object> kwargs)
      +
      Constructs a new Task object.
      +
      +
      Parameters:
      +
      name - the name of the task.
      +
      args - the arguments passed to the task.
      +
      kwargs - the keyword arguments passed to the task.
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      await

      +
      public Result<Object,Throwable> await()
      +
      Add the task to the queue and wait for it to be completed by the task thread.
      +
      +
      Returns:
      +
      the result of the task.
      +
      +
      +
    • +
    • +
      +

      isFinished

      +
      public Boolean isFinished()
      +
      +
    • +
    • +
      +

      then

      +
      public void then(Consumer<Object> callback)
      +
      +
    • +
    • +
      +

      resolve

      +
      public void resolve(Object result)
      +
      +
    • +
    • +
      +

      reject

      +
      public void reject()
      +
      +
    • +
    • +
      +

      reject

      +
      public void reject(Throwable error)
      +
      +
    • +
    • +
      +

      reject

      +
      public void reject(Optional<Throwable> error)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.html b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.html new file mode 100644 index 00000000..66e74170 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelExecutor.html @@ -0,0 +1,1082 @@ + + + + +ScalpelExecutor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelExecutor

+
+
java.lang.Object +
lexfo.scalpel.ScalpelExecutor
+
+
+
+
public class ScalpelExecutor +extends Object
+
Responds to requested Python tasks from multiple threads through a task queue handled in a single sepearate thread. + +

The executor is responsible for managing a single global Python interpreter + for every script that's being executed. + +

The executor itself is designed to be used concurrently by different threads. + It provides a simple interface for submitting tasks to be executed by the script, + and blocks each thread until the task has been completed, providing a thread-safe + way to ensure that the script's state remains consistent. + +

Tasks are submitted as function calls with optional arguments and keyword + arguments. Each function call is executed in the script's global context, and + the result of the function is returned to the JVM thread that submitted the + task. + +

The executor is capable of restarting the Python interpreter when the + script file changes on disk. This ensures that any modifications made to the + script are automatically loaded by the executor without requiring a manual + restart of the extension.

+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      API

      +
      private final MontoyaApi API
      +
      The MontoyaApi object to use for sending and receiving HTTP messages.
      +
      +
    • +
    • +
      +

      script

      +
      private Optional<File> script
      +
      The path of the Scalpel script that will be passed to the framework.
      +
      +
    • +
    • +
      +

      framework

      +
      private Optional<File> framework
      +
      The path of the Scalpel framework that will be used to execute the script.
      +
      +
    • +
    • +
      +

      runner

      +
      private Thread runner
      +
      The task runner thread.
      +
      +
    • +
    • +
      +

      tasks

      +
      private final Queue<ScalpelExecutor.Task> tasks
      +
      The Python task queue.
      +
      +
    • +
    • +
      +

      lastScriptModificationTimestamp

      +
      private long lastScriptModificationTimestamp
      +
      The timestamp of the last recorded modification to the script file.
      +
      +
    • +
    • +
      +

      lastFrameworkModificationTimestamp

      +
      private long lastFrameworkModificationTimestamp
      +
      The timestamp of the last recorded modification to the framework file.
      +
      +
    • +
    • +
      +

      lastConfigModificationTimestamp

      +
      private long lastConfigModificationTimestamp
      +
      +
    • +
    • +
      +

      isRunnerAlive

      +
      private Boolean isRunnerAlive
      +
      Flag indicating whether the task runner loop is running.
      +
      +
    • +
    • +
      +

      isRunnerStarting

      +
      private Boolean isRunnerStarting
      +
      +
    • +
    • +
      +

      config

      +
      private final Config config
      +
      +
    • +
    • +
      +

      editorProvider

      +
      private Optional<ScalpelEditorProvider> editorProvider
      +
      +
    • +
    • +
      +

      isEnabled

      +
      private Boolean isEnabled
      +
      +
    • +
    • +
      +

      pythonStdout

      +
      private final OutputStream pythonStdout
      +
      +
    • +
    • +
      +

      pythonStderr

      +
      private final OutputStream pythonStderr
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ScalpelExecutor

      +
      public ScalpelExecutor(MontoyaApi API, + Config config)
      +
      Constructs a new ScalpelExecutor object.
      +
      +
      Parameters:
      +
      API - the MontoyaApi object to use for sending and receiving HTTP messages.
      +
      config - the Config object to use for getting the configuration values.
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      isEnabled

      +
      public boolean isEnabled()
      +
      +
    • +
    • +
      +

      isRunning

      +
      public boolean isRunning()
      +
      +
    • +
    • +
      +

      isStarting

      +
      public boolean isStarting()
      +
      +
    • +
    • +
      +

      enable

      +
      public void enable()
      +
      +
    • +
    • +
      +

      disable

      +
      public void disable()
      +
      +
    • +
    • +
      +

      addTask

      +
      private ScalpelExecutor.Task addTask(String name, + Object[] args, + Map<String,Object> kwargs, + boolean rejectOnReload)
      +
      Adds a new task to the queue of tasks to be executed by the script.
      +
      +
      Parameters:
      +
      name - the name of the python function to be called.
      +
      args - the arguments to pass to the python function.
      +
      kwargs - the keyword arguments to pass to the python function.
      +
      rejectOnReload - reject the task when the runner is reloading.
      +
      Returns:
      +
      a Task object representing the added task.
      +
      +
      +
    • +
    • +
      +

      addTask

      +
      private ScalpelExecutor.Task addTask(String name, + Object[] args, + Map<String,Object> kwargs)
      +
      Adds a new task to the queue of tasks to be executed by the script.
      +
      +
      Parameters:
      +
      name - the name of the python function to be called.
      +
      args - the arguments to pass to the python function.
      +
      kwargs - the keyword arguments to pass to the python function.
      +
      Returns:
      +
      a Task object representing the added task.
      +
      +
      +
    • +
    • +
      +

      awaitTask

      +
      private final <T> Result<T,Throwable> awaitTask(String name, + Object[] args, + Map<String,Object> kwargs, + Class<T> expectedClass)
      +
      Awaits the result of a task.
      +
      +
      Type Parameters:
      +
      T - the type of the result of the task.
      +
      Parameters:
      +
      name - the name of the python function to be called.
      +
      args - the arguments to pass to the python function.
      +
      kwargs - the keyword arguments to pass to the python function.
      +
      Returns:
      +
      an Optional object containing the result of the task, or empty if the task was rejected or failed.
      +
      +
      +
    • +
    • +
      +

      hasScriptChanged

      +
      private Boolean hasScriptChanged()
      +
      Checks if the script file has been modified since the last check.
      +
      +
      Returns:
      +
      true if the script file has been modified since the last check, false otherwise.
      +
      +
      +
    • +
    • +
      +

      hasConfigChanged

      +
      private Boolean hasConfigChanged()
      +
      +
    • +
    • +
      +

      resetChangeIndicators

      +
      private void resetChangeIndicators()
      +
      +
    • +
    • +
      +

      mustReload

      +
      private Boolean mustReload()
      +
      Checks if either the framework or user script file has been modified since the last check.
      +
      +
      Returns:
      +
      true if either the framework or user script file has been modified since the last check, false otherwise.
      +
      +
      +
    • +
    • +
      +

      hasFrameworkChanged

      +
      private final Boolean hasFrameworkChanged()
      +
      Checks if the framework file has been modified since the last check.
      +
      +
      Returns:
      +
      true if the framework file has been modified since the last check, false otherwise.
      +
      +
      +
    • +
    • +
      +

      setEditorsProvider

      +
      public void setEditorsProvider(ScalpelEditorProvider provider)
      +
      +
    • +
    • +
      +

      notifyEventLoop

      +
      public void notifyEventLoop()
      +
      +
    • +
    • +
      +

      rejectAllTasks

      +
      private void rejectAllTasks()
      +
      +
    • +
    • +
      +

      processTask

      +
      private void processTask(SubInterpreter interp, + ScalpelExecutor.Task task)
      +
      +
    • +
    • +
      +

      _innerTaskLoop

      +
      private void _innerTaskLoop(SubInterpreter interp) + throws InterruptedException
      +
      +
      Throws:
      +
      InterruptedException
      +
      +
      +
    • +
    • +
      +

      safeCloseInterpreter

      +
      private void safeCloseInterpreter(SubInterpreter interp)
      +
      +
    • +
    • +
      +

      taskLoop

      +
      private void taskLoop()
      +
      +
    • +
    • +
      +

      launchTaskRunner

      +
      private Thread launchTaskRunner()
      +
      Launches the task runner thread.
      +
      +
      Returns:
      +
      the launched thread.
      +
      +
      +
    • +
    • +
      +

      getDefaultIncludePath

      +
      private Optional<Path> getDefaultIncludePath()
      +
      +
    • +
    • +
      +

      initInterpreter

      +
      private SubInterpreter initInterpreter()
      +
      Initializes the interpreter.
      +
      +
      Returns:
      +
      the initialized interpreter.
      +
      +
      +
    • +
    • +
      +

      evalAndCaptureOutput

      +
      public String[] evalAndCaptureOutput(String scriptContent)
      +
      Evaluates the given script and returns the output.
      +
      +
      Parameters:
      +
      scriptContent - the script to evaluate.
      +
      Returns:
      +
      the output of the script.
      +
      +
      +
    • +
    • +
      +

      getMessageCbName

      +
      private static final <T extends HttpMessage> String getMessageCbName(T msg)
      +
      Returns the name of the corresponding Python callback for the given message intercepted by Proxy.
      +
      +
      Type Parameters:
      +
      T - the type of the message.
      +
      Parameters:
      +
      msg - the message to get the callback name for.
      +
      Returns:
      +
      the name of the corresponding Python callback.
      +
      +
      +
    • +
    • +
      +

      callIntercepterHook

      +
      public <T extends HttpMessage> Result<T,Throwable> callIntercepterHook(T msg, + HttpService service)
      +
      Calls the corresponding Python callback for the given message intercepted by Proxy.
      +
      +
      Type Parameters:
      +
      T - the type of the message.
      +
      Parameters:
      +
      msg - the message to call the callback for.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      getEditorCallbackName

      +
      private static final String getEditorCallbackName(Boolean isRequest, + Boolean isInbound)
      +
      Returns the name of the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      tabName - the name of the tab.
      +
      isRequest - whether the tab is a request tab.
      +
      isInbound - whether the callback is use to modify the request back or update the editor's content.
      +
      Returns:
      +
      the name of the corresponding Python callback.
      +
      +
      +
    • +
    • +
      +

      safeJepInvoke

      +
      public <T> Result<T,Throwable> safeJepInvoke(String name, + Object[] args, + Map<String,Object> kwargs, + Class<T> expectedClass)
      +
      Calls the given Python function with the given arguments and keyword arguments.
      +
      +
      Type Parameters:
      +
      T - the expected class of the returned value.
      +
      Parameters:
      +
      name - the name of the Python function to call.
      +
      args - the arguments to pass to the function.
      +
      kwargs - the keyword arguments to pass to the function.
      +
      expectedClass - the expected class of the returned value.
      +
      Returns:
      +
      the result of the function call.
      +
      +
      +
    • +
    • +
      +

      safeJepInvoke

      +
      public <T> Result<T,Throwable> safeJepInvoke(String name, + Object arg, + Class<T> expectedClass)
      +
      Calls the given Python function with the given argument.
      +
      +
      Type Parameters:
      +
      T - the expected class of the returned value.
      +
      Parameters:
      +
      name - the name of the Python function to call.
      +
      arg - the argument to pass to the function.
      +
      expectedClass - the expected class of the returned value.
      +
      Returns:
      +
      the result of the function call.
      +
      +
      +
    • +
    • +
      +

      safeJepInvoke

      +
      public <T> Result<T,Throwable> safeJepInvoke(String name, + Class<T> expectedClass)
      +
      Calls the given Python function without any argument.
      +
      +
      Type Parameters:
      +
      T - the expected class of the returned value.
      +
      Parameters:
      +
      name - the name of the Python function to call.
      +
      arg - the argument to pass to the function.
      +
      expectedClass - the expected class of the returned value.
      +
      Returns:
      +
      the result of the function call.
      +
      +
      +
    • +
    • +
      +

      callEditorHook

      +
      public <T> Result<T,Throwable> callEditorHook(Object[] params, + Boolean isRequest, + Boolean isInbound, + String tabName, + Class<T> expectedClass)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Type Parameters:
      +
      T - the expected class of the returned value.
      +
      Parameters:
      +
      params - the parameters to pass to the callback.
      +
      isRequest - whether the tab is a request tab.
      +
      isInbound - whether the callback is use to modify the request back or update the editor's content.
      +
      tabName - the name of the tab.
      +
      expectedClass - the expected class of the returned value.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHook

      +
      public <T> Result<T,Throwable> callEditorHook(Object param, + HttpService service, + Boolean isRequest, + Boolean isInbound, + String tabName, + Class<T> expectedClass)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Type Parameters:
      +
      T - the expected class of the returned value.
      +
      Parameters:
      +
      param - the parameter to pass to the callback.
      +
      isRequest - whether the tab is a request tab.
      +
      isInbound - whether the callback is use to modify the request back or update the editor's content.
      +
      tabName - the name of the tab.
      +
      expectedClass - the expected class of the returned value.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHook

      +
      public Result<ByteArray,Throwable> callEditorHook(HttpMessage msg, + HttpService service, + Boolean isInbound, + String tabName)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      msg - the message to pass to the callback.
      +
      isInbound - whether the callback is use to modify the request back or update the editor's content.
      +
      tabName - the name of the tab.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHook

      +
      public Result<Object,Throwable> callEditorHook(HttpMessage msg, + HttpService service, + ByteArray byteArray, + Boolean isInbound, + String tabName)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      msg - the message to pass to the callback.
      +
      byteArray - the byte array to pass to the callback (editor content).
      +
      isInbound - whether the callback is use to modify the request back or update the editor's content.
      +
      tabName - the name of the tab.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHookInRequest

      +
      public Result<ByteArray,Throwable> callEditorHookInRequest(HttpRequest req, + HttpService service, + String tabName)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      req - the message to pass to the callback.
      +
      byteArray - the byte array to pass to the callback (editor content).
      +
      tabName - the name of the tab.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHookInResponse

      +
      public Result<ByteArray,Throwable> callEditorHookInResponse(HttpResponse res, + HttpRequest req, + HttpService service, + String tabName)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      res - the message to pass to the callback.
      +
      byteArray - the byte array to pass to the callback (editor content).
      +
      tabName - the name of the tab.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHookOutRequest

      +
      public Result<HttpRequest,Throwable> callEditorHookOutRequest(HttpRequest req, + HttpService service, + ByteArray byteArray, + String tabName)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      msg - the message to pass to the callback.
      +
      byteArray - the byte array to pass to the callback (editor content).
      +
      tabName - the name of the tab.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      callEditorHookOutResponse

      +
      public Result<HttpResponse,Throwable> callEditorHookOutResponse(HttpResponse res, + HttpRequest req, + HttpService service, + ByteArray byteArray, + String tabName)
      +
      Calls the corresponding Python callback for the given tab.
      +
      +
      Parameters:
      +
      msg - the message to pass to the callback.
      +
      byteArray - the byte array to pass to the callback (editor content).
      +
      tabName - the name of the tab.
      +
      Returns:
      +
      the result of the callback.
      +
      +
      +
    • +
    • +
      +

      getCallables

      +
      public List<ScalpelExecutor.CallableData> getCallables() + throws RuntimeException
      +
      +
      Throws:
      +
      RuntimeException
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelHttpRequestHandler.html b/docs/public/javadoc/lexfo/scalpel/ScalpelHttpRequestHandler.html new file mode 100644 index 00000000..2861d0aa --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelHttpRequestHandler.html @@ -0,0 +1,257 @@ + + + + +ScalpelHttpRequestHandler (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelHttpRequestHandler

+
+
java.lang.Object +
lexfo.scalpel.ScalpelHttpRequestHandler
+
+
+
+
All Implemented Interfaces:
+
HttpHandler
+
+
+
public class ScalpelHttpRequestHandler +extends Object +implements HttpHandler
+
Handles HTTP requests and responses.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      API

      +
      private final MontoyaApi API
      +
      +
    • +
    • + +
    • +
    • +
      +

      executor

      +
      private final ScalpelExecutor executor
      +
      The ScalpelExecutor object used to execute Python scripts.
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ScalpelHttpRequestHandler

      +
      public ScalpelHttpRequestHandler(MontoyaApi API, + ScalpelEditorProvider editorProvider, + ScalpelExecutor executor)
      +
      Constructs a new ScalpelHttpRequestHandler object with the specified MontoyaApi object and ScalpelExecutor object.
      +
      +
      Parameters:
      +
      API - The MontoyaApi object to use.
      +
      editorProvider - The ScalpelEditorProvider object to use.
      +
      executor - The ScalpelExecutor object to use.
      +
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      handleHttpRequestToBeSent

      +
      public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent httpRequestToBeSent)
      +
      Handles HTTP requests.
      +
      +
      Specified by:
      +
      handleHttpRequestToBeSent in interface HttpHandler
      +
      Parameters:
      +
      httpRequestToBeSent - The HttpRequestToBeSent object containing information about the HTTP request.
      +
      Returns:
      +
      A RequestToBeSentAction object containing information about how to handle the HTTP request.
      +
      +
      +
    • +
    • +
      +

      handleHttpResponseReceived

      +
      public ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived httpResponseReceived)
      +
      Handles HTTP responses.
      +
      +
      Specified by:
      +
      handleHttpResponseReceived in interface HttpHandler
      +
      Parameters:
      +
      httpResponseReceived - The HttpResponseReceived object containing information about the HTTP response.
      +
      Returns:
      +
      A ResponseReceivedAction object containing information about how to handle the HTTP response.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelLogger.Level.html b/docs/public/javadoc/lexfo/scalpel/ScalpelLogger.Level.html new file mode 100644 index 00000000..3af9c63d --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelLogger.Level.html @@ -0,0 +1,347 @@ + + + + +ScalpelLogger.Level (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Enum Class ScalpelLogger.Level

+
+
java.lang.Object +
java.lang.Enum<ScalpelLogger.Level> +
lexfo.scalpel.ScalpelLogger.Level
+
+
+
+
+
All Implemented Interfaces:
+
Serializable, Comparable<ScalpelLogger.Level>, Constable
+
+
+
Enclosing class:
+
ScalpelLogger
+
+
+
public static enum ScalpelLogger.Level +extends Enum<ScalpelLogger.Level>
+
Log levels used to filtrate logs by weight + Useful for debugging.
+
+
+ +
+
+
    + +
  • +
    +

    Enum Constant Details

    + +
    +
  • + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      names

      +
      public static final String[] names
      +
      +
    • +
    • +
      +

      nameToLevel

      +
      public static final Map<String,ScalpelLogger.Level> nameToLevel
      +
      +
    • +
    • +
      +

      value

      +
      private int value
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Level

      +
      private Level(int value)
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      values

      +
      public static ScalpelLogger.Level[] values()
      +
      Returns an array containing the constants of this enum class, in +the order they are declared.
      +
      +
      Returns:
      +
      an array containing the constants of this enum class, in the order they are declared
      +
      +
      +
    • +
    • +
      +

      valueOf

      +
      public static ScalpelLogger.Level valueOf(String name)
      +
      Returns the enum constant of this class with the specified name. +The string must match exactly an identifier used to declare an +enum constant in this class. (Extraneous whitespace characters are +not permitted.)
      +
      +
      Parameters:
      +
      name - the name of the enum constant to be returned.
      +
      Returns:
      +
      the enum constant with the specified name
      +
      Throws:
      +
      IllegalArgumentException - if this enum class has no constant with the specified name
      +
      NullPointerException - if the argument is null
      +
      +
      +
    • +
    • +
      +

      value

      +
      public int value()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/ScalpelLogger.html b/docs/public/javadoc/lexfo/scalpel/ScalpelLogger.html new file mode 100644 index 00000000..a26fed48 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/ScalpelLogger.html @@ -0,0 +1,480 @@ + + + + +ScalpelLogger (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class ScalpelLogger

+
+
java.lang.Object +
lexfo.scalpel.ScalpelLogger
+
+
+
+
public class ScalpelLogger +extends Object
+
Provides methods for logging messages to the Burp Suite output and standard streams.
+
+
+
    + +
  • +
    +

    Nested Class Summary

    +
    Nested Classes
    +
    +
    Modifier and Type
    +
    Class
    +
    Description
    +
    static enum 
    + +
    +
    Log levels used to filtrate logs by weight + Useful for debugging.
    +
    +
    +
    +
  • + +
  • +
    +

    Field Summary

    +
    Fields
    +
    +
    Modifier and Type
    +
    Field
    +
    Description
    +
    private static Logging
    + +
     
    +
    private static ScalpelLogger.Level
    + +
    +
    Configured log level
    +
    +
    +
    +
  • + +
  • +
    +

    Constructor Summary

    +
    Constructors
    +
    +
    Constructor
    +
    Description
    + +
     
    +
    +
    +
  • + +
  • +
    +

    Method Summary

    +
    +
    +
    +
    +
    Modifier and Type
    +
    Method
    +
    Description
    +
    static void
    +
    all(String msg)
    +
     
    +
    static void
    + +
    +
    Logs the specified message to the Burp Suite output and standard output at the DEBUG level.
    +
    +
    static void
    + +
    +
    Logs the specified message to the Burp Suite error output and standard error.
    +
    +
    static String
    + +
     
    +
    static void
    + +
    +
    Logs the specified message to the Burp Suite output and standard output at the FATAL level.
    +
    +
    static void
    +
    info(String msg)
    +
    +
    Logs the specified message to the Burp Suite output and standard output at the INFO level.
    +
    +
    static void
    +
    log(String msg)
    +
    +
    Logs the specified message to the Burp Suite output and standard output at the TRACE level.
    +
    +
    static void
    + +
    +
    Logs the specified message to the Burp Suite output and standard output.
    +
    +
    static void
    + +
     
    +
    static void
    + +
    +
    Logs the current thread stack trace to the Burp Suite error output and standard error.
    +
    +
    static void
    + +
    +
    Logs the current thread stack trace to either the Burp Suite output and standard output or the Burp Suite error output and standard error.
    +
    +
    static void
    +
    logStackTrace(String title, + Throwable throwed)
    +
    +
    Logs the specified throwable stack trace to the Burp Suite error output and standard error.
    +
    +
    static void
    + +
    +
    Logs the specified throwable stack trace to the Burp Suite error output and standard error.
    +
    +
    static void
    +
    setLogger(Logging logging)
    +
    +
    Set the Burp logger instance to use.
    +
    +
    static void
    + +
     
    +
    private static String
    + +
     
    +
    static void
    + +
    +
    Logs the specified message to the Burp Suite output and standard output at the TRACE level.
    +
    +
    static void
    +
    warn(String msg)
    +
    +
    Logs the specified message to the Burp Suite output and standard output at the WARN level.
    +
    +
    +
    +
    +
    +

    Methods inherited from class java.lang.Object

    +clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
    +
    +
  • +
+
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      logger

      +
      private static Logging logger
      +
      +
    • +
    • +
      +

      loggerLevel

      +
      private static ScalpelLogger.Level loggerLevel
      +
      Configured log level
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ScalpelLogger

      +
      public ScalpelLogger()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      setLogger

      +
      public static void setLogger(Logging logging)
      +
      Set the Burp logger instance to use.
      +
      +
      Parameters:
      +
      logger -
      +
      +
      +
    • +
    • +
      +

      trace

      +
      public static void trace(String msg)
      +
      Logs the specified message to the Burp Suite output and standard output at the TRACE level.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      debug

      +
      public static void debug(String msg)
      +
      Logs the specified message to the Burp Suite output and standard output at the DEBUG level.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      info

      +
      public static void info(String msg)
      +
      Logs the specified message to the Burp Suite output and standard output at the INFO level.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      warn

      +
      public static void warn(String msg)
      +
      Logs the specified message to the Burp Suite output and standard output at the WARN level.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      fatal

      +
      public static void fatal(String msg)
      +
      Logs the specified message to the Burp Suite output and standard output at the FATAL level.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      log

      +
      public static void log(ScalpelLogger.Level level, + String msg)
      +
      Logs the specified message to the Burp Suite output and standard output.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      level - The log level.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      log

      +
      public static void log(String msg)
      +
      Logs the specified message to the Burp Suite output and standard output at the TRACE level.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      error

      +
      public static void error(String msg)
      +
      Logs the specified message to the Burp Suite error output and standard error.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      msg - The message to log.
      +
      +
      +
    • +
    • +
      +

      stackTraceToString

      +
      private static String stackTraceToString(StackTraceElement[] elems)
      +
      +
    • +
    • +
      +

      exceptionToErrorMsg

      +
      public static String exceptionToErrorMsg(Throwable throwed, + String title)
      +
      +
    • +
    • +
      +

      logStackTrace

      +
      public static void logStackTrace(Throwable throwed)
      +
      Logs the specified throwable stack trace to the Burp Suite error output and standard error.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      throwed - The throwable to log.
      +
      +
      +
    • +
    • +
      +

      logFatalStackTrace

      +
      public static void logFatalStackTrace(Throwable throwed)
      +
      +
    • +
    • +
      +

      logStackTrace

      +
      public static void logStackTrace(String title, + Throwable throwed)
      +
      Logs the specified throwable stack trace to the Burp Suite error output and standard error.
      +
      +
      Parameters:
      +
      title - title to display before the stacktrace
      +
      logger - The Logging object to use.
      +
      throwed - The throwable to log.
      +
      +
      +
    • +
    • +
      +

      logStackTrace

      +
      public static void logStackTrace()
      +
      Logs the current thread stack trace to the Burp Suite error output and standard error.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      +
      +
    • +
    • +
      +

      logStackTrace

      +
      public static void logStackTrace(Boolean error)
      +
      Logs the current thread stack trace to either the Burp Suite output and standard output or the Burp Suite error output and standard error.
      +
      +
      Parameters:
      +
      logger - The Logging object to use.
      +
      error - Whether to log to the error output or not.
      +
      +
      +
    • +
    • +
      +

      all

      +
      public static void all(String msg)
      +
      +
    • +
    • +
      +

      setLogLevel

      +
      public static void setLogLevel(ScalpelLogger.Level level)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Terminal.html b/docs/public/javadoc/lexfo/scalpel/Terminal.html new file mode 100644 index 00000000..3b687261 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Terminal.html @@ -0,0 +1,284 @@ + + + + +Terminal (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Terminal

+
+
java.lang.Object +
lexfo.scalpel.Terminal
+
+
+
+
public class Terminal +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Terminal

      +
      public Terminal()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      createSettingsProvider

      +
      private static com.jediterm.terminal.ui.settings.SettingsProvider createSettingsProvider(Theme theme)
      +
      +
    • +
    • +
      +

      createTerminalWidget

      +
      private static com.jediterm.terminal.ui.JediTermWidget createTerminalWidget(Theme theme, + String venvPath, + Optional<String> cwd, + Optional<String> cmd)
      +
      +
    • +
    • +
      +

      escapeshellarg

      +
      public static String escapeshellarg(String str)
      +
      +
    • +
    • +
      +

      dumps

      +
      private static String dumps(Object obj) + throws com.fasterxml.jackson.core.JsonProcessingException
      +
      +
      Throws:
      +
      com.fasterxml.jackson.core.JsonProcessingException
      +
      +
      +
    • +
    • +
      +

      createTtyConnector

      +
      public static com.jediterm.terminal.TtyConnector createTtyConnector(String venvPath)
      +
      Creates a TtyConnector that will run a shell in the virtualenv.
      +
      +
      Parameters:
      +
      venvPath - The path to the virtualenv.
      +
      Returns:
      +
      The TtyConnector.
      +
      +
      +
    • +
    • +
      +

      createTtyConnector

      +
      protected static com.jediterm.terminal.TtyConnector createTtyConnector(String workspacePath, + Optional<Dimension> ttyDimension, + Optional<String> cwd, + Optional<String> cmd)
      +
      Creates a TtyConnector that will run a shell in the virtualenv.
      +
      +
      Parameters:
      +
      workspacePath - The path to the virtualenv.
      +
      Returns:
      +
      The TtyConnector.
      +
      +
      +
    • +
    • +
      +

      createTerminal

      +
      public static com.jediterm.terminal.ui.JediTermWidget createTerminal(Theme theme, + String venvPath)
      +
      Creates a JediTermWidget that will run a shell in the virtualenv.
      +
      +
      Parameters:
      +
      theme - The theme to use. (Dark or Light)
      +
      venvPath - The path to the virtualenv.
      +
      Returns:
      +
      The JediTermWidget.
      +
      +
      +
    • +
    • +
      +

      createTerminal

      +
      public static com.jediterm.terminal.ui.JediTermWidget createTerminal(Theme theme, + String venvPath, + String cwd, + String cmd)
      +
      Creates a JediTermWidget that will run a shell in the virtualenv.
      +
      +
      Parameters:
      +
      theme - The theme to use. (Dark or Light)
      +
      venvPath - The path to the virtualenv.
      +
      cmd - The command to run
      +
      Returns:
      +
      The JediTermWidget.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/UIBuilder.html b/docs/public/javadoc/lexfo/scalpel/UIBuilder.html new file mode 100644 index 00000000..20bd3ccc --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/UIBuilder.html @@ -0,0 +1,195 @@ + + + + +UIBuilder (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class UIBuilder

+
+
java.lang.Object +
lexfo.scalpel.UIBuilder
+
+
+
+
public class UIBuilder +extends Object
+
Provides methods for constructing the Burp Suite UI.
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      UIBuilder

      +
      public UIBuilder()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      constructConfigTab

      +
      public static final Component constructConfigTab(MontoyaApi API, + ScalpelExecutor executor, + Config config, + Theme theme)
      +
      Constructs the configuration Burp tab.
      +
      +
      Parameters:
      +
      executor - The ScalpelExecutor object to use.
      +
      defaultScriptPath - The default text content
      +
      Returns:
      +
      The constructed tab.
      +
      +
      +
    • +
    • +
      +

      constructScalpelInterpreterTab

      +
      public static final Component constructScalpelInterpreterTab(Config config, + ScalpelExecutor executor)
      +
      Constructs the debug Python testing Burp tab.
      +
      +
      Parameters:
      +
      executor - The ScalpelExecutor object to use.
      +
      logger - The Logging object to use.
      +
      Returns:
      +
      The constructed tab.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/UIUtils.html b/docs/public/javadoc/lexfo/scalpel/UIUtils.html new file mode 100644 index 00000000..23bd0af3 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/UIUtils.html @@ -0,0 +1,170 @@ + + + + +UIUtils (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class UIUtils

+
+
java.lang.Object +
lexfo.scalpel.UIUtils
+
+
+
+
public class UIUtils +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      UIUtils

      +
      public UIUtils()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      setupAutoScroll

      +
      public static void setupAutoScroll(JScrollPane scrollPane, + JTextArea textArea)
      +
      Set up auto-scrolling for a script output text area. +

      + If the user scrolls up, auto-scroll is disabled. + If the user scrolls to the bottom, auto-scroll is enabled.

      +
      +
      Parameters:
      +
      scrollPane - The scroll pane containing the text area.
      +
      textArea - The text area to auto-scroll.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/UnObfuscator.html b/docs/public/javadoc/lexfo/scalpel/UnObfuscator.html new file mode 100644 index 00000000..5ae95664 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/UnObfuscator.html @@ -0,0 +1,174 @@ + + + + +UnObfuscator (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class UnObfuscator

+
+
java.lang.Object +
lexfo.scalpel.UnObfuscator
+
+
+
+
public class UnObfuscator +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      UnObfuscator

      +
      public UnObfuscator()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      getClassName

      +
      public static String getClassName(Object obj)
      +
      Finds a Montoya interface in the specified class, its superclasses or + interfaces, and return its name. Otherwise, returns the name of the class + of the object.
      +
      +
    • +
    • +
      +

      findMontoyaInterface

      +
      public static String findMontoyaInterface(Class<?> c, + HashSet<Class<?>> visited)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Venv.PackageInfo.html b/docs/public/javadoc/lexfo/scalpel/Venv.PackageInfo.html new file mode 100644 index 00000000..e1eb633c --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Venv.PackageInfo.html @@ -0,0 +1,170 @@ + + + + +Venv.PackageInfo (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Venv.PackageInfo

+
+
java.lang.Object +
lexfo.scalpel.Venv.PackageInfo
+
+
+
+
Enclosing class:
+
Venv
+
+
+
protected static final class Venv.PackageInfo +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      name

      +
      public String name
      +
      +
    • +
    • +
      +

      version

      +
      public String version
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      PackageInfo

      +
      protected PackageInfo()
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Venv.html b/docs/public/javadoc/lexfo/scalpel/Venv.html new file mode 100644 index 00000000..4bafd966 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Venv.html @@ -0,0 +1,442 @@ + + + + +Venv (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Venv

+
+
java.lang.Object +
lexfo.scalpel.Venv
+
+
+
+
public class Venv +extends Object
+
Manage Python virtual environments.
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Venv

      +
      public Venv()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      create

      +
      public static Process create(Path path) + throws IOException, +InterruptedException
      +
      Create a virtual environment.
      +
      +
      Parameters:
      +
      path - The path to the virtual environment directory.
      +
      Returns:
      +
      The finished process of the "python3 -m venv" command.
      +
      Throws:
      +
      IOException
      +
      InterruptedException
      +
      +
      +
    • +
    • +
      +

      clearPipCache

      +
      private static void clearPipCache(Path venv) + throws IOException, +InterruptedException
      +
      +
      Throws:
      +
      IOException
      +
      InterruptedException
      +
      +
      +
    • +
    • +
      +

      installDefaults

      +
      public static Process installDefaults(Path venv, + Map<String,String> env, + Boolean installJep) + throws IOException, +InterruptedException
      +
      +
      Throws:
      +
      IOException
      +
      InterruptedException
      +
      +
      +
    • +
    • +
      +

      installDefaults

      +
      public static Process installDefaults(Path path) + throws IOException, +InterruptedException
      +
      +
      Throws:
      +
      IOException
      +
      InterruptedException
      +
      +
      +
    • +
    • +
      +

      createAndInstallDefaults

      +
      public static Process createAndInstallDefaults(Path venv) + throws IOException, +InterruptedException
      +
      Create a virtual environment and install the default packages.
      +
      +
      Parameters:
      +
      venv - The path to the virtual environment directory.
      +
      Returns:
      +
      The exit code of the "pip install ..." command.
      +
      Throws:
      +
      IOException
      +
      InterruptedException
      +
      +
      +
    • +
    • +
      +

      delete

      +
      public static void delete(Path venv)
      +
      Delete a virtual environment.
      +
      +
      Parameters:
      +
      venv - The path to the virtual environment directory.
      +
      +
      +
    • +
    • +
      +

      install_background

      +
      public static Thread install_background(Path path, + String... pkgs)
      +
      Install a package in a virtual environment in a new thread.
      +
      +
      Parameters:
      +
      path - The path to the virtual environment directory.
      +
      pkgs - The name of the package to install.
      +
      Returns:
      +
      The exit code of the "pip install ..." command.
      +
      +
      +
    • +
    • +
      +

      install

      +
      public static Process install(Path path, + String... pkgs) + throws IOException
      +
      Install a package in a virtual environment.
      +
      +
      Parameters:
      +
      path - The path to the virtual environment directory.
      +
      pkgs - The name of the package to install.
      +
      Returns:
      +
      The exit code of the "pip install ..." command.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      install_background

      +
      public static Process install_background(Path path, + Map<String,String> env, + String... pkgs) + throws IOException
      +
      Install a package in a virtual environment.
      +
      +
      Parameters:
      +
      path - The path to the virtual environment directory.
      +
      env - The environnement variables to pass
      +
      pkgs - The name of the package to install.
      +
      Returns:
      +
      The exit code of the "pip install ..." command.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      install

      +
      public static Process install(Path path, + Map<String,String> env, + String... pkgs) + throws IOException
      +
      Install a package in a virtual environment.
      +
      +
      Parameters:
      +
      path - The path to the virtual environment directory.
      +
      env - The environnement variables to pass
      +
      pkgs - The name of the package to install.
      +
      Returns:
      +
      The exit code of the "pip install ..." command.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getSitePackagesPath

      +
      public static Path getSitePackagesPath(Path venvPath) + throws IOException
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getExecutablePath

      +
      public static Path getExecutablePath(Path venvPath, + String filename) + throws IOException
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getPipPath

      +
      public static Path getPipPath(Path venvPath) + throws IOException
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getInstalledPackages

      +
      public static Venv.PackageInfo[] getInstalledPackages(Path path) + throws IOException
      +
      Get the list of installed packages in a virtual environment.
      +
      +
      Parameters:
      +
      path - The path to the virtual environment directory.
      +
      Returns:
      +
      The list of installed packages.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/Workspace.html b/docs/public/javadoc/lexfo/scalpel/Workspace.html new file mode 100644 index 00000000..87da4d88 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/Workspace.html @@ -0,0 +1,367 @@ + + + + +Workspace (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+
Package lexfo.scalpel
+

Class Workspace

+
+
java.lang.Object +
lexfo.scalpel.Workspace
+
+
+
+
public class Workspace +extends Object
+
A workspace is a folder containing a venv and the associated scripts. +
+ We may still call that a "venv" in the front-end to avoid confusing the user.
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    + +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      Workspace

      +
      public Workspace()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      createExceptionFromProcess

      +
      private static RuntimeException createExceptionFromProcess(Process proc, + String msg, + String defaultCmdLine)
      +
      +
    • +
    • +
      +

      copyScriptToWorkspace

      +
      public static Path copyScriptToWorkspace(Path workspace, + Path scriptPath)
      +
      Copy the script to the selected workspace
      +
      +
      Parameters:
      +
      scriptPath - The script to copy
      +
      Returns:
      +
      The new file path
      +
      +
      +
    • +
    • +
      +

      copyWorkspaceFiles

      +
      public static void copyWorkspaceFiles(Path workspace) + throws IOException
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      println

      +
      private static void println(com.jediterm.terminal.Terminal terminal, + String line)
      +
      +
    • +
    • +
      +

      createAndInitWorkspace

      +
      public static void createAndInitWorkspace(Path workspace, + Optional<Path> javaHome, + Optional<com.jediterm.terminal.Terminal> terminal)
      +
      +
    • +
    • +
      +

      isJepInstalled

      +
      private static boolean isJepInstalled(Path workspace) + throws IOException
      +
      If a previous install failed because python dependencies were not installed, + this will be false, in this case, we just try to resume the install.
      +
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getOrCreateDefaultWorkspace

      +
      public static Path getOrCreateDefaultWorkspace(Path javaHome) + throws IOException
      +
      Get the default workspace path. + This is the workspace that will be used when the project is created. + If the default workspace does not exist, it will be created. + If the default workspace cannot be created, an exception will be thrown.
      +
      +
      Returns:
      +
      The default workspace path.
      +
      Throws:
      +
      IOException
      +
      +
      +
    • +
    • +
      +

      getVenvDir

      +
      public static Path getVenvDir(Path workspace)
      +
      +
    • +
    • +
      +

      getDefaultWorkspace

      +
      public static Path getDefaultWorkspace()
      +
      +
    • +
    • +
      +

      getScalpelDir

      +
      public static File getScalpelDir()
      +
      Get the scalpel configuration directory.
      +
      +
      Returns:
      +
      The scalpel configuration directory. (default: $HOME/.scalpel)
      +
      +
      +
    • +
    • +
      +

      getWorkspacesDir

      +
      public static File getWorkspacesDir()
      +
      Get the default venvs directory.
      +
      +
      Returns:
      +
      The default venvs directory. (default: $HOME/.scalpel/venvs)
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/ErrorDialog.html b/docs/public/javadoc/lexfo/scalpel/components/ErrorDialog.html new file mode 100644 index 00000000..781c546d --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/ErrorDialog.html @@ -0,0 +1,177 @@ + + + + +ErrorDialog (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ErrorDialog

+
+
java.lang.Object +
lexfo.scalpel.components.ErrorDialog
+
+
+
+
public class ErrorDialog +extends Object
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ErrorDialog

      +
      public ErrorDialog()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      showErrorDialog

      +
      public static void showErrorDialog(Frame parent, + String errorText)
      +
      +
    • +
    • +
      +

      sanitizeHTML

      +
      private static String sanitizeHTML(String text)
      +
      +
    • +
    • +
      +

      linkifyURLs

      +
      private static String linkifyURLs(String text)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/ErrorPopup.html b/docs/public/javadoc/lexfo/scalpel/components/ErrorPopup.html new file mode 100644 index 00000000..9665ff9b --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/ErrorPopup.html @@ -0,0 +1,289 @@ + + + + +ErrorPopup (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ErrorPopup

+
+ +
+
+
All Implemented Interfaces:
+
ImageObserver, MenuContainer, Serializable, Accessible, RootPaneContainer, WindowConstants
+
+
+
public class ErrorPopup +extends JFrame
+
+
See Also:
+
+ +
+
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    + +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      ErrorPopup

      +
      public ErrorPopup(MontoyaApi API)
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      displayErrors

      +
      public void displayErrors()
      +
      +
    • +
    • +
      +

      addError

      +
      public void addError(String message)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/PlaceholderTextField.html b/docs/public/javadoc/lexfo/scalpel/components/PlaceholderTextField.html new file mode 100644 index 00000000..68326e53 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/PlaceholderTextField.html @@ -0,0 +1,275 @@ + + + + +PlaceholderTextField (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class PlaceholderTextField

+
+ +
+
+
All Implemented Interfaces:
+
ImageObserver, MenuContainer, Serializable, Accessible, Scrollable, SwingConstants
+
+
+
public class PlaceholderTextField +extends JTextField
+
+
See Also:
+
+ +
+
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      placeholder

      +
      private String placeholder
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      PlaceholderTextField

      +
      public PlaceholderTextField(String placeholder)
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    + +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/SettingsPanel.html b/docs/public/javadoc/lexfo/scalpel/components/SettingsPanel.html new file mode 100644 index 00000000..6254b40a --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/SettingsPanel.html @@ -0,0 +1,362 @@ + + + + +SettingsPanel (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class SettingsPanel

+
+ +
+
+
All Implemented Interfaces:
+
ImageObserver, MenuContainer, Serializable, Accessible
+
+
+
public class SettingsPanel +extends JPanel
+
+
See Also:
+
+ +
+
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    + +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      SettingsPanel

      +
      public SettingsPanel()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      addListener

      +
      public void addListener(Consumer<Map<String,String>> listener)
      +
      +
    • +
    • +
      +

      addCheckboxSetting

      +
      public void addCheckboxSetting(String key, + String label, + boolean isSelected)
      +
      +
    • +
    • +
      +

      addTextFieldSetting

      +
      public void addTextFieldSetting(String key, + String label, + String text)
      +
      +
    • +
    • +
      +

      addDropdownSetting

      +
      public void addDropdownSetting(String key, + String label, + String[] options, + String selectedItem)
      +
      +
    • +
    • +
      +

      addSettingComponent

      +
      private void addSettingComponent(String key, + String label, + JComponent component)
      +
      +
    • +
    • +
      +

      addInformationText

      +
      public void addInformationText(String text)
      +
      +
    • +
    • +
      +

      notifyChangeListeners

      +
      private void notifyChangeListeners()
      +
      +
    • +
    • +
      +

      getSettingsValues

      +
      public Map<String,String> getSettingsValues()
      +
      +
    • +
    • +
      +

      setSettingsValues

      +
      public void setSettingsValues(Map<String,String> settingsValues)
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/WorkingPopup.html b/docs/public/javadoc/lexfo/scalpel/components/WorkingPopup.html new file mode 100644 index 00000000..c3313336 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/WorkingPopup.html @@ -0,0 +1,167 @@ + + + + +WorkingPopup (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class WorkingPopup

+
+
java.lang.Object +
lexfo.scalpel.components.WorkingPopup
+
+
+
+
public class WorkingPopup +extends Object
+
Provides a blocking wait dialog GUI popup.
+
+
+ +
+
+
    + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      WorkingPopup

      +
      public WorkingPopup()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    +
      +
    • +
      +

      showBlockingWaitDialog

      +
      public static void showBlockingWaitDialog(String message, + Consumer<JLabel> task)
      +
      Shows a blocking wait dialog.
      +
      +
      Parameters:
      +
      task - The task to run while the dialog is shown.
      +
      +
      +
    • +
    +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/package-summary.html b/docs/public/javadoc/lexfo/scalpel/components/package-summary.html new file mode 100644 index 00000000..716df062 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/package-summary.html @@ -0,0 +1,104 @@ + + + + +lexfo.scalpel.components (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Package lexfo.scalpel.components

+
+
+
package lexfo.scalpel.components
+
+ +
+
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/components/package-tree.html b/docs/public/javadoc/lexfo/scalpel/components/package-tree.html new file mode 100644 index 00000000..ca9ee08b --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/components/package-tree.html @@ -0,0 +1,110 @@ + + + + +lexfo.scalpel.components Class Hierarchy (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Hierarchy For Package lexfo.scalpel.components

+Package Hierarchies: + +
+
+

Class Hierarchy

+ +
+
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/AbstractEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/AbstractEditor.html new file mode 100644 index 00000000..7e6d7838 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/AbstractEditor.html @@ -0,0 +1,760 @@ + + + + +AbstractEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class AbstractEditor

+
+
java.lang.Object +
lexfo.scalpel.editors.AbstractEditor
+
+
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
Direct Known Subclasses:
+
ScalpelGenericBinaryEditor, ScalpelRawEditor
+
+
+
public abstract class AbstractEditor +extends Object +implements IMessageEditor
+
Base class for implementing Scalpel editors + It handles all the Python stuff and only leaves the content setter/getter, modification checker and selection parts abstract + That way, if you wish to implement you own editor, you only have to add logic specific to it (get/set, selected data, has content been modified by user ?)
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/DisplayableWhiteSpaceCharset.html b/docs/public/javadoc/lexfo/scalpel/editors/DisplayableWhiteSpaceCharset.html new file mode 100644 index 00000000..90ea25bc --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/DisplayableWhiteSpaceCharset.html @@ -0,0 +1,225 @@ + + + + +DisplayableWhiteSpaceCharset (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class DisplayableWhiteSpaceCharset

+
+
java.lang.Object +
java.nio.charset.Charset +
lexfo.scalpel.editors.DisplayableWhiteSpaceCharset
+
+
+
+
+
All Implemented Interfaces:
+
Comparable<Charset>
+
+
+
public class DisplayableWhiteSpaceCharset +extends Charset
+
+
+ +
+
+
    + +
  • +
    +

    Field Details

    +
      +
    • +
      +

      utf8

      +
      private final Charset utf8
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Constructor Details

    +
      +
    • +
      +

      DisplayableWhiteSpaceCharset

      +
      public DisplayableWhiteSpaceCharset()
      +
      +
    • +
    +
    +
  • + +
  • +
    +

    Method Details

    + +
    +
  • +
+
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/IMessageEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/IMessageEditor.html new file mode 100644 index 00000000..18c9bf0c --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/IMessageEditor.html @@ -0,0 +1,226 @@ + + + + +IMessageEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Interface IMessageEditor

+
+
+
+
All Superinterfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor
+
+
+
All Known Implementing Classes:
+
AbstractEditor, ScalpelBinaryEditor, ScalpelDecimalEditor, ScalpelGenericBinaryEditor, ScalpelHexEditor, ScalpelOctalEditor, ScalpelRawEditor
+
+
+ +
Interface declaring all the necessary methods to implement a Scalpel editor + If you wish to implement your own type of editor, you should use the AbstractEditor class as a base.
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/ScalpelBinaryEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelBinaryEditor.html new file mode 100644 index 00000000..3e0a0401 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelBinaryEditor.html @@ -0,0 +1,161 @@ + + + + +ScalpelBinaryEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ScalpelBinaryEditor

+
+ +
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
public class ScalpelBinaryEditor +extends ScalpelGenericBinaryEditor
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/ScalpelDecimalEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelDecimalEditor.html new file mode 100644 index 00000000..c0aed8d1 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelDecimalEditor.html @@ -0,0 +1,161 @@ + + + + +ScalpelDecimalEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ScalpelDecimalEditor

+
+ +
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
public class ScalpelDecimalEditor +extends ScalpelGenericBinaryEditor
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/ScalpelGenericBinaryEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelGenericBinaryEditor.html new file mode 100644 index 00000000..f78fdaf1 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelGenericBinaryEditor.html @@ -0,0 +1,374 @@ + + + + +ScalpelGenericBinaryEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ScalpelGenericBinaryEditor

+
+
java.lang.Object +
lexfo.scalpel.editors.AbstractEditor +
lexfo.scalpel.editors.ScalpelGenericBinaryEditor
+
+
+
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
Direct Known Subclasses:
+
ScalpelBinaryEditor, ScalpelDecimalEditor, ScalpelHexEditor, ScalpelOctalEditor
+
+
+
public class ScalpelGenericBinaryEditor +extends AbstractEditor
+
Hexadecimal editor implementation for a Scalpel editor + Users can press their keyboard's INSER key to enter insertion mode + (which is impossible in Burp's native hex editor)
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/ScalpelHexEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelHexEditor.html new file mode 100644 index 00000000..239bc805 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelHexEditor.html @@ -0,0 +1,161 @@ + + + + +ScalpelHexEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ScalpelHexEditor

+
+ +
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
public class ScalpelHexEditor +extends ScalpelGenericBinaryEditor
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/ScalpelOctalEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelOctalEditor.html new file mode 100644 index 00000000..14509914 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelOctalEditor.html @@ -0,0 +1,161 @@ + + + + +ScalpelOctalEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ScalpelOctalEditor

+
+ +
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
public class ScalpelOctalEditor +extends ScalpelGenericBinaryEditor
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/ScalpelRawEditor.html b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelRawEditor.html new file mode 100644 index 00000000..2731daec --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/ScalpelRawEditor.html @@ -0,0 +1,322 @@ + + + + +ScalpelRawEditor (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class ScalpelRawEditor

+
+
java.lang.Object +
lexfo.scalpel.editors.AbstractEditor +
lexfo.scalpel.editors.ScalpelRawEditor
+
+
+
+
+
All Implemented Interfaces:
+
ExtensionProvidedEditor, ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor, IMessageEditor
+
+
+
public class ScalpelRawEditor +extends AbstractEditor
+
Provides an UI text editor component for editing HTTP requests or responses. + Calls Python scripts to initialize the editor and update the requests or responses.
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/WhitspaceCharsetDecoder.html b/docs/public/javadoc/lexfo/scalpel/editors/WhitspaceCharsetDecoder.html new file mode 100644 index 00000000..cd6ca78b --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/WhitspaceCharsetDecoder.html @@ -0,0 +1,201 @@ + + + + +WhitspaceCharsetDecoder (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class WhitspaceCharsetDecoder

+
+
java.lang.Object +
java.nio.charset.CharsetDecoder +
lexfo.scalpel.editors.WhitspaceCharsetDecoder
+
+
+
+
+
class WhitspaceCharsetDecoder +extends CharsetDecoder
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/WhitspaceCharsetEncoder.html b/docs/public/javadoc/lexfo/scalpel/editors/WhitspaceCharsetEncoder.html new file mode 100644 index 00000000..128881d6 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/WhitspaceCharsetEncoder.html @@ -0,0 +1,201 @@ + + + + +WhitspaceCharsetEncoder (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+ +
+ +

Class WhitspaceCharsetEncoder

+
+
java.lang.Object +
java.nio.charset.CharsetEncoder +
lexfo.scalpel.editors.WhitspaceCharsetEncoder
+
+
+
+
+
class WhitspaceCharsetEncoder +extends CharsetEncoder
+
+
+ +
+
+ +
+ +
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/package-summary.html b/docs/public/javadoc/lexfo/scalpel/editors/package-summary.html new file mode 100644 index 00000000..d9d38462 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/package-summary.html @@ -0,0 +1,133 @@ + + + + +lexfo.scalpel.editors (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Package lexfo.scalpel.editors

+
+
+
package lexfo.scalpel.editors
+
+ +
+
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/editors/package-tree.html b/docs/public/javadoc/lexfo/scalpel/editors/package-tree.html new file mode 100644 index 00000000..155ea91f --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/editors/package-tree.html @@ -0,0 +1,116 @@ + + + + +lexfo.scalpel.editors Class Hierarchy (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Hierarchy For Package lexfo.scalpel.editors

+Package Hierarchies: + +
+
+

Class Hierarchy

+ +
+
+

Interface Hierarchy

+ +
+
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/package-summary.html b/docs/public/javadoc/lexfo/scalpel/package-summary.html new file mode 100644 index 00000000..64828e59 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/package-summary.html @@ -0,0 +1,225 @@ + + + + +lexfo.scalpel (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Package lexfo.scalpel

+
+
+
package lexfo.scalpel
+
+
    +
  • + +
  • +
  • +
    +
    +
    +
    +
    Class
    +
    Description
    + +
     
    + +
    +
    Provides utilities to get default commands.
    +
    + +
    +
    Scalpel configuration.
    +
    + +
    +
    Global configuration.
    +
    + +
     
    + +
    +
    Burp tab handling Scalpel configuration + IntelliJ's GUI designer is needed to edit most components.
    +
    + +
    +
    Contains constants used by the extension.
    +
    + +
    +
    Enum used by editors to identify themselves
    +
    + +
    +
    Utilities to perform IO utilities conveniently
    +
    + +
     
    + +
     
    + +
    +
    Color palette for the embedded terminal + Contains colors for both light and dark theme
    +
    + +
    +
    Utilities to initialize Java Embedded Python (jep)
    +
    + +
    +
    Utility class for Python scripts.
    +
    + +
    +
    Provides methods for unpacking the Scalpel resources.
    +
    +
    Result<T,E extends Throwable>
    +
    +
    Optional-style class for handling python task results + + A completed python task can have multiple outcomes: + - The task completes successfully and returns a value + - The task completes successfully but returns no value + - The task throws an exception + + Result allows us to handle returned values and errors uniformly to handle them when needed.
    +
    + +
    +
    The main class of the extension.
    +
    + +
    +
    Provides a new ScalpelProvidedEditor object for editing HTTP requests or responses.
    +
    + +
    +
    Provides an UI text editor component for editing HTTP requests or responses.
    +
    + +
    +
    This stores all the informations required to create a tab.
    +
    + +
    +
    A tab can be associated with at most two hooks + (e.g req_edit_in and req_edit_out) + + This stores the informations related to only one hook and is later merged with the second hook information into a HookTabInfo
    +
    + +
    +
    Responds to requested Python tasks from multiple threads through a task queue handled in a single sepearate thread.
    +
    + +
     
    + +
    +
    Handles HTTP requests and responses.
    +
    + +
    +
    Provides methods for logging messages to the Burp Suite output and standard streams.
    +
    + +
    +
    Log levels used to filtrate logs by weight + Useful for debugging.
    +
    + +
     
    + +
    +
    Provides methods for constructing the Burp Suite UI.
    +
    + +
     
    + +
     
    + +
    +
    Manage Python virtual environments.
    +
    + +
     
    + +
    +
    A workspace is a folder containing a venv and the associated scripts.
    +
    +
    +
    +
    +
  • +
+
+
+
+
+ + diff --git a/docs/public/javadoc/lexfo/scalpel/package-tree.html b/docs/public/javadoc/lexfo/scalpel/package-tree.html new file mode 100644 index 00000000..c59289c5 --- /dev/null +++ b/docs/public/javadoc/lexfo/scalpel/package-tree.html @@ -0,0 +1,150 @@ + + + + +lexfo.scalpel Class Hierarchy (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Hierarchy For Package lexfo.scalpel

+Package Hierarchies: + +
+
+

Class Hierarchy

+ +
+
+

Interface Hierarchy

+ +
+
+

Enum Class Hierarchy

+ +
+
+
+
+ + diff --git a/docs/public/javadoc/member-search-index.js b/docs/public/javadoc/member-search-index.js new file mode 100644 index 00000000..34bcc65f --- /dev/null +++ b/docs/public/javadoc/member-search-index.js @@ -0,0 +1 @@ +memberSearchIndex = [{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"_GlobalData()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"_innerTaskLoop(SubInterpreter)","u":"_innerTaskLoop(jep.SubInterpreter)"},{"p":"lexfo.scalpel","c":"Config","l":"_jdkPath"},{"p":"lexfo.scalpel","c":"Config._ProjectData","l":"_ProjectData()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"_requestResponse"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"_requestResponse"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"$$$getRootComponent$$$()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"$$$setupUI$$$()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"AbstractEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"addCheckboxSetting(String, String, boolean)","u":"addCheckboxSetting(java.lang.String,java.lang.String,boolean)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"addDropdownSetting(String, String, String[], String)","u":"addDropdownSetting(java.lang.String,java.lang.String,java.lang.String[],java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"addEditorToDisplayedTabs(IMessageEditor)","u":"addEditorToDisplayedTabs(lexfo.scalpel.editors.IMessageEditor)"},{"p":"lexfo.scalpel.components","c":"ErrorPopup","l":"addError(String)","u":"addError(java.lang.String)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"addInformationText(String)","u":"addInformationText(java.lang.String)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"addListDoubleClickListener(JList, Consumer)","u":"addListDoubleClickListener(javax.swing.JList,java.util.function.Consumer)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"addListener(Consumer>)","u":"addListener(java.util.function.Consumer)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"addSettingComponent(String, String, JComponent)","u":"addSettingComponent(java.lang.String,java.lang.String,javax.swing.JComponent)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"addTask(String, Object[], Map)","u":"addTask(java.lang.String,java.lang.Object[],java.util.Map)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"addTask(String, Object[], Map, boolean)","u":"addTask(java.lang.String,java.lang.Object[],java.util.Map,boolean)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"addTextFieldSetting(String, String, String)","u":"addTextFieldSetting(java.lang.String,java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"addVentText"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"addVenvButton"},{"p":"lexfo.scalpel","c":"Config","l":"addVenvPath(Path)","u":"addVenvPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"adjustTabBarVisibility()"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"ALL"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"all(String)","u":"all(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"annotations"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"annotations()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"API"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"API"},{"p":"lexfo.scalpel","c":"Scalpel","l":"API"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"API"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"API"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"API"},{"p":"lexfo.scalpel","c":"ScalpelHttpRequestHandler","l":"API"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"appendToDebugInfo(String)","u":"appendToDebugInfo(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"args"},{"p":"lexfo.scalpel","c":"Async","l":"Async()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"await()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"awaitTask(String, Object[], Map, Class)","u":"awaitTask(java.lang.String,java.lang.Object[],java.util.Map,java.lang.Class)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CustomEnquirer","l":"base"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"BASH_INIT_FILE_PATH"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"binaryDataToByteArray(BinaryData)","u":"binaryDataToByteArray(org.exbin.auxiliary.paged_data.BinaryData)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"browsePanel"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"burpFrame"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"byteArrayToBinaryData(ByteArray)","u":"byteArrayToBinaryData(burp.api.montoya.core.ByteArray)"},{"p":"lexfo.scalpel","c":"IO.IOSupplier","l":"call()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"CallableData(String, HashMap)","u":"%3Cinit%3E(java.lang.String,java.util.HashMap)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHook(HttpMessage, HttpService, Boolean, String)","u":"callEditorHook(burp.api.montoya.http.message.HttpMessage,burp.api.montoya.http.HttpService,java.lang.Boolean,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHook(HttpMessage, HttpService, ByteArray, Boolean, String)","u":"callEditorHook(burp.api.montoya.http.message.HttpMessage,burp.api.montoya.http.HttpService,burp.api.montoya.core.ByteArray,java.lang.Boolean,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHook(Object, HttpService, Boolean, Boolean, String, Class)","u":"callEditorHook(java.lang.Object,burp.api.montoya.http.HttpService,java.lang.Boolean,java.lang.Boolean,java.lang.String,java.lang.Class)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHook(Object[], Boolean, Boolean, String, Class)","u":"callEditorHook(java.lang.Object[],java.lang.Boolean,java.lang.Boolean,java.lang.String,java.lang.Class)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHookInRequest(HttpRequest, HttpService, String)","u":"callEditorHookInRequest(burp.api.montoya.http.message.requests.HttpRequest,burp.api.montoya.http.HttpService,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHookInResponse(HttpResponse, HttpRequest, HttpService, String)","u":"callEditorHookInResponse(burp.api.montoya.http.message.responses.HttpResponse,burp.api.montoya.http.message.requests.HttpRequest,burp.api.montoya.http.HttpService,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHookOutRequest(HttpRequest, HttpService, ByteArray, String)","u":"callEditorHookOutRequest(burp.api.montoya.http.message.requests.HttpRequest,burp.api.montoya.http.HttpService,burp.api.montoya.core.ByteArray,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callEditorHookOutResponse(HttpResponse, HttpRequest, HttpService, ByteArray, String)","u":"callEditorHookOutResponse(burp.api.montoya.http.message.responses.HttpResponse,burp.api.montoya.http.message.requests.HttpRequest,burp.api.montoya.http.HttpService,burp.api.montoya.core.ByteArray,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"callIntercepterHook(T, HttpService)","u":"callIntercepterHook(T,burp.api.montoya.http.HttpService)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"caption()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"caption()"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"changeListeners"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"clearOutputs(String)","u":"clearOutputs(java.lang.String)"},{"p":"lexfo.scalpel","c":"Venv","l":"clearPipCache(Path)","u":"clearPipCache(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"cmdFormat(String, Object, Object)","u":"cmdFormat(java.lang.String,java.lang.Object,java.lang.Object)"},{"p":"lexfo.scalpel","c":"CommandChecker","l":"CommandChecker()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"config"},{"p":"lexfo.scalpel","c":"Scalpel","l":"config"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"config"},{"p":"lexfo.scalpel","c":"Config","l":"CONFIG_EXT"},{"p":"lexfo.scalpel","c":"Config","l":"Config(MontoyaApi)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"ConfigTab(MontoyaApi, ScalpelExecutor, Config, Theme)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi,lexfo.scalpel.ScalpelExecutor,lexfo.scalpel.Config,burp.api.montoya.ui.Theme)"},{"p":"lexfo.scalpel","c":"Constants","l":"Constants()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"UIBuilder","l":"constructConfigTab(MontoyaApi, ScalpelExecutor, Config, Theme)","u":"constructConfigTab(burp.api.montoya.MontoyaApi,lexfo.scalpel.ScalpelExecutor,lexfo.scalpel.Config,burp.api.montoya.ui.Theme)"},{"p":"lexfo.scalpel","c":"UIBuilder","l":"constructScalpelInterpreterTab(Config, ScalpelExecutor)","u":"constructScalpelInterpreterTab(lexfo.scalpel.Config,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel.editors","c":"DisplayableWhiteSpaceCharset","l":"contains(Charset)","u":"contains(java.nio.charset.Charset)"},{"p":"lexfo.scalpel","c":"Workspace","l":"copyScriptToWorkspace(Path, Path)","u":"copyScriptToWorkspace(java.nio.file.Path,java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"copyToClipboardButton"},{"p":"lexfo.scalpel","c":"Workspace","l":"copyWorkspaceFiles(Path)","u":"copyWorkspaceFiles(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Venv","l":"create(Path)","u":"create(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Workspace","l":"createAndInitWorkspace(Path, Optional, Optional)","u":"createAndInitWorkspace(java.nio.file.Path,java.util.Optional,java.util.Optional)"},{"p":"lexfo.scalpel","c":"Venv","l":"createAndInstallDefaults(Path)","u":"createAndInstallDefaults(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"createButton"},{"p":"lexfo.scalpel","c":"Workspace","l":"createExceptionFromProcess(Process, String, String)","u":"createExceptionFromProcess(java.lang.Process,java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"Terminal","l":"createSettingsProvider(Theme)","u":"createSettingsProvider(burp.api.montoya.ui.Theme)"},{"p":"lexfo.scalpel","c":"Terminal","l":"createTerminal(Theme, String)","u":"createTerminal(burp.api.montoya.ui.Theme,java.lang.String)"},{"p":"lexfo.scalpel","c":"Terminal","l":"createTerminal(Theme, String, String, String)","u":"createTerminal(burp.api.montoya.ui.Theme,java.lang.String,java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"Terminal","l":"createTerminalWidget(Theme, String, Optional, Optional)","u":"createTerminalWidget(burp.api.montoya.ui.Theme,java.lang.String,java.util.Optional,java.util.Optional)"},{"p":"lexfo.scalpel","c":"Terminal","l":"createTtyConnector(String)","u":"createTtyConnector(java.lang.String)"},{"p":"lexfo.scalpel","c":"Terminal","l":"createTtyConnector(String, Optional, Optional, Optional)","u":"createTtyConnector(java.lang.String,java.util.Optional,java.util.Optional,java.util.Optional)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"createUIComponents()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"ctx"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"ctx"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CustomEnquirer","l":"CustomEnquirer()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"Palette","l":"DARK_COLORS"},{"p":"lexfo.scalpel","c":"Palette","l":"DARK_PALETTE"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"DATA_DIR_PATH"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"DATA_DIRNAME"},{"p":"lexfo.scalpel","c":"Config","l":"DATA_PREFIX"},{"p":"lexfo.scalpel","c":"Config","l":"DATA_PROJECT_ID_KEY"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"DEBUG"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"debug(String)","u":"debug(java.lang.String)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"debugInfoTextPane"},{"p":"lexfo.scalpel.editors","c":"WhitspaceCharsetDecoder","l":"decodeLoop(ByteBuffer, CharBuffer)","u":"decodeLoop(java.nio.ByteBuffer,java.nio.CharBuffer)"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_EDITOR_MODE"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_LINUX_OPEN_DIR_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_LINUX_OPEN_FILE_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_LINUX_TERM_EDIT_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_OPEN_DIR_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_OPEN_FILE_CMD"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"DEFAULT_SCRIPT_FILENAME"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"DEFAULT_SCRIPT_PATH"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_TERM_EDIT_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_TERMINAL_EDITOR"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_UNIX_SHELL"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_VENV_DEPENDENCIES"},{"p":"lexfo.scalpel","c":"Workspace","l":"DEFAULT_VENV_NAME"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_WINDOWS_EDITOR"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_WINDOWS_OPEN_DIR_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_WINDOWS_OPEN_FILE_CMD"},{"p":"lexfo.scalpel","c":"Constants","l":"DEFAULT_WINDOWS_TERM_EDIT_CMD"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"defaultScriptPath"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"defaultWorkspacePath"},{"p":"lexfo.scalpel","c":"Venv","l":"delete(Path)","u":"delete(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"direction"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"direction()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"directions"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"directions()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"disable()"},{"p":"lexfo.scalpel.editors","c":"DisplayableWhiteSpaceCharset","l":"DisplayableWhiteSpaceCharset()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel.components","c":"ErrorPopup","l":"displayErrors()"},{"p":"lexfo.scalpel","c":"Config._ProjectData","l":"displayProxyErrorPopup"},{"p":"lexfo.scalpel","c":"Config","l":"dumpConfig()"},{"p":"lexfo.scalpel","c":"Terminal","l":"dumps(Object)","u":"dumps(java.lang.Object)"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"editor"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"editor"},{"p":"lexfo.scalpel","c":"Constants","l":"EDITOR_MODE_ANNOTATION_KEY"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"editorProvider"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"editors"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"editorsRefs"},{"p":"lexfo.scalpel","c":"EditorType","l":"EditorType()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"editScriptCommand"},{"p":"lexfo.scalpel","c":"Result","l":"empty()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"enable()"},{"p":"lexfo.scalpel.editors","c":"WhitspaceCharsetEncoder","l":"encodeLoop(CharBuffer, ByteBuffer)","u":"encodeLoop(java.nio.CharBuffer,java.nio.ByteBuffer)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"equals(Object)","u":"equals(java.lang.Object)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"equals(Object)","u":"equals(java.lang.Object)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"equals(Object)","u":"equals(java.lang.Object)"},{"p":"lexfo.scalpel","c":"Result","l":"error"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"ERROR"},{"p":"lexfo.scalpel","c":"Result","l":"error(E)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"error(String)","u":"error(java.lang.String)"},{"p":"lexfo.scalpel.components","c":"ErrorPopup","l":"errorArea"},{"p":"lexfo.scalpel.components","c":"ErrorDialog","l":"ErrorDialog()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel.components","c":"ErrorPopup","l":"errorMessages"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"errorPane"},{"p":"lexfo.scalpel.components","c":"ErrorPopup","l":"ErrorPopup(MontoyaApi)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi)"},{"p":"lexfo.scalpel","c":"Terminal","l":"escapeshellarg(String)","u":"escapeshellarg(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"evalAndCaptureOutput(String)","u":"evalAndCaptureOutput(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"exceptionToErrorMsg(Throwable, String)","u":"exceptionToErrorMsg(java.lang.Throwable,java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"executeHook(HttpRequestResponse)","u":"executeHook(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"executeHook(HttpRequestResponse)","u":"executeHook(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel","c":"PythonSetup","l":"executePythonCommand(String)","u":"executePythonCommand(java.lang.String)"},{"p":"lexfo.scalpel","c":"Async","l":"executor"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"executor"},{"p":"lexfo.scalpel","c":"Scalpel","l":"executor"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"executor"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"executor"},{"p":"lexfo.scalpel","c":"ScalpelHttpRequestHandler","l":"executor"},{"p":"lexfo.scalpel","c":"CommandChecker","l":"extractBinary(String)","u":"extractBinary(java.lang.String)"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"extractRessources(String, String, Set)","u":"extractRessources(java.lang.String,java.lang.String,java.util.Set)"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"extractRessourcesToHome()"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"FATAL"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"fatal(String)","u":"fatal(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"filterEditorHooks(List)","u":"filterEditorHooks(java.util.List)"},{"p":"lexfo.scalpel","c":"Config","l":"findBinaryInPath(String)","u":"findBinaryInPath(java.lang.String)"},{"p":"lexfo.scalpel","c":"Config","l":"findJdkPath()"},{"p":"lexfo.scalpel","c":"UnObfuscator","l":"findMontoyaInterface(Class, HashSet>)","u":"findMontoyaInterface(java.lang.Class,java.util.HashSet)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"finished"},{"p":"lexfo.scalpel","c":"Result","l":"flatMap(Function>)","u":"flatMap(java.util.function.Function)"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"forceGarbageCollection()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"framework"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"FRAMEWORK_PATH"},{"p":"lexfo.scalpel","c":"Constants","l":"FRAMEWORK_REQ_CB_NAME"},{"p":"lexfo.scalpel","c":"Constants","l":"FRAMEWORK_REQ_EDIT_PREFIX"},{"p":"lexfo.scalpel","c":"Constants","l":"FRAMEWORK_RES_CB_NAME"},{"p":"lexfo.scalpel","c":"Constants","l":"FRAMEWORK_RES_EDIT_PREFIX"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"frameworkBrowseButton"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"frameworkConfigPanel"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"frameworkPathField"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"frameworkPathTextArea"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"gbc"},{"p":"lexfo.scalpel","c":"Constants","l":"GET_CB_NAME"},{"p":"lexfo.scalpel","c":"CommandChecker","l":"getAvailableCommand(String...)","u":"getAvailableCommand(java.lang.String...)"},{"p":"lexfo.scalpel","c":"Palette","l":"getBackgroundByColorIndex(int)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getCallables()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"getCallables()"},{"p":"lexfo.scalpel","c":"UnObfuscator","l":"getClassName(Object)","u":"getClassName(java.lang.Object)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CustomEnquirer","l":"getClassNames(String)","u":"getClassNames(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getCtx()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"getCtx()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getCtx()"},{"p":"lexfo.scalpel","c":"Config","l":"getDefaultGlobalData()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"getDefaultIncludePath()"},{"p":"lexfo.scalpel","c":"Config","l":"getDefaultProjectData()"},{"p":"lexfo.scalpel","c":"Workspace","l":"getDefaultWorkspace()"},{"p":"lexfo.scalpel","c":"Config","l":"getDisplayProxyErrorPopup()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"getEditorCallbackName(Boolean, Boolean)","u":"getEditorCallbackName(java.lang.Boolean,java.lang.Boolean)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getEditorContent()"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"getEditorContent()"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"getEditorContent()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getEditorType()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"getEditorType()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getEditorType()"},{"p":"lexfo.scalpel","c":"Config","l":"getEditScriptCommand()"},{"p":"lexfo.scalpel","c":"Result","l":"getError()"},{"p":"lexfo.scalpel","c":"Venv","l":"getExecutablePath(Path, String)","u":"getExecutablePath(java.nio.file.Path,java.lang.String)"},{"p":"lexfo.scalpel","c":"Palette","l":"getForegroundByColorIndex(int)"},{"p":"lexfo.scalpel","c":"Config","l":"getFrameworkPath()"},{"p":"lexfo.scalpel","c":"Config","l":"getGlobalConfigFile()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getHookPrefix(String)","u":"getHookPrefix(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getHookSuffix(String)","u":"getHookSuffix(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getHttpService()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"getHttpService()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getHttpService()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getId()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"getId()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getId()"},{"p":"lexfo.scalpel","c":"Venv","l":"getInstalledPackages(Path)","u":"getInstalledPackages(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Config","l":"getInstance()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"getInstance()"},{"p":"lexfo.scalpel","c":"Config","l":"getInstance(MontoyaApi)","u":"getInstance(burp.api.montoya.MontoyaApi)"},{"p":"lexfo.scalpel","c":"Config","l":"getInstance(Optional)","u":"getInstance(java.util.Optional)"},{"p":"lexfo.scalpel","c":"Config","l":"getJdkPath()"},{"p":"lexfo.scalpel","c":"Config","l":"getLastModified()"},{"p":"lexfo.scalpel","c":"Config","l":"getLogLevel()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getMessage()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"getMessage()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getMessage()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"getMessageCbName(T)"},{"p":"lexfo.scalpel","c":"Config","l":"getOpenFolderCommand()"},{"p":"lexfo.scalpel","c":"Config","l":"getOpenScriptCommand()"},{"p":"lexfo.scalpel","c":"Workspace","l":"getOrCreateDefaultWorkspace(Path)","u":"getOrCreateDefaultWorkspace(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getPane()"},{"p":"lexfo.scalpel","c":"Venv","l":"getPipPath(Path)","u":"getPipPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"PythonSetup","l":"getPythonVersion()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getRequest()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getRequest()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getRequestResponse()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"getRequestResponse()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getRequestResponse()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getResponse()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getResponse()"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"getRunningJarPath()"},{"p":"lexfo.scalpel","c":"Workspace","l":"getScalpelDir()"},{"p":"lexfo.scalpel","c":"Config","l":"getSelectedWorkspacePath()"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"getSettingsValues()"},{"p":"lexfo.scalpel","c":"Venv","l":"getSitePackagesPath(Path)","u":"getSitePackagesPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CustomEnquirer","l":"getSubPackages(String)","u":"getSubPackages(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"getTabNameOffsetInHookName(String)","u":"getTabNameOffsetInHookName(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"getUiComponent()"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"getUiComponent()"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"getUiComponent()"},{"p":"lexfo.scalpel","c":"PythonSetup","l":"getUsedPythonBin()"},{"p":"lexfo.scalpel","c":"Config","l":"getUserScriptPath()"},{"p":"lexfo.scalpel","c":"Result","l":"getValue()"},{"p":"lexfo.scalpel","c":"Workspace","l":"getVenvDir(Path)","u":"getVenvDir(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Config","l":"getVenvPaths()"},{"p":"lexfo.scalpel","c":"Workspace","l":"getWorkspacesDir()"},{"p":"lexfo.scalpel","c":"Config","l":"globalConfig"},{"p":"lexfo.scalpel","c":"Config","l":"guessJdkPath()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleBrowseButtonClick(Supplier, Consumer)","u":"handleBrowseButtonClick(java.util.function.Supplier,java.util.function.Consumer)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleEnableButton()"},{"p":"lexfo.scalpel","c":"ScalpelHttpRequestHandler","l":"handleHttpRequestToBeSent(HttpRequestToBeSent)","u":"handleHttpRequestToBeSent(burp.api.montoya.http.handler.HttpRequestToBeSent)"},{"p":"lexfo.scalpel","c":"ScalpelHttpRequestHandler","l":"handleHttpResponseReceived(HttpResponseReceived)","u":"handleHttpResponseReceived(burp.api.montoya.http.handler.HttpResponseReceived)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleNewScriptButton()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleOpenScriptButton()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleOpenScriptFolderButton()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleScriptListSelectionEvent()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleVenvButton()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"handleVenvListSelectionEvent(ListSelectionEvent)","u":"handleVenvListSelectionEvent(javax.swing.event.ListSelectionEvent)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"hasConfigChanged()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"hasFrameworkChanged()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"hashCode()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"hashCode()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"hashCode()"},{"p":"lexfo.scalpel","c":"Config","l":"hasIncludeDir(Path)","u":"hasIncludeDir(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"hasScriptChanged()"},{"p":"lexfo.scalpel","c":"Result","l":"hasValue()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"helpTextPane"},{"p":"lexfo.scalpel","c":"Constants","l":"HEX_EDITOR_MODE"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"hookInPrefix"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"hookOutPrefix"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"hookPrefix"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"HookTabInfo(String, String, Set)","u":"%3Cinit%3E(java.lang.String,java.lang.String,java.util.Set)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"id"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"id"},{"p":"lexfo.scalpel","c":"Result","l":"ifEmpty(Runnable)","u":"ifEmpty(java.lang.Runnable)"},{"p":"lexfo.scalpel","c":"Result","l":"ifError(Consumer)","u":"ifError(java.util.function.Consumer)"},{"p":"lexfo.scalpel","c":"Result","l":"ifSuccess(Consumer)","u":"ifSuccess(java.util.function.Consumer)"},{"p":"lexfo.scalpel","c":"Constants","l":"IN_SUFFIX"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"inError"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"INFO"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"info(String)","u":"info(java.lang.String)"},{"p":"lexfo.scalpel","c":"Config","l":"initGlobalConfig()"},{"p":"lexfo.scalpel","c":"Scalpel","l":"initialize(MontoyaApi)","u":"initialize(burp.api.montoya.MontoyaApi)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"initInterpreter()"},{"p":"lexfo.scalpel","c":"Config","l":"initProjectConfig()"},{"p":"lexfo.scalpel","c":"Venv","l":"install_background(Path, Map, String...)","u":"install_background(java.nio.file.Path,java.util.Map,java.lang.String...)"},{"p":"lexfo.scalpel","c":"Venv","l":"install_background(Path, String...)","u":"install_background(java.nio.file.Path,java.lang.String...)"},{"p":"lexfo.scalpel","c":"Venv","l":"install(Path, Map, String...)","u":"install(java.nio.file.Path,java.util.Map,java.lang.String...)"},{"p":"lexfo.scalpel","c":"Venv","l":"install(Path, String...)","u":"install(java.nio.file.Path,java.lang.String...)"},{"p":"lexfo.scalpel","c":"Venv","l":"installDefaults(Path)","u":"installDefaults(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Venv","l":"installDefaults(Path, Map, Boolean)","u":"installDefaults(java.nio.file.Path,java.util.Map,java.lang.Boolean)"},{"p":"lexfo.scalpel","c":"Config","l":"instance"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"instance"},{"p":"lexfo.scalpel","c":"IO","l":"IO()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"IO","l":"ioWrap(IO.IOSupplier)","u":"ioWrap(lexfo.scalpel.IO.IOSupplier)"},{"p":"lexfo.scalpel","c":"IO","l":"ioWrap(IO.IOSupplier, Supplier)","u":"ioWrap(lexfo.scalpel.IO.IOSupplier,com.google.common.base.Supplier)"},{"p":"lexfo.scalpel","c":"CommandChecker","l":"isCommandAvailable(String)","u":"isCommandAvailable(java.lang.String)"},{"p":"lexfo.scalpel","c":"Result","l":"isEmpty"},{"p":"lexfo.scalpel","c":"Result","l":"isEmpty()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"isEnabled"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"isEnabled()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"isEnabledFor(HttpRequestResponse)","u":"isEnabledFor(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"isEnabledFor(HttpRequestResponse)","u":"isEnabledFor(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"isFinished()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CustomEnquirer","l":"isJavaPackage(String)","u":"isJavaPackage(java.lang.String)"},{"p":"lexfo.scalpel","c":"Workspace","l":"isJepInstalled(Path)","u":"isJepInstalled(java.nio.file.Path)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"isModified()"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"isModified()"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"isModified()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"isModified()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"isRunnerAlive"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"isRunnerStarting"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"isRunning()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"isStarting()"},{"p":"lexfo.scalpel","c":"Result","l":"isSuccess()"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"jdkPath"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"keyToLabel"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"kwargs"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"lastConfigModificationTimestamp"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"lastFrameworkModificationTimestamp"},{"p":"lexfo.scalpel","c":"Config","l":"lastModified"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"lastScriptModificationTimestamp"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"launchOpenScriptCommand(Path)","u":"launchOpenScriptCommand(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"launchTaskRunner()"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"Level(int)","u":"%3Cinit%3E(int)"},{"p":"lexfo.scalpel","c":"Palette","l":"LIGHT_COLORS"},{"p":"lexfo.scalpel","c":"Palette","l":"LIGHT_PALETTE"},{"p":"lexfo.scalpel.components","c":"ErrorDialog","l":"linkifyURLs(String)","u":"linkifyURLs(java.lang.String)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"listPannel"},{"p":"lexfo.scalpel","c":"PythonSetup","l":"loadLibPython3()"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"log(ScalpelLogger.Level, String)","u":"log(lexfo.scalpel.ScalpelLogger.Level,java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"log(String)","u":"log(java.lang.String)"},{"p":"lexfo.scalpel","c":"Scalpel","l":"logConfig(Config)","u":"logConfig(lexfo.scalpel.Config)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"logFatalStackTrace(Throwable)","u":"logFatalStackTrace(java.lang.Throwable)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"logger"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"loggerLevel"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"logLevel"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"logStackTrace()"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"logStackTrace(Boolean)","u":"logStackTrace(java.lang.Boolean)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"logStackTrace(String, Throwable)","u":"logStackTrace(java.lang.String,java.lang.Throwable)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"logStackTrace(Throwable)","u":"logStackTrace(java.lang.Throwable)"},{"p":"lexfo.scalpel.components","c":"PlaceholderTextField","l":"main(String[])","u":"main(java.lang.String[])"},{"p":"lexfo.scalpel","c":"Result","l":"map(Function)","u":"map(java.util.function.Function)"},{"p":"lexfo.scalpel","c":"IO","l":"mapper"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"mergeHookTabInfo(Stream)","u":"mergeHookTabInfo(java.util.stream.Stream)"},{"p":"lexfo.scalpel","c":"Constants","l":"MIN_SUPPORTED_PYTHON_VERSION"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"mode"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"mode"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"mode()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"mode()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"modeToEditorMap"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"mustReload()"},{"p":"lexfo.scalpel","c":"Palette","l":"myColors"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"name"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"name"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"name"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"name"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"name"},{"p":"lexfo.scalpel","c":"Venv.PackageInfo","l":"name"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"name()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"name()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"name()"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"names"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"nameToLevel"},{"p":"lexfo.scalpel","c":"Constants","l":"NATIVE_LIBJEP_FILE"},{"p":"lexfo.scalpel.editors","c":"DisplayableWhiteSpaceCharset","l":"newDecoder()"},{"p":"lexfo.scalpel.editors","c":"DisplayableWhiteSpaceCharset","l":"newEncoder()"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"notifyChangeListeners()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"notifyEventLoop()"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"oldContent"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"openEditorInTerminal(Path)","u":"openEditorInTerminal(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"openFolderButton"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"openFolderCommand"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"openIssueOnGitHubButton"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"openScriptButton"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"openScriptCommand"},{"p":"lexfo.scalpel","c":"Result","l":"or(Result)","u":"or(lexfo.scalpel.Result)"},{"p":"lexfo.scalpel","c":"Result","l":"orElse(T)"},{"p":"lexfo.scalpel","c":"Result","l":"orElseGet(Supplier)","u":"orElseGet(java.util.function.Supplier)"},{"p":"lexfo.scalpel.editors","c":"WhitspaceCharsetDecoder","l":"originalDecoder"},{"p":"lexfo.scalpel.editors","c":"WhitspaceCharsetEncoder","l":"originalEncoder"},{"p":"lexfo.scalpel","c":"Constants","l":"OUT_SUFFIX"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"outError"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"outputTabPanel"},{"p":"lexfo.scalpel","c":"Venv.PackageInfo","l":"PackageInfo()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"packagesTable"},{"p":"lexfo.scalpel.components","c":"PlaceholderTextField","l":"paintComponent(Graphics)","u":"paintComponent(java.awt.Graphics)"},{"p":"lexfo.scalpel","c":"Palette","l":"Palette(Color[])","u":"%3Cinit%3E(java.awt.Color[])"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"pane"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"PartialHookTabInfo(String, String, String)","u":"%3Cinit%3E(java.lang.String,java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"Constants","l":"PERSISTED_FRAMEWORK"},{"p":"lexfo.scalpel","c":"Constants","l":"PERSISTED_SCRIPT"},{"p":"lexfo.scalpel","c":"Constants","l":"PERSISTENCE_PREFIX"},{"p":"lexfo.scalpel","c":"Constants","l":"PIP_BIN"},{"p":"lexfo.scalpel.components","c":"PlaceholderTextField","l":"placeholder"},{"p":"lexfo.scalpel.components","c":"PlaceholderTextField","l":"PlaceholderTextField(String)","u":"%3Cinit%3E(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelHttpRequestHandler","l":"popup"},{"p":"lexfo.scalpel","c":"Constants","l":"PREFERRED_PYTHON_VERSION"},{"p":"lexfo.scalpel","c":"Workspace","l":"println(Terminal, String)","u":"println(com.jediterm.terminal.Terminal,java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"processOutboundMessage()"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"processOutboundMessage()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"processOutboundMessage()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"processTask(SubInterpreter, ScalpelExecutor.Task)","u":"processTask(jep.SubInterpreter,lexfo.scalpel.ScalpelExecutor.Task)"},{"p":"lexfo.scalpel","c":"Config","l":"projectConfig"},{"p":"lexfo.scalpel","c":"Config","l":"projectID"},{"p":"lexfo.scalpel","c":"Config","l":"projectScalpelConfig"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"provideHttpRequestEditor(EditorCreationContext)","u":"provideHttpRequestEditor(burp.api.montoya.ui.editor.extension.EditorCreationContext)"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"provideHttpResponseEditor(EditorCreationContext)","u":"provideHttpResponseEditor(burp.api.montoya.ui.editor.extension.EditorCreationContext)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"provider"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"provider"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"pushCharToOutput(int, boolean)","u":"pushCharToOutput(int,boolean)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"putStringToOutput(String, boolean)","u":"putStringToOutput(java.lang.String,boolean)"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"PYSCALPEL_PATH"},{"p":"lexfo.scalpel","c":"Constants","l":"PYTHON_BIN"},{"p":"lexfo.scalpel","c":"Constants","l":"PYTHON_DEPENDENCIES"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"PYTHON_DIRNAME"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"PYTHON_PATH"},{"p":"lexfo.scalpel","c":"PythonSetup","l":"PythonSetup()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"pythonStderr"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"pythonStdout"},{"p":"lexfo.scalpel","c":"PythonUtils","l":"PythonUtils()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"Constants","l":"RAW_EDITOR_MODE"},{"p":"lexfo.scalpel","c":"Config","l":"readConfigFile(File, Class)","u":"readConfigFile(java.io.File,java.lang.Class)"},{"p":"lexfo.scalpel","c":"IO","l":"readJSON(File, Class)","u":"readJSON(java.io.File,java.lang.Class)"},{"p":"lexfo.scalpel","c":"IO","l":"readJSON(File, Class, Consumer)","u":"readJSON(java.io.File,java.lang.Class,java.util.function.Consumer)"},{"p":"lexfo.scalpel","c":"IO","l":"readJSON(String, Class)","u":"readJSON(java.lang.String,java.lang.Class)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"recreateEditors()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"recreateEditorsAsync()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"reject()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"reject(Optional)","u":"reject(java.util.Optional)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"reject(Throwable)","u":"reject(java.lang.Throwable)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"rejectAllTasks()"},{"p":"lexfo.scalpel","c":"Config","l":"removeVenvPath(Path)","u":"removeVenvPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Constants","l":"REQ_CB_NAME"},{"p":"lexfo.scalpel","c":"Constants","l":"REQ_EDIT_PREFIX"},{"p":"lexfo.scalpel","c":"EditorType","l":"REQUEST"},{"p":"lexfo.scalpel","c":"Constants","l":"RES_CB_NAME"},{"p":"lexfo.scalpel","c":"Constants","l":"RES_EDIT_PREFIX"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"resetChangeIndicators()"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"resetEditors()"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"resetEditorsAsync()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"resetTerminalButton"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"resolve(Object)","u":"resolve(java.lang.Object)"},{"p":"lexfo.scalpel","c":"EditorType","l":"RESPONSE"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"RESSOURCES_DIRNAME"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"RESSOURCES_PATH"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"RESSOURCES_TO_COPY"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"RessourcesUnpacker()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"result"},{"p":"lexfo.scalpel","c":"Result","l":"Result(T, E, boolean)","u":"%3Cinit%3E(T,E,boolean)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"rootPanel"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"rootPanel"},{"p":"lexfo.scalpel","c":"IO.IORunnable","l":"run()"},{"p":"lexfo.scalpel","c":"IO","l":"run(IO.IORunnable)","u":"run(lexfo.scalpel.IO.IORunnable)"},{"p":"lexfo.scalpel","c":"Async","l":"run(Runnable)","u":"run(java.lang.Runnable)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"runner"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"safeCloseInterpreter(SubInterpreter)","u":"safeCloseInterpreter(jep.SubInterpreter)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"safeJepInvoke(String, Class)","u":"safeJepInvoke(java.lang.String,java.lang.Class)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"safeJepInvoke(String, Object, Class)","u":"safeJepInvoke(java.lang.String,java.lang.Object,java.lang.Class)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"safeJepInvoke(String, Object[], Map, Class)","u":"safeJepInvoke(java.lang.String,java.lang.Object[],java.util.Map,java.lang.Class)"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"SAMPLES_DIRNAME"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"SAMPLES_PATH"},{"p":"lexfo.scalpel.components","c":"ErrorDialog","l":"sanitizeHTML(String)","u":"sanitizeHTML(java.lang.String)"},{"p":"lexfo.scalpel","c":"Config","l":"saveAllConfig()"},{"p":"lexfo.scalpel","c":"Config","l":"saveGlobalConfig()"},{"p":"lexfo.scalpel","c":"Config","l":"saveProjectConfig()"},{"p":"lexfo.scalpel","c":"Scalpel","l":"Scalpel()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel.editors","c":"ScalpelBinaryEditor","l":"ScalpelBinaryEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel.editors","c":"ScalpelDecimalEditor","l":"ScalpelDecimalEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ScalpelEditorProvider","l":"ScalpelEditorProvider(MontoyaApi, ScalpelExecutor)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"ScalpelEditorTabbedPane(MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorProvider, ScalpelExecutor)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorProvider,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"scalpelExecutor"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"ScalpelExecutor(MontoyaApi, Config)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi,lexfo.scalpel.Config)"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"ScalpelGenericBinaryEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor, CodeType)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor,org.exbin.bined.CodeType)"},{"p":"lexfo.scalpel.editors","c":"ScalpelHexEditor","l":"ScalpelHexEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ScalpelHttpRequestHandler","l":"ScalpelHttpRequestHandler(MontoyaApi, ScalpelEditorProvider, ScalpelExecutor)","u":"%3Cinit%3E(burp.api.montoya.MontoyaApi,lexfo.scalpel.ScalpelEditorProvider,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"scalpelIsENABLEDButton"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"ScalpelLogger()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel.editors","c":"ScalpelOctalEditor","l":"ScalpelOctalEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"ScalpelRawEditor(String, Boolean, MontoyaApi, EditorCreationContext, EditorType, ScalpelEditorTabbedPane, ScalpelExecutor)","u":"%3Cinit%3E(java.lang.String,java.lang.Boolean,burp.api.montoya.MontoyaApi,burp.api.montoya.ui.editor.extension.EditorCreationContext,lexfo.scalpel.EditorType,lexfo.scalpel.ScalpelEditorTabbedPane,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"script"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"scriptBrowseButton"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"scriptConfigPanel"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"scriptPathTextArea"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"selectedData()"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"selectedData()"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"selectedData()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"selectedData()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"selectEditor()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"selectedScriptLabel"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"selectScript(Path)","u":"selectScript(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setAndStoreScript(Path)","u":"setAndStoreScript(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Config","l":"setDisplayProxyErrorPopup(String)","u":"setDisplayProxyErrorPopup(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"setEditorContent(ByteArray)","u":"setEditorContent(burp.api.montoya.core.ByteArray)"},{"p":"lexfo.scalpel.editors","c":"ScalpelGenericBinaryEditor","l":"setEditorContent(ByteArray)","u":"setEditorContent(burp.api.montoya.core.ByteArray)"},{"p":"lexfo.scalpel.editors","c":"ScalpelRawEditor","l":"setEditorContent(ByteArray)","u":"setEditorContent(burp.api.montoya.core.ByteArray)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"setEditorError(Throwable)","u":"setEditorError(java.lang.Throwable)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"setEditorsProvider(ScalpelEditorProvider)","u":"setEditorsProvider(lexfo.scalpel.ScalpelEditorProvider)"},{"p":"lexfo.scalpel","c":"Config","l":"setEditScriptCommand(String)","u":"setEditScriptCommand(java.lang.String)"},{"p":"lexfo.scalpel","c":"Config","l":"setJdkPath(Path)","u":"setJdkPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"setLogger(Logging)","u":"setLogger(burp.api.montoya.logging.Logging)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"setLogLevel(ScalpelLogger.Level)","u":"setLogLevel(lexfo.scalpel.ScalpelLogger.Level)"},{"p":"lexfo.scalpel","c":"Config","l":"setLogLevel(String)","u":"setLogLevel(java.lang.String)"},{"p":"lexfo.scalpel","c":"Config","l":"setOpenFolderCommand(String)","u":"setOpenFolderCommand(java.lang.String)"},{"p":"lexfo.scalpel","c":"Config","l":"setOpenScriptCommand(String)","u":"setOpenScriptCommand(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"setRequestResponse(HttpRequestResponse)","u":"setRequestResponse(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"setRequestResponse(HttpRequestResponse)","u":"setRequestResponse(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"setRequestResponseInternal(HttpRequestResponse)","u":"setRequestResponseInternal(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"setRequestResponseInternal(HttpRequestResponse)","u":"setRequestResponseInternal(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel","c":"Config","l":"setSelectedVenvPath(Path)","u":"setSelectedVenvPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setSettings(Map)","u":"setSettings(java.util.Map)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"setSettingsValues(Map)","u":"setSettingsValues(java.util.Map)"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"settingsComponentsByKey"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"settingsPanel"},{"p":"lexfo.scalpel.components","c":"SettingsPanel","l":"SettingsPanel()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"settingsTab"},{"p":"lexfo.scalpel","c":"UIUtils","l":"setupAutoScroll(JScrollPane, JTextArea)","u":"setupAutoScroll(javax.swing.JScrollPane,javax.swing.JTextArea)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupCopyButton()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupDebugInfoTab()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupGitHubIssueButton()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupHelpTab()"},{"p":"lexfo.scalpel","c":"Scalpel","l":"setupJepFromConfig(Config)","u":"setupJepFromConfig(lexfo.scalpel.Config)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupLogsTab()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupSettingsTab()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"setupVenvTab()"},{"p":"lexfo.scalpel","c":"Config","l":"setUserScriptPath(Path)","u":"setUserScriptPath(java.nio.file.Path)"},{"p":"lexfo.scalpel","c":"Config","l":"setVenvPaths(ArrayList)","u":"setVenvPaths(java.util.ArrayList)"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"SHELL_DIRNAME"},{"p":"lexfo.scalpel.components","c":"WorkingPopup","l":"showBlockingWaitDialog(String, Consumer)","u":"showBlockingWaitDialog(java.lang.String,java.util.function.Consumer)"},{"p":"lexfo.scalpel.components","c":"ErrorDialog","l":"showErrorDialog(Frame, String)","u":"showErrorDialog(java.awt.Frame,java.lang.String)"},{"p":"lexfo.scalpel","c":"IO","l":"sleep(Integer)","u":"sleep(java.lang.Integer)"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"stackTraceToString(StackTraceElement[])","u":"stackTraceToString(java.lang.StackTraceElement[])"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"stderrScrollPane"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"stderrTextArea"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"stdoutScrollPane"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"stdoutTextArea"},{"p":"lexfo.scalpel","c":"Result","l":"success(T)"},{"p":"lexfo.scalpel.components","c":"ErrorPopup","l":"suppressCheckBox"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"tabs"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"Task(String, Object[], Map)","u":"%3Cinit%3E(java.lang.String,java.lang.Object[],java.util.Map)"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"taskLoop()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor","l":"tasks"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"TEMPLATES_DIRNAME"},{"p":"lexfo.scalpel","c":"Terminal","l":"Terminal()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"terminalForVenvConfig"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"theme"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.Task","l":"then(Consumer)","u":"then(java.util.function.Consumer)"},{"p":"lexfo.scalpel","c":"PythonUtils","l":"toByteArray(byte[])"},{"p":"lexfo.scalpel","c":"PythonUtils","l":"toJavaBytes(byte[])"},{"p":"lexfo.scalpel","c":"PythonUtils","l":"toPythonBytes(byte[])"},{"p":"lexfo.scalpel","c":"Result","l":"toString()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.HookTabInfo","l":"toString()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane.PartialHookTabInfo","l":"toString()"},{"p":"lexfo.scalpel","c":"ScalpelExecutor.CallableData","l":"toString()"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"TRACE"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"trace(String)","u":"trace(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"type"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"type"},{"p":"lexfo.scalpel","c":"UIBuilder","l":"UIBuilder()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"uiComponent()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"uiComponent()"},{"p":"lexfo.scalpel","c":"ScalpelEditorTabbedPane","l":"uiComponent()"},{"p":"lexfo.scalpel","c":"UIUtils","l":"UIUtils()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"UnObfuscator","l":"UnObfuscator()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"updateContent(HttpRequestResponse)","u":"updateContent(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel.editors","c":"IMessageEditor","l":"updateContent(HttpRequestResponse)","u":"updateContent(burp.api.montoya.http.message.HttpRequestResponse)"},{"p":"lexfo.scalpel","c":"PythonUtils","l":"updateHeader(T, String, String)","u":"updateHeader(T,java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"updatePackagesTable()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"updatePackagesTable(Consumer)","u":"updatePackagesTable(java.util.function.Consumer)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"updatePackagesTable(Consumer, Runnable)","u":"updatePackagesTable(java.util.function.Consumer,java.lang.Runnable)"},{"p":"lexfo.scalpel.editors","c":"AbstractEditor","l":"updateRootPanel()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"updateScriptList()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"updateTerminal(String)","u":"updateTerminal(java.lang.String)"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"updateTerminal(String, String, String)","u":"updateTerminal(java.lang.String,java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"Config._ProjectData","l":"userScriptPath"},{"p":"lexfo.scalpel.editors","c":"DisplayableWhiteSpaceCharset","l":"utf8"},{"p":"lexfo.scalpel","c":"Constants","l":"VALID_HOOK_PREFIXES"},{"p":"lexfo.scalpel","c":"Result","l":"value"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"value"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"value()"},{"p":"lexfo.scalpel","c":"EditorType","l":"valueOf(String)","u":"valueOf(java.lang.String)"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"valueOf(String)","u":"valueOf(java.lang.String)"},{"p":"lexfo.scalpel","c":"EditorType","l":"values()"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"values()"},{"p":"lexfo.scalpel","c":"Constants","l":"VENV_BIN_DIR"},{"p":"lexfo.scalpel","c":"Workspace","l":"VENV_DIR"},{"p":"lexfo.scalpel","c":"Constants","l":"VENV_LIB_DIR"},{"p":"lexfo.scalpel","c":"Venv","l":"Venv()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"venvListComponent"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"venvScriptList"},{"p":"lexfo.scalpel","c":"ConfigTab","l":"venvSelectPanel"},{"p":"lexfo.scalpel","c":"Venv.PackageInfo","l":"version"},{"p":"lexfo.scalpel","c":"Scalpel","l":"waitForExecutor(MontoyaApi, ScalpelEditorProvider, ScalpelExecutor)","u":"waitForExecutor(burp.api.montoya.MontoyaApi,lexfo.scalpel.ScalpelEditorProvider,lexfo.scalpel.ScalpelExecutor)"},{"p":"lexfo.scalpel","c":"ScalpelLogger.Level","l":"WARN"},{"p":"lexfo.scalpel","c":"ScalpelLogger","l":"warn(String)","u":"warn(java.lang.String)"},{"p":"lexfo.scalpel.editors","c":"WhitspaceCharsetDecoder","l":"WhitspaceCharsetDecoder(Charset, CharsetDecoder)","u":"%3Cinit%3E(java.nio.charset.Charset,java.nio.charset.CharsetDecoder)"},{"p":"lexfo.scalpel.editors","c":"WhitspaceCharsetEncoder","l":"WhitspaceCharsetEncoder(Charset, CharsetEncoder)","u":"%3Cinit%3E(java.nio.charset.Charset,java.nio.charset.CharsetEncoder)"},{"p":"lexfo.scalpel","c":"Palette","l":"WINDOWS_COLORS"},{"p":"lexfo.scalpel","c":"Palette","l":"WINDOWS_PALETTE"},{"p":"lexfo.scalpel.components","c":"WorkingPopup","l":"WorkingPopup()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"WORKSPACE_DIRNAME"},{"p":"lexfo.scalpel","c":"RessourcesUnpacker","l":"WORKSPACE_PATH"},{"p":"lexfo.scalpel","c":"Workspace","l":"Workspace()","u":"%3Cinit%3E()"},{"p":"lexfo.scalpel","c":"Config._ProjectData","l":"workspacePath"},{"p":"lexfo.scalpel","c":"Config._GlobalData","l":"workspacePaths"},{"p":"lexfo.scalpel","c":"IO","l":"writeFile(String, String)","u":"writeFile(java.lang.String,java.lang.String)"},{"p":"lexfo.scalpel","c":"IO","l":"writeJSON(File, Object)","u":"writeJSON(java.io.File,java.lang.Object)"},{"p":"lexfo.scalpel","c":"IO","l":"writer"}];updateSearchResults(); \ No newline at end of file diff --git a/docs/public/javadoc/module-search-index.js b/docs/public/javadoc/module-search-index.js new file mode 100644 index 00000000..0d59754f --- /dev/null +++ b/docs/public/javadoc/module-search-index.js @@ -0,0 +1 @@ +moduleSearchIndex = [];updateSearchResults(); \ No newline at end of file diff --git a/docs/public/javadoc/overview-summary.html b/docs/public/javadoc/overview-summary.html new file mode 100644 index 00000000..23688589 --- /dev/null +++ b/docs/public/javadoc/overview-summary.html @@ -0,0 +1,25 @@ + + + + +scalpel 1.0.0 API + + + + + + + + + + +
+ +

index.html

+
+ + diff --git a/docs/public/javadoc/overview-tree.html b/docs/public/javadoc/overview-tree.html new file mode 100644 index 00000000..f3b4d48a --- /dev/null +++ b/docs/public/javadoc/overview-tree.html @@ -0,0 +1,215 @@ + + + + +Class Hierarchy (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
+ +
+
+
+

Hierarchy For All Packages

+Package Hierarchies: + +
+
+

Class Hierarchy

+ +
+
+

Interface Hierarchy

+ +
+
+

Enum Class Hierarchy

+ +
+
+
+
+ + diff --git a/docs/public/javadoc/package-search-index.js b/docs/public/javadoc/package-search-index.js new file mode 100644 index 00000000..46a32221 --- /dev/null +++ b/docs/public/javadoc/package-search-index.js @@ -0,0 +1 @@ +packageSearchIndex = [{"l":"All Packages","u":"allpackages-index.html"},{"l":"lexfo.scalpel"},{"l":"lexfo.scalpel.components"},{"l":"lexfo.scalpel.editors"}];updateSearchResults(); \ No newline at end of file diff --git a/docs/public/javadoc/resources/glass.png b/docs/public/javadoc/resources/glass.png new file mode 100644 index 00000000..a7f591f4 Binary files /dev/null and b/docs/public/javadoc/resources/glass.png differ diff --git a/docs/public/javadoc/resources/x.png b/docs/public/javadoc/resources/x.png new file mode 100644 index 00000000..30548a75 Binary files /dev/null and b/docs/public/javadoc/resources/x.png differ diff --git a/docs/public/javadoc/script-dir/jquery-3.6.1.min.js b/docs/public/javadoc/script-dir/jquery-3.6.1.min.js new file mode 100644 index 00000000..2c69bc90 --- /dev/null +++ b/docs/public/javadoc/script-dir/jquery-3.6.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0",options:{classes:{},disabled:!1,create:null},_createWidget:function(t,e){e=x(e||this.defaultElement||this)[0],this.element=x(e),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=x(),this.hoverable=x(),this.focusable=x(),this.classesElementLookup={},e!==this&&(x.data(e,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===e&&this.destroy()}}),this.document=x(e.style?e.ownerDocument:e.document||e),this.window=x(this.document[0].defaultView||this.document[0].parentWindow)),this.options=x.widget.extend({},this.options,this._getCreateOptions(),t),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:x.noop,_create:x.noop,_init:x.noop,destroy:function(){var i=this;this._destroy(),x.each(this.classesElementLookup,function(t,e){i._removeClass(e,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:x.noop,widget:function(){return this.element},option:function(t,e){var i,s,n,o=t;if(0===arguments.length)return x.widget.extend({},this.options);if("string"==typeof t)if(o={},t=(i=t.split(".")).shift(),i.length){for(s=o[t]=x.widget.extend({},this.options[t]),n=0;n
"),i=e.children()[0];return x("body").append(e),t=i.offsetWidth,e.css("overflow","scroll"),t===(i=i.offsetWidth)&&(i=e[0].clientWidth),e.remove(),s=t-i},getScrollInfo:function(t){var e=t.isWindow||t.isDocument?"":t.element.css("overflow-x"),i=t.isWindow||t.isDocument?"":t.element.css("overflow-y"),e="scroll"===e||"auto"===e&&t.widthC(E(s),E(n))?o.important="horizontal":o.important="vertical",c.using.call(this,t,o)}),l.offset(x.extend(u,{using:t}))})},x.ui.position={fit:{left:function(t,e){var i=e.within,s=i.isWindow?i.scrollLeft:i.offset.left,n=i.width,o=t.left-e.collisionPosition.marginLeft,l=s-o,a=o+e.collisionWidth-n-s;e.collisionWidth>n?0n?0",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.lastMousePosition={x:null,y:null},this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault(),this._activateItem(t)},"click .ui-menu-item":function(t){var e=x(t.target),i=x(x.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&e.not(".ui-state-disabled").length&&(this.select(t),t.isPropagationStopped()||(this.mouseHandled=!0),e.has(".ui-menu").length?this.expand(t):!this.element.is(":focus")&&i.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":"_activateItem","mousemove .ui-menu-item":"_activateItem",mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this._menuItems().first();e||this.focus(t,i)},blur:function(t){this._delay(function(){x.contains(this.element[0],x.ui.safeActiveElement(this.document[0]))||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t,!0),this.mouseHandled=!1}})},_activateItem:function(t){var e,i;this.previousFilter||t.clientX===this.lastMousePosition.x&&t.clientY===this.lastMousePosition.y||(this.lastMousePosition={x:t.clientX,y:t.clientY},e=x(t.target).closest(".ui-menu-item"),i=x(t.currentTarget),e[0]===i[0]&&(i.is(".ui-state-active")||(this._removeClass(i.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(t,i))))},_destroy:function(){var t=this.element.find(".ui-menu-item").removeAttr("role aria-disabled").children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),t.children().each(function(){var t=x(this);t.data("ui-menu-submenu-caret")&&t.remove()})},_keydown:function(t){var e,i,s,n=!0;switch(t.keyCode){case x.ui.keyCode.PAGE_UP:this.previousPage(t);break;case x.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case x.ui.keyCode.HOME:this._move("first","first",t);break;case x.ui.keyCode.END:this._move("last","last",t);break;case x.ui.keyCode.UP:this.previous(t);break;case x.ui.keyCode.DOWN:this.next(t);break;case x.ui.keyCode.LEFT:this.collapse(t);break;case x.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case x.ui.keyCode.ENTER:case x.ui.keyCode.SPACE:this._activate(t);break;case x.ui.keyCode.ESCAPE:this.collapse(t);break;default:e=this.previousFilter||"",s=n=!1,i=96<=t.keyCode&&t.keyCode<=105?(t.keyCode-96).toString():String.fromCharCode(t.keyCode),clearTimeout(this.filterTimer),i===e?s=!0:i=e+i,e=this._filterMenuItems(i),(e=s&&-1!==e.index(this.active.next())?this.active.nextAll(".ui-menu-item"):e).length||(i=String.fromCharCode(t.keyCode),e=this._filterMenuItems(i)),e.length?(this.focus(t,e),this.previousFilter=i,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}n&&t.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var t,e,s=this,n=this.options.icons.submenu,i=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),e=i.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=x(this),e=t.prev(),i=x("").data("ui-menu-submenu-caret",!0);s._addClass(i,"ui-menu-icon","ui-icon "+n),e.attr("aria-haspopup","true").prepend(i),t.attr("aria-labelledby",e.attr("id"))}),this._addClass(e,"ui-menu","ui-widget ui-widget-content ui-front"),(t=i.add(this.element).find(this.options.items)).not(".ui-menu-item").each(function(){var t=x(this);s._isDivider(t)&&s._addClass(t,"ui-menu-divider","ui-widget-content")}),i=(e=t.not(".ui-menu-item, .ui-menu-divider")).children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(e,"ui-menu-item")._addClass(i,"ui-menu-item-wrapper"),t.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!x.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){var i;"icons"===t&&(i=this.element.find(".ui-menu-icon"),this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",String(t)),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),i=this.active.children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",i.attr("id")),i=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(i,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),(i=e.children(".ui-menu")).length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(t){var e,i,s;this._hasScroll()&&(i=parseFloat(x.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(x.css(this.activeMenu[0],"paddingTop"))||0,e=t.offset().top-this.activeMenu.offset().top-i-s,i=this.activeMenu.scrollTop(),s=this.activeMenu.height(),t=t.outerHeight(),e<0?this.activeMenu.scrollTop(i+e):s",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,liveRegionTimer:null,_create:function(){var i,s,n,t=this.element[0].nodeName.toLowerCase(),e="textarea"===t,t="input"===t;this.isMultiLine=e||!t&&this._isContentEditable(this.element),this.valueMethod=this.element[e||t?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(t){if(this.element.prop("readOnly"))s=n=i=!0;else{s=n=i=!1;var e=x.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:i=!0,this._move("previousPage",t);break;case e.PAGE_DOWN:i=!0,this._move("nextPage",t);break;case e.UP:i=!0,this._keyEvent("previous",t);break;case e.DOWN:i=!0,this._keyEvent("next",t);break;case e.ENTER:this.menu.active&&(i=!0,t.preventDefault(),this.menu.select(t));break;case e.TAB:this.menu.active&&this.menu.select(t);break;case e.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(t),t.preventDefault());break;default:s=!0,this._searchTimeout(t)}}},keypress:function(t){if(i)return i=!1,void(this.isMultiLine&&!this.menu.element.is(":visible")||t.preventDefault());if(!s){var e=x.ui.keyCode;switch(t.keyCode){case e.PAGE_UP:this._move("previousPage",t);break;case e.PAGE_DOWN:this._move("nextPage",t);break;case e.UP:this._keyEvent("previous",t);break;case e.DOWN:this._keyEvent("next",t)}}},input:function(t){if(n)return n=!1,void t.preventDefault();this._searchTimeout(t)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){clearTimeout(this.searching),this.close(t),this._change(t)}}),this._initSource(),this.menu=x("
    ").appendTo(this._appendTo()).menu({role:null}).hide().attr({unselectable:"on"}).menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(t){t.preventDefault()},menufocus:function(t,e){var i,s;if(this.isNewMenu&&(this.isNewMenu=!1,t.originalEvent&&/^mouse/.test(t.originalEvent.type)))return this.menu.blur(),void this.document.one("mousemove",function(){x(t.target).trigger(t.originalEvent)});s=e.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",t,{item:s})&&t.originalEvent&&/^key/.test(t.originalEvent.type)&&this._value(s.value),(i=e.item.attr("aria-label")||s.value)&&String.prototype.trim.call(i).length&&(clearTimeout(this.liveRegionTimer),this.liveRegionTimer=this._delay(function(){this.liveRegion.html(x("
    ").text(i))},100))},menuselect:function(t,e){var i=e.item.data("ui-autocomplete-item"),s=this.previous;this.element[0]!==x.ui.safeActiveElement(this.document[0])&&(this.element.trigger("focus"),this.previous=s,this._delay(function(){this.previous=s,this.selectedItem=i})),!1!==this._trigger("select",t,{item:i})&&this._value(i.value),this.term=this._value(),this.close(t),this.selectedItem=i}}),this.liveRegion=x("
    ",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(t){var e=this.menu.element[0];return t.target===this.element[0]||t.target===e||x.contains(e,t.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var t=this.options.appendTo;return t=!(t=!(t=t&&(t.jquery||t.nodeType?x(t):this.document.find(t).eq(0)))||!t[0]?this.element.closest(".ui-front, dialog"):t).length?this.document[0].body:t},_initSource:function(){var i,s,n=this;Array.isArray(this.options.source)?(i=this.options.source,this.source=function(t,e){e(x.ui.autocomplete.filter(i,t.term))}):"string"==typeof this.options.source?(s=this.options.source,this.source=function(t,e){n.xhr&&n.xhr.abort(),n.xhr=x.ajax({url:s,data:t,dataType:"json",success:function(t){e(t)},error:function(){e([])}})}):this.source=this.options.source},_searchTimeout:function(s){clearTimeout(this.searching),this.searching=this._delay(function(){var t=this.term===this._value(),e=this.menu.element.is(":visible"),i=s.altKey||s.ctrlKey||s.metaKey||s.shiftKey;t&&(e||i)||(this.selectedItem=null,this.search(null,s))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length").append(x("
    ").text(e.label)).appendTo(t)},_move:function(t,e){if(this.menu.element.is(":visible"))return this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),void this.menu.blur()):void this.menu[t](e);this.search(null,e)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){this.isMultiLine&&!this.menu.element.is(":visible")||(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),x.extend(x.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(t,e){var i=new RegExp(x.ui.autocomplete.escapeRegex(e),"i");return x.grep(t,function(t){return i.test(t.label||t.value||t)})}}),x.widget("ui.autocomplete",x.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(1").text(e))},100))}});x.ui.autocomplete}); \ No newline at end of file diff --git a/docs/public/javadoc/script.js b/docs/public/javadoc/script.js new file mode 100644 index 00000000..0765364e --- /dev/null +++ b/docs/public/javadoc/script.js @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2013, 2020, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +var moduleSearchIndex; +var packageSearchIndex; +var typeSearchIndex; +var memberSearchIndex; +var tagSearchIndex; +function loadScripts(doc, tag) { + createElem(doc, tag, 'search.js'); + + createElem(doc, tag, 'module-search-index.js'); + createElem(doc, tag, 'package-search-index.js'); + createElem(doc, tag, 'type-search-index.js'); + createElem(doc, tag, 'member-search-index.js'); + createElem(doc, tag, 'tag-search-index.js'); +} + +function createElem(doc, tag, path) { + var script = doc.createElement(tag); + var scriptElement = doc.getElementsByTagName(tag)[0]; + script.src = pathtoroot + path; + scriptElement.parentNode.insertBefore(script, scriptElement); +} + +function show(tableId, selected, columns) { + if (tableId !== selected) { + document.querySelectorAll('div.' + tableId + ':not(.' + selected + ')') + .forEach(function(elem) { + elem.style.display = 'none'; + }); + } + document.querySelectorAll('div.' + selected) + .forEach(function(elem, index) { + elem.style.display = ''; + var isEvenRow = index % (columns * 2) < columns; + elem.classList.remove(isEvenRow ? oddRowColor : evenRowColor); + elem.classList.add(isEvenRow ? evenRowColor : oddRowColor); + }); + updateTabs(tableId, selected); +} + +function updateTabs(tableId, selected) { + document.querySelector('div#' + tableId +' .summary-table') + .setAttribute('aria-labelledby', selected); + document.querySelectorAll('button[id^="' + tableId + '"]') + .forEach(function(tab, index) { + if (selected === tab.id || (tableId === selected && index === 0)) { + tab.className = activeTableTab; + tab.setAttribute('aria-selected', true); + tab.setAttribute('tabindex',0); + } else { + tab.className = tableTab; + tab.setAttribute('aria-selected', false); + tab.setAttribute('tabindex',-1); + } + }); +} + +function switchTab(e) { + var selected = document.querySelector('[aria-selected=true]'); + if (selected) { + if ((e.keyCode === 37 || e.keyCode === 38) && selected.previousSibling) { + // left or up arrow key pressed: move focus to previous tab + selected.previousSibling.click(); + selected.previousSibling.focus(); + e.preventDefault(); + } else if ((e.keyCode === 39 || e.keyCode === 40) && selected.nextSibling) { + // right or down arrow key pressed: move focus to next tab + selected.nextSibling.click(); + selected.nextSibling.focus(); + e.preventDefault(); + } + } +} + +var updateSearchResults = function() {}; + +function indexFilesLoaded() { + return moduleSearchIndex + && packageSearchIndex + && typeSearchIndex + && memberSearchIndex + && tagSearchIndex; +} + +// Workaround for scroll position not being included in browser history (8249133) +document.addEventListener("DOMContentLoaded", function(e) { + var contentDiv = document.querySelector("div.flex-content"); + window.addEventListener("popstate", function(e) { + if (e.state !== null) { + contentDiv.scrollTop = e.state; + } + }); + window.addEventListener("hashchange", function(e) { + history.replaceState(contentDiv.scrollTop, document.title); + }); + contentDiv.addEventListener("scroll", function(e) { + var timeoutID; + if (!timeoutID) { + timeoutID = setTimeout(function() { + history.replaceState(contentDiv.scrollTop, document.title); + timeoutID = null; + }, 100); + } + }); + if (!location.hash) { + history.replaceState(contentDiv.scrollTop, document.title); + } +}); diff --git a/docs/public/javadoc/search.js b/docs/public/javadoc/search.js new file mode 100644 index 00000000..13aba853 --- /dev/null +++ b/docs/public/javadoc/search.js @@ -0,0 +1,354 @@ +/* + * Copyright (c) 2015, 2020, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ + +var noResult = {l: "No results found"}; +var loading = {l: "Loading search index..."}; +var catModules = "Modules"; +var catPackages = "Packages"; +var catTypes = "Classes and Interfaces"; +var catMembers = "Members"; +var catSearchTags = "Search Tags"; +var highlight = "$&"; +var searchPattern = ""; +var fallbackPattern = ""; +var RANKING_THRESHOLD = 2; +var NO_MATCH = 0xffff; +var MIN_RESULTS = 3; +var MAX_RESULTS = 500; +var UNNAMED = ""; +function escapeHtml(str) { + return str.replace(//g, ">"); +} +function getHighlightedText(item, matcher, fallbackMatcher) { + var escapedItem = escapeHtml(item); + var highlighted = escapedItem.replace(matcher, highlight); + if (highlighted === escapedItem) { + highlighted = escapedItem.replace(fallbackMatcher, highlight) + } + return highlighted; +} +function getURLPrefix(ui) { + var urlPrefix=""; + var slash = "/"; + if (ui.item.category === catModules) { + return ui.item.l + slash; + } else if (ui.item.category === catPackages && ui.item.m) { + return ui.item.m + slash; + } else if (ui.item.category === catTypes || ui.item.category === catMembers) { + if (ui.item.m) { + urlPrefix = ui.item.m + slash; + } else { + $.each(packageSearchIndex, function(index, item) { + if (item.m && ui.item.p === item.l) { + urlPrefix = item.m + slash; + } + }); + } + } + return urlPrefix; +} +function createSearchPattern(term) { + var pattern = ""; + var isWordToken = false; + term.replace(/,\s*/g, ", ").trim().split(/\s+/).forEach(function(w, index) { + if (index > 0) { + // whitespace between identifiers is significant + pattern += (isWordToken && /^\w/.test(w)) ? "\\s+" : "\\s*"; + } + var tokens = w.split(/(?=[A-Z,.()<>[\/])/); + for (var i = 0; i < tokens.length; i++) { + var s = tokens[i]; + if (s === "") { + continue; + } + pattern += $.ui.autocomplete.escapeRegex(s); + isWordToken = /\w$/.test(s); + if (isWordToken) { + pattern += "([a-z0-9_$<>\\[\\]]*?)"; + } + } + }); + return pattern; +} +function createMatcher(pattern, flags) { + var isCamelCase = /[A-Z]/.test(pattern); + return new RegExp(pattern, flags + (isCamelCase ? "" : "i")); +} +var watermark = 'Search'; +$(function() { + var search = $("#search-input"); + var reset = $("#reset-button"); + search.val(''); + search.prop("disabled", false); + reset.prop("disabled", false); + search.val(watermark).addClass('watermark'); + search.blur(function() { + if ($(this).val().length === 0) { + $(this).val(watermark).addClass('watermark'); + } + }); + search.on('click keydown paste', function() { + if ($(this).val() === watermark) { + $(this).val('').removeClass('watermark'); + } + }); + reset.click(function() { + search.val('').focus(); + }); + search.focus()[0].setSelectionRange(0, 0); +}); +$.widget("custom.catcomplete", $.ui.autocomplete, { + _create: function() { + this._super(); + this.widget().menu("option", "items", "> :not(.ui-autocomplete-category)"); + }, + _renderMenu: function(ul, items) { + var rMenu = this; + var currentCategory = ""; + rMenu.menu.bindings = $(); + $.each(items, function(index, item) { + var li; + if (item.category && item.category !== currentCategory) { + ul.append("
  • " + item.category + "
  • "); + currentCategory = item.category; + } + li = rMenu._renderItemData(ul, item); + if (item.category) { + li.attr("aria-label", item.category + " : " + item.l); + li.attr("class", "result-item"); + } else { + li.attr("aria-label", item.l); + li.attr("class", "result-item"); + } + }); + }, + _renderItem: function(ul, item) { + var label = ""; + var matcher = createMatcher(escapeHtml(searchPattern), "g"); + var fallbackMatcher = new RegExp(fallbackPattern, "gi") + if (item.category === catModules) { + label = getHighlightedText(item.l, matcher, fallbackMatcher); + } else if (item.category === catPackages) { + label = getHighlightedText(item.l, matcher, fallbackMatcher); + } else if (item.category === catTypes) { + label = (item.p && item.p !== UNNAMED) + ? getHighlightedText(item.p + "." + item.l, matcher, fallbackMatcher) + : getHighlightedText(item.l, matcher, fallbackMatcher); + } else if (item.category === catMembers) { + label = (item.p && item.p !== UNNAMED) + ? getHighlightedText(item.p + "." + item.c + "." + item.l, matcher, fallbackMatcher) + : getHighlightedText(item.c + "." + item.l, matcher, fallbackMatcher); + } else if (item.category === catSearchTags) { + label = getHighlightedText(item.l, matcher, fallbackMatcher); + } else { + label = item.l; + } + var li = $("
  • ").appendTo(ul); + var div = $("
    ").appendTo(li); + if (item.category === catSearchTags && item.h) { + if (item.d) { + div.html(label + " (" + item.h + ")
    " + + item.d + "
    "); + } else { + div.html(label + " (" + item.h + ")"); + } + } else { + if (item.m) { + div.html(item.m + "/" + label); + } else { + div.html(label); + } + } + return li; + } +}); +function rankMatch(match, category) { + if (!match) { + return NO_MATCH; + } + var index = match.index; + var input = match.input; + var leftBoundaryMatch = 2; + var periferalMatch = 0; + // make sure match is anchored on a left word boundary + if (index === 0 || /\W/.test(input[index - 1]) || "_" === input[index]) { + leftBoundaryMatch = 0; + } else if ("_" === input[index - 1] || (input[index] === input[index].toUpperCase() && !/^[A-Z0-9_$]+$/.test(input))) { + leftBoundaryMatch = 1; + } + var matchEnd = index + match[0].length; + var leftParen = input.indexOf("("); + var endOfName = leftParen > -1 ? leftParen : input.length; + // exclude peripheral matches + if (category !== catModules && category !== catSearchTags) { + var delim = category === catPackages ? "/" : "."; + if (leftParen > -1 && leftParen < index) { + periferalMatch += 2; + } else if (input.lastIndexOf(delim, endOfName) >= matchEnd) { + periferalMatch += 2; + } + } + var delta = match[0].length === endOfName ? 0 : 1; // rank full match higher than partial match + for (var i = 1; i < match.length; i++) { + // lower ranking if parts of the name are missing + if (match[i]) + delta += match[i].length; + } + if (category === catTypes) { + // lower ranking if a type name contains unmatched camel-case parts + if (/[A-Z]/.test(input.substring(matchEnd))) + delta += 5; + if (/[A-Z]/.test(input.substring(0, index))) + delta += 5; + } + return leftBoundaryMatch + periferalMatch + (delta / 200); + +} +function doSearch(request, response) { + var result = []; + searchPattern = createSearchPattern(request.term); + fallbackPattern = createSearchPattern(request.term.toLowerCase()); + if (searchPattern === "") { + return this.close(); + } + var camelCaseMatcher = createMatcher(searchPattern, ""); + var fallbackMatcher = new RegExp(fallbackPattern, "i"); + + function searchIndexWithMatcher(indexArray, matcher, category, nameFunc) { + if (indexArray) { + var newResults = []; + $.each(indexArray, function (i, item) { + item.category = category; + var ranking = rankMatch(matcher.exec(nameFunc(item)), category); + if (ranking < RANKING_THRESHOLD) { + newResults.push({ranking: ranking, item: item}); + } + return newResults.length <= MAX_RESULTS; + }); + return newResults.sort(function(e1, e2) { + return e1.ranking - e2.ranking; + }).map(function(e) { + return e.item; + }); + } + return []; + } + function searchIndex(indexArray, category, nameFunc) { + var primaryResults = searchIndexWithMatcher(indexArray, camelCaseMatcher, category, nameFunc); + result = result.concat(primaryResults); + if (primaryResults.length <= MIN_RESULTS && !camelCaseMatcher.ignoreCase) { + var secondaryResults = searchIndexWithMatcher(indexArray, fallbackMatcher, category, nameFunc); + result = result.concat(secondaryResults.filter(function (item) { + return primaryResults.indexOf(item) === -1; + })); + } + } + + searchIndex(moduleSearchIndex, catModules, function(item) { return item.l; }); + searchIndex(packageSearchIndex, catPackages, function(item) { + return (item.m && request.term.indexOf("/") > -1) + ? (item.m + "/" + item.l) : item.l; + }); + searchIndex(typeSearchIndex, catTypes, function(item) { + return request.term.indexOf(".") > -1 ? item.p + "." + item.l : item.l; + }); + searchIndex(memberSearchIndex, catMembers, function(item) { + return request.term.indexOf(".") > -1 + ? item.p + "." + item.c + "." + item.l : item.l; + }); + searchIndex(tagSearchIndex, catSearchTags, function(item) { return item.l; }); + + if (!indexFilesLoaded()) { + updateSearchResults = function() { + doSearch(request, response); + } + result.unshift(loading); + } else { + updateSearchResults = function() {}; + } + response(result); +} +$(function() { + $("#search-input").catcomplete({ + minLength: 1, + delay: 300, + source: doSearch, + response: function(event, ui) { + if (!ui.content.length) { + ui.content.push(noResult); + } else { + $("#search-input").empty(); + } + }, + autoFocus: true, + focus: function(event, ui) { + return false; + }, + position: { + collision: "flip" + }, + select: function(event, ui) { + if (ui.item.category) { + var url = getURLPrefix(ui); + if (ui.item.category === catModules) { + url += "module-summary.html"; + } else if (ui.item.category === catPackages) { + if (ui.item.u) { + url = ui.item.u; + } else { + url += ui.item.l.replace(/\./g, '/') + "/package-summary.html"; + } + } else if (ui.item.category === catTypes) { + if (ui.item.u) { + url = ui.item.u; + } else if (ui.item.p === UNNAMED) { + url += ui.item.l + ".html"; + } else { + url += ui.item.p.replace(/\./g, '/') + "/" + ui.item.l + ".html"; + } + } else if (ui.item.category === catMembers) { + if (ui.item.p === UNNAMED) { + url += ui.item.c + ".html" + "#"; + } else { + url += ui.item.p.replace(/\./g, '/') + "/" + ui.item.c + ".html" + "#"; + } + if (ui.item.u) { + url += ui.item.u; + } else { + url += ui.item.l; + } + } else if (ui.item.category === catSearchTags) { + url += ui.item.u; + } + if (top !== window) { + parent.classFrame.location = pathtoroot + url; + } else { + window.location.href = pathtoroot + url; + } + $("#search-input").focus(); + } + } + }); +}); diff --git a/docs/public/javadoc/serialized-form.html b/docs/public/javadoc/serialized-form.html new file mode 100644 index 00000000..3c57dd7c --- /dev/null +++ b/docs/public/javadoc/serialized-form.html @@ -0,0 +1,304 @@ + + + + +Serialized Form (scalpel 1.0.0 API) + + + + + + + + + + + + + + +
    + +
    +
    +
    +

    Serialized Form

    +
    + +
    +
    +
    + + diff --git a/docs/public/javadoc/stylesheet.css b/docs/public/javadoc/stylesheet.css new file mode 100644 index 00000000..6dc5b365 --- /dev/null +++ b/docs/public/javadoc/stylesheet.css @@ -0,0 +1,866 @@ +/* + * Javadoc style sheet + */ + +@import url('resources/fonts/dejavu.css'); + +/* + * Styles for individual HTML elements. + * + * These are styles that are specific to individual HTML elements. Changing them affects the style of a particular + * HTML element throughout the page. + */ + +body { + background-color:#ffffff; + color:#353833; + font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:14px; + margin:0; + padding:0; + height:100%; + width:100%; +} +iframe { + margin:0; + padding:0; + height:100%; + width:100%; + overflow-y:scroll; + border:none; +} +a:link, a:visited { + text-decoration:none; + color:#4A6782; +} +a[href]:hover, a[href]:focus { + text-decoration:none; + color:#bb7a2a; +} +a[name] { + color:#353833; +} +pre { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; +} +h1 { + font-size:20px; +} +h2 { + font-size:18px; +} +h3 { + font-size:16px; +} +h4 { + font-size:15px; +} +h5 { + font-size:14px; +} +h6 { + font-size:13px; +} +ul { + list-style-type:disc; +} +code, tt { + font-family:'DejaVu Sans Mono', monospace; +} +:not(h1, h2, h3, h4, h5, h6) > code, +:not(h1, h2, h3, h4, h5, h6) > tt { + font-size:14px; + padding-top:4px; + margin-top:8px; + line-height:1.4em; +} +dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; +} +.summary-table dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + vertical-align:top; + padding-top:4px; +} +sup { + font-size:8px; +} +button { + font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size: 14px; +} +/* + * Styles for HTML generated by javadoc. + * + * These are style classes that are used by the standard doclet to generate HTML documentation. + */ + +/* + * Styles for document title and copyright. + */ +.clear { + clear:both; + height:0; + overflow:hidden; +} +.about-language { + float:right; + padding:0 21px 8px 8px; + font-size:11px; + margin-top:-9px; + height:2.9em; +} +.legal-copy { + margin-left:.5em; +} +.tab { + background-color:#0066FF; + color:#ffffff; + padding:8px; + width:5em; + font-weight:bold; +} +/* + * Styles for navigation bar. + */ +@media screen { + .flex-box { + position:fixed; + display:flex; + flex-direction:column; + height: 100%; + width: 100%; + } + .flex-header { + flex: 0 0 auto; + } + .flex-content { + flex: 1 1 auto; + overflow-y: auto; + } +} +.top-nav { + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + min-height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.sub-nav { + background-color:#dee3e9; + float:left; + width:100%; + overflow:hidden; + font-size:12px; +} +.sub-nav div { + clear:left; + float:left; + padding:0 0 5px 6px; + text-transform:uppercase; +} +.sub-nav .nav-list { + padding-top:5px; +} +ul.nav-list { + display:block; + margin:0 25px 0 0; + padding:0; +} +ul.sub-nav-list { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.nav-list li { + list-style:none; + float:left; + padding: 5px 6px; + text-transform:uppercase; +} +.sub-nav .nav-list-search { + float:right; + margin:0 0 0 0; + padding:5px 6px; + clear:none; +} +.nav-list-search label { + position:relative; + right:-16px; +} +ul.sub-nav-list li { + list-style:none; + float:left; + padding-top:10px; +} +.top-nav a:link, .top-nav a:active, .top-nav a:visited { + color:#FFFFFF; + text-decoration:none; + text-transform:uppercase; +} +.top-nav a:hover { + text-decoration:none; + color:#bb7a2a; + text-transform:uppercase; +} +.nav-bar-cell1-rev { + background-color:#F8981D; + color:#253441; + margin: auto 5px; +} +.skip-nav { + position:absolute; + top:auto; + left:-9999px; + overflow:hidden; +} +/* + * Hide navigation links and search box in print layout + */ +@media print { + ul.nav-list, div.sub-nav { + display:none; + } +} +/* + * Styles for page header and footer. + */ +.title { + color:#2c4557; + margin:10px 0; +} +.sub-title { + margin:5px 0 0 0; +} +.header ul { + margin:0 0 15px 0; + padding:0; +} +.header ul li, .footer ul li { + list-style:none; + font-size:13px; +} +/* + * Styles for headings. + */ +body.class-declaration-page .summary h2, +body.class-declaration-page .details h2, +body.class-use-page h2, +body.module-declaration-page .block-list h2 { + font-style: italic; + padding:0; + margin:15px 0; +} +body.class-declaration-page .summary h3, +body.class-declaration-page .details h3, +body.class-declaration-page .summary .inherited-list h2 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +/* + * Styles for page layout containers. + */ +main { + clear:both; + padding:10px 20px; + position:relative; +} +dl.notes > dt { + font-family: 'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:12px; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +dl.notes > dd { + margin:5px 10px 10px 0; + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; +} +dl.name-value > dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +dl.name-value > dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* + * Styles for lists. + */ +li.circle { + list-style:circle; +} +ul.horizontal li { + display:inline; + font-size:0.9em; +} +div.inheritance { + margin:0; + padding:0; +} +div.inheritance div.inheritance { + margin-left:2em; +} +ul.block-list, +ul.details-list, +ul.member-list, +ul.summary-list { + margin:10px 0 10px 0; + padding:0; +} +ul.block-list > li, +ul.details-list > li, +ul.member-list > li, +ul.summary-list > li { + list-style:none; + margin-bottom:15px; + line-height:1.4; +} +.summary-table dl, .summary-table dl dt, .summary-table dl dd { + margin-top:0; + margin-bottom:1px; +} +ul.see-list, ul.see-list-long { + padding-left: 0; + list-style: none; +} +ul.see-list li { + display: inline; +} +ul.see-list li:not(:last-child):after, +ul.see-list-long li:not(:last-child):after { + content: ", "; + white-space: pre-wrap; +} +/* + * Styles for tables. + */ +.summary-table, .details-table { + width:100%; + border-spacing:0; + border-left:1px solid #EEE; + border-right:1px solid #EEE; + border-bottom:1px solid #EEE; + padding:0; +} +.caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:#253441; + font-weight:bold; + clear:none; + overflow:hidden; + padding:0; + padding-top:10px; + padding-left:1px; + margin:0; + white-space:pre; +} +.caption a:link, .caption a:visited { + color:#1f389c; +} +.caption a:hover, +.caption a:active { + color:#FFFFFF; +} +.caption span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + padding-bottom:7px; + display:inline-block; + float:left; + background-color:#F8981D; + border: none; + height:16px; +} +div.table-tabs { + padding:10px 0 0 1px; + margin:0; +} +div.table-tabs > button { + border: none; + cursor: pointer; + padding: 5px 12px 7px 12px; + font-weight: bold; + margin-right: 3px; +} +div.table-tabs > button.active-table-tab { + background: #F8981D; + color: #253441; +} +div.table-tabs > button.table-tab { + background: #4D7A97; + color: #FFFFFF; +} +.two-column-summary { + display: grid; + grid-template-columns: minmax(15%, max-content) minmax(15%, auto); +} +.three-column-summary { + display: grid; + grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto); +} +.four-column-summary { + display: grid; + grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto); +} +@media screen and (max-width: 600px) { + .two-column-summary { + display: grid; + grid-template-columns: 1fr; + } +} +@media screen and (max-width: 800px) { + .three-column-summary { + display: grid; + grid-template-columns: minmax(10%, max-content) minmax(25%, auto); + } + .three-column-summary .col-last { + grid-column-end: span 2; + } +} +@media screen and (max-width: 1000px) { + .four-column-summary { + display: grid; + grid-template-columns: minmax(15%, max-content) minmax(15%, auto); + } +} +.summary-table > div, .details-table > div { + text-align:left; + padding: 8px 3px 3px 7px; +} +.col-first, .col-second, .col-last, .col-constructor-name, .col-summary-item-name { + vertical-align:top; + padding-right:0; + padding-top:8px; + padding-bottom:3px; +} +.table-header { + background:#dee3e9; + font-weight: bold; +} +.col-first, .col-first { + font-size:13px; +} +.col-second, .col-second, .col-last, .col-constructor-name, .col-summary-item-name, .col-last { + font-size:13px; +} +.col-first, .col-second, .col-constructor-name { + vertical-align:top; + overflow: auto; +} +.col-last { + white-space:normal; +} +.col-first a:link, .col-first a:visited, +.col-second a:link, .col-second a:visited, +.col-first a:link, .col-first a:visited, +.col-second a:link, .col-second a:visited, +.col-constructor-name a:link, .col-constructor-name a:visited, +.col-summary-item-name a:link, .col-summary-item-name a:visited, +.constant-values-container a:link, .constant-values-container a:visited, +.all-classes-container a:link, .all-classes-container a:visited, +.all-packages-container a:link, .all-packages-container a:visited { + font-weight:bold; +} +.table-sub-heading-color { + background-color:#EEEEFF; +} +.even-row-color, .even-row-color .table-header { + background-color:#FFFFFF; +} +.odd-row-color, .odd-row-color .table-header { + background-color:#EEEEEF; +} +/* + * Styles for contents. + */ +.deprecated-content { + margin:0; + padding:10px 0; +} +div.block { + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; +} +.col-last div { + padding-top:0; +} +.col-last a { + padding-bottom:3px; +} +.module-signature, +.package-signature, +.type-signature, +.member-signature { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + margin:14px 0; + white-space: pre-wrap; +} +.module-signature, +.package-signature, +.type-signature { + margin-top: 0; +} +.member-signature .type-parameters-long, +.member-signature .parameters, +.member-signature .exceptions { + display: inline-block; + vertical-align: top; + white-space: pre; +} +.member-signature .type-parameters { + white-space: normal; +} +/* + * Styles for formatting effect. + */ +.source-line-no { + color:green; + padding:0 30px 0 0; +} +h1.hidden { + visibility:hidden; + overflow:hidden; + font-size:10px; +} +.block { + display:block; + margin:0 10px 5px 0; + color:#474747; +} +.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link, +.module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type, +.package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { + font-weight:bold; +} +.deprecation-comment, .help-footnote, .preview-comment { + font-style:italic; +} +.deprecation-block { + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; + border-style:solid; + border-width:thin; + border-radius:10px; + padding:10px; + margin-bottom:10px; + margin-right:10px; + display:inline-block; +} +.preview-block { + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; + border-style:solid; + border-width:thin; + border-radius:10px; + padding:10px; + margin-bottom:10px; + margin-right:10px; + display:inline-block; +} +div.block div.deprecation-comment { + font-style:normal; +} +/* + * Styles specific to HTML5 elements. + */ +main, nav, header, footer, section { + display:block; +} +/* + * Styles for javadoc search. + */ +.ui-autocomplete-category { + font-weight:bold; + font-size:15px; + padding:7px 0 7px 3px; + background-color:#4D7A97; + color:#FFFFFF; +} +.ui-autocomplete { + max-height:85%; + max-width:65%; + overflow-y:scroll; + overflow-x:scroll; + white-space:nowrap; + box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23); +} +ul.ui-autocomplete { + position:fixed; + z-index:999999; + background-color: #FFFFFF; +} +ul.ui-autocomplete li { + float:left; + clear:both; + width:100%; +} +.ui-autocomplete .result-item { + font-size: inherit; +} +.ui-autocomplete .result-highlight { + font-weight:bold; +} +#search-input { + background-image:url('resources/glass.png'); + background-size:13px; + background-repeat:no-repeat; + background-position:2px 3px; + padding-left:20px; + position:relative; + right:-18px; + width:400px; +} +#reset-button { + background-color: rgb(255,255,255); + background-image:url('resources/x.png'); + background-position:center; + background-repeat:no-repeat; + background-size:12px; + border:0 none; + width:16px; + height:16px; + position:relative; + left:-4px; + top:-4px; + font-size:0px; +} +.watermark { + color:#545454; +} +.search-tag-desc-result { + font-style:italic; + font-size:11px; +} +.search-tag-holder-result { + font-style:italic; + font-size:12px; +} +.search-tag-result:target { + background-color:yellow; +} +.module-graph span { + display:none; + position:absolute; +} +.module-graph:hover span { + display:block; + margin: -100px 0 0 100px; + z-index: 1; +} +.inherited-list { + margin: 10px 0 10px 0; +} +section.class-description { + line-height: 1.4; +} +.summary section[class$="-summary"], .details section[class$="-details"], +.class-uses .detail, .serialized-class-details { + padding: 0px 20px 5px 10px; + border: 1px solid #ededed; + background-color: #f8f8f8; +} +.inherited-list, section[class$="-details"] .detail { + padding:0 0 5px 8px; + background-color:#ffffff; + border:none; +} +.vertical-separator { + padding: 0 5px; +} +ul.help-section-list { + margin: 0; +} +ul.help-subtoc > li { + display: inline-block; + padding-right: 5px; + font-size: smaller; +} +ul.help-subtoc > li::before { + content: "\2022" ; + padding-right:2px; +} +span.help-note { + font-style: italic; +} +/* + * Indicator icon for external links. + */ +main a[href*="://"]::after { + content:""; + display:inline-block; + background-image:url('data:image/svg+xml; utf8, \ + \ + \ + '); + background-size:100% 100%; + width:7px; + height:7px; + margin-left:2px; + margin-bottom:4px; +} +main a[href*="://"]:hover::after, +main a[href*="://"]:focus::after { + background-image:url('data:image/svg+xml; utf8, \ + \ + \ + '); +} + +/* + * Styles for user-provided tables. + * + * borderless: + * No borders, vertical margins, styled caption. + * This style is provided for use with existing doc comments. + * In general, borderless tables should not be used for layout purposes. + * + * plain: + * Plain borders around table and cells, vertical margins, styled caption. + * Best for small tables or for complex tables for tables with cells that span + * rows and columns, when the "striped" style does not work well. + * + * striped: + * Borders around the table and vertical borders between cells, striped rows, + * vertical margins, styled caption. + * Best for tables that have a header row, and a body containing a series of simple rows. + */ + +table.borderless, +table.plain, +table.striped { + margin-top: 10px; + margin-bottom: 10px; +} +table.borderless > caption, +table.plain > caption, +table.striped > caption { + font-weight: bold; + font-size: smaller; +} +table.borderless th, table.borderless td, +table.plain th, table.plain td, +table.striped th, table.striped td { + padding: 2px 5px; +} +table.borderless, +table.borderless > thead > tr > th, table.borderless > tbody > tr > th, table.borderless > tr > th, +table.borderless > thead > tr > td, table.borderless > tbody > tr > td, table.borderless > tr > td { + border: none; +} +table.borderless > thead > tr, table.borderless > tbody > tr, table.borderless > tr { + background-color: transparent; +} +table.plain { + border-collapse: collapse; + border: 1px solid black; +} +table.plain > thead > tr, table.plain > tbody tr, table.plain > tr { + background-color: transparent; +} +table.plain > thead > tr > th, table.plain > tbody > tr > th, table.plain > tr > th, +table.plain > thead > tr > td, table.plain > tbody > tr > td, table.plain > tr > td { + border: 1px solid black; +} +table.striped { + border-collapse: collapse; + border: 1px solid black; +} +table.striped > thead { + background-color: #E3E3E3; +} +table.striped > thead > tr > th, table.striped > thead > tr > td { + border: 1px solid black; +} +table.striped > tbody > tr:nth-child(even) { + background-color: #EEE +} +table.striped > tbody > tr:nth-child(odd) { + background-color: #FFF +} +table.striped > tbody > tr > th, table.striped > tbody > tr > td { + border-left: 1px solid black; + border-right: 1px solid black; +} +table.striped > tbody > tr > th { + font-weight: normal; +} +/** + * Tweak font sizes and paddings for small screens. + */ +@media screen and (max-width: 1050px) { + #search-input { + width: 300px; + } +} +@media screen and (max-width: 800px) { + #search-input { + width: 200px; + } + .top-nav, + .bottom-nav { + font-size: 11px; + padding-top: 6px; + } + .sub-nav { + font-size: 11px; + } + .about-language { + padding-right: 16px; + } + ul.nav-list li, + .sub-nav .nav-list-search { + padding: 6px; + } + ul.sub-nav-list li { + padding-top: 5px; + } + main { + padding: 10px; + } + .summary section[class$="-summary"], .details section[class$="-details"], + .class-uses .detail, .serialized-class-details { + padding: 0 8px 5px 8px; + } + body { + -webkit-text-size-adjust: none; + } +} +@media screen and (max-width: 500px) { + #search-input { + width: 150px; + } + .top-nav, + .bottom-nav { + font-size: 10px; + } + .sub-nav { + font-size: 10px; + } + .about-language { + font-size: 10px; + padding-right: 12px; + } +} diff --git a/docs/public/javadoc/tag-search-index.js b/docs/public/javadoc/tag-search-index.js new file mode 100644 index 00000000..bf10aaf6 --- /dev/null +++ b/docs/public/javadoc/tag-search-index.js @@ -0,0 +1 @@ +tagSearchIndex = [{"l":"Constant Field Values","h":"","u":"constant-values.html"},{"l":"Serialized Form","h":"","u":"serialized-form.html"}];updateSearchResults(); \ No newline at end of file diff --git a/docs/public/javadoc/type-search-index.js b/docs/public/javadoc/type-search-index.js new file mode 100644 index 00000000..68ff88ef --- /dev/null +++ b/docs/public/javadoc/type-search-index.js @@ -0,0 +1 @@ +typeSearchIndex = [{"p":"lexfo.scalpel","l":"Config._GlobalData"},{"p":"lexfo.scalpel","l":"Config._ProjectData"},{"p":"lexfo.scalpel.editors","l":"AbstractEditor"},{"l":"All Classes and Interfaces","u":"allclasses-index.html"},{"p":"lexfo.scalpel","l":"Async"},{"p":"lexfo.scalpel","l":"ScalpelExecutor.CallableData"},{"p":"lexfo.scalpel","l":"CommandChecker"},{"p":"lexfo.scalpel","l":"Config"},{"p":"lexfo.scalpel","l":"ConfigTab"},{"p":"lexfo.scalpel","l":"Constants"},{"p":"lexfo.scalpel","l":"ScalpelExecutor.CustomEnquirer"},{"p":"lexfo.scalpel.editors","l":"DisplayableWhiteSpaceCharset"},{"p":"lexfo.scalpel","l":"EditorType"},{"p":"lexfo.scalpel.components","l":"ErrorDialog"},{"p":"lexfo.scalpel.components","l":"ErrorPopup"},{"p":"lexfo.scalpel","l":"ScalpelEditorTabbedPane.HookTabInfo"},{"p":"lexfo.scalpel.editors","l":"IMessageEditor"},{"p":"lexfo.scalpel","l":"IO"},{"p":"lexfo.scalpel","l":"IO.IORunnable"},{"p":"lexfo.scalpel","l":"IO.IOSupplier"},{"p":"lexfo.scalpel","l":"ScalpelLogger.Level"},{"p":"lexfo.scalpel","l":"Venv.PackageInfo"},{"p":"lexfo.scalpel","l":"Palette"},{"p":"lexfo.scalpel","l":"ScalpelEditorTabbedPane.PartialHookTabInfo"},{"p":"lexfo.scalpel.components","l":"PlaceholderTextField"},{"p":"lexfo.scalpel","l":"PythonSetup"},{"p":"lexfo.scalpel","l":"PythonUtils"},{"p":"lexfo.scalpel","l":"RessourcesUnpacker"},{"p":"lexfo.scalpel","l":"Result"},{"p":"lexfo.scalpel","l":"Scalpel"},{"p":"lexfo.scalpel.editors","l":"ScalpelBinaryEditor"},{"p":"lexfo.scalpel.editors","l":"ScalpelDecimalEditor"},{"p":"lexfo.scalpel","l":"ScalpelEditorProvider"},{"p":"lexfo.scalpel","l":"ScalpelEditorTabbedPane"},{"p":"lexfo.scalpel","l":"ScalpelExecutor"},{"p":"lexfo.scalpel.editors","l":"ScalpelGenericBinaryEditor"},{"p":"lexfo.scalpel.editors","l":"ScalpelHexEditor"},{"p":"lexfo.scalpel","l":"ScalpelHttpRequestHandler"},{"p":"lexfo.scalpel","l":"ScalpelLogger"},{"p":"lexfo.scalpel.editors","l":"ScalpelOctalEditor"},{"p":"lexfo.scalpel.editors","l":"ScalpelRawEditor"},{"p":"lexfo.scalpel.components","l":"SettingsPanel"},{"p":"lexfo.scalpel","l":"ScalpelExecutor.Task"},{"p":"lexfo.scalpel","l":"Terminal"},{"p":"lexfo.scalpel","l":"UIBuilder"},{"p":"lexfo.scalpel","l":"UIUtils"},{"p":"lexfo.scalpel","l":"UnObfuscator"},{"p":"lexfo.scalpel","l":"Venv"},{"p":"lexfo.scalpel.editors","l":"WhitspaceCharsetDecoder"},{"p":"lexfo.scalpel.editors","l":"WhitspaceCharsetEncoder"},{"p":"lexfo.scalpel.components","l":"WorkingPopup"},{"p":"lexfo.scalpel","l":"Workspace"}];updateSearchResults(); \ No newline at end of file diff --git a/docs/public/logo-docs.svg b/docs/public/logo-docs.svg new file mode 100644 index 00000000..05b61f9d --- /dev/null +++ b/docs/public/logo-docs.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/public/overview-faq/index.html b/docs/public/overview-faq/index.html new file mode 100644 index 00000000..d06247eb --- /dev/null +++ b/docs/public/overview-faq/index.html @@ -0,0 +1,282 @@ + + + + + + + + + FAQ + + + + + + + + + + + + +
    + +
    + + + + + Edit on GitHub + + + +

    #  FAQ

    +

    #  Table of Contents

    +
      +
    1. Why does Scalpel depend on JDK whereas Burp comes with its own JRE?
    2. +
    3. Why using Java with Jep to execute Python whereas Burp already supports Python extensions with Jython?
    4. +
    5. Once the .jar is loaded, no additional request shows up in the editor
    6. +
    7. My distribution/OS comes with an outdated python.
    8. +
    9. Configuring my editor for Python
    10. +
    11. I installed Python using the Microsoft Store and Scalpel doesn’t work.
    12. +
    +
    +

    #  Why does Scalpel depend on JDK whereas Burp comes with its own JRE?

    +
      +
    • Scalpel uses a project called jep to call Python from Java. jep needs a JDK to function.
    • +
    • If you are curious or need more technical information about Scalpel’s implementation, read How scalpel works.
    • +
    +

    #  Why using Java with Jep to execute Python whereas Burp already supports Python extensions with Jython?

    +
      +
    • Jython supports up to Python 2.7. Unfortunately, Python 3 is not handled at all. Python 2.7 is basically a dead language and nobody should still be using it.
    • +
    • Burp’s developers released a new API for extensions and deprecated the former one. The new version only supports Java. That’s why the most appropriate choice was to reimplement a partial Python scripting support for Burp.
    • +
    +

    #  Once the .jar is loaded, no additional request shows up in the editor!

    +
      +
    • When first installing Scalpel, the installation of all its dependencies may take a while. Look at the “Output” logs in the Burp “Extension” tab to ensure that the extension has completed.
    • +
    • Examine the “Errors” logs in the Burp “Extension” tab. There should be an explicit error message with some tips to solve the problem.
    • +
    • Make sure you followed the installation guidelines. In case you didn’t, remove the ~/.scalpel directory and try one more time.
    • +
    • If the error message doesn’t help, please open a GitHub issue including the “Output” and “Errors” logs, and your system information (OS / Distribution version, CPU architecture, JDK and Python version and installation path, environment variables which Burp runs with, and so forth).
    • +
    +

    #  Scalpel requires python >=3.8 but my distribution is outdated and I can’t install such recent Python versions using the package manager.

    +
      +
    • Try updating your distribution.
    • +
    • If that is not possible, you must setup a separate Python >=3.8 installation and run Burp with the appropriate environment so this separate installation is used. +
      +

      💡 Tip: Use pyenv to easily install different Python versions and switch between them.

      +
      +
    • +
    +

    #  How can I configure my editor to recognize the Python library

    +
      +
    • +

      Python modules are extracted in ~/.scalpel/.extracted/python, adding this to your PYTHONPATH should do it.

      +
    • +
    • +

      For VSCode users, Scalpel extracts a .vscode containing the correct settings in the scripts directory, so you can simply open the folder and it should work out of the box.

      +
        +
      • +

        Alternatively, you can use the following settings.json:

        +
        {
        +    "terminal.integrated.env.linux": {
        +        "PYTHONPATH": "${env:HOME}/.scalpel/extracted/python:${env:PYTHONPATH}",
        +        "PATH": "${env:HOME}/.scalpel/extracted/python:${env:PATH}"
        +    },
        +
        +    // '~' or ${env:HOME} is not supported by this setting, it must be replaced manually.
        +    "python.analysis.extraPaths": ["<REPLACE_WITH_ABSOLUTE_HOME_PATH>/.extracted/python"]
        +}
        +
      • +
      +
    • +
    +

    #  I installed Python using the Microsoft Store and Scalpel doesn’t work.

    +
      +
    • The Microsoft Store Python is a sandboxed version designed for educational purposes. Many of its behaviors are incompatible with Scalpel. To use Scalpel on Windows, it is required to install Python from the official source.
    • +
    +

    #  error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1

    +
      +
    • Some users encouter this error when the python developpement libraries are missing:
    • +
    +
    x86_64-linux-gnu-gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -fPIC -DPACKAGE=jep -DUSE_DEALLOC=1 -DJEP_NUMPY_ENABLED=0 -DVERSION=\"4.1.1\" -DPYTHON_LDLIBRARY=\"libpython3.10.so\" -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -Isrc/main/c/Include -Ibuild/include -I/home/<user>/.scalpel/venvs/default/.venv/include -I/usr/include/python3.10 -c src/main/c/Jep/convert_j2p.c -o build/temp.linux-x86_64-cpython-310/src/main/c/Jep/convert_j2p.o
    +      In file included from src/main/c/Include/Jep.h:35,
    +                       from src/main/c/Jep/convert_j2p.c:28:
    +      src/main/c/Include/jep_platform.h:35:10: fatal error: Python.h: Aucun fichier ou dossier de ce type
    +         35 | #include <Python.h>
    +            |          ^~~~~~~~~~
    +      compilation terminated.
    +      error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1
    +      [end of output]
    +
    + + +
    +
    + + + diff --git a/docs/public/overview-installation/index.html b/docs/public/overview-installation/index.html new file mode 100644 index 00000000..785fd10d --- /dev/null +++ b/docs/public/overview-installation/index.html @@ -0,0 +1,269 @@ + + + + + + + + + Installation + + + + + + + + + + + + +
    + +
    + + + + + Edit on GitHub + + + +

    #  Installation

    +

    #  Requirements

    +
      +
    • OpenJDK >= 17
    • +
    • Python >= 3.8
    • +
    • pip
    • +
    • python-virtualenv
    • +
    +

    #  Debian-based distributions

    +

    The following packages are required:

    +
    sudo apt install build-essential python3 python3-dev python3-venv openjdk-17-jdk
    +

    #  Fedora / RHEL / CentOS

    +

    The following packages are required:

    +
    sudo dnf install @development-tools python3 python3-devel python3-virtualenv java-17-openjdk-devel
    +

    #  Arch-based distributions

    +

    The following packages are required:

    +
    sudo pacman -S base-devel python python-pip python-virtualenv jdk-openjdk
    +

    #  Windows

    +

    Microsoft Visual C++ >=14.0 is required: +https://visualstudio.microsoft.com/visual-cpp-build-tools/

    +

    #  Step-by-step instructions

    +
      +
    1. +

      Download the latest JAR release. +

      +
      +

      +
    2. +
    3. +

      Import the .jar to Burp. +

      +
      +

      +
    4. +
    5. +

      Wait for the dependencies to install. +

      +
      +

      +
    6. +
    7. +

      Once Scalpel is properly initialized, you should get the following. +

      +
      +

      +
    8. +
    9. +

      If the installation was successful, a Scalpel tab should show in the Request/Response editor as follows: +

      +
      +

      +
    10. +
    11. +

      And also a Scalpel tab for configuration to install additional packages via terminal. +

      +
      +

      +
    12. +
    +

    Scalpel is now properly installed and initialized!

    +
    +

    #  ðŸ’¡ To unload and reload Scalpel, you must restart Burp, simply disabling and re-enabling the extension will not work

    +
    +

    #  What’s next

    +
      +
    • Check the Usage page to get a glimpse of how to use the tool.
    • +
    • Read this tutorial to see Scalpel in a real use case context.
    • +
    + + +
    +
    + + + diff --git a/docs/public/overview-usage/index.html b/docs/public/overview-usage/index.html new file mode 100644 index 00000000..3f7ca7d2 --- /dev/null +++ b/docs/public/overview-usage/index.html @@ -0,0 +1,222 @@ + + + + + + + + + Usage + + + + + + + + + + + + +
    + +
    + + + + + Edit on GitHub + + + +

    #  Usage

    +

    Scalpel allows you to programmatically intercept and modify HTTP requests/responses going through Burp, as well as creating custom request/response editors with Python.

    +

    To do so, Scalpel provides a Burp extension GUI for scripting and a set of predefined function names corresponding to specific actions:

    +
      +
    • match: Determine whether an event should be handled by a hook.
    • +
    • request: Intercept and rewrite a request.
    • +
    • response: Intercept and rewrite a response.
    • +
    • req_edit_in: Create or update a request editor’s content from a request.
    • +
    • req_edit_out: Update a request from an editor’s modified content.
    • +
    • res_edit_in: Create or update a response editor’s content from a response.
    • +
    • res_edit_out: Update a response from an editor’s modified content.
    • +
    +

    Simply write a Python script implementing the ones you need and load the file with Scalpel Burp GUI:

    +
    +

    + +
    +

    #  ðŸ’¡ To get started with Scalpel, see First steps

    +
    +

    #  Further reading

    +

    Learn more about the predefined function names and find examples in the Features category.

    + + +
    +
    + + + diff --git a/docs/public/pdoc/index.html b/docs/public/pdoc/index.html new file mode 100644 index 00000000..249ae660 --- /dev/null +++ b/docs/public/pdoc/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/public/pdoc/python3-10.html b/docs/public/pdoc/python3-10.html new file mode 100644 index 00000000..e0ea4e3d --- /dev/null +++ b/docs/public/pdoc/python3-10.html @@ -0,0 +1,296 @@ + + + + + + + python3-10 API documentation + + + + + + + + + +
    +
    +

    +python3-10

    + +

    Python libraries bundled with Scalpel

    + +
    + +

    pyscalpel

    + +

    This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension.

    + +

    It provides many utilities to manipulate HTTP requests, responses and converting data.

    + +
    + +

    qs

    + +

    A small module to parse PHP-style query strings.

    + +

    Used by pyscalpel

    + +
    + +

    ↠Go back to the user documentation

    +
    + + + + + +
     1"""
    + 2# Python libraries bundled with Scalpel
    + 3
    + 4---
    + 5
    + 6## [pyscalpel](python3-10/pyscalpel.html)
    + 7This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension.
    + 8
    + 9It provides many utilities to manipulate HTTP requests, responses and converting data.
    +10
    +11---
    +12
    +13## [qs](python3-10/qs.html)
    +14A small module to parse PHP-style query strings.
    +15
    +16Used by pyscalpel
    +17
    +18---
    +19
    +20# [↠Go back to the user documentation](../)
    +21"""
    +22
    +23import pyscalpel
    +24import qs
    +25
    +26
    +27__all__ = ["pyscalpel", "qs"]
    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel.html b/docs/public/pdoc/python3-10/pyscalpel.html new file mode 100644 index 00000000..50ecc634 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel.html @@ -0,0 +1,3916 @@ + + + + + + + python3-10.pyscalpel API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel

    + +

    This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension.

    + +

    It provides many utilities to manipulate HTTP requests, responses and converting data.

    +
    + + + + + +
     1"""
    + 2This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension.
    + 3
    + 4It provides many utilities to manipulate HTTP requests, responses and converting data.
    + 5"""
    + 6
    + 7from pyscalpel.http import Request, Response, Flow
    + 8from pyscalpel.edit import editor
    + 9from pyscalpel.burp_utils import ctx as _context
    +10from pyscalpel.java.scalpel_types import Context
    +11from pyscalpel.logger import Logger, logger
    +12from pyscalpel.events import MatchEvent
    +13from . import http
    +14from . import java
    +15from . import encoding
    +16from . import utils
    +17from . import burp_utils
    +18from . import venv
    +19from . import edit
    +20
    +21ctx: Context = _context
    +22"""The Scalpel Python execution context
    +23
    +24Contains the Burp Java API object, the venv directory, the user script path,
    +25the path to the file loading the user script and a logging object
    +26"""
    +27
    +28
    +29__all__ = [
    +30    "http",
    +31    "java",
    +32    "encoding",
    +33    "utils",
    +34    "burp_utils",
    +35    "venv",
    +36    "edit",
    +37    "Request",
    +38    "Response",
    +39    "Flow",
    +40    "ctx",
    +41    "Context",
    +42    "MatchEvent",
    +43    "editor",
    +44    "logger",
    +45    "Logger",
    +46]
    +
    + + +
    +
    + +
    + + class + Request: + + + +
    + +
     70class Request:
    + 71    """A "Burp oriented" HTTP request class
    + 72
    + 73
    + 74    This class allows to manipulate Burp requests in a Pythonic way.
    + 75    """
    + 76
    + 77    _Port = int
    + 78    _QueryParam = tuple[str, str]
    + 79    _ParsedQuery = tuple[_QueryParam, ...]
    + 80    _HttpVersion = str
    + 81    _HeaderKey = str
    + 82    _HeaderValue = str
    + 83    _Header = tuple[_HeaderKey, _HeaderValue]
    + 84    _Host = str
    + 85    _Method = str
    + 86    _Scheme = Literal["http", "https"]
    + 87    _Authority = str
    + 88    _Content = bytes
    + 89    _Path = str
    + 90
    + 91    host: _Host
    + 92    port: _Port
    + 93    method: _Method
    + 94    scheme: _Scheme
    + 95    authority: _Authority
    + 96
    + 97    # Path also includes URI parameters (;), query (?) and fragment (#)
    + 98    # Simply because it is more conveninent to manipulate that way in a pentensting context
    + 99    # It also mimics the way mitmproxy works.
    +100    path: _Path
    +101
    +102    http_version: _HttpVersion
    +103    _headers: Headers
    +104    _serializer: FormSerializer | None = None
    +105    _deserialized_content: Any = None
    +106    _content: _Content | None = None
    +107    _old_deserialized_content: Any = None
    +108    _is_form_initialized: bool = False
    +109    update_content_length: bool = True
    +110
    +111    def __init__(
    +112        self,
    +113        method: str,
    +114        scheme: Literal["http", "https"],
    +115        host: str,
    +116        port: int,
    +117        path: str,
    +118        http_version: str,
    +119        headers: (
    +120            Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]]
    +121        ),
    +122        authority: str,
    +123        content: bytes | None,
    +124    ):
    +125        self.scheme = scheme
    +126        self.host = host
    +127        self.port = port
    +128        self.path = path
    +129        self.method = method
    +130        self.authority = authority
    +131        self.http_version = http_version
    +132        self.headers = headers if isinstance(headers, Headers) else Headers(headers)
    +133        self._content = content
    +134
    +135        # Initialize the serializer (json,urlencoded,multipart)
    +136        self.update_serializer_from_content_type(
    +137            self.headers.get("Content-Type"), fail_silently=True
    +138        )
    +139
    +140        # Initialize old deserialized content to avoid modifying content if it has not been modified
    +141        # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py)
    +142        self._old_deserialized_content = deepcopy(self._deserialized_content)
    +143
    +144    def _del_header(self, header: str) -> bool:
    +145        if header in self._headers.keys():
    +146            del self._headers[header]
    +147            return True
    +148
    +149        return False
    +150
    +151    def _update_content_length(self) -> None:
    +152        if self.update_content_length:
    +153            if self._content is None:
    +154                self._del_header("Content-Length")
    +155            else:
    +156                length = len(cast(bytes, self._content))
    +157                self._headers["Content-Length"] = str(length)
    +158
    +159    @staticmethod
    +160    def _parse_qs(query_string: str) -> _ParsedQuery:
    +161        return tuple(urllib.parse.parse_qsl(query_string))
    +162
    +163    @staticmethod
    +164    def _parse_url(
    +165        url: str,
    +166    ) -> tuple[_Scheme, _Host, _Port, _Path]:
    +167        scheme, host, port, path = url_parse(url)
    +168
    +169        # This method is only used to create HTTP requests from URLs
    +170        #   so we can ensure the scheme is valid for this usage
    +171        if scheme not in (b"http", b"https"):
    +172            scheme = b"http"
    +173
    +174        return cast(
    +175            tuple[Literal["http", "https"], str, int, str],
    +176            (scheme.decode("ascii"), host.decode("idna"), port, path.decode("ascii")),
    +177        )
    +178
    +179    @staticmethod
    +180    def _unparse_url(scheme: _Scheme, host: _Host, port: _Port, path: _Path) -> str:
    +181        return url_unparse(scheme, host, port, path)
    +182
    +183    @classmethod
    +184    def make(
    +185        cls,
    +186        method: str,
    +187        url: str,
    +188        content: bytes | str = "",
    +189        headers: (
    +190            Headers
    +191            | dict[str | bytes, str | bytes]
    +192            | dict[str, str]
    +193            | dict[bytes, bytes]
    +194            | Iterable[tuple[bytes, bytes]]
    +195        ) = (),
    +196    ) -> Request:
    +197        """Create a request from an URL
    +198
    +199        Args:
    +200            method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)
    +201            url (str): The request URL
    +202            content (bytes | str, optional): The request content. Defaults to "".
    +203            headers (Headers, optional): The request headers. Defaults to ().
    +204
    +205        Returns:
    +206            Request: The HTTP request
    +207        """
    +208        scalpel_headers: Headers
    +209        match headers:
    +210            case Headers():
    +211                scalpel_headers = headers
    +212            case dict():
    +213                casted_headers = cast(dict[str | bytes, str | bytes], headers)
    +214                scalpel_headers = Headers(
    +215                    (
    +216                        (always_bytes(key), always_bytes(val))
    +217                        for key, val in casted_headers.items()
    +218                    )
    +219                )
    +220            case _:
    +221                scalpel_headers = Headers(headers)
    +222
    +223        scheme, host, port, path = Request._parse_url(url)
    +224        http_version = "HTTP/1.1"
    +225
    +226        # Inferr missing Host header from URL
    +227        host_header = scalpel_headers.get("Host")
    +228        if host_header is None:
    +229            match (scheme, port):
    +230                case ("http", 80) | ("https", 443):
    +231                    host_header = host
    +232                case _:
    +233                    host_header = f"{host}:{port}"
    +234
    +235            scalpel_headers["Host"] = host_header
    +236
    +237        authority: str = host_header
    +238        encoded_content = always_bytes(content)
    +239
    +240        assert isinstance(host, str)
    +241
    +242        return cls(
    +243            method=method,
    +244            scheme=scheme,
    +245            host=host,
    +246            port=port,
    +247            path=path,
    +248            http_version=http_version,
    +249            headers=scalpel_headers,
    +250            authority=authority,
    +251            content=encoded_content,
    +252        )
    +253
    +254    @classmethod
    +255    def from_burp(
    +256        cls, request: IHttpRequest, service: IHttpService | None = None
    +257    ) -> Request:  # pragma: no cover (uses Java API)
    +258        """Construct an instance of the Request class from a Burp suite HttpRequest.
    +259        :param request: The Burp suite HttpRequest to convert.
    +260        :return: A Request with the same data as the Burp suite HttpRequest.
    +261        """
    +262        service = service or request.httpService()
    +263        body = get_bytes(request.body())
    +264
    +265        # Burp will give you lowercased and pseudo headers when using HTTP/2.
    +266        # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-.
    +267        # https://blog.yaakov.online/http-2-header-casing/
    +268        headers: Headers = Headers.from_burp(request.headers())
    +269
    +270        # Burp gives a 0 length byte array body even when it doesn't exist, instead of null.
    +271        # Empty but existing bodies without a Content-Length header are lost in the process.
    +272        if not body and not headers.get("Content-Length"):
    +273            body = None
    +274
    +275        # request.url() gives a relative url for some reason
    +276        # So we have to parse and unparse to get the full path
    +277        #   (path + parameters + query + fragment)
    +278        _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url())
    +279
    +280        # Concatenate the path components
    +281        # Empty parameters,query and fragment are lost in the process
    +282        # e.g.: http://example.com;?# becomes http://example.com
    +283        # To use such an URL, the user must set the path directly
    +284        # To fix this we would need to write our own URL parser, which is a bit overkill for now.
    +285        path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment))
    +286
    +287        host = ""
    +288        port = 0
    +289        scheme = "http"
    +290        if service:
    +291            host = service.host()
    +292            port = service.port()
    +293            scheme = "https" if service.secure() else "http"
    +294
    +295        return cls(
    +296            method=request.method(),
    +297            scheme=scheme,
    +298            host=host,
    +299            port=port,
    +300            path=path,
    +301            http_version=request.httpVersion() or "HTTP/1.1",
    +302            headers=headers,
    +303            authority=headers.get(":authority") or headers.get("Host") or "",
    +304            content=body,
    +305        )
    +306
    +307    def __bytes__(self) -> bytes:
    +308        """Convert the request to bytes
    +309        :return: The request as bytes.
    +310        """
    +311        # Reserialize the request to bytes.
    +312        first_line = (
    +313            b" ".join(
    +314                always_bytes(s) for s in (self.method, self.path, self.http_version)
    +315            )
    +316            + b"\r\n"
    +317        )
    +318
    +319        # Strip HTTP/2 pseudo headers.
    +320        # https://portswigger.net/burp/documentation/desktop/http2/http2-basics-for-burp-users#:~:text=HTTP/2%20specification.-,Pseudo%2Dheaders,-In%20HTTP/2
    +321        mapped_headers = tuple(
    +322            field for field in self.headers.fields if not field[0].startswith(b":")
    +323        )
    +324
    +325        if self.headers.get(b"Host") is None and self.http_version == "HTTP/2":
    +326            # Host header is not present in HTTP/2, but is required by Burp message editor.
    +327            # So we have to add it back from the :authority pseudo-header.
    +328            # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=pseudo%2Dheaders%20and-,derives,-the%20%3Aauthority%20from
    +329            mapped_headers = (
    +330                (b"Host", always_bytes(self.headers[":authority"])),
    +331            ) + tuple(mapped_headers)
    +332
    +333        # Construct the request's headers part.
    +334        headers_lines = b"".join(
    +335            b"%s: %s\r\n" % (key, val) for key, val in mapped_headers
    +336        )
    +337
    +338        # Set a default value for the request's body. (None -> b"")
    +339        body = self.content or b""
    +340
    +341        # Construct the whole request and return it.
    +342        return first_line + headers_lines + b"\r\n" + body
    +343
    +344    def to_burp(self) -> IHttpRequest:  # pragma: no cover
    +345        """Convert the request to a Burp suite :class:`IHttpRequest`.
    +346        :return: The request as a Burp suite :class:`IHttpRequest`.
    +347        """
    +348        # Convert the request to a Burp ByteArray.
    +349        request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +350
    +351        if self.port == 0:
    +352            # No networking information is available, so we build a plain network-less request.
    +353            return HttpRequest.httpRequest(request_byte_array)
    +354
    +355        # Build the Burp HTTP networking service.
    +356        service: IHttpService = HttpService.httpService(
    +357            self.host, self.port, self.scheme == "https"
    +358        )
    +359
    +360        # Instantiate and return a new Burp HTTP request.
    +361        return HttpRequest.httpRequest(service, request_byte_array)
    +362
    +363    @classmethod
    +364    def from_raw(
    +365        cls,
    +366        data: bytes | str,
    +367        real_host: str = "",
    +368        port: int = 0,
    +369        scheme: Literal["http"] | Literal["https"] | str = "http",
    +370    ) -> Request:  # pragma: no cover
    +371        """Construct an instance of the Request class from raw bytes.
    +372        :param data: The raw bytes to convert.
    +373        :param real_host: The real host to connect to.
    +374        :param port: The port of the request.
    +375        :param scheme: The scheme of the request.
    +376        :return: A :class:`Request` with the same data as the raw bytes.
    +377        """
    +378        # Convert the raw bytes to a Burp ByteArray.
    +379        # We use the Burp API to trivialize the parsing of the request from raw bytes.
    +380        str_or_byte_array: IByteArray | str = (
    +381            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +382        )
    +383
    +384        # Handle the case where the networking informations are not provided.
    +385        if port == 0:
    +386            # Instantiate and return a new Burp HTTP request without networking informations.
    +387            burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array)
    +388        else:
    +389            # Build the Burp HTTP networking service.
    +390            service: IHttpService = HttpService.httpService(
    +391                real_host, port, scheme == "https"
    +392            )
    +393
    +394            # Instantiate a new Burp HTTP request with networking informations.
    +395            burp_request: IHttpRequest = HttpRequest.httpRequest(
    +396                service, str_or_byte_array
    +397            )
    +398
    +399        # Construct the request from the Burp.
    +400        return cls.from_burp(burp_request)
    +401
    +402    @property
    +403    def url(self) -> str:
    +404        """
    +405        The full URL string, constructed from `Request.scheme`,
    +406            `Request.host`, `Request.port` and `Request.path`.
    +407
    +408        Setting this property updates these attributes as well.
    +409        """
    +410        return Request._unparse_url(self.scheme, self.host, self.port, self.path)
    +411
    +412    @url.setter
    +413    def url(self, val: str | bytes) -> None:
    +414        (self.scheme, self.host, self.port, self.path) = Request._parse_url(
    +415            always_str(val)
    +416        )
    +417
    +418    def _get_query(self) -> _ParsedQuery:
    +419        query = urllib.parse.urlparse(self.url).query
    +420        return tuple(url_decode(query))
    +421
    +422    def _set_query(self, query_data: Sequence[_QueryParam]):
    +423        query = url_encode(query_data)
    +424        _, _, path, params, _, fragment = urllib.parse.urlparse(self.url)
    +425        self.path = urllib.parse.urlunparse(["", "", path, params, query, fragment])
    +426
    +427    @property
    +428    def query(self) -> URLEncodedFormView:
    +429        """The query string parameters as a dict-like object
    +430
    +431        Returns:
    +432            QueryParamsView: The query string parameters
    +433        """
    +434        return URLEncodedFormView(
    +435            multidict.MultiDictView(self._get_query, self._set_query)
    +436        )
    +437
    +438    @query.setter
    +439    def query(self, value: Sequence[tuple[str, str]]):
    +440        self._set_query(value)
    +441
    +442    def _has_deserialized_content_changed(self) -> bool:
    +443        return self._deserialized_content != self._old_deserialized_content
    +444
    +445    def _serialize_content(self):
    +446        if self._serializer is None:
    +447            return
    +448
    +449        if self._deserialized_content is None:
    +450            self._content = None
    +451            return
    +452
    +453        self._update_serialized_content(
    +454            self._serializer.serialize(self._deserialized_content, req=self)
    +455        )
    +456
    +457    def _update_serialized_content(self, serialized: bytes):
    +458        if self._serializer is None:
    +459            self._content = serialized
    +460            return
    +461
    +462        # Update the parsed form
    +463        self._deserialized_content = self._serializer.deserialize(serialized, self)
    +464        self._old_deserialized_content = deepcopy(self._deserialized_content)
    +465
    +466        # Set the raw content directly
    +467        self._content = serialized
    +468
    +469    def _deserialize_content(self):
    +470        if self._serializer is None:
    +471            return
    +472
    +473        if self._content:
    +474            self._deserialized_content = self._serializer.deserialize(
    +475                self._content, req=self
    +476            )
    +477
    +478    def _update_deserialized_content(self, deserialized: Any):
    +479        if self._serializer is None:
    +480            return
    +481
    +482        if deserialized is None:
    +483            self._deserialized_content = None
    +484            self._old_deserialized_content = None
    +485            return
    +486
    +487        self._deserialized_content = deserialized
    +488        self._content = self._serializer.serialize(deserialized, self)
    +489        self._update_content_length()
    +490
    +491    @property
    +492    def content(self) -> bytes | None:
    +493        """The request content / body as raw bytes
    +494
    +495        Returns:
    +496            bytes | None: The content if it exists
    +497        """
    +498        if self._serializer and self._has_deserialized_content_changed():
    +499            self._update_deserialized_content(self._deserialized_content)
    +500            self._old_deserialized_content = deepcopy(self._deserialized_content)
    +501
    +502        self._update_content_length()
    +503
    +504        return self._content
    +505
    +506    @content.setter
    +507    def content(self, value: bytes | str | None):
    +508        match value:
    +509            case None:
    +510                self._content = None
    +511                self._deserialized_content = None
    +512                return
    +513            case str():
    +514                value = value.encode("latin-1")
    +515
    +516        self._update_content_length()
    +517
    +518        self._update_serialized_content(value)
    +519
    +520    @property
    +521    def body(self) -> bytes | None:
    +522        """Alias for content()
    +523
    +524        Returns:
    +525            bytes | None: The request body / content
    +526        """
    +527        return self.content
    +528
    +529    @body.setter
    +530    def body(self, value: bytes | str | None):
    +531        self.content = value
    +532
    +533    def update_serializer_from_content_type(
    +534        self,
    +535        content_type: ImplementedContentType | str | None = None,
    +536        fail_silently: bool = False,
    +537    ):
    +538        """Update the form parsing based on the given Content-Type
    +539
    +540        Args:
    +541            content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.
    +542            fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.
    +543
    +544        Raises:
    +545            FormNotParsedException: Raised when the content-type is unknown.
    +546        """
    +547        # Strip the boundary param so we can use our content-type to serializer map
    +548        _content_type: str = get_header_value_without_params(
    +549            content_type or self.headers.get("Content-Type") or ""
    +550        )
    +551
    +552        serializer = None
    +553        if _content_type in IMPLEMENTED_CONTENT_TYPES:
    +554            serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type)
    +555
    +556        if serializer is None:
    +557            if fail_silently:
    +558                serializer = self._serializer
    +559            else:
    +560                raise FormNotParsedException(
    +561                    f"Unimplemented form content-type: {_content_type}"
    +562                )
    +563        self._set_serializer(serializer)
    +564
    +565    @property
    +566    def content_type(self) -> str | None:
    +567        """The Content-Type header value.
    +568
    +569        Returns:
    +570            str | None: <=> self.headers.get("Content-Type")
    +571        """
    +572        return self.headers.get("Content-Type")
    +573
    +574    @content_type.setter
    +575    def content_type(self, value: str) -> str | None:
    +576        self.headers["Content-Type"] = value
    +577
    +578    def create_defaultform(
    +579        self,
    +580        content_type: ImplementedContentType | str | None = None,
    +581        update_header: bool = True,
    +582    ) -> MutableMapping[Any, Any]:
    +583        """Creates the form if it doesn't exist, else returns the existing one
    +584
    +585        Args:
    +586            content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.
    +587            update_header (bool, optional): Whether to update the header. Defaults to True.
    +588
    +589        Raises:
    +590            FormNotParsedException: Thrown when provided content-type has no implemented form-serializer
    +591            FormNotParsedException: Thrown when the raw content could not be parsed.
    +592
    +593        Returns:
    +594            MutableMapping[Any, Any]: The mapped form.
    +595        """
    +596        if not self._is_form_initialized or content_type:
    +597            self.update_serializer_from_content_type(content_type)
    +598
    +599            # Set content-type if it does not exist
    +600            if (content_type and update_header) or not self.headers.get_all(
    +601                "Content-Type"
    +602            ):
    +603                self.headers["Content-Type"] = content_type
    +604
    +605        serializer = self._serializer
    +606        if serializer is None:
    +607            # This should probably never trigger here as it should already be raised by update_serializer_from_content_type
    +608            raise FormNotParsedException(
    +609                f"Form of content-type {self.content_type} not implemented."
    +610            )
    +611
    +612        # Create default form.
    +613        if not self.content:
    +614            self._deserialized_content = serializer.get_empty_form(self)
    +615        elif self._deserialized_content is None:
    +616            self._deserialize_content()
    +617
    +618        if self._deserialized_content is None:
    +619            raise FormNotParsedException(
    +620                f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}"
    +621            )
    +622
    +623        if not isinstance(self._deserialized_content, serializer.deserialized_type()):
    +624            self._deserialized_content = serializer.get_empty_form(self)
    +625
    +626        self._is_form_initialized = True
    +627        return self._deserialized_content
    +628
    +629    @property
    +630    def form(self) -> MutableMapping[Any, Any]:
    +631        """Mapping from content parsed accordingly to Content-Type
    +632
    +633        Raises:
    +634            FormNotParsedException: The content could not be parsed accordingly to Content-Type
    +635
    +636        Returns:
    +637            MutableMapping[Any, Any]: The mapped request form
    +638        """
    +639        if not self._is_form_initialized:
    +640            self.update_serializer_from_content_type()
    +641
    +642        self.create_defaultform()
    +643        if self._deserialized_content is None:
    +644            raise FormNotParsedException()
    +645
    +646        self._is_form_initialized = True
    +647        return self._deserialized_content
    +648
    +649    @form.setter
    +650    def form(self, form: MutableMapping[Any, Any]):
    +651        if not self._is_form_initialized:
    +652            self.update_serializer_from_content_type()
    +653            self._is_form_initialized = True
    +654
    +655        self._deserialized_content = form
    +656
    +657        # Update raw _content
    +658        self._serialize_content()
    +659
    +660    def _set_serializer(self, serializer: FormSerializer | None):
    +661        # Update the serializer
    +662        old_serializer = self._serializer
    +663        self._serializer = serializer
    +664
    +665        if serializer is None:
    +666            self._deserialized_content = None
    +667            return
    +668
    +669        if type(serializer) == type(old_serializer):
    +670            return
    +671
    +672        if old_serializer is None:
    +673            self._deserialize_content()
    +674            return
    +675
    +676        old_form = self._deserialized_content
    +677
    +678        if old_form is None:
    +679            self._deserialize_content()
    +680            return
    +681
    +682        # Convert the form to an intermediate format for easier conversion
    +683        exported_form = old_serializer.export_form(old_form)
    +684
    +685        # Parse the intermediate data to the new serializer format
    +686        imported_form = serializer.import_form(exported_form, self)
    +687        self._deserialized_content = imported_form
    +688
    +689    def _update_serializer_and_get_form(
    +690        self, serializer: FormSerializer
    +691    ) -> MutableMapping[Any, Any] | None:
    +692        # Set the serializer and update the content
    +693        self._set_serializer(serializer)
    +694
    +695        # Return the new form
    +696        return self._deserialized_content
    +697
    +698    def _update_serializer_and_set_form(
    +699        self, serializer: FormSerializer, form: MutableMapping[Any, Any]
    +700    ) -> None:
    +701        # NOOP when the serializer is the same
    +702        self._set_serializer(serializer)
    +703
    +704        self._update_deserialized_content(form)
    +705
    +706    @property
    +707    def urlencoded_form(self) -> URLEncodedForm:
    +708        """The urlencoded form data
    +709
    +710        Converts the content to the urlencoded form format if needed.
    +711        Modification to this object will update Request.content and vice versa
    +712
    +713        Returns:
    +714            QueryParams: The urlencoded form data
    +715        """
    +716        self._is_form_initialized = True
    +717        return cast(
    +718            URLEncodedForm,
    +719            self._update_serializer_and_get_form(URLEncodedFormSerializer()),
    +720        )
    +721
    +722    @urlencoded_form.setter
    +723    def urlencoded_form(self, form: URLEncodedForm):
    +724        self._is_form_initialized = True
    +725        self._update_serializer_and_set_form(URLEncodedFormSerializer(), form)
    +726
    +727    @property
    +728    def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]:
    +729        """The JSON form data
    +730
    +731        Converts the content to the JSON form format if needed.
    +732        Modification to this object will update Request.content and vice versa
    +733
    +734        Returns:
    +735          dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data
    +736        """
    +737        self._is_form_initialized = True
    +738        if self._update_serializer_and_get_form(JSONFormSerializer()) is None:
    +739            serializer = cast(JSONFormSerializer, self._serializer)
    +740            self._deserialized_content = serializer.get_empty_form(self)
    +741
    +742        return self._deserialized_content
    +743
    +744    @json_form.setter
    +745    def json_form(self, form: dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]):
    +746        self._is_form_initialized = True
    +747        self._update_serializer_and_set_form(JSONFormSerializer(), JSONForm(form))
    +748
    +749    def _ensure_multipart_content_type(self) -> str:
    +750        content_types_headers = self.headers.get_all("Content-Type")
    +751        pattern = re.compile(
    +752            r"^multipart/form-data;\s*boundary=([^;\s]+)", re.IGNORECASE
    +753        )
    +754
    +755        # Find a valid multipart content-type header with a valid boundary
    +756        matched_content_type: str | None = None
    +757        for content_type in content_types_headers:
    +758            if pattern.match(content_type):
    +759                matched_content_type = content_type
    +760                break
    +761
    +762        # If no boundary was found, overwrite the Content-Type header
    +763        # If an user wants to avoid this behaviour,they should manually create a MultiPartForm(), convert it to bytes
    +764        #   and pass it as raw_form()
    +765        if matched_content_type is None:
    +766            # TODO: Randomly generate this? The boundary could be used to fingerprint Scalpel
    +767            new_content_type = (
    +768                "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI"
    +769            )
    +770            self.headers["Content-Type"] = new_content_type
    +771            return new_content_type
    +772
    +773        return matched_content_type
    +774
    +775    @property
    +776    def multipart_form(self) -> MultiPartForm:
    +777        """The multipart form data
    +778
    +779        Converts the content to the multipart form format if needed.
    +780        Modification to this object will update Request.content and vice versa
    +781
    +782        Returns:
    +783            MultiPartForm
    +784        """
    +785        self._is_form_initialized = True
    +786
    +787        # Keep boundary even if content-type has changed
    +788        if isinstance(self._deserialized_content, MultiPartForm):
    +789            return self._deserialized_content
    +790
    +791        # We do not have an existing form, so we have to ensure we have a content-type header with a boundary
    +792        self._ensure_multipart_content_type()
    +793
    +794        # Serialize the current form and try to parse it with the new serializer
    +795        form = self._update_serializer_and_get_form(MultiPartFormSerializer())
    +796        serializer = cast(MultiPartFormSerializer, self._serializer)
    +797
    +798        # Set a default value
    +799        if not form:
    +800            self._deserialized_content = serializer.get_empty_form(self)
    +801
    +802        # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary
    +803        if self._deserialized_content is None:
    +804            raise FormNotParsedException(
    +805                f"Could not parse content to {serializer.deserialized_type()}"
    +806            )
    +807
    +808        return self._deserialized_content
    +809
    +810    @multipart_form.setter
    +811    def multipart_form(self, form: MultiPartForm):
    +812        self._is_form_initialized = True
    +813        if not isinstance(self._deserialized_content, MultiPartForm):
    +814            # Generate a multipart header because we don't have any boundary to format the multipart.
    +815            self._ensure_multipart_content_type()
    +816
    +817        return self._update_serializer_and_set_form(
    +818            MultiPartFormSerializer(), cast(MutableMapping, form)
    +819        )
    +820
    +821    @property
    +822    def cookies(self) -> multidict.MultiDictView[str, str]:
    +823        """
    +824        The request cookies.
    +825        For the most part, this behaves like a dictionary.
    +826        Modifications to the MultiDictView update `Request.headers`, and vice versa.
    +827        """
    +828        return multidict.MultiDictView(self._get_cookies, self._set_cookies)
    +829
    +830    def _get_cookies(self) -> tuple[tuple[str, str], ...]:
    +831        header = self.headers.get_all("Cookie")
    +832        return tuple(cookies.parse_cookie_headers(header))
    +833
    +834    def _set_cookies(self, value: tuple[tuple[str, str], ...]):
    +835        self.headers["cookie"] = cookies.format_cookie_header(value)
    +836
    +837    @cookies.setter
    +838    def cookies(self, value: tuple[tuple[str, str], ...] | Mapping[str, str]):
    +839        if hasattr(value, "items") and callable(getattr(value, "items")):
    +840            value = tuple(cast(Mapping[str, str], value).items())
    +841        self._set_cookies(cast(tuple[tuple[str, str], ...], value))
    +842
    +843    @property
    +844    def host_header(self) -> str | None:
    +845        """Host header value
    +846
    +847        Returns:
    +848            str | None: The host header value
    +849        """
    +850        return self.headers.get("Host")
    +851
    +852    @host_header.setter
    +853    def host_header(self, value: str | None):
    +854        self.headers["Host"] = value
    +855
    +856    def text(self, encoding="utf-8") -> str:
    +857        """The decoded content
    +858
    +859        Args:
    +860            encoding (str, optional): encoding to use. Defaults to "utf-8".
    +861
    +862        Returns:
    +863            str: The decoded content
    +864        """
    +865        if self.content is None:
    +866            return ""
    +867
    +868        return self.content.decode(encoding)
    +869
    +870    @property
    +871    def headers(self) -> Headers:
    +872        """The request HTTP headers
    +873
    +874        Returns:
    +875            Headers: a case insensitive dict containing the HTTP headers
    +876        """
    +877        self._update_content_length()
    +878        return self._headers
    +879
    +880    @headers.setter
    +881    def headers(self, value: Headers):
    +882        self._headers = value
    +883        self._update_content_length()
    +884
    +885    @property
    +886    def content_length(self) -> int:
    +887        """Returns the Content-Length header value
    +888           Returns 0 if the header is absent
    +889
    +890        Args:
    +891            value (int | str): The Content-Length value
    +892
    +893        Raises:
    +894            RuntimeError: Throws RuntimeError when the value is invalid
    +895        """
    +896        content_length: str | None = self.headers.get("Content-Length")
    +897        if content_length is None:
    +898            return 0
    +899
    +900        trimmed = content_length.strip()
    +901        if not trimmed.isdigit():
    +902            raise ValueError("Content-Length does not contain only digits")
    +903
    +904        return int(trimmed)
    +905
    +906    @content_length.setter
    +907    def content_length(self, value: int | str):
    +908        if self.update_content_length:
    +909            # It is useless to manually set content-length because the value will be erased.
    +910            raise RuntimeError(
    +911                "Cannot set content_length when self.update_content_length is True"
    +912            )
    +913
    +914        if isinstance(value, int):
    +915            value = str(value)
    +916
    +917        self._headers["Content-Length"] = value
    +918
    +919    @property
    +920    def pretty_host(self) -> str:
    +921        """Returns the most approriate host
    +922        Returns self.host when it exists, else it returns self.host_header
    +923
    +924        Returns:
    +925            str: The request target host
    +926        """
    +927        return self.host or self.headers.get("Host") or ""
    +928
    +929    def host_is(self, *patterns: str) -> bool:
    +930        """Perform wildcard matching (fnmatch) on the target host.
    +931
    +932        Args:
    +933            pattern (str): The pattern to use
    +934
    +935        Returns:
    +936            bool: Whether the pattern matches
    +937        """
    +938        return host_is(self.pretty_host, *patterns)
    +939
    +940    def path_is(self, *patterns: str) -> bool:
    +941        return match_patterns(self.path, *patterns)
    +
    + + +

    A "Burp oriented" HTTP request class

    + +

    This class allows to manipulate Burp requests in a Pythonic way.

    +
    + + +
    + +
    + + Request( method: str, scheme: Literal['http', 'https'], host: str, port: int, path: str, http_version: str, headers: Union[pyscalpel.http.headers.Headers, tuple[tuple[bytes, bytes], ...], Iterable[tuple[bytes, bytes]]], authority: str, content: bytes | None) + + + +
    + +
    111    def __init__(
    +112        self,
    +113        method: str,
    +114        scheme: Literal["http", "https"],
    +115        host: str,
    +116        port: int,
    +117        path: str,
    +118        http_version: str,
    +119        headers: (
    +120            Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]]
    +121        ),
    +122        authority: str,
    +123        content: bytes | None,
    +124    ):
    +125        self.scheme = scheme
    +126        self.host = host
    +127        self.port = port
    +128        self.path = path
    +129        self.method = method
    +130        self.authority = authority
    +131        self.http_version = http_version
    +132        self.headers = headers if isinstance(headers, Headers) else Headers(headers)
    +133        self._content = content
    +134
    +135        # Initialize the serializer (json,urlencoded,multipart)
    +136        self.update_serializer_from_content_type(
    +137            self.headers.get("Content-Type"), fail_silently=True
    +138        )
    +139
    +140        # Initialize old deserialized content to avoid modifying content if it has not been modified
    +141        # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py)
    +142        self._old_deserialized_content = deepcopy(self._deserialized_content)
    +
    + + + + +
    +
    +
    + host: str + + +
    + + + + +
    +
    +
    + port: int + + +
    + + + + +
    +
    +
    + method: str + + +
    + + + + +
    +
    +
    + scheme: Literal['http', 'https'] + + +
    + + + + +
    +
    +
    + authority: str + + +
    + + + + +
    +
    +
    + path: str + + +
    + + + + +
    +
    +
    + http_version: str + + +
    + + + + +
    +
    +
    + update_content_length: bool = +True + + +
    + + + + +
    +
    + +
    + headers: pyscalpel.http.headers.Headers + + + +
    + +
    870    @property
    +871    def headers(self) -> Headers:
    +872        """The request HTTP headers
    +873
    +874        Returns:
    +875            Headers: a case insensitive dict containing the HTTP headers
    +876        """
    +877        self._update_content_length()
    +878        return self._headers
    +
    + + +

    The request HTTP headers

    + +

    Returns: + Headers: a case insensitive dict containing the HTTP headers

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + make( cls, method: str, url: str, content: bytes | str = '', headers: Union[pyscalpel.http.headers.Headers, dict[str | bytes, str | bytes], dict[str, str], dict[bytes, bytes], Iterable[tuple[bytes, bytes]]] = ()) -> Request: + + + +
    + +
    183    @classmethod
    +184    def make(
    +185        cls,
    +186        method: str,
    +187        url: str,
    +188        content: bytes | str = "",
    +189        headers: (
    +190            Headers
    +191            | dict[str | bytes, str | bytes]
    +192            | dict[str, str]
    +193            | dict[bytes, bytes]
    +194            | Iterable[tuple[bytes, bytes]]
    +195        ) = (),
    +196    ) -> Request:
    +197        """Create a request from an URL
    +198
    +199        Args:
    +200            method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)
    +201            url (str): The request URL
    +202            content (bytes | str, optional): The request content. Defaults to "".
    +203            headers (Headers, optional): The request headers. Defaults to ().
    +204
    +205        Returns:
    +206            Request: The HTTP request
    +207        """
    +208        scalpel_headers: Headers
    +209        match headers:
    +210            case Headers():
    +211                scalpel_headers = headers
    +212            case dict():
    +213                casted_headers = cast(dict[str | bytes, str | bytes], headers)
    +214                scalpel_headers = Headers(
    +215                    (
    +216                        (always_bytes(key), always_bytes(val))
    +217                        for key, val in casted_headers.items()
    +218                    )
    +219                )
    +220            case _:
    +221                scalpel_headers = Headers(headers)
    +222
    +223        scheme, host, port, path = Request._parse_url(url)
    +224        http_version = "HTTP/1.1"
    +225
    +226        # Inferr missing Host header from URL
    +227        host_header = scalpel_headers.get("Host")
    +228        if host_header is None:
    +229            match (scheme, port):
    +230                case ("http", 80) | ("https", 443):
    +231                    host_header = host
    +232                case _:
    +233                    host_header = f"{host}:{port}"
    +234
    +235            scalpel_headers["Host"] = host_header
    +236
    +237        authority: str = host_header
    +238        encoded_content = always_bytes(content)
    +239
    +240        assert isinstance(host, str)
    +241
    +242        return cls(
    +243            method=method,
    +244            scheme=scheme,
    +245            host=host,
    +246            port=port,
    +247            path=path,
    +248            http_version=http_version,
    +249            headers=scalpel_headers,
    +250            authority=authority,
    +251            content=encoded_content,
    +252        )
    +
    + + +

    Create a request from an URL

    + +

    Args: + method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...) + url (str): The request URL + content (bytes | str, optional): The request content. Defaults to "". + headers (Headers, optional): The request headers. Defaults to ().

    + +

    Returns: + Request: The HTTP request

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_burp( cls, request: pyscalpel.java.burp.http_request.IHttpRequest, service: pyscalpel.java.burp.http_service.IHttpService | None = None) -> Request: + + + +
    + +
    254    @classmethod
    +255    def from_burp(
    +256        cls, request: IHttpRequest, service: IHttpService | None = None
    +257    ) -> Request:  # pragma: no cover (uses Java API)
    +258        """Construct an instance of the Request class from a Burp suite HttpRequest.
    +259        :param request: The Burp suite HttpRequest to convert.
    +260        :return: A Request with the same data as the Burp suite HttpRequest.
    +261        """
    +262        service = service or request.httpService()
    +263        body = get_bytes(request.body())
    +264
    +265        # Burp will give you lowercased and pseudo headers when using HTTP/2.
    +266        # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-.
    +267        # https://blog.yaakov.online/http-2-header-casing/
    +268        headers: Headers = Headers.from_burp(request.headers())
    +269
    +270        # Burp gives a 0 length byte array body even when it doesn't exist, instead of null.
    +271        # Empty but existing bodies without a Content-Length header are lost in the process.
    +272        if not body and not headers.get("Content-Length"):
    +273            body = None
    +274
    +275        # request.url() gives a relative url for some reason
    +276        # So we have to parse and unparse to get the full path
    +277        #   (path + parameters + query + fragment)
    +278        _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url())
    +279
    +280        # Concatenate the path components
    +281        # Empty parameters,query and fragment are lost in the process
    +282        # e.g.: http://example.com;?# becomes http://example.com
    +283        # To use such an URL, the user must set the path directly
    +284        # To fix this we would need to write our own URL parser, which is a bit overkill for now.
    +285        path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment))
    +286
    +287        host = ""
    +288        port = 0
    +289        scheme = "http"
    +290        if service:
    +291            host = service.host()
    +292            port = service.port()
    +293            scheme = "https" if service.secure() else "http"
    +294
    +295        return cls(
    +296            method=request.method(),
    +297            scheme=scheme,
    +298            host=host,
    +299            port=port,
    +300            path=path,
    +301            http_version=request.httpVersion() or "HTTP/1.1",
    +302            headers=headers,
    +303            authority=headers.get(":authority") or headers.get("Host") or "",
    +304            content=body,
    +305        )
    +
    + + +

    Construct an instance of the Request class from a Burp suite HttpRequest.

    + +
    Parameters
    + +
      +
    • request: The Burp suite HttpRequest to convert.
    • +
    + +
    Returns
    + +
    +

    A Request with the same data as the Burp suite HttpRequest.

    +
    +
    + + +
    +
    + +
    + + def + to_burp(self) -> pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    344    def to_burp(self) -> IHttpRequest:  # pragma: no cover
    +345        """Convert the request to a Burp suite :class:`IHttpRequest`.
    +346        :return: The request as a Burp suite :class:`IHttpRequest`.
    +347        """
    +348        # Convert the request to a Burp ByteArray.
    +349        request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +350
    +351        if self.port == 0:
    +352            # No networking information is available, so we build a plain network-less request.
    +353            return HttpRequest.httpRequest(request_byte_array)
    +354
    +355        # Build the Burp HTTP networking service.
    +356        service: IHttpService = HttpService.httpService(
    +357            self.host, self.port, self.scheme == "https"
    +358        )
    +359
    +360        # Instantiate and return a new Burp HTTP request.
    +361        return HttpRequest.httpRequest(service, request_byte_array)
    +
    + + +

    Convert the request to a Burp suite IHttpRequest.

    + +
    Returns
    + +
    +

    The request as a Burp suite IHttpRequest.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_raw( cls, data: bytes | str, real_host: str = '', port: int = 0, scheme: Union[Literal['http'], Literal['https'], str] = 'http') -> Request: + + + +
    + +
    363    @classmethod
    +364    def from_raw(
    +365        cls,
    +366        data: bytes | str,
    +367        real_host: str = "",
    +368        port: int = 0,
    +369        scheme: Literal["http"] | Literal["https"] | str = "http",
    +370    ) -> Request:  # pragma: no cover
    +371        """Construct an instance of the Request class from raw bytes.
    +372        :param data: The raw bytes to convert.
    +373        :param real_host: The real host to connect to.
    +374        :param port: The port of the request.
    +375        :param scheme: The scheme of the request.
    +376        :return: A :class:`Request` with the same data as the raw bytes.
    +377        """
    +378        # Convert the raw bytes to a Burp ByteArray.
    +379        # We use the Burp API to trivialize the parsing of the request from raw bytes.
    +380        str_or_byte_array: IByteArray | str = (
    +381            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +382        )
    +383
    +384        # Handle the case where the networking informations are not provided.
    +385        if port == 0:
    +386            # Instantiate and return a new Burp HTTP request without networking informations.
    +387            burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array)
    +388        else:
    +389            # Build the Burp HTTP networking service.
    +390            service: IHttpService = HttpService.httpService(
    +391                real_host, port, scheme == "https"
    +392            )
    +393
    +394            # Instantiate a new Burp HTTP request with networking informations.
    +395            burp_request: IHttpRequest = HttpRequest.httpRequest(
    +396                service, str_or_byte_array
    +397            )
    +398
    +399        # Construct the request from the Burp.
    +400        return cls.from_burp(burp_request)
    +
    + + +

    Construct an instance of the Request class from raw bytes.

    + +
    Parameters
    + +
      +
    • data: The raw bytes to convert.
    • +
    • real_host: The real host to connect to.
    • +
    • port: The port of the request.
    • +
    • scheme: The scheme of the request.
    • +
    + +
    Returns
    + +
    +

    A Request with the same data as the raw bytes.

    +
    +
    + + +
    +
    + +
    + url: str + + + +
    + +
    402    @property
    +403    def url(self) -> str:
    +404        """
    +405        The full URL string, constructed from `Request.scheme`,
    +406            `Request.host`, `Request.port` and `Request.path`.
    +407
    +408        Setting this property updates these attributes as well.
    +409        """
    +410        return Request._unparse_url(self.scheme, self.host, self.port, self.path)
    +
    + + +

    The full URL string, constructed from Request.scheme, + Request.host, Request.port and Request.path.

    + +

    Setting this property updates these attributes as well.

    +
    + + +
    +
    + +
    + query: pyscalpel.http.body.urlencoded.URLEncodedFormView + + + +
    + +
    427    @property
    +428    def query(self) -> URLEncodedFormView:
    +429        """The query string parameters as a dict-like object
    +430
    +431        Returns:
    +432            QueryParamsView: The query string parameters
    +433        """
    +434        return URLEncodedFormView(
    +435            multidict.MultiDictView(self._get_query, self._set_query)
    +436        )
    +
    + + +

    The query string parameters as a dict-like object

    + +

    Returns: + QueryParamsView: The query string parameters

    +
    + + +
    +
    + +
    + content: bytes | None + + + +
    + +
    491    @property
    +492    def content(self) -> bytes | None:
    +493        """The request content / body as raw bytes
    +494
    +495        Returns:
    +496            bytes | None: The content if it exists
    +497        """
    +498        if self._serializer and self._has_deserialized_content_changed():
    +499            self._update_deserialized_content(self._deserialized_content)
    +500            self._old_deserialized_content = deepcopy(self._deserialized_content)
    +501
    +502        self._update_content_length()
    +503
    +504        return self._content
    +
    + + +

    The request content / body as raw bytes

    + +

    Returns: + bytes | None: The content if it exists

    +
    + + +
    +
    + +
    + body: bytes | None + + + +
    + +
    520    @property
    +521    def body(self) -> bytes | None:
    +522        """Alias for content()
    +523
    +524        Returns:
    +525            bytes | None: The request body / content
    +526        """
    +527        return self.content
    +
    + + +

    Alias for content()

    + +

    Returns: + bytes | None: The request body / content

    +
    + + +
    +
    + +
    + + def + update_serializer_from_content_type( self, content_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None, fail_silently: bool = False): + + + +
    + +
    533    def update_serializer_from_content_type(
    +534        self,
    +535        content_type: ImplementedContentType | str | None = None,
    +536        fail_silently: bool = False,
    +537    ):
    +538        """Update the form parsing based on the given Content-Type
    +539
    +540        Args:
    +541            content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.
    +542            fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.
    +543
    +544        Raises:
    +545            FormNotParsedException: Raised when the content-type is unknown.
    +546        """
    +547        # Strip the boundary param so we can use our content-type to serializer map
    +548        _content_type: str = get_header_value_without_params(
    +549            content_type or self.headers.get("Content-Type") or ""
    +550        )
    +551
    +552        serializer = None
    +553        if _content_type in IMPLEMENTED_CONTENT_TYPES:
    +554            serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type)
    +555
    +556        if serializer is None:
    +557            if fail_silently:
    +558                serializer = self._serializer
    +559            else:
    +560                raise FormNotParsedException(
    +561                    f"Unimplemented form content-type: {_content_type}"
    +562                )
    +563        self._set_serializer(serializer)
    +
    + + +

    Update the form parsing based on the given Content-Type

    + +

    Args: + content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None. + fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.

    + +

    Raises: + FormNotParsedException: Raised when the content-type is unknown.

    +
    + + +
    +
    + +
    + content_type: str | None + + + +
    + +
    565    @property
    +566    def content_type(self) -> str | None:
    +567        """The Content-Type header value.
    +568
    +569        Returns:
    +570            str | None: <=> self.headers.get("Content-Type")
    +571        """
    +572        return self.headers.get("Content-Type")
    +
    + + +

    The Content-Type header value.

    + +

    Returns: + str | None: <=> self.headers.get("Content-Type")

    +
    + + +
    +
    + +
    + + def + create_defaultform( self, content_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None, update_header: bool = True) -> MutableMapping[Any, Any]: + + + +
    + +
    578    def create_defaultform(
    +579        self,
    +580        content_type: ImplementedContentType | str | None = None,
    +581        update_header: bool = True,
    +582    ) -> MutableMapping[Any, Any]:
    +583        """Creates the form if it doesn't exist, else returns the existing one
    +584
    +585        Args:
    +586            content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.
    +587            update_header (bool, optional): Whether to update the header. Defaults to True.
    +588
    +589        Raises:
    +590            FormNotParsedException: Thrown when provided content-type has no implemented form-serializer
    +591            FormNotParsedException: Thrown when the raw content could not be parsed.
    +592
    +593        Returns:
    +594            MutableMapping[Any, Any]: The mapped form.
    +595        """
    +596        if not self._is_form_initialized or content_type:
    +597            self.update_serializer_from_content_type(content_type)
    +598
    +599            # Set content-type if it does not exist
    +600            if (content_type and update_header) or not self.headers.get_all(
    +601                "Content-Type"
    +602            ):
    +603                self.headers["Content-Type"] = content_type
    +604
    +605        serializer = self._serializer
    +606        if serializer is None:
    +607            # This should probably never trigger here as it should already be raised by update_serializer_from_content_type
    +608            raise FormNotParsedException(
    +609                f"Form of content-type {self.content_type} not implemented."
    +610            )
    +611
    +612        # Create default form.
    +613        if not self.content:
    +614            self._deserialized_content = serializer.get_empty_form(self)
    +615        elif self._deserialized_content is None:
    +616            self._deserialize_content()
    +617
    +618        if self._deserialized_content is None:
    +619            raise FormNotParsedException(
    +620                f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}"
    +621            )
    +622
    +623        if not isinstance(self._deserialized_content, serializer.deserialized_type()):
    +624            self._deserialized_content = serializer.get_empty_form(self)
    +625
    +626        self._is_form_initialized = True
    +627        return self._deserialized_content
    +
    + + +

    Creates the form if it doesn't exist, else returns the existing one

    + +

    Args: + content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None. + update_header (bool, optional): Whether to update the header. Defaults to True.

    + +

    Raises: + FormNotParsedException: Thrown when provided content-type has no implemented form-serializer + FormNotParsedException: Thrown when the raw content could not be parsed.

    + +

    Returns: + MutableMapping[Any, Any]: The mapped form.

    +
    + + +
    +
    + +
    + form: MutableMapping[Any, Any] + + + +
    + +
    629    @property
    +630    def form(self) -> MutableMapping[Any, Any]:
    +631        """Mapping from content parsed accordingly to Content-Type
    +632
    +633        Raises:
    +634            FormNotParsedException: The content could not be parsed accordingly to Content-Type
    +635
    +636        Returns:
    +637            MutableMapping[Any, Any]: The mapped request form
    +638        """
    +639        if not self._is_form_initialized:
    +640            self.update_serializer_from_content_type()
    +641
    +642        self.create_defaultform()
    +643        if self._deserialized_content is None:
    +644            raise FormNotParsedException()
    +645
    +646        self._is_form_initialized = True
    +647        return self._deserialized_content
    +
    + + +

    Mapping from content parsed accordingly to Content-Type

    + +

    Raises: + FormNotParsedException: The content could not be parsed accordingly to Content-Type

    + +

    Returns: + MutableMapping[Any, Any]: The mapped request form

    +
    + + +
    +
    + +
    + urlencoded_form: pyscalpel.http.body.urlencoded.URLEncodedForm + + + +
    + +
    706    @property
    +707    def urlencoded_form(self) -> URLEncodedForm:
    +708        """The urlencoded form data
    +709
    +710        Converts the content to the urlencoded form format if needed.
    +711        Modification to this object will update Request.content and vice versa
    +712
    +713        Returns:
    +714            QueryParams: The urlencoded form data
    +715        """
    +716        self._is_form_initialized = True
    +717        return cast(
    +718            URLEncodedForm,
    +719            self._update_serializer_and_get_form(URLEncodedFormSerializer()),
    +720        )
    +
    + + +

    The urlencoded form data

    + +

    Converts the content to the urlencoded form format if needed. +Modification to this object will update Request.content and vice versa

    + +

    Returns: + QueryParams: The urlencoded form data

    +
    + + +
    +
    + +
    + json_form: dict[str | int | float, str | int | float | bool | None | list[str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]] | dict[str | int | float, str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]]] + + + +
    + +
    727    @property
    +728    def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]:
    +729        """The JSON form data
    +730
    +731        Converts the content to the JSON form format if needed.
    +732        Modification to this object will update Request.content and vice versa
    +733
    +734        Returns:
    +735          dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data
    +736        """
    +737        self._is_form_initialized = True
    +738        if self._update_serializer_and_get_form(JSONFormSerializer()) is None:
    +739            serializer = cast(JSONFormSerializer, self._serializer)
    +740            self._deserialized_content = serializer.get_empty_form(self)
    +741
    +742        return self._deserialized_content
    +
    + + +

    The JSON form data

    + +

    Converts the content to the JSON form format if needed. +Modification to this object will update Request.content and vice versa

    + +

    Returns: + dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data

    +
    + + +
    +
    + +
    + multipart_form: pyscalpel.http.body.multipart.MultiPartForm + + + +
    + +
    775    @property
    +776    def multipart_form(self) -> MultiPartForm:
    +777        """The multipart form data
    +778
    +779        Converts the content to the multipart form format if needed.
    +780        Modification to this object will update Request.content and vice versa
    +781
    +782        Returns:
    +783            MultiPartForm
    +784        """
    +785        self._is_form_initialized = True
    +786
    +787        # Keep boundary even if content-type has changed
    +788        if isinstance(self._deserialized_content, MultiPartForm):
    +789            return self._deserialized_content
    +790
    +791        # We do not have an existing form, so we have to ensure we have a content-type header with a boundary
    +792        self._ensure_multipart_content_type()
    +793
    +794        # Serialize the current form and try to parse it with the new serializer
    +795        form = self._update_serializer_and_get_form(MultiPartFormSerializer())
    +796        serializer = cast(MultiPartFormSerializer, self._serializer)
    +797
    +798        # Set a default value
    +799        if not form:
    +800            self._deserialized_content = serializer.get_empty_form(self)
    +801
    +802        # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary
    +803        if self._deserialized_content is None:
    +804            raise FormNotParsedException(
    +805                f"Could not parse content to {serializer.deserialized_type()}"
    +806            )
    +807
    +808        return self._deserialized_content
    +
    + + +

    The multipart form data

    + +

    Converts the content to the multipart form format if needed. +Modification to this object will update Request.content and vice versa

    + +

    Returns: + MultiPartForm

    +
    + + +
    +
    + +
    + cookies: _internal_mitmproxy.coretypes.multidict.MultiDictView[str, str] + + + +
    + +
    821    @property
    +822    def cookies(self) -> multidict.MultiDictView[str, str]:
    +823        """
    +824        The request cookies.
    +825        For the most part, this behaves like a dictionary.
    +826        Modifications to the MultiDictView update `Request.headers`, and vice versa.
    +827        """
    +828        return multidict.MultiDictView(self._get_cookies, self._set_cookies)
    +
    + + +

    The request cookies. +For the most part, this behaves like a dictionary. +Modifications to the MultiDictView update Request.headers, and vice versa.

    +
    + + +
    +
    + +
    + host_header: str | None + + + +
    + +
    843    @property
    +844    def host_header(self) -> str | None:
    +845        """Host header value
    +846
    +847        Returns:
    +848            str | None: The host header value
    +849        """
    +850        return self.headers.get("Host")
    +
    + + +

    Host header value

    + +

    Returns: + str | None: The host header value

    +
    + + +
    +
    + +
    + + def + text(self, encoding='utf-8') -> str: + + + +
    + +
    856    def text(self, encoding="utf-8") -> str:
    +857        """The decoded content
    +858
    +859        Args:
    +860            encoding (str, optional): encoding to use. Defaults to "utf-8".
    +861
    +862        Returns:
    +863            str: The decoded content
    +864        """
    +865        if self.content is None:
    +866            return ""
    +867
    +868        return self.content.decode(encoding)
    +
    + + +

    The decoded content

    + +

    Args: + encoding (str, optional): encoding to use. Defaults to "utf-8".

    + +

    Returns: + str: The decoded content

    +
    + + +
    +
    + +
    + content_length: int + + + +
    + +
    885    @property
    +886    def content_length(self) -> int:
    +887        """Returns the Content-Length header value
    +888           Returns 0 if the header is absent
    +889
    +890        Args:
    +891            value (int | str): The Content-Length value
    +892
    +893        Raises:
    +894            RuntimeError: Throws RuntimeError when the value is invalid
    +895        """
    +896        content_length: str | None = self.headers.get("Content-Length")
    +897        if content_length is None:
    +898            return 0
    +899
    +900        trimmed = content_length.strip()
    +901        if not trimmed.isdigit():
    +902            raise ValueError("Content-Length does not contain only digits")
    +903
    +904        return int(trimmed)
    +
    + + +

    Returns the Content-Length header value + Returns 0 if the header is absent

    + +

    Args: + value (int | str): The Content-Length value

    + +

    Raises: + RuntimeError: Throws RuntimeError when the value is invalid

    +
    + + +
    +
    + +
    + pretty_host: str + + + +
    + +
    919    @property
    +920    def pretty_host(self) -> str:
    +921        """Returns the most approriate host
    +922        Returns self.host when it exists, else it returns self.host_header
    +923
    +924        Returns:
    +925            str: The request target host
    +926        """
    +927        return self.host or self.headers.get("Host") or ""
    +
    + + +

    Returns the most approriate host +Returns self.host when it exists, else it returns self.host_header

    + +

    Returns: + str: The request target host

    +
    + + +
    +
    + +
    + + def + host_is(self, *patterns: str) -> bool: + + + +
    + +
    929    def host_is(self, *patterns: str) -> bool:
    +930        """Perform wildcard matching (fnmatch) on the target host.
    +931
    +932        Args:
    +933            pattern (str): The pattern to use
    +934
    +935        Returns:
    +936            bool: Whether the pattern matches
    +937        """
    +938        return host_is(self.pretty_host, *patterns)
    +
    + + +

    Perform wildcard matching (fnmatch) on the target host.

    + +

    Args: + pattern (str): The pattern to use

    + +

    Returns: + bool: Whether the pattern matches

    +
    + + +
    +
    + +
    + + def + path_is(self, *patterns: str) -> bool: + + + +
    + +
    940    def path_is(self, *patterns: str) -> bool:
    +941        return match_patterns(self.path, *patterns)
    +
    + + + + +
    +
    +
    + +
    + + class + Response(_internal_mitmproxy.http.Response): + + + +
    + +
     22class Response(MITMProxyResponse):
    + 23    """A "Burp oriented" HTTP response class
    + 24
    + 25
    + 26    This class allows to manipulate Burp responses in a Pythonic way.
    + 27
    + 28    Fields:
    + 29        scheme: http or https
    + 30        host: The initiating request target host
    + 31        port: The initiating request target port
    + 32        request: The initiating request.
    + 33    """
    + 34
    + 35    scheme: Literal["http", "https"] = "http"
    + 36    host: str = ""
    + 37    port: int = 0
    + 38    request: Request | None = None
    + 39
    + 40    def __init__(
    + 41        self,
    + 42        http_version: bytes,
    + 43        status_code: int,
    + 44        reason: bytes,
    + 45        headers: Headers | tuple[tuple[bytes, bytes], ...],
    + 46        content: bytes | None,
    + 47        trailers: Headers | tuple[tuple[bytes, bytes], ...] | None,
    + 48        scheme: Literal["http", "https"] = "http",
    + 49        host: str = "",
    + 50        port: int = 0,
    + 51    ):
    + 52        # Construct the base/inherited MITMProxy response.
    + 53        super().__init__(
    + 54            http_version,
    + 55            status_code,
    + 56            reason,
    + 57            headers,
    + 58            content,
    + 59            trailers,
    + 60            timestamp_start=time.time(),
    + 61            timestamp_end=time.time(),
    + 62        )
    + 63        self.scheme = scheme
    + 64        self.host = host
    + 65        self.port = port
    + 66
    + 67    @classmethod
    + 68    # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
    + 69    # link to mitmproxy documentation
    + 70    def from_mitmproxy(cls, response: MITMProxyResponse) -> Response:
    + 71        """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    + 72        :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert.
    + 73        :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    + 74        """
    + 75        return cls(
    + 76            always_bytes(response.http_version),
    + 77            response.status_code,
    + 78            always_bytes(response.reason),
    + 79            Headers.from_mitmproxy(response.headers),
    + 80            response.content,
    + 81            Headers.from_mitmproxy(response.trailers) if response.trailers else None,
    + 82        )
    + 83
    + 84    @classmethod
    + 85    def from_burp(
    + 86        cls,
    + 87        response: IHttpResponse,
    + 88        service: IHttpService | None = None,
    + 89        request: IHttpRequest | None = None,
    + 90    ) -> Response:
    + 91        """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`."""
    + 92        body = get_bytes(cast(IByteArray, response.body())) if response.body() else b""
    + 93        scalpel_response = cls(
    + 94            always_bytes(response.httpVersion() or "HTTP/1.1"),
    + 95            response.statusCode(),
    + 96            always_bytes(response.reasonPhrase() or b""),
    + 97            Headers.from_burp(response.headers()),
    + 98            body,
    + 99            None,
    +100        )
    +101
    +102        burp_request: IHttpRequest | None = request
    +103        if burp_request is None:
    +104            try:
    +105                # Some responses can have a "initiatingRequest" field.
    +106                # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A
    +107                burp_request = response.initiatingRequest()  # type: ignore
    +108            except AttributeError:
    +109                pass
    +110
    +111        if burp_request:
    +112            scalpel_response.request = Request.from_burp(burp_request, service)
    +113
    +114        if not service and burp_request:
    +115            # The only way to check if the Java method exist without writing Java is catching the error.
    +116            service = burp_request.httpService()
    +117
    +118        if service:
    +119            scalpel_response.scheme = "https" if service.secure() else "http"
    +120            scalpel_response.host = service.host()
    +121            scalpel_response.port = service.port()
    +122
    +123        return scalpel_response
    +124
    +125    def __bytes__(self) -> bytes:
    +126        """Convert the response to raw bytes."""
    +127        # Reserialize the response to bytes.
    +128
    +129        # Format the first line of the response. (e.g. "HTTP/1.1 200 OK\r\n")
    +130        first_line = (
    +131            b" ".join(
    +132                always_bytes(s)
    +133                for s in (self.http_version, str(self.status_code), self.reason)
    +134            )
    +135            + b"\r\n"
    +136        )
    +137
    +138        # Format the response's headers part.
    +139        headers_lines = b"".join(
    +140            b"%s: %s\r\n" % (key, val) for key, val in self.headers.fields
    +141        )
    +142
    +143        # Set a default value for the response's body. (None -> b"")
    +144        body = self.content or b""
    +145
    +146        # Build the whole response and return it.
    +147        return first_line + headers_lines + b"\r\n" + body
    +148
    +149    def to_burp(self) -> IHttpResponse:  # pragma: no cover (uses Java API)
    +150        """Convert the response to a Burp suite :class:`IHttpResponse`."""
    +151        response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +152
    +153        return HttpResponse.httpResponse(response_byte_array)
    +154
    +155    @classmethod
    +156    def from_raw(
    +157        cls, data: bytes | str
    +158    ) -> Response:  # pragma: no cover (uses Java API)
    +159        """Construct an instance of the Response class from raw bytes.
    +160        :param data: The raw bytes to convert.
    +161        :return: A :class:`Response` parsed from the raw bytes.
    +162        """
    +163        # Use the Burp API to trivialize the parsing of the response from raw bytes.
    +164        # Convert the raw bytes to a Burp ByteArray.
    +165        # Plain strings are OK too.
    +166        str_or_byte_array: IByteArray | str = (
    +167            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +168        )
    +169
    +170        # Instantiate a new Burp HTTP response.
    +171        burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array)
    +172
    +173        return cls.from_burp(burp_response)
    +174
    +175    @classmethod
    +176    def make(
    +177        cls,
    +178        status_code: int = 200,
    +179        content: bytes | str = b"",
    +180        headers: Headers | tuple[tuple[bytes, bytes], ...] = (),
    +181        host: str = "",
    +182        port: int = 0,
    +183        scheme: Literal["http", "https"] = "http",
    +184    ) -> "Response":
    +185        # Use the base/inherited make method to construct a MITMProxy response.
    +186        mitmproxy_res = MITMProxyResponse.make(status_code, content, headers)
    +187
    +188        res = cls.from_mitmproxy(mitmproxy_res)
    +189        res.host = host
    +190        res.scheme = scheme
    +191        res.port = port
    +192
    +193        return res
    +194
    +195    def host_is(self, *patterns: str) -> bool:
    +196        """Matches the host against the provided patterns
    +197
    +198        Returns:
    +199            bool: Whether at least one pattern matched
    +200        """
    +201        return host_is(self.host, *patterns)
    +202
    +203    @property
    +204    def body(self) -> bytes | None:
    +205        """Alias for content()
    +206
    +207        Returns:
    +208            bytes | None: The request body / content
    +209        """
    +210        return self.content
    +211
    +212    @body.setter
    +213    def body(self, val: bytes | None):
    +214        self.content = val
    +
    + + +

    A "Burp oriented" HTTP response class

    + +

    This class allows to manipulate Burp responses in a Pythonic way.

    + +

    Fields: + scheme: http or https + host: The initiating request target host + port: The initiating request target port + request: The initiating request.

    +
    + + +
    + +
    + + Response( http_version: bytes, status_code: int, reason: bytes, headers: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...], content: bytes | None, trailers: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...] | None, scheme: Literal['http', 'https'] = 'http', host: str = '', port: int = 0) + + + +
    + +
    40    def __init__(
    +41        self,
    +42        http_version: bytes,
    +43        status_code: int,
    +44        reason: bytes,
    +45        headers: Headers | tuple[tuple[bytes, bytes], ...],
    +46        content: bytes | None,
    +47        trailers: Headers | tuple[tuple[bytes, bytes], ...] | None,
    +48        scheme: Literal["http", "https"] = "http",
    +49        host: str = "",
    +50        port: int = 0,
    +51    ):
    +52        # Construct the base/inherited MITMProxy response.
    +53        super().__init__(
    +54            http_version,
    +55            status_code,
    +56            reason,
    +57            headers,
    +58            content,
    +59            trailers,
    +60            timestamp_start=time.time(),
    +61            timestamp_end=time.time(),
    +62        )
    +63        self.scheme = scheme
    +64        self.host = host
    +65        self.port = port
    +
    + + + + +
    +
    +
    + scheme: Literal['http', 'https'] = +'http' + + +
    + + + + +
    +
    +
    + host: str = +'' + + +
    + + + + +
    +
    +
    + port: int = +0 + + +
    + + + + +
    +
    +
    + request: Request | None = +None + + +
    + + + + +
    +
    + +
    +
    @classmethod
    + + def + from_mitmproxy( cls, response: _internal_mitmproxy.http.Response) -> Response: + + + +
    + +
    67    @classmethod
    +68    # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
    +69    # link to mitmproxy documentation
    +70    def from_mitmproxy(cls, response: MITMProxyResponse) -> Response:
    +71        """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    +72        :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert.
    +73        :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    +74        """
    +75        return cls(
    +76            always_bytes(response.http_version),
    +77            response.status_code,
    +78            always_bytes(response.reason),
    +79            Headers.from_mitmproxy(response.headers),
    +80            response.content,
    +81            Headers.from_mitmproxy(response.trailers) if response.trailers else None,
    +82        )
    +
    + + +

    Construct an instance of the Response class from a mitmproxy.http.HTTPResponse.

    + +
    Parameters
    + + + +
    Returns
    + +
    +

    A Response with the same data as the mitmproxy.http.HTTPResponse.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_burp( cls, response: pyscalpel.java.burp.http_response.IHttpResponse, service: pyscalpel.java.burp.http_service.IHttpService | None = None, request: pyscalpel.java.burp.http_request.IHttpRequest | None = None) -> Response: + + + +
    + +
     84    @classmethod
    + 85    def from_burp(
    + 86        cls,
    + 87        response: IHttpResponse,
    + 88        service: IHttpService | None = None,
    + 89        request: IHttpRequest | None = None,
    + 90    ) -> Response:
    + 91        """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`."""
    + 92        body = get_bytes(cast(IByteArray, response.body())) if response.body() else b""
    + 93        scalpel_response = cls(
    + 94            always_bytes(response.httpVersion() or "HTTP/1.1"),
    + 95            response.statusCode(),
    + 96            always_bytes(response.reasonPhrase() or b""),
    + 97            Headers.from_burp(response.headers()),
    + 98            body,
    + 99            None,
    +100        )
    +101
    +102        burp_request: IHttpRequest | None = request
    +103        if burp_request is None:
    +104            try:
    +105                # Some responses can have a "initiatingRequest" field.
    +106                # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A
    +107                burp_request = response.initiatingRequest()  # type: ignore
    +108            except AttributeError:
    +109                pass
    +110
    +111        if burp_request:
    +112            scalpel_response.request = Request.from_burp(burp_request, service)
    +113
    +114        if not service and burp_request:
    +115            # The only way to check if the Java method exist without writing Java is catching the error.
    +116            service = burp_request.httpService()
    +117
    +118        if service:
    +119            scalpel_response.scheme = "https" if service.secure() else "http"
    +120            scalpel_response.host = service.host()
    +121            scalpel_response.port = service.port()
    +122
    +123        return scalpel_response
    +
    + + +

    Construct an instance of the Response class from a Burp suite IHttpResponse.

    +
    + + +
    +
    + +
    + + def + to_burp(self) -> pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    149    def to_burp(self) -> IHttpResponse:  # pragma: no cover (uses Java API)
    +150        """Convert the response to a Burp suite :class:`IHttpResponse`."""
    +151        response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +152
    +153        return HttpResponse.httpResponse(response_byte_array)
    +
    + + +

    Convert the response to a Burp suite IHttpResponse.

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_raw(cls, data: bytes | str) -> Response: + + + +
    + +
    155    @classmethod
    +156    def from_raw(
    +157        cls, data: bytes | str
    +158    ) -> Response:  # pragma: no cover (uses Java API)
    +159        """Construct an instance of the Response class from raw bytes.
    +160        :param data: The raw bytes to convert.
    +161        :return: A :class:`Response` parsed from the raw bytes.
    +162        """
    +163        # Use the Burp API to trivialize the parsing of the response from raw bytes.
    +164        # Convert the raw bytes to a Burp ByteArray.
    +165        # Plain strings are OK too.
    +166        str_or_byte_array: IByteArray | str = (
    +167            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +168        )
    +169
    +170        # Instantiate a new Burp HTTP response.
    +171        burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array)
    +172
    +173        return cls.from_burp(burp_response)
    +
    + + +

    Construct an instance of the Response class from raw bytes.

    + +
    Parameters
    + +
      +
    • data: The raw bytes to convert.
    • +
    + +
    Returns
    + +
    +

    A Response parsed from the raw bytes.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + make( cls, status_code: int = 200, content: bytes | str = b'', headers: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...] = (), host: str = '', port: int = 0, scheme: Literal['http', 'https'] = 'http') -> Response: + + + +
    + +
    175    @classmethod
    +176    def make(
    +177        cls,
    +178        status_code: int = 200,
    +179        content: bytes | str = b"",
    +180        headers: Headers | tuple[tuple[bytes, bytes], ...] = (),
    +181        host: str = "",
    +182        port: int = 0,
    +183        scheme: Literal["http", "https"] = "http",
    +184    ) -> "Response":
    +185        # Use the base/inherited make method to construct a MITMProxy response.
    +186        mitmproxy_res = MITMProxyResponse.make(status_code, content, headers)
    +187
    +188        res = cls.from_mitmproxy(mitmproxy_res)
    +189        res.host = host
    +190        res.scheme = scheme
    +191        res.port = port
    +192
    +193        return res
    +
    + + +

    Simplified API for creating response objects.

    +
    + + +
    +
    + +
    + + def + host_is(self, *patterns: str) -> bool: + + + +
    + +
    195    def host_is(self, *patterns: str) -> bool:
    +196        """Matches the host against the provided patterns
    +197
    +198        Returns:
    +199            bool: Whether at least one pattern matched
    +200        """
    +201        return host_is(self.host, *patterns)
    +
    + + +

    Matches the host against the provided patterns

    + +

    Returns: + bool: Whether at least one pattern matched

    +
    + + +
    +
    + +
    + body: bytes | None + + + +
    + +
    203    @property
    +204    def body(self) -> bytes | None:
    +205        """Alias for content()
    +206
    +207        Returns:
    +208            bytes | None: The request body / content
    +209        """
    +210        return self.content
    +
    + + +

    Alias for content()

    + +

    Returns: + bytes | None: The request body / content

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    _internal_mitmproxy.http.Response
    +
    data
    +
    status_code
    +
    reason
    +
    cookies
    +
    refresh
    + +
    +
    _internal_mitmproxy.http.Message
    +
    from_state
    +
    get_state
    +
    set_state
    +
    stream
    +
    http_version
    +
    is_http10
    +
    is_http11
    +
    is_http2
    +
    headers
    +
    trailers
    +
    raw_content
    +
    content
    +
    text
    +
    set_content
    +
    get_content
    +
    set_text
    +
    get_text
    +
    timestamp_start
    +
    timestamp_end
    +
    decode
    +
    encode
    +
    json
    + +
    +
    _internal_mitmproxy.coretypes.serializable.Serializable
    +
    copy
    + +
    +
    +
    +
    +
    + +
    + + class + Flow: + + + +
    + +
    10class Flow:
    +11    """Contains request and response and some utilities for match()"""
    +12
    +13    def __init__(
    +14        self,
    +15        scheme: Literal["http", "https"] = "http",
    +16        host: str = "",
    +17        port: int = 0,
    +18        request: Request | None = None,
    +19        response: Response | None = None,
    +20        text: bytes | None = None,
    +21    ):
    +22        self.scheme = scheme
    +23        self.host = host
    +24        self.port = port
    +25        self.request = request
    +26        self.response = response
    +27        self.text = text
    +28
    +29    def host_is(self, *patterns: str) -> bool:
    +30        """Matches a wildcard pattern against the target host
    +31
    +32        Returns:
    +33            bool: True if at least one pattern matched
    +34        """
    +35        return host_is(self.host, *patterns)
    +36
    +37    def path_is(self, *patterns: str) -> bool:
    +38        """Matches a wildcard pattern against the request path
    +39
    +40        Includes query string `?` and fragment `#`
    +41
    +42        Returns:
    +43            bool: True if at least one pattern matched
    +44        """
    +45        req = self.request
    +46        if req is None:
    +47            return False
    +48
    +49        return req.path_is(*patterns)
    +
    + + +

    Contains request and response and some utilities for match()

    +
    + + +
    + +
    + + Flow( scheme: Literal['http', 'https'] = 'http', host: str = '', port: int = 0, request: Request | None = None, response: Response | None = None, text: bytes | None = None) + + + +
    + +
    13    def __init__(
    +14        self,
    +15        scheme: Literal["http", "https"] = "http",
    +16        host: str = "",
    +17        port: int = 0,
    +18        request: Request | None = None,
    +19        response: Response | None = None,
    +20        text: bytes | None = None,
    +21    ):
    +22        self.scheme = scheme
    +23        self.host = host
    +24        self.port = port
    +25        self.request = request
    +26        self.response = response
    +27        self.text = text
    +
    + + + + +
    +
    +
    + scheme + + +
    + + + + +
    +
    +
    + host + + +
    + + + + +
    +
    +
    + port + + +
    + + + + +
    +
    +
    + request + + +
    + + + + +
    +
    +
    + response + + +
    + + + + +
    +
    +
    + text + + +
    + + + + +
    +
    + +
    + + def + host_is(self, *patterns: str) -> bool: + + + +
    + +
    29    def host_is(self, *patterns: str) -> bool:
    +30        """Matches a wildcard pattern against the target host
    +31
    +32        Returns:
    +33            bool: True if at least one pattern matched
    +34        """
    +35        return host_is(self.host, *patterns)
    +
    + + +

    Matches a wildcard pattern against the target host

    + +

    Returns: + bool: True if at least one pattern matched

    +
    + + +
    +
    + +
    + + def + path_is(self, *patterns: str) -> bool: + + + +
    + +
    37    def path_is(self, *patterns: str) -> bool:
    +38        """Matches a wildcard pattern against the request path
    +39
    +40        Includes query string `?` and fragment `#`
    +41
    +42        Returns:
    +43            bool: True if at least one pattern matched
    +44        """
    +45        req = self.request
    +46        if req is None:
    +47            return False
    +48
    +49        return req.path_is(*patterns)
    +
    + + +

    Matches a wildcard pattern against the request path

    + +

    Includes query string ? and fragment #

    + +

    Returns: + bool: True if at least one pattern matched

    +
    + + +
    +
    +
    +
    + ctx: Context = +{} + + +
    + + +

    The Scalpel Python execution context

    + +

    Contains the Burp Java API object, the venv directory, the user script path, +the path to the file loading the user script and a logging object

    +
    + + +
    +
    + +
    + + class + Context(typing.TypedDict): + + + +
    + +
     6class Context(TypedDict):
    + 7    """Scalpel Python execution context"""
    + 8
    + 9    API: Any
    +10    """
    +11        The Burp [Montoya API]
    +12        (https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html)
    +13        root object.
    +14
    +15        Allows you to interact with Burp by directly manipulating the Java object.
    +16    
    +17    """
    +18
    +19    directory: str
    +20    """The framework directory"""
    +21
    +22    user_script: str
    +23    """The loaded script path"""
    +24
    +25    framework: str
    +26    """The framework (loader script) path"""
    +27
    +28    venv: str
    +29    """The venv the script was loaded in"""
    +
    + + +

    Scalpel Python execution context

    +
    + + +
    +
    + API: Any + + +
    + + +

    The Burp [Montoya API] +(https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html) +root object.

    + +

    Allows you to interact with Burp by directly manipulating the Java object.

    +
    + + +
    +
    +
    + directory: str + + +
    + + +

    The framework directory

    +
    + + +
    +
    +
    + user_script: str + + +
    + + +

    The loaded script path

    +
    + + +
    +
    +
    + framework: str + + +
    + + +

    The framework (loader script) path

    +
    + + +
    +
    +
    + venv: str + + +
    + + +

    The venv the script was loaded in

    +
    + + +
    +
    +
    +
    + MatchEvent = +typing.Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out'] + + +
    + + + + +
    +
    + +
    + + def + editor(mode: Literal['raw', 'hex', 'octal', 'binary', 'decimal']): + + + +
    + +
    12def editor(mode: EditorMode):
    +13    """Decorator to specify the editor type for a given hook
    +14
    +15    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp
    +16
    +17    Example:
    +18    ```py
    +19        @editor("hex")
    +20        def req_edit_in(req: Request) -> bytes | None:
    +21            return bytes(req)
    +22    ```
    +23    This displays the request in an hex editor.
    +24
    +25    Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`.
    +26
    +27
    +28    Args:
    +29        mode (EDITOR_MODE): The editor mode (raw, hex,...)
    +30    """
    +31
    +32    if mode not in EDITOR_MODES:
    +33        raise ValueError(f"Argument must be one of {EDITOR_MODES}")
    +34
    +35    def decorator(hook: Callable):
    +36        hook.__annotations__["scalpel_editor_mode"] = mode
    +37        return hook
    +38
    +39    return decorator
    +
    + + +

    Decorator to specify the editor type for a given hook

    + +

    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp

    + +

    Example:

    + +
    +
        @editor("hex")
    +    def req_edit_in(req: Request) -> bytes | None:
    +        return bytes(req)
    +
    +
    + +

    This displays the request in an hex editor.

    + +

    Currently, the only modes supported are "raw", "hex", "octal", "binary" and "decimal".

    + +

    Args: + mode (EDITOR_MODE): The editor mode (raw, hex,...)

    +
    + + +
    +
    + +
    + + class + Logger: + + + +
    + +
     8class Logger:  # pragma: no cover
    + 9    """Provides methods for logging messages to the Burp Suite output and standard streams."""
    +10
    +11    def all(self, msg: str):
    +12        """Prints the message to the standard output
    +13
    +14        Args:
    +15            msg (str): The message to print
    +16        """
    +17        print(f"(default): {msg}")
    +18
    +19    def trace(self, msg: str):
    +20        """Prints the message to the standard output
    +21
    +22        Args:
    +23            msg (str): The message to print
    +24        """
    +25        print(f"(default): {msg}")
    +26
    +27    def debug(self, msg: str):
    +28        """Prints the message to the standard output
    +29
    +30        Args:
    +31            msg (str): The message to print
    +32        """
    +33        print(f"(default): {msg}")
    +34
    +35    def info(self, msg: str):
    +36        """Prints the message to the standard output
    +37
    +38        Args:
    +39            msg (str): The message to print
    +40        """
    +41        print(f"(default): {msg}")
    +42
    +43    def warn(self, msg: str):
    +44        """Prints the message to the standard output
    +45
    +46        Args:
    +47            msg (str): The message to print
    +48        """
    +49        print(f"(default): {msg}")
    +50
    +51    def fatal(self, msg: str):
    +52        """Prints the message to the standard output
    +53
    +54        Args:
    +55            msg (str): The message to print
    +56        """
    +57        print(f"(default): {msg}")
    +58
    +59    def error(self, msg: str):
    +60        """Prints the message to the standard error
    +61
    +62        Args:
    +63            msg (str): The message to print
    +64        """
    +65        print(f"(default): {msg}", file=sys.stderr)
    +
    + + +

    Provides methods for logging messages to the Burp Suite output and standard streams.

    +
    + + +
    + +
    + + def + all(self, msg: str): + + + +
    + +
    11    def all(self, msg: str):
    +12        """Prints the message to the standard output
    +13
    +14        Args:
    +15            msg (str): The message to print
    +16        """
    +17        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + trace(self, msg: str): + + + +
    + +
    19    def trace(self, msg: str):
    +20        """Prints the message to the standard output
    +21
    +22        Args:
    +23            msg (str): The message to print
    +24        """
    +25        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + debug(self, msg: str): + + + +
    + +
    27    def debug(self, msg: str):
    +28        """Prints the message to the standard output
    +29
    +30        Args:
    +31            msg (str): The message to print
    +32        """
    +33        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + info(self, msg: str): + + + +
    + +
    35    def info(self, msg: str):
    +36        """Prints the message to the standard output
    +37
    +38        Args:
    +39            msg (str): The message to print
    +40        """
    +41        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + warn(self, msg: str): + + + +
    + +
    43    def warn(self, msg: str):
    +44        """Prints the message to the standard output
    +45
    +46        Args:
    +47            msg (str): The message to print
    +48        """
    +49        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + fatal(self, msg: str): + + + +
    + +
    51    def fatal(self, msg: str):
    +52        """Prints the message to the standard output
    +53
    +54        Args:
    +55            msg (str): The message to print
    +56        """
    +57        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + error(self, msg: str): + + + +
    + +
    59    def error(self, msg: str):
    +60        """Prints the message to the standard error
    +61
    +62        Args:
    +63            msg (str): The message to print
    +64        """
    +65        print(f"(default): {msg}", file=sys.stderr)
    +
    + + +

    Prints the message to the standard error

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/burp_utils.html b/docs/public/pdoc/python3-10/pyscalpel/burp_utils.html new file mode 100644 index 00000000..1c45777a --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/burp_utils.html @@ -0,0 +1,478 @@ + + + + + + + python3-10.pyscalpel.burp_utils API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.burp_utils

    + + + + + + +
     1from typing import TypeVar, cast
    + 2from functools import singledispatch
    + 3from collections.abc import Iterable
    + 4
    + 5import pyscalpel._globals
    + 6from pyscalpel.java.burp.http_request import IHttpRequest, HttpRequest
    + 7from pyscalpel.java.burp.http_response import IHttpResponse, HttpResponse
    + 8from pyscalpel.java.burp.byte_array import IByteArray, ByteArray
    + 9from pyscalpel.java.burp.http_parameter import IHttpParameter, HttpParameter
    +10from pyscalpel.java.bytes import JavaBytes
    +11from pyscalpel.java.scalpel_types.utils import PythonUtils
    +12from pyscalpel.encoding import always_bytes, urldecode, urlencode_all
    +13
    +14
    +15ctx = pyscalpel._globals.ctx
    +16
    +17
    +18HttpRequestOrResponse = TypeVar("HttpRequestOrResponse", IHttpRequest, IHttpResponse)
    +19
    +20ByteArraySerialisable = TypeVar("ByteArraySerialisable", IHttpRequest, IHttpResponse)
    +21
    +22ByteArrayConvertible = TypeVar(
    +23    "ByteArrayConvertible", bytes, JavaBytes, list[int], str, bytearray
    +24)
    +25
    +26
    +27@singledispatch
    +28def new_response(obj: ByteArrayConvertible) -> IHttpResponse:  # pragma: no cover
    +29    """Create a new HttpResponse from the given bytes"""
    +30    return HttpResponse.httpResponse(byte_array(obj))
    +31
    +32
    +33@new_response.register
    +34def _new_response(obj: IByteArray) -> IHttpResponse:  # pragma: no cover
    +35    return HttpResponse.httpResponse(obj)
    +36
    +37
    +38@singledispatch
    +39def new_request(obj: ByteArrayConvertible) -> IHttpRequest:  # pragma: no cover
    +40    """Create a new HttpRequest from the given bytes"""
    +41    return HttpRequest.httpRequest(byte_array(obj))
    +42
    +43
    +44@new_request.register
    +45def _new_request(obj: IByteArray) -> IHttpRequest:  # pragma: no cover
    +46    return HttpRequest.httpRequest(obj)
    +47
    +48
    +49@singledispatch
    +50def byte_array(
    +51    _bytes: bytes | JavaBytes | list[int] | bytearray,
    +52) -> IByteArray:  # pragma: no cover
    +53    """Create a new :class:`IByteArray` from the given bytes-like obbject"""
    +54    # Handle buggy bytes casting
    +55    # This is needed because Python will _sometimes_ try
    +56    # to interpret bytes as a an integer when passing to ByteArray.byteArray() and crash like this:
    +57    #
    +58    # TypeError: Error converting parameter 1: 'bytes' object cannot be interpreted as an integer
    +59    #
    +60    # Restarting Burp fixes the issue when it happens, so to avoid unstable behaviour
    +61    #   we explcitely convert the bytes to a PyJArray of Java byte
    +62    cast_value = cast(JavaBytes, PythonUtils.toJavaBytes(bytes(_bytes)))
    +63    return ByteArray.byteArray(cast_value)
    +64
    +65
    +66@byte_array.register
    +67def _byte_array_str(string: str) -> IByteArray:  # pragma: no cover
    +68    return ByteArray.byteArray(string)
    +69
    +70
    +71def get_bytes(array: IByteArray) -> bytes:  # pragma: no cover
    +72    return to_bytes(array.getBytes())
    +73
    +74
    +75def to_bytes(obj: ByteArraySerialisable | JavaBytes) -> bytes:  # pragma: no cover
    +76    # Handle java signed bytes
    +77    if isinstance(obj, Iterable):
    +78        # Convert java signed bytes to python unsigned bytes
    +79        return bytes([b & 0xFF for b in cast(JavaBytes, obj)])
    +80
    +81    return get_bytes(cast(ByteArraySerialisable, obj).toByteArray())
    +
    + + +
    +
    +
    + ctx = +{} + + +
    + + + + +
    +
    + +
    +
    @singledispatch
    + + def + new_response( obj: ~ByteArrayConvertible) -> pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    28@singledispatch
    +29def new_response(obj: ByteArrayConvertible) -> IHttpResponse:  # pragma: no cover
    +30    """Create a new HttpResponse from the given bytes"""
    +31    return HttpResponse.httpResponse(byte_array(obj))
    +
    + + +

    Create a new HttpResponse from the given bytes

    +
    + + +
    +
    + +
    +
    @singledispatch
    + + def + new_request( obj: ~ByteArrayConvertible) -> pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    39@singledispatch
    +40def new_request(obj: ByteArrayConvertible) -> IHttpRequest:  # pragma: no cover
    +41    """Create a new HttpRequest from the given bytes"""
    +42    return HttpRequest.httpRequest(byte_array(obj))
    +
    + + +

    Create a new HttpRequest from the given bytes

    +
    + + +
    +
    + +
    +
    @singledispatch
    + + def + byte_array( _bytes: bytes | pyscalpel.java.bytes.JavaBytes | list[int] | bytearray) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    50@singledispatch
    +51def byte_array(
    +52    _bytes: bytes | JavaBytes | list[int] | bytearray,
    +53) -> IByteArray:  # pragma: no cover
    +54    """Create a new :class:`IByteArray` from the given bytes-like obbject"""
    +55    # Handle buggy bytes casting
    +56    # This is needed because Python will _sometimes_ try
    +57    # to interpret bytes as a an integer when passing to ByteArray.byteArray() and crash like this:
    +58    #
    +59    # TypeError: Error converting parameter 1: 'bytes' object cannot be interpreted as an integer
    +60    #
    +61    # Restarting Burp fixes the issue when it happens, so to avoid unstable behaviour
    +62    #   we explcitely convert the bytes to a PyJArray of Java byte
    +63    cast_value = cast(JavaBytes, PythonUtils.toJavaBytes(bytes(_bytes)))
    +64    return ByteArray.byteArray(cast_value)
    +
    + + +

    Create a new IByteArray from the given bytes-like obbject

    +
    + + +
    +
    + +
    + + def + get_bytes(array: pyscalpel.java.burp.byte_array.IByteArray) -> bytes: + + + +
    + +
    72def get_bytes(array: IByteArray) -> bytes:  # pragma: no cover
    +73    return to_bytes(array.getBytes())
    +
    + + + + +
    +
    + +
    + + def + to_bytes( obj: Union[~ByteArraySerialisable, pyscalpel.java.bytes.JavaBytes]) -> bytes: + + + +
    + +
    76def to_bytes(obj: ByteArraySerialisable | JavaBytes) -> bytes:  # pragma: no cover
    +77    # Handle java signed bytes
    +78    if isinstance(obj, Iterable):
    +79        # Convert java signed bytes to python unsigned bytes
    +80        return bytes([b & 0xFF for b in cast(JavaBytes, obj)])
    +81
    +82    return get_bytes(cast(ByteArraySerialisable, obj).toByteArray())
    +
    + + + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/edit.html b/docs/public/pdoc/python3-10/pyscalpel/edit.html new file mode 100644 index 00000000..bf4c6c81 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/edit.html @@ -0,0 +1,380 @@ + + + + + + + python3-10.pyscalpel.edit API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.edit

    + +

    Scalpel allows choosing between normal and binary editors, +to do so, the user can apply the editor decorator to the req_edit_in / res_edit_int hook:

    +
    + + + + + +
     1"""
    + 2    Scalpel allows choosing between normal and binary editors,
    + 3    to do so, the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_int` hook:
    + 4"""
    + 5from typing import Callable, Literal, get_args
    + 6
    + 7EditorMode = Literal["raw", "hex", "octal", "binary", "decimal"]
    + 8EDITOR_MODES: set[EditorMode] = set(get_args(EditorMode))
    + 9
    +10
    +11def editor(mode: EditorMode):
    +12    """Decorator to specify the editor type for a given hook
    +13
    +14    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp
    +15
    +16    Example:
    +17    ```py
    +18        @editor("hex")
    +19        def req_edit_in(req: Request) -> bytes | None:
    +20            return bytes(req)
    +21    ```
    +22    This displays the request in an hex editor.
    +23
    +24    Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`.
    +25
    +26
    +27    Args:
    +28        mode (EDITOR_MODE): The editor mode (raw, hex,...)
    +29    """
    +30
    +31    if mode not in EDITOR_MODES:
    +32        raise ValueError(f"Argument must be one of {EDITOR_MODES}")
    +33
    +34    def decorator(hook: Callable):
    +35        hook.__annotations__["scalpel_editor_mode"] = mode
    +36        return hook
    +37
    +38    return decorator
    +
    + + +
    +
    +
    + EditorMode = +typing.Literal['raw', 'hex', 'octal', 'binary', 'decimal'] + + +
    + + + + +
    +
    +
    + EDITOR_MODES: set[typing.Literal['raw', 'hex', 'octal', 'binary', 'decimal']] = +{'raw', 'binary', 'octal', 'hex', 'decimal'} + + +
    + + + + +
    +
    + +
    + + def + editor(mode: Literal['raw', 'hex', 'octal', 'binary', 'decimal']): + + + +
    + +
    12def editor(mode: EditorMode):
    +13    """Decorator to specify the editor type for a given hook
    +14
    +15    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp
    +16
    +17    Example:
    +18    ```py
    +19        @editor("hex")
    +20        def req_edit_in(req: Request) -> bytes | None:
    +21            return bytes(req)
    +22    ```
    +23    This displays the request in an hex editor.
    +24
    +25    Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`.
    +26
    +27
    +28    Args:
    +29        mode (EDITOR_MODE): The editor mode (raw, hex,...)
    +30    """
    +31
    +32    if mode not in EDITOR_MODES:
    +33        raise ValueError(f"Argument must be one of {EDITOR_MODES}")
    +34
    +35    def decorator(hook: Callable):
    +36        hook.__annotations__["scalpel_editor_mode"] = mode
    +37        return hook
    +38
    +39    return decorator
    +
    + + +

    Decorator to specify the editor type for a given hook

    + +

    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp

    + +

    Example:

    + +
    +
        @editor("hex")
    +    def req_edit_in(req: Request) -> bytes | None:
    +        return bytes(req)
    +
    +
    + +

    This displays the request in an hex editor.

    + +

    Currently, the only modes supported are "raw", "hex", "octal", "binary" and "decimal".

    + +

    Args: + mode (EDITOR_MODE): The editor mode (raw, hex,...)

    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/encoding.html b/docs/public/pdoc/python3-10/pyscalpel/encoding.html new file mode 100644 index 00000000..8f35a0c7 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/encoding.html @@ -0,0 +1,420 @@ + + + + + + + python3-10.pyscalpel.encoding API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.encoding

    + +

    Utilities for encoding data.

    +
    + + + + + +
     1"""
    + 2    Utilities for encoding data.
    + 3"""
    + 4
    + 5from urllib.parse import unquote_to_bytes as urllibdecode
    + 6from _internal_mitmproxy.utils import strutils
    + 7
    + 8
    + 9# str/bytes conversion helpers from mitmproxy/http.py:
    +10# https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/http.py#:~:text=def-,_native,-(x%3A
    +11def always_bytes(data: str | bytes | int, encoding="latin-1") -> bytes:
    +12    """Convert data to bytes
    +13
    +14    Args:
    +15        data (str | bytes | int): The data to convert
    +16
    +17    Returns:
    +18        bytes: The converted bytes
    +19    """
    +20    if isinstance(data, int):
    +21        data = str(data)
    +22    return strutils.always_bytes(data, encoding, "surrogateescape")
    +23
    +24
    +25def always_str(data: str | bytes | int, encoding="latin-1") -> str:
    +26    """Convert data to string
    +27
    +28    Args:
    +29        data (str | bytes | int): The data to convert
    +30
    +31    Returns:
    +32        str: The converted string
    +33    """
    +34    if isinstance(data, int):
    +35        return str(data)
    +36    return strutils.always_str(data, encoding, "surrogateescape")
    +37
    +38
    +39
    +40def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes:
    +41    """URL Encode all bytes in the given bytes object"""
    +42    return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding)
    +43
    +44
    +45def urldecode(data: bytes | str, encoding="latin-1") -> bytes:
    +46    """URL Decode all bytes in the given bytes object"""
    +47    return urllibdecode(always_bytes(data, encoding))
    +
    + + +
    +
    + +
    + + def + always_bytes(data: str | bytes | int, encoding='latin-1') -> bytes: + + + +
    + +
    12def always_bytes(data: str | bytes | int, encoding="latin-1") -> bytes:
    +13    """Convert data to bytes
    +14
    +15    Args:
    +16        data (str | bytes | int): The data to convert
    +17
    +18    Returns:
    +19        bytes: The converted bytes
    +20    """
    +21    if isinstance(data, int):
    +22        data = str(data)
    +23    return strutils.always_bytes(data, encoding, "surrogateescape")
    +
    + + +

    Convert data to bytes

    + +

    Args: + data (str | bytes | int): The data to convert

    + +

    Returns: + bytes: The converted bytes

    +
    + + +
    +
    + +
    + + def + always_str(data: str | bytes | int, encoding='latin-1') -> str: + + + +
    + +
    26def always_str(data: str | bytes | int, encoding="latin-1") -> str:
    +27    """Convert data to string
    +28
    +29    Args:
    +30        data (str | bytes | int): The data to convert
    +31
    +32    Returns:
    +33        str: The converted string
    +34    """
    +35    if isinstance(data, int):
    +36        return str(data)
    +37    return strutils.always_str(data, encoding, "surrogateescape")
    +
    + + +

    Convert data to string

    + +

    Args: + data (str | bytes | int): The data to convert

    + +

    Returns: + str: The converted string

    +
    + + +
    +
    + +
    + + def + urlencode_all(data: bytes | str, encoding='latin-1') -> bytes: + + + +
    + +
    41def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes:
    +42    """URL Encode all bytes in the given bytes object"""
    +43    return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding)
    +
    + + +

    URL Encode all bytes in the given bytes object

    +
    + + +
    +
    + +
    + + def + urldecode(data: bytes | str, encoding='latin-1') -> bytes: + + + +
    + +
    46def urldecode(data: bytes | str, encoding="latin-1") -> bytes:
    +47    """URL Decode all bytes in the given bytes object"""
    +48    return urllibdecode(always_bytes(data, encoding))
    +
    + + +

    URL Decode all bytes in the given bytes object

    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/http.html b/docs/public/pdoc/python3-10/pyscalpel/http.html new file mode 100644 index 00000000..6d1986c5 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/http.html @@ -0,0 +1,3715 @@ + + + + + + + python3-10.pyscalpel.http API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.http

    + +

    This module contains objects representing HTTP objects passed to the user's hooks

    +
    + + + + + +
     1"""
    + 2    This module contains objects representing HTTP objects passed to the user's hooks
    + 3"""
    + 4
    + 5from .request import Request, Headers
    + 6from .response import Response
    + 7from .flow import Flow
    + 8from .utils import match_patterns, host_is
    + 9from . import body
    +10
    +11__all__ = [
    +12    "body",  # <- pdoc shows a warning for this declaration but won't display it when absent
    +13    "Request",
    +14    "Response",
    +15    "Headers",
    +16    "Flow",
    +17    "host_is",
    +18    "match_patterns",
    +19]
    +
    + + +
    +
    + +
    + + class + Request: + + + +
    + +
     70class Request:
    + 71    """A "Burp oriented" HTTP request class
    + 72
    + 73
    + 74    This class allows to manipulate Burp requests in a Pythonic way.
    + 75    """
    + 76
    + 77    _Port = int
    + 78    _QueryParam = tuple[str, str]
    + 79    _ParsedQuery = tuple[_QueryParam, ...]
    + 80    _HttpVersion = str
    + 81    _HeaderKey = str
    + 82    _HeaderValue = str
    + 83    _Header = tuple[_HeaderKey, _HeaderValue]
    + 84    _Host = str
    + 85    _Method = str
    + 86    _Scheme = Literal["http", "https"]
    + 87    _Authority = str
    + 88    _Content = bytes
    + 89    _Path = str
    + 90
    + 91    host: _Host
    + 92    port: _Port
    + 93    method: _Method
    + 94    scheme: _Scheme
    + 95    authority: _Authority
    + 96
    + 97    # Path also includes URI parameters (;), query (?) and fragment (#)
    + 98    # Simply because it is more conveninent to manipulate that way in a pentensting context
    + 99    # It also mimics the way mitmproxy works.
    +100    path: _Path
    +101
    +102    http_version: _HttpVersion
    +103    _headers: Headers
    +104    _serializer: FormSerializer | None = None
    +105    _deserialized_content: Any = None
    +106    _content: _Content | None = None
    +107    _old_deserialized_content: Any = None
    +108    _is_form_initialized: bool = False
    +109    update_content_length: bool = True
    +110
    +111    def __init__(
    +112        self,
    +113        method: str,
    +114        scheme: Literal["http", "https"],
    +115        host: str,
    +116        port: int,
    +117        path: str,
    +118        http_version: str,
    +119        headers: (
    +120            Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]]
    +121        ),
    +122        authority: str,
    +123        content: bytes | None,
    +124    ):
    +125        self.scheme = scheme
    +126        self.host = host
    +127        self.port = port
    +128        self.path = path
    +129        self.method = method
    +130        self.authority = authority
    +131        self.http_version = http_version
    +132        self.headers = headers if isinstance(headers, Headers) else Headers(headers)
    +133        self._content = content
    +134
    +135        # Initialize the serializer (json,urlencoded,multipart)
    +136        self.update_serializer_from_content_type(
    +137            self.headers.get("Content-Type"), fail_silently=True
    +138        )
    +139
    +140        # Initialize old deserialized content to avoid modifying content if it has not been modified
    +141        # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py)
    +142        self._old_deserialized_content = deepcopy(self._deserialized_content)
    +143
    +144    def _del_header(self, header: str) -> bool:
    +145        if header in self._headers.keys():
    +146            del self._headers[header]
    +147            return True
    +148
    +149        return False
    +150
    +151    def _update_content_length(self) -> None:
    +152        if self.update_content_length:
    +153            if self._content is None:
    +154                self._del_header("Content-Length")
    +155            else:
    +156                length = len(cast(bytes, self._content))
    +157                self._headers["Content-Length"] = str(length)
    +158
    +159    @staticmethod
    +160    def _parse_qs(query_string: str) -> _ParsedQuery:
    +161        return tuple(urllib.parse.parse_qsl(query_string))
    +162
    +163    @staticmethod
    +164    def _parse_url(
    +165        url: str,
    +166    ) -> tuple[_Scheme, _Host, _Port, _Path]:
    +167        scheme, host, port, path = url_parse(url)
    +168
    +169        # This method is only used to create HTTP requests from URLs
    +170        #   so we can ensure the scheme is valid for this usage
    +171        if scheme not in (b"http", b"https"):
    +172            scheme = b"http"
    +173
    +174        return cast(
    +175            tuple[Literal["http", "https"], str, int, str],
    +176            (scheme.decode("ascii"), host.decode("idna"), port, path.decode("ascii")),
    +177        )
    +178
    +179    @staticmethod
    +180    def _unparse_url(scheme: _Scheme, host: _Host, port: _Port, path: _Path) -> str:
    +181        return url_unparse(scheme, host, port, path)
    +182
    +183    @classmethod
    +184    def make(
    +185        cls,
    +186        method: str,
    +187        url: str,
    +188        content: bytes | str = "",
    +189        headers: (
    +190            Headers
    +191            | dict[str | bytes, str | bytes]
    +192            | dict[str, str]
    +193            | dict[bytes, bytes]
    +194            | Iterable[tuple[bytes, bytes]]
    +195        ) = (),
    +196    ) -> Request:
    +197        """Create a request from an URL
    +198
    +199        Args:
    +200            method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)
    +201            url (str): The request URL
    +202            content (bytes | str, optional): The request content. Defaults to "".
    +203            headers (Headers, optional): The request headers. Defaults to ().
    +204
    +205        Returns:
    +206            Request: The HTTP request
    +207        """
    +208        scalpel_headers: Headers
    +209        match headers:
    +210            case Headers():
    +211                scalpel_headers = headers
    +212            case dict():
    +213                casted_headers = cast(dict[str | bytes, str | bytes], headers)
    +214                scalpel_headers = Headers(
    +215                    (
    +216                        (always_bytes(key), always_bytes(val))
    +217                        for key, val in casted_headers.items()
    +218                    )
    +219                )
    +220            case _:
    +221                scalpel_headers = Headers(headers)
    +222
    +223        scheme, host, port, path = Request._parse_url(url)
    +224        http_version = "HTTP/1.1"
    +225
    +226        # Inferr missing Host header from URL
    +227        host_header = scalpel_headers.get("Host")
    +228        if host_header is None:
    +229            match (scheme, port):
    +230                case ("http", 80) | ("https", 443):
    +231                    host_header = host
    +232                case _:
    +233                    host_header = f"{host}:{port}"
    +234
    +235            scalpel_headers["Host"] = host_header
    +236
    +237        authority: str = host_header
    +238        encoded_content = always_bytes(content)
    +239
    +240        assert isinstance(host, str)
    +241
    +242        return cls(
    +243            method=method,
    +244            scheme=scheme,
    +245            host=host,
    +246            port=port,
    +247            path=path,
    +248            http_version=http_version,
    +249            headers=scalpel_headers,
    +250            authority=authority,
    +251            content=encoded_content,
    +252        )
    +253
    +254    @classmethod
    +255    def from_burp(
    +256        cls, request: IHttpRequest, service: IHttpService | None = None
    +257    ) -> Request:  # pragma: no cover (uses Java API)
    +258        """Construct an instance of the Request class from a Burp suite HttpRequest.
    +259        :param request: The Burp suite HttpRequest to convert.
    +260        :return: A Request with the same data as the Burp suite HttpRequest.
    +261        """
    +262        service = service or request.httpService()
    +263        body = get_bytes(request.body())
    +264
    +265        # Burp will give you lowercased and pseudo headers when using HTTP/2.
    +266        # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-.
    +267        # https://blog.yaakov.online/http-2-header-casing/
    +268        headers: Headers = Headers.from_burp(request.headers())
    +269
    +270        # Burp gives a 0 length byte array body even when it doesn't exist, instead of null.
    +271        # Empty but existing bodies without a Content-Length header are lost in the process.
    +272        if not body and not headers.get("Content-Length"):
    +273            body = None
    +274
    +275        # request.url() gives a relative url for some reason
    +276        # So we have to parse and unparse to get the full path
    +277        #   (path + parameters + query + fragment)
    +278        _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url())
    +279
    +280        # Concatenate the path components
    +281        # Empty parameters,query and fragment are lost in the process
    +282        # e.g.: http://example.com;?# becomes http://example.com
    +283        # To use such an URL, the user must set the path directly
    +284        # To fix this we would need to write our own URL parser, which is a bit overkill for now.
    +285        path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment))
    +286
    +287        host = ""
    +288        port = 0
    +289        scheme = "http"
    +290        if service:
    +291            host = service.host()
    +292            port = service.port()
    +293            scheme = "https" if service.secure() else "http"
    +294
    +295        return cls(
    +296            method=request.method(),
    +297            scheme=scheme,
    +298            host=host,
    +299            port=port,
    +300            path=path,
    +301            http_version=request.httpVersion() or "HTTP/1.1",
    +302            headers=headers,
    +303            authority=headers.get(":authority") or headers.get("Host") or "",
    +304            content=body,
    +305        )
    +306
    +307    def __bytes__(self) -> bytes:
    +308        """Convert the request to bytes
    +309        :return: The request as bytes.
    +310        """
    +311        # Reserialize the request to bytes.
    +312        first_line = (
    +313            b" ".join(
    +314                always_bytes(s) for s in (self.method, self.path, self.http_version)
    +315            )
    +316            + b"\r\n"
    +317        )
    +318
    +319        # Strip HTTP/2 pseudo headers.
    +320        # https://portswigger.net/burp/documentation/desktop/http2/http2-basics-for-burp-users#:~:text=HTTP/2%20specification.-,Pseudo%2Dheaders,-In%20HTTP/2
    +321        mapped_headers = tuple(
    +322            field for field in self.headers.fields if not field[0].startswith(b":")
    +323        )
    +324
    +325        if self.headers.get(b"Host") is None and self.http_version == "HTTP/2":
    +326            # Host header is not present in HTTP/2, but is required by Burp message editor.
    +327            # So we have to add it back from the :authority pseudo-header.
    +328            # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=pseudo%2Dheaders%20and-,derives,-the%20%3Aauthority%20from
    +329            mapped_headers = (
    +330                (b"Host", always_bytes(self.headers[":authority"])),
    +331            ) + tuple(mapped_headers)
    +332
    +333        # Construct the request's headers part.
    +334        headers_lines = b"".join(
    +335            b"%s: %s\r\n" % (key, val) for key, val in mapped_headers
    +336        )
    +337
    +338        # Set a default value for the request's body. (None -> b"")
    +339        body = self.content or b""
    +340
    +341        # Construct the whole request and return it.
    +342        return first_line + headers_lines + b"\r\n" + body
    +343
    +344    def to_burp(self) -> IHttpRequest:  # pragma: no cover
    +345        """Convert the request to a Burp suite :class:`IHttpRequest`.
    +346        :return: The request as a Burp suite :class:`IHttpRequest`.
    +347        """
    +348        # Convert the request to a Burp ByteArray.
    +349        request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +350
    +351        if self.port == 0:
    +352            # No networking information is available, so we build a plain network-less request.
    +353            return HttpRequest.httpRequest(request_byte_array)
    +354
    +355        # Build the Burp HTTP networking service.
    +356        service: IHttpService = HttpService.httpService(
    +357            self.host, self.port, self.scheme == "https"
    +358        )
    +359
    +360        # Instantiate and return a new Burp HTTP request.
    +361        return HttpRequest.httpRequest(service, request_byte_array)
    +362
    +363    @classmethod
    +364    def from_raw(
    +365        cls,
    +366        data: bytes | str,
    +367        real_host: str = "",
    +368        port: int = 0,
    +369        scheme: Literal["http"] | Literal["https"] | str = "http",
    +370    ) -> Request:  # pragma: no cover
    +371        """Construct an instance of the Request class from raw bytes.
    +372        :param data: The raw bytes to convert.
    +373        :param real_host: The real host to connect to.
    +374        :param port: The port of the request.
    +375        :param scheme: The scheme of the request.
    +376        :return: A :class:`Request` with the same data as the raw bytes.
    +377        """
    +378        # Convert the raw bytes to a Burp ByteArray.
    +379        # We use the Burp API to trivialize the parsing of the request from raw bytes.
    +380        str_or_byte_array: IByteArray | str = (
    +381            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +382        )
    +383
    +384        # Handle the case where the networking informations are not provided.
    +385        if port == 0:
    +386            # Instantiate and return a new Burp HTTP request without networking informations.
    +387            burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array)
    +388        else:
    +389            # Build the Burp HTTP networking service.
    +390            service: IHttpService = HttpService.httpService(
    +391                real_host, port, scheme == "https"
    +392            )
    +393
    +394            # Instantiate a new Burp HTTP request with networking informations.
    +395            burp_request: IHttpRequest = HttpRequest.httpRequest(
    +396                service, str_or_byte_array
    +397            )
    +398
    +399        # Construct the request from the Burp.
    +400        return cls.from_burp(burp_request)
    +401
    +402    @property
    +403    def url(self) -> str:
    +404        """
    +405        The full URL string, constructed from `Request.scheme`,
    +406            `Request.host`, `Request.port` and `Request.path`.
    +407
    +408        Setting this property updates these attributes as well.
    +409        """
    +410        return Request._unparse_url(self.scheme, self.host, self.port, self.path)
    +411
    +412    @url.setter
    +413    def url(self, val: str | bytes) -> None:
    +414        (self.scheme, self.host, self.port, self.path) = Request._parse_url(
    +415            always_str(val)
    +416        )
    +417
    +418    def _get_query(self) -> _ParsedQuery:
    +419        query = urllib.parse.urlparse(self.url).query
    +420        return tuple(url_decode(query))
    +421
    +422    def _set_query(self, query_data: Sequence[_QueryParam]):
    +423        query = url_encode(query_data)
    +424        _, _, path, params, _, fragment = urllib.parse.urlparse(self.url)
    +425        self.path = urllib.parse.urlunparse(["", "", path, params, query, fragment])
    +426
    +427    @property
    +428    def query(self) -> URLEncodedFormView:
    +429        """The query string parameters as a dict-like object
    +430
    +431        Returns:
    +432            QueryParamsView: The query string parameters
    +433        """
    +434        return URLEncodedFormView(
    +435            multidict.MultiDictView(self._get_query, self._set_query)
    +436        )
    +437
    +438    @query.setter
    +439    def query(self, value: Sequence[tuple[str, str]]):
    +440        self._set_query(value)
    +441
    +442    def _has_deserialized_content_changed(self) -> bool:
    +443        return self._deserialized_content != self._old_deserialized_content
    +444
    +445    def _serialize_content(self):
    +446        if self._serializer is None:
    +447            return
    +448
    +449        if self._deserialized_content is None:
    +450            self._content = None
    +451            return
    +452
    +453        self._update_serialized_content(
    +454            self._serializer.serialize(self._deserialized_content, req=self)
    +455        )
    +456
    +457    def _update_serialized_content(self, serialized: bytes):
    +458        if self._serializer is None:
    +459            self._content = serialized
    +460            return
    +461
    +462        # Update the parsed form
    +463        self._deserialized_content = self._serializer.deserialize(serialized, self)
    +464        self._old_deserialized_content = deepcopy(self._deserialized_content)
    +465
    +466        # Set the raw content directly
    +467        self._content = serialized
    +468
    +469    def _deserialize_content(self):
    +470        if self._serializer is None:
    +471            return
    +472
    +473        if self._content:
    +474            self._deserialized_content = self._serializer.deserialize(
    +475                self._content, req=self
    +476            )
    +477
    +478    def _update_deserialized_content(self, deserialized: Any):
    +479        if self._serializer is None:
    +480            return
    +481
    +482        if deserialized is None:
    +483            self._deserialized_content = None
    +484            self._old_deserialized_content = None
    +485            return
    +486
    +487        self._deserialized_content = deserialized
    +488        self._content = self._serializer.serialize(deserialized, self)
    +489        self._update_content_length()
    +490
    +491    @property
    +492    def content(self) -> bytes | None:
    +493        """The request content / body as raw bytes
    +494
    +495        Returns:
    +496            bytes | None: The content if it exists
    +497        """
    +498        if self._serializer and self._has_deserialized_content_changed():
    +499            self._update_deserialized_content(self._deserialized_content)
    +500            self._old_deserialized_content = deepcopy(self._deserialized_content)
    +501
    +502        self._update_content_length()
    +503
    +504        return self._content
    +505
    +506    @content.setter
    +507    def content(self, value: bytes | str | None):
    +508        match value:
    +509            case None:
    +510                self._content = None
    +511                self._deserialized_content = None
    +512                return
    +513            case str():
    +514                value = value.encode("latin-1")
    +515
    +516        self._update_content_length()
    +517
    +518        self._update_serialized_content(value)
    +519
    +520    @property
    +521    def body(self) -> bytes | None:
    +522        """Alias for content()
    +523
    +524        Returns:
    +525            bytes | None: The request body / content
    +526        """
    +527        return self.content
    +528
    +529    @body.setter
    +530    def body(self, value: bytes | str | None):
    +531        self.content = value
    +532
    +533    def update_serializer_from_content_type(
    +534        self,
    +535        content_type: ImplementedContentType | str | None = None,
    +536        fail_silently: bool = False,
    +537    ):
    +538        """Update the form parsing based on the given Content-Type
    +539
    +540        Args:
    +541            content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.
    +542            fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.
    +543
    +544        Raises:
    +545            FormNotParsedException: Raised when the content-type is unknown.
    +546        """
    +547        # Strip the boundary param so we can use our content-type to serializer map
    +548        _content_type: str = get_header_value_without_params(
    +549            content_type or self.headers.get("Content-Type") or ""
    +550        )
    +551
    +552        serializer = None
    +553        if _content_type in IMPLEMENTED_CONTENT_TYPES:
    +554            serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type)
    +555
    +556        if serializer is None:
    +557            if fail_silently:
    +558                serializer = self._serializer
    +559            else:
    +560                raise FormNotParsedException(
    +561                    f"Unimplemented form content-type: {_content_type}"
    +562                )
    +563        self._set_serializer(serializer)
    +564
    +565    @property
    +566    def content_type(self) -> str | None:
    +567        """The Content-Type header value.
    +568
    +569        Returns:
    +570            str | None: <=> self.headers.get("Content-Type")
    +571        """
    +572        return self.headers.get("Content-Type")
    +573
    +574    @content_type.setter
    +575    def content_type(self, value: str) -> str | None:
    +576        self.headers["Content-Type"] = value
    +577
    +578    def create_defaultform(
    +579        self,
    +580        content_type: ImplementedContentType | str | None = None,
    +581        update_header: bool = True,
    +582    ) -> MutableMapping[Any, Any]:
    +583        """Creates the form if it doesn't exist, else returns the existing one
    +584
    +585        Args:
    +586            content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.
    +587            update_header (bool, optional): Whether to update the header. Defaults to True.
    +588
    +589        Raises:
    +590            FormNotParsedException: Thrown when provided content-type has no implemented form-serializer
    +591            FormNotParsedException: Thrown when the raw content could not be parsed.
    +592
    +593        Returns:
    +594            MutableMapping[Any, Any]: The mapped form.
    +595        """
    +596        if not self._is_form_initialized or content_type:
    +597            self.update_serializer_from_content_type(content_type)
    +598
    +599            # Set content-type if it does not exist
    +600            if (content_type and update_header) or not self.headers.get_all(
    +601                "Content-Type"
    +602            ):
    +603                self.headers["Content-Type"] = content_type
    +604
    +605        serializer = self._serializer
    +606        if serializer is None:
    +607            # This should probably never trigger here as it should already be raised by update_serializer_from_content_type
    +608            raise FormNotParsedException(
    +609                f"Form of content-type {self.content_type} not implemented."
    +610            )
    +611
    +612        # Create default form.
    +613        if not self.content:
    +614            self._deserialized_content = serializer.get_empty_form(self)
    +615        elif self._deserialized_content is None:
    +616            self._deserialize_content()
    +617
    +618        if self._deserialized_content is None:
    +619            raise FormNotParsedException(
    +620                f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}"
    +621            )
    +622
    +623        if not isinstance(self._deserialized_content, serializer.deserialized_type()):
    +624            self._deserialized_content = serializer.get_empty_form(self)
    +625
    +626        self._is_form_initialized = True
    +627        return self._deserialized_content
    +628
    +629    @property
    +630    def form(self) -> MutableMapping[Any, Any]:
    +631        """Mapping from content parsed accordingly to Content-Type
    +632
    +633        Raises:
    +634            FormNotParsedException: The content could not be parsed accordingly to Content-Type
    +635
    +636        Returns:
    +637            MutableMapping[Any, Any]: The mapped request form
    +638        """
    +639        if not self._is_form_initialized:
    +640            self.update_serializer_from_content_type()
    +641
    +642        self.create_defaultform()
    +643        if self._deserialized_content is None:
    +644            raise FormNotParsedException()
    +645
    +646        self._is_form_initialized = True
    +647        return self._deserialized_content
    +648
    +649    @form.setter
    +650    def form(self, form: MutableMapping[Any, Any]):
    +651        if not self._is_form_initialized:
    +652            self.update_serializer_from_content_type()
    +653            self._is_form_initialized = True
    +654
    +655        self._deserialized_content = form
    +656
    +657        # Update raw _content
    +658        self._serialize_content()
    +659
    +660    def _set_serializer(self, serializer: FormSerializer | None):
    +661        # Update the serializer
    +662        old_serializer = self._serializer
    +663        self._serializer = serializer
    +664
    +665        if serializer is None:
    +666            self._deserialized_content = None
    +667            return
    +668
    +669        if type(serializer) == type(old_serializer):
    +670            return
    +671
    +672        if old_serializer is None:
    +673            self._deserialize_content()
    +674            return
    +675
    +676        old_form = self._deserialized_content
    +677
    +678        if old_form is None:
    +679            self._deserialize_content()
    +680            return
    +681
    +682        # Convert the form to an intermediate format for easier conversion
    +683        exported_form = old_serializer.export_form(old_form)
    +684
    +685        # Parse the intermediate data to the new serializer format
    +686        imported_form = serializer.import_form(exported_form, self)
    +687        self._deserialized_content = imported_form
    +688
    +689    def _update_serializer_and_get_form(
    +690        self, serializer: FormSerializer
    +691    ) -> MutableMapping[Any, Any] | None:
    +692        # Set the serializer and update the content
    +693        self._set_serializer(serializer)
    +694
    +695        # Return the new form
    +696        return self._deserialized_content
    +697
    +698    def _update_serializer_and_set_form(
    +699        self, serializer: FormSerializer, form: MutableMapping[Any, Any]
    +700    ) -> None:
    +701        # NOOP when the serializer is the same
    +702        self._set_serializer(serializer)
    +703
    +704        self._update_deserialized_content(form)
    +705
    +706    @property
    +707    def urlencoded_form(self) -> URLEncodedForm:
    +708        """The urlencoded form data
    +709
    +710        Converts the content to the urlencoded form format if needed.
    +711        Modification to this object will update Request.content and vice versa
    +712
    +713        Returns:
    +714            QueryParams: The urlencoded form data
    +715        """
    +716        self._is_form_initialized = True
    +717        return cast(
    +718            URLEncodedForm,
    +719            self._update_serializer_and_get_form(URLEncodedFormSerializer()),
    +720        )
    +721
    +722    @urlencoded_form.setter
    +723    def urlencoded_form(self, form: URLEncodedForm):
    +724        self._is_form_initialized = True
    +725        self._update_serializer_and_set_form(URLEncodedFormSerializer(), form)
    +726
    +727    @property
    +728    def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]:
    +729        """The JSON form data
    +730
    +731        Converts the content to the JSON form format if needed.
    +732        Modification to this object will update Request.content and vice versa
    +733
    +734        Returns:
    +735          dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data
    +736        """
    +737        self._is_form_initialized = True
    +738        if self._update_serializer_and_get_form(JSONFormSerializer()) is None:
    +739            serializer = cast(JSONFormSerializer, self._serializer)
    +740            self._deserialized_content = serializer.get_empty_form(self)
    +741
    +742        return self._deserialized_content
    +743
    +744    @json_form.setter
    +745    def json_form(self, form: dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]):
    +746        self._is_form_initialized = True
    +747        self._update_serializer_and_set_form(JSONFormSerializer(), JSONForm(form))
    +748
    +749    def _ensure_multipart_content_type(self) -> str:
    +750        content_types_headers = self.headers.get_all("Content-Type")
    +751        pattern = re.compile(
    +752            r"^multipart/form-data;\s*boundary=([^;\s]+)", re.IGNORECASE
    +753        )
    +754
    +755        # Find a valid multipart content-type header with a valid boundary
    +756        matched_content_type: str | None = None
    +757        for content_type in content_types_headers:
    +758            if pattern.match(content_type):
    +759                matched_content_type = content_type
    +760                break
    +761
    +762        # If no boundary was found, overwrite the Content-Type header
    +763        # If an user wants to avoid this behaviour,they should manually create a MultiPartForm(), convert it to bytes
    +764        #   and pass it as raw_form()
    +765        if matched_content_type is None:
    +766            # TODO: Randomly generate this? The boundary could be used to fingerprint Scalpel
    +767            new_content_type = (
    +768                "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI"
    +769            )
    +770            self.headers["Content-Type"] = new_content_type
    +771            return new_content_type
    +772
    +773        return matched_content_type
    +774
    +775    @property
    +776    def multipart_form(self) -> MultiPartForm:
    +777        """The multipart form data
    +778
    +779        Converts the content to the multipart form format if needed.
    +780        Modification to this object will update Request.content and vice versa
    +781
    +782        Returns:
    +783            MultiPartForm
    +784        """
    +785        self._is_form_initialized = True
    +786
    +787        # Keep boundary even if content-type has changed
    +788        if isinstance(self._deserialized_content, MultiPartForm):
    +789            return self._deserialized_content
    +790
    +791        # We do not have an existing form, so we have to ensure we have a content-type header with a boundary
    +792        self._ensure_multipart_content_type()
    +793
    +794        # Serialize the current form and try to parse it with the new serializer
    +795        form = self._update_serializer_and_get_form(MultiPartFormSerializer())
    +796        serializer = cast(MultiPartFormSerializer, self._serializer)
    +797
    +798        # Set a default value
    +799        if not form:
    +800            self._deserialized_content = serializer.get_empty_form(self)
    +801
    +802        # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary
    +803        if self._deserialized_content is None:
    +804            raise FormNotParsedException(
    +805                f"Could not parse content to {serializer.deserialized_type()}"
    +806            )
    +807
    +808        return self._deserialized_content
    +809
    +810    @multipart_form.setter
    +811    def multipart_form(self, form: MultiPartForm):
    +812        self._is_form_initialized = True
    +813        if not isinstance(self._deserialized_content, MultiPartForm):
    +814            # Generate a multipart header because we don't have any boundary to format the multipart.
    +815            self._ensure_multipart_content_type()
    +816
    +817        return self._update_serializer_and_set_form(
    +818            MultiPartFormSerializer(), cast(MutableMapping, form)
    +819        )
    +820
    +821    @property
    +822    def cookies(self) -> multidict.MultiDictView[str, str]:
    +823        """
    +824        The request cookies.
    +825        For the most part, this behaves like a dictionary.
    +826        Modifications to the MultiDictView update `Request.headers`, and vice versa.
    +827        """
    +828        return multidict.MultiDictView(self._get_cookies, self._set_cookies)
    +829
    +830    def _get_cookies(self) -> tuple[tuple[str, str], ...]:
    +831        header = self.headers.get_all("Cookie")
    +832        return tuple(cookies.parse_cookie_headers(header))
    +833
    +834    def _set_cookies(self, value: tuple[tuple[str, str], ...]):
    +835        self.headers["cookie"] = cookies.format_cookie_header(value)
    +836
    +837    @cookies.setter
    +838    def cookies(self, value: tuple[tuple[str, str], ...] | Mapping[str, str]):
    +839        if hasattr(value, "items") and callable(getattr(value, "items")):
    +840            value = tuple(cast(Mapping[str, str], value).items())
    +841        self._set_cookies(cast(tuple[tuple[str, str], ...], value))
    +842
    +843    @property
    +844    def host_header(self) -> str | None:
    +845        """Host header value
    +846
    +847        Returns:
    +848            str | None: The host header value
    +849        """
    +850        return self.headers.get("Host")
    +851
    +852    @host_header.setter
    +853    def host_header(self, value: str | None):
    +854        self.headers["Host"] = value
    +855
    +856    def text(self, encoding="utf-8") -> str:
    +857        """The decoded content
    +858
    +859        Args:
    +860            encoding (str, optional): encoding to use. Defaults to "utf-8".
    +861
    +862        Returns:
    +863            str: The decoded content
    +864        """
    +865        if self.content is None:
    +866            return ""
    +867
    +868        return self.content.decode(encoding)
    +869
    +870    @property
    +871    def headers(self) -> Headers:
    +872        """The request HTTP headers
    +873
    +874        Returns:
    +875            Headers: a case insensitive dict containing the HTTP headers
    +876        """
    +877        self._update_content_length()
    +878        return self._headers
    +879
    +880    @headers.setter
    +881    def headers(self, value: Headers):
    +882        self._headers = value
    +883        self._update_content_length()
    +884
    +885    @property
    +886    def content_length(self) -> int:
    +887        """Returns the Content-Length header value
    +888           Returns 0 if the header is absent
    +889
    +890        Args:
    +891            value (int | str): The Content-Length value
    +892
    +893        Raises:
    +894            RuntimeError: Throws RuntimeError when the value is invalid
    +895        """
    +896        content_length: str | None = self.headers.get("Content-Length")
    +897        if content_length is None:
    +898            return 0
    +899
    +900        trimmed = content_length.strip()
    +901        if not trimmed.isdigit():
    +902            raise ValueError("Content-Length does not contain only digits")
    +903
    +904        return int(trimmed)
    +905
    +906    @content_length.setter
    +907    def content_length(self, value: int | str):
    +908        if self.update_content_length:
    +909            # It is useless to manually set content-length because the value will be erased.
    +910            raise RuntimeError(
    +911                "Cannot set content_length when self.update_content_length is True"
    +912            )
    +913
    +914        if isinstance(value, int):
    +915            value = str(value)
    +916
    +917        self._headers["Content-Length"] = value
    +918
    +919    @property
    +920    def pretty_host(self) -> str:
    +921        """Returns the most approriate host
    +922        Returns self.host when it exists, else it returns self.host_header
    +923
    +924        Returns:
    +925            str: The request target host
    +926        """
    +927        return self.host or self.headers.get("Host") or ""
    +928
    +929    def host_is(self, *patterns: str) -> bool:
    +930        """Perform wildcard matching (fnmatch) on the target host.
    +931
    +932        Args:
    +933            pattern (str): The pattern to use
    +934
    +935        Returns:
    +936            bool: Whether the pattern matches
    +937        """
    +938        return host_is(self.pretty_host, *patterns)
    +939
    +940    def path_is(self, *patterns: str) -> bool:
    +941        return match_patterns(self.path, *patterns)
    +
    + + +

    A "Burp oriented" HTTP request class

    + +

    This class allows to manipulate Burp requests in a Pythonic way.

    +
    + + +
    + +
    + + Request( method: str, scheme: Literal['http', 'https'], host: str, port: int, path: str, http_version: str, headers: Union[Headers, tuple[tuple[bytes, bytes], ...], Iterable[tuple[bytes, bytes]]], authority: str, content: bytes | None) + + + +
    + +
    111    def __init__(
    +112        self,
    +113        method: str,
    +114        scheme: Literal["http", "https"],
    +115        host: str,
    +116        port: int,
    +117        path: str,
    +118        http_version: str,
    +119        headers: (
    +120            Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]]
    +121        ),
    +122        authority: str,
    +123        content: bytes | None,
    +124    ):
    +125        self.scheme = scheme
    +126        self.host = host
    +127        self.port = port
    +128        self.path = path
    +129        self.method = method
    +130        self.authority = authority
    +131        self.http_version = http_version
    +132        self.headers = headers if isinstance(headers, Headers) else Headers(headers)
    +133        self._content = content
    +134
    +135        # Initialize the serializer (json,urlencoded,multipart)
    +136        self.update_serializer_from_content_type(
    +137            self.headers.get("Content-Type"), fail_silently=True
    +138        )
    +139
    +140        # Initialize old deserialized content to avoid modifying content if it has not been modified
    +141        # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py)
    +142        self._old_deserialized_content = deepcopy(self._deserialized_content)
    +
    + + + + +
    +
    +
    + host: str + + +
    + + + + +
    +
    +
    + port: int + + +
    + + + + +
    +
    +
    + method: str + + +
    + + + + +
    +
    +
    + scheme: Literal['http', 'https'] + + +
    + + + + +
    +
    +
    + authority: str + + +
    + + + + +
    +
    +
    + path: str + + +
    + + + + +
    +
    +
    + http_version: str + + +
    + + + + +
    +
    +
    + update_content_length: bool = +True + + +
    + + + + +
    +
    + +
    + headers: Headers + + + +
    + +
    870    @property
    +871    def headers(self) -> Headers:
    +872        """The request HTTP headers
    +873
    +874        Returns:
    +875            Headers: a case insensitive dict containing the HTTP headers
    +876        """
    +877        self._update_content_length()
    +878        return self._headers
    +
    + + +

    The request HTTP headers

    + +

    Returns: + Headers: a case insensitive dict containing the HTTP headers

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + make( cls, method: str, url: str, content: bytes | str = '', headers: Union[Headers, dict[str | bytes, str | bytes], dict[str, str], dict[bytes, bytes], Iterable[tuple[bytes, bytes]]] = ()) -> python3-10.pyscalpel.http.request.Request: + + + +
    + +
    183    @classmethod
    +184    def make(
    +185        cls,
    +186        method: str,
    +187        url: str,
    +188        content: bytes | str = "",
    +189        headers: (
    +190            Headers
    +191            | dict[str | bytes, str | bytes]
    +192            | dict[str, str]
    +193            | dict[bytes, bytes]
    +194            | Iterable[tuple[bytes, bytes]]
    +195        ) = (),
    +196    ) -> Request:
    +197        """Create a request from an URL
    +198
    +199        Args:
    +200            method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)
    +201            url (str): The request URL
    +202            content (bytes | str, optional): The request content. Defaults to "".
    +203            headers (Headers, optional): The request headers. Defaults to ().
    +204
    +205        Returns:
    +206            Request: The HTTP request
    +207        """
    +208        scalpel_headers: Headers
    +209        match headers:
    +210            case Headers():
    +211                scalpel_headers = headers
    +212            case dict():
    +213                casted_headers = cast(dict[str | bytes, str | bytes], headers)
    +214                scalpel_headers = Headers(
    +215                    (
    +216                        (always_bytes(key), always_bytes(val))
    +217                        for key, val in casted_headers.items()
    +218                    )
    +219                )
    +220            case _:
    +221                scalpel_headers = Headers(headers)
    +222
    +223        scheme, host, port, path = Request._parse_url(url)
    +224        http_version = "HTTP/1.1"
    +225
    +226        # Inferr missing Host header from URL
    +227        host_header = scalpel_headers.get("Host")
    +228        if host_header is None:
    +229            match (scheme, port):
    +230                case ("http", 80) | ("https", 443):
    +231                    host_header = host
    +232                case _:
    +233                    host_header = f"{host}:{port}"
    +234
    +235            scalpel_headers["Host"] = host_header
    +236
    +237        authority: str = host_header
    +238        encoded_content = always_bytes(content)
    +239
    +240        assert isinstance(host, str)
    +241
    +242        return cls(
    +243            method=method,
    +244            scheme=scheme,
    +245            host=host,
    +246            port=port,
    +247            path=path,
    +248            http_version=http_version,
    +249            headers=scalpel_headers,
    +250            authority=authority,
    +251            content=encoded_content,
    +252        )
    +
    + + +

    Create a request from an URL

    + +

    Args: + method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...) + url (str): The request URL + content (bytes | str, optional): The request content. Defaults to "". + headers (Headers, optional): The request headers. Defaults to ().

    + +

    Returns: + Request: The HTTP request

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_burp( cls, request: pyscalpel.java.burp.http_request.IHttpRequest, service: pyscalpel.java.burp.http_service.IHttpService | None = None) -> python3-10.pyscalpel.http.request.Request: + + + +
    + +
    254    @classmethod
    +255    def from_burp(
    +256        cls, request: IHttpRequest, service: IHttpService | None = None
    +257    ) -> Request:  # pragma: no cover (uses Java API)
    +258        """Construct an instance of the Request class from a Burp suite HttpRequest.
    +259        :param request: The Burp suite HttpRequest to convert.
    +260        :return: A Request with the same data as the Burp suite HttpRequest.
    +261        """
    +262        service = service or request.httpService()
    +263        body = get_bytes(request.body())
    +264
    +265        # Burp will give you lowercased and pseudo headers when using HTTP/2.
    +266        # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-.
    +267        # https://blog.yaakov.online/http-2-header-casing/
    +268        headers: Headers = Headers.from_burp(request.headers())
    +269
    +270        # Burp gives a 0 length byte array body even when it doesn't exist, instead of null.
    +271        # Empty but existing bodies without a Content-Length header are lost in the process.
    +272        if not body and not headers.get("Content-Length"):
    +273            body = None
    +274
    +275        # request.url() gives a relative url for some reason
    +276        # So we have to parse and unparse to get the full path
    +277        #   (path + parameters + query + fragment)
    +278        _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url())
    +279
    +280        # Concatenate the path components
    +281        # Empty parameters,query and fragment are lost in the process
    +282        # e.g.: http://example.com;?# becomes http://example.com
    +283        # To use such an URL, the user must set the path directly
    +284        # To fix this we would need to write our own URL parser, which is a bit overkill for now.
    +285        path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment))
    +286
    +287        host = ""
    +288        port = 0
    +289        scheme = "http"
    +290        if service:
    +291            host = service.host()
    +292            port = service.port()
    +293            scheme = "https" if service.secure() else "http"
    +294
    +295        return cls(
    +296            method=request.method(),
    +297            scheme=scheme,
    +298            host=host,
    +299            port=port,
    +300            path=path,
    +301            http_version=request.httpVersion() or "HTTP/1.1",
    +302            headers=headers,
    +303            authority=headers.get(":authority") or headers.get("Host") or "",
    +304            content=body,
    +305        )
    +
    + + +

    Construct an instance of the Request class from a Burp suite HttpRequest.

    + +
    Parameters
    + +
      +
    • request: The Burp suite HttpRequest to convert.
    • +
    + +
    Returns
    + +
    +

    A Request with the same data as the Burp suite HttpRequest.

    +
    +
    + + +
    +
    + +
    + + def + to_burp(self) -> pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    344    def to_burp(self) -> IHttpRequest:  # pragma: no cover
    +345        """Convert the request to a Burp suite :class:`IHttpRequest`.
    +346        :return: The request as a Burp suite :class:`IHttpRequest`.
    +347        """
    +348        # Convert the request to a Burp ByteArray.
    +349        request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +350
    +351        if self.port == 0:
    +352            # No networking information is available, so we build a plain network-less request.
    +353            return HttpRequest.httpRequest(request_byte_array)
    +354
    +355        # Build the Burp HTTP networking service.
    +356        service: IHttpService = HttpService.httpService(
    +357            self.host, self.port, self.scheme == "https"
    +358        )
    +359
    +360        # Instantiate and return a new Burp HTTP request.
    +361        return HttpRequest.httpRequest(service, request_byte_array)
    +
    + + +

    Convert the request to a Burp suite IHttpRequest.

    + +
    Returns
    + +
    +

    The request as a Burp suite IHttpRequest.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_raw( cls, data: bytes | str, real_host: str = '', port: int = 0, scheme: Union[Literal['http'], Literal['https'], str] = 'http') -> python3-10.pyscalpel.http.request.Request: + + + +
    + +
    363    @classmethod
    +364    def from_raw(
    +365        cls,
    +366        data: bytes | str,
    +367        real_host: str = "",
    +368        port: int = 0,
    +369        scheme: Literal["http"] | Literal["https"] | str = "http",
    +370    ) -> Request:  # pragma: no cover
    +371        """Construct an instance of the Request class from raw bytes.
    +372        :param data: The raw bytes to convert.
    +373        :param real_host: The real host to connect to.
    +374        :param port: The port of the request.
    +375        :param scheme: The scheme of the request.
    +376        :return: A :class:`Request` with the same data as the raw bytes.
    +377        """
    +378        # Convert the raw bytes to a Burp ByteArray.
    +379        # We use the Burp API to trivialize the parsing of the request from raw bytes.
    +380        str_or_byte_array: IByteArray | str = (
    +381            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +382        )
    +383
    +384        # Handle the case where the networking informations are not provided.
    +385        if port == 0:
    +386            # Instantiate and return a new Burp HTTP request without networking informations.
    +387            burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array)
    +388        else:
    +389            # Build the Burp HTTP networking service.
    +390            service: IHttpService = HttpService.httpService(
    +391                real_host, port, scheme == "https"
    +392            )
    +393
    +394            # Instantiate a new Burp HTTP request with networking informations.
    +395            burp_request: IHttpRequest = HttpRequest.httpRequest(
    +396                service, str_or_byte_array
    +397            )
    +398
    +399        # Construct the request from the Burp.
    +400        return cls.from_burp(burp_request)
    +
    + + +

    Construct an instance of the Request class from raw bytes.

    + +
    Parameters
    + +
      +
    • data: The raw bytes to convert.
    • +
    • real_host: The real host to connect to.
    • +
    • port: The port of the request.
    • +
    • scheme: The scheme of the request.
    • +
    + +
    Returns
    + +
    +

    A Request with the same data as the raw bytes.

    +
    +
    + + +
    +
    + +
    + url: str + + + +
    + +
    402    @property
    +403    def url(self) -> str:
    +404        """
    +405        The full URL string, constructed from `Request.scheme`,
    +406            `Request.host`, `Request.port` and `Request.path`.
    +407
    +408        Setting this property updates these attributes as well.
    +409        """
    +410        return Request._unparse_url(self.scheme, self.host, self.port, self.path)
    +
    + + +

    The full URL string, constructed from Request.scheme, + Request.host, Request.port and Request.path.

    + +

    Setting this property updates these attributes as well.

    +
    + + +
    +
    + +
    + query: pyscalpel.http.body.urlencoded.URLEncodedFormView + + + +
    + +
    427    @property
    +428    def query(self) -> URLEncodedFormView:
    +429        """The query string parameters as a dict-like object
    +430
    +431        Returns:
    +432            QueryParamsView: The query string parameters
    +433        """
    +434        return URLEncodedFormView(
    +435            multidict.MultiDictView(self._get_query, self._set_query)
    +436        )
    +
    + + +

    The query string parameters as a dict-like object

    + +

    Returns: + QueryParamsView: The query string parameters

    +
    + + +
    +
    + +
    + content: bytes | None + + + +
    + +
    491    @property
    +492    def content(self) -> bytes | None:
    +493        """The request content / body as raw bytes
    +494
    +495        Returns:
    +496            bytes | None: The content if it exists
    +497        """
    +498        if self._serializer and self._has_deserialized_content_changed():
    +499            self._update_deserialized_content(self._deserialized_content)
    +500            self._old_deserialized_content = deepcopy(self._deserialized_content)
    +501
    +502        self._update_content_length()
    +503
    +504        return self._content
    +
    + + +

    The request content / body as raw bytes

    + +

    Returns: + bytes | None: The content if it exists

    +
    + + +
    +
    + +
    + body: bytes | None + + + +
    + +
    520    @property
    +521    def body(self) -> bytes | None:
    +522        """Alias for content()
    +523
    +524        Returns:
    +525            bytes | None: The request body / content
    +526        """
    +527        return self.content
    +
    + + +

    Alias for content()

    + +

    Returns: + bytes | None: The request body / content

    +
    + + +
    +
    + +
    + + def + update_serializer_from_content_type( self, content_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None, fail_silently: bool = False): + + + +
    + +
    533    def update_serializer_from_content_type(
    +534        self,
    +535        content_type: ImplementedContentType | str | None = None,
    +536        fail_silently: bool = False,
    +537    ):
    +538        """Update the form parsing based on the given Content-Type
    +539
    +540        Args:
    +541            content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.
    +542            fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.
    +543
    +544        Raises:
    +545            FormNotParsedException: Raised when the content-type is unknown.
    +546        """
    +547        # Strip the boundary param so we can use our content-type to serializer map
    +548        _content_type: str = get_header_value_without_params(
    +549            content_type or self.headers.get("Content-Type") or ""
    +550        )
    +551
    +552        serializer = None
    +553        if _content_type in IMPLEMENTED_CONTENT_TYPES:
    +554            serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type)
    +555
    +556        if serializer is None:
    +557            if fail_silently:
    +558                serializer = self._serializer
    +559            else:
    +560                raise FormNotParsedException(
    +561                    f"Unimplemented form content-type: {_content_type}"
    +562                )
    +563        self._set_serializer(serializer)
    +
    + + +

    Update the form parsing based on the given Content-Type

    + +

    Args: + content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None. + fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.

    + +

    Raises: + FormNotParsedException: Raised when the content-type is unknown.

    +
    + + +
    +
    + +
    + content_type: str | None + + + +
    + +
    565    @property
    +566    def content_type(self) -> str | None:
    +567        """The Content-Type header value.
    +568
    +569        Returns:
    +570            str | None: <=> self.headers.get("Content-Type")
    +571        """
    +572        return self.headers.get("Content-Type")
    +
    + + +

    The Content-Type header value.

    + +

    Returns: + str | None: <=> self.headers.get("Content-Type")

    +
    + + +
    +
    + +
    + + def + create_defaultform( self, content_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None, update_header: bool = True) -> MutableMapping[Any, Any]: + + + +
    + +
    578    def create_defaultform(
    +579        self,
    +580        content_type: ImplementedContentType | str | None = None,
    +581        update_header: bool = True,
    +582    ) -> MutableMapping[Any, Any]:
    +583        """Creates the form if it doesn't exist, else returns the existing one
    +584
    +585        Args:
    +586            content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.
    +587            update_header (bool, optional): Whether to update the header. Defaults to True.
    +588
    +589        Raises:
    +590            FormNotParsedException: Thrown when provided content-type has no implemented form-serializer
    +591            FormNotParsedException: Thrown when the raw content could not be parsed.
    +592
    +593        Returns:
    +594            MutableMapping[Any, Any]: The mapped form.
    +595        """
    +596        if not self._is_form_initialized or content_type:
    +597            self.update_serializer_from_content_type(content_type)
    +598
    +599            # Set content-type if it does not exist
    +600            if (content_type and update_header) or not self.headers.get_all(
    +601                "Content-Type"
    +602            ):
    +603                self.headers["Content-Type"] = content_type
    +604
    +605        serializer = self._serializer
    +606        if serializer is None:
    +607            # This should probably never trigger here as it should already be raised by update_serializer_from_content_type
    +608            raise FormNotParsedException(
    +609                f"Form of content-type {self.content_type} not implemented."
    +610            )
    +611
    +612        # Create default form.
    +613        if not self.content:
    +614            self._deserialized_content = serializer.get_empty_form(self)
    +615        elif self._deserialized_content is None:
    +616            self._deserialize_content()
    +617
    +618        if self._deserialized_content is None:
    +619            raise FormNotParsedException(
    +620                f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}"
    +621            )
    +622
    +623        if not isinstance(self._deserialized_content, serializer.deserialized_type()):
    +624            self._deserialized_content = serializer.get_empty_form(self)
    +625
    +626        self._is_form_initialized = True
    +627        return self._deserialized_content
    +
    + + +

    Creates the form if it doesn't exist, else returns the existing one

    + +

    Args: + content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None. + update_header (bool, optional): Whether to update the header. Defaults to True.

    + +

    Raises: + FormNotParsedException: Thrown when provided content-type has no implemented form-serializer + FormNotParsedException: Thrown when the raw content could not be parsed.

    + +

    Returns: + MutableMapping[Any, Any]: The mapped form.

    +
    + + +
    +
    + +
    + form: MutableMapping[Any, Any] + + + +
    + +
    629    @property
    +630    def form(self) -> MutableMapping[Any, Any]:
    +631        """Mapping from content parsed accordingly to Content-Type
    +632
    +633        Raises:
    +634            FormNotParsedException: The content could not be parsed accordingly to Content-Type
    +635
    +636        Returns:
    +637            MutableMapping[Any, Any]: The mapped request form
    +638        """
    +639        if not self._is_form_initialized:
    +640            self.update_serializer_from_content_type()
    +641
    +642        self.create_defaultform()
    +643        if self._deserialized_content is None:
    +644            raise FormNotParsedException()
    +645
    +646        self._is_form_initialized = True
    +647        return self._deserialized_content
    +
    + + +

    Mapping from content parsed accordingly to Content-Type

    + +

    Raises: + FormNotParsedException: The content could not be parsed accordingly to Content-Type

    + +

    Returns: + MutableMapping[Any, Any]: The mapped request form

    +
    + + +
    +
    + +
    + urlencoded_form: pyscalpel.http.body.urlencoded.URLEncodedForm + + + +
    + +
    706    @property
    +707    def urlencoded_form(self) -> URLEncodedForm:
    +708        """The urlencoded form data
    +709
    +710        Converts the content to the urlencoded form format if needed.
    +711        Modification to this object will update Request.content and vice versa
    +712
    +713        Returns:
    +714            QueryParams: The urlencoded form data
    +715        """
    +716        self._is_form_initialized = True
    +717        return cast(
    +718            URLEncodedForm,
    +719            self._update_serializer_and_get_form(URLEncodedFormSerializer()),
    +720        )
    +
    + + +

    The urlencoded form data

    + +

    Converts the content to the urlencoded form format if needed. +Modification to this object will update Request.content and vice versa

    + +

    Returns: + QueryParams: The urlencoded form data

    +
    + + +
    +
    + +
    + json_form: dict[str | int | float, str | int | float | bool | None | list[str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]] | dict[str | int | float, str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]]] + + + +
    + +
    727    @property
    +728    def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]:
    +729        """The JSON form data
    +730
    +731        Converts the content to the JSON form format if needed.
    +732        Modification to this object will update Request.content and vice versa
    +733
    +734        Returns:
    +735          dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data
    +736        """
    +737        self._is_form_initialized = True
    +738        if self._update_serializer_and_get_form(JSONFormSerializer()) is None:
    +739            serializer = cast(JSONFormSerializer, self._serializer)
    +740            self._deserialized_content = serializer.get_empty_form(self)
    +741
    +742        return self._deserialized_content
    +
    + + +

    The JSON form data

    + +

    Converts the content to the JSON form format if needed. +Modification to this object will update Request.content and vice versa

    + +

    Returns: + dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data

    +
    + + +
    +
    + +
    + multipart_form: pyscalpel.http.body.multipart.MultiPartForm + + + +
    + +
    775    @property
    +776    def multipart_form(self) -> MultiPartForm:
    +777        """The multipart form data
    +778
    +779        Converts the content to the multipart form format if needed.
    +780        Modification to this object will update Request.content and vice versa
    +781
    +782        Returns:
    +783            MultiPartForm
    +784        """
    +785        self._is_form_initialized = True
    +786
    +787        # Keep boundary even if content-type has changed
    +788        if isinstance(self._deserialized_content, MultiPartForm):
    +789            return self._deserialized_content
    +790
    +791        # We do not have an existing form, so we have to ensure we have a content-type header with a boundary
    +792        self._ensure_multipart_content_type()
    +793
    +794        # Serialize the current form and try to parse it with the new serializer
    +795        form = self._update_serializer_and_get_form(MultiPartFormSerializer())
    +796        serializer = cast(MultiPartFormSerializer, self._serializer)
    +797
    +798        # Set a default value
    +799        if not form:
    +800            self._deserialized_content = serializer.get_empty_form(self)
    +801
    +802        # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary
    +803        if self._deserialized_content is None:
    +804            raise FormNotParsedException(
    +805                f"Could not parse content to {serializer.deserialized_type()}"
    +806            )
    +807
    +808        return self._deserialized_content
    +
    + + +

    The multipart form data

    + +

    Converts the content to the multipart form format if needed. +Modification to this object will update Request.content and vice versa

    + +

    Returns: + MultiPartForm

    +
    + + +
    +
    + +
    + cookies: _internal_mitmproxy.coretypes.multidict.MultiDictView[str, str] + + + +
    + +
    821    @property
    +822    def cookies(self) -> multidict.MultiDictView[str, str]:
    +823        """
    +824        The request cookies.
    +825        For the most part, this behaves like a dictionary.
    +826        Modifications to the MultiDictView update `Request.headers`, and vice versa.
    +827        """
    +828        return multidict.MultiDictView(self._get_cookies, self._set_cookies)
    +
    + + +

    The request cookies. +For the most part, this behaves like a dictionary. +Modifications to the MultiDictView update Request.headers, and vice versa.

    +
    + + +
    +
    + +
    + host_header: str | None + + + +
    + +
    843    @property
    +844    def host_header(self) -> str | None:
    +845        """Host header value
    +846
    +847        Returns:
    +848            str | None: The host header value
    +849        """
    +850        return self.headers.get("Host")
    +
    + + +

    Host header value

    + +

    Returns: + str | None: The host header value

    +
    + + +
    +
    + +
    + + def + text(self, encoding='utf-8') -> str: + + + +
    + +
    856    def text(self, encoding="utf-8") -> str:
    +857        """The decoded content
    +858
    +859        Args:
    +860            encoding (str, optional): encoding to use. Defaults to "utf-8".
    +861
    +862        Returns:
    +863            str: The decoded content
    +864        """
    +865        if self.content is None:
    +866            return ""
    +867
    +868        return self.content.decode(encoding)
    +
    + + +

    The decoded content

    + +

    Args: + encoding (str, optional): encoding to use. Defaults to "utf-8".

    + +

    Returns: + str: The decoded content

    +
    + + +
    +
    + +
    + content_length: int + + + +
    + +
    885    @property
    +886    def content_length(self) -> int:
    +887        """Returns the Content-Length header value
    +888           Returns 0 if the header is absent
    +889
    +890        Args:
    +891            value (int | str): The Content-Length value
    +892
    +893        Raises:
    +894            RuntimeError: Throws RuntimeError when the value is invalid
    +895        """
    +896        content_length: str | None = self.headers.get("Content-Length")
    +897        if content_length is None:
    +898            return 0
    +899
    +900        trimmed = content_length.strip()
    +901        if not trimmed.isdigit():
    +902            raise ValueError("Content-Length does not contain only digits")
    +903
    +904        return int(trimmed)
    +
    + + +

    Returns the Content-Length header value + Returns 0 if the header is absent

    + +

    Args: + value (int | str): The Content-Length value

    + +

    Raises: + RuntimeError: Throws RuntimeError when the value is invalid

    +
    + + +
    +
    + +
    + pretty_host: str + + + +
    + +
    919    @property
    +920    def pretty_host(self) -> str:
    +921        """Returns the most approriate host
    +922        Returns self.host when it exists, else it returns self.host_header
    +923
    +924        Returns:
    +925            str: The request target host
    +926        """
    +927        return self.host or self.headers.get("Host") or ""
    +
    + + +

    Returns the most approriate host +Returns self.host when it exists, else it returns self.host_header

    + +

    Returns: + str: The request target host

    +
    + + +
    +
    + +
    + + def + host_is(self, *patterns: str) -> bool: + + + +
    + +
    929    def host_is(self, *patterns: str) -> bool:
    +930        """Perform wildcard matching (fnmatch) on the target host.
    +931
    +932        Args:
    +933            pattern (str): The pattern to use
    +934
    +935        Returns:
    +936            bool: Whether the pattern matches
    +937        """
    +938        return host_is(self.pretty_host, *patterns)
    +
    + + +

    Perform wildcard matching (fnmatch) on the target host.

    + +

    Args: + pattern (str): The pattern to use

    + +

    Returns: + bool: Whether the pattern matches

    +
    + + +
    +
    + +
    + + def + path_is(self, *patterns: str) -> bool: + + + +
    + +
    940    def path_is(self, *patterns: str) -> bool:
    +941        return match_patterns(self.path, *patterns)
    +
    + + + + +
    +
    +
    + +
    + + class + Response(_internal_mitmproxy.http.Response): + + + +
    + +
     22class Response(MITMProxyResponse):
    + 23    """A "Burp oriented" HTTP response class
    + 24
    + 25
    + 26    This class allows to manipulate Burp responses in a Pythonic way.
    + 27
    + 28    Fields:
    + 29        scheme: http or https
    + 30        host: The initiating request target host
    + 31        port: The initiating request target port
    + 32        request: The initiating request.
    + 33    """
    + 34
    + 35    scheme: Literal["http", "https"] = "http"
    + 36    host: str = ""
    + 37    port: int = 0
    + 38    request: Request | None = None
    + 39
    + 40    def __init__(
    + 41        self,
    + 42        http_version: bytes,
    + 43        status_code: int,
    + 44        reason: bytes,
    + 45        headers: Headers | tuple[tuple[bytes, bytes], ...],
    + 46        content: bytes | None,
    + 47        trailers: Headers | tuple[tuple[bytes, bytes], ...] | None,
    + 48        scheme: Literal["http", "https"] = "http",
    + 49        host: str = "",
    + 50        port: int = 0,
    + 51    ):
    + 52        # Construct the base/inherited MITMProxy response.
    + 53        super().__init__(
    + 54            http_version,
    + 55            status_code,
    + 56            reason,
    + 57            headers,
    + 58            content,
    + 59            trailers,
    + 60            timestamp_start=time.time(),
    + 61            timestamp_end=time.time(),
    + 62        )
    + 63        self.scheme = scheme
    + 64        self.host = host
    + 65        self.port = port
    + 66
    + 67    @classmethod
    + 68    # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
    + 69    # link to mitmproxy documentation
    + 70    def from_mitmproxy(cls, response: MITMProxyResponse) -> Response:
    + 71        """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    + 72        :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert.
    + 73        :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    + 74        """
    + 75        return cls(
    + 76            always_bytes(response.http_version),
    + 77            response.status_code,
    + 78            always_bytes(response.reason),
    + 79            Headers.from_mitmproxy(response.headers),
    + 80            response.content,
    + 81            Headers.from_mitmproxy(response.trailers) if response.trailers else None,
    + 82        )
    + 83
    + 84    @classmethod
    + 85    def from_burp(
    + 86        cls,
    + 87        response: IHttpResponse,
    + 88        service: IHttpService | None = None,
    + 89        request: IHttpRequest | None = None,
    + 90    ) -> Response:
    + 91        """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`."""
    + 92        body = get_bytes(cast(IByteArray, response.body())) if response.body() else b""
    + 93        scalpel_response = cls(
    + 94            always_bytes(response.httpVersion() or "HTTP/1.1"),
    + 95            response.statusCode(),
    + 96            always_bytes(response.reasonPhrase() or b""),
    + 97            Headers.from_burp(response.headers()),
    + 98            body,
    + 99            None,
    +100        )
    +101
    +102        burp_request: IHttpRequest | None = request
    +103        if burp_request is None:
    +104            try:
    +105                # Some responses can have a "initiatingRequest" field.
    +106                # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A
    +107                burp_request = response.initiatingRequest()  # type: ignore
    +108            except AttributeError:
    +109                pass
    +110
    +111        if burp_request:
    +112            scalpel_response.request = Request.from_burp(burp_request, service)
    +113
    +114        if not service and burp_request:
    +115            # The only way to check if the Java method exist without writing Java is catching the error.
    +116            service = burp_request.httpService()
    +117
    +118        if service:
    +119            scalpel_response.scheme = "https" if service.secure() else "http"
    +120            scalpel_response.host = service.host()
    +121            scalpel_response.port = service.port()
    +122
    +123        return scalpel_response
    +124
    +125    def __bytes__(self) -> bytes:
    +126        """Convert the response to raw bytes."""
    +127        # Reserialize the response to bytes.
    +128
    +129        # Format the first line of the response. (e.g. "HTTP/1.1 200 OK\r\n")
    +130        first_line = (
    +131            b" ".join(
    +132                always_bytes(s)
    +133                for s in (self.http_version, str(self.status_code), self.reason)
    +134            )
    +135            + b"\r\n"
    +136        )
    +137
    +138        # Format the response's headers part.
    +139        headers_lines = b"".join(
    +140            b"%s: %s\r\n" % (key, val) for key, val in self.headers.fields
    +141        )
    +142
    +143        # Set a default value for the response's body. (None -> b"")
    +144        body = self.content or b""
    +145
    +146        # Build the whole response and return it.
    +147        return first_line + headers_lines + b"\r\n" + body
    +148
    +149    def to_burp(self) -> IHttpResponse:  # pragma: no cover (uses Java API)
    +150        """Convert the response to a Burp suite :class:`IHttpResponse`."""
    +151        response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +152
    +153        return HttpResponse.httpResponse(response_byte_array)
    +154
    +155    @classmethod
    +156    def from_raw(
    +157        cls, data: bytes | str
    +158    ) -> Response:  # pragma: no cover (uses Java API)
    +159        """Construct an instance of the Response class from raw bytes.
    +160        :param data: The raw bytes to convert.
    +161        :return: A :class:`Response` parsed from the raw bytes.
    +162        """
    +163        # Use the Burp API to trivialize the parsing of the response from raw bytes.
    +164        # Convert the raw bytes to a Burp ByteArray.
    +165        # Plain strings are OK too.
    +166        str_or_byte_array: IByteArray | str = (
    +167            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +168        )
    +169
    +170        # Instantiate a new Burp HTTP response.
    +171        burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array)
    +172
    +173        return cls.from_burp(burp_response)
    +174
    +175    @classmethod
    +176    def make(
    +177        cls,
    +178        status_code: int = 200,
    +179        content: bytes | str = b"",
    +180        headers: Headers | tuple[tuple[bytes, bytes], ...] = (),
    +181        host: str = "",
    +182        port: int = 0,
    +183        scheme: Literal["http", "https"] = "http",
    +184    ) -> "Response":
    +185        # Use the base/inherited make method to construct a MITMProxy response.
    +186        mitmproxy_res = MITMProxyResponse.make(status_code, content, headers)
    +187
    +188        res = cls.from_mitmproxy(mitmproxy_res)
    +189        res.host = host
    +190        res.scheme = scheme
    +191        res.port = port
    +192
    +193        return res
    +194
    +195    def host_is(self, *patterns: str) -> bool:
    +196        """Matches the host against the provided patterns
    +197
    +198        Returns:
    +199            bool: Whether at least one pattern matched
    +200        """
    +201        return host_is(self.host, *patterns)
    +202
    +203    @property
    +204    def body(self) -> bytes | None:
    +205        """Alias for content()
    +206
    +207        Returns:
    +208            bytes | None: The request body / content
    +209        """
    +210        return self.content
    +211
    +212    @body.setter
    +213    def body(self, val: bytes | None):
    +214        self.content = val
    +
    + + +

    A "Burp oriented" HTTP response class

    + +

    This class allows to manipulate Burp responses in a Pythonic way.

    + +

    Fields: + scheme: http or https + host: The initiating request target host + port: The initiating request target port + request: The initiating request.

    +
    + + +
    + +
    + + Response( http_version: bytes, status_code: int, reason: bytes, headers: Headers | tuple[tuple[bytes, bytes], ...], content: bytes | None, trailers: Headers | tuple[tuple[bytes, bytes], ...] | None, scheme: Literal['http', 'https'] = 'http', host: str = '', port: int = 0) + + + +
    + +
    40    def __init__(
    +41        self,
    +42        http_version: bytes,
    +43        status_code: int,
    +44        reason: bytes,
    +45        headers: Headers | tuple[tuple[bytes, bytes], ...],
    +46        content: bytes | None,
    +47        trailers: Headers | tuple[tuple[bytes, bytes], ...] | None,
    +48        scheme: Literal["http", "https"] = "http",
    +49        host: str = "",
    +50        port: int = 0,
    +51    ):
    +52        # Construct the base/inherited MITMProxy response.
    +53        super().__init__(
    +54            http_version,
    +55            status_code,
    +56            reason,
    +57            headers,
    +58            content,
    +59            trailers,
    +60            timestamp_start=time.time(),
    +61            timestamp_end=time.time(),
    +62        )
    +63        self.scheme = scheme
    +64        self.host = host
    +65        self.port = port
    +
    + + + + +
    +
    +
    + scheme: Literal['http', 'https'] = +'http' + + +
    + + + + +
    +
    +
    + host: str = +'' + + +
    + + + + +
    +
    +
    + port: int = +0 + + +
    + + + + +
    +
    +
    + request: pyscalpel.http.request.Request | None = +None + + +
    + + + + +
    +
    + +
    +
    @classmethod
    + + def + from_mitmproxy( cls, response: _internal_mitmproxy.http.Response) -> python3-10.pyscalpel.http.response.Response: + + + +
    + +
    67    @classmethod
    +68    # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response
    +69    # link to mitmproxy documentation
    +70    def from_mitmproxy(cls, response: MITMProxyResponse) -> Response:
    +71        """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    +72        :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert.
    +73        :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response).
    +74        """
    +75        return cls(
    +76            always_bytes(response.http_version),
    +77            response.status_code,
    +78            always_bytes(response.reason),
    +79            Headers.from_mitmproxy(response.headers),
    +80            response.content,
    +81            Headers.from_mitmproxy(response.trailers) if response.trailers else None,
    +82        )
    +
    + + +

    Construct an instance of the Response class from a mitmproxy.http.HTTPResponse.

    + +
    Parameters
    + + + +
    Returns
    + +
    +

    A Response with the same data as the mitmproxy.http.HTTPResponse.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_burp( cls, response: pyscalpel.java.burp.http_response.IHttpResponse, service: pyscalpel.java.burp.http_service.IHttpService | None = None, request: pyscalpel.java.burp.http_request.IHttpRequest | None = None) -> python3-10.pyscalpel.http.response.Response: + + + +
    + +
     84    @classmethod
    + 85    def from_burp(
    + 86        cls,
    + 87        response: IHttpResponse,
    + 88        service: IHttpService | None = None,
    + 89        request: IHttpRequest | None = None,
    + 90    ) -> Response:
    + 91        """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`."""
    + 92        body = get_bytes(cast(IByteArray, response.body())) if response.body() else b""
    + 93        scalpel_response = cls(
    + 94            always_bytes(response.httpVersion() or "HTTP/1.1"),
    + 95            response.statusCode(),
    + 96            always_bytes(response.reasonPhrase() or b""),
    + 97            Headers.from_burp(response.headers()),
    + 98            body,
    + 99            None,
    +100        )
    +101
    +102        burp_request: IHttpRequest | None = request
    +103        if burp_request is None:
    +104            try:
    +105                # Some responses can have a "initiatingRequest" field.
    +106                # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A
    +107                burp_request = response.initiatingRequest()  # type: ignore
    +108            except AttributeError:
    +109                pass
    +110
    +111        if burp_request:
    +112            scalpel_response.request = Request.from_burp(burp_request, service)
    +113
    +114        if not service and burp_request:
    +115            # The only way to check if the Java method exist without writing Java is catching the error.
    +116            service = burp_request.httpService()
    +117
    +118        if service:
    +119            scalpel_response.scheme = "https" if service.secure() else "http"
    +120            scalpel_response.host = service.host()
    +121            scalpel_response.port = service.port()
    +122
    +123        return scalpel_response
    +
    + + +

    Construct an instance of the Response class from a Burp suite IHttpResponse.

    +
    + + +
    +
    + +
    + + def + to_burp(self) -> pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    149    def to_burp(self) -> IHttpResponse:  # pragma: no cover (uses Java API)
    +150        """Convert the response to a Burp suite :class:`IHttpResponse`."""
    +151        response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self))
    +152
    +153        return HttpResponse.httpResponse(response_byte_array)
    +
    + + +

    Convert the response to a Burp suite IHttpResponse.

    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_raw(cls, data: bytes | str) -> python3-10.pyscalpel.http.response.Response: + + + +
    + +
    155    @classmethod
    +156    def from_raw(
    +157        cls, data: bytes | str
    +158    ) -> Response:  # pragma: no cover (uses Java API)
    +159        """Construct an instance of the Response class from raw bytes.
    +160        :param data: The raw bytes to convert.
    +161        :return: A :class:`Response` parsed from the raw bytes.
    +162        """
    +163        # Use the Burp API to trivialize the parsing of the response from raw bytes.
    +164        # Convert the raw bytes to a Burp ByteArray.
    +165        # Plain strings are OK too.
    +166        str_or_byte_array: IByteArray | str = (
    +167            data if isinstance(data, str) else PythonUtils.toByteArray(data)
    +168        )
    +169
    +170        # Instantiate a new Burp HTTP response.
    +171        burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array)
    +172
    +173        return cls.from_burp(burp_response)
    +
    + + +

    Construct an instance of the Response class from raw bytes.

    + +
    Parameters
    + +
      +
    • data: The raw bytes to convert.
    • +
    + +
    Returns
    + +
    +

    A Response parsed from the raw bytes.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + make( cls, status_code: int = 200, content: bytes | str = b'', headers: Headers | tuple[tuple[bytes, bytes], ...] = (), host: str = '', port: int = 0, scheme: Literal['http', 'https'] = 'http') -> python3-10.pyscalpel.http.response.Response: + + + +
    + +
    175    @classmethod
    +176    def make(
    +177        cls,
    +178        status_code: int = 200,
    +179        content: bytes | str = b"",
    +180        headers: Headers | tuple[tuple[bytes, bytes], ...] = (),
    +181        host: str = "",
    +182        port: int = 0,
    +183        scheme: Literal["http", "https"] = "http",
    +184    ) -> "Response":
    +185        # Use the base/inherited make method to construct a MITMProxy response.
    +186        mitmproxy_res = MITMProxyResponse.make(status_code, content, headers)
    +187
    +188        res = cls.from_mitmproxy(mitmproxy_res)
    +189        res.host = host
    +190        res.scheme = scheme
    +191        res.port = port
    +192
    +193        return res
    +
    + + +

    Simplified API for creating response objects.

    +
    + + +
    +
    + +
    + + def + host_is(self, *patterns: str) -> bool: + + + +
    + +
    195    def host_is(self, *patterns: str) -> bool:
    +196        """Matches the host against the provided patterns
    +197
    +198        Returns:
    +199            bool: Whether at least one pattern matched
    +200        """
    +201        return host_is(self.host, *patterns)
    +
    + + +

    Matches the host against the provided patterns

    + +

    Returns: + bool: Whether at least one pattern matched

    +
    + + +
    +
    + +
    + body: bytes | None + + + +
    + +
    203    @property
    +204    def body(self) -> bytes | None:
    +205        """Alias for content()
    +206
    +207        Returns:
    +208            bytes | None: The request body / content
    +209        """
    +210        return self.content
    +
    + + +

    Alias for content()

    + +

    Returns: + bytes | None: The request body / content

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    _internal_mitmproxy.http.Response
    +
    data
    +
    status_code
    +
    reason
    +
    cookies
    +
    refresh
    + +
    +
    _internal_mitmproxy.http.Message
    +
    from_state
    +
    get_state
    +
    set_state
    +
    stream
    +
    http_version
    +
    is_http10
    +
    is_http11
    +
    is_http2
    +
    headers
    +
    trailers
    +
    raw_content
    +
    content
    +
    text
    +
    set_content
    +
    get_content
    +
    set_text
    +
    get_text
    +
    timestamp_start
    +
    timestamp_end
    +
    decode
    +
    encode
    +
    json
    + +
    +
    _internal_mitmproxy.coretypes.serializable.Serializable
    +
    copy
    + +
    +
    +
    +
    +
    + +
    + + class + Headers(_internal_mitmproxy.coretypes.multidict._MultiDict[~KT, ~VT], _internal_mitmproxy.coretypes.serializable.Serializable): + + + +
    + +
    16class Headers(MITMProxyHeaders):
    +17    """A wrapper around the MITMProxy Headers.
    +18
    +19    This class provides additional methods for converting headers between Burp suite and MITMProxy formats.
    +20    """
    +21
    +22    def __init__(self, fields: Iterable[tuple[bytes, bytes]] | None = None, **headers):
    +23        """
    +24        :param fields: The headers to construct the from.
    +25        :param headers: The headers to construct the from.
    +26        """
    +27        # Cannot safely use [] as default param because the reference will be shared with all __init__ calls
    +28        fields = fields or []
    +29
    +30        # Construct the base/inherited MITMProxy headers.
    +31        super().__init__(fields, **headers)
    +32
    +33    @classmethod
    +34    def from_mitmproxy(cls, headers: MITMProxyHeaders) -> Headers:
    +35        """
    +36        Creates a `Headers` from a `mitmproxy.http.Headers`.
    +37
    +38        :param headers: The `mitmproxy.http.Headers` to convert.
    +39        :type headers: :class Headers <https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Headers>`
    +40        :return: A `Headers` with the same headers as the `mitmproxy.http.Headers`.
    +41        """
    +42
    +43        # Construct from the raw MITMProxy headers data.
    +44        return cls(headers.fields)
    +45
    +46    @classmethod
    +47    def from_burp(cls, headers: list[IHttpHeader]) -> Headers:  # pragma: no cover
    +48        """Construct an instance of the Headers class from a Burp suite HttpHeader array.
    +49        :param headers: The Burp suite HttpHeader array to convert.
    +50        :return: A Headers with the same headers as the Burp suite HttpHeader array.
    +51        """
    +52
    +53        # print(f"burp: {headers}")
    +54        # Convert the list of Burp IHttpHeaders to a list of tuples: (key, value)
    +55        return cls(
    +56            (
    +57                (
    +58                    always_bytes(header.name()),
    +59                    always_bytes(header.value()),
    +60                )
    +61                for header in headers
    +62            )
    +63        )
    +64
    +65    def to_burp(self) -> list[IHttpHeader]:  # pragma: no cover
    +66        """Convert the headers to a Burp suite HttpHeader array.
    +67        :return: A Burp suite HttpHeader array.
    +68        """
    +69
    +70        # Convert the list of tuples: (key, value) to a list of Burp IHttpHeaders
    +71        return [
    +72            HttpHeader.httpHeader(always_str(header[0]), always_str(header[1]))
    +73            for header in self.fields
    +74        ]
    +
    + + +

    A wrapper around the MITMProxy Headers.

    + +

    This class provides additional methods for converting headers between Burp suite and MITMProxy formats.

    +
    + + +
    + +
    + + Headers(fields: Optional[Iterable[tuple[bytes, bytes]]] = None, **headers) + + + +
    + +
    22    def __init__(self, fields: Iterable[tuple[bytes, bytes]] | None = None, **headers):
    +23        """
    +24        :param fields: The headers to construct the from.
    +25        :param headers: The headers to construct the from.
    +26        """
    +27        # Cannot safely use [] as default param because the reference will be shared with all __init__ calls
    +28        fields = fields or []
    +29
    +30        # Construct the base/inherited MITMProxy headers.
    +31        super().__init__(fields, **headers)
    +
    + + +
    Parameters
    + +
      +
    • fields: The headers to construct the from.
    • +
    • headers: The headers to construct the from.
    • +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_mitmproxy( cls, headers: _internal_mitmproxy.http.Headers) -> Headers: + + + +
    + +
    33    @classmethod
    +34    def from_mitmproxy(cls, headers: MITMProxyHeaders) -> Headers:
    +35        """
    +36        Creates a `Headers` from a `mitmproxy.http.Headers`.
    +37
    +38        :param headers: The `mitmproxy.http.Headers` to convert.
    +39        :type headers: :class Headers <https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Headers>`
    +40        :return: A `Headers` with the same headers as the `mitmproxy.http.Headers`.
    +41        """
    +42
    +43        # Construct from the raw MITMProxy headers data.
    +44        return cls(headers.fields)
    +
    + + +

    Creates a Headers from a mitmproxy.http.Headers.

    + +
    Parameters
    + +
      +
    • headers: The mitmproxy.http.Headers to convert.
    • +
    + +
    Returns
    + +
    +

    A Headers with the same headers as the mitmproxy.http.Headers.

    +
    +
    + + +
    +
    + +
    +
    @classmethod
    + + def + from_burp( cls, headers: list[pyscalpel.java.burp.http_header.IHttpHeader]) -> Headers: + + + +
    + +
    46    @classmethod
    +47    def from_burp(cls, headers: list[IHttpHeader]) -> Headers:  # pragma: no cover
    +48        """Construct an instance of the Headers class from a Burp suite HttpHeader array.
    +49        :param headers: The Burp suite HttpHeader array to convert.
    +50        :return: A Headers with the same headers as the Burp suite HttpHeader array.
    +51        """
    +52
    +53        # print(f"burp: {headers}")
    +54        # Convert the list of Burp IHttpHeaders to a list of tuples: (key, value)
    +55        return cls(
    +56            (
    +57                (
    +58                    always_bytes(header.name()),
    +59                    always_bytes(header.value()),
    +60                )
    +61                for header in headers
    +62            )
    +63        )
    +
    + + +

    Construct an instance of the Headers class from a Burp suite HttpHeader array.

    + +
    Parameters
    + +
      +
    • headers: The Burp suite HttpHeader array to convert.
    • +
    + +
    Returns
    + +
    +

    A Headers with the same headers as the Burp suite HttpHeader array.

    +
    +
    + + +
    +
    + +
    + + def + to_burp(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]: + + + +
    + +
    65    def to_burp(self) -> list[IHttpHeader]:  # pragma: no cover
    +66        """Convert the headers to a Burp suite HttpHeader array.
    +67        :return: A Burp suite HttpHeader array.
    +68        """
    +69
    +70        # Convert the list of tuples: (key, value) to a list of Burp IHttpHeaders
    +71        return [
    +72            HttpHeader.httpHeader(always_str(header[0]), always_str(header[1]))
    +73            for header in self.fields
    +74        ]
    +
    + + +

    Convert the headers to a Burp suite HttpHeader array.

    + +
    Returns
    + +
    +

    A Burp suite HttpHeader array.

    +
    +
    + + +
    +
    +
    Inherited Members
    +
    +
    _internal_mitmproxy.coretypes.multidict._MultiDict
    +
    fields
    +
    get_all
    +
    set_all
    +
    add
    +
    insert
    +
    keys
    +
    values
    +
    items
    + +
    +
    _internal_mitmproxy.coretypes.serializable.Serializable
    +
    from_state
    +
    get_state
    +
    set_state
    +
    copy
    + +
    +
    collections.abc.MutableMapping
    +
    pop
    +
    popitem
    +
    clear
    +
    update
    +
    setdefault
    + +
    +
    collections.abc.Mapping
    +
    get
    + +
    +
    +
    +
    +
    + +
    + + class + Flow: + + + +
    + +
    10class Flow:
    +11    """Contains request and response and some utilities for match()"""
    +12
    +13    def __init__(
    +14        self,
    +15        scheme: Literal["http", "https"] = "http",
    +16        host: str = "",
    +17        port: int = 0,
    +18        request: Request | None = None,
    +19        response: Response | None = None,
    +20        text: bytes | None = None,
    +21    ):
    +22        self.scheme = scheme
    +23        self.host = host
    +24        self.port = port
    +25        self.request = request
    +26        self.response = response
    +27        self.text = text
    +28
    +29    def host_is(self, *patterns: str) -> bool:
    +30        """Matches a wildcard pattern against the target host
    +31
    +32        Returns:
    +33            bool: True if at least one pattern matched
    +34        """
    +35        return host_is(self.host, *patterns)
    +36
    +37    def path_is(self, *patterns: str) -> bool:
    +38        """Matches a wildcard pattern against the request path
    +39
    +40        Includes query string `?` and fragment `#`
    +41
    +42        Returns:
    +43            bool: True if at least one pattern matched
    +44        """
    +45        req = self.request
    +46        if req is None:
    +47            return False
    +48
    +49        return req.path_is(*patterns)
    +
    + + +

    Contains request and response and some utilities for match()

    +
    + + +
    + +
    + + Flow( scheme: Literal['http', 'https'] = 'http', host: str = '', port: int = 0, request: pyscalpel.http.request.Request | None = None, response: pyscalpel.http.response.Response | None = None, text: bytes | None = None) + + + +
    + +
    13    def __init__(
    +14        self,
    +15        scheme: Literal["http", "https"] = "http",
    +16        host: str = "",
    +17        port: int = 0,
    +18        request: Request | None = None,
    +19        response: Response | None = None,
    +20        text: bytes | None = None,
    +21    ):
    +22        self.scheme = scheme
    +23        self.host = host
    +24        self.port = port
    +25        self.request = request
    +26        self.response = response
    +27        self.text = text
    +
    + + + + +
    +
    +
    + scheme + + +
    + + + + +
    +
    +
    + host + + +
    + + + + +
    +
    +
    + port + + +
    + + + + +
    +
    +
    + request + + +
    + + + + +
    +
    +
    + response + + +
    + + + + +
    +
    +
    + text + + +
    + + + + +
    +
    + +
    + + def + host_is(self, *patterns: str) -> bool: + + + +
    + +
    29    def host_is(self, *patterns: str) -> bool:
    +30        """Matches a wildcard pattern against the target host
    +31
    +32        Returns:
    +33            bool: True if at least one pattern matched
    +34        """
    +35        return host_is(self.host, *patterns)
    +
    + + +

    Matches a wildcard pattern against the target host

    + +

    Returns: + bool: True if at least one pattern matched

    +
    + + +
    +
    + +
    + + def + path_is(self, *patterns: str) -> bool: + + + +
    + +
    37    def path_is(self, *patterns: str) -> bool:
    +38        """Matches a wildcard pattern against the request path
    +39
    +40        Includes query string `?` and fragment `#`
    +41
    +42        Returns:
    +43            bool: True if at least one pattern matched
    +44        """
    +45        req = self.request
    +46        if req is None:
    +47            return False
    +48
    +49        return req.path_is(*patterns)
    +
    + + +

    Matches a wildcard pattern against the request path

    + +

    Includes query string ? and fragment #

    + +

    Returns: + bool: True if at least one pattern matched

    +
    + + +
    +
    +
    + +
    + + def + host_is(host: str, *patterns: str) -> bool: + + + +
    + +
    21def host_is(host: str, *patterns: str) -> bool:
    +22    """Matches a host using unix-like wildcard matching against multiple patterns
    +23
    +24    Args:
    +25        host (str): The host to match against
    +26        patterns (str): The patterns to use
    +27
    +28    Returns:
    +29        bool: The match result (True if at least one pattern matches, else False)
    +30    """
    +31    return match_patterns(host, *patterns)
    +
    + + +

    Matches a host using unix-like wildcard matching against multiple patterns

    + +

    Args: + host (str): The host to match against + patterns (str): The patterns to use

    + +

    Returns: + bool: The match result (True if at least one pattern matches, else False)

    +
    + + +
    +
    + +
    + + def + match_patterns(to_match: str, *patterns: str) -> bool: + + + +
    + +
     5def match_patterns(to_match: str, *patterns: str) -> bool:
    + 6    """Matches a string using unix-like wildcard matching against multiple patterns
    + 7
    + 8    Args:
    + 9        to_match (str): The string to match against
    +10        patterns (str): The patterns to use
    +11
    +12    Returns:
    +13        bool: The match result (True if at least one pattern matches, else False)
    +14    """
    +15    for pattern in patterns:
    +16        if fnmatch(to_match, pattern):
    +17            return True
    +18    return False
    +
    + + +

    Matches a string using unix-like wildcard matching against multiple patterns

    + +

    Args: + to_match (str): The string to match against + patterns (str): The patterns to use

    + +

    Returns: + bool: The match result (True if at least one pattern matches, else False)

    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/http/body.html b/docs/public/pdoc/python3-10/pyscalpel/http/body.html new file mode 100644 index 00000000..dbaab22c --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/http/body.html @@ -0,0 +1,2247 @@ + + + + + + + python3-10.pyscalpel.http.body API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.http.body

    + +

    Pentesters often have to manipulate form data in precise and extensive ways

    + +

    This module contains implementations for the most common forms (multipart,urlencoded, JSON)

    + +

    Users may be implement their own form by creating a Serializer, +assigning the .serializer attribute in Request and using the "form" property

    + +

    Forms are designed to be convertible from one to another.

    + +

    For example, JSON forms may be converted to URL encoded forms +by using the php query string syntax:

    + +

    {"key1": {"key2" : {"key3" : "nested_value"}}} -> key1[key2][key3]=nested_value

    + +

    And vice-versa.

    +
    + + + + + +
     1"""
    + 2    Pentesters often have to manipulate form data in precise and extensive ways
    + 3
    + 4    This module contains implementations for the most common forms (multipart,urlencoded, JSON)
    + 5    
    + 6    Users may be implement their own form by creating a Serializer,
    + 7    assigning the .serializer attribute in `Request` and using the "form" property
    + 8    
    + 9    Forms are designed to be convertible from one to another.
    +10    
    +11    For example, JSON forms may be converted to URL encoded forms
    +12    by using the php query string syntax:
    +13    
    +14    ```{"key1": {"key2" : {"key3" : "nested_value"}}} -> key1[key2][key3]=nested_value```
    +15    
    +16    And vice-versa.
    +17"""
    +18
    +19from .form import *
    +20
    +21
    +22__all__ = [
    +23    "Form",
    +24    "JSON_VALUE_TYPES",
    +25    "JSONForm",
    +26    "MultiPartForm",
    +27    "MultiPartFormField",
    +28    "URLEncodedForm",
    +29    "FormSerializer",
    +30    "json_unescape",
    +31    "json_unescape_bytes",
    +32    "json_escape_bytes",
    +33]
    +
    + + +
    +
    + +
    + + class + Form(collections.abc.MutableMapping[~KT, ~VT]): + + + +
    + +
    33class Form(MutableMapping[KT, VT], metaclass=ABCMeta):
    +34    pass
    +
    + + +

    A MutableMapping is a generic container for associating +key/value pairs.

    + +

    This class provides concrete generic implementations of all +methods except for __getitem__, __setitem__, __delitem__, +__iter__, and __len__.

    +
    + + +
    +
    Inherited Members
    +
    +
    collections.abc.MutableMapping
    +
    pop
    +
    popitem
    +
    clear
    +
    update
    +
    setdefault
    + +
    +
    collections.abc.Mapping
    +
    get
    +
    keys
    +
    items
    +
    values
    + +
    +
    +
    +
    +
    +
    + JSON_VALUE_TYPES = + + str | int | float | bool | None | list['JSON_VALUE_TYPES'] | dict[str | int | float, 'JSON_VALUE_TYPES'] + + +
    + + + + +
    +
    + +
    + + class + JSONForm(dict[str | int | float, str | int | float | bool | None | list['JSON_VALUE_TYPES'] | dict[str | int | float, 'JSON_VALUE_TYPES']]): + + + +
    + +
    37class JSONForm(dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]):
    +38    """Form representing a JSON object {}
    +39
    +40    Implemented by a plain dict
    +41
    +42    Args:
    +43        dict (_type_): A dict containing JSON-compatible types.
    +44    """
    +45
    +46    pass
    +
    + + +

    Form representing a JSON object {}

    + +

    Implemented by a plain dict

    + +

    Args: + dict (_type_): A dict containing JSON-compatible types.

    +
    + + +
    +
    Inherited Members
    +
    +
    builtins.dict
    +
    get
    +
    setdefault
    +
    pop
    +
    popitem
    +
    keys
    +
    items
    +
    values
    +
    update
    +
    fromkeys
    +
    clear
    +
    copy
    + +
    +
    +
    +
    +
    + +
    + + class + MultiPartForm(collections.abc.Mapping[str, pyscalpel.http.body.multipart.MultiPartFormField]): + + + +
    + +
    341class MultiPartForm(Mapping[str, MultiPartFormField]):
    +342    """
    +343    This class represents a multipart/form-data request.
    +344
    +345    It contains a collection of MultiPartFormField objects, providing methods
    +346    to add, get, and delete form fields.
    +347
    +348    The class also enables the conversion of the entire form
    +349    into bytes for transmission.
    +350
    +351    - Args:
    +352        - fields (Sequence[MultiPartFormField]): A sequence of MultiPartFormField objects that make up the form.
    +353        - content_type (str): The content type of the form.
    +354        - encoding (str): The encoding of the form.
    +355
    +356    - Raises:
    +357        - TypeError: Raised when an incorrect type is passed to MultiPartForm.set.
    +358        - KeyError: Raised when trying to access a field that does not exist in the form.
    +359
    +360    - Returns:
    +361        - MultiPartForm: An instance of the class representing a multipart/form-data request.
    +362
    +363    - Yields:
    +364        - Iterator[MultiPartFormField]: Yields each field in the form.
    +365    """
    +366
    +367    fields: list[MultiPartFormField]
    +368    content_type: str
    +369    encoding: str
    +370
    +371    def __init__(
    +372        self,
    +373        fields: Sequence[MultiPartFormField],
    +374        content_type: str,
    +375        encoding: str = "utf-8",
    +376    ):
    +377        self.content_type = content_type
    +378        self.encoding = encoding
    +379        super().__init__()
    +380        self.fields = list(fields)
    +381
    +382    @classmethod
    +383    def from_bytes(
    +384        cls, content: bytes, content_type: str, encoding: str = "utf-8"
    +385    ) -> MultiPartForm:
    +386        """Create a MultiPartForm by parsing a raw multipart form
    +387
    +388        - Args:
    +389            - content (bytes): The multipart form as raw bytes
    +390            - content_type (str): The Content-Type header with the corresponding boundary param (required).
    +391            - encoding (str, optional): The encoding to use (not required). Defaults to "utf-8".
    +392
    +393        - Returns:
    +394           - MultiPartForm: The parsed multipart form
    +395        """
    +396        decoder = MultipartDecoder(content, content_type, encoding=encoding)
    +397        parts: tuple[BodyPart] = decoder.parts
    +398        fields: tuple[MultiPartFormField, ...] = tuple(
    +399            MultiPartFormField.from_body_part(body_part) for body_part in parts
    +400        )
    +401        return cls(fields, content_type, encoding)
    +402
    +403    @property
    +404    def boundary(self) -> bytes:
    +405        """Get the form multipart boundary
    +406
    +407        Returns:
    +408            bytes: The multipart boundary
    +409        """
    +410        return extract_boundary(self.content_type, self.encoding)
    +411
    +412    def __bytes__(self) -> bytes:
    +413        boundary = self.boundary
    +414        serialized = b""
    +415        encoding = self.encoding
    +416        for field in self.fields:
    +417            serialized += b"--" + boundary + b"\r\n"
    +418
    +419            # Format the headers
    +420            for key, val in field.headers.items():
    +421                serialized += (
    +422                    key.encode(encoding) + b": " + val.encode(encoding) + b"\r\n"
    +423                )
    +424            serialized += b"\r\n" + field.content + b"\r\n"
    +425
    +426        # Format the final boundary
    +427        serialized += b"--" + boundary + b"--\r\n\r\n"
    +428        return serialized
    +429
    +430    # Override
    +431    def get_all(self, key: str) -> list[MultiPartFormField]:
    +432        """
    +433        Return the list of all values for a given key.
    +434        If that key is not in the MultiDict, the return value will be an empty list.
    +435        """
    +436        return [field for field in self.fields if key == field.name]
    +437
    +438    def get(
    +439        self, key: str, default: MultiPartFormField | None = None
    +440    ) -> MultiPartFormField | None:
    +441        values = self.get_all(key)
    +442        if not values:
    +443            return default
    +444
    +445        return values[0]
    +446
    +447    def del_all(self, key: str):
    +448        # Mutate object to avoid invalidating user references to fields
    +449        for field in self.fields:
    +450            if key == field.name:
    +451                self.fields.remove(field)
    +452
    +453    def __delitem__(self, key: str):
    +454        self.del_all(key)
    +455
    +456    def set(
    +457        self,
    +458        key: str,
    +459        value: (
    +460            TextIOWrapper
    +461            | BufferedReader
    +462            | IOBase
    +463            | MultiPartFormField
    +464            | bytes
    +465            | str
    +466            | int
    +467            | float
    +468            | None
    +469        ),
    +470    ) -> None:
    +471        new_field: MultiPartFormField
    +472        match value:
    +473            case MultiPartFormField():
    +474                new_field = value
    +475            case int() | float():
    +476                return self.set(key, str(value))
    +477            case bytes() | str():
    +478                new_field = MultiPartFormField.make(key)
    +479                new_field.content = always_bytes(value)
    +480            case IOBase():
    +481                new_field = MultiPartFormField.from_file(key, value)
    +482            case None:
    +483                self.del_all(key)
    +484                return
    +485            case _:
    +486                raise TypeError("Wrong type was passed to MultiPartForm.set")
    +487
    +488        for i, field in enumerate(self.fields):
    +489            if field.name == key:
    +490                self.fields[i] = new_field
    +491                return
    +492
    +493        self.append(new_field)
    +494
    +495    def setdefault(
    +496        self, key: str, default: MultiPartFormField | None = None
    +497    ) -> MultiPartFormField:
    +498        found = self.get(key)
    +499        if found is None:
    +500            default = default or MultiPartFormField.make(key)
    +501            self[key] = default
    +502            return default
    +503
    +504        return found
    +505
    +506    def __setitem__(
    +507        self,
    +508        key: str,
    +509        value: (
    +510            TextIOWrapper
    +511            | BufferedReader
    +512            | MultiPartFormField
    +513            | IOBase
    +514            | bytes
    +515            | str
    +516            | int
    +517            | float
    +518            | None
    +519        ),
    +520    ) -> None:
    +521        self.set(key, value)
    +522
    +523    def __getitem__(self, key: str) -> MultiPartFormField:
    +524        values = self.get_all(key)
    +525        if not values:
    +526            raise KeyError(key)
    +527        return values[0]
    +528
    +529    def __len__(self) -> int:
    +530        return len(self.fields)
    +531
    +532    def __eq__(self, other) -> bool:
    +533        if isinstance(other, MultiPartForm):
    +534            return self.fields == other.fields
    +535        return False
    +536
    +537    def __iter__(self) -> Iterator[MultiPartFormField]:
    +538        seen = set()
    +539        for field in self.fields:
    +540            if field not in seen:
    +541                seen.add(field)
    +542                yield field
    +543
    +544    def insert(self, index: int, value: MultiPartFormField) -> None:
    +545        """
    +546        Insert an additional value for the given key at the specified position.
    +547        """
    +548        self.fields.insert(index, value)
    +549
    +550    def append(self, value: MultiPartFormField) -> None:
    +551        self.fields.append(value)
    +552
    +553    def __repr__(self):  # pragma: no cover
    +554        fields = (repr(field) for field in self.fields)
    +555        return f"{type(self).__name__}[{', '.join(fields)}]"
    +556
    +557    def items(self) -> tuple[tuple[str, MultiPartFormField], ...]:
    +558        fields = self.fields
    +559        items = ((i.name, i) for i in fields)
    +560        return tuple(items)
    +561
    +562    def keys(self) -> tuple[str, ...]:
    +563        return tuple(field.name for field in self.fields)
    +564
    +565    def values(self) -> tuple[MultiPartFormField, ...]:
    +566        return tuple(self.fields)
    +
    + + +

    This class represents a multipart/form-data request.

    + +

    It contains a collection of MultiPartFormField objects, providing methods +to add, get, and delete form fields.

    + +

    The class also enables the conversion of the entire form +into bytes for transmission.

    + +
      +
    • Args:

      + +
        +
      • fields (Sequence[MultiPartFormField]): A sequence of MultiPartFormField objects that make up the form.
      • +
      • content_type (str): The content type of the form.
      • +
      • encoding (str): The encoding of the form.
      • +
    • +
    • Raises:

      + +
        +
      • TypeError: Raised when an incorrect type is passed to MultiPartForm.set.
      • +
      • KeyError: Raised when trying to access a field that does not exist in the form.
      • +
    • +
    • Returns:

      + +
        +
      • MultiPartForm: An instance of the class representing a multipart/form-data request.
      • +
    • +
    • Yields:

      + +
        +
      • Iterator[MultiPartFormField]: Yields each field in the form.
      • +
    • +
    +
    + + +
    + +
    + + MultiPartForm( fields: Sequence[MultiPartFormField], content_type: str, encoding: str = 'utf-8') + + + +
    + +
    371    def __init__(
    +372        self,
    +373        fields: Sequence[MultiPartFormField],
    +374        content_type: str,
    +375        encoding: str = "utf-8",
    +376    ):
    +377        self.content_type = content_type
    +378        self.encoding = encoding
    +379        super().__init__()
    +380        self.fields = list(fields)
    +
    + + + + +
    +
    +
    + fields: list[MultiPartFormField] + + +
    + + + + +
    +
    +
    + content_type: str + + +
    + + + + +
    +
    +
    + encoding: str + + +
    + + + + +
    +
    + +
    +
    @classmethod
    + + def + from_bytes( cls, content: bytes, content_type: str, encoding: str = 'utf-8') -> MultiPartForm: + + + +
    + +
    382    @classmethod
    +383    def from_bytes(
    +384        cls, content: bytes, content_type: str, encoding: str = "utf-8"
    +385    ) -> MultiPartForm:
    +386        """Create a MultiPartForm by parsing a raw multipart form
    +387
    +388        - Args:
    +389            - content (bytes): The multipart form as raw bytes
    +390            - content_type (str): The Content-Type header with the corresponding boundary param (required).
    +391            - encoding (str, optional): The encoding to use (not required). Defaults to "utf-8".
    +392
    +393        - Returns:
    +394           - MultiPartForm: The parsed multipart form
    +395        """
    +396        decoder = MultipartDecoder(content, content_type, encoding=encoding)
    +397        parts: tuple[BodyPart] = decoder.parts
    +398        fields: tuple[MultiPartFormField, ...] = tuple(
    +399            MultiPartFormField.from_body_part(body_part) for body_part in parts
    +400        )
    +401        return cls(fields, content_type, encoding)
    +
    + + +

    Create a MultiPartForm by parsing a raw multipart form

    + +
      +
    • Args:

      + +
        +
      • content (bytes): The multipart form as raw bytes
      • +
      • content_type (str): The Content-Type header with the corresponding boundary param (required).
      • +
      • encoding (str, optional): The encoding to use (not required). Defaults to "utf-8".
      • +
    • +
    • Returns:

      + +
        +
      • MultiPartForm: The parsed multipart form
      • +
    • +
    +
    + + +
    +
    + +
    + boundary: bytes + + + +
    + +
    403    @property
    +404    def boundary(self) -> bytes:
    +405        """Get the form multipart boundary
    +406
    +407        Returns:
    +408            bytes: The multipart boundary
    +409        """
    +410        return extract_boundary(self.content_type, self.encoding)
    +
    + + +

    Get the form multipart boundary

    + +

    Returns: + bytes: The multipart boundary

    +
    + + +
    +
    + +
    + + def + get_all(self, key: str) -> list[MultiPartFormField]: + + + +
    + +
    431    def get_all(self, key: str) -> list[MultiPartFormField]:
    +432        """
    +433        Return the list of all values for a given key.
    +434        If that key is not in the MultiDict, the return value will be an empty list.
    +435        """
    +436        return [field for field in self.fields if key == field.name]
    +
    + + +

    Return the list of all values for a given key. +If that key is not in the MultiDict, the return value will be an empty list.

    +
    + + +
    +
    + +
    + + def + del_all(self, key: str): + + + +
    + +
    447    def del_all(self, key: str):
    +448        # Mutate object to avoid invalidating user references to fields
    +449        for field in self.fields:
    +450            if key == field.name:
    +451                self.fields.remove(field)
    +
    + + + + +
    +
    + +
    + + def + set( self, key: str, value: _io.TextIOWrapper | _io.BufferedReader | io.IOBase | MultiPartFormField | bytes | str | int | float | None) -> None: + + + +
    + +
    456    def set(
    +457        self,
    +458        key: str,
    +459        value: (
    +460            TextIOWrapper
    +461            | BufferedReader
    +462            | IOBase
    +463            | MultiPartFormField
    +464            | bytes
    +465            | str
    +466            | int
    +467            | float
    +468            | None
    +469        ),
    +470    ) -> None:
    +471        new_field: MultiPartFormField
    +472        match value:
    +473            case MultiPartFormField():
    +474                new_field = value
    +475            case int() | float():
    +476                return self.set(key, str(value))
    +477            case bytes() | str():
    +478                new_field = MultiPartFormField.make(key)
    +479                new_field.content = always_bytes(value)
    +480            case IOBase():
    +481                new_field = MultiPartFormField.from_file(key, value)
    +482            case None:
    +483                self.del_all(key)
    +484                return
    +485            case _:
    +486                raise TypeError("Wrong type was passed to MultiPartForm.set")
    +487
    +488        for i, field in enumerate(self.fields):
    +489            if field.name == key:
    +490                self.fields[i] = new_field
    +491                return
    +492
    +493        self.append(new_field)
    +
    + + + + +
    +
    + +
    + + def + setdefault( self, key: str, default: MultiPartFormField | None = None) -> MultiPartFormField: + + + +
    + +
    495    def setdefault(
    +496        self, key: str, default: MultiPartFormField | None = None
    +497    ) -> MultiPartFormField:
    +498        found = self.get(key)
    +499        if found is None:
    +500            default = default or MultiPartFormField.make(key)
    +501            self[key] = default
    +502            return default
    +503
    +504        return found
    +
    + + + + +
    +
    + +
    + + def + insert( self, index: int, value: MultiPartFormField) -> None: + + + +
    + +
    544    def insert(self, index: int, value: MultiPartFormField) -> None:
    +545        """
    +546        Insert an additional value for the given key at the specified position.
    +547        """
    +548        self.fields.insert(index, value)
    +
    + + +

    Insert an additional value for the given key at the specified position.

    +
    + + +
    +
    + +
    + + def + append(self, value: MultiPartFormField) -> None: + + + +
    + +
    550    def append(self, value: MultiPartFormField) -> None:
    +551        self.fields.append(value)
    +
    + + + + +
    +
    +
    Inherited Members
    +
    +
    collections.abc.Mapping
    +
    get
    +
    items
    +
    keys
    +
    values
    + +
    +
    +
    +
    +
    + +
    + + class + MultiPartFormField: + + + +
    + +
     86class MultiPartFormField:
    + 87    """
    + 88    This class represents a field in a multipart/form-data request.
    + 89
    + 90    It provides functionalities to create form fields from various inputs like raw body parts,
    + 91    files and manual construction with name, filename, body, and content type.
    + 92
    + 93    It also offers properties and methods to interact with the form field's headers and content.
    + 94
    + 95    Raises:
    + 96        StopIteration: Raised when the specified Content-Disposition header is not found or could not be parsed.
    + 97
    + 98    Returns:
    + 99        MultiPartFormField: An instance of the class representing a form field in a multipart/form-data request.
    +100    """
    +101
    +102    headers: CaseInsensitiveDict[str]
    +103    content: bytes
    +104    encoding: str
    +105
    +106    def __init__(
    +107        self,
    +108        headers: CaseInsensitiveDict[str],
    +109        content: bytes = b"",
    +110        encoding: str = "utf-8",
    +111    ):
    +112        self.headers = headers
    +113        self.content = content
    +114        self.encoding = encoding
    +115
    +116    @classmethod
    +117    def from_body_part(cls, body_part: BodyPart):
    +118        headers = cls._fix_headers(cast(Mapping[bytes, bytes], body_part.headers))
    +119        return cls(headers, body_part.content, body_part.encoding)
    +120
    +121    @classmethod
    +122    def make(
    +123        cls,
    +124        name: str,
    +125        filename: str | None = None,
    +126        body: bytes = b"",
    +127        content_type: str | None = None,
    +128        encoding: str = "utf-8",
    +129    ) -> MultiPartFormField:
    +130        # Ensure the form won't break if someone includes quotes
    +131        escaped_name: str = escape_parameter(name)
    +132
    +133        # rfc7578  4.2. specifies that urlencoding shouldn't be applied to filename
    +134        #   But most browsers still replaces the " character by %22 to avoid breaking the parameters quotation.
    +135        escaped_filename: str | None = filename and escape_parameter(filename)
    +136
    +137        if content_type is None:
    +138            content_type = get_mime(filename)
    +139
    +140        urlencoded_content_type = urllibquote(content_type)
    +141
    +142        disposition = f'form-data; name="{escaped_name}"'
    +143        if filename is not None:
    +144            # When the param is a file, add a filename MIME param and a content-type header
    +145            disposition += f'; filename="{escaped_filename}"'
    +146            headers = CaseInsensitiveDict(
    +147                {
    +148                    CONTENT_DISPOSITION_KEY: disposition,
    +149                    CONTENT_TYPE_KEY: urlencoded_content_type,
    +150                }
    +151            )
    +152        else:
    +153            headers = CaseInsensitiveDict(
    +154                {
    +155                    CONTENT_DISPOSITION_KEY: disposition,
    +156                }
    +157            )
    +158
    +159        return cls(headers, body, encoding)
    +160
    +161    # TODO: Rewrite request_toolbelt multipart parser to get rid of encoding.
    +162    @staticmethod
    +163    def from_file(
    +164        name: str,
    +165        file: TextIOWrapper | BufferedReader | str | IOBase,
    +166        filename: str | None = None,
    +167        content_type: str | None = None,
    +168        encoding: str | None = None,
    +169    ):
    +170        if isinstance(file, str):
    +171            file = open(file, mode="rb")
    +172
    +173        if filename is None:
    +174            match file:
    +175                case TextIOWrapper() | BufferedReader():
    +176                    filename = os.path.basename(file.name)
    +177                case _:
    +178                    filename = name
    +179
    +180        # Guess the MIME content-type from the file extension
    +181        if content_type is None:
    +182            content_type = (
    +183                mimetypes.guess_type(filename)[0] or "application/octet-stream"
    +184            )
    +185
    +186        # Read the whole file into memory
    +187        content: bytes
    +188        match file:
    +189            case TextIOWrapper():
    +190                content = file.read().encode(file.encoding)
    +191                # Override file.encoding if provided.
    +192                encoding = encoding or file.encoding
    +193            case BufferedReader() | IOBase():
    +194                content = file.read()
    +195
    +196        instance = MultiPartFormField.make(
    +197            name,
    +198            filename=filename,
    +199            body=content,
    +200            content_type=content_type,
    +201            encoding=encoding or "utf-8",
    +202        )
    +203
    +204        file.close()
    +205
    +206        return instance
    +207
    +208    @staticmethod
    +209    def __serialize_content(
    +210        content: bytes, headers: Mapping[str | bytes, str | bytes]
    +211    ) -> bytes:
    +212        # Prepend content with headers
    +213        merged_content: bytes = b""
    +214        header_lines = (
    +215            always_bytes(key) + b": " + always_bytes(value)
    +216            for key, value in headers.items()
    +217        )
    +218        merged_content += b"\r\n".join(header_lines)
    +219        merged_content += b"\r\n\r\n"
    +220        merged_content += content
    +221        return merged_content
    +222
    +223    def __bytes__(self) -> bytes:
    +224        return self.__serialize_content(
    +225            self.content,
    +226            cast(Mapping[bytes | str, bytes | str], self.headers),
    +227        )
    +228    
    +229    def __eq__(self, other) -> bool:
    +230        match other:
    +231            case MultiPartFormField() | bytes():
    +232                return bytes(other) == bytes(self)
    +233            case str():
    +234                return other.encode("latin-1") == bytes(self)
    +235        return False
    +236
    +237    def __hash__(self) -> int:
    +238        return hash(bytes(self))
    +239
    +240    @staticmethod
    +241    def _fix_headers(headers: Mapping[bytes, bytes]) -> CaseInsensitiveDict[str]:
    +242        # Fix the headers key by converting them to strings
    +243        # https://github.com/requests/toolbelt/pull/353
    +244
    +245        fixed_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict()
    +246        for key, value in headers.items():
    +247            fixed_headers[always_str(key)] = always_str(value.decode())
    +248        return fixed_headers
    +249
    +250    # Unused for now
    +251    # @staticmethod
    +252    # def _unfix_headers(headers: Mapping[str, str]) -> CaseInsensitiveDict[bytes]:
    +253    #     # Unfix the headers key by converting them to bytes
    +254
    +255    #     unfixed_headers: CaseInsensitiveDict[bytes] = CaseInsensitiveDict()
    +256    #     for key, value in headers.items():
    +257    #         unfixed_headers[always_bytes(key)] = always_bytes(value)  # type: ignore requests_toolbelt uses wrong types but it still works fine.
    +258    #     return unfixed_headers
    +259
    +260    @property
    +261    def text(self) -> str:
    +262        return self.content.decode(self.encoding)
    +263
    +264    @property
    +265    def content_type(self) -> str | None:
    +266        return self.headers.get(CONTENT_TYPE_KEY)
    +267
    +268    @content_type.setter
    +269    def content_type(self, content_type: str | None) -> None:
    +270        headers = self.headers
    +271        if content_type is None:
    +272            del headers[CONTENT_TYPE_KEY]
    +273        else:
    +274            headers[CONTENT_TYPE_KEY] = content_type
    +275
    +276    def _parse_disposition(self) -> list[tuple[str, str]]:
    +277        header_key = CONTENT_DISPOSITION_KEY
    +278        header_value = self.headers[header_key]
    +279        return parse_header(header_key, header_value)
    +280
    +281    def _unparse_disposition(self, parsed_header: list[tuple[str, str]]):
    +282        unparsed = unparse_header_value(parsed_header)
    +283        self.headers[CONTENT_DISPOSITION_KEY] = unparsed
    +284
    +285    def get_disposition_param(self, key: str) -> tuple[str, str | None] | None:
    +286        """Get a param from the Content-Disposition header
    +287
    +288        Args:
    +289            key (str): the param name
    +290
    +291        Raises:
    +292            StopIteration: Raised when the param was not found.
    +293
    +294        Returns:
    +295            tuple[str, str | None] | None: Returns the param as (key, value)
    +296        """
    +297        # Parse the Content-Disposition header
    +298        parsed_disposition = self._parse_disposition()
    +299        return find_header_param(parsed_disposition, key)
    +300
    +301    def set_disposition_param(self, key: str, value: str):
    +302        """Set a Content-Type header parameter
    +303
    +304        Args:
    +305            key (str): The parameter name
    +306            value (str): The parameter value
    +307        """
    +308        parsed = self._parse_disposition()
    +309        updated = update_header_param(parsed, key, value)
    +310        self._unparse_disposition(cast(list[tuple[str, str]], updated))
    +311
    +312    @property
    +313    def name(self) -> str:
    +314        """Get the Content-Disposition header name parameter
    +315
    +316        Returns:
    +317            str: The Content-Disposition header name parameter value
    +318        """
    +319        # Assume name is always present
    +320        return cast(tuple[str, str], self.get_disposition_param("name"))[1]
    +321
    +322    @name.setter
    +323    def name(self, value: str):
    +324        self.set_disposition_param("name", value)
    +325
    +326    @property
    +327    def filename(self) -> str | None:
    +328        """Get the Content-Disposition header filename parameter
    +329
    +330        Returns:
    +331            str | None: The Content-Disposition header filename parameter value
    +332        """
    +333        param = self.get_disposition_param("filename")
    +334        return param and param[1]
    +335
    +336    @filename.setter
    +337    def filename(self, value: str):
    +338        self.set_disposition_param("filename", value)
    +
    + + +

    This class represents a field in a multipart/form-data request.

    + +

    It provides functionalities to create form fields from various inputs like raw body parts, +files and manual construction with name, filename, body, and content type.

    + +

    It also offers properties and methods to interact with the form field's headers and content.

    + +

    Raises: + StopIteration: Raised when the specified Content-Disposition header is not found or could not be parsed.

    + +

    Returns: + MultiPartFormField: An instance of the class representing a form field in a multipart/form-data request.

    +
    + + +
    + +
    + + MultiPartFormField( headers: requests.structures.CaseInsensitiveDict[str], content: bytes = b'', encoding: str = 'utf-8') + + + +
    + +
    106    def __init__(
    +107        self,
    +108        headers: CaseInsensitiveDict[str],
    +109        content: bytes = b"",
    +110        encoding: str = "utf-8",
    +111    ):
    +112        self.headers = headers
    +113        self.content = content
    +114        self.encoding = encoding
    +
    + + + + +
    +
    +
    + headers: requests.structures.CaseInsensitiveDict[str] + + +
    + + + + +
    +
    +
    + content: bytes + + +
    + + + + +
    +
    +
    + encoding: str + + +
    + + + + +
    +
    + +
    +
    @classmethod
    + + def + from_body_part(cls, body_part: requests_toolbelt.multipart.decoder.BodyPart): + + + +
    + +
    116    @classmethod
    +117    def from_body_part(cls, body_part: BodyPart):
    +118        headers = cls._fix_headers(cast(Mapping[bytes, bytes], body_part.headers))
    +119        return cls(headers, body_part.content, body_part.encoding)
    +
    + + + + +
    +
    + +
    +
    @classmethod
    + + def + make( cls, name: str, filename: str | None = None, body: bytes = b'', content_type: str | None = None, encoding: str = 'utf-8') -> MultiPartFormField: + + + +
    + +
    121    @classmethod
    +122    def make(
    +123        cls,
    +124        name: str,
    +125        filename: str | None = None,
    +126        body: bytes = b"",
    +127        content_type: str | None = None,
    +128        encoding: str = "utf-8",
    +129    ) -> MultiPartFormField:
    +130        # Ensure the form won't break if someone includes quotes
    +131        escaped_name: str = escape_parameter(name)
    +132
    +133        # rfc7578  4.2. specifies that urlencoding shouldn't be applied to filename
    +134        #   But most browsers still replaces the " character by %22 to avoid breaking the parameters quotation.
    +135        escaped_filename: str | None = filename and escape_parameter(filename)
    +136
    +137        if content_type is None:
    +138            content_type = get_mime(filename)
    +139
    +140        urlencoded_content_type = urllibquote(content_type)
    +141
    +142        disposition = f'form-data; name="{escaped_name}"'
    +143        if filename is not None:
    +144            # When the param is a file, add a filename MIME param and a content-type header
    +145            disposition += f'; filename="{escaped_filename}"'
    +146            headers = CaseInsensitiveDict(
    +147                {
    +148                    CONTENT_DISPOSITION_KEY: disposition,
    +149                    CONTENT_TYPE_KEY: urlencoded_content_type,
    +150                }
    +151            )
    +152        else:
    +153            headers = CaseInsensitiveDict(
    +154                {
    +155                    CONTENT_DISPOSITION_KEY: disposition,
    +156                }
    +157            )
    +158
    +159        return cls(headers, body, encoding)
    +
    + + + + +
    +
    + +
    +
    @staticmethod
    + + def + from_file( name: str, file: _io.TextIOWrapper | _io.BufferedReader | str | io.IOBase, filename: str | None = None, content_type: str | None = None, encoding: str | None = None): + + + +
    + +
    162    @staticmethod
    +163    def from_file(
    +164        name: str,
    +165        file: TextIOWrapper | BufferedReader | str | IOBase,
    +166        filename: str | None = None,
    +167        content_type: str | None = None,
    +168        encoding: str | None = None,
    +169    ):
    +170        if isinstance(file, str):
    +171            file = open(file, mode="rb")
    +172
    +173        if filename is None:
    +174            match file:
    +175                case TextIOWrapper() | BufferedReader():
    +176                    filename = os.path.basename(file.name)
    +177                case _:
    +178                    filename = name
    +179
    +180        # Guess the MIME content-type from the file extension
    +181        if content_type is None:
    +182            content_type = (
    +183                mimetypes.guess_type(filename)[0] or "application/octet-stream"
    +184            )
    +185
    +186        # Read the whole file into memory
    +187        content: bytes
    +188        match file:
    +189            case TextIOWrapper():
    +190                content = file.read().encode(file.encoding)
    +191                # Override file.encoding if provided.
    +192                encoding = encoding or file.encoding
    +193            case BufferedReader() | IOBase():
    +194                content = file.read()
    +195
    +196        instance = MultiPartFormField.make(
    +197            name,
    +198            filename=filename,
    +199            body=content,
    +200            content_type=content_type,
    +201            encoding=encoding or "utf-8",
    +202        )
    +203
    +204        file.close()
    +205
    +206        return instance
    +
    + + + + +
    +
    + +
    + text: str + + + +
    + +
    260    @property
    +261    def text(self) -> str:
    +262        return self.content.decode(self.encoding)
    +
    + + + + +
    +
    + +
    + content_type: str | None + + + +
    + +
    264    @property
    +265    def content_type(self) -> str | None:
    +266        return self.headers.get(CONTENT_TYPE_KEY)
    +
    + + + + +
    +
    + +
    + + def + get_disposition_param(self, key: str) -> tuple[str, str | None] | None: + + + +
    + +
    285    def get_disposition_param(self, key: str) -> tuple[str, str | None] | None:
    +286        """Get a param from the Content-Disposition header
    +287
    +288        Args:
    +289            key (str): the param name
    +290
    +291        Raises:
    +292            StopIteration: Raised when the param was not found.
    +293
    +294        Returns:
    +295            tuple[str, str | None] | None: Returns the param as (key, value)
    +296        """
    +297        # Parse the Content-Disposition header
    +298        parsed_disposition = self._parse_disposition()
    +299        return find_header_param(parsed_disposition, key)
    +
    + + +

    Get a param from the Content-Disposition header

    + +

    Args: + key (str): the param name

    + +

    Raises: + StopIteration: Raised when the param was not found.

    + +

    Returns: + tuple[str, str | None] | None: Returns the param as (key, value)

    +
    + + +
    +
    + +
    + + def + set_disposition_param(self, key: str, value: str): + + + +
    + +
    301    def set_disposition_param(self, key: str, value: str):
    +302        """Set a Content-Type header parameter
    +303
    +304        Args:
    +305            key (str): The parameter name
    +306            value (str): The parameter value
    +307        """
    +308        parsed = self._parse_disposition()
    +309        updated = update_header_param(parsed, key, value)
    +310        self._unparse_disposition(cast(list[tuple[str, str]], updated))
    +
    + + +

    Set a Content-Type header parameter

    + +

    Args: + key (str): The parameter name + value (str): The parameter value

    +
    + + +
    +
    + +
    + name: str + + + +
    + +
    312    @property
    +313    def name(self) -> str:
    +314        """Get the Content-Disposition header name parameter
    +315
    +316        Returns:
    +317            str: The Content-Disposition header name parameter value
    +318        """
    +319        # Assume name is always present
    +320        return cast(tuple[str, str], self.get_disposition_param("name"))[1]
    +
    + + +

    Get the Content-Disposition header name parameter

    + +

    Returns: + str: The Content-Disposition header name parameter value

    +
    + + +
    +
    + +
    + filename: str | None + + + +
    + +
    326    @property
    +327    def filename(self) -> str | None:
    +328        """Get the Content-Disposition header filename parameter
    +329
    +330        Returns:
    +331            str | None: The Content-Disposition header filename parameter value
    +332        """
    +333        param = self.get_disposition_param("filename")
    +334        return param and param[1]
    +
    + + +

    Get the Content-Disposition header filename parameter

    + +

    Returns: + str | None: The Content-Disposition header filename parameter value

    +
    + + +
    +
    +
    + +
    + + class + URLEncodedForm(_internal_mitmproxy.coretypes.multidict.MultiDict[bytes, bytes]): + + + +
    + +
    27class URLEncodedForm(multidict.MultiDict[bytes, bytes]):
    +28    def __init__(self, fields: Iterable[tuple[str | bytes, str | bytes]]) -> None:
    +29        fields_converted_to_bytes: Iterable[tuple[bytes, bytes]] = (
    +30            (
    +31                always_bytes(key),
    +32                always_bytes(val),
    +33            )
    +34            for (key, val) in fields
    +35        )
    +36        super().__init__(fields_converted_to_bytes)
    +37
    +38    def __setitem__(self, key: int | str | bytes, value: int | str | bytes) -> None:
    +39        super().__setitem__(always_bytes(key), always_bytes(value))
    +40
    +41    def __getitem__(self, key: int | bytes | str) -> bytes:
    +42        return super().__getitem__(always_bytes(key))
    +
    + + +

    A concrete MultiDict, storing its own data.

    +
    + + +
    +
    Inherited Members
    +
    +
    _internal_mitmproxy.coretypes.multidict.MultiDict
    +
    MultiDict
    +
    fields
    +
    get_state
    +
    set_state
    +
    from_state
    + +
    +
    _internal_mitmproxy.coretypes.multidict._MultiDict
    +
    get_all
    +
    set_all
    +
    add
    +
    insert
    +
    keys
    +
    values
    +
    items
    + +
    +
    collections.abc.MutableMapping
    +
    pop
    +
    popitem
    +
    clear
    +
    update
    +
    setdefault
    + +
    +
    _internal_mitmproxy.coretypes.serializable.Serializable
    +
    copy
    + +
    +
    collections.abc.Mapping
    +
    get
    + +
    +
    +
    +
    +
    + +
    + + class + FormSerializer(abc.ABC): + + + +
    + +
     49class FormSerializer(ABC):
    + 50    @abstractmethod
    + 51    def serialize(self, deserialized_body: Form, req: ObjectWithHeaders) -> bytes:
    + 52        """Serialize a parsed form to raw bytes
    + 53
    + 54        Args:
    + 55            deserialized_body (Form): The parsed form
    + 56            req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)
    + 57
    + 58        Returns:
    + 59            bytes: Form's raw bytes representation
    + 60        """
    + 61
    + 62    @abstractmethod
    + 63    def deserialize(self, body: bytes, req: ObjectWithHeaders) -> Form | None:
    + 64        """Parses the form from its raw bytes representation
    + 65
    + 66        Args:
    + 67            body (bytes): The form as bytes
    + 68            req (ObjectWithHeaders): The originating request  (used for multipart to get an up to date boundary from content-type)
    + 69
    + 70        Returns:
    + 71            Form | None: The parsed form
    + 72        """
    + 73
    + 74    @abstractmethod
    + 75    def get_empty_form(self, req: ObjectWithHeaders) -> Form:
    + 76        """Get an empty parsed form object
    + 77
    + 78        Args:
    + 79            req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)
    + 80
    + 81        Returns:
    + 82            Form: The empty form
    + 83        """
    + 84
    + 85    @abstractmethod
    + 86    def deserialized_type(self) -> type[Form]:
    + 87        """Gets the form concrete type
    + 88
    + 89        Returns:
    + 90            type[Form]: The form concrete type
    + 91        """
    + 92
    + 93    @abstractmethod
    + 94    def import_form(self, exported: ExportedForm, req: ObjectWithHeaders) -> Form:
    + 95        """Imports a form exported by a serializer
    + 96            Used to convert a form from a Content-Type to another
    + 97            Information may be lost in the process
    + 98
    + 99        Args:
    +100            exported (ExportedForm): The exported form
    +101            req: (ObjectWithHeaders): Used to get multipart boundary
    +102
    +103        Returns:
    +104            Form: The form converted to this serializer's format
    +105        """
    +106
    +107    @abstractmethod
    +108    def export_form(self, source: Form) -> TupleExportedForm:
    +109        """Formats a form so it can be imported by another serializer
    +110            Information may be lost in the process
    +111
    +112        Args:
    +113            form (Form): The form to export
    +114
    +115        Returns:
    +116            ExportedForm: The exported form
    +117        """
    +
    + + +

    Helper class that provides a standard way to create an ABC using +inheritance.

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + serialize( self, deserialized_body: Form, req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> bytes: + + + +
    + +
    50    @abstractmethod
    +51    def serialize(self, deserialized_body: Form, req: ObjectWithHeaders) -> bytes:
    +52        """Serialize a parsed form to raw bytes
    +53
    +54        Args:
    +55            deserialized_body (Form): The parsed form
    +56            req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)
    +57
    +58        Returns:
    +59            bytes: Form's raw bytes representation
    +60        """
    +
    + + +

    Serialize a parsed form to raw bytes

    + +

    Args: + deserialized_body (Form): The parsed form + req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)

    + +

    Returns: + bytes: Form's raw bytes representation

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + deserialize( self, body: bytes, req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> Form | None: + + + +
    + +
    62    @abstractmethod
    +63    def deserialize(self, body: bytes, req: ObjectWithHeaders) -> Form | None:
    +64        """Parses the form from its raw bytes representation
    +65
    +66        Args:
    +67            body (bytes): The form as bytes
    +68            req (ObjectWithHeaders): The originating request  (used for multipart to get an up to date boundary from content-type)
    +69
    +70        Returns:
    +71            Form | None: The parsed form
    +72        """
    +
    + + +

    Parses the form from its raw bytes representation

    + +

    Args: + body (bytes): The form as bytes + req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)

    + +

    Returns: + Form | None: The parsed form

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + get_empty_form( self, req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> Form: + + + +
    + +
    74    @abstractmethod
    +75    def get_empty_form(self, req: ObjectWithHeaders) -> Form:
    +76        """Get an empty parsed form object
    +77
    +78        Args:
    +79            req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)
    +80
    +81        Returns:
    +82            Form: The empty form
    +83        """
    +
    + + +

    Get an empty parsed form object

    + +

    Args: + req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)

    + +

    Returns: + Form: The empty form

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + deserialized_type(self) -> type[Form]: + + + +
    + +
    85    @abstractmethod
    +86    def deserialized_type(self) -> type[Form]:
    +87        """Gets the form concrete type
    +88
    +89        Returns:
    +90            type[Form]: The form concrete type
    +91        """
    +
    + + +

    Gets the form concrete type

    + +

    Returns: + type[Form]: The form concrete type

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + import_form( self, exported: tuple[tuple[bytes, bytes | None], ...], req: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> Form: + + + +
    + +
     93    @abstractmethod
    + 94    def import_form(self, exported: ExportedForm, req: ObjectWithHeaders) -> Form:
    + 95        """Imports a form exported by a serializer
    + 96            Used to convert a form from a Content-Type to another
    + 97            Information may be lost in the process
    + 98
    + 99        Args:
    +100            exported (ExportedForm): The exported form
    +101            req: (ObjectWithHeaders): Used to get multipart boundary
    +102
    +103        Returns:
    +104            Form: The form converted to this serializer's format
    +105        """
    +
    + + +

    Imports a form exported by a serializer + Used to convert a form from a Content-Type to another + Information may be lost in the process

    + +

    Args: + exported (ExportedForm): The exported form + req: (ObjectWithHeaders): Used to get multipart boundary

    + +

    Returns: + Form: The form converted to this serializer's format

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + export_form( self, source: Form) -> tuple[tuple[bytes, bytes | None], ...]: + + + +
    + +
    107    @abstractmethod
    +108    def export_form(self, source: Form) -> TupleExportedForm:
    +109        """Formats a form so it can be imported by another serializer
    +110            Information may be lost in the process
    +111
    +112        Args:
    +113            form (Form): The form to export
    +114
    +115        Returns:
    +116            ExportedForm: The exported form
    +117        """
    +
    + + +

    Formats a form so it can be imported by another serializer + Information may be lost in the process

    + +

    Args: + form (Form): The form to export

    + +

    Returns: + ExportedForm: The exported form

    +
    + + +
    +
    +
    + +
    + + def + json_unescape(escaped: str) -> str: + + + +
    + +
    55def json_unescape(escaped: str) -> str:
    +56    def decode_match(match):
    +57        return chr(int(match.group(1), 16))
    +58
    +59    return re.sub(r"\\u([0-9a-fA-F]{4})", decode_match, escaped)
    +
    + + + + +
    +
    + +
    + + def + json_unescape_bytes(escaped: str) -> bytes: + + + +
    + +
    62def json_unescape_bytes(escaped: str) -> bytes:
    +63    return json_unescape(escaped).encode("latin-1")
    +
    + + + + +
    +
    + +
    + + def + json_escape_bytes(data: bytes) -> str: + + + +
    + +
    49def json_escape_bytes(data: bytes) -> str:
    +50    printable = string.printable.encode("utf-8")
    +51
    +52    return "".join(chr(ch) if ch in printable else f"\\u{ch:04x}" for ch in data)
    +
    + + + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/java.html b/docs/public/pdoc/python3-10/pyscalpel/java.html new file mode 100644 index 00000000..5e7d4301 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/java.html @@ -0,0 +1,705 @@ + + + + + + + python3-10.pyscalpel.java API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.java

    + +

    This module declares type definitions used for Java objects.

    + +

    If you are a normal user, you should probably never have to manipulate these objects yourself.

    +
    + + + + + +
     1"""
    + 2    This module declares type definitions used for Java objects.
    + 3    
    + 4    If you are a normal user, you should probably never have to manipulate these objects yourself.
    + 5"""
    + 6from .bytes import JavaBytes
    + 7from .import_java import import_java
    + 8from .object import JavaClass, JavaObject
    + 9from . import burp
    +10from . import scalpel_types
    +11
    +12__all__ = [
    +13    "burp",
    +14    "scalpel_types",
    +15    "import_java",
    +16    "JavaObject",
    +17    "JavaBytes",
    +18    "JavaClass",
    +19]
    +
    + + +
    +
    + +
    + + def + import_java( module: str, name: str, expected_type: Type[~ExpectedObject] = <class 'pyscalpel.java.object.JavaObject'>) -> ~ExpectedObject: + + + +
    + +
    19def import_java(
    +20    module: str, name: str, expected_type: Type[ExpectedObject] = JavaObject
    +21) -> ExpectedObject:
    +22    """Import a Java class using Python's import mechanism.
    +23
    +24    :param module: The module to import from. (e.g. "java.lang")
    +25    :param name: The name of the class to import. (e.g. "String")
    +26    :param expected_type: The expected type of the class. (e.g. JavaObject)
    +27    :return: The imported class.
    +28    """
    +29    if _is_pdoc() or os.environ.get("_DO_NOT_IMPORT_JAVA") is not None:
    +30        return None  # type: ignore
    +31    try:  # pragma: no cover
    +32        module = __import__(module, fromlist=[name])
    +33        return getattr(module, name)
    +34    except ImportError as exc:  # pragma: no cover
    +35        raise ImportError(f"Could not import Java class {name}") from exc
    +
    + + +

    Import a Java class using Python's import mechanism.

    + +
    Parameters
    + +
      +
    • module: The module to import from. (e.g. "java.lang")
    • +
    • name: The name of the class to import. (e.g. "String")
    • +
    • expected_type: The expected type of the class. (e.g. JavaObject)
    • +
    + +
    Returns
    + +
    +

    The imported class.

    +
    +
    + + +
    +
    + +
    + + class + JavaObject(typing.Protocol): + + + +
    + +
    10class JavaObject(Protocol, metaclass=ABCMeta):
    +11    """generated source for class Object"""
    +12
    +13    @abstractmethod
    +14    def __init__(self):
    +15        """generated source for method __init__"""
    +16
    +17    @abstractmethod
    +18    def getClass(self) -> JavaClass:
    +19        """generated source for method getClass"""
    +20
    +21    @abstractmethod
    +22    def hashCode(self) -> int:
    +23        """generated source for method hashCode"""
    +24
    +25    @abstractmethod
    +26    def equals(self, obj) -> bool:
    +27        """generated source for method equals"""
    +28
    +29    @abstractmethod
    +30    def clone(self) -> JavaObject:
    +31        """generated source for method clone"""
    +32
    +33    @abstractmethod
    +34    def __str__(self) -> str:
    +35        """generated source for method toString"""
    +36
    +37    @abstractmethod
    +38    def notify(self) -> None:
    +39        """generated source for method notify"""
    +40
    +41    @abstractmethod
    +42    def notifyAll(self) -> None:
    +43        """generated source for method notifyAll"""
    +44
    +45    @abstractmethod
    +46    @overload
    +47    def wait(self) -> None:
    +48        """generated source for method wait"""
    +49
    +50    @abstractmethod
    +51    @overload
    +52    def wait(self, arg0: int) -> None:
    +53        """generated source for method wait_0"""
    +54
    +55    @abstractmethod
    +56    @overload
    +57    def wait(self, timeoutMillis: int, nanos: int) -> None:
    +58        """generated source for method wait_1"""
    +59
    +60    @abstractmethod
    +61    def finalize(self) -> None:
    +62        """generated source for method finalize"""
    +
    + + +

    generated source for class Object

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + getClass(self) -> python3-10.pyscalpel.java.object.JavaClass: + + + +
    + +
    17    @abstractmethod
    +18    def getClass(self) -> JavaClass:
    +19        """generated source for method getClass"""
    +
    + + +

    generated source for method getClass

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + hashCode(self) -> int: + + + +
    + +
    21    @abstractmethod
    +22    def hashCode(self) -> int:
    +23        """generated source for method hashCode"""
    +
    + + +

    generated source for method hashCode

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + equals(self, obj) -> bool: + + + +
    + +
    25    @abstractmethod
    +26    def equals(self, obj) -> bool:
    +27        """generated source for method equals"""
    +
    + + +

    generated source for method equals

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + clone(self) -> python3-10.pyscalpel.java.object.JavaObject: + + + +
    + +
    29    @abstractmethod
    +30    def clone(self) -> JavaObject:
    +31        """generated source for method clone"""
    +
    + + +

    generated source for method clone

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + notify(self) -> None: + + + +
    + +
    37    @abstractmethod
    +38    def notify(self) -> None:
    +39        """generated source for method notify"""
    +
    + + +

    generated source for method notify

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + notifyAll(self) -> None: + + + +
    + +
    41    @abstractmethod
    +42    def notifyAll(self) -> None:
    +43        """generated source for method notifyAll"""
    +
    + + +

    generated source for method notifyAll

    +
    + + +
    +
    + +
    + + def + wait(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + finalize(self) -> None: + + + +
    + +
    60    @abstractmethod
    +61    def finalize(self) -> None:
    +62        """generated source for method finalize"""
    +
    + + +

    generated source for method finalize

    +
    + + +
    +
    +
    + +
    + + class + JavaBytes(list[int]): + + + +
    + +
    5class JavaBytes(list[int]):
    +6    __metaclass__ = ABCMeta
    +
    + + +

    Built-in mutable sequence.

    + +

    If no argument is given, the constructor creates a new empty list. +The argument must be an iterable if specified.

    +
    + + +
    +
    Inherited Members
    +
    +
    builtins.list
    +
    list
    +
    clear
    +
    copy
    +
    append
    +
    insert
    +
    extend
    +
    pop
    +
    remove
    +
    index
    +
    count
    +
    reverse
    +
    sort
    + +
    +
    +
    +
    +
    + +
    + + class + JavaClass(python3-10.pyscalpel.java.JavaObject): + + + +
    + +
    65class JavaClass(JavaObject, metaclass=ABCMeta):
    +66    pass
    +
    + + +

    generated source for class Object

    +
    + + +
    +
    Inherited Members
    +
    + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/java/burp.html b/docs/public/pdoc/python3-10/pyscalpel/java/burp.html new file mode 100644 index 00000000..c84e2f30 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/java/burp.html @@ -0,0 +1,4968 @@ + + + + + + + python3-10.pyscalpel.java.burp API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.java.burp

    + +

    This module exposes Java objects from Burp's extensions API

    + +

    If you are a normal user, you should probably never have to manipulate these objects yourself.

    +
    + + + + + +
     1"""
    + 2    This module exposes Java objects from Burp's extensions API
    + 3    
    + 4    If you are a normal user, you should probably never have to manipulate these objects yourself.
    + 5"""
    + 6from .byte_array import IByteArray, ByteArray
    + 7from .http_header import IHttpHeader, HttpHeader
    + 8from .http_message import IHttpMessage
    + 9from .http_request import IHttpRequest, HttpRequest
    +10from .http_response import IHttpResponse, HttpResponse
    +11from .http_parameter import IHttpParameter, HttpParameter
    +12from .http_service import IHttpService, HttpService
    +13from .http_request_response import IHttpRequestResponse
    +14from .http import IHttp
    +15from .logging import Logging
    +16
    +17__all__ = [
    +18    "IHttp",
    +19    "IHttpRequest",
    +20    "HttpRequest",
    +21    "IHttpResponse",
    +22    "HttpResponse",
    +23    "IHttpRequestResponse",
    +24    "IHttpHeader",
    +25    "HttpHeader",
    +26    "IHttpMessage",
    +27    "IHttpParameter",
    +28    "HttpParameter",
    +29    "IHttpService",
    +30    "HttpService",
    +31    "IByteArray",
    +32    "ByteArray",
    +33    "Logging",
    +34]
    +
    + + +
    +
    + +
    + + class + IHttp(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
    + +
    12class IHttp(JavaObject, Protocol, metaclass=ABCMeta):  # pragma: no cover
    +13    """generated source for interface Http"""
    +14
    +15    __metaclass__ = ABCMeta
    +16
    +17    @abstractmethod
    +18    def sendRequest(self, request: IHttpRequest) -> IHttpRequestResponse:
    +19        ...
    +
    + + +

    generated source for interface Http

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + sendRequest( self, request: pyscalpel.java.burp.http_request.IHttpRequest) -> pyscalpel.java.burp.http_request_response.IHttpRequestResponse: + + + +
    + +
    17    @abstractmethod
    +18    def sendRequest(self, request: IHttpRequest) -> IHttpRequestResponse:
    +19        ...
    +
    + + + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    + +
    + + class + IHttpRequest(pyscalpel.java.burp.http_message.IHttpMessage, typing.Protocol): + + + +
    + +
     20class IHttpRequest(IHttpMessage, Protocol):  # pragma: no cover
    + 21    """generated source for interface HttpRequest"""
    + 22
    + 23    #      * HTTP service for the request.
    + 24    #      *
    + 25    #      * @return An {@link HttpService} object containing details of the HTTP service.
    + 26    #
    + 27
    + 28    @abstractmethod
    + 29    def httpService(self) -> IHttpService:
    + 30        """generated source for method httpService"""
    + 31
    + 32    #
    + 33    #      * URL for the request.
    + 34    #      * If the request is malformed, then a {@link MalformedRequestException} is thrown.
    + 35    #      *
    + 36    #      * @return The URL in the request.
    + 37    #      * @throws MalformedRequestException if request is malformed.
    + 38    #
    + 39
    + 40    @abstractmethod
    + 41    def url(self) -> str:
    + 42        """generated source for method url"""
    + 43
    + 44    #
    + 45    #      * HTTP method for the request.
    + 46    #      * If the request is malformed, then a {@link MalformedRequestException} is thrown.
    + 47    #      *
    + 48    #      * @return The HTTP method used in the request.
    + 49    #      * @throws MalformedRequestException if request is malformed.
    + 50    #
    + 51
    + 52    @abstractmethod
    + 53    def method(self) -> str:
    + 54        """generated source for method method"""
    + 55
    + 56    #
    + 57    #      * Path and File for the request.
    + 58    #      * If the request is malformed, then a {@link MalformedRequestException} is thrown.
    + 59    #      *
    + 60    #      * @return the path and file in the request
    + 61    #      * @throws MalformedRequestException if request is malformed.
    + 62    #
    + 63
    + 64    @abstractmethod
    + 65    def path(self) -> str:
    + 66        """generated source for method path"""
    + 67
    + 68    #
    + 69    #      * HTTP Version text parsed from the request line for HTTP 1 messages.
    + 70    #      * HTTP 2 messages will return "HTTP/2"
    + 71    #      *
    + 72    #      * @return Version string
    + 73    #
    + 74
    + 75    @abstractmethod
    + 76    def httpVersion(self) -> str | None:
    + 77        """generated source for method httpVersion"""
    + 78
    + 79    #
    + 80    #      * {@inheritDoc}
    + 81    #
    + 82
    + 83    @abstractmethod
    + 84    def headers(self) -> list[IHttpHeader]:
    + 85        """generated source for method headers"""
    + 86
    + 87    #
    + 88    #      * @return The detected content type of the request.
    + 89    #
    + 90
    + 91    @abstractmethod
    + 92    def contentType(self) -> JavaObject:
    + 93        """generated source for method contentType"""
    + 94
    + 95    #
    + 96    #      * @return The parameters contained in the request.
    + 97    #
    + 98
    + 99    @abstractmethod
    +100    def parameters(self) -> list[IHttpParameter]:
    +101        """generated source for method parameters"""
    +102
    +103    #
    +104    #      * {@inheritDoc}
    +105    #
    +106
    +107    @abstractmethod
    +108    def body(self) -> IByteArray:
    +109        """generated source for method body"""
    +110
    +111    #
    +112    #      * {@inheritDoc}
    +113    #
    +114
    +115    @abstractmethod
    +116    def bodyToString(self) -> str:
    +117        """generated source for method bodyToString"""
    +118
    +119    #
    +120    #      * {@inheritDoc}
    +121    #
    +122
    +123    @abstractmethod
    +124    def bodyOffset(self) -> int:
    +125        """generated source for method bodyOffset"""
    +126
    +127    #
    +128    #      * {@inheritDoc}
    +129    #
    +130
    +131    @abstractmethod
    +132    def markers(self):
    +133        """generated source for method markers"""
    +134
    +135    #
    +136    #      * {@inheritDoc}
    +137    #
    +138
    +139    @abstractmethod
    +140    def toByteArray(self) -> IByteArray:
    +141        """generated source for method toByteArray"""
    +142
    +143    #
    +144    #      * {@inheritDoc}
    +145    #
    +146
    +147    @abstractmethod
    +148    def __str__(self) -> str:
    +149        """generated source for method toString"""
    +150
    +151    #
    +152    #      * Create a copy of the {@code HttpRequest} in temporary file.<br>
    +153    #      * This method is used to save the {@code HttpRequest} object to a temporary file,
    +154    #      * so that it is no longer held in memory. Extensions can use this method to convert
    +155    #      * {@code HttpRequest} objects into a form suitable for long-term usage.
    +156    #      *
    +157    #      * @return A new {@code ByteArray} instance stored in temporary file.
    +158    #
    +159
    +160    @abstractmethod
    +161    def copyToTempFile(self) -> IHttpRequest:
    +162        """generated source for method copyToTempFile"""
    +163
    +164    #
    +165    #      * Create a copy of the {@code HttpRequest} with the new service.
    +166    #      *
    +167    #      * @param service An {@link HttpService} reference to add.
    +168    #      *
    +169    #      * @return A new {@code HttpRequest} instance.
    +170    #
    +171
    +172    @abstractmethod
    +173    def withService(self, service: IHttpService) -> IHttpRequest:
    +174        """generated source for method withService"""
    +175
    +176    #
    +177    #      * Create a copy of the {@code HttpRequest} with the new path.
    +178    #      *
    +179    #      * @param path The path to use.
    +180    #      *
    +181    #      * @return A new {@code HttpRequest} instance with updated path.
    +182    #
    +183
    +184    @abstractmethod
    +185    def withPath(self, path: str) -> IHttpRequest:
    +186        """generated source for method withPath"""
    +187
    +188    #
    +189    #      * Create a copy of the {@code HttpRequest} with the new method.
    +190    #      *
    +191    #      * @param method the method to use
    +192    #      *
    +193    #      * @return a new {@code HttpRequest} instance with updated method.
    +194    #
    +195
    +196    @abstractmethod
    +197    def withMethod(self, method: str) -> IHttpRequest:
    +198        """generated source for method withMethod"""
    +199
    +200    #
    +201    #      * Create a copy of the {@code HttpRequest} with the added HTTP parameters.
    +202    #      *
    +203    #      * @param parameters HTTP parameters to add.
    +204    #      *
    +205    #      * @return A new {@code HttpRequest} instance.
    +206    #
    +207
    +208    @abstractmethod
    +209    def withAddedParameters(self, parameters: Iterable[IHttpParameter]) -> IHttpRequest:
    +210        """generated source for method withAddedParameters"""
    +211
    +212    #
    +213    #      * Create a copy of the {@code HttpRequest} with the added HTTP parameters.
    +214    #      *
    +215    #      * @param parameters HTTP parameters to add.
    +216    #      *
    +217    #      * @return A new {@code HttpRequest} instance.
    +218    #
    +219
    +220    @abstractmethod
    +221    def withAddedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
    +222        """generated source for method withAddedParameters_0"""
    +223
    +224    #
    +225    #      * Create a copy of the {@code HttpRequest} with the removed HTTP parameters.
    +226    #      *
    +227    #      * @param parameters HTTP parameters to remove.
    +228    #      *
    +229    #      * @return A new {@code HttpRequest} instance.
    +230    #
    +231
    +232    @abstractmethod
    +233    def withRemovedParameters(
    +234        self, parameters: Iterable[IHttpParameter]
    +235    ) -> IHttpRequest:
    +236        """generated source for method withRemovedParameters"""
    +237
    +238    #
    +239    #      * Create a copy of the {@code HttpRequest} with the removed HTTP parameters.
    +240    #      *
    +241    #      * @param parameters HTTP parameters to remove.
    +242    #      *
    +243    #      * @return A new {@code HttpRequest} instance.
    +244    #
    +245
    +246    @abstractmethod
    +247    def withRemovedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
    +248        """generated source for method withRemovedParameters_0"""
    +249
    +250    #
    +251    #      * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.<br>
    +252    #      * If a parameter does not exist in the request, a new one will be added.
    +253    #      *
    +254    #      * @param parameters HTTP parameters to update.
    +255    #      *
    +256    #      * @return A new {@code HttpRequest} instance.
    +257    #
    +258
    +259    @abstractmethod
    +260    def withUpdatedParameters(self, parameters: list[IHttpParameter]) -> IHttpRequest:
    +261        """generated source for method withUpdatedParameters"""
    +262
    +263    #
    +264    #      * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.<br>
    +265    #      * If a parameter does not exist in the request, a new one will be added.
    +266    #      *
    +267    #      * @param parameters HTTP parameters to update.
    +268    #      *
    +269    #      * @return A new {@code HttpRequest} instance.
    +270    #
    +271
    +272    @abstractmethod
    +273    def withUpdatedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
    +274        """generated source for method withUpdatedParameters_0"""
    +275
    +276    #
    +277    #      * Create a copy of the {@code HttpRequest} with the transformation applied.
    +278    #      *
    +279    #      * @param transformation Transformation to apply.
    +280    #      *
    +281    #      * @return A new {@code HttpRequest} instance.
    +282    #
    +283
    +284    @abstractmethod
    +285    def withTransformationApplied(self, transformation) -> IHttpRequest:
    +286        """generated source for method withTransformationApplied"""
    +287
    +288    #
    +289    #      * Create a copy of the {@code HttpRequest} with the updated body.<br>
    +290    #      * Updates Content-Length header.
    +291    #      *
    +292    #      * @param body the new body for the request
    +293    #      *
    +294    #      * @return A new {@code HttpRequest} instance.
    +295    #
    +296
    +297    @abstractmethod
    +298    def withBody(self, body) -> IHttpRequest:
    +299        """generated source for method withBody"""
    +300
    +301    #
    +302    #      * Create a copy of the {@code HttpRequest} with the updated body.<br>
    +303    #      * Updates Content-Length header.
    +304    #      *
    +305    #      * @param body the new body for the request
    +306    #      *
    +307    #      * @return A new {@code HttpRequest} instance.
    +308    #
    +309
    +310    @abstractmethod
    +311    def withBody_0(self, body: IByteArray) -> IHttpRequest:
    +312        """generated source for method withBody_0"""
    +313
    +314    #
    +315    #      * Create a copy of the {@code HttpRequest} with the added header.
    +316    #      *
    +317    #      * @param name  The name of the header.
    +318    #      * @param value The value of the header.
    +319    #      *
    +320    #      * @return The updated HTTP request with the added header.
    +321    #
    +322
    +323    @abstractmethod
    +324    def withAddedHeader(self, name: str, value: str) -> IHttpRequest:
    +325        """generated source for method withAddedHeader"""
    +326
    +327    #
    +328    #      * Create a copy of the {@code HttpRequest} with the added header.
    +329    #      *
    +330    #      * @param header The {@link HttpHeader} to add to the HTTP request.
    +331    #      *
    +332    #      * @return The updated HTTP request with the added header.
    +333    #
    +334
    +335    @abstractmethod
    +336    def withAddedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
    +337        """generated source for method withAddedHeader_0"""
    +338
    +339    #
    +340    #      * Create a copy of the {@code HttpRequest} with the updated header.
    +341    #      *
    +342    #      * @param name  The name of the header to update the value of.
    +343    #      * @param value The new value of the specified HTTP header.
    +344    #      *
    +345    #      * @return The updated request containing the updated header.
    +346    #
    +347
    +348    @abstractmethod
    +349    def withUpdatedHeader(self, name: str, value: str) -> IHttpRequest:
    +350        """generated source for method withUpdatedHeader"""
    +351
    +352    #
    +353    #      * Create a copy of the {@code HttpRequest} with the updated header.
    +354    #      *
    +355    #      * @param header The {@link HttpHeader} to update containing the new value.
    +356    #      *
    +357    #      * @return The updated request containing the updated header.
    +358    #
    +359
    +360    @abstractmethod
    +361    def withUpdatedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
    +362        """generated source for method withUpdatedHeader_0"""
    +363
    +364    #
    +365    #      * Removes an existing HTTP header from the current request.
    +366    #      *
    +367    #      * @param name The name of the HTTP header to remove from the request.
    +368    #      *
    +369    #      * @return The updated request containing the removed header.
    +370    #
    +371
    +372    @abstractmethod
    +373    def withRemovedHeader(self, name: str) -> IHttpRequest:
    +374        """generated source for method withRemovedHeader"""
    +375
    +376    #
    +377    #      * Removes an existing HTTP header from the current request.
    +378    #      *
    +379    #      * @param header The {@link HttpHeader} to remove from the request.
    +380    #      *
    +381    #      * @return The updated request containing the removed header.
    +382    #
    +383
    +384    @abstractmethod
    +385    def withRemovedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
    +386        """generated source for method withRemovedHeader_0"""
    +387
    +388    #
    +389    #      * Create a copy of the {@code HttpRequest} with the added markers.
    +390    #      *
    +391    #      * @param markers Request markers to add.
    +392    #      *
    +393    #      * @return A new {@code MarkedHttpRequestResponse} instance.
    +394    #
    +395
    +396    @abstractmethod
    +397    def withMarkers(self, markers) -> IHttpRequest:
    +398        """generated source for method withMarkers"""
    +399
    +400    #
    +401    #      * Create a copy of the {@code HttpRequest} with the added markers.
    +402    #      *
    +403    #      * @param markers Request markers to add.
    +404    #      *
    +405    #      * @return A new {@code MarkedHttpRequestResponse} instance.
    +406    #
    +407
    +408    @abstractmethod
    +409    def withMarkers_0(self, *markers) -> IHttpRequest:
    +410        """generated source for method withMarkers_0"""
    +411
    +412    #
    +413    #      * Create a copy of the {@code HttpRequest} with added default headers.
    +414    #      *
    +415    #      * @return a new (@code HttpRequest) with added default headers
    +416    #
    +417
    +418    @abstractmethod
    +419    def withDefaultHeaders(self) -> IHttpRequest:
    +420        """generated source for method withDefaultHeaders"""
    +421
    +422    #
    +423    #      * Create a new empty instance of {@link HttpRequest}.<br>
    +424    #      *
    +425    #      * @².
    +426    #
    +427
    +428    @abstractmethod
    +429    @overload
    +430    def httpRequest(self, request: IByteArray | str) -> IHttpRequest:
    +431        """generated source for method httpRequest"""
    +432
    +433    @abstractmethod
    +434    @overload
    +435    def httpRequest(self, service: IHttpService, req: IByteArray | str) -> IHttpRequest:
    +436        """generated source for method httpRequest"""
    +437
    +438    #
    +439    #      * Create a new instance of {@link HttpRequest}.<br>
    +440    #      *
    +441    #      *
    +442    #      * @².
    +443    #
    +444    #
    +445
    +446    @abstractmethod
    +447    def httpRequestFromUrl(self, url: str) -> IHttpRequest:
    +448        """generated source for method httpRequestFromUrl"""
    +449
    +450    #
    +451    #      * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.<br>
    +452    #      *
    +453    #      * @param service An HTTP service for the request.
    +454    #      * @param headers A list of HTTP 2 headers.
    +455    #      * @param body    A body of the HTTP 2 request.
    +456    #      *
    +457    #      * @².
    +458    #
    +459
    +460    @abstractmethod
    +461    def http2Request(
    +462        self, service: IHttpService, headers: Iterable[IHttpHeader], body: IByteArray
    +463    ) -> IHttpRequest:
    +464        """generated source for method http2Request"""
    +465
    +466    #
    +467    #      * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.<br>
    +468    #      *
    +469    #      * @param service An HTTP service for the request.
    +470    #      * @param headers A list of HTTP 2 headers.
    +471    #      * @param body    A body of the HTTP 2 request.
    +472    #      *
    +473    #      * @².
    +474    #
    +
    + + +

    generated source for interface HttpRequest

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + httpService(self) -> pyscalpel.java.burp.http_service.IHttpService: + + + +
    + +
    28    @abstractmethod
    +29    def httpService(self) -> IHttpService:
    +30        """generated source for method httpService"""
    +
    + + +

    generated source for method httpService

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + url(self) -> str: + + + +
    + +
    40    @abstractmethod
    +41    def url(self) -> str:
    +42        """generated source for method url"""
    +
    + + +

    generated source for method url

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + method(self) -> str: + + + +
    + +
    52    @abstractmethod
    +53    def method(self) -> str:
    +54        """generated source for method method"""
    +
    + + +

    generated source for method method

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + path(self) -> str: + + + +
    + +
    64    @abstractmethod
    +65    def path(self) -> str:
    +66        """generated source for method path"""
    +
    + + +

    generated source for method path

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + httpVersion(self) -> str | None: + + + +
    + +
    75    @abstractmethod
    +76    def httpVersion(self) -> str | None:
    +77        """generated source for method httpVersion"""
    +
    + + +

    generated source for method httpVersion

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + headers(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]: + + + +
    + +
    83    @abstractmethod
    +84    def headers(self) -> list[IHttpHeader]:
    +85        """generated source for method headers"""
    +
    + + +

    generated source for method headers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + contentType(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    91    @abstractmethod
    +92    def contentType(self) -> JavaObject:
    +93        """generated source for method contentType"""
    +
    + + +

    generated source for method contentType

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + parameters(self) -> list[pyscalpel.java.burp.http_parameter.IHttpParameter]: + + + +
    + +
     99    @abstractmethod
    +100    def parameters(self) -> list[IHttpParameter]:
    +101        """generated source for method parameters"""
    +
    + + +

    generated source for method parameters

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + body(self) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    107    @abstractmethod
    +108    def body(self) -> IByteArray:
    +109        """generated source for method body"""
    +
    + + +

    generated source for method body

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyToString(self) -> str: + + + +
    + +
    115    @abstractmethod
    +116    def bodyToString(self) -> str:
    +117        """generated source for method bodyToString"""
    +
    + + +

    generated source for method bodyToString

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyOffset(self) -> int: + + + +
    + +
    123    @abstractmethod
    +124    def bodyOffset(self) -> int:
    +125        """generated source for method bodyOffset"""
    +
    + + +

    generated source for method bodyOffset

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + markers(self): + + + +
    + +
    131    @abstractmethod
    +132    def markers(self):
    +133        """generated source for method markers"""
    +
    + + +

    generated source for method markers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + toByteArray(self) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    139    @abstractmethod
    +140    def toByteArray(self) -> IByteArray:
    +141        """generated source for method toByteArray"""
    +
    + + +

    generated source for method toByteArray

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + copyToTempFile(self) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    160    @abstractmethod
    +161    def copyToTempFile(self) -> IHttpRequest:
    +162        """generated source for method copyToTempFile"""
    +
    + + +

    generated source for method copyToTempFile

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withService( self, service: pyscalpel.java.burp.http_service.IHttpService) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    172    @abstractmethod
    +173    def withService(self, service: IHttpService) -> IHttpRequest:
    +174        """generated source for method withService"""
    +
    + + +

    generated source for method withService

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withPath( self, path: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    184    @abstractmethod
    +185    def withPath(self, path: str) -> IHttpRequest:
    +186        """generated source for method withPath"""
    +
    + + +

    generated source for method withPath

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withMethod( self, method: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    196    @abstractmethod
    +197    def withMethod(self, method: str) -> IHttpRequest:
    +198        """generated source for method withMethod"""
    +
    + + +

    generated source for method withMethod

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withAddedParameters( self, parameters: Iterable[pyscalpel.java.burp.http_parameter.IHttpParameter]) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    208    @abstractmethod
    +209    def withAddedParameters(self, parameters: Iterable[IHttpParameter]) -> IHttpRequest:
    +210        """generated source for method withAddedParameters"""
    +
    + + +

    generated source for method withAddedParameters

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withAddedParameters_0( self, *parameters: pyscalpel.java.burp.http_parameter.IHttpParameter) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    220    @abstractmethod
    +221    def withAddedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
    +222        """generated source for method withAddedParameters_0"""
    +
    + + +

    generated source for method withAddedParameters_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withRemovedParameters( self, parameters: Iterable[pyscalpel.java.burp.http_parameter.IHttpParameter]) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    232    @abstractmethod
    +233    def withRemovedParameters(
    +234        self, parameters: Iterable[IHttpParameter]
    +235    ) -> IHttpRequest:
    +236        """generated source for method withRemovedParameters"""
    +
    + + +

    generated source for method withRemovedParameters

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withRemovedParameters_0( self, *parameters: pyscalpel.java.burp.http_parameter.IHttpParameter) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    246    @abstractmethod
    +247    def withRemovedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
    +248        """generated source for method withRemovedParameters_0"""
    +
    + + +

    generated source for method withRemovedParameters_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withUpdatedParameters( self, parameters: list[pyscalpel.java.burp.http_parameter.IHttpParameter]) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    259    @abstractmethod
    +260    def withUpdatedParameters(self, parameters: list[IHttpParameter]) -> IHttpRequest:
    +261        """generated source for method withUpdatedParameters"""
    +
    + + +

    generated source for method withUpdatedParameters

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withUpdatedParameters_0( self, *parameters: pyscalpel.java.burp.http_parameter.IHttpParameter) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    272    @abstractmethod
    +273    def withUpdatedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest:
    +274        """generated source for method withUpdatedParameters_0"""
    +
    + + +

    generated source for method withUpdatedParameters_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withTransformationApplied( self, transformation) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    284    @abstractmethod
    +285    def withTransformationApplied(self, transformation) -> IHttpRequest:
    +286        """generated source for method withTransformationApplied"""
    +
    + + +

    generated source for method withTransformationApplied

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withBody(self, body) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    297    @abstractmethod
    +298    def withBody(self, body) -> IHttpRequest:
    +299        """generated source for method withBody"""
    +
    + + +

    generated source for method withBody

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withBody_0( self, body: pyscalpel.java.burp.byte_array.IByteArray) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    310    @abstractmethod
    +311    def withBody_0(self, body: IByteArray) -> IHttpRequest:
    +312        """generated source for method withBody_0"""
    +
    + + +

    generated source for method withBody_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withAddedHeader( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    323    @abstractmethod
    +324    def withAddedHeader(self, name: str, value: str) -> IHttpRequest:
    +325        """generated source for method withAddedHeader"""
    +
    + + +

    generated source for method withAddedHeader

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withAddedHeader_0( self, header: pyscalpel.java.burp.http_header.IHttpHeader) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    335    @abstractmethod
    +336    def withAddedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
    +337        """generated source for method withAddedHeader_0"""
    +
    + + +

    generated source for method withAddedHeader_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withUpdatedHeader( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    348    @abstractmethod
    +349    def withUpdatedHeader(self, name: str, value: str) -> IHttpRequest:
    +350        """generated source for method withUpdatedHeader"""
    +
    + + +

    generated source for method withUpdatedHeader

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withUpdatedHeader_0( self, header: pyscalpel.java.burp.http_header.IHttpHeader) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    360    @abstractmethod
    +361    def withUpdatedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
    +362        """generated source for method withUpdatedHeader_0"""
    +
    + + +

    generated source for method withUpdatedHeader_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withRemovedHeader( self, name: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    372    @abstractmethod
    +373    def withRemovedHeader(self, name: str) -> IHttpRequest:
    +374        """generated source for method withRemovedHeader"""
    +
    + + +

    generated source for method withRemovedHeader

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withRemovedHeader_0( self, header: pyscalpel.java.burp.http_header.IHttpHeader) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    384    @abstractmethod
    +385    def withRemovedHeader_0(self, header: IHttpHeader) -> IHttpRequest:
    +386        """generated source for method withRemovedHeader_0"""
    +
    + + +

    generated source for method withRemovedHeader_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withMarkers( self, markers) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    396    @abstractmethod
    +397    def withMarkers(self, markers) -> IHttpRequest:
    +398        """generated source for method withMarkers"""
    +
    + + +

    generated source for method withMarkers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withMarkers_0( self, *markers) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    408    @abstractmethod
    +409    def withMarkers_0(self, *markers) -> IHttpRequest:
    +410        """generated source for method withMarkers_0"""
    +
    + + +

    generated source for method withMarkers_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withDefaultHeaders(self) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    418    @abstractmethod
    +419    def withDefaultHeaders(self) -> IHttpRequest:
    +420        """generated source for method withDefaultHeaders"""
    +
    + + +

    generated source for method withDefaultHeaders

    +
    + + +
    +
    + +
    + + def + httpRequest(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + httpRequestFromUrl( self, url: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    446    @abstractmethod
    +447    def httpRequestFromUrl(self, url: str) -> IHttpRequest:
    +448        """generated source for method httpRequestFromUrl"""
    +
    + + +

    generated source for method httpRequestFromUrl

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + http2Request( self, service: pyscalpel.java.burp.http_service.IHttpService, headers: Iterable[pyscalpel.java.burp.http_header.IHttpHeader], body: pyscalpel.java.burp.byte_array.IByteArray) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest: + + + +
    + +
    460    @abstractmethod
    +461    def http2Request(
    +462        self, service: IHttpService, headers: Iterable[IHttpHeader], body: IByteArray
    +463    ) -> IHttpRequest:
    +464        """generated source for method http2Request"""
    +
    + + +

    generated source for method http2Request

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + HttpRequest = +None + + +
    + + + + +
    +
    + +
    + + class + IHttpResponse(pyscalpel.java.burp.http_message.IHttpMessage, typing.Protocol): + + + +
    + +
     18class IHttpResponse(IHttpMessage, Protocol):  # pragma: no cover
    + 19    """generated source for interface HttpResponse"""
    + 20
    + 21    #
    + 22    #      * Obtain the HTTP status code contained in the response.
    + 23    #      *
    + 24    #      * @return HTTP status code.
    + 25    #
    + 26    @abstractmethod
    + 27    def statusCode(self) -> int:
    + 28        """generated source for method statusCode"""
    + 29
    + 30    #
    + 31    #      * Obtain the HTTP reason phrase contained in the response for HTTP 1 messages.
    + 32    #      * HTTP 2 messages will return a mapped phrase based on the status code.
    + 33    #      *
    + 34    #      * @return HTTP Reason phrase.
    + 35    #
    + 36    @abstractmethod
    + 37    def reasonPhrase(self) -> str | None:
    + 38        """generated source for method reasonPhrase"""
    + 39
    + 40    #
    + 41    #      * Return the HTTP Version text parsed from the response line for HTTP 1 messages.
    + 42    #      * HTTP 2 messages will return "HTTP/2"
    + 43    #      *
    + 44    #      * @return Version string
    + 45    #
    + 46    @abstractmethod
    + 47    def httpVersion(self) -> str | None:
    + 48        """generated source for method httpVersion"""
    + 49
    + 50    #
    + 51    #      * {@inheritDoc}
    + 52    #
    + 53    @abstractmethod
    + 54    def headers(self) -> list[IHttpHeader]:
    + 55        """generated source for method headers"""
    + 56
    + 57    #
    + 58    #      * {@inheritDoc}
    + 59    #
    + 60    @abstractmethod
    + 61    def body(self) -> IByteArray | None:
    + 62        """generated source for method body"""
    + 63
    + 64    #
    + 65    #      * {@inheritDoc}
    + 66    #
    + 67    @abstractmethod
    + 68    def bodyToString(self) -> str:
    + 69        """generated source for method bodyToString"""
    + 70
    + 71    #
    + 72    #      * {@inheritDoc}
    + 73    #
    + 74    @abstractmethod
    + 75    def bodyOffset(self) -> int:
    + 76        """generated source for method bodyOffset"""
    + 77
    + 78    #
    + 79    #      * {@inheritDoc}
    + 80    #
    + 81    @abstractmethod
    + 82    def markers(self) -> JavaObject:
    + 83        """generated source for method markers"""
    + 84
    + 85    #
    + 86    #      * Obtain details of the HTTP cookies set in the response.
    + 87    #      *
    + 88    #      * @return A list of {@link Cookie} objects representing the cookies set in the response, if any.
    + 89    #
    + 90    @abstractmethod
    + 91    def cookies(self) -> JavaObject:
    + 92        """generated source for method cookies"""
    + 93
    + 94    #
    + 95    #      * Obtain the MIME type of the response, as stated in the HTTP headers.
    + 96    #      *
    + 97    #      * @return The stated MIME type.
    + 98    #
    + 99    @abstractmethod
    +100    def statedMimeType(self) -> JavaObject:
    +101        """generated source for method statedMimeType"""
    +102
    +103    #
    +104    #      * Obtain the MIME type of the response, as inferred from the contents of the HTTP message body.
    +105    #      *
    +106    #      * @return The inferred MIME type.
    +107    #
    +108    @abstractmethod
    +109    def inferredMimeType(self) -> JavaObject:
    +110        """generated source for method inferredMimeType"""
    +111
    +112    #
    +113    #      * Retrieve the number of types given keywords appear in the response.
    +114    #      *
    +115    #      * @param keywords Keywords to count.
    +116    #      *
    +117    #      * @return List of keyword counts in the order they were provided.
    +118    #
    +119    @abstractmethod
    +120    def keywordCounts(self, *keywords) -> int:
    +121        """generated source for method keywordCounts"""
    +122
    +123    #
    +124    #      * Retrieve the values of response attributes.
    +125    #      *
    +126    #      * @param types Response attributes to retrieve values for.
    +127    #      *
    +128    #      * @return List of {@link Attribute} objects.
    +129    #
    +130    @abstractmethod
    +131    def attributes(self, *types) -> JavaObject:
    +132        """generated source for method attributes"""
    +133
    +134    #
    +135    #      * {@inheritDoc}
    +136    #
    +137    @abstractmethod
    +138    def toByteArray(self) -> IByteArray:
    +139        """generated source for method toByteArray"""
    +140
    +141    #
    +142    #      * {@inheritDoc}
    +143    #
    +144    @abstractmethod
    +145    def __str__(self) -> str:
    +146        """generated source for method toString"""
    +147
    +148    #
    +149    #      * Create a copy of the {@code HttpResponse} in temporary file.<br>
    +150    #      * This method is used to save the {@code HttpResponse} object to a temporary file,
    +151    #      * so that it is no longer held in memory. Extensions can use this method to convert
    +152    #      * {@code HttpResponse} objects into a form suitable for long-term usage.
    +153    #      *
    +154    #      * @return A new {@code HttpResponse} instance stored in temporary file.
    +155    #
    +156    @abstractmethod
    +157    def copyToTempFile(self) -> IHttpResponse:
    +158        """generated source for method copyToTempFile"""
    +159
    +160    #
    +161    #      * Create a copy of the {@code HttpResponse} with the provided status code.
    +162    #      *
    +163    #      * @param statusCode the new status code for response
    +164    #      *
    +165    #      * @return A new {@code HttpResponse} instance.
    +166    #
    +167    @abstractmethod
    +168    def withStatusCode(self, statusCode: int) -> IHttpResponse:
    +169        """generated source for method withStatusCode"""
    +170
    +171    #
    +172    #      * Create a copy of the {@code HttpResponse} with the new reason phrase.
    +173    #      *
    +174    #      * @param reasonPhrase the new reason phrase for response
    +175    #      *
    +176    #      * @return A new {@code HttpResponse} instance.
    +177    #
    +178    @abstractmethod
    +179    def withReasonPhrase(self, reasonPhrase: str) -> IHttpResponse:
    +180        """generated source for method withReasonPhrase"""
    +181
    +182    #
    +183    #      * Create a copy of the {@code HttpResponse} with the new http version.
    +184    #      *
    +185    #      * @param httpVersion the new http version for response
    +186    #      *
    +187    #      * @return A new {@code HttpResponse} instance.
    +188    #
    +189    @abstractmethod
    +190    def withHttpVersion(self, httpVersion: str) -> IHttpResponse:
    +191        """generated source for method withHttpVersion"""
    +192
    +193    #
    +194    #      * Create a copy of the {@code HttpResponse} with the updated body.<br>
    +195    #      * Updates Content-Length header.
    +196    #      *
    +197    #      * @param body the new body for the response
    +198    #      *
    +199    #      * @return A new {@code HttpResponse} instance.
    +200    #
    +201    @abstractmethod
    +202    def withBody(self, body: IByteArray | str) -> IHttpResponse:
    +203        """generated source for method withBody"""
    +204
    +205    #
    +206    #      * Create a copy of the {@code HttpResponse} with the added header.
    +207    #      *
    +208    #      * @param header The {@link HttpHeader} to add to the response.
    +209    #      *
    +210    #      * @return The updated response containing the added header.
    +211    #
    +212    # @abstractmethod
    +213    # def withAddedHeader(self, header) -> 'IHttpResponse':
    +214    #     """ generated source for method withAddedHeader """
    +215
    +216    # #
    +217    # #      * Create a copy of the {@code HttpResponse}  with the added header.
    +218    # #      *
    +219    # #      * @param name  The name of the header.
    +220    # #      * @param value The value of the header.
    +221    # #      *
    +222    # #      * @return The updated response containing the added header.
    +223    # #
    +224    @abstractmethod
    +225    def withAddedHeader(self, name: str, value: str) -> IHttpResponse:
    +226        """generated source for method withAddedHeader_0"""
    +227
    +228    #
    +229    #      * Create a copy of the {@code HttpResponse}  with the updated header.
    +230    #      *
    +231    #      * @param header The {@link HttpHeader} to update containing the new value.
    +232    #      *
    +233    #      * @return The updated response containing the updated header.
    +234    #
    +235    # @abstractmethod
    +236    # def withUpdatedHeader(self, header) -> 'IHttpResponse':
    +237    #     """ generated source for method withUpdatedHeader """
    +238
    +239    # #
    +240    # #      * Create a copy of the {@code HttpResponse}  with the updated header.
    +241    # #      *
    +242    # #      * @param name  The name of the header to update the value of.
    +243    # #      * @param value The new value of the specified HTTP header.
    +244    # #      *
    +245    # #      * @return The updated response containing the updated header.
    +246    # #
    +247    @abstractmethod
    +248    def withUpdatedHeader(self, name: str, value: str) -> IHttpResponse:
    +249        """generated source for method withUpdatedHeader_0"""
    +250
    +251    #
    +252    #      * Create a copy of the {@code HttpResponse}  with the removed header.
    +253    #      *
    +254    #      * @param header The {@link HttpHeader} to remove from the response.
    +255    #      *
    +256    #      * @return The updated response containing the removed header.
    +257    #
    +258    # @abstractmethod
    +259    # def withRemovedHeader(self, header) -> 'IHttpResponse':
    +260    #     """ generated source for method withRemovedHeader """
    +261
    +262    # #
    +263    # #      * Create a copy of the {@code HttpResponse}  with the removed header.
    +264    # #      *
    +265    # #      * @param name The name of the HTTP header to remove from the response.
    +266    # #      *
    +267    # #      * @return The updated response containing the removed header.
    +268    # #
    +269    @abstractmethod
    +270    def withRemovedHeader(self, name: str) -> IHttpResponse:
    +271        """generated source for method withRemovedHeader_0"""
    +272
    +273    #
    +274    #      * Create a copy of the {@code HttpResponse} with the added markers.
    +275    #      *
    +276    #      * @param markers Request markers to add.
    +277    #      *
    +278    #      * @return A new {@code MarkedHttpRequestResponse} instance.
    +279    #
    +280    @abstractmethod
    +281    @overload
    +282    def withMarkers(self, markers: JavaObject) -> IHttpResponse:
    +283        """generated source for method withMarkers"""
    +284
    +285    #
    +286    #      * Create a copy of the {@code HttpResponse} with the added markers.
    +287    #      *
    +288    #      * @param markers Request markers to add.
    +289    #      *
    +290    #      * @return A new {@code MarkedHttpRequestResponse} instance.
    +291    #
    +292    @abstractmethod
    +293    @overload
    +294    def withMarkers(self, *markers: JavaObject) -> IHttpResponse:
    +295        """generated source for method withMarkers_0"""
    +296
    +297    #
    +298    #      * Create a new empty instance of {@link HttpResponse}.<br>
    +299    #      *
    +300    #      * @return A new {@link HttpResponse} instance.
    +301    #
    +302    @abstractmethod
    +303    def httpResponse(self, response: IByteArray | str) -> IHttpResponse:
    +304        """generated source for method httpResponse"""
    +
    + + +

    generated source for interface HttpResponse

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + statusCode(self) -> int: + + + +
    + +
    26    @abstractmethod
    +27    def statusCode(self) -> int:
    +28        """generated source for method statusCode"""
    +
    + + +

    generated source for method statusCode

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + reasonPhrase(self) -> str | None: + + + +
    + +
    36    @abstractmethod
    +37    def reasonPhrase(self) -> str | None:
    +38        """generated source for method reasonPhrase"""
    +
    + + +

    generated source for method reasonPhrase

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + httpVersion(self) -> str | None: + + + +
    + +
    46    @abstractmethod
    +47    def httpVersion(self) -> str | None:
    +48        """generated source for method httpVersion"""
    +
    + + +

    generated source for method httpVersion

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + headers(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]: + + + +
    + +
    53    @abstractmethod
    +54    def headers(self) -> list[IHttpHeader]:
    +55        """generated source for method headers"""
    +
    + + +

    generated source for method headers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + body(self) -> pyscalpel.java.burp.byte_array.IByteArray | None: + + + +
    + +
    60    @abstractmethod
    +61    def body(self) -> IByteArray | None:
    +62        """generated source for method body"""
    +
    + + +

    generated source for method body

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyToString(self) -> str: + + + +
    + +
    67    @abstractmethod
    +68    def bodyToString(self) -> str:
    +69        """generated source for method bodyToString"""
    +
    + + +

    generated source for method bodyToString

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyOffset(self) -> int: + + + +
    + +
    74    @abstractmethod
    +75    def bodyOffset(self) -> int:
    +76        """generated source for method bodyOffset"""
    +
    + + +

    generated source for method bodyOffset

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + markers(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    81    @abstractmethod
    +82    def markers(self) -> JavaObject:
    +83        """generated source for method markers"""
    +
    + + +

    generated source for method markers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + cookies(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    90    @abstractmethod
    +91    def cookies(self) -> JavaObject:
    +92        """generated source for method cookies"""
    +
    + + +

    generated source for method cookies

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + statedMimeType(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
     99    @abstractmethod
    +100    def statedMimeType(self) -> JavaObject:
    +101        """generated source for method statedMimeType"""
    +
    + + +

    generated source for method statedMimeType

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + inferredMimeType(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    108    @abstractmethod
    +109    def inferredMimeType(self) -> JavaObject:
    +110        """generated source for method inferredMimeType"""
    +
    + + +

    generated source for method inferredMimeType

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + keywordCounts(self, *keywords) -> int: + + + +
    + +
    119    @abstractmethod
    +120    def keywordCounts(self, *keywords) -> int:
    +121        """generated source for method keywordCounts"""
    +
    + + +

    generated source for method keywordCounts

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + attributes(self, *types) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    130    @abstractmethod
    +131    def attributes(self, *types) -> JavaObject:
    +132        """generated source for method attributes"""
    +
    + + +

    generated source for method attributes

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + toByteArray(self) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    137    @abstractmethod
    +138    def toByteArray(self) -> IByteArray:
    +139        """generated source for method toByteArray"""
    +
    + + +

    generated source for method toByteArray

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + copyToTempFile(self) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    156    @abstractmethod
    +157    def copyToTempFile(self) -> IHttpResponse:
    +158        """generated source for method copyToTempFile"""
    +
    + + +

    generated source for method copyToTempFile

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withStatusCode( self, statusCode: int) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    167    @abstractmethod
    +168    def withStatusCode(self, statusCode: int) -> IHttpResponse:
    +169        """generated source for method withStatusCode"""
    +
    + + +

    generated source for method withStatusCode

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withReasonPhrase( self, reasonPhrase: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    178    @abstractmethod
    +179    def withReasonPhrase(self, reasonPhrase: str) -> IHttpResponse:
    +180        """generated source for method withReasonPhrase"""
    +
    + + +

    generated source for method withReasonPhrase

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withHttpVersion( self, httpVersion: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    189    @abstractmethod
    +190    def withHttpVersion(self, httpVersion: str) -> IHttpResponse:
    +191        """generated source for method withHttpVersion"""
    +
    + + +

    generated source for method withHttpVersion

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withBody( self, body: pyscalpel.java.burp.byte_array.IByteArray | str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    201    @abstractmethod
    +202    def withBody(self, body: IByteArray | str) -> IHttpResponse:
    +203        """generated source for method withBody"""
    +
    + + +

    generated source for method withBody

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withAddedHeader( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    224    @abstractmethod
    +225    def withAddedHeader(self, name: str, value: str) -> IHttpResponse:
    +226        """generated source for method withAddedHeader_0"""
    +
    + + +

    generated source for method withAddedHeader_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withUpdatedHeader( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    247    @abstractmethod
    +248    def withUpdatedHeader(self, name: str, value: str) -> IHttpResponse:
    +249        """generated source for method withUpdatedHeader_0"""
    +
    + + +

    generated source for method withUpdatedHeader_0

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withRemovedHeader( self, name: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    269    @abstractmethod
    +270    def withRemovedHeader(self, name: str) -> IHttpResponse:
    +271        """generated source for method withRemovedHeader_0"""
    +
    + + +

    generated source for method withRemovedHeader_0

    +
    + + +
    +
    + +
    + + def + withMarkers(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + httpResponse( self, response: pyscalpel.java.burp.byte_array.IByteArray | str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse: + + + +
    + +
    302    @abstractmethod
    +303    def httpResponse(self, response: IByteArray | str) -> IHttpResponse:
    +304        """generated source for method httpResponse"""
    +
    + + +

    generated source for method httpResponse

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + HttpResponse = +None + + +
    + + + + +
    +
    + +
    + + class + IHttpRequestResponse(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
    + +
    12class IHttpRequestResponse(JavaObject, Protocol, metaclass=ABCMeta):  # pragma: no cover
    +13    """generated source for interface HttpRequestResponse"""
    +14
    +15    __metaclass__ = ABCMeta
    +16
    +17    @abstractmethod
    +18    def request(self) -> IHttpRequest | None:
    +19        ...
    +20
    +21    @abstractmethod
    +22    def response(self) -> IHttpResponse | None:
    +23        ...
    +
    + + +

    generated source for interface HttpRequestResponse

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + request(self) -> pyscalpel.java.burp.http_request.IHttpRequest | None: + + + +
    + +
    17    @abstractmethod
    +18    def request(self) -> IHttpRequest | None:
    +19        ...
    +
    + + + + +
    +
    + +
    +
    @abstractmethod
    + + def + response(self) -> pyscalpel.java.burp.http_response.IHttpResponse | None: + + + +
    + +
    21    @abstractmethod
    +22    def response(self) -> IHttpResponse | None:
    +23        ...
    +
    + + + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    + +
    + + class + IHttpHeader(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
    + +
    16class IHttpHeader(JavaObject, Protocol, metaclass=ABCMeta):  # pragma: no cover
    +17    """generated source for interface HttpHeader"""
    +18
    +19    __metaclass__ = ABCMeta
    +20    #
    +21    #      * @return The name of the header.
    +22    #
    +23
    +24    @abstractmethod
    +25    def name(self) -> str:
    +26        """generated source for method name"""
    +27
    +28    #
    +29    #      * @return The value of the header.
    +30    #
    +31    @abstractmethod
    +32    def value(self) -> str:
    +33        """generated source for method value"""
    +34
    +35    #
    +36    #      * @return The {@code String} representation of the header.
    +37    #
    +38    @abstractmethod
    +39    def __str__(self):
    +40        """generated source for method toString"""
    +41
    +42    #
    +43    #      * Create a new instance of {@code HttpHeader} from name and value.
    +44    #      *
    +45    #      * @param name  The name of the header.
    +46    #      * @param value The value of the header.
    +47    #      *
    +48    #      * @return A new {@code HttpHeader} instance.
    +49    #
    +50    @abstractmethod
    +51    @overload
    +52    def httpHeader(self, name: str, value: str) -> IHttpHeader:
    +53        """generated source for method httpHeader"""
    +54
    +55    #
    +56    #      * Create a new instance of HttpHeader from a {@code String} header representation.
    +57    #      * It will be parsed according to the HTTP/1.1 specification for headers.
    +58    #      *
    +59    #      * @param header The {@code String} header representation.
    +60    #      *
    +61    #      * @return A new {@code HttpHeader} instance.
    +62    #
    +63    @abstractmethod
    +64    @overload
    +65    def httpHeader(self, header: str) -> IHttpHeader:
    +66        """generated source for method httpHeader_0"""
    +
    + + +

    generated source for interface HttpHeader

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + name(self) -> str: + + + +
    + +
    24    @abstractmethod
    +25    def name(self) -> str:
    +26        """generated source for method name"""
    +
    + + +

    generated source for method name

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + value(self) -> str: + + + +
    + +
    31    @abstractmethod
    +32    def value(self) -> str:
    +33        """generated source for method value"""
    +
    + + +

    generated source for method value

    +
    + + +
    +
    + +
    + + def + httpHeader(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + HttpHeader = +None + + +
    + + + + +
    +
    + +
    + + class + IHttpMessage(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
    + +
    15class IHttpMessage(JavaObject, Protocol):  # pragma: no cover
    +16    """generated source for interface HttpMessage"""
    +17
    +18    #
    +19    #      * HTTP headers contained in the message.
    +20    #      *
    +21    #      * @return A list of HTTP headers.
    +22    #
    +23    @abstractmethod
    +24    def headers(self) -> IHttpHeader:
    +25        """generated source for method headers"""
    +26
    +27    #
    +28    #      * Offset within the message where the message body begins.
    +29    #      *
    +30    #      * @return The message body offset.
    +31    #
    +32    @abstractmethod
    +33    def bodyOffset(self) -> int:
    +34        """generated source for method bodyOffset"""
    +35
    +36    #
    +37    #      * Body of a message as a byte array.
    +38    #      *
    +39    #      * @return The body of a message as a byte array.
    +40    #
    +41    @abstractmethod
    +42    def body(self) -> IByteArray:
    +43        """generated source for method body"""
    +44
    +45    #
    +46    #      * Body of a message as a {@code String}.
    +47    #      *
    +48    #      * @return The body of a message as a {@code String}.
    +49    #
    +50    @abstractmethod
    +51    def bodyToString(self) -> str:
    +52        """generated source for method bodyToString"""
    +53
    +54    #
    +55    #      * Markers for the message.
    +56    #      *
    +57    #      * @return A list of markers.
    +58    #
    +59    @abstractmethod
    +60    def markers(self) -> JavaObject:
    +61        """generated source for method markers"""
    +62
    +63    #
    +64    #      * Message as a byte array.
    +65    #      *
    +66    #      * @return The message as a byte array.
    +67    #
    +68    @abstractmethod
    +69    def toByteArray(self) -> IByteArray:
    +70        """generated source for method toByteArray"""
    +71
    +72    #
    +73    #      * Message as a {@code String}.
    +74    #      *
    +75    #      * @return The message as a {@code String}.
    +76    #
    +77    @abstractmethod
    +78    def __str__(self) -> str:
    +79        """generated source for method toString"""
    +
    + + +

    generated source for interface HttpMessage

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + headers(self) -> pyscalpel.java.burp.http_header.IHttpHeader: + + + +
    + +
    23    @abstractmethod
    +24    def headers(self) -> IHttpHeader:
    +25        """generated source for method headers"""
    +
    + + +

    generated source for method headers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyOffset(self) -> int: + + + +
    + +
    32    @abstractmethod
    +33    def bodyOffset(self) -> int:
    +34        """generated source for method bodyOffset"""
    +
    + + +

    generated source for method bodyOffset

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + body(self) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    41    @abstractmethod
    +42    def body(self) -> IByteArray:
    +43        """generated source for method body"""
    +
    + + +

    generated source for method body

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyToString(self) -> str: + + + +
    + +
    50    @abstractmethod
    +51    def bodyToString(self) -> str:
    +52        """generated source for method bodyToString"""
    +
    + + +

    generated source for method bodyToString

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + markers(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    59    @abstractmethod
    +60    def markers(self) -> JavaObject:
    +61        """generated source for method markers"""
    +
    + + +

    generated source for method markers

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + toByteArray(self) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    68    @abstractmethod
    +69    def toByteArray(self) -> IByteArray:
    +70        """generated source for method toByteArray"""
    +
    + + +

    generated source for method toByteArray

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    + +
    + + class + IHttpParameter(pyscalpel.java.object.JavaObject): + + + +
    + +
    15class IHttpParameter(JavaObject):  # pragma: no cover
    +16    """generated source for interface HttpParameter"""
    +17
    +18    __metaclass__ = ABCMeta
    +19    #
    +20    #      * @return The parameter type.
    +21    #
    +22
    +23    @abstractmethod
    +24    def type_(self) -> JavaObject:
    +25        """generated source for method type_"""
    +26
    +27    #
    +28    #      * @return The parameter name.
    +29    #
    +30    @abstractmethod
    +31    def name(self) -> str:
    +32        """generated source for method name"""
    +33
    +34    #
    +35    #      * @return The parameter value.
    +36    #
    +37    @abstractmethod
    +38    def value(self) -> str:
    +39        """generated source for method value"""
    +40
    +41    #
    +42    #      * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#URL} type.
    +43    #      *
    +44    #      * @param name  The parameter name.
    +45    #      * @param value The parameter value.
    +46    #      *
    +47    #      * @return A new {@code HttpParameter} instance.
    +48    #
    +49    @abstractmethod
    +50    def urlParameter(self, name: str, value: str) -> IHttpParameter:
    +51        """generated source for method urlParameter"""
    +52
    +53    #
    +54    #      * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#BODY} type.
    +55    #      *
    +56    #      * @param name  The parameter name.
    +57    #      * @param value The parameter value.
    +58    #      *
    +59    #      * @return A new {@code HttpParameter} instance.
    +60    #
    +61    @abstractmethod
    +62    def bodyParameter(self, name: str, value: str) -> IHttpParameter:
    +63        """generated source for method bodyParameter"""
    +64
    +65    #
    +66    #      * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#COOKIE} type.
    +67    #      *
    +68    #      * @param name  The parameter name.
    +69    #      * @param value The parameter value.
    +70    #      *
    +71    #      * @return A new {@code HttpParameter} instance.
    +72    #
    +73    @abstractmethod
    +74    def cookieParameter(self, name: str, value: str) -> IHttpParameter:
    +75        """generated source for method cookieParameter"""
    +76
    +77    #
    +78    #      * Create a new Instance of {@code HttpParameter} with the specified type.
    +79    #      *
    +80    #      * @param name  The parameter name.
    +81    #      * @param value The parameter value.
    +82    #      * @param type  The header type.
    +83    #      *
    +84    #      * @return A new {@code HttpParameter} instance.
    +85    #
    +86    @abstractmethod
    +87    def parameter(self, name: str, value: str, type_: JavaObject) -> IHttpParameter:
    +88        """generated source for method parameter"""
    +
    + + +

    generated source for interface HttpParameter

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + type_(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    23    @abstractmethod
    +24    def type_(self) -> JavaObject:
    +25        """generated source for method type_"""
    +
    + + +

    generated source for method type_

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + name(self) -> str: + + + +
    + +
    30    @abstractmethod
    +31    def name(self) -> str:
    +32        """generated source for method name"""
    +
    + + +

    generated source for method name

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + value(self) -> str: + + + +
    + +
    37    @abstractmethod
    +38    def value(self) -> str:
    +39        """generated source for method value"""
    +
    + + +

    generated source for method value

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + urlParameter( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter: + + + +
    + +
    49    @abstractmethod
    +50    def urlParameter(self, name: str, value: str) -> IHttpParameter:
    +51        """generated source for method urlParameter"""
    +
    + + +

    generated source for method urlParameter

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + bodyParameter( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter: + + + +
    + +
    61    @abstractmethod
    +62    def bodyParameter(self, name: str, value: str) -> IHttpParameter:
    +63        """generated source for method bodyParameter"""
    +
    + + +

    generated source for method bodyParameter

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + cookieParameter( self, name: str, value: str) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter: + + + +
    + +
    73    @abstractmethod
    +74    def cookieParameter(self, name: str, value: str) -> IHttpParameter:
    +75        """generated source for method cookieParameter"""
    +
    + + +

    generated source for method cookieParameter

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + parameter( self, name: str, value: str, type_: pyscalpel.java.object.JavaObject) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter: + + + +
    + +
    86    @abstractmethod
    +87    def parameter(self, name: str, value: str, type_: JavaObject) -> IHttpParameter:
    +88        """generated source for method parameter"""
    +
    + + +

    generated source for method parameter

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + HttpParameter = +None + + +
    + + + + +
    +
    + +
    + + class + IHttpService(pyscalpel.java.object.JavaObject): + + + +
    + +
    12class IHttpService(JavaObject, metaclass=ABCMeta):  # pragma: no cover
    +13    @abstractmethod
    +14    def host(self) -> str:
    +15        """The hostname or IP address for the service."""
    +16
    +17    @abstractmethod
    +18    @overload
    +19    def httpService(self, baseUrl: str) -> IHttpService:
    +20        """Create a new instance of {@code HttpService} from a base URL."""
    +21
    +22    @abstractmethod
    +23    @overload
    +24    def httpService(self, baseUrl: str, secure: bool) -> IHttpService:
    +25        """Create a new instance of {@code HttpService} from a base URL and a protocol."""
    +26
    +27    @abstractmethod
    +28    @overload
    +29    def httpService(self, host: str, port: int, secure: bool) -> IHttpService:
    +30        """Create a new instance of {@code HttpService} from a host, a port and a protocol."""
    +31
    +32    @abstractmethod
    +33    def httpService(self, *args, **kwargs) -> IHttpService:
    +34        """Create a new instance of {@code HttpService} from a host, a port and a protocol."""
    +35
    +36    @abstractmethod
    +37    def port(self) -> int:
    +38        """The port number for the service."""
    +39
    +40    @abstractmethod
    +41    def secure(self) -> bool:
    +42        """True if a secure protocol is used for the connection, false otherwise."""
    +43
    +44    @abstractmethod
    +45    def __str__(self) -> str:
    +46        """The {@code String} representation of the service."""
    +
    + + +

    generated source for class Object

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + host(self) -> str: + + + +
    + +
    13    @abstractmethod
    +14    def host(self) -> str:
    +15        """The hostname or IP address for the service."""
    +
    + + +

    The hostname or IP address for the service.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + httpService( self, *args, **kwargs) -> python3-10.pyscalpel.java.burp.http_service.IHttpService: + + + +
    + +
    32    @abstractmethod
    +33    def httpService(self, *args, **kwargs) -> IHttpService:
    +34        """Create a new instance of {@code HttpService} from a host, a port and a protocol."""
    +
    + + +

    Create a new instance of {@code HttpService} from a host, a port and a protocol.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + port(self) -> int: + + + +
    + +
    36    @abstractmethod
    +37    def port(self) -> int:
    +38        """The port number for the service."""
    +
    + + +

    The port number for the service.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + secure(self) -> bool: + + + +
    + +
    40    @abstractmethod
    +41    def secure(self) -> bool:
    +42        """True if a secure protocol is used for the connection, false otherwise."""
    +
    + + +

    True if a secure protocol is used for the connection, false otherwise.

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + HttpService = +None + + +
    + + + + +
    +
    + +
    + + class + IByteArray(pyscalpel.java.object.JavaObject, typing.Protocol): + + + +
    + +
     13class IByteArray(JavaObject, Protocol):  # pragma: no cover
    + 14    __metaclass__ = ABCMeta
    + 15
    + 16    """ generated source for interface ByteArray """
    + 17
    + 18    #
    + 19    #      * Access the byte stored at the provided index.
    + 20    #      *
    + 21    #      * @param index Index of the byte to be retrieved.
    + 22    #      *
    + 23    #      * @return The byte at the index.
    + 24    #
    + 25    @abstractmethod
    + 26    def getByte(self, index: int) -> int:
    + 27        """generated source for method getByte"""
    + 28
    + 29    #
    + 30    #      * Sets the byte at the provided index to the provided byte.
    + 31    #      *
    + 32    #      * @param index Index of the byte to be set.
    + 33    #      * @param value The byte to be set.
    + 34    #
    + 35    @abstractmethod
    + 36    @overload
    + 37    def setByte(self, index: int, value: int) -> None:
    + 38        """generated source for method setByte"""
    + 39
    + 40    #
    + 41    #      * Sets the byte at the provided index to the provided narrowed integer value.
    + 42    #      *
    + 43    #      * @param index Index of the byte to be set.
    + 44    #      * @param value The integer value to be set after a narrowing primitive conversion to a byte.
    + 45    #
    + 46    @abstractmethod
    + 47    @overload
    + 48    def setByte(self, index: int, value: int) -> None:
    + 49        """generated source for method setByte_0"""
    + 50
    + 51    #
    + 52    #      * Sets bytes starting at the specified index to the provided bytes.
    + 53    #      *
    + 54    #      * @param index The index of the first byte to set.
    + 55    #      * @param data  The byte[] or sequence of bytes to be set.
    + 56    #
    + 57    @abstractmethod
    + 58    @overload
    + 59    def setBytes(self, index: int, *data: int) -> None:
    + 60        """generated source for method setBytes"""
    + 61
    + 62    #
    + 63    #      * Sets bytes starting at the specified index to the provided integers after narrowing primitive conversion to bytes.
    + 64    #      *
    + 65    #      * @param index The index of the first byte to set.
    + 66    #      * @param data  The int[] or the sequence of integers to be set after a narrowing primitive conversion to bytes.
    + 67    #
    + 68
    + 69    @abstractmethod
    + 70    @overload
    + 71    def setBytes(self, index: int, byteArray: IByteArray) -> None:
    + 72        """generated source for method setBytes_1"""
    + 73
    + 74    #
    + 75    #      * Number of bytes stored in the {@code ByteArray}.
    + 76    #      *
    + 77    #      * @return Length of the {@code ByteArray}.
    + 78    #
    + 79    @abstractmethod
    + 80    def length(self) -> int:
    + 81        """generated source for method length"""
    + 82
    + 83    #
    + 84    #      * Copy of all bytes
    + 85    #      *
    + 86    #      * @return Copy of all bytes.
    + 87    #
    + 88    @abstractmethod
    + 89    def getBytes(self) -> JavaBytes:
    + 90        """generated source for method getBytes"""
    + 91
    + 92    #
    + 93    #      * New ByteArray with all bytes between the start index (inclusive) and the end index (exclusive).
    + 94    #      *
    + 95    #      * @param startIndexInclusive The inclusive start index of retrieved range.
    + 96    #      * @param endIndexExclusive   The exclusive end index of retrieved range.
    + 97    #      *
    + 98    #      * @return ByteArray containing all bytes in the specified range.
    + 99    #
    +100    @abstractmethod
    +101    @overload
    +102    def subArray(
    +103        self, startIndexInclusive: int, endIndexExclusive: int
    +104    ) -> "IByteArray":
    +105        """generated source for method subArray"""
    +106
    +107    #
    +108    #      * New ByteArray with all bytes in the specified range.
    +109    #      *
    +110    #      * @param range The {@link Range} of bytes to be returned.
    +111    #      *
    +112    #      * @return ByteArray containing all bytes in the specified range.
    +113    #
    +114    @abstractmethod
    +115    @overload
    +116    def subArray(self, _range) -> IByteArray:
    +117        """generated source for method subArray_0"""
    +118
    +119    #
    +120    #      * Create a copy of the {@code ByteArray}
    +121    #      *
    +122    #      * @return New {@code ByteArray} with a copy of the wrapped bytes.
    +123    #
    +124    @abstractmethod
    +125    def copy(self) -> IByteArray:
    +126        """generated source for method copy"""
    +127
    +128    #
    +129    #      * Create a copy of the {@code ByteArray} in temporary file.<br>
    +130    #      * This method is used to save the {@code ByteArray} object to a temporary file,
    +131    #      * so that it is no longer held in memory. Extensions can use this method to convert
    +132    #      * {@code ByteArray} objects into a form suitable for long-term usage.
    +133    #      *
    +134    #      * @return A new {@code ByteArray} instance stored in temporary file.
    +135    #
    +136    @abstractmethod
    +137    def copyToTempFile(self) -> IByteArray:
    +138        """generated source for method copyToTempFile"""
    +139
    +140    #
    +141    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
    +142    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
    +143    #      *
    +144    #      * @param searchTerm The value to be searched for.
    +145    #      *
    +146    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
    +147    #
    +148    @abstractmethod
    +149    @overload
    +150    def indexOf(self, searchTerm: IByteArray) -> int:
    +151        """generated source for method indexOf"""
    +152
    +153    #
    +154    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
    +155    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
    +156    #      *
    +157    #      * @param searchTerm The value to be searched for.
    +158    #      *
    +159    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
    +160    #
    +161    @abstractmethod
    +162    @overload
    +163    def indexOf(self, searchTerm: str) -> int:
    +164        """generated source for method indexOf_0"""
    +165
    +166    #
    +167    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
    +168    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
    +169    #      *
    +170    #      * @param searchTerm    The value to be searched for.
    +171    #      * @param caseSensitive Flags whether the search is case-sensitive.
    +172    #      *
    +173    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
    +174    #
    +175    @abstractmethod
    +176    @overload
    +177    def indexOf(self, searchTerm: IByteArray, caseSensitive: bool) -> int:
    +178        """generated source for method indexOf_1"""
    +179
    +180    #
    +181    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
    +182    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
    +183    #      *
    +184    #      * @param searchTerm    The value to be searched for.
    +185    #      * @param caseSensitive Flags whether the search is case-sensitive.
    +186    #      *
    +187    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
    +188    #
    +189    @abstractmethod
    +190    @overload
    +191    def indexOf(self, searchTerm: str, caseSensitive: bool) -> int:
    +192        """generated source for method indexOf_2"""
    +193
    +194    #
    +195    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
    +196    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
    +197    #      *
    +198    #      * @param searchTerm          The value to be searched for.
    +199    #      * @param caseSensitive       Flags whether the search is case-sensitive.
    +200    #      * @param startIndexInclusive The inclusive start index for the search.
    +201    #      * @param endIndexExclusive   The exclusive end index for the search.
    +202    #      *
    +203    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
    +204    #
    +205    @abstractmethod
    +206    @overload
    +207    def indexOf(
    +208        self,
    +209        searchTerm: IByteArray,
    +210        caseSensitive: bool,
    +211        startIndexInclusive: int,
    +212        endIndexExclusive: int,
    +213    ) -> int:
    +214        """generated source for method indexOf_3"""
    +215
    +216    #
    +217    #      * Searches the data in the ByteArray for the first occurrence of a specified term.
    +218    #      * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data.
    +219    #      *
    +220    #      * @param searchTerm          The value to be searched for.
    +221    #      * @param caseSensitive       Flags whether the search is case-sensitive.
    +222    #      * @param startIndexInclusive The inclusive start index for the search.
    +223    #      * @param endIndexExclusive   The exclusive end index for the search.
    +224    #      *
    +225    #      * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found.
    +226    #
    +227    @abstractmethod
    +228    @overload
    +229    def indexOf(
    +230        self,
    +231        searchTerm: str,
    +232        caseSensitive: bool,
    +233        startIndexInclusive: int,
    +234        endIndexExclusive: int,
    +235    ) -> int:
    +236        """generated source for method indexOf_4"""
    +237
    +238    #
    +239    #      * Searches the data in the ByteArray and counts all matches for a specified term.
    +240    #      *
    +241    #      * @param searchTerm The value to be searched for.
    +242    #      *
    +243    #      * @return The count of all matches of the pattern
    +244    #
    +245    @abstractmethod
    +246    @overload
    +247    def countMatches(self, searchTerm: IByteArray) -> int:
    +248        """generated source for method countMatches"""
    +249
    +250    #
    +251    #      * Searches the data in the ByteArray and counts all matches for a specified term.
    +252    #      *
    +253    #      * @param searchTerm The value to be searched for.
    +254    #      *
    +255    #      * @return The count of all matches of the pattern
    +256    #
    +257    @abstractmethod
    +258    @overload
    +259    def countMatches(self, searchTerm: str) -> int:
    +260        """generated source for method countMatches_0"""
    +261
    +262    #
    +263    #      * Searches the data in the ByteArray and counts all matches for a specified term.
    +264    #      *
    +265    #      * @param searchTerm    The value to be searched for.
    +266    #      * @param caseSensitive Flags whether the search is case-sensitive.
    +267    #      *
    +268    #      * @return The count of all matches of the pattern
    +269    #
    +270    @abstractmethod
    +271    @overload
    +272    def countMatches(self, searchTerm: IByteArray, caseSensitive: bool) -> int:
    +273        """generated source for method countMatches_1"""
    +274
    +275    #
    +276    #      * Searches the data in the ByteArray and counts all matches for a specified term.
    +277    #      *
    +278    #      * @param searchTerm    The value to be searched for.
    +279    #      * @param caseSensitive Flags whether the search is case-sensitive.
    +280    #      *
    +281    #      * @return The count of all matches of the pattern
    +282    #
    +283    @abstractmethod
    +284    @overload
    +285    def countMatches(self, searchTerm: str, caseSensitive: bool) -> int:
    +286        """generated source for method countMatches_2"""
    +287
    +288    #
    +289    #      * Searches the data in the ByteArray and counts all matches for a specified term.
    +290    #      *
    +291    #      * @param searchTerm          The value to be searched for.
    +292    #      * @param caseSensitive       Flags whether the search is case-sensitive.
    +293    #      * @param startIndexInclusive The inclusive start index for the search.
    +294    #      * @param endIndexExclusive   The exclusive end index for the search.
    +295    #      *
    +296    #      * @return The count of all matches of the pattern within the specified bounds
    +297    #
    +298    @abstractmethod
    +299    @overload
    +300    def countMatches(
    +301        self,
    +302        searchTerm: IByteArray,
    +303        caseSensitive: bool,
    +304        startIndexInclusive: int,
    +305        endIndexExclusive: int,
    +306    ) -> int:
    +307        """generated source for method countMatches_3"""
    +308
    +309    #
    +310    #      * Searches the data in the ByteArray and counts all matches for a specified term.
    +311    #      *
    +312    #      * @param searchTerm          The value to be searched for.
    +313    #      * @param caseSensitive       Flags whether the search is case-sensitive.
    +314    #      * @param startIndexInclusive The inclusive start index for the search.
    +315    #      * @param endIndexExclusive   The exclusive end index for the search.
    +316    #      *
    +317    #      * @return The count of all matches of the pattern within the specified bounds
    +318    #
    +319    @abstractmethod
    +320    @overload
    +321    def countMatches(
    +322        self,
    +323        searchTerm: str,
    +324        caseSensitive: bool,
    +325        startIndexInclusive: int,
    +326        endIndexExclusive: int,
    +327    ) -> int:
    +328        """generated source for method countMatches_4"""
    +329
    +330    #
    +331    #      * Convert the bytes of the ByteArray into String form using the encoding specified by Burp Suite.
    +332    #      *
    +333    #      * @return The converted data in String form.
    +334    #
    +335    @abstractmethod
    +336    def __str__(self) -> str:
    +337        """generated source for method toString"""
    +338
    +339    #
    +340    #      * Create a copy of the {@code ByteArray} appended with the provided bytes.
    +341    #      *
    +342    #      * @param data The byte[] or sequence of bytes to append.
    +343    #
    +344    @abstractmethod
    +345    def withAppended(self, *data: int) -> IByteArray:
    +346        """generated source for method withAppended"""
    +347
    +348    #
    +349    #      * Create a copy of the {@code ByteArray} appended with the provided integers after narrowing primitive conversion to bytes.
    +350    #      *
    +351    #      * @param data The int[] or sequence of integers to append after narrowing primitive conversion to bytes.
    +352    #
    +353
    +354    #
    +355    @abstractmethod
    +356    def byteArrayOfLength(self, length: int) -> IByteArray:
    +357        """generated source for method byteArrayOfLength"""
    +358
    +359    #
    +360    #      * Create a new {@code ByteArray} with the provided byte data.<br>
    +361    #      *
    +362    #      * @param data byte[] to wrap, or sequence of bytes to wrap.
    +363    #      *
    +364    #      * @return New {@code ByteArray} wrapping the provided byte array.
    +365    #
    +366    # @abstractmethod
    +367    @abstractmethod
    +368    def byteArray(self, data: bytes | JavaBytes | list[int] | str) -> IByteArray:
    +369        """generated source for method byteArray"""
    +370
    +371    #
    +372    #      * Create a new {@code ByteArray} with the provided integers after a narrowing primitive conversion to bytes.<br>
    +373    #      *
    +374    #      * @param data bytes.
    +375    #      *
    +376    #      * @return New {@code ByteArray} wrapping the provided data after a narrowing primitive conversion to bytes.
    +377    #
    +
    + + +

    generated source for class Object

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + getByte(self, index: int) -> int: + + + +
    + +
    25    @abstractmethod
    +26    def getByte(self, index: int) -> int:
    +27        """generated source for method getByte"""
    +
    + + +

    generated source for method getByte

    +
    + + +
    +
    + +
    + + def + setByte(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    + + def + setBytes(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + length(self) -> int: + + + +
    + +
    79    @abstractmethod
    +80    def length(self) -> int:
    +81        """generated source for method length"""
    +
    + + +

    generated source for method length

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + getBytes(self) -> pyscalpel.java.bytes.JavaBytes: + + + +
    + +
    88    @abstractmethod
    +89    def getBytes(self) -> JavaBytes:
    +90        """generated source for method getBytes"""
    +
    + + +

    generated source for method getBytes

    +
    + + +
    +
    + +
    + + def + subArray(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + copy(self) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    124    @abstractmethod
    +125    def copy(self) -> IByteArray:
    +126        """generated source for method copy"""
    +
    + + +

    generated source for method copy

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + copyToTempFile(self) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    136    @abstractmethod
    +137    def copyToTempFile(self) -> IByteArray:
    +138        """generated source for method copyToTempFile"""
    +
    + + +

    generated source for method copyToTempFile

    +
    + + +
    +
    + +
    + + def + indexOf(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    + + def + countMatches(*args, **kwds): + + + +
    + +
    2010def _overload_dummy(*args, **kwds):
    +2011    """Helper for @overload to raise when called."""
    +2012    raise NotImplementedError(
    +2013        "You should not call an overloaded function. "
    +2014        "A series of @overload-decorated functions "
    +2015        "outside a stub module should always be followed "
    +2016        "by an implementation that is not @overload-ed.")
    +
    + + +

    Helper for @overload to raise when called.

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + withAppended(self, *data: int) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    344    @abstractmethod
    +345    def withAppended(self, *data: int) -> IByteArray:
    +346        """generated source for method withAppended"""
    +
    + + +

    generated source for method withAppended

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + byteArrayOfLength( self, length: int) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    355    @abstractmethod
    +356    def byteArrayOfLength(self, length: int) -> IByteArray:
    +357        """generated source for method byteArrayOfLength"""
    +
    + + +

    generated source for method byteArrayOfLength

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + byteArray( self, data: bytes | pyscalpel.java.bytes.JavaBytes | list[int] | str) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    367    @abstractmethod
    +368    def byteArray(self, data: bytes | JavaBytes | list[int] | str) -> IByteArray:
    +369        """generated source for method byteArray"""
    +
    + + +

    generated source for method byteArray

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + ByteArray = +None + + +
    + + + + +
    +
    + +
    + + class + Logging(pyscalpel.java.object.JavaObject): + + + +
    + +
    12class Logging(JavaObject):  # pragma: no cover
    +13    """generated source for interface Logging"""
    +14
    +15    #
    +16    #       * Obtain the current extension's standard output
    +17    #       * stream. Extensions should write all output to this stream, allowing the
    +18    #       * Burp user to configure how that output is handled from within the UI.
    +19    #       *
    +20    #       * @return The extension's standard output stream.
    +21    #
    +22    @abstractmethod
    +23    def output(self) -> JavaObject:
    +24        """generated source for method output"""
    +25
    +26    #
    +27    #       * Obtain the current extension's standard error
    +28    #       * stream. Extensions should write all error messages to this stream,
    +29    #       * allowing the Burp user to configure how that output is handled from
    +30    #       * within the UI.
    +31    #       *
    +32    #       * @return The extension's standard error stream.
    +33    #
    +34    @abstractmethod
    +35    def error(self) -> JavaObject:
    +36        """generated source for method error"""
    +37
    +38    #
    +39    #       * This method prints a line of output to the current extension's standard
    +40    #       * output stream.
    +41    #       *
    +42    #       * @param message The message to print.
    +43    #
    +44    @abstractmethod
    +45    def logToOutput(self, message: str) -> None:
    +46        """generated source for method logToOutput"""
    +47
    +48    #
    +49    #       * This method prints a line of output to the current extension's standard
    +50    #       * error stream.
    +51    #       *
    +52    #       * @param message The message to print.
    +53    #
    +54    @abstractmethod
    +55    def error(self, message: str) -> None:
    +56        """generated source for method error"""
    +57
    +58    #
    +59    #       * This method can be used to display a debug event in the Burp Suite
    +60    #       * event log.
    +61    #       *
    +62    #       * @param message The debug message to display.
    +63    #
    +64    @abstractmethod
    +65    def raiseDebugEvent(self, message: str) -> None:
    +66        """generated source for method raiseDebugEvent"""
    +67
    +68    #
    +69    #       * This method can be used to display an informational event in the Burp
    +70    #       * Suite event log.
    +71    #       *
    +72    #       * @param message The informational message to display.
    +73    #
    +74    @abstractmethod
    +75    def raiseInfoEvent(self, message: str) -> None:
    +76        """generated source for method raiseInfoEvent"""
    +77
    +78    #
    +79    #       * This method can be used to display an error event in the Burp Suite
    +80    #       * event log.
    +81    #       *
    +82    #       * @param message The error message to display.
    +83    #
    +84    @abstractmethod
    +85    def raiseErrorEvent(self, message: str) -> None:
    +86        """generated source for method raiseErrorEvent"""
    +87
    +88    #
    +89    #       * This method can be used to display a critical event in the Burp Suite
    +90    #       * event log.
    +91    #       *
    +92    #       * @param message The critical message to display.
    +93    #
    +94    @abstractmethod
    +95    def raiseCriticalEvent(self, message: str) -> None:
    +96        """generated source for method raiseCriticalEvent"""
    +
    + + +

    generated source for interface Logging

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + output(self) -> pyscalpel.java.object.JavaObject: + + + +
    + +
    22    @abstractmethod
    +23    def output(self) -> JavaObject:
    +24        """generated source for method output"""
    +
    + + +

    generated source for method output

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + error(self, message: str) -> None: + + + +
    + +
    54    @abstractmethod
    +55    def error(self, message: str) -> None:
    +56        """generated source for method error"""
    +
    + + +

    generated source for method error

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + logToOutput(self, message: str) -> None: + + + +
    + +
    44    @abstractmethod
    +45    def logToOutput(self, message: str) -> None:
    +46        """generated source for method logToOutput"""
    +
    + + +

    generated source for method logToOutput

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + raiseDebugEvent(self, message: str) -> None: + + + +
    + +
    64    @abstractmethod
    +65    def raiseDebugEvent(self, message: str) -> None:
    +66        """generated source for method raiseDebugEvent"""
    +
    + + +

    generated source for method raiseDebugEvent

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + raiseInfoEvent(self, message: str) -> None: + + + +
    + +
    74    @abstractmethod
    +75    def raiseInfoEvent(self, message: str) -> None:
    +76        """generated source for method raiseInfoEvent"""
    +
    + + +

    generated source for method raiseInfoEvent

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + raiseErrorEvent(self, message: str) -> None: + + + +
    + +
    84    @abstractmethod
    +85    def raiseErrorEvent(self, message: str) -> None:
    +86        """generated source for method raiseErrorEvent"""
    +
    + + +

    generated source for method raiseErrorEvent

    +
    + + +
    +
    + +
    +
    @abstractmethod
    + + def + raiseCriticalEvent(self, message: str) -> None: + + + +
    + +
    94    @abstractmethod
    +95    def raiseCriticalEvent(self, message: str) -> None:
    +96        """generated source for method raiseCriticalEvent"""
    +
    + + +

    generated source for method raiseCriticalEvent

    +
    + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/java/import_java.html b/docs/public/pdoc/python3-10/pyscalpel/java/import_java.html new file mode 100644 index 00000000..9e2c96eb --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/java/import_java.html @@ -0,0 +1,328 @@ + + + + + + + python3-10.pyscalpel.java.import_java API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.java.import_java

    + + + + + + +
     1import os
    + 2
    + 3from typing import cast, Type, TypeVar
    + 4from functools import lru_cache
    + 5from sys import modules
    + 6
    + 7from pyscalpel.java.object import JavaObject
    + 8
    + 9
    +10@lru_cache
    +11def _is_pdoc() -> bool:  # pragma: no cover
    +12    return "pdoc" in modules
    +13
    +14
    +15ExpectedObject = TypeVar("ExpectedObject")
    +16
    +17
    +18def import_java(
    +19    module: str, name: str, expected_type: Type[ExpectedObject] = JavaObject
    +20) -> ExpectedObject:
    +21    """Import a Java class using Python's import mechanism.
    +22
    +23    :param module: The module to import from. (e.g. "java.lang")
    +24    :param name: The name of the class to import. (e.g. "String")
    +25    :param expected_type: The expected type of the class. (e.g. JavaObject)
    +26    :return: The imported class.
    +27    """
    +28    if _is_pdoc() or os.environ.get("_DO_NOT_IMPORT_JAVA") is not None:
    +29        return None  # type: ignore
    +30    try:  # pragma: no cover
    +31        module = __import__(module, fromlist=[name])
    +32        return getattr(module, name)
    +33    except ImportError as exc:  # pragma: no cover
    +34        raise ImportError(f"Could not import Java class {name}") from exc
    +
    + + +
    +
    + +
    + + def + import_java( module: str, name: str, expected_type: Type[~ExpectedObject] = <class 'pyscalpel.java.object.JavaObject'>) -> ~ExpectedObject: + + + +
    + +
    19def import_java(
    +20    module: str, name: str, expected_type: Type[ExpectedObject] = JavaObject
    +21) -> ExpectedObject:
    +22    """Import a Java class using Python's import mechanism.
    +23
    +24    :param module: The module to import from. (e.g. "java.lang")
    +25    :param name: The name of the class to import. (e.g. "String")
    +26    :param expected_type: The expected type of the class. (e.g. JavaObject)
    +27    :return: The imported class.
    +28    """
    +29    if _is_pdoc() or os.environ.get("_DO_NOT_IMPORT_JAVA") is not None:
    +30        return None  # type: ignore
    +31    try:  # pragma: no cover
    +32        module = __import__(module, fromlist=[name])
    +33        return getattr(module, name)
    +34    except ImportError as exc:  # pragma: no cover
    +35        raise ImportError(f"Could not import Java class {name}") from exc
    +
    + + +

    Import a Java class using Python's import mechanism.

    + +
    Parameters
    + +
      +
    • module: The module to import from. (e.g. "java.lang")
    • +
    • name: The name of the class to import. (e.g. "String")
    • +
    • expected_type: The expected type of the class. (e.g. JavaObject)
    • +
    + +
    Returns
    + +
    +

    The imported class.

    +
    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/java/scalpel_types.html b/docs/public/pdoc/python3-10/pyscalpel/java/scalpel_types.html new file mode 100644 index 00000000..2cbbba4b --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/java/scalpel_types.html @@ -0,0 +1,580 @@ + + + + + + + python3-10.pyscalpel.java.scalpel_types API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.java.scalpel_types

    + + + + + + +
    1from .context import Context
    +2from .utils import IPythonUtils, PythonUtils
    +3
    +4__all__ = ["Context", "IPythonUtils", "PythonUtils"]
    +
    + + +
    +
    + +
    + + class + Context(typing.TypedDict): + + + +
    + +
     6class Context(TypedDict):
    + 7    """Scalpel Python execution context"""
    + 8
    + 9    API: Any
    +10    """
    +11        The Burp [Montoya API]
    +12        (https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html)
    +13        root object.
    +14
    +15        Allows you to interact with Burp by directly manipulating the Java object.
    +16    
    +17    """
    +18
    +19    directory: str
    +20    """The framework directory"""
    +21
    +22    user_script: str
    +23    """The loaded script path"""
    +24
    +25    framework: str
    +26    """The framework (loader script) path"""
    +27
    +28    venv: str
    +29    """The venv the script was loaded in"""
    +
    + + +

    Scalpel Python execution context

    +
    + + +
    +
    + API: Any + + +
    + + +

    The Burp [Montoya API] +(https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html) +root object.

    + +

    Allows you to interact with Burp by directly manipulating the Java object.

    +
    + + +
    +
    +
    + directory: str + + +
    + + +

    The framework directory

    +
    + + +
    +
    +
    + user_script: str + + +
    + + +

    The loaded script path

    +
    + + +
    +
    +
    + framework: str + + +
    + + +

    The framework (loader script) path

    +
    + + +
    +
    +
    + venv: str + + +
    + + +

    The venv the script was loaded in

    +
    + + +
    +
    +
    + +
    + + class + IPythonUtils(pyscalpel.java.object.JavaObject): + + + +
    + +
    16class IPythonUtils(JavaObject):  # pragma: no cover
    +17    __metaclass__ = ABCMeta
    +18
    +19    @abstractmethod
    +20    def toPythonBytes(self, java_bytes: JavaBytes) -> list[int]:
    +21        pass
    +22
    +23    @abstractmethod
    +24    def toJavaBytes(self, python_bytes: bytes | list[int] | bytearray) -> JavaBytes:
    +25        pass
    +26
    +27    @abstractmethod
    +28    def toByteArray(self, python_bytes: bytes | list[int] | bytearray) -> IByteArray:
    +29        pass
    +30
    +31    @abstractmethod
    +32    def getClassName(self, msg: JavaObject) -> str:
    +33        pass
    +34
    +35    @abstractmethod
    +36    def updateHeader(
    +37        self, msg: RequestOrResponse, name: str, value: str
    +38    ) -> RequestOrResponse:
    +39        pass
    +
    + + +

    generated source for class Object

    +
    + + +
    + +
    +
    @abstractmethod
    + + def + toPythonBytes(self, java_bytes: pyscalpel.java.bytes.JavaBytes) -> list[int]: + + + +
    + +
    19    @abstractmethod
    +20    def toPythonBytes(self, java_bytes: JavaBytes) -> list[int]:
    +21        pass
    +
    + + + + +
    +
    + +
    +
    @abstractmethod
    + + def + toJavaBytes( self, python_bytes: bytes | list[int] | bytearray) -> pyscalpel.java.bytes.JavaBytes: + + + +
    + +
    23    @abstractmethod
    +24    def toJavaBytes(self, python_bytes: bytes | list[int] | bytearray) -> JavaBytes:
    +25        pass
    +
    + + + + +
    +
    + +
    +
    @abstractmethod
    + + def + toByteArray( self, python_bytes: bytes | list[int] | bytearray) -> pyscalpel.java.burp.byte_array.IByteArray: + + + +
    + +
    27    @abstractmethod
    +28    def toByteArray(self, python_bytes: bytes | list[int] | bytearray) -> IByteArray:
    +29        pass
    +
    + + + + +
    +
    + +
    +
    @abstractmethod
    + + def + getClassName(self, msg: pyscalpel.java.object.JavaObject) -> str: + + + +
    + +
    31    @abstractmethod
    +32    def getClassName(self, msg: JavaObject) -> str:
    +33        pass
    +
    + + + + +
    +
    + +
    +
    @abstractmethod
    + + def + updateHeader( self, msg: ~RequestOrResponse, name: str, value: str) -> ~RequestOrResponse: + + + +
    + +
    35    @abstractmethod
    +36    def updateHeader(
    +37        self, msg: RequestOrResponse, name: str, value: str
    +38    ) -> RequestOrResponse:
    +39        pass
    +
    + + + + +
    +
    +
    Inherited Members
    +
    +
    pyscalpel.java.object.JavaObject
    +
    getClass
    +
    hashCode
    +
    equals
    +
    clone
    +
    notify
    +
    notifyAll
    +
    wait
    +
    finalize
    + +
    +
    +
    +
    +
    +
    + PythonUtils = +None + + +
    + + + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/logger.html b/docs/public/pdoc/python3-10/pyscalpel/logger.html new file mode 100644 index 00000000..591c4ce3 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/logger.html @@ -0,0 +1,635 @@ + + + + + + + python3-10.pyscalpel.logger API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.logger

    + + + + + + +
     1import sys
    + 2from pyscalpel.java import import_java
    + 3
    + 4
    + 5# Define a default logger to use if for some reason the logger is not initialized
    + 6# (e.g. running the script from pdoc)
    + 7class Logger:  # pragma: no cover
    + 8    """Provides methods for logging messages to the Burp Suite output and standard streams."""
    + 9
    +10    def all(self, msg: str):
    +11        """Prints the message to the standard output
    +12
    +13        Args:
    +14            msg (str): The message to print
    +15        """
    +16        print(f"(default): {msg}")
    +17
    +18    def trace(self, msg: str):
    +19        """Prints the message to the standard output
    +20
    +21        Args:
    +22            msg (str): The message to print
    +23        """
    +24        print(f"(default): {msg}")
    +25
    +26    def debug(self, msg: str):
    +27        """Prints the message to the standard output
    +28
    +29        Args:
    +30            msg (str): The message to print
    +31        """
    +32        print(f"(default): {msg}")
    +33
    +34    def info(self, msg: str):
    +35        """Prints the message to the standard output
    +36
    +37        Args:
    +38            msg (str): The message to print
    +39        """
    +40        print(f"(default): {msg}")
    +41
    +42    def warn(self, msg: str):
    +43        """Prints the message to the standard output
    +44
    +45        Args:
    +46            msg (str): The message to print
    +47        """
    +48        print(f"(default): {msg}")
    +49
    +50    def fatal(self, msg: str):
    +51        """Prints the message to the standard output
    +52
    +53        Args:
    +54            msg (str): The message to print
    +55        """
    +56        print(f"(default): {msg}")
    +57
    +58    def error(self, msg: str):
    +59        """Prints the message to the standard error
    +60
    +61        Args:
    +62            msg (str): The message to print
    +63        """
    +64        print(f"(default): {msg}", file=sys.stderr)
    +65
    +66
    +67try:
    +68    logger: Logger = import_java("lexfo.scalpel", "ScalpelLogger", Logger)
    +69except ImportError as ex:  # pragma: no cover
    +70    logger: Logger = Logger()
    +71    logger.error("(default): Couldn't import logger")
    +72    logger.error(str(ex))
    +
    + + +
    +
    + +
    + + class + Logger: + + + +
    + +
     8class Logger:  # pragma: no cover
    + 9    """Provides methods for logging messages to the Burp Suite output and standard streams."""
    +10
    +11    def all(self, msg: str):
    +12        """Prints the message to the standard output
    +13
    +14        Args:
    +15            msg (str): The message to print
    +16        """
    +17        print(f"(default): {msg}")
    +18
    +19    def trace(self, msg: str):
    +20        """Prints the message to the standard output
    +21
    +22        Args:
    +23            msg (str): The message to print
    +24        """
    +25        print(f"(default): {msg}")
    +26
    +27    def debug(self, msg: str):
    +28        """Prints the message to the standard output
    +29
    +30        Args:
    +31            msg (str): The message to print
    +32        """
    +33        print(f"(default): {msg}")
    +34
    +35    def info(self, msg: str):
    +36        """Prints the message to the standard output
    +37
    +38        Args:
    +39            msg (str): The message to print
    +40        """
    +41        print(f"(default): {msg}")
    +42
    +43    def warn(self, msg: str):
    +44        """Prints the message to the standard output
    +45
    +46        Args:
    +47            msg (str): The message to print
    +48        """
    +49        print(f"(default): {msg}")
    +50
    +51    def fatal(self, msg: str):
    +52        """Prints the message to the standard output
    +53
    +54        Args:
    +55            msg (str): The message to print
    +56        """
    +57        print(f"(default): {msg}")
    +58
    +59    def error(self, msg: str):
    +60        """Prints the message to the standard error
    +61
    +62        Args:
    +63            msg (str): The message to print
    +64        """
    +65        print(f"(default): {msg}", file=sys.stderr)
    +
    + + +

    Provides methods for logging messages to the Burp Suite output and standard streams.

    +
    + + +
    + +
    + + def + all(self, msg: str): + + + +
    + +
    11    def all(self, msg: str):
    +12        """Prints the message to the standard output
    +13
    +14        Args:
    +15            msg (str): The message to print
    +16        """
    +17        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + trace(self, msg: str): + + + +
    + +
    19    def trace(self, msg: str):
    +20        """Prints the message to the standard output
    +21
    +22        Args:
    +23            msg (str): The message to print
    +24        """
    +25        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + debug(self, msg: str): + + + +
    + +
    27    def debug(self, msg: str):
    +28        """Prints the message to the standard output
    +29
    +30        Args:
    +31            msg (str): The message to print
    +32        """
    +33        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + info(self, msg: str): + + + +
    + +
    35    def info(self, msg: str):
    +36        """Prints the message to the standard output
    +37
    +38        Args:
    +39            msg (str): The message to print
    +40        """
    +41        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + warn(self, msg: str): + + + +
    + +
    43    def warn(self, msg: str):
    +44        """Prints the message to the standard output
    +45
    +46        Args:
    +47            msg (str): The message to print
    +48        """
    +49        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + fatal(self, msg: str): + + + +
    + +
    51    def fatal(self, msg: str):
    +52        """Prints the message to the standard output
    +53
    +54        Args:
    +55            msg (str): The message to print
    +56        """
    +57        print(f"(default): {msg}")
    +
    + + +

    Prints the message to the standard output

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    + +
    + + def + error(self, msg: str): + + + +
    + +
    59    def error(self, msg: str):
    +60        """Prints the message to the standard error
    +61
    +62        Args:
    +63            msg (str): The message to print
    +64        """
    +65        print(f"(default): {msg}", file=sys.stderr)
    +
    + + +

    Prints the message to the standard error

    + +

    Args: + msg (str): The message to print

    +
    + + +
    +
    +
    +
    + logger: python3-10.pyscalpel.logger.Logger = +None + + +
    + + + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/utils.html b/docs/public/pdoc/python3-10/pyscalpel/utils.html new file mode 100644 index 00000000..62ab0450 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/utils.html @@ -0,0 +1,405 @@ + + + + + + + python3-10.pyscalpel.utils API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.utils

    + + + + + + +
     1import inspect
    + 2from typing import TypeVar, Union
    + 3from pyscalpel.burp_utils import (
    + 4    urldecode,
    + 5    urlencode_all,
    + 6)
    + 7
    + 8
    + 9T = TypeVar("T", str, bytes)
    +10
    +11
    +12def removeprefix(s: T, prefix: Union[str, bytes]) -> T:
    +13    if isinstance(s, str) and isinstance(prefix, str):
    +14        if s.startswith(prefix):
    +15            return s[len(prefix) :]  # type: ignore
    +16    elif isinstance(s, bytes) and isinstance(prefix, bytes):
    +17        if s.startswith(prefix):
    +18            return s[len(prefix) :]  # type: ignore
    +19    return s
    +20
    +21
    +22def removesuffix(s: T, suffix: Union[str, bytes]) -> T:
    +23    if isinstance(s, str) and isinstance(suffix, str):
    +24        if s.endswith(suffix):
    +25            return s[: -len(suffix)]
    +26    elif isinstance(s, bytes) and isinstance(suffix, bytes):
    +27        if s.endswith(suffix):
    +28            return s[: -len(suffix)]
    +29    return s
    +30
    +31
    +32def current_function_name() -> str:
    +33    """Get current function name
    +34
    +35    Returns:
    +36        str: The function name
    +37    """
    +38    frame = inspect.currentframe()
    +39    if frame is None:
    +40        return ""
    +41
    +42    caller_frame = frame.f_back
    +43    if caller_frame is None:
    +44        return ""
    +45
    +46    return caller_frame.f_code.co_name
    +47
    +48
    +49def get_tab_name() -> str:
    +50    """Get current editor tab name
    +51
    +52    Returns:
    +53        str: The tab name
    +54    """
    +55    frame = inspect.currentframe()
    +56    prefixes = ("req_edit_in", "req_edit_out")
    +57
    +58    # Go to previous frame till the editor name is found
    +59    while frame is not None:
    +60        frame_name = frame.f_code.co_name
    +61        for prefix in prefixes:
    +62            if frame_name.startswith(prefix):
    +63                return removeprefix(removeprefix(frame_name, prefix), "_")
    +64
    +65        frame = frame.f_back
    +66
    +67    raise RuntimeError("get_tab_name() wasn't called from an editor callback.")
    +68
    +69
    +70__all__ = [
    +71    "urldecode",
    +72    "urlencode_all",
    +73    "current_function_name",
    +74]
    +
    + + +
    +
    + +
    + + def + urldecode(data: bytes | str, encoding='latin-1') -> bytes: + + + +
    + +
    46def urldecode(data: bytes | str, encoding="latin-1") -> bytes:
    +47    """URL Decode all bytes in the given bytes object"""
    +48    return urllibdecode(always_bytes(data, encoding))
    +
    + + +

    URL Decode all bytes in the given bytes object

    +
    + + +
    +
    + +
    + + def + urlencode_all(data: bytes | str, encoding='latin-1') -> bytes: + + + +
    + +
    41def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes:
    +42    """URL Encode all bytes in the given bytes object"""
    +43    return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding)
    +
    + + +

    URL Encode all bytes in the given bytes object

    +
    + + +
    +
    + +
    + + def + current_function_name() -> str: + + + +
    + +
    33def current_function_name() -> str:
    +34    """Get current function name
    +35
    +36    Returns:
    +37        str: The function name
    +38    """
    +39    frame = inspect.currentframe()
    +40    if frame is None:
    +41        return ""
    +42
    +43    caller_frame = frame.f_back
    +44    if caller_frame is None:
    +45        return ""
    +46
    +47    return caller_frame.f_code.co_name
    +
    + + +

    Get current function name

    + +

    Returns: + str: The function name

    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/pyscalpel/venv.html b/docs/public/pdoc/python3-10/pyscalpel/venv.html new file mode 100644 index 00000000..18c96481 --- /dev/null +++ b/docs/public/pdoc/python3-10/pyscalpel/venv.html @@ -0,0 +1,588 @@ + + + + + + + python3-10.pyscalpel.venv API documentation + + + + + + + + + +
    +
    +

    +python3-10.pyscalpel.venv

    + +

    This module provides reimplementations of Python virtual environnements scripts

    + +

    This is designed to be used internally, +but in the case where the user desires to dynamically switch venvs using this, +they should ensure the selected venv has the dependencies required by Scalpel.

    +
    + + + + + +
      1"""
    +  2This module provides reimplementations of Python virtual environnements scripts
    +  3
    +  4This is designed to be used internally, 
    +  5but in the case where the user desires to dynamically switch venvs using this,
    +  6they should ensure the selected venv has the dependencies required by Scalpel.
    +  7"""
    +  8
    +  9import os
    + 10import sys
    + 11import glob
    + 12import subprocess
    + 13
    + 14_old_prefix = sys.prefix
    + 15_old_exec_prefix = sys.exec_prefix
    + 16
    + 17# Python's virtualenv's activate/deactivate ported from the bash script to Python code.
    + 18# https://docs.python.org/3/library/venv.html#:~:text=each%20provided%20path.-,How%20venvs%20work%C2%B6,-When%20a%20Python
    + 19
    + 20# pragma: no cover
    + 21
    + 22
    + 23def deactivate() -> None:  # pragma: no cover
    + 24    """Deactivates the current virtual environment."""
    + 25    if "_OLD_VIRTUAL_PATH" in os.environ:
    + 26        os.environ["PATH"] = os.environ["_OLD_VIRTUAL_PATH"]
    + 27        del os.environ["_OLD_VIRTUAL_PATH"]
    + 28    if "_OLD_VIRTUAL_PYTHONHOME" in os.environ:
    + 29        os.environ["PYTHONHOME"] = os.environ["_OLD_VIRTUAL_PYTHONHOME"]
    + 30        del os.environ["_OLD_VIRTUAL_PYTHONHOME"]
    + 31    if "VIRTUAL_ENV" in os.environ:
    + 32        del os.environ["VIRTUAL_ENV"]
    + 33
    + 34    sys.prefix = _old_prefix
    + 35    sys.exec_prefix = _old_exec_prefix
    + 36
    + 37
    + 38def activate(path: str | None) -> None:  # pragma: no cover
    + 39    """Activates the virtual environment at the given path."""
    + 40    deactivate()
    + 41
    + 42    if path is None:
    + 43        return
    + 44
    + 45    virtual_env = os.path.abspath(path)
    + 46    os.environ["_OLD_VIRTUAL_PATH"] = os.environ.get("PATH", "")
    + 47    os.environ["VIRTUAL_ENV"] = virtual_env
    + 48
    + 49    old_pythonhome = os.environ.pop("PYTHONHOME", None)
    + 50    if old_pythonhome:
    + 51        os.environ["_OLD_VIRTUAL_PYTHONHOME"] = old_pythonhome
    + 52
    + 53    if os.name == "nt":
    + 54        site_packages_paths = os.path.join(virtual_env, "Lib", "site-packages")
    + 55    else:
    + 56        site_packages_paths = glob.glob(
    + 57            os.path.join(virtual_env, "lib", "python*", "site-packages")
    + 58        )
    + 59
    + 60    if not site_packages_paths:
    + 61        raise RuntimeError(
    + 62            f"No 'site-packages' directory found in virtual environment at {virtual_env}"
    + 63        )
    + 64
    + 65    site_packages = site_packages_paths[0]
    + 66    sys.path.insert(0, site_packages)
    + 67    sys.prefix = virtual_env
    + 68    sys.exec_prefix = virtual_env
    + 69
    + 70
    + 71def install(*packages: str) -> int:  # pragma: no cover
    + 72    """Install a Python package in the current venv.
    + 73
    + 74    Returns:
    + 75        int: The pip install command exit code.
    + 76    """
    + 77    pip = os.path.join(sys.prefix, "bin", "pip")
    + 78    return subprocess.call([pip, "install", "--require-virtualenv", "--", *packages])
    + 79
    + 80
    + 81def uninstall(*packages: str) -> int:  # pragma: no cover
    + 82    """Uninstall a Python package from the current venv.
    + 83
    + 84    Returns:
    + 85        int: The pip uninstall command exit code.
    + 86    """
    + 87    pip = os.path.join(sys.prefix, "bin", "pip")
    + 88    return subprocess.call(
    + 89        [pip, "uninstall", "--require-virtualenv", "-y", "--", *packages]
    + 90    )
    + 91
    + 92
    + 93def create(path: str) -> int:  # pragma: no cover
    + 94    """Creates a Python venv on the given path
    + 95
    + 96    Returns:
    + 97        int: The `python3 -m venv` command exit code.
    + 98    """
    + 99    return subprocess.call(["python3", "-m", "venv", "--", path])
    +100
    +101
    +102def create_default() -> str:  # pragma: no cover
    +103    """Creates a default venv in the user's home directory
    +104        Only creates it if the directory doesn't already exist
    +105
    +106    Returns:
    +107        str: The venv directory path.
    +108    """
    +109    scalpel_venv = os.path.join(os.path.expanduser("~"), ".scalpel", "venv_default")
    +110    # Don't recreate the venv if it alreay exists
    +111    if not os.path.exists(scalpel_venv):
    +112        os.makedirs(scalpel_venv, exist_ok=True)
    +113        create(scalpel_venv)
    +114    return scalpel_venv
    +
    + + +
    +
    + +
    + + def + deactivate() -> None: + + + +
    + +
    24def deactivate() -> None:  # pragma: no cover
    +25    """Deactivates the current virtual environment."""
    +26    if "_OLD_VIRTUAL_PATH" in os.environ:
    +27        os.environ["PATH"] = os.environ["_OLD_VIRTUAL_PATH"]
    +28        del os.environ["_OLD_VIRTUAL_PATH"]
    +29    if "_OLD_VIRTUAL_PYTHONHOME" in os.environ:
    +30        os.environ["PYTHONHOME"] = os.environ["_OLD_VIRTUAL_PYTHONHOME"]
    +31        del os.environ["_OLD_VIRTUAL_PYTHONHOME"]
    +32    if "VIRTUAL_ENV" in os.environ:
    +33        del os.environ["VIRTUAL_ENV"]
    +34
    +35    sys.prefix = _old_prefix
    +36    sys.exec_prefix = _old_exec_prefix
    +
    + + +

    Deactivates the current virtual environment.

    +
    + + +
    +
    + +
    + + def + activate(path: str | None) -> None: + + + +
    + +
    39def activate(path: str | None) -> None:  # pragma: no cover
    +40    """Activates the virtual environment at the given path."""
    +41    deactivate()
    +42
    +43    if path is None:
    +44        return
    +45
    +46    virtual_env = os.path.abspath(path)
    +47    os.environ["_OLD_VIRTUAL_PATH"] = os.environ.get("PATH", "")
    +48    os.environ["VIRTUAL_ENV"] = virtual_env
    +49
    +50    old_pythonhome = os.environ.pop("PYTHONHOME", None)
    +51    if old_pythonhome:
    +52        os.environ["_OLD_VIRTUAL_PYTHONHOME"] = old_pythonhome
    +53
    +54    if os.name == "nt":
    +55        site_packages_paths = os.path.join(virtual_env, "Lib", "site-packages")
    +56    else:
    +57        site_packages_paths = glob.glob(
    +58            os.path.join(virtual_env, "lib", "python*", "site-packages")
    +59        )
    +60
    +61    if not site_packages_paths:
    +62        raise RuntimeError(
    +63            f"No 'site-packages' directory found in virtual environment at {virtual_env}"
    +64        )
    +65
    +66    site_packages = site_packages_paths[0]
    +67    sys.path.insert(0, site_packages)
    +68    sys.prefix = virtual_env
    +69    sys.exec_prefix = virtual_env
    +
    + + +

    Activates the virtual environment at the given path.

    +
    + + +
    +
    + +
    + + def + install(*packages: str) -> int: + + + +
    + +
    72def install(*packages: str) -> int:  # pragma: no cover
    +73    """Install a Python package in the current venv.
    +74
    +75    Returns:
    +76        int: The pip install command exit code.
    +77    """
    +78    pip = os.path.join(sys.prefix, "bin", "pip")
    +79    return subprocess.call([pip, "install", "--require-virtualenv", "--", *packages])
    +
    + + +

    Install a Python package in the current venv.

    + +

    Returns: + int: The pip install command exit code.

    +
    + + +
    +
    + +
    + + def + uninstall(*packages: str) -> int: + + + +
    + +
    82def uninstall(*packages: str) -> int:  # pragma: no cover
    +83    """Uninstall a Python package from the current venv.
    +84
    +85    Returns:
    +86        int: The pip uninstall command exit code.
    +87    """
    +88    pip = os.path.join(sys.prefix, "bin", "pip")
    +89    return subprocess.call(
    +90        [pip, "uninstall", "--require-virtualenv", "-y", "--", *packages]
    +91    )
    +
    + + +

    Uninstall a Python package from the current venv.

    + +

    Returns: + int: The pip uninstall command exit code.

    +
    + + +
    +
    + +
    + + def + create(path: str) -> int: + + + +
    + +
     94def create(path: str) -> int:  # pragma: no cover
    + 95    """Creates a Python venv on the given path
    + 96
    + 97    Returns:
    + 98        int: The `python3 -m venv` command exit code.
    + 99    """
    +100    return subprocess.call(["python3", "-m", "venv", "--", path])
    +
    + + +

    Creates a Python venv on the given path

    + +

    Returns: + int: The python3 -m venv command exit code.

    +
    + + +
    +
    + +
    + + def + create_default() -> str: + + + +
    + +
    103def create_default() -> str:  # pragma: no cover
    +104    """Creates a default venv in the user's home directory
    +105        Only creates it if the directory doesn't already exist
    +106
    +107    Returns:
    +108        str: The venv directory path.
    +109    """
    +110    scalpel_venv = os.path.join(os.path.expanduser("~"), ".scalpel", "venv_default")
    +111    # Don't recreate the venv if it alreay exists
    +112    if not os.path.exists(scalpel_venv):
    +113        os.makedirs(scalpel_venv, exist_ok=True)
    +114        create(scalpel_venv)
    +115    return scalpel_venv
    +
    + + +

    Creates a default venv in the user's home directory + Only creates it if the directory doesn't already exist

    + +

    Returns: + str: The venv directory path.

    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/python3-10/qs.html b/docs/public/pdoc/python3-10/qs.html new file mode 100644 index 00000000..af42f1c4 --- /dev/null +++ b/docs/public/pdoc/python3-10/qs.html @@ -0,0 +1,687 @@ + + + + + + + python3-10.qs API documentation + + + + + + + + + +
    +
    +

    +python3-10.qs

    + + + + + + +
     1from .qs import *
    + 2
    + 3__all__ = [
    + 4    "list_to_dict",
    + 5    "is_valid_php_query_name",
    + 6    "merge_dict_in_list",
    + 7    "merge",
    + 8    "qs_parse",
    + 9    "build_qs",
    +10    "qs_parse_pairs",
    +11]
    +
    + + +
    +
    + +
    + + def + list_to_dict(lst: list[typing.Any]) -> dict[int, typing.Any]: + + + +
    + +
    12def list_to_dict(lst: list[Any]) -> dict[int, Any]:
    +13    """Maps a list to an equivalent dictionary
    +14
    +15    e.g: ["a","b","c"] -> {0:"a",1:"b",2:"c"}
    +16
    +17    Used to convert lists to PHP-style arrays
    +18
    +19    Args:
    +20        lst (list[Any]): The list to transform
    +21
    +22    Returns:
    +23        dict[int, Any]: The "PHP-style array" dict
    +24    """
    +25
    +26    return {i: value for i, value in enumerate(lst)}
    +
    + + +

    Maps a list to an equivalent dictionary

    + +

    e.g: ["a","b","c"] -> {0:"a",1:"b",2:"c"}

    + +

    Used to convert lists to PHP-style arrays

    + +

    Args: + lst (list[Any]): The list to transform

    + +

    Returns: + dict[int, Any]: The "PHP-style array" dict

    +
    + + +
    +
    + +
    + + def + is_valid_php_query_name(name: str) -> bool: + + + +
    + +
    29def is_valid_php_query_name(name: str) -> bool:
    +30    """
    +31    Check if a given name follows PHP query string syntax.
    +32    This implementation assumes that names will be structured like:
    +33    field
    +34    field[key]
    +35    field[key1][key2]
    +36    field[]
    +37    """
    +38    pattern = r"""
    +39    ^               # Asserts the start of the line, it means to start matching from the beginning of the string.
    +40    [^\[\]&]+       # Matches one or more characters that are not `[`, `]`, or `&`. It describes the base key.
    +41    (               # Opens a group. This group is used to match any subsequent keys within brackets.
    +42    \[              # Matches a literal `[`, which is the start of a key.
    +43    [^\[\]&]*       # Matches zero or more characters that are not `[`, `]`, or `&`, which is the content of a key.
    +44    \]              # Matches a literal `]`, which is the end of a key.
    +45    )*              # Closes the group and asserts that the group can appear zero or more times, for nested keys.
    +46    $               # Asserts the end of the line, meaning the string should end with the preceding group.
    +47    """
    +48    return bool(re.match(pattern, name, re.VERBOSE))
    +
    + + +

    Check if a given name follows PHP query string syntax. +This implementation assumes that names will be structured like: +field +field[key] +field[key1][key2] +field[]

    +
    + + +
    +
    + +
    + + def + merge_dict_in_list(source: dict, destination: list) -> list | dict: + + + +
    + +
    125def merge_dict_in_list(source: dict, destination: list) -> list | dict:
    +126    """
    +127    Merge a dictionary into a list.
    +128
    +129    Only the values of integer keys from the dictionary are merged into the list.
    +130
    +131    If the dictionary contains only integer keys, returns a merged list.
    +132    If the dictionary contains other keys as well, returns a merged dict.
    +133
    +134    Args:
    +135        source (dict): The dictionary to merge.
    +136        destination (list): The list to merge.
    +137
    +138    Returns:
    +139        list | dict: Merged data.
    +140    """
    +141    # Retain only integer keys:
    +142    int_keys = sorted([key for key in source.keys() if isinstance(key, int)])
    +143    array_values = [source[key] for key in int_keys]
    +144    merged_array = array_values + destination
    +145
    +146    if len(int_keys) == len(source.keys()):
    +147        return merged_array
    +148
    +149    return merge(source, list_to_dict(merged_array))
    +
    + + +

    Merge a dictionary into a list.

    + +

    Only the values of integer keys from the dictionary are merged into the list.

    + +

    If the dictionary contains only integer keys, returns a merged list. +If the dictionary contains other keys as well, returns a merged dict.

    + +

    Args: + source (dict): The dictionary to merge. + destination (list): The list to merge.

    + +

    Returns: + list | dict: Merged data.

    +
    + + +
    +
    + +
    + + def + merge(source: dict | list, destination: dict | list, shallow: bool = True): + + + +
    + +
    152def merge(source: dict | list, destination: dict | list, shallow: bool = True):
    +153    """
    +154    Merge the `source` and `destination`.
    +155    Performs a shallow or deep merge based on the `shallow` flag.
    +156    Args:
    +157        source (Any): The source data to merge.
    +158        destination (Any): The destination data to merge into.
    +159        shallow (bool): If True, perform a shallow merge. Defaults to True.
    +160    Returns:
    +161        Any: Merged data.
    +162    """
    +163    if not shallow:
    +164        source = deepcopy(source)
    +165        destination = deepcopy(destination)
    +166
    +167    match (source, destination):
    +168        case (list(), list()):
    +169            return source + destination
    +170        case (dict(), list()):
    +171            return merge_dict_in_list(source, destination)
    +172
    +173    items = cast(Mapping, source).items()
    +174    for key, value in items:
    +175        if isinstance(value, dict) and isinstance(destination, dict):
    +176            # get node or create one
    +177            node = destination.setdefault(key, {})
    +178            node = merge(value, node)
    +179            destination[key] = node
    +180        else:
    +181            if (
    +182                isinstance(value, list) or isinstance(value, tuple)
    +183            ) and key in destination:
    +184                value = merge(destination[key], list(value))
    +185
    +186            if isinstance(key, str) and isinstance(destination, list):
    +187                destination = list_to_dict(
    +188                    destination
    +189                )  # << WRITE TEST THAT WILL REACH THIS LINE
    +190
    +191            cast(dict, destination)[key] = value
    +192    return destination
    +
    + + +

    Merge the source and destination. +Performs a shallow or deep merge based on the shallow flag. +Args: + source (Any): The source data to merge. + destination (Any): The destination data to merge into. + shallow (bool): If True, perform a shallow merge. Defaults to True. +Returns: + Any: Merged data.

    +
    + + +
    +
    + +
    + + def + qs_parse( qs: str, keep_blank_values: bool = True, strict_parsing: bool = False) -> dict: + + + +
    + +
    195def qs_parse(
    +196    qs: str, keep_blank_values: bool = True, strict_parsing: bool = False
    +197) -> dict:
    +198    """
    +199    Parses a query string using PHP's nesting syntax, and returns a dict.
    +200
    +201    Args:
    +202        qs (str): The query string to parse.
    +203        keep_blank_values (bool): If True, includes keys with blank values. Defaults to True.
    +204        strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.
    +205
    +206    Returns:
    +207        dict: A dictionary representing the parsed query string.
    +208    """
    +209
    +210    tokens = {}
    +211    pairs = [
    +212        pair for query_segment in qs.split("&") for pair in query_segment.split(";")
    +213    ]
    +214
    +215    for name_val in pairs:
    +216        if not name_val and not strict_parsing:
    +217            continue
    +218        nv = name_val.split("=")
    +219
    +220        if len(nv) != 2:
    +221            if strict_parsing:
    +222                raise ValueError(f"Bad query field: {name_val}")
    +223            # Handle case of a control-name with no equal sign
    +224            if keep_blank_values:
    +225                nv.append("")
    +226            else:
    +227                continue
    +228
    +229        if len(nv[1]) or keep_blank_values:
    +230            _get_name_value(tokens, nv[0], nv[1], urlencoded=True)
    +231
    +232    return tokens
    +
    + + +

    Parses a query string using PHP's nesting syntax, and returns a dict.

    + +

    Args: + qs (str): The query string to parse. + keep_blank_values (bool): If True, includes keys with blank values. Defaults to True. + strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.

    + +

    Returns: + dict: A dictionary representing the parsed query string.

    +
    + + +
    +
    + +
    + + def + build_qs(query: Mapping) -> str: + + + +
    + +
    235def build_qs(query: Mapping) -> str:
    +236    """
    +237    Build a query string from a dictionary or list of 2-tuples.
    +238    Coerces data types before serialization.
    +239    Args:
    +240        query (Mapping): The query data to build the string from.
    +241    Returns:
    +242        str: A query string.
    +243    """
    +244
    +245    def dict_generator(indict, pre=None):
    +246        pre = pre[:] if pre else []
    +247        if isinstance(indict, dict):
    +248            for key, value in indict.items():
    +249                if isinstance(value, dict):
    +250                    for d in dict_generator(value, pre + [key]):
    +251                        yield d
    +252                else:
    +253                    yield pre + [key, value]
    +254        else:
    +255            yield indict
    +256
    +257    paths = [i for i in dict_generator(query)]
    +258    qs = []
    +259
    +260    for path in paths:
    +261        names = path[:-1]
    +262        value = path[-1]
    +263        s: list[str] = []
    +264        for i, n in enumerate(names):
    +265            n = f"[{n}]" if i > 0 else str(n)
    +266            s.append(n)
    +267
    +268        match value:
    +269            case list() | tuple():
    +270                for v in value:
    +271                    multi = s[:]
    +272                    if not s[-1].endswith("[]"):
    +273                        multi.append("[]")
    +274                    multi.append("=")
    +275                    # URLEncode value
    +276                    multi.append(quote_plus(str(v)))
    +277                    qs.append("".join(multi))
    +278            case _:
    +279                s.append("=")
    +280                # URLEncode value
    +281                s.append(quote_plus(str(value)))
    +282                qs.append("".join(s))
    +283
    +284    return "&".join(qs)
    +
    + + +

    Build a query string from a dictionary or list of 2-tuples. +Coerces data types before serialization. +Args: + query (Mapping): The query data to build the string from. +Returns: + str: A query string.

    +
    + + +
    +
    + +
    + + def + qs_parse_pairs( pairs: Sequence[tuple[str, str] | tuple[str]], keep_blank_values: bool = True, strict_parsing: bool = False) -> dict: + + + +
    + +
    287def qs_parse_pairs(
    +288    pairs: Sequence[tuple[str, str] | tuple[str]],
    +289    keep_blank_values: bool = True,
    +290    strict_parsing: bool = False,
    +291) -> dict:
    +292    """
    +293    Parses a list of key/value pairs and returns a dict.
    +294
    +295    Args:
    +296        pairs (list[tuple[str, str]]): The list of key/value pairs.
    +297        keep_blank_values (bool): If True, includes keys with blank values. Defaults to True.
    +298        strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.
    +299
    +300    Returns:
    +301        dict: A dictionary representing the parsed pairs.
    +302    """
    +303
    +304    tokens = {}
    +305
    +306    for name_val in pairs:
    +307        if not name_val and not strict_parsing:
    +308            continue
    +309        nv = name_val
    +310
    +311        if len(nv) != 2:
    +312            if strict_parsing:
    +313                raise ValueError(f"Bad query field: {name_val}")
    +314            # Handle case of a control-name with no equal sign
    +315            if keep_blank_values:
    +316                nv = (nv[0], "")
    +317            else:
    +318                continue
    +319
    +320        if len(nv[1]) or keep_blank_values:
    +321            _get_name_value(tokens, nv[0], nv[1], False)
    +322
    +323    return tokens
    +
    + + +

    Parses a list of key/value pairs and returns a dict.

    + +

    Args: + pairs (list[tuple[str, str]]): The list of key/value pairs. + keep_blank_values (bool): If True, includes keys with blank values. Defaults to True. + strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.

    + +

    Returns: + dict: A dictionary representing the parsed pairs.

    +
    + + +
    +
    + + \ No newline at end of file diff --git a/docs/public/pdoc/search.js b/docs/public/pdoc/search.js new file mode 100644 index 00000000..67b1f620 --- /dev/null +++ b/docs/public/pdoc/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oPython libraries bundled with Scalpel\n\n
    \n\n

    pyscalpel

    \n\n

    This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension.

    \n\n

    It provides many utilities to manipulate HTTP requests, responses and converting data.

    \n\n
    \n\n

    qs

    \n\n

    A small module to parse PHP-style query strings.

    \n\n

    Used by pyscalpel

    \n\n
    \n\n

    \u2190 Go back to the user documentation

    \n"}, {"fullname": "python3-10.pyscalpel", "modulename": "python3-10.pyscalpel", "kind": "module", "doc": "

    This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension.

    \n\n

    It provides many utilities to manipulate HTTP requests, responses and converting data.

    \n"}, {"fullname": "python3-10.pyscalpel.Request", "modulename": "python3-10.pyscalpel", "qualname": "Request", "kind": "class", "doc": "

    A \"Burp oriented\" HTTP request class

    \n\n

    This class allows to manipulate Burp requests in a Pythonic way.

    \n"}, {"fullname": "python3-10.pyscalpel.Request.__init__", "modulename": "python3-10.pyscalpel", "qualname": "Request.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tmethod: str,\tscheme: Literal['http', 'https'],\thost: str,\tport: int,\tpath: str,\thttp_version: str,\theaders: Union[pyscalpel.http.headers.Headers, tuple[tuple[bytes, bytes], ...], Iterable[tuple[bytes, bytes]]],\tauthority: str,\tcontent: bytes | None)"}, {"fullname": "python3-10.pyscalpel.Request.host", "modulename": "python3-10.pyscalpel", "qualname": "Request.host", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.port", "modulename": "python3-10.pyscalpel", "qualname": "Request.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "python3-10.pyscalpel.Request.method", "modulename": "python3-10.pyscalpel", "qualname": "Request.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.scheme", "modulename": "python3-10.pyscalpel", "qualname": "Request.scheme", "kind": "variable", "doc": "

    \n", "annotation": ": Literal['http', 'https']"}, {"fullname": "python3-10.pyscalpel.Request.authority", "modulename": "python3-10.pyscalpel", "qualname": "Request.authority", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.path", "modulename": "python3-10.pyscalpel", "qualname": "Request.path", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.http_version", "modulename": "python3-10.pyscalpel", "qualname": "Request.http_version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.update_content_length", "modulename": "python3-10.pyscalpel", "qualname": "Request.update_content_length", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "True"}, {"fullname": "python3-10.pyscalpel.Request.headers", "modulename": "python3-10.pyscalpel", "qualname": "Request.headers", "kind": "variable", "doc": "

    The request HTTP headers

    \n\n

    Returns:\n Headers: a case insensitive dict containing the HTTP headers

    \n", "annotation": ": pyscalpel.http.headers.Headers"}, {"fullname": "python3-10.pyscalpel.Request.make", "modulename": "python3-10.pyscalpel", "qualname": "Request.make", "kind": "function", "doc": "

    Create a request from an URL

    \n\n

    Args:\n method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)\n url (str): The request URL\n content (bytes | str, optional): The request content. Defaults to \"\".\n headers (Headers, optional): The request headers. Defaults to ().

    \n\n

    Returns:\n Request: The HTTP request

    \n", "signature": "(\tcls,\tmethod: str,\turl: str,\tcontent: bytes | str = '',\theaders: Union[pyscalpel.http.headers.Headers, dict[str | bytes, str | bytes], dict[str, str], dict[bytes, bytes], Iterable[tuple[bytes, bytes]]] = ()) -> pyscalpel.http.request.Request:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.from_burp", "modulename": "python3-10.pyscalpel", "qualname": "Request.from_burp", "kind": "function", "doc": "

    Construct an instance of the Request class from a Burp suite HttpRequest.

    \n\n
    Parameters
    \n\n
      \n
    • request: The Burp suite HttpRequest to convert.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Request with the same data as the Burp suite HttpRequest.

    \n
    \n", "signature": "(\tcls,\trequest: pyscalpel.java.burp.http_request.IHttpRequest,\tservice: pyscalpel.java.burp.http_service.IHttpService | None = None) -> pyscalpel.http.request.Request:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.to_burp", "modulename": "python3-10.pyscalpel", "qualname": "Request.to_burp", "kind": "function", "doc": "

    Convert the request to a Burp suite IHttpRequest.

    \n\n
    Returns
    \n\n
    \n

    The request as a Burp suite IHttpRequest.

    \n
    \n", "signature": "(self) -> pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.from_raw", "modulename": "python3-10.pyscalpel", "qualname": "Request.from_raw", "kind": "function", "doc": "

    Construct an instance of the Request class from raw bytes.

    \n\n
    Parameters
    \n\n
      \n
    • data: The raw bytes to convert.
    • \n
    • real_host: The real host to connect to.
    • \n
    • port: The port of the request.
    • \n
    • scheme: The scheme of the request.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Request with the same data as the raw bytes.

    \n
    \n", "signature": "(\tcls,\tdata: bytes | str,\treal_host: str = '',\tport: int = 0,\tscheme: Union[Literal['http'], Literal['https'], str] = 'http') -> pyscalpel.http.request.Request:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.url", "modulename": "python3-10.pyscalpel", "qualname": "Request.url", "kind": "variable", "doc": "

    The full URL string, constructed from Request.scheme,\n Request.host, Request.port and Request.path.

    \n\n

    Setting this property updates these attributes as well.

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.query", "modulename": "python3-10.pyscalpel", "qualname": "Request.query", "kind": "variable", "doc": "

    The query string parameters as a dict-like object

    \n\n

    Returns:\n QueryParamsView: The query string parameters

    \n", "annotation": ": pyscalpel.http.body.urlencoded.URLEncodedFormView"}, {"fullname": "python3-10.pyscalpel.Request.content", "modulename": "python3-10.pyscalpel", "qualname": "Request.content", "kind": "variable", "doc": "

    The request content / body as raw bytes

    \n\n

    Returns:\n bytes | None: The content if it exists

    \n", "annotation": ": bytes | None"}, {"fullname": "python3-10.pyscalpel.Request.body", "modulename": "python3-10.pyscalpel", "qualname": "Request.body", "kind": "variable", "doc": "

    Alias for content()

    \n\n

    Returns:\n bytes | None: The request body / content

    \n", "annotation": ": bytes | None"}, {"fullname": "python3-10.pyscalpel.Request.update_serializer_from_content_type", "modulename": "python3-10.pyscalpel", "qualname": "Request.update_serializer_from_content_type", "kind": "function", "doc": "

    Update the form parsing based on the given Content-Type

    \n\n

    Args:\n content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.\n fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.

    \n\n

    Raises:\n FormNotParsedException: Raised when the content-type is unknown.

    \n", "signature": "(\tself,\tcontent_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None,\tfail_silently: bool = False):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.content_type", "modulename": "python3-10.pyscalpel", "qualname": "Request.content_type", "kind": "variable", "doc": "

    The Content-Type header value.

    \n\n

    Returns:\n str | None: <=> self.headers.get(\"Content-Type\")

    \n", "annotation": ": str | None"}, {"fullname": "python3-10.pyscalpel.Request.create_defaultform", "modulename": "python3-10.pyscalpel", "qualname": "Request.create_defaultform", "kind": "function", "doc": "

    Creates the form if it doesn't exist, else returns the existing one

    \n\n

    Args:\n content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.\n update_header (bool, optional): Whether to update the header. Defaults to True.

    \n\n

    Raises:\n FormNotParsedException: Thrown when provided content-type has no implemented form-serializer\n FormNotParsedException: Thrown when the raw content could not be parsed.

    \n\n

    Returns:\n MutableMapping[Any, Any]: The mapped form.

    \n", "signature": "(\tself,\tcontent_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None,\tupdate_header: bool = True) -> MutableMapping[Any, Any]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.form", "modulename": "python3-10.pyscalpel", "qualname": "Request.form", "kind": "variable", "doc": "

    Mapping from content parsed accordingly to Content-Type

    \n\n

    Raises:\n FormNotParsedException: The content could not be parsed accordingly to Content-Type

    \n\n

    Returns:\n MutableMapping[Any, Any]: The mapped request form

    \n", "annotation": ": MutableMapping[Any, Any]"}, {"fullname": "python3-10.pyscalpel.Request.urlencoded_form", "modulename": "python3-10.pyscalpel", "qualname": "Request.urlencoded_form", "kind": "variable", "doc": "

    The urlencoded form data

    \n\n

    Converts the content to the urlencoded form format if needed.\nModification to this object will update Request.content and vice versa

    \n\n

    Returns:\n QueryParams: The urlencoded form data

    \n", "annotation": ": pyscalpel.http.body.urlencoded.URLEncodedForm"}, {"fullname": "python3-10.pyscalpel.Request.json_form", "modulename": "python3-10.pyscalpel", "qualname": "Request.json_form", "kind": "variable", "doc": "

    The JSON form data

    \n\n

    Converts the content to the JSON form format if needed.\nModification to this object will update Request.content and vice versa

    \n\n

    Returns:\n dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data

    \n", "annotation": ": dict[str | int | float, str | int | float | bool | None | list[str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]] | dict[str | int | float, str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]]]"}, {"fullname": "python3-10.pyscalpel.Request.multipart_form", "modulename": "python3-10.pyscalpel", "qualname": "Request.multipart_form", "kind": "variable", "doc": "

    The multipart form data

    \n\n

    Converts the content to the multipart form format if needed.\nModification to this object will update Request.content and vice versa

    \n\n

    Returns:\n MultiPartForm

    \n", "annotation": ": pyscalpel.http.body.multipart.MultiPartForm"}, {"fullname": "python3-10.pyscalpel.Request.cookies", "modulename": "python3-10.pyscalpel", "qualname": "Request.cookies", "kind": "variable", "doc": "

    The request cookies.\nFor the most part, this behaves like a dictionary.\nModifications to the MultiDictView update Request.headers, and vice versa.

    \n", "annotation": ": _internal_mitmproxy.coretypes.multidict.MultiDictView[str, str]"}, {"fullname": "python3-10.pyscalpel.Request.host_header", "modulename": "python3-10.pyscalpel", "qualname": "Request.host_header", "kind": "variable", "doc": "

    Host header value

    \n\n

    Returns:\n str | None: The host header value

    \n", "annotation": ": str | None"}, {"fullname": "python3-10.pyscalpel.Request.text", "modulename": "python3-10.pyscalpel", "qualname": "Request.text", "kind": "function", "doc": "

    The decoded content

    \n\n

    Args:\n encoding (str, optional): encoding to use. Defaults to \"utf-8\".

    \n\n

    Returns:\n str: The decoded content

    \n", "signature": "(self, encoding='utf-8') -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.content_length", "modulename": "python3-10.pyscalpel", "qualname": "Request.content_length", "kind": "variable", "doc": "

    Returns the Content-Length header value\n Returns 0 if the header is absent

    \n\n

    Args:\n value (int | str): The Content-Length value

    \n\n

    Raises:\n RuntimeError: Throws RuntimeError when the value is invalid

    \n", "annotation": ": int"}, {"fullname": "python3-10.pyscalpel.Request.pretty_host", "modulename": "python3-10.pyscalpel", "qualname": "Request.pretty_host", "kind": "variable", "doc": "

    Returns the most approriate host\nReturns self.host when it exists, else it returns self.host_header

    \n\n

    Returns:\n str: The request target host

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Request.host_is", "modulename": "python3-10.pyscalpel", "qualname": "Request.host_is", "kind": "function", "doc": "

    Perform wildcard matching (fnmatch) on the target host.

    \n\n

    Args:\n pattern (str): The pattern to use

    \n\n

    Returns:\n bool: Whether the pattern matches

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Request.path_is", "modulename": "python3-10.pyscalpel", "qualname": "Request.path_is", "kind": "function", "doc": "

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response", "modulename": "python3-10.pyscalpel", "qualname": "Response", "kind": "class", "doc": "

    A \"Burp oriented\" HTTP response class

    \n\n

    This class allows to manipulate Burp responses in a Pythonic way.

    \n\n

    Fields:\n scheme: http or https\n host: The initiating request target host\n port: The initiating request target port\n request: The initiating request.

    \n", "bases": "_internal_mitmproxy.http.Response"}, {"fullname": "python3-10.pyscalpel.Response.__init__", "modulename": "python3-10.pyscalpel", "qualname": "Response.__init__", "kind": "function", "doc": "

    \n", "signature": "(\thttp_version: bytes,\tstatus_code: int,\treason: bytes,\theaders: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...],\tcontent: bytes | None,\ttrailers: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...] | None,\tscheme: Literal['http', 'https'] = 'http',\thost: str = '',\tport: int = 0)"}, {"fullname": "python3-10.pyscalpel.Response.scheme", "modulename": "python3-10.pyscalpel", "qualname": "Response.scheme", "kind": "variable", "doc": "

    \n", "annotation": ": Literal['http', 'https']", "default_value": "'http'"}, {"fullname": "python3-10.pyscalpel.Response.host", "modulename": "python3-10.pyscalpel", "qualname": "Response.host", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "''"}, {"fullname": "python3-10.pyscalpel.Response.port", "modulename": "python3-10.pyscalpel", "qualname": "Response.port", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "0"}, {"fullname": "python3-10.pyscalpel.Response.request", "modulename": "python3-10.pyscalpel", "qualname": "Response.request", "kind": "variable", "doc": "

    \n", "annotation": ": pyscalpel.http.request.Request | None", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.Response.from_mitmproxy", "modulename": "python3-10.pyscalpel", "qualname": "Response.from_mitmproxy", "kind": "function", "doc": "

    Construct an instance of the Response class from a mitmproxy.http.HTTPResponse.

    \n\n
    Parameters
    \n\n\n\n
    Returns
    \n\n
    \n

    A Response with the same data as the mitmproxy.http.HTTPResponse.

    \n
    \n", "signature": "(\tcls,\tresponse: _internal_mitmproxy.http.Response) -> pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response.from_burp", "modulename": "python3-10.pyscalpel", "qualname": "Response.from_burp", "kind": "function", "doc": "

    Construct an instance of the Response class from a Burp suite IHttpResponse.

    \n", "signature": "(\tcls,\tresponse: pyscalpel.java.burp.http_response.IHttpResponse,\tservice: pyscalpel.java.burp.http_service.IHttpService | None = None,\trequest: pyscalpel.java.burp.http_request.IHttpRequest | None = None) -> pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response.to_burp", "modulename": "python3-10.pyscalpel", "qualname": "Response.to_burp", "kind": "function", "doc": "

    Convert the response to a Burp suite IHttpResponse.

    \n", "signature": "(self) -> pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response.from_raw", "modulename": "python3-10.pyscalpel", "qualname": "Response.from_raw", "kind": "function", "doc": "

    Construct an instance of the Response class from raw bytes.

    \n\n
    Parameters
    \n\n
      \n
    • data: The raw bytes to convert.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Response parsed from the raw bytes.

    \n
    \n", "signature": "(cls, data: bytes | str) -> pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response.make", "modulename": "python3-10.pyscalpel", "qualname": "Response.make", "kind": "function", "doc": "

    Simplified API for creating response objects.

    \n", "signature": "(\tcls,\tstatus_code: int = 200,\tcontent: bytes | str = b'',\theaders: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...] = (),\thost: str = '',\tport: int = 0,\tscheme: Literal['http', 'https'] = 'http') -> pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response.host_is", "modulename": "python3-10.pyscalpel", "qualname": "Response.host_is", "kind": "function", "doc": "

    Matches the host against the provided patterns

    \n\n

    Returns:\n bool: Whether at least one pattern matched

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Response.body", "modulename": "python3-10.pyscalpel", "qualname": "Response.body", "kind": "variable", "doc": "

    Alias for content()

    \n\n

    Returns:\n bytes | None: The request body / content

    \n", "annotation": ": bytes | None"}, {"fullname": "python3-10.pyscalpel.Flow", "modulename": "python3-10.pyscalpel", "qualname": "Flow", "kind": "class", "doc": "

    Contains request and response and some utilities for match()

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.__init__", "modulename": "python3-10.pyscalpel", "qualname": "Flow.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tscheme: Literal['http', 'https'] = 'http',\thost: str = '',\tport: int = 0,\trequest: pyscalpel.http.request.Request | None = None,\tresponse: pyscalpel.http.response.Response | None = None,\ttext: bytes | None = None)"}, {"fullname": "python3-10.pyscalpel.Flow.scheme", "modulename": "python3-10.pyscalpel", "qualname": "Flow.scheme", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.host", "modulename": "python3-10.pyscalpel", "qualname": "Flow.host", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.port", "modulename": "python3-10.pyscalpel", "qualname": "Flow.port", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.request", "modulename": "python3-10.pyscalpel", "qualname": "Flow.request", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.response", "modulename": "python3-10.pyscalpel", "qualname": "Flow.response", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.text", "modulename": "python3-10.pyscalpel", "qualname": "Flow.text", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.Flow.host_is", "modulename": "python3-10.pyscalpel", "qualname": "Flow.host_is", "kind": "function", "doc": "

    Matches a wildcard pattern against the target host

    \n\n

    Returns:\n bool: True if at least one pattern matched

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Flow.path_is", "modulename": "python3-10.pyscalpel", "qualname": "Flow.path_is", "kind": "function", "doc": "

    Matches a wildcard pattern against the request path

    \n\n

    Includes query string ? and fragment #

    \n\n

    Returns:\n bool: True if at least one pattern matched

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.ctx", "modulename": "python3-10.pyscalpel", "qualname": "ctx", "kind": "variable", "doc": "

    The Scalpel Python execution context

    \n\n

    Contains the Burp Java API object, the venv directory, the user script path,\nthe path to the file loading the user script and a logging object

    \n", "annotation": ": pyscalpel.java.scalpel_types.context.Context", "default_value": "{}"}, {"fullname": "python3-10.pyscalpel.Context", "modulename": "python3-10.pyscalpel", "qualname": "Context", "kind": "class", "doc": "

    Scalpel Python execution context

    \n", "bases": "typing.TypedDict"}, {"fullname": "python3-10.pyscalpel.Context.API", "modulename": "python3-10.pyscalpel", "qualname": "Context.API", "kind": "variable", "doc": "

    The Burp [Montoya API]\n(https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html)\nroot object.

    \n\n

    Allows you to interact with Burp by directly manipulating the Java object.

    \n", "annotation": ": Any"}, {"fullname": "python3-10.pyscalpel.Context.directory", "modulename": "python3-10.pyscalpel", "qualname": "Context.directory", "kind": "variable", "doc": "

    The framework directory

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Context.user_script", "modulename": "python3-10.pyscalpel", "qualname": "Context.user_script", "kind": "variable", "doc": "

    The loaded script path

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Context.framework", "modulename": "python3-10.pyscalpel", "qualname": "Context.framework", "kind": "variable", "doc": "

    The framework (loader script) path

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.Context.venv", "modulename": "python3-10.pyscalpel", "qualname": "Context.venv", "kind": "variable", "doc": "

    The venv the script was loaded in

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.MatchEvent", "modulename": "python3-10.pyscalpel", "qualname": "MatchEvent", "kind": "variable", "doc": "

    \n", "default_value": "typing.Literal['request', 'response', 'req_edit_in', 'req_edit_out', 'res_edit_in', 'res_edit_out']"}, {"fullname": "python3-10.pyscalpel.editor", "modulename": "python3-10.pyscalpel", "qualname": "editor", "kind": "function", "doc": "

    Decorator to specify the editor type for a given hook

    \n\n

    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp

    \n\n

    Example:

    \n\n
    \n
        @editor("hex")\n    def req_edit_in(req: Request) -> bytes | None:\n        return bytes(req)\n
    \n
    \n\n

    This displays the request in an hex editor.

    \n\n

    Currently, the only modes supported are \"raw\", \"hex\", \"octal\", \"binary\" and \"decimal\".

    \n\n

    Args:\n mode (EDITOR_MODE): The editor mode (raw, hex,...)

    \n", "signature": "(mode: Literal['raw', 'hex', 'octal', 'binary', 'decimal']):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger", "modulename": "python3-10.pyscalpel", "qualname": "Logger", "kind": "class", "doc": "

    Provides methods for logging messages to the Burp Suite output and standard streams.

    \n"}, {"fullname": "python3-10.pyscalpel.Logger.all", "modulename": "python3-10.pyscalpel", "qualname": "Logger.all", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger.trace", "modulename": "python3-10.pyscalpel", "qualname": "Logger.trace", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger.debug", "modulename": "python3-10.pyscalpel", "qualname": "Logger.debug", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger.info", "modulename": "python3-10.pyscalpel", "qualname": "Logger.info", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger.warn", "modulename": "python3-10.pyscalpel", "qualname": "Logger.warn", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger.fatal", "modulename": "python3-10.pyscalpel", "qualname": "Logger.fatal", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.Logger.error", "modulename": "python3-10.pyscalpel", "qualname": "Logger.error", "kind": "function", "doc": "

    Prints the message to the standard error

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.burp_utils", "modulename": "python3-10.pyscalpel.burp_utils", "kind": "module", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.burp_utils.ctx", "modulename": "python3-10.pyscalpel.burp_utils", "qualname": "ctx", "kind": "variable", "doc": "

    \n", "default_value": "{}"}, {"fullname": "python3-10.pyscalpel.burp_utils.new_response", "modulename": "python3-10.pyscalpel.burp_utils", "qualname": "new_response", "kind": "function", "doc": "

    Create a new HttpResponse from the given bytes

    \n", "signature": "(\tobj: ~ByteArrayConvertible) -> pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.burp_utils.new_request", "modulename": "python3-10.pyscalpel.burp_utils", "qualname": "new_request", "kind": "function", "doc": "

    Create a new HttpRequest from the given bytes

    \n", "signature": "(\tobj: ~ByteArrayConvertible) -> pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.burp_utils.byte_array", "modulename": "python3-10.pyscalpel.burp_utils", "qualname": "byte_array", "kind": "function", "doc": "

    Create a new IByteArray from the given bytes-like obbject

    \n", "signature": "(\t_bytes: bytes | pyscalpel.java.bytes.JavaBytes | list[int] | bytearray) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.burp_utils.get_bytes", "modulename": "python3-10.pyscalpel.burp_utils", "qualname": "get_bytes", "kind": "function", "doc": "

    \n", "signature": "(array: pyscalpel.java.burp.byte_array.IByteArray) -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.burp_utils.to_bytes", "modulename": "python3-10.pyscalpel.burp_utils", "qualname": "to_bytes", "kind": "function", "doc": "

    \n", "signature": "(\tobj: Union[~ByteArraySerialisable, pyscalpel.java.bytes.JavaBytes]) -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.edit", "modulename": "python3-10.pyscalpel.edit", "kind": "module", "doc": "

    Scalpel allows choosing between normal and binary editors,\nto do so, the user can apply the editor decorator to the req_edit_in / res_edit_int hook:

    \n"}, {"fullname": "python3-10.pyscalpel.edit.EditorMode", "modulename": "python3-10.pyscalpel.edit", "qualname": "EditorMode", "kind": "variable", "doc": "

    \n", "default_value": "typing.Literal['raw', 'hex', 'octal', 'binary', 'decimal']"}, {"fullname": "python3-10.pyscalpel.edit.EDITOR_MODES", "modulename": "python3-10.pyscalpel.edit", "qualname": "EDITOR_MODES", "kind": "variable", "doc": "

    \n", "annotation": ": set[typing.Literal['raw', 'hex', 'octal', 'binary', 'decimal']]", "default_value": "{'raw', 'binary', 'octal', 'hex', 'decimal'}"}, {"fullname": "python3-10.pyscalpel.edit.editor", "modulename": "python3-10.pyscalpel.edit", "qualname": "editor", "kind": "function", "doc": "

    Decorator to specify the editor type for a given hook

    \n\n

    This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp

    \n\n

    Example:

    \n\n
    \n
        @editor("hex")\n    def req_edit_in(req: Request) -> bytes | None:\n        return bytes(req)\n
    \n
    \n\n

    This displays the request in an hex editor.

    \n\n

    Currently, the only modes supported are \"raw\", \"hex\", \"octal\", \"binary\" and \"decimal\".

    \n\n

    Args:\n mode (EDITOR_MODE): The editor mode (raw, hex,...)

    \n", "signature": "(mode: Literal['raw', 'hex', 'octal', 'binary', 'decimal']):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.encoding", "modulename": "python3-10.pyscalpel.encoding", "kind": "module", "doc": "

    Utilities for encoding data.

    \n"}, {"fullname": "python3-10.pyscalpel.encoding.always_bytes", "modulename": "python3-10.pyscalpel.encoding", "qualname": "always_bytes", "kind": "function", "doc": "

    Convert data to bytes

    \n\n

    Args:\n data (str | bytes | int): The data to convert

    \n\n

    Returns:\n bytes: The converted bytes

    \n", "signature": "(data: str | bytes | int, encoding='latin-1') -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.encoding.always_str", "modulename": "python3-10.pyscalpel.encoding", "qualname": "always_str", "kind": "function", "doc": "

    Convert data to string

    \n\n

    Args:\n data (str | bytes | int): The data to convert

    \n\n

    Returns:\n str: The converted string

    \n", "signature": "(data: str | bytes | int, encoding='latin-1') -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.encoding.urlencode_all", "modulename": "python3-10.pyscalpel.encoding", "qualname": "urlencode_all", "kind": "function", "doc": "

    URL Encode all bytes in the given bytes object

    \n", "signature": "(data: bytes | str, encoding='latin-1') -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.encoding.urldecode", "modulename": "python3-10.pyscalpel.encoding", "qualname": "urldecode", "kind": "function", "doc": "

    URL Decode all bytes in the given bytes object

    \n", "signature": "(data: bytes | str, encoding='latin-1') -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http", "modulename": "python3-10.pyscalpel.http", "kind": "module", "doc": "

    This module contains objects representing HTTP objects passed to the user's hooks

    \n"}, {"fullname": "python3-10.pyscalpel.http.Request", "modulename": "python3-10.pyscalpel.http", "qualname": "Request", "kind": "class", "doc": "

    A \"Burp oriented\" HTTP request class

    \n\n

    This class allows to manipulate Burp requests in a Pythonic way.

    \n"}, {"fullname": "python3-10.pyscalpel.http.Request.__init__", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tmethod: str,\tscheme: Literal['http', 'https'],\thost: str,\tport: int,\tpath: str,\thttp_version: str,\theaders: Union[pyscalpel.http.headers.Headers, tuple[tuple[bytes, bytes], ...], Iterable[tuple[bytes, bytes]]],\tauthority: str,\tcontent: bytes | None)"}, {"fullname": "python3-10.pyscalpel.http.Request.host", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.host", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.port", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.port", "kind": "variable", "doc": "

    \n", "annotation": ": int"}, {"fullname": "python3-10.pyscalpel.http.Request.method", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.method", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.scheme", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.scheme", "kind": "variable", "doc": "

    \n", "annotation": ": Literal['http', 'https']"}, {"fullname": "python3-10.pyscalpel.http.Request.authority", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.authority", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.path", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.path", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.http_version", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.http_version", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.update_content_length", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.update_content_length", "kind": "variable", "doc": "

    \n", "annotation": ": bool", "default_value": "True"}, {"fullname": "python3-10.pyscalpel.http.Request.headers", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.headers", "kind": "variable", "doc": "

    The request HTTP headers

    \n\n

    Returns:\n Headers: a case insensitive dict containing the HTTP headers

    \n", "annotation": ": pyscalpel.http.headers.Headers"}, {"fullname": "python3-10.pyscalpel.http.Request.make", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.make", "kind": "function", "doc": "

    Create a request from an URL

    \n\n

    Args:\n method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...)\n url (str): The request URL\n content (bytes | str, optional): The request content. Defaults to \"\".\n headers (Headers, optional): The request headers. Defaults to ().

    \n\n

    Returns:\n Request: The HTTP request

    \n", "signature": "(\tcls,\tmethod: str,\turl: str,\tcontent: bytes | str = '',\theaders: Union[pyscalpel.http.headers.Headers, dict[str | bytes, str | bytes], dict[str, str], dict[bytes, bytes], Iterable[tuple[bytes, bytes]]] = ()) -> python3-10.pyscalpel.http.request.Request:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.from_burp", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.from_burp", "kind": "function", "doc": "

    Construct an instance of the Request class from a Burp suite HttpRequest.

    \n\n
    Parameters
    \n\n
      \n
    • request: The Burp suite HttpRequest to convert.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Request with the same data as the Burp suite HttpRequest.

    \n
    \n", "signature": "(\tcls,\trequest: pyscalpel.java.burp.http_request.IHttpRequest,\tservice: pyscalpel.java.burp.http_service.IHttpService | None = None) -> python3-10.pyscalpel.http.request.Request:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.to_burp", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.to_burp", "kind": "function", "doc": "

    Convert the request to a Burp suite IHttpRequest.

    \n\n
    Returns
    \n\n
    \n

    The request as a Burp suite IHttpRequest.

    \n
    \n", "signature": "(self) -> pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.from_raw", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.from_raw", "kind": "function", "doc": "

    Construct an instance of the Request class from raw bytes.

    \n\n
    Parameters
    \n\n
      \n
    • data: The raw bytes to convert.
    • \n
    • real_host: The real host to connect to.
    • \n
    • port: The port of the request.
    • \n
    • scheme: The scheme of the request.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Request with the same data as the raw bytes.

    \n
    \n", "signature": "(\tcls,\tdata: bytes | str,\treal_host: str = '',\tport: int = 0,\tscheme: Union[Literal['http'], Literal['https'], str] = 'http') -> python3-10.pyscalpel.http.request.Request:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.url", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.url", "kind": "variable", "doc": "

    The full URL string, constructed from Request.scheme,\n Request.host, Request.port and Request.path.

    \n\n

    Setting this property updates these attributes as well.

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.query", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.query", "kind": "variable", "doc": "

    The query string parameters as a dict-like object

    \n\n

    Returns:\n QueryParamsView: The query string parameters

    \n", "annotation": ": pyscalpel.http.body.urlencoded.URLEncodedFormView"}, {"fullname": "python3-10.pyscalpel.http.Request.content", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.content", "kind": "variable", "doc": "

    The request content / body as raw bytes

    \n\n

    Returns:\n bytes | None: The content if it exists

    \n", "annotation": ": bytes | None"}, {"fullname": "python3-10.pyscalpel.http.Request.body", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.body", "kind": "variable", "doc": "

    Alias for content()

    \n\n

    Returns:\n bytes | None: The request body / content

    \n", "annotation": ": bytes | None"}, {"fullname": "python3-10.pyscalpel.http.Request.update_serializer_from_content_type", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.update_serializer_from_content_type", "kind": "function", "doc": "

    Update the form parsing based on the given Content-Type

    \n\n

    Args:\n content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None.\n fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False.

    \n\n

    Raises:\n FormNotParsedException: Raised when the content-type is unknown.

    \n", "signature": "(\tself,\tcontent_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None,\tfail_silently: bool = False):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.content_type", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.content_type", "kind": "variable", "doc": "

    The Content-Type header value.

    \n\n

    Returns:\n str | None: <=> self.headers.get(\"Content-Type\")

    \n", "annotation": ": str | None"}, {"fullname": "python3-10.pyscalpel.http.Request.create_defaultform", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.create_defaultform", "kind": "function", "doc": "

    Creates the form if it doesn't exist, else returns the existing one

    \n\n

    Args:\n content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None.\n update_header (bool, optional): Whether to update the header. Defaults to True.

    \n\n

    Raises:\n FormNotParsedException: Thrown when provided content-type has no implemented form-serializer\n FormNotParsedException: Thrown when the raw content could not be parsed.

    \n\n

    Returns:\n MutableMapping[Any, Any]: The mapped form.

    \n", "signature": "(\tself,\tcontent_type: Union[Literal['application/x-www-form-urlencoded', 'application/json', 'multipart/form-data'], str, NoneType] = None,\tupdate_header: bool = True) -> MutableMapping[Any, Any]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.form", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.form", "kind": "variable", "doc": "

    Mapping from content parsed accordingly to Content-Type

    \n\n

    Raises:\n FormNotParsedException: The content could not be parsed accordingly to Content-Type

    \n\n

    Returns:\n MutableMapping[Any, Any]: The mapped request form

    \n", "annotation": ": MutableMapping[Any, Any]"}, {"fullname": "python3-10.pyscalpel.http.Request.urlencoded_form", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.urlencoded_form", "kind": "variable", "doc": "

    The urlencoded form data

    \n\n

    Converts the content to the urlencoded form format if needed.\nModification to this object will update Request.content and vice versa

    \n\n

    Returns:\n QueryParams: The urlencoded form data

    \n", "annotation": ": pyscalpel.http.body.urlencoded.URLEncodedForm"}, {"fullname": "python3-10.pyscalpel.http.Request.json_form", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.json_form", "kind": "variable", "doc": "

    The JSON form data

    \n\n

    Converts the content to the JSON form format if needed.\nModification to this object will update Request.content and vice versa

    \n\n

    Returns:\n dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data

    \n", "annotation": ": dict[str | int | float, str | int | float | bool | None | list[str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]] | dict[str | int | float, str | int | float | bool | None | list[ForwardRef('JSON_VALUE_TYPES')] | dict[str | int | float, ForwardRef('JSON_VALUE_TYPES')]]]"}, {"fullname": "python3-10.pyscalpel.http.Request.multipart_form", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.multipart_form", "kind": "variable", "doc": "

    The multipart form data

    \n\n

    Converts the content to the multipart form format if needed.\nModification to this object will update Request.content and vice versa

    \n\n

    Returns:\n MultiPartForm

    \n", "annotation": ": pyscalpel.http.body.multipart.MultiPartForm"}, {"fullname": "python3-10.pyscalpel.http.Request.cookies", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.cookies", "kind": "variable", "doc": "

    The request cookies.\nFor the most part, this behaves like a dictionary.\nModifications to the MultiDictView update Request.headers, and vice versa.

    \n", "annotation": ": _internal_mitmproxy.coretypes.multidict.MultiDictView[str, str]"}, {"fullname": "python3-10.pyscalpel.http.Request.host_header", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.host_header", "kind": "variable", "doc": "

    Host header value

    \n\n

    Returns:\n str | None: The host header value

    \n", "annotation": ": str | None"}, {"fullname": "python3-10.pyscalpel.http.Request.text", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.text", "kind": "function", "doc": "

    The decoded content

    \n\n

    Args:\n encoding (str, optional): encoding to use. Defaults to \"utf-8\".

    \n\n

    Returns:\n str: The decoded content

    \n", "signature": "(self, encoding='utf-8') -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.content_length", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.content_length", "kind": "variable", "doc": "

    Returns the Content-Length header value\n Returns 0 if the header is absent

    \n\n

    Args:\n value (int | str): The Content-Length value

    \n\n

    Raises:\n RuntimeError: Throws RuntimeError when the value is invalid

    \n", "annotation": ": int"}, {"fullname": "python3-10.pyscalpel.http.Request.pretty_host", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.pretty_host", "kind": "variable", "doc": "

    Returns the most approriate host\nReturns self.host when it exists, else it returns self.host_header

    \n\n

    Returns:\n str: The request target host

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.Request.host_is", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.host_is", "kind": "function", "doc": "

    Perform wildcard matching (fnmatch) on the target host.

    \n\n

    Args:\n pattern (str): The pattern to use

    \n\n

    Returns:\n bool: Whether the pattern matches

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Request.path_is", "modulename": "python3-10.pyscalpel.http", "qualname": "Request.path_is", "kind": "function", "doc": "

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response", "modulename": "python3-10.pyscalpel.http", "qualname": "Response", "kind": "class", "doc": "

    A \"Burp oriented\" HTTP response class

    \n\n

    This class allows to manipulate Burp responses in a Pythonic way.

    \n\n

    Fields:\n scheme: http or https\n host: The initiating request target host\n port: The initiating request target port\n request: The initiating request.

    \n", "bases": "_internal_mitmproxy.http.Response"}, {"fullname": "python3-10.pyscalpel.http.Response.__init__", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.__init__", "kind": "function", "doc": "

    \n", "signature": "(\thttp_version: bytes,\tstatus_code: int,\treason: bytes,\theaders: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...],\tcontent: bytes | None,\ttrailers: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...] | None,\tscheme: Literal['http', 'https'] = 'http',\thost: str = '',\tport: int = 0)"}, {"fullname": "python3-10.pyscalpel.http.Response.scheme", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.scheme", "kind": "variable", "doc": "

    \n", "annotation": ": Literal['http', 'https']", "default_value": "'http'"}, {"fullname": "python3-10.pyscalpel.http.Response.host", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.host", "kind": "variable", "doc": "

    \n", "annotation": ": str", "default_value": "''"}, {"fullname": "python3-10.pyscalpel.http.Response.port", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.port", "kind": "variable", "doc": "

    \n", "annotation": ": int", "default_value": "0"}, {"fullname": "python3-10.pyscalpel.http.Response.request", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.request", "kind": "variable", "doc": "

    \n", "annotation": ": pyscalpel.http.request.Request | None", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.http.Response.from_mitmproxy", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.from_mitmproxy", "kind": "function", "doc": "

    Construct an instance of the Response class from a mitmproxy.http.HTTPResponse.

    \n\n
    Parameters
    \n\n\n\n
    Returns
    \n\n
    \n

    A Response with the same data as the mitmproxy.http.HTTPResponse.

    \n
    \n", "signature": "(\tcls,\tresponse: _internal_mitmproxy.http.Response) -> python3-10.pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response.from_burp", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.from_burp", "kind": "function", "doc": "

    Construct an instance of the Response class from a Burp suite IHttpResponse.

    \n", "signature": "(\tcls,\tresponse: pyscalpel.java.burp.http_response.IHttpResponse,\tservice: pyscalpel.java.burp.http_service.IHttpService | None = None,\trequest: pyscalpel.java.burp.http_request.IHttpRequest | None = None) -> python3-10.pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response.to_burp", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.to_burp", "kind": "function", "doc": "

    Convert the response to a Burp suite IHttpResponse.

    \n", "signature": "(self) -> pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response.from_raw", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.from_raw", "kind": "function", "doc": "

    Construct an instance of the Response class from raw bytes.

    \n\n
    Parameters
    \n\n
      \n
    • data: The raw bytes to convert.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Response parsed from the raw bytes.

    \n
    \n", "signature": "(cls, data: bytes | str) -> python3-10.pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response.make", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.make", "kind": "function", "doc": "

    Simplified API for creating response objects.

    \n", "signature": "(\tcls,\tstatus_code: int = 200,\tcontent: bytes | str = b'',\theaders: pyscalpel.http.headers.Headers | tuple[tuple[bytes, bytes], ...] = (),\thost: str = '',\tport: int = 0,\tscheme: Literal['http', 'https'] = 'http') -> python3-10.pyscalpel.http.response.Response:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response.host_is", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.host_is", "kind": "function", "doc": "

    Matches the host against the provided patterns

    \n\n

    Returns:\n bool: Whether at least one pattern matched

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Response.body", "modulename": "python3-10.pyscalpel.http", "qualname": "Response.body", "kind": "variable", "doc": "

    Alias for content()

    \n\n

    Returns:\n bytes | None: The request body / content

    \n", "annotation": ": bytes | None"}, {"fullname": "python3-10.pyscalpel.http.Headers", "modulename": "python3-10.pyscalpel.http", "qualname": "Headers", "kind": "class", "doc": "

    A wrapper around the MITMProxy Headers.

    \n\n

    This class provides additional methods for converting headers between Burp suite and MITMProxy formats.

    \n", "bases": "_internal_mitmproxy.coretypes.multidict._MultiDict[~KT, ~VT], _internal_mitmproxy.coretypes.serializable.Serializable"}, {"fullname": "python3-10.pyscalpel.http.Headers.__init__", "modulename": "python3-10.pyscalpel.http", "qualname": "Headers.__init__", "kind": "function", "doc": "
    Parameters
    \n\n
      \n
    • fields: The headers to construct the from.
    • \n
    • headers: The headers to construct the from.
    • \n
    \n", "signature": "(fields: Optional[Iterable[tuple[bytes, bytes]]] = None, **headers)"}, {"fullname": "python3-10.pyscalpel.http.Headers.from_mitmproxy", "modulename": "python3-10.pyscalpel.http", "qualname": "Headers.from_mitmproxy", "kind": "function", "doc": "

    Creates a Headers from a mitmproxy.http.Headers.

    \n\n
    Parameters
    \n\n
      \n
    • headers: The mitmproxy.http.Headers to convert.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Headers with the same headers as the mitmproxy.http.Headers.

    \n
    \n", "signature": "(\tcls,\theaders: _internal_mitmproxy.http.Headers) -> pyscalpel.http.headers.Headers:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Headers.from_burp", "modulename": "python3-10.pyscalpel.http", "qualname": "Headers.from_burp", "kind": "function", "doc": "

    Construct an instance of the Headers class from a Burp suite HttpHeader array.

    \n\n
    Parameters
    \n\n
      \n
    • headers: The Burp suite HttpHeader array to convert.
    • \n
    \n\n
    Returns
    \n\n
    \n

    A Headers with the same headers as the Burp suite HttpHeader array.

    \n
    \n", "signature": "(\tcls,\theaders: list[pyscalpel.java.burp.http_header.IHttpHeader]) -> pyscalpel.http.headers.Headers:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Headers.to_burp", "modulename": "python3-10.pyscalpel.http", "qualname": "Headers.to_burp", "kind": "function", "doc": "

    Convert the headers to a Burp suite HttpHeader array.

    \n\n
    Returns
    \n\n
    \n

    A Burp suite HttpHeader array.

    \n
    \n", "signature": "(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Flow", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow", "kind": "class", "doc": "

    Contains request and response and some utilities for match()

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.__init__", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tscheme: Literal['http', 'https'] = 'http',\thost: str = '',\tport: int = 0,\trequest: pyscalpel.http.request.Request | None = None,\tresponse: pyscalpel.http.response.Response | None = None,\ttext: bytes | None = None)"}, {"fullname": "python3-10.pyscalpel.http.Flow.scheme", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.scheme", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.host", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.host", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.port", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.port", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.request", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.request", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.response", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.response", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.text", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.text", "kind": "variable", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.http.Flow.host_is", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.host_is", "kind": "function", "doc": "

    Matches a wildcard pattern against the target host

    \n\n

    Returns:\n bool: True if at least one pattern matched

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.Flow.path_is", "modulename": "python3-10.pyscalpel.http", "qualname": "Flow.path_is", "kind": "function", "doc": "

    Matches a wildcard pattern against the request path

    \n\n

    Includes query string ? and fragment #

    \n\n

    Returns:\n bool: True if at least one pattern matched

    \n", "signature": "(self, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.host_is", "modulename": "python3-10.pyscalpel.http", "qualname": "host_is", "kind": "function", "doc": "

    Matches a host using unix-like wildcard matching against multiple patterns

    \n\n

    Args:\n host (str): The host to match against\n patterns (str): The patterns to use

    \n\n

    Returns:\n bool: The match result (True if at least one pattern matches, else False)

    \n", "signature": "(host: str, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.match_patterns", "modulename": "python3-10.pyscalpel.http", "qualname": "match_patterns", "kind": "function", "doc": "

    Matches a string using unix-like wildcard matching against multiple patterns

    \n\n

    Args:\n to_match (str): The string to match against\n patterns (str): The patterns to use

    \n\n

    Returns:\n bool: The match result (True if at least one pattern matches, else False)

    \n", "signature": "(to_match: str, *patterns: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body", "modulename": "python3-10.pyscalpel.http.body", "kind": "module", "doc": "

    Pentesters often have to manipulate form data in precise and extensive ways

    \n\n

    This module contains implementations for the most common forms (multipart,urlencoded, JSON)

    \n\n

    Users may be implement their own form by creating a Serializer,\nassigning the .serializer attribute in Request and using the \"form\" property

    \n\n

    Forms are designed to be convertible from one to another.

    \n\n

    For example, JSON forms may be converted to URL encoded forms\nby using the php query string syntax:

    \n\n

    {\"key1\": {\"key2\" : {\"key3\" : \"nested_value\"}}} -> key1[key2][key3]=nested_value

    \n\n

    And vice-versa.

    \n"}, {"fullname": "python3-10.pyscalpel.http.body.Form", "modulename": "python3-10.pyscalpel.http.body", "qualname": "Form", "kind": "class", "doc": "

    A MutableMapping is a generic container for associating\nkey/value pairs.

    \n\n

    This class provides concrete generic implementations of all\nmethods except for __getitem__, __setitem__, __delitem__,\n__iter__, and __len__.

    \n", "bases": "collections.abc.MutableMapping[~KT, ~VT]"}, {"fullname": "python3-10.pyscalpel.http.body.JSON_VALUE_TYPES", "modulename": "python3-10.pyscalpel.http.body", "qualname": "JSON_VALUE_TYPES", "kind": "variable", "doc": "

    \n", "default_value": "str | int | float | bool | None | list['JSON_VALUE_TYPES'] | dict[str | int | float, 'JSON_VALUE_TYPES']"}, {"fullname": "python3-10.pyscalpel.http.body.JSONForm", "modulename": "python3-10.pyscalpel.http.body", "qualname": "JSONForm", "kind": "class", "doc": "

    Form representing a JSON object {}

    \n\n

    Implemented by a plain dict

    \n\n

    Args:\n dict (_type_): A dict containing JSON-compatible types.

    \n", "bases": "dict[str | int | float, str | int | float | bool | None | list['JSON_VALUE_TYPES'] | dict[str | int | float, 'JSON_VALUE_TYPES']]"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm", "kind": "class", "doc": "

    This class represents a multipart/form-data request.

    \n\n

    It contains a collection of MultiPartFormField objects, providing methods\nto add, get, and delete form fields.

    \n\n

    The class also enables the conversion of the entire form\ninto bytes for transmission.

    \n\n
      \n
    • Args:

      \n\n
        \n
      • fields (Sequence[MultiPartFormField]): A sequence of MultiPartFormField objects that make up the form.
      • \n
      • content_type (str): The content type of the form.
      • \n
      • encoding (str): The encoding of the form.
      • \n
    • \n
    • Raises:

      \n\n
        \n
      • TypeError: Raised when an incorrect type is passed to MultiPartForm.set.
      • \n
      • KeyError: Raised when trying to access a field that does not exist in the form.
      • \n
    • \n
    • Returns:

      \n\n
        \n
      • MultiPartForm: An instance of the class representing a multipart/form-data request.
      • \n
    • \n
    • Yields:

      \n\n
        \n
      • Iterator[MultiPartFormField]: Yields each field in the form.
      • \n
    • \n
    \n", "bases": "collections.abc.Mapping[str, pyscalpel.http.body.multipart.MultiPartFormField]"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.__init__", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.__init__", "kind": "function", "doc": "

    \n", "signature": "(\tfields: Sequence[pyscalpel.http.body.multipart.MultiPartFormField],\tcontent_type: str,\tencoding: str = 'utf-8')"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.fields", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.fields", "kind": "variable", "doc": "

    \n", "annotation": ": list[pyscalpel.http.body.multipart.MultiPartFormField]"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.content_type", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.content_type", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.encoding", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.encoding", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.from_bytes", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.from_bytes", "kind": "function", "doc": "

    Create a MultiPartForm by parsing a raw multipart form

    \n\n
      \n
    • Args:

      \n\n
        \n
      • content (bytes): The multipart form as raw bytes
      • \n
      • content_type (str): The Content-Type header with the corresponding boundary param (required).
      • \n
      • encoding (str, optional): The encoding to use (not required). Defaults to \"utf-8\".
      • \n
    • \n
    • Returns:

      \n\n
        \n
      • MultiPartForm: The parsed multipart form
      • \n
    • \n
    \n", "signature": "(\tcls,\tcontent: bytes,\tcontent_type: str,\tencoding: str = 'utf-8') -> pyscalpel.http.body.multipart.MultiPartForm:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.boundary", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.boundary", "kind": "variable", "doc": "

    Get the form multipart boundary

    \n\n

    Returns:\n bytes: The multipart boundary

    \n", "annotation": ": bytes"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.get_all", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.get_all", "kind": "function", "doc": "

    Return the list of all values for a given key.\nIf that key is not in the MultiDict, the return value will be an empty list.

    \n", "signature": "(self, key: str) -> list[pyscalpel.http.body.multipart.MultiPartFormField]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.del_all", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.del_all", "kind": "function", "doc": "

    \n", "signature": "(self, key: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.set", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.set", "kind": "function", "doc": "

    \n", "signature": "(\tself,\tkey: str,\tvalue: _io.TextIOWrapper | _io.BufferedReader | io.IOBase | pyscalpel.http.body.multipart.MultiPartFormField | bytes | str | int | float | None) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.setdefault", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.setdefault", "kind": "function", "doc": "

    \n", "signature": "(\tself,\tkey: str,\tdefault: pyscalpel.http.body.multipart.MultiPartFormField | None = None) -> pyscalpel.http.body.multipart.MultiPartFormField:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.insert", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.insert", "kind": "function", "doc": "

    Insert an additional value for the given key at the specified position.

    \n", "signature": "(\tself,\tindex: int,\tvalue: pyscalpel.http.body.multipart.MultiPartFormField) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartForm.append", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartForm.append", "kind": "function", "doc": "

    \n", "signature": "(self, value: pyscalpel.http.body.multipart.MultiPartFormField) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField", "kind": "class", "doc": "

    This class represents a field in a multipart/form-data request.

    \n\n

    It provides functionalities to create form fields from various inputs like raw body parts,\nfiles and manual construction with name, filename, body, and content type.

    \n\n

    It also offers properties and methods to interact with the form field's headers and content.

    \n\n

    Raises:\n StopIteration: Raised when the specified Content-Disposition header is not found or could not be parsed.

    \n\n

    Returns:\n MultiPartFormField: An instance of the class representing a form field in a multipart/form-data request.

    \n"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.__init__", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.__init__", "kind": "function", "doc": "

    \n", "signature": "(\theaders: requests.structures.CaseInsensitiveDict[str],\tcontent: bytes = b'',\tencoding: str = 'utf-8')"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.headers", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.headers", "kind": "variable", "doc": "

    \n", "annotation": ": requests.structures.CaseInsensitiveDict[str]"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.content", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.content", "kind": "variable", "doc": "

    \n", "annotation": ": bytes"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.encoding", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.encoding", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.from_body_part", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.from_body_part", "kind": "function", "doc": "

    \n", "signature": "(cls, body_part: requests_toolbelt.multipart.decoder.BodyPart):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.make", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.make", "kind": "function", "doc": "

    \n", "signature": "(\tcls,\tname: str,\tfilename: str | None = None,\tbody: bytes = b'',\tcontent_type: str | None = None,\tencoding: str = 'utf-8') -> pyscalpel.http.body.multipart.MultiPartFormField:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.from_file", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.from_file", "kind": "function", "doc": "

    \n", "signature": "(\tname: str,\tfile: _io.TextIOWrapper | _io.BufferedReader | str | io.IOBase,\tfilename: str | None = None,\tcontent_type: str | None = None,\tencoding: str | None = None):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.text", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.text", "kind": "variable", "doc": "

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.content_type", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.content_type", "kind": "variable", "doc": "

    \n", "annotation": ": str | None"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.get_disposition_param", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.get_disposition_param", "kind": "function", "doc": "

    Get a param from the Content-Disposition header

    \n\n

    Args:\n key (str): the param name

    \n\n

    Raises:\n StopIteration: Raised when the param was not found.

    \n\n

    Returns:\n tuple[str, str | None] | None: Returns the param as (key, value)

    \n", "signature": "(self, key: str) -> tuple[str, str | None] | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.set_disposition_param", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.set_disposition_param", "kind": "function", "doc": "

    Set a Content-Type header parameter

    \n\n

    Args:\n key (str): The parameter name\n value (str): The parameter value

    \n", "signature": "(self, key: str, value: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.name", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.name", "kind": "variable", "doc": "

    Get the Content-Disposition header name parameter

    \n\n

    Returns:\n str: The Content-Disposition header name parameter value

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.http.body.MultiPartFormField.filename", "modulename": "python3-10.pyscalpel.http.body", "qualname": "MultiPartFormField.filename", "kind": "variable", "doc": "

    Get the Content-Disposition header filename parameter

    \n\n

    Returns:\n str | None: The Content-Disposition header filename parameter value

    \n", "annotation": ": str | None"}, {"fullname": "python3-10.pyscalpel.http.body.URLEncodedForm", "modulename": "python3-10.pyscalpel.http.body", "qualname": "URLEncodedForm", "kind": "class", "doc": "

    A concrete MultiDict, storing its own data.

    \n", "bases": "_internal_mitmproxy.coretypes.multidict.MultiDict[bytes, bytes]"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer", "kind": "class", "doc": "

    Helper class that provides a standard way to create an ABC using\ninheritance.

    \n", "bases": "abc.ABC"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer.serialize", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer.serialize", "kind": "function", "doc": "

    Serialize a parsed form to raw bytes

    \n\n

    Args:\n deserialized_body (Form): The parsed form\n req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)

    \n\n

    Returns:\n bytes: Form's raw bytes representation

    \n", "signature": "(\tself,\tdeserialized_body: pyscalpel.http.body.abstract.Form,\treq: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer.deserialize", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer.deserialize", "kind": "function", "doc": "

    Parses the form from its raw bytes representation

    \n\n

    Args:\n body (bytes): The form as bytes\n req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type)

    \n\n

    Returns:\n Form | None: The parsed form

    \n", "signature": "(\tself,\tbody: bytes,\treq: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> pyscalpel.http.body.abstract.Form | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer.get_empty_form", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer.get_empty_form", "kind": "function", "doc": "

    Get an empty parsed form object

    \n\n

    Args:\n req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms)

    \n\n

    Returns:\n Form: The empty form

    \n", "signature": "(\tself,\treq: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> pyscalpel.http.body.abstract.Form:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer.deserialized_type", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer.deserialized_type", "kind": "function", "doc": "

    Gets the form concrete type

    \n\n

    Returns:\n type[Form]: The form concrete type

    \n", "signature": "(self) -> type[pyscalpel.http.body.abstract.Form]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer.import_form", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer.import_form", "kind": "function", "doc": "

    Imports a form exported by a serializer\n Used to convert a form from a Content-Type to another\n Information may be lost in the process

    \n\n

    Args:\n exported (ExportedForm): The exported form\n req: (ObjectWithHeaders): Used to get multipart boundary

    \n\n

    Returns:\n Form: The form converted to this serializer's format

    \n", "signature": "(\tself,\texported: tuple[tuple[bytes, bytes | None], ...],\treq: pyscalpel.http.body.abstract.ObjectWithHeadersField | pyscalpel.http.body.abstract.ObjectWithHeadersProperty) -> pyscalpel.http.body.abstract.Form:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.FormSerializer.export_form", "modulename": "python3-10.pyscalpel.http.body", "qualname": "FormSerializer.export_form", "kind": "function", "doc": "

    Formats a form so it can be imported by another serializer\n Information may be lost in the process

    \n\n

    Args:\n form (Form): The form to export

    \n\n

    Returns:\n ExportedForm: The exported form

    \n", "signature": "(\tself,\tsource: pyscalpel.http.body.abstract.Form) -> tuple[tuple[bytes, bytes | None], ...]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.json_unescape", "modulename": "python3-10.pyscalpel.http.body", "qualname": "json_unescape", "kind": "function", "doc": "

    \n", "signature": "(escaped: str) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.json_unescape_bytes", "modulename": "python3-10.pyscalpel.http.body", "qualname": "json_unescape_bytes", "kind": "function", "doc": "

    \n", "signature": "(escaped: str) -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.http.body.json_escape_bytes", "modulename": "python3-10.pyscalpel.http.body", "qualname": "json_escape_bytes", "kind": "function", "doc": "

    \n", "signature": "(data: bytes) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java", "modulename": "python3-10.pyscalpel.java", "kind": "module", "doc": "

    This module declares type definitions used for Java objects.

    \n\n

    If you are a normal user, you should probably never have to manipulate these objects yourself.

    \n"}, {"fullname": "python3-10.pyscalpel.java.import_java", "modulename": "python3-10.pyscalpel.java", "qualname": "import_java", "kind": "function", "doc": "

    Import a Java class using Python's import mechanism.

    \n\n
    Parameters
    \n\n
      \n
    • module: The module to import from. (e.g. \"java.lang\")
    • \n
    • name: The name of the class to import. (e.g. \"String\")
    • \n
    • expected_type: The expected type of the class. (e.g. JavaObject)
    • \n
    \n\n
    Returns
    \n\n
    \n

    The imported class.

    \n
    \n", "signature": "(\tmodule: str,\tname: str,\texpected_type: Type[~ExpectedObject] = <class 'pyscalpel.java.object.JavaObject'>) -> ~ExpectedObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject", "kind": "class", "doc": "

    generated source for class Object

    \n", "bases": "typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.getClass", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.getClass", "kind": "function", "doc": "

    generated source for method getClass

    \n", "signature": "(self) -> python3-10.pyscalpel.java.object.JavaClass:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.hashCode", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.hashCode", "kind": "function", "doc": "

    generated source for method hashCode

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.equals", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.equals", "kind": "function", "doc": "

    generated source for method equals

    \n", "signature": "(self, obj) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.clone", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.clone", "kind": "function", "doc": "

    generated source for method clone

    \n", "signature": "(self) -> python3-10.pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.notify", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.notify", "kind": "function", "doc": "

    generated source for method notify

    \n", "signature": "(self) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.notifyAll", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.notifyAll", "kind": "function", "doc": "

    generated source for method notifyAll

    \n", "signature": "(self) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.wait", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.wait", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaObject.finalize", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaObject.finalize", "kind": "function", "doc": "

    generated source for method finalize

    \n", "signature": "(self) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.JavaBytes", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaBytes", "kind": "class", "doc": "

    Built-in mutable sequence.

    \n\n

    If no argument is given, the constructor creates a new empty list.\nThe argument must be an iterable if specified.

    \n", "bases": "list[int]"}, {"fullname": "python3-10.pyscalpel.java.JavaClass", "modulename": "python3-10.pyscalpel.java", "qualname": "JavaClass", "kind": "class", "doc": "

    generated source for class Object

    \n", "bases": "python3-10.pyscalpel.java.object.JavaObject"}, {"fullname": "python3-10.pyscalpel.java.burp", "modulename": "python3-10.pyscalpel.java.burp", "kind": "module", "doc": "

    This module exposes Java objects from Burp's extensions API

    \n\n

    If you are a normal user, you should probably never have to manipulate these objects yourself.

    \n"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttp", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttp", "kind": "class", "doc": "

    generated source for interface Http

    \n", "bases": "pyscalpel.java.object.JavaObject, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttp.sendRequest", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttp.sendRequest", "kind": "function", "doc": "

    \n", "signature": "(\tself,\trequest: pyscalpel.java.burp.http_request.IHttpRequest) -> pyscalpel.java.burp.http_request_response.IHttpRequestResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest", "kind": "class", "doc": "

    generated source for interface HttpRequest

    \n", "bases": "pyscalpel.java.burp.http_message.IHttpMessage, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.httpService", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.httpService", "kind": "function", "doc": "

    generated source for method httpService

    \n", "signature": "(self) -> pyscalpel.java.burp.http_service.IHttpService:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.url", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.url", "kind": "function", "doc": "

    generated source for method url

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.method", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.method", "kind": "function", "doc": "

    generated source for method method

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.path", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.path", "kind": "function", "doc": "

    generated source for method path

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.httpVersion", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.httpVersion", "kind": "function", "doc": "

    generated source for method httpVersion

    \n", "signature": "(self) -> str | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.headers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.headers", "kind": "function", "doc": "

    generated source for method headers

    \n", "signature": "(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.contentType", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.contentType", "kind": "function", "doc": "

    generated source for method contentType

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.parameters", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.parameters", "kind": "function", "doc": "

    generated source for method parameters

    \n", "signature": "(self) -> list[pyscalpel.java.burp.http_parameter.IHttpParameter]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.body", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.body", "kind": "function", "doc": "

    generated source for method body

    \n", "signature": "(self) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.bodyToString", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.bodyToString", "kind": "function", "doc": "

    generated source for method bodyToString

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.bodyOffset", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.bodyOffset", "kind": "function", "doc": "

    generated source for method bodyOffset

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.markers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.markers", "kind": "function", "doc": "

    generated source for method markers

    \n", "signature": "(self):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.toByteArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.toByteArray", "kind": "function", "doc": "

    generated source for method toByteArray

    \n", "signature": "(self) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.copyToTempFile", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.copyToTempFile", "kind": "function", "doc": "

    generated source for method copyToTempFile

    \n", "signature": "(self) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withService", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withService", "kind": "function", "doc": "

    generated source for method withService

    \n", "signature": "(\tself,\tservice: pyscalpel.java.burp.http_service.IHttpService) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withPath", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withPath", "kind": "function", "doc": "

    generated source for method withPath

    \n", "signature": "(\tself,\tpath: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withMethod", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withMethod", "kind": "function", "doc": "

    generated source for method withMethod

    \n", "signature": "(\tself,\tmethod: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withAddedParameters", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withAddedParameters", "kind": "function", "doc": "

    generated source for method withAddedParameters

    \n", "signature": "(\tself,\tparameters: Iterable[pyscalpel.java.burp.http_parameter.IHttpParameter]) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withAddedParameters_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withAddedParameters_0", "kind": "function", "doc": "

    generated source for method withAddedParameters_0

    \n", "signature": "(\tself,\t*parameters: pyscalpel.java.burp.http_parameter.IHttpParameter) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withRemovedParameters", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withRemovedParameters", "kind": "function", "doc": "

    generated source for method withRemovedParameters

    \n", "signature": "(\tself,\tparameters: Iterable[pyscalpel.java.burp.http_parameter.IHttpParameter]) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withRemovedParameters_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withRemovedParameters_0", "kind": "function", "doc": "

    generated source for method withRemovedParameters_0

    \n", "signature": "(\tself,\t*parameters: pyscalpel.java.burp.http_parameter.IHttpParameter) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withUpdatedParameters", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withUpdatedParameters", "kind": "function", "doc": "

    generated source for method withUpdatedParameters

    \n", "signature": "(\tself,\tparameters: list[pyscalpel.java.burp.http_parameter.IHttpParameter]) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withUpdatedParameters_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withUpdatedParameters_0", "kind": "function", "doc": "

    generated source for method withUpdatedParameters_0

    \n", "signature": "(\tself,\t*parameters: pyscalpel.java.burp.http_parameter.IHttpParameter) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withTransformationApplied", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withTransformationApplied", "kind": "function", "doc": "

    generated source for method withTransformationApplied

    \n", "signature": "(\tself,\ttransformation) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withBody", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withBody", "kind": "function", "doc": "

    generated source for method withBody

    \n", "signature": "(self, body) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withBody_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withBody_0", "kind": "function", "doc": "

    generated source for method withBody_0

    \n", "signature": "(\tself,\tbody: pyscalpel.java.burp.byte_array.IByteArray) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withAddedHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withAddedHeader", "kind": "function", "doc": "

    generated source for method withAddedHeader

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withAddedHeader_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withAddedHeader_0", "kind": "function", "doc": "

    generated source for method withAddedHeader_0

    \n", "signature": "(\tself,\theader: pyscalpel.java.burp.http_header.IHttpHeader) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withUpdatedHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withUpdatedHeader", "kind": "function", "doc": "

    generated source for method withUpdatedHeader

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withUpdatedHeader_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withUpdatedHeader_0", "kind": "function", "doc": "

    generated source for method withUpdatedHeader_0

    \n", "signature": "(\tself,\theader: pyscalpel.java.burp.http_header.IHttpHeader) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withRemovedHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withRemovedHeader", "kind": "function", "doc": "

    generated source for method withRemovedHeader

    \n", "signature": "(\tself,\tname: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withRemovedHeader_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withRemovedHeader_0", "kind": "function", "doc": "

    generated source for method withRemovedHeader_0

    \n", "signature": "(\tself,\theader: pyscalpel.java.burp.http_header.IHttpHeader) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withMarkers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withMarkers", "kind": "function", "doc": "

    generated source for method withMarkers

    \n", "signature": "(\tself,\tmarkers) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withMarkers_0", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withMarkers_0", "kind": "function", "doc": "

    generated source for method withMarkers_0

    \n", "signature": "(\tself,\t*markers) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.withDefaultHeaders", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.withDefaultHeaders", "kind": "function", "doc": "

    generated source for method withDefaultHeaders

    \n", "signature": "(self) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.httpRequest", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.httpRequest", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.httpRequestFromUrl", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.httpRequestFromUrl", "kind": "function", "doc": "

    generated source for method httpRequestFromUrl

    \n", "signature": "(\tself,\turl: str) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequest.http2Request", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequest.http2Request", "kind": "function", "doc": "

    generated source for method http2Request

    \n", "signature": "(\tself,\tservice: pyscalpel.java.burp.http_service.IHttpService,\theaders: Iterable[pyscalpel.java.burp.http_header.IHttpHeader],\tbody: pyscalpel.java.burp.byte_array.IByteArray) -> python3-10.pyscalpel.java.burp.http_request.IHttpRequest:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.HttpRequest", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "HttpRequest", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse", "kind": "class", "doc": "

    generated source for interface HttpResponse

    \n", "bases": "pyscalpel.java.burp.http_message.IHttpMessage, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.statusCode", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.statusCode", "kind": "function", "doc": "

    generated source for method statusCode

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.reasonPhrase", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.reasonPhrase", "kind": "function", "doc": "

    generated source for method reasonPhrase

    \n", "signature": "(self) -> str | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.httpVersion", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.httpVersion", "kind": "function", "doc": "

    generated source for method httpVersion

    \n", "signature": "(self) -> str | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.headers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.headers", "kind": "function", "doc": "

    generated source for method headers

    \n", "signature": "(self) -> list[pyscalpel.java.burp.http_header.IHttpHeader]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.body", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.body", "kind": "function", "doc": "

    generated source for method body

    \n", "signature": "(self) -> pyscalpel.java.burp.byte_array.IByteArray | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.bodyToString", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.bodyToString", "kind": "function", "doc": "

    generated source for method bodyToString

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.bodyOffset", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.bodyOffset", "kind": "function", "doc": "

    generated source for method bodyOffset

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.markers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.markers", "kind": "function", "doc": "

    generated source for method markers

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.cookies", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.cookies", "kind": "function", "doc": "

    generated source for method cookies

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.statedMimeType", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.statedMimeType", "kind": "function", "doc": "

    generated source for method statedMimeType

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.inferredMimeType", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.inferredMimeType", "kind": "function", "doc": "

    generated source for method inferredMimeType

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.keywordCounts", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.keywordCounts", "kind": "function", "doc": "

    generated source for method keywordCounts

    \n", "signature": "(self, *keywords) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.attributes", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.attributes", "kind": "function", "doc": "

    generated source for method attributes

    \n", "signature": "(self, *types) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.toByteArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.toByteArray", "kind": "function", "doc": "

    generated source for method toByteArray

    \n", "signature": "(self) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.copyToTempFile", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.copyToTempFile", "kind": "function", "doc": "

    generated source for method copyToTempFile

    \n", "signature": "(self) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withStatusCode", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withStatusCode", "kind": "function", "doc": "

    generated source for method withStatusCode

    \n", "signature": "(\tself,\tstatusCode: int) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withReasonPhrase", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withReasonPhrase", "kind": "function", "doc": "

    generated source for method withReasonPhrase

    \n", "signature": "(\tself,\treasonPhrase: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withHttpVersion", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withHttpVersion", "kind": "function", "doc": "

    generated source for method withHttpVersion

    \n", "signature": "(\tself,\thttpVersion: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withBody", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withBody", "kind": "function", "doc": "

    generated source for method withBody

    \n", "signature": "(\tself,\tbody: pyscalpel.java.burp.byte_array.IByteArray | str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withAddedHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withAddedHeader", "kind": "function", "doc": "

    generated source for method withAddedHeader_0

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withUpdatedHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withUpdatedHeader", "kind": "function", "doc": "

    generated source for method withUpdatedHeader_0

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withRemovedHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withRemovedHeader", "kind": "function", "doc": "

    generated source for method withRemovedHeader_0

    \n", "signature": "(\tself,\tname: str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.withMarkers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.withMarkers", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpResponse.httpResponse", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpResponse.httpResponse", "kind": "function", "doc": "

    generated source for method httpResponse

    \n", "signature": "(\tself,\tresponse: pyscalpel.java.burp.byte_array.IByteArray | str) -> python3-10.pyscalpel.java.burp.http_response.IHttpResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.HttpResponse", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "HttpResponse", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequestResponse", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequestResponse", "kind": "class", "doc": "

    generated source for interface HttpRequestResponse

    \n", "bases": "pyscalpel.java.object.JavaObject, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequestResponse.request", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequestResponse.request", "kind": "function", "doc": "

    \n", "signature": "(self) -> pyscalpel.java.burp.http_request.IHttpRequest | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpRequestResponse.response", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpRequestResponse.response", "kind": "function", "doc": "

    \n", "signature": "(self) -> pyscalpel.java.burp.http_response.IHttpResponse | None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpHeader", "kind": "class", "doc": "

    generated source for interface HttpHeader

    \n", "bases": "pyscalpel.java.object.JavaObject, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpHeader.name", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpHeader.name", "kind": "function", "doc": "

    generated source for method name

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpHeader.value", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpHeader.value", "kind": "function", "doc": "

    generated source for method value

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpHeader.httpHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpHeader.httpHeader", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.HttpHeader", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "HttpHeader", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage", "kind": "class", "doc": "

    generated source for interface HttpMessage

    \n", "bases": "pyscalpel.java.object.JavaObject, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage.headers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage.headers", "kind": "function", "doc": "

    generated source for method headers

    \n", "signature": "(self) -> pyscalpel.java.burp.http_header.IHttpHeader:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage.bodyOffset", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage.bodyOffset", "kind": "function", "doc": "

    generated source for method bodyOffset

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage.body", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage.body", "kind": "function", "doc": "

    generated source for method body

    \n", "signature": "(self) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage.bodyToString", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage.bodyToString", "kind": "function", "doc": "

    generated source for method bodyToString

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage.markers", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage.markers", "kind": "function", "doc": "

    generated source for method markers

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpMessage.toByteArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpMessage.toByteArray", "kind": "function", "doc": "

    generated source for method toByteArray

    \n", "signature": "(self) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter", "kind": "class", "doc": "

    generated source for interface HttpParameter

    \n", "bases": "pyscalpel.java.object.JavaObject"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.type_", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.type_", "kind": "function", "doc": "

    generated source for method type_

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.name", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.name", "kind": "function", "doc": "

    generated source for method name

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.value", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.value", "kind": "function", "doc": "

    generated source for method value

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.urlParameter", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.urlParameter", "kind": "function", "doc": "

    generated source for method urlParameter

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.bodyParameter", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.bodyParameter", "kind": "function", "doc": "

    generated source for method bodyParameter

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.cookieParameter", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.cookieParameter", "kind": "function", "doc": "

    generated source for method cookieParameter

    \n", "signature": "(\tself,\tname: str,\tvalue: str) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpParameter.parameter", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpParameter.parameter", "kind": "function", "doc": "

    generated source for method parameter

    \n", "signature": "(\tself,\tname: str,\tvalue: str,\ttype_: pyscalpel.java.object.JavaObject) -> python3-10.pyscalpel.java.burp.http_parameter.IHttpParameter:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.HttpParameter", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "HttpParameter", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpService", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpService", "kind": "class", "doc": "

    generated source for class Object

    \n", "bases": "pyscalpel.java.object.JavaObject"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpService.host", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpService.host", "kind": "function", "doc": "

    The hostname or IP address for the service.

    \n", "signature": "(self) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpService.httpService", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpService.httpService", "kind": "function", "doc": "

    Create a new instance of {@code HttpService} from a host, a port and a protocol.

    \n", "signature": "(\tself,\t*args,\t**kwargs) -> python3-10.pyscalpel.java.burp.http_service.IHttpService:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpService.port", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpService.port", "kind": "function", "doc": "

    The port number for the service.

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IHttpService.secure", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IHttpService.secure", "kind": "function", "doc": "

    True if a secure protocol is used for the connection, false otherwise.

    \n", "signature": "(self) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.HttpService", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "HttpService", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray", "kind": "class", "doc": "

    generated source for class Object

    \n", "bases": "pyscalpel.java.object.JavaObject, typing.Protocol"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.getByte", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.getByte", "kind": "function", "doc": "

    generated source for method getByte

    \n", "signature": "(self, index: int) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.setByte", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.setByte", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.setBytes", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.setBytes", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.length", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.length", "kind": "function", "doc": "

    generated source for method length

    \n", "signature": "(self) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.getBytes", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.getBytes", "kind": "function", "doc": "

    generated source for method getBytes

    \n", "signature": "(self) -> pyscalpel.java.bytes.JavaBytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.subArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.subArray", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.copy", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.copy", "kind": "function", "doc": "

    generated source for method copy

    \n", "signature": "(self) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.copyToTempFile", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.copyToTempFile", "kind": "function", "doc": "

    generated source for method copyToTempFile

    \n", "signature": "(self) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.indexOf", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.indexOf", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.countMatches", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.countMatches", "kind": "function", "doc": "

    Helper for @overload to raise when called.

    \n", "signature": "(*args, **kwds):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.withAppended", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.withAppended", "kind": "function", "doc": "

    generated source for method withAppended

    \n", "signature": "(self, *data: int) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.byteArrayOfLength", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.byteArrayOfLength", "kind": "function", "doc": "

    generated source for method byteArrayOfLength

    \n", "signature": "(\tself,\tlength: int) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.IByteArray.byteArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "IByteArray.byteArray", "kind": "function", "doc": "

    generated source for method byteArray

    \n", "signature": "(\tself,\tdata: bytes | pyscalpel.java.bytes.JavaBytes | list[int] | str) -> python3-10.pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.ByteArray", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "ByteArray", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging", "kind": "class", "doc": "

    generated source for interface Logging

    \n", "bases": "pyscalpel.java.object.JavaObject"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.output", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.output", "kind": "function", "doc": "

    generated source for method output

    \n", "signature": "(self) -> pyscalpel.java.object.JavaObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.error", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.error", "kind": "function", "doc": "

    generated source for method error

    \n", "signature": "(self, message: str) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.logToOutput", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.logToOutput", "kind": "function", "doc": "

    generated source for method logToOutput

    \n", "signature": "(self, message: str) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.raiseDebugEvent", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.raiseDebugEvent", "kind": "function", "doc": "

    generated source for method raiseDebugEvent

    \n", "signature": "(self, message: str) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.raiseInfoEvent", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.raiseInfoEvent", "kind": "function", "doc": "

    generated source for method raiseInfoEvent

    \n", "signature": "(self, message: str) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.raiseErrorEvent", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.raiseErrorEvent", "kind": "function", "doc": "

    generated source for method raiseErrorEvent

    \n", "signature": "(self, message: str) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.burp.Logging.raiseCriticalEvent", "modulename": "python3-10.pyscalpel.java.burp", "qualname": "Logging.raiseCriticalEvent", "kind": "function", "doc": "

    generated source for method raiseCriticalEvent

    \n", "signature": "(self, message: str) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.import_java", "modulename": "python3-10.pyscalpel.java.import_java", "kind": "module", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.java.import_java.import_java", "modulename": "python3-10.pyscalpel.java.import_java", "qualname": "import_java", "kind": "function", "doc": "

    Import a Java class using Python's import mechanism.

    \n\n
    Parameters
    \n\n
      \n
    • module: The module to import from. (e.g. \"java.lang\")
    • \n
    • name: The name of the class to import. (e.g. \"String\")
    • \n
    • expected_type: The expected type of the class. (e.g. JavaObject)
    • \n
    \n\n
    Returns
    \n\n
    \n

    The imported class.

    \n
    \n", "signature": "(\tmodule: str,\tname: str,\texpected_type: Type[~ExpectedObject] = <class 'pyscalpel.java.object.JavaObject'>) -> ~ExpectedObject:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types", "modulename": "python3-10.pyscalpel.java.scalpel_types", "kind": "module", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.Context", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "Context", "kind": "class", "doc": "

    Scalpel Python execution context

    \n", "bases": "typing.TypedDict"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.Context.API", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "Context.API", "kind": "variable", "doc": "

    The Burp [Montoya API]\n(https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html)\nroot object.

    \n\n

    Allows you to interact with Burp by directly manipulating the Java object.

    \n", "annotation": ": Any"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.Context.directory", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "Context.directory", "kind": "variable", "doc": "

    The framework directory

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.Context.user_script", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "Context.user_script", "kind": "variable", "doc": "

    The loaded script path

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.Context.framework", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "Context.framework", "kind": "variable", "doc": "

    The framework (loader script) path

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.Context.venv", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "Context.venv", "kind": "variable", "doc": "

    The venv the script was loaded in

    \n", "annotation": ": str"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.IPythonUtils", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "IPythonUtils", "kind": "class", "doc": "

    generated source for class Object

    \n", "bases": "pyscalpel.java.object.JavaObject"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.IPythonUtils.toPythonBytes", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "IPythonUtils.toPythonBytes", "kind": "function", "doc": "

    \n", "signature": "(self, java_bytes: pyscalpel.java.bytes.JavaBytes) -> list[int]:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.IPythonUtils.toJavaBytes", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "IPythonUtils.toJavaBytes", "kind": "function", "doc": "

    \n", "signature": "(\tself,\tpython_bytes: bytes | list[int] | bytearray) -> pyscalpel.java.bytes.JavaBytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.IPythonUtils.toByteArray", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "IPythonUtils.toByteArray", "kind": "function", "doc": "

    \n", "signature": "(\tself,\tpython_bytes: bytes | list[int] | bytearray) -> pyscalpel.java.burp.byte_array.IByteArray:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.IPythonUtils.getClassName", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "IPythonUtils.getClassName", "kind": "function", "doc": "

    \n", "signature": "(self, msg: pyscalpel.java.object.JavaObject) -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.IPythonUtils.updateHeader", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "IPythonUtils.updateHeader", "kind": "function", "doc": "

    \n", "signature": "(\tself,\tmsg: ~RequestOrResponse,\tname: str,\tvalue: str) -> ~RequestOrResponse:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.java.scalpel_types.PythonUtils", "modulename": "python3-10.pyscalpel.java.scalpel_types", "qualname": "PythonUtils", "kind": "variable", "doc": "

    \n", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.logger", "modulename": "python3-10.pyscalpel.logger", "kind": "module", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.logger.Logger", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger", "kind": "class", "doc": "

    Provides methods for logging messages to the Burp Suite output and standard streams.

    \n"}, {"fullname": "python3-10.pyscalpel.logger.Logger.all", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.all", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.Logger.trace", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.trace", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.Logger.debug", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.debug", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.Logger.info", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.info", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.Logger.warn", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.warn", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.Logger.fatal", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.fatal", "kind": "function", "doc": "

    Prints the message to the standard output

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.Logger.error", "modulename": "python3-10.pyscalpel.logger", "qualname": "Logger.error", "kind": "function", "doc": "

    Prints the message to the standard error

    \n\n

    Args:\n msg (str): The message to print

    \n", "signature": "(self, msg: str):", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.logger.logger", "modulename": "python3-10.pyscalpel.logger", "qualname": "logger", "kind": "variable", "doc": "

    \n", "annotation": ": python3-10.pyscalpel.logger.Logger", "default_value": "None"}, {"fullname": "python3-10.pyscalpel.utils", "modulename": "python3-10.pyscalpel.utils", "kind": "module", "doc": "

    \n"}, {"fullname": "python3-10.pyscalpel.utils.urldecode", "modulename": "python3-10.pyscalpel.utils", "qualname": "urldecode", "kind": "function", "doc": "

    URL Decode all bytes in the given bytes object

    \n", "signature": "(data: bytes | str, encoding='latin-1') -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.utils.urlencode_all", "modulename": "python3-10.pyscalpel.utils", "qualname": "urlencode_all", "kind": "function", "doc": "

    URL Encode all bytes in the given bytes object

    \n", "signature": "(data: bytes | str, encoding='latin-1') -> bytes:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.utils.current_function_name", "modulename": "python3-10.pyscalpel.utils", "qualname": "current_function_name", "kind": "function", "doc": "

    Get current function name

    \n\n

    Returns:\n str: The function name

    \n", "signature": "() -> str:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.venv", "modulename": "python3-10.pyscalpel.venv", "kind": "module", "doc": "

    This module provides reimplementations of Python virtual environnements scripts

    \n\n

    This is designed to be used internally, \nbut in the case where the user desires to dynamically switch venvs using this,\nthey should ensure the selected venv has the dependencies required by Scalpel.

    \n"}, {"fullname": "python3-10.pyscalpel.venv.deactivate", "modulename": "python3-10.pyscalpel.venv", "qualname": "deactivate", "kind": "function", "doc": "

    Deactivates the current virtual environment.

    \n", "signature": "() -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.venv.activate", "modulename": "python3-10.pyscalpel.venv", "qualname": "activate", "kind": "function", "doc": "

    Activates the virtual environment at the given path.

    \n", "signature": "(path: str | None) -> None:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.venv.install", "modulename": "python3-10.pyscalpel.venv", "qualname": "install", "kind": "function", "doc": "

    Install a Python package in the current venv.

    \n\n

    Returns:\n int: The pip install command exit code.

    \n", "signature": "(*packages: str) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.venv.uninstall", "modulename": "python3-10.pyscalpel.venv", "qualname": "uninstall", "kind": "function", "doc": "

    Uninstall a Python package from the current venv.

    \n\n

    Returns:\n int: The pip uninstall command exit code.

    \n", "signature": "(*packages: str) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.venv.create", "modulename": "python3-10.pyscalpel.venv", "qualname": "create", "kind": "function", "doc": "

    Creates a Python venv on the given path

    \n\n

    Returns:\n int: The python3 -m venv command exit code.

    \n", "signature": "(path: str) -> int:", "funcdef": "def"}, {"fullname": "python3-10.pyscalpel.venv.create_default", "modulename": "python3-10.pyscalpel.venv", "qualname": "create_default", "kind": "function", "doc": "

    Creates a default venv in the user's home directory\n Only creates it if the directory doesn't already exist

    \n\n

    Returns:\n str: The venv directory path.

    \n", "signature": "() -> str:", "funcdef": "def"}, {"fullname": "python3-10.qs", "modulename": "python3-10.qs", "kind": "module", "doc": "

    \n"}, {"fullname": "python3-10.qs.list_to_dict", "modulename": "python3-10.qs", "qualname": "list_to_dict", "kind": "function", "doc": "

    Maps a list to an equivalent dictionary

    \n\n

    e.g: [\"a\",\"b\",\"c\"] -> {0:\"a\",1:\"b\",2:\"c\"}

    \n\n

    Used to convert lists to PHP-style arrays

    \n\n

    Args:\n lst (list[Any]): The list to transform

    \n\n

    Returns:\n dict[int, Any]: The \"PHP-style array\" dict

    \n", "signature": "(lst: list[typing.Any]) -> dict[int, typing.Any]:", "funcdef": "def"}, {"fullname": "python3-10.qs.is_valid_php_query_name", "modulename": "python3-10.qs", "qualname": "is_valid_php_query_name", "kind": "function", "doc": "

    Check if a given name follows PHP query string syntax.\nThis implementation assumes that names will be structured like:\nfield\nfield[key]\nfield[key1][key2]\nfield[]

    \n", "signature": "(name: str) -> bool:", "funcdef": "def"}, {"fullname": "python3-10.qs.merge_dict_in_list", "modulename": "python3-10.qs", "qualname": "merge_dict_in_list", "kind": "function", "doc": "

    Merge a dictionary into a list.

    \n\n

    Only the values of integer keys from the dictionary are merged into the list.

    \n\n

    If the dictionary contains only integer keys, returns a merged list.\nIf the dictionary contains other keys as well, returns a merged dict.

    \n\n

    Args:\n source (dict): The dictionary to merge.\n destination (list): The list to merge.

    \n\n

    Returns:\n list | dict: Merged data.

    \n", "signature": "(source: dict, destination: list) -> list | dict:", "funcdef": "def"}, {"fullname": "python3-10.qs.merge", "modulename": "python3-10.qs", "qualname": "merge", "kind": "function", "doc": "

    Merge the source and destination.\nPerforms a shallow or deep merge based on the shallow flag.\nArgs:\n source (Any): The source data to merge.\n destination (Any): The destination data to merge into.\n shallow (bool): If True, perform a shallow merge. Defaults to True.\nReturns:\n Any: Merged data.

    \n", "signature": "(source: dict | list, destination: dict | list, shallow: bool = True):", "funcdef": "def"}, {"fullname": "python3-10.qs.qs_parse", "modulename": "python3-10.qs", "qualname": "qs_parse", "kind": "function", "doc": "

    Parses a query string using PHP's nesting syntax, and returns a dict.

    \n\n

    Args:\n qs (str): The query string to parse.\n keep_blank_values (bool): If True, includes keys with blank values. Defaults to True.\n strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.

    \n\n

    Returns:\n dict: A dictionary representing the parsed query string.

    \n", "signature": "(\tqs: str,\tkeep_blank_values: bool = True,\tstrict_parsing: bool = False) -> dict:", "funcdef": "def"}, {"fullname": "python3-10.qs.build_qs", "modulename": "python3-10.qs", "qualname": "build_qs", "kind": "function", "doc": "

    Build a query string from a dictionary or list of 2-tuples.\nCoerces data types before serialization.\nArgs:\n query (Mapping): The query data to build the string from.\nReturns:\n str: A query string.

    \n", "signature": "(query: Mapping) -> str:", "funcdef": "def"}, {"fullname": "python3-10.qs.qs_parse_pairs", "modulename": "python3-10.qs", "qualname": "qs_parse_pairs", "kind": "function", "doc": "

    Parses a list of key/value pairs and returns a dict.

    \n\n

    Args:\n pairs (list[tuple[str, str]]): The list of key/value pairs.\n keep_blank_values (bool): If True, includes keys with blank values. Defaults to True.\n strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False.

    \n\n

    Returns:\n dict: A dictionary representing the parsed pairs.

    \n", "signature": "(\tpairs: Sequence[tuple[str, str] | tuple[str]],\tkeep_blank_values: bool = True,\tstrict_parsing: bool = False) -> dict:", "funcdef": "def"}]; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file diff --git a/docs/public/schematics/scalpel-diagram.dark.svg b/docs/public/schematics/scalpel-diagram.dark.svg new file mode 100644 index 00000000..d9f143dc --- /dev/null +++ b/docs/public/schematics/scalpel-diagram.dark.svg @@ -0,0 +1,4 @@ + + + +
    Burp
    Burp
    Scalpel (Java Extension)
    Scalpel (Java Extens...
    _framework.py
    _framework.py
    Jep
    Jep
    <user script>
    def request ..
    def response ...
    def req_edit_in ...
    def req_edit_out ...
    <user script>...
    Loads and calls
    Loads and calls
    Handles the Python interpreter through native libraries
    Handles the Python interpreter...
    CPython
    CPython
    Activates user virtual env
    Activates user virtual env
    Registers HTTP messqge handlers, editors, returns modified HTTP request/response
    Registers HTTP messqge handlers, editors...
    Calls the extension initializing method

    Provides listeners for HTTP messages interception and messages editors.

    Calls extension
     callbacks 
    Calls the extension initi...
    Provides a Java API to execute Python and automatically convert objects to their Java/Python equivalents.
    Provides a Java API to execute Pytho...
    Convert Burp objects to Pythonic custom objects 
    Convert Burp objects to Pythonic custo...
    Pass Burp objects
    Pass Burp object...
    Pass Burp objects
    Pass Burp object...
    Returns modified Python objects
    Returns modified Python objects
    Convert Pythonic objects to Burp objects
    Convert Pythonic objects to Burp o...
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/docs/public/schematics/scalpel-diagram.svg b/docs/public/schematics/scalpel-diagram.svg new file mode 100644 index 00000000..40978c00 --- /dev/null +++ b/docs/public/schematics/scalpel-diagram.svg @@ -0,0 +1,4 @@ + + + +
    Burp
    Burp
    Scalpel (Java Extension)
    Scalpel (Java Extens...
    _framework.py
    _framework.py
    Jep
    Jep
    <user script>
    def request ..
    def response ...
    def req_edit_in ...
    def req_edit_out ...
    <user script>...
    Loads and calls
    Loads and calls
    Handles the Python interpreter through native libraries
    Handles the Python interpreter...
    CPython
    CPython
    Activates user virtual env
    Activates user virtual env
    Registers HTTP message handlers, editors, returns modified HTTP request/response
    Registers HTTP message handlers, editors...
    Calls the extension initializing method

    Provides listeners for HTTP messages interception and messages editors.

    Calls extension
     callbacks 
    Calls the extension initi...
    Provides a Java API to execute Python and automatically convert objects to their Java/Python equivalents.
    Provides a Java API to execute Pytho...
    Convert Burp objects to Pythonic custom objects 
    Convert Burp objects to Pythonic custo...
    Pass Burp objects
    Pass Burp object...
    Pass Burp objects
    Pass Burp object...
    Returns modified Python objects
    Returns modified Python objects
    Convert Pythonic objects to Burp objects
    Convert Pythonic objects to Burp o...
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/docs/public/screenshots/aes-venv.png b/docs/public/screenshots/aes-venv.png new file mode 100644 index 00000000..e69e932a Binary files /dev/null and b/docs/public/screenshots/aes-venv.png differ diff --git a/docs/public/screenshots/bin-request.png b/docs/public/screenshots/bin-request.png new file mode 100644 index 00000000..fb20f13d Binary files /dev/null and b/docs/public/screenshots/bin-request.png differ diff --git a/docs/public/screenshots/bin-response.png b/docs/public/screenshots/bin-response.png new file mode 100644 index 00000000..227d00a7 Binary files /dev/null and b/docs/public/screenshots/bin-response.png differ diff --git a/docs/public/screenshots/choose_script.png b/docs/public/screenshots/choose_script.png new file mode 100644 index 00000000..40051eb0 Binary files /dev/null and b/docs/public/screenshots/choose_script.png differ diff --git a/docs/public/screenshots/create-script-edit.png b/docs/public/screenshots/create-script-edit.png new file mode 100644 index 00000000..35f80a81 Binary files /dev/null and b/docs/public/screenshots/create-script-edit.png differ diff --git a/docs/public/screenshots/create-script-prompt.png b/docs/public/screenshots/create-script-prompt.png new file mode 100644 index 00000000..e4a36415 Binary files /dev/null and b/docs/public/screenshots/create-script-prompt.png differ diff --git a/docs/public/screenshots/create-script-success.png b/docs/public/screenshots/create-script-success.png new file mode 100644 index 00000000..6d8a20b9 Binary files /dev/null and b/docs/public/screenshots/create-script-success.png differ diff --git a/docs/public/screenshots/create-script.png b/docs/public/screenshots/create-script.png new file mode 100644 index 00000000..b26607e9 Binary files /dev/null and b/docs/public/screenshots/create-script.png differ diff --git a/docs/public/screenshots/debug-image-1.png b/docs/public/screenshots/debug-image-1.png new file mode 100644 index 00000000..e017b08b Binary files /dev/null and b/docs/public/screenshots/debug-image-1.png differ diff --git a/docs/public/screenshots/debug-image-2.png b/docs/public/screenshots/debug-image-2.png new file mode 100644 index 00000000..383bf381 Binary files /dev/null and b/docs/public/screenshots/debug-image-2.png differ diff --git a/docs/public/screenshots/debug-image-3.png b/docs/public/screenshots/debug-image-3.png new file mode 100644 index 00000000..1128d586 Binary files /dev/null and b/docs/public/screenshots/debug-image-3.png differ diff --git a/docs/public/screenshots/debug-image.png b/docs/public/screenshots/debug-image.png new file mode 100644 index 00000000..4a996c5d Binary files /dev/null and b/docs/public/screenshots/debug-image.png differ diff --git a/docs/public/screenshots/decoded.png b/docs/public/screenshots/decoded.png new file mode 100644 index 00000000..d82f7089 Binary files /dev/null and b/docs/public/screenshots/decoded.png differ diff --git a/docs/public/screenshots/decrypted-response.png b/docs/public/screenshots/decrypted-response.png new file mode 100644 index 00000000..c7a76218 Binary files /dev/null and b/docs/public/screenshots/decrypted-response.png differ diff --git a/docs/public/screenshots/encrypt-edited.png b/docs/public/screenshots/encrypt-edited.png new file mode 100644 index 00000000..2a3bfbdc Binary files /dev/null and b/docs/public/screenshots/encrypt-edited.png differ diff --git a/docs/public/screenshots/encrypt-tab-selected.png b/docs/public/screenshots/encrypt-tab-selected.png new file mode 100644 index 00000000..9e1180bb Binary files /dev/null and b/docs/public/screenshots/encrypt-tab-selected.png differ diff --git a/docs/public/screenshots/encrypty-scalpel-tab.png b/docs/public/screenshots/encrypty-scalpel-tab.png new file mode 100644 index 00000000..852fc478 Binary files /dev/null and b/docs/public/screenshots/encrypty-scalpel-tab.png differ diff --git a/docs/public/screenshots/error-popup.png b/docs/public/screenshots/error-popup.png new file mode 100644 index 00000000..17a68a4f Binary files /dev/null and b/docs/public/screenshots/error-popup.png differ diff --git a/docs/public/screenshots/first-steps-0.png b/docs/public/screenshots/first-steps-0.png new file mode 100644 index 00000000..8c41184d Binary files /dev/null and b/docs/public/screenshots/first-steps-0.png differ diff --git a/docs/public/screenshots/first-steps-1.png b/docs/public/screenshots/first-steps-1.png new file mode 100644 index 00000000..82f9ddcf Binary files /dev/null and b/docs/public/screenshots/first-steps-1.png differ diff --git a/docs/public/screenshots/first-steps-10.png b/docs/public/screenshots/first-steps-10.png new file mode 100644 index 00000000..04e23e04 Binary files /dev/null and b/docs/public/screenshots/first-steps-10.png differ diff --git a/docs/public/screenshots/first-steps-2.png b/docs/public/screenshots/first-steps-2.png new file mode 100644 index 00000000..8c41184d Binary files /dev/null and b/docs/public/screenshots/first-steps-2.png differ diff --git a/docs/public/screenshots/first-steps-3.png b/docs/public/screenshots/first-steps-3.png new file mode 100644 index 00000000..3024b8aa Binary files /dev/null and b/docs/public/screenshots/first-steps-3.png differ diff --git a/docs/public/screenshots/first-steps-4.png b/docs/public/screenshots/first-steps-4.png new file mode 100644 index 00000000..16c95cf4 Binary files /dev/null and b/docs/public/screenshots/first-steps-4.png differ diff --git a/docs/public/screenshots/first-steps-5.png b/docs/public/screenshots/first-steps-5.png new file mode 100644 index 00000000..c6a55c03 Binary files /dev/null and b/docs/public/screenshots/first-steps-5.png differ diff --git a/docs/public/screenshots/first-steps-6.png b/docs/public/screenshots/first-steps-6.png new file mode 100644 index 00000000..9aa8399a Binary files /dev/null and b/docs/public/screenshots/first-steps-6.png differ diff --git a/docs/public/screenshots/first-steps-7.png b/docs/public/screenshots/first-steps-7.png new file mode 100644 index 00000000..c396b2fc Binary files /dev/null and b/docs/public/screenshots/first-steps-7.png differ diff --git a/docs/public/screenshots/first-steps-8.png b/docs/public/screenshots/first-steps-8.png new file mode 100644 index 00000000..6165106b Binary files /dev/null and b/docs/public/screenshots/first-steps-8.png differ diff --git a/docs/public/screenshots/first-steps-9.png b/docs/public/screenshots/first-steps-9.png new file mode 100644 index 00000000..b660ee9b Binary files /dev/null and b/docs/public/screenshots/first-steps-9.png differ diff --git a/docs/public/screenshots/import.png b/docs/public/screenshots/import.png new file mode 100644 index 00000000..1edb1cb1 Binary files /dev/null and b/docs/public/screenshots/import.png differ diff --git a/docs/public/screenshots/init.png b/docs/public/screenshots/init.png new file mode 100644 index 00000000..b1145640 Binary files /dev/null and b/docs/public/screenshots/init.png differ diff --git a/docs/public/screenshots/mitmproxy.png b/docs/public/screenshots/mitmproxy.png new file mode 100644 index 00000000..3fd14b9d Binary files /dev/null and b/docs/public/screenshots/mitmproxy.png differ diff --git a/docs/public/screenshots/mitmweb.png b/docs/public/screenshots/mitmweb.png new file mode 100644 index 00000000..5f0cc925 Binary files /dev/null and b/docs/public/screenshots/mitmweb.png differ diff --git a/docs/public/screenshots/multiple_params.png b/docs/public/screenshots/multiple_params.png new file mode 100644 index 00000000..772df9d6 Binary files /dev/null and b/docs/public/screenshots/multiple_params.png differ diff --git a/docs/public/screenshots/multiple_tabs.png b/docs/public/screenshots/multiple_tabs.png new file mode 100644 index 00000000..4d64b8d0 Binary files /dev/null and b/docs/public/screenshots/multiple_tabs.png differ diff --git a/docs/public/screenshots/output.png b/docs/public/screenshots/output.png new file mode 100644 index 00000000..398e5600 Binary files /dev/null and b/docs/public/screenshots/output.png differ diff --git a/docs/public/screenshots/release.png b/docs/public/screenshots/release.png new file mode 100644 index 00000000..8e9cb937 Binary files /dev/null and b/docs/public/screenshots/release.png differ diff --git a/docs/public/screenshots/select-venv.png b/docs/public/screenshots/select-venv.png new file mode 100644 index 00000000..e13fb41b Binary files /dev/null and b/docs/public/screenshots/select-venv.png differ diff --git a/docs/public/screenshots/tabs.png b/docs/public/screenshots/tabs.png new file mode 100644 index 00000000..e81a3d87 Binary files /dev/null and b/docs/public/screenshots/tabs.png differ diff --git a/docs/public/screenshots/terminal.png b/docs/public/screenshots/terminal.png new file mode 100644 index 00000000..9a0747ab Binary files /dev/null and b/docs/public/screenshots/terminal.png differ diff --git a/docs/public/screenshots/traversal.png b/docs/public/screenshots/traversal.png new file mode 100644 index 00000000..cf4c5a07 Binary files /dev/null and b/docs/public/screenshots/traversal.png differ diff --git a/docs/public/screenshots/updated.png b/docs/public/screenshots/updated.png new file mode 100644 index 00000000..668f1272 Binary files /dev/null and b/docs/public/screenshots/updated.png differ diff --git a/docs/public/screenshots/urlencode.png b/docs/public/screenshots/urlencode.png new file mode 100644 index 00000000..9f3b25df Binary files /dev/null and b/docs/public/screenshots/urlencode.png differ diff --git a/docs/public/screenshots/venv-installing.png b/docs/public/screenshots/venv-installing.png new file mode 100644 index 00000000..d7cc8ebb Binary files /dev/null and b/docs/public/screenshots/venv-installing.png differ diff --git a/docs/public/screenshots/venv-pycryptodome.png b/docs/public/screenshots/venv-pycryptodome.png new file mode 100644 index 00000000..46cd621b Binary files /dev/null and b/docs/public/screenshots/venv-pycryptodome.png differ diff --git a/docs/public/screenshots/venv.png b/docs/public/screenshots/venv.png new file mode 100644 index 00000000..367f3af5 Binary files /dev/null and b/docs/public/screenshots/venv.png differ diff --git a/docs/public/screenshots/wait.png b/docs/public/screenshots/wait.png new file mode 100644 index 00000000..c34aa899 Binary files /dev/null and b/docs/public/screenshots/wait.png differ diff --git a/docs/public/sitemap.xml b/docs/public/sitemap.xml new file mode 100644 index 00000000..ef69e9ba --- /dev/null +++ b/docs/public/sitemap.xml @@ -0,0 +1,55 @@ + + + + /addons-debugging/ + + /api/ + + /categories/ + + /feature-editors/ + + /tute-aes/ + + /api/events.html + + /addons-examples/ + + /overview-faq/ + + /tute-first-steps/ + + /concepts-howscalpelworks/ + + /overview-installation/ + + /feature-http/ + + / + + /api/pyscalpel/edit.html + + /api/pyscalpel/encoding.html + + /api/pyscalpel/events.html + + /api/pyscalpel/http.html + + /api/pyscalpel/http/body.html + + /api/pyscalpel/java.html + + /api/pyscalpel/java/burp.html + + /api/pyscalpel/utils.html + + /api/pyscalpel/venv.html + + /tags/ + + /overview-usage/ + + /addons-java/ + + diff --git a/docs/public/style.min.css b/docs/public/style.min.css new file mode 100644 index 00000000..1337ce83 --- /dev/null +++ b/docs/public/style.min.css @@ -0,0 +1,2 @@ +.chroma span.linenos{color:inherit;background-color:transparent;padding-left:5px;padding-right:20px}.chroma{background-color:#fff}.chroma .err{color:#a61717;background-color:#e3d2d2}.chroma .lntd{vertical-align:top;padding:0;margin:0;border:0}.chroma .lntable{border-spacing:0;padding:0;margin:0;border:0;width:auto;overflow:auto;display:block}.chroma .hl{display:block;width:100%;background-color:#ffc}.chroma .lnt{margin-right:.4em;padding:0 .4em;color:#7f7f7f}.chroma .ln{margin-right:.4em;padding:0 .4em;color:#7f7f7f}.chroma .k{color:#000;font-weight:700}.chroma .kc{color:#000;font-weight:700}.chroma .kd{color:#000;font-weight:700}.chroma .kn{color:#000;font-weight:700}.chroma .kp{color:#000;font-weight:700}.chroma .kr{color:#000;font-weight:700}.chroma .kt{color:#458;font-weight:700}.chroma .na{color:teal}.chroma .nb{color:#0086b3}.chroma .bp{color:#999}.chroma .nc{color:#458;font-weight:700}.chroma .no{color:teal}.chroma .nd{color:#3c5d5d;font-weight:700}.chroma .ni{color:purple}.chroma .ne{color:#900;font-weight:700}.chroma .nf{color:#900;font-weight:700}.chroma .nl{color:#900;font-weight:700}.chroma .nn{color:#555}.chroma .nt{color:navy}.chroma .nv{color:teal}.chroma .vc{color:teal}.chroma .vg{color:teal}.chroma .vi{color:teal}.chroma .s{color:#d14}.chroma .sa{color:#d14}.chroma .sb{color:#d14}.chroma .sc{color:#d14}.chroma .dl{color:#d14}.chroma .sd{color:#d14}.chroma .s2{color:#d14}.chroma .se{color:#d14}.chroma .sh{color:#d14}.chroma .si{color:#d14}.chroma .sx{color:#d14}.chroma .sr{color:#009926}.chroma .s1{color:#d14}.chroma .ss{color:#990073}.chroma .m{color:#099}.chroma .mb{color:#099}.chroma .mf{color:#099}.chroma .mh{color:#099}.chroma .mi{color:#099}.chroma .il{color:#099}.chroma .mo{color:#099}.chroma .o{color:#000;font-weight:700}.chroma .ow{color:#000;font-weight:700}.chroma .c{color:#998;font-style:italic}.chroma .ch{color:#998;font-style:italic}.chroma .cm{color:#998;font-style:italic}.chroma .c1{color:#998;font-style:italic}.chroma .cs{color:#999;font-weight:700;font-style:italic}.chroma .cp{color:#999;font-weight:700;font-style:italic}.chroma .cpf{color:#999;font-weight:700;font-style:italic}.chroma .gd{color:#000;background-color:#fdd}.chroma .ge{color:#000;font-style:italic}.chroma .gr{color:#a00}.chroma .gh{color:#999}.chroma .gi{color:#000;background-color:#dfd}.chroma .go{color:#888}.chroma .gp{color:#555}.chroma .gs{font-weight:700}.chroma .gu{color:#aaa}.chroma .gt{color:#a00}.chroma .gl{text-decoration:underline}.chroma .w{color:#bbb}.chroma pre,pre.chroma{background-color:#f7f7f7;border-top:1px solid #ccc;border-bottom:1px solid #ccc;padding:.5rem 0 .5rem .5rem}.badge{color:#fff;background-color:#6c757d;display:inline-block;padding:.25em .4em;font-size:75%;font-weight:1;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}/*!* +bulma.io v0.8.0 | MIT License | github.com/jgthms/bulma*/@keyframes spinAround{from{transform:rotate(0)}to{transform:rotate(359deg)}}.tabs,.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis,.breadcrumb,.button,.is-unselectable,.modal-close,.delete{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navbar-link:not(.is-arrowless)::after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:" ";display:block;height:.625em;margin-top:-.4375em;pointer-events:none;position:absolute;top:50%;transform:rotate(-45deg);transform-origin:center;width:.625em}.tabs:not(:last-child),.pagination:not(:last-child),.message:not(:last-child),.list:not(:last-child),.level:not(:last-child),.breadcrumb:not(:last-child),.highlight:not(:last-child),.block:not(:last-child),.title:not(:last-child),.subtitle:not(:last-child),.table-container:not(:last-child),.table:not(:last-child),.progress:not(:last-child),.notification:not(:last-child),.content:not(:last-child),.box:not(:last-child){margin-bottom:1.5rem}.modal-close,.delete{-moz-appearance:none;-webkit-appearance:none;background-color:rgba(10,10,10,.2);border:none;border-radius:290486px;cursor:pointer;pointer-events:auto;display:inline-block;flex-grow:0;flex-shrink:0;font-size:0;height:20px;max-height:20px;max-width:20px;min-height:20px;min-width:20px;outline:none;position:relative;vertical-align:top;width:20px}.modal-close::before,.delete::before,.modal-close::after,.delete::after{background-color:#fff;content:"";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%)translateY(-50%)rotate(45deg);transform-origin:center center}.modal-close::before,.delete::before{height:2px;width:50%}.modal-close::after,.delete::after{height:50%;width:2px}.modal-close:hover,.delete:hover,.modal-close:focus,.delete:focus{background-color:rgba(10,10,10,.3)}.modal-close:active,.delete:active{background-color:rgba(10,10,10,.4)}.is-small.modal-close,.is-small.delete{height:16px;max-height:16px;max-width:16px;min-height:16px;min-width:16px;width:16px}.is-medium.modal-close,.is-medium.delete{height:24px;max-height:24px;max-width:24px;min-height:24px;min-width:24px;width:24px}.is-large.modal-close,.is-large.delete{height:32px;max-height:32px;max-width:32px;min-height:32px;min-width:32px;width:32px}.loader,.button.is-loading::after{animation:spinAround 500ms infinite linear;border:2px solid #dbdbdb;border-radius:290486px;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:1em;position:relative;width:1em}.hero-video,.modal-background,.modal,.image.is-square img,.image.is-square .has-ratio,.image.is-1by1 img,.image.is-1by1 .has-ratio,.image.is-5by4 img,.image.is-5by4 .has-ratio,.image.is-4by3 img,.image.is-4by3 .has-ratio,.image.is-3by2 img,.image.is-3by2 .has-ratio,.image.is-5by3 img,.image.is-5by3 .has-ratio,.image.is-16by9 img,.image.is-16by9 .has-ratio,.image.is-2by1 img,.image.is-2by1 .has-ratio,.image.is-3by1 img,.image.is-3by1 .has-ratio,.image.is-4by5 img,.image.is-4by5 .has-ratio,.image.is-3by4 img,.image.is-3by4 .has-ratio,.image.is-2by3 img,.image.is-2by3 .has-ratio,.image.is-3by5 img,.image.is-3by5 .has-ratio,.image.is-9by16 img,.image.is-9by16 .has-ratio,.image.is-1by2 img,.image.is-1by2 .has-ratio,.image.is-1by3 img,.image.is-1by3 .has-ratio,.is-overlay{bottom:0;left:0;position:absolute;right:0;top:0}.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis,.button{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:1px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.5em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(.5em - 1px);padding-left:calc(.75em - 1px);padding-right:calc(.75em - 1px);padding-top:calc(.5em - 1px);position:relative;vertical-align:top}.pagination-previous:focus,.pagination-next:focus,.pagination-link:focus,.pagination-ellipsis:focus,.button:focus,.is-focused.pagination-previous,.is-focused.pagination-next,.is-focused.pagination-link,.is-focused.pagination-ellipsis,.is-focused.button,.pagination-previous:active,.pagination-next:active,.pagination-link:active,.pagination-ellipsis:active,.button:active,.is-active.pagination-previous,.is-active.pagination-next,.is-active.pagination-link,.is-active.pagination-ellipsis,.is-active.button{outline:none}[disabled].pagination-previous,[disabled].pagination-next,[disabled].pagination-link,[disabled].pagination-ellipsis,[disabled].button,fieldset[disabled] .pagination-previous,fieldset[disabled] .pagination-next,fieldset[disabled] .pagination-link,fieldset[disabled] .pagination-ellipsis,fieldset[disabled] .button{cursor:not-allowed}/*!minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css*/html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:400}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}td:not([align]),th:not([align]){text-align:left}html{background-color:#fff;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;text-size-adjust:100%}article,aside,figure,footer,header,hgroup,section{display:block}body,button,input,select,textarea{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif,"font awesome 5 free","font awesome 5 brands"}code,pre{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#4a4a4a;font-size:1em;font-weight:400;line-height:1.5}a{color:#3273dc;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#f14668;font-size:.875em;font-weight:400;padding:.25em .5em}hr{background-color:#f5f5f5;border:none;display:block;height:2px;margin:1.5rem 0}img{height:auto;max-width:100%}input[type=checkbox],input[type=radio]{vertical-align:baseline}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:700}fieldset{border:none}pre{-webkit-overflow-scrolling:touch;background-color:#f5f5f5;color:#4a4a4a;font-size:.875em;overflow-x:auto;padding:1.25rem 1.5rem;white-space:pre;word-wrap:normal}pre code{background-color:transparent;color:currentColor;font-size:1em;padding:0}table td,table th{vertical-align:top}table td:not([align]),table th:not([align]){text-align:left}table th{color:#363636}.is-clearfix::after{clear:both;content:" ";display:table}.is-pulled-left{float:left!important}.is-pulled-right{float:right!important}.is-clipped{overflow:hidden!important}.is-size-1{font-size:3rem!important}.is-size-2{font-size:2.5rem!important}.is-size-3{font-size:2rem!important}.is-size-4{font-size:1.5rem!important}.is-size-5{font-size:1.25rem!important}.is-size-6{font-size:1rem!important}.is-size-7{font-size:.75rem!important}@media screen and (max-width:768px){.is-size-1-mobile{font-size:3rem!important}.is-size-2-mobile{font-size:2.5rem!important}.is-size-3-mobile{font-size:2rem!important}.is-size-4-mobile{font-size:1.5rem!important}.is-size-5-mobile{font-size:1.25rem!important}.is-size-6-mobile{font-size:1rem!important}.is-size-7-mobile{font-size:.75rem!important}}@media screen and (min-width:769px),print{.is-size-1-tablet{font-size:3rem!important}.is-size-2-tablet{font-size:2.5rem!important}.is-size-3-tablet{font-size:2rem!important}.is-size-4-tablet{font-size:1.5rem!important}.is-size-5-tablet{font-size:1.25rem!important}.is-size-6-tablet{font-size:1rem!important}.is-size-7-tablet{font-size:.75rem!important}}@media screen and (max-width:1023px){.is-size-1-touch{font-size:3rem!important}.is-size-2-touch{font-size:2.5rem!important}.is-size-3-touch{font-size:2rem!important}.is-size-4-touch{font-size:1.5rem!important}.is-size-5-touch{font-size:1.25rem!important}.is-size-6-touch{font-size:1rem!important}.is-size-7-touch{font-size:.75rem!important}}@media screen and (min-width:1024px){.is-size-1-desktop{font-size:3rem!important}.is-size-2-desktop{font-size:2.5rem!important}.is-size-3-desktop{font-size:2rem!important}.is-size-4-desktop{font-size:1.5rem!important}.is-size-5-desktop{font-size:1.25rem!important}.is-size-6-desktop{font-size:1rem!important}.is-size-7-desktop{font-size:.75rem!important}}@media screen and (min-width:1216px){.is-size-1-widescreen{font-size:3rem!important}.is-size-2-widescreen{font-size:2.5rem!important}.is-size-3-widescreen{font-size:2rem!important}.is-size-4-widescreen{font-size:1.5rem!important}.is-size-5-widescreen{font-size:1.25rem!important}.is-size-6-widescreen{font-size:1rem!important}.is-size-7-widescreen{font-size:.75rem!important}}@media screen and (min-width:1408px){.is-size-1-fullhd{font-size:3rem!important}.is-size-2-fullhd{font-size:2.5rem!important}.is-size-3-fullhd{font-size:2rem!important}.is-size-4-fullhd{font-size:1.5rem!important}.is-size-5-fullhd{font-size:1.25rem!important}.is-size-6-fullhd{font-size:1rem!important}.is-size-7-fullhd{font-size:.75rem!important}}.has-text-centered{text-align:center!important}.has-text-justified{text-align:justify!important}.has-text-left{text-align:left!important}.has-text-right{text-align:right!important}@media screen and (max-width:768px){.has-text-centered-mobile{text-align:center!important}}@media screen and (min-width:769px),print{.has-text-centered-tablet{text-align:center!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-centered-tablet-only{text-align:center!important}}@media screen and (max-width:1023px){.has-text-centered-touch{text-align:center!important}}@media screen and (min-width:1024px){.has-text-centered-desktop{text-align:center!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-centered-desktop-only{text-align:center!important}}@media screen and (min-width:1216px){.has-text-centered-widescreen{text-align:center!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-centered-widescreen-only{text-align:center!important}}@media screen and (min-width:1408px){.has-text-centered-fullhd{text-align:center!important}}@media screen and (max-width:768px){.has-text-justified-mobile{text-align:justify!important}}@media screen and (min-width:769px),print{.has-text-justified-tablet{text-align:justify!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-justified-tablet-only{text-align:justify!important}}@media screen and (max-width:1023px){.has-text-justified-touch{text-align:justify!important}}@media screen and (min-width:1024px){.has-text-justified-desktop{text-align:justify!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-justified-desktop-only{text-align:justify!important}}@media screen and (min-width:1216px){.has-text-justified-widescreen{text-align:justify!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-justified-widescreen-only{text-align:justify!important}}@media screen and (min-width:1408px){.has-text-justified-fullhd{text-align:justify!important}}@media screen and (max-width:768px){.has-text-left-mobile{text-align:left!important}}@media screen and (min-width:769px),print{.has-text-left-tablet{text-align:left!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-left-tablet-only{text-align:left!important}}@media screen and (max-width:1023px){.has-text-left-touch{text-align:left!important}}@media screen and (min-width:1024px){.has-text-left-desktop{text-align:left!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-left-desktop-only{text-align:left!important}}@media screen and (min-width:1216px){.has-text-left-widescreen{text-align:left!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-left-widescreen-only{text-align:left!important}}@media screen and (min-width:1408px){.has-text-left-fullhd{text-align:left!important}}@media screen and (max-width:768px){.has-text-right-mobile{text-align:right!important}}@media screen and (min-width:769px),print{.has-text-right-tablet{text-align:right!important}}@media screen and (min-width:769px) and (max-width:1023px){.has-text-right-tablet-only{text-align:right!important}}@media screen and (max-width:1023px){.has-text-right-touch{text-align:right!important}}@media screen and (min-width:1024px){.has-text-right-desktop{text-align:right!important}}@media screen and (min-width:1024px) and (max-width:1215px){.has-text-right-desktop-only{text-align:right!important}}@media screen and (min-width:1216px){.has-text-right-widescreen{text-align:right!important}}@media screen and (min-width:1216px) and (max-width:1407px){.has-text-right-widescreen-only{text-align:right!important}}@media screen and (min-width:1408px){.has-text-right-fullhd{text-align:right!important}}.is-capitalized{text-transform:capitalize!important}.is-lowercase{text-transform:lowercase!important}.is-uppercase{text-transform:uppercase!important}.is-italic{font-style:italic!important}.has-text-white{color:#fff!important}a.has-text-white:hover,a.has-text-white:focus{color:#e6e6e6!important}.has-background-white{background-color:#fff!important}.has-text-black{color:#0a0a0a!important}a.has-text-black:hover,a.has-text-black:focus{color:#000!important}.has-background-black{background-color:#0a0a0a!important}.has-text-light{color:#f5f5f5!important}a.has-text-light:hover,a.has-text-light:focus{color:#dbdbdb!important}.has-background-light{background-color:#f5f5f5!important}.has-text-dark{color:#363636!important}a.has-text-dark:hover,a.has-text-dark:focus{color:#1c1c1c!important}.has-background-dark{background-color:#363636!important}.has-text-primary{color:#c93312!important}a.has-text-primary:hover,a.has-text-primary:focus{color:#9a270e!important}.has-background-primary{background-color:#c93312!important}.has-text-link{color:#3273dc!important}a.has-text-link:hover,a.has-text-link:focus{color:#205bbc!important}.has-background-link{background-color:#3273dc!important}.has-text-info{color:#3298dc!important}a.has-text-info:hover,a.has-text-info:focus{color:#207dbc!important}.has-background-info{background-color:#3298dc!important}.has-text-success{color:#48c774!important}a.has-text-success:hover,a.has-text-success:focus{color:#34a85c!important}.has-background-success{background-color:#48c774!important}.has-text-warning{color:#ffdd57!important}a.has-text-warning:hover,a.has-text-warning:focus{color:#ffd324!important}.has-background-warning{background-color:#ffdd57!important}.has-text-danger{color:#f14668!important}a.has-text-danger:hover,a.has-text-danger:focus{color:#ee1742!important}.has-background-danger{background-color:#f14668!important}.has-text-black-bis{color:#121212!important}.has-background-black-bis{background-color:#121212!important}.has-text-black-ter{color:#242424!important}.has-background-black-ter{background-color:#242424!important}.has-text-grey-darker{color:#363636!important}.has-background-grey-darker{background-color:#363636!important}.has-text-grey-dark{color:#4a4a4a!important}.has-background-grey-dark{background-color:#4a4a4a!important}.has-text-grey{color:#7a7a7a!important}.has-background-grey{background-color:#7a7a7a!important}.has-text-grey-light{color:#b5b5b5!important}.has-background-grey-light{background-color:#b5b5b5!important}.has-text-grey-lighter{color:#dbdbdb!important}.has-background-grey-lighter{background-color:#dbdbdb!important}.has-text-white-ter{color:#f5f5f5!important}.has-background-white-ter{background-color:#f5f5f5!important}.has-text-white-bis{color:#fafafa!important}.has-background-white-bis{background-color:#fafafa!important}.has-text-weight-light{font-weight:300!important}.has-text-weight-normal{font-weight:400!important}.has-text-weight-medium{font-weight:500!important}.has-text-weight-semibold{font-weight:600!important}.has-text-weight-bold{font-weight:700!important}.is-family-primary{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif,"font awesome 5 free","font awesome 5 brands"!important}.is-family-secondary{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif,"font awesome 5 free","font awesome 5 brands"!important}.is-family-sans-serif{font-family:BlinkMacSystemFont,-apple-system,segoe ui,roboto,oxygen,ubuntu,cantarell,fira sans,droid sans,helvetica neue,helvetica,arial,sans-serif,"font awesome 5 free","font awesome 5 brands"!important}.is-family-monospace{font-family:monospace!important}.is-family-code{font-family:monospace!important}.is-block{display:block!important}@media screen and (max-width:768px){.is-block-mobile{display:block!important}}@media screen and (min-width:769px),print{.is-block-tablet{display:block!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-block-tablet-only{display:block!important}}@media screen and (max-width:1023px){.is-block-touch{display:block!important}}@media screen and (min-width:1024px){.is-block-desktop{display:block!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-block-desktop-only{display:block!important}}@media screen and (min-width:1216px){.is-block-widescreen{display:block!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-block-widescreen-only{display:block!important}}@media screen and (min-width:1408px){.is-block-fullhd{display:block!important}}.is-flex{display:flex!important}@media screen and (max-width:768px){.is-flex-mobile{display:flex!important}}@media screen and (min-width:769px),print{.is-flex-tablet{display:flex!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-flex-tablet-only{display:flex!important}}@media screen and (max-width:1023px){.is-flex-touch{display:flex!important}}@media screen and (min-width:1024px){.is-flex-desktop{display:flex!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-flex-desktop-only{display:flex!important}}@media screen and (min-width:1216px){.is-flex-widescreen{display:flex!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-flex-widescreen-only{display:flex!important}}@media screen and (min-width:1408px){.is-flex-fullhd{display:flex!important}}.is-inline{display:inline!important}@media screen and (max-width:768px){.is-inline-mobile{display:inline!important}}@media screen and (min-width:769px),print{.is-inline-tablet{display:inline!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-tablet-only{display:inline!important}}@media screen and (max-width:1023px){.is-inline-touch{display:inline!important}}@media screen and (min-width:1024px){.is-inline-desktop{display:inline!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-desktop-only{display:inline!important}}@media screen and (min-width:1216px){.is-inline-widescreen{display:inline!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-widescreen-only{display:inline!important}}@media screen and (min-width:1408px){.is-inline-fullhd{display:inline!important}}.is-inline-block{display:inline-block!important}@media screen and (max-width:768px){.is-inline-block-mobile{display:inline-block!important}}@media screen and (min-width:769px),print{.is-inline-block-tablet{display:inline-block!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-block-tablet-only{display:inline-block!important}}@media screen and (max-width:1023px){.is-inline-block-touch{display:inline-block!important}}@media screen and (min-width:1024px){.is-inline-block-desktop{display:inline-block!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-block-desktop-only{display:inline-block!important}}@media screen and (min-width:1216px){.is-inline-block-widescreen{display:inline-block!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-block-widescreen-only{display:inline-block!important}}@media screen and (min-width:1408px){.is-inline-block-fullhd{display:inline-block!important}}.is-inline-flex{display:inline-flex!important}@media screen and (max-width:768px){.is-inline-flex-mobile{display:inline-flex!important}}@media screen and (min-width:769px),print{.is-inline-flex-tablet{display:inline-flex!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-inline-flex-tablet-only{display:inline-flex!important}}@media screen and (max-width:1023px){.is-inline-flex-touch{display:inline-flex!important}}@media screen and (min-width:1024px){.is-inline-flex-desktop{display:inline-flex!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-inline-flex-desktop-only{display:inline-flex!important}}@media screen and (min-width:1216px){.is-inline-flex-widescreen{display:inline-flex!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-inline-flex-widescreen-only{display:inline-flex!important}}@media screen and (min-width:1408px){.is-inline-flex-fullhd{display:inline-flex!important}}.is-hidden{display:none!important}.is-sr-only{border:none!important;clip:rect(0,0,0,0)!important;height:.01em!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:.01em!important}@media screen and (max-width:768px){.is-hidden-mobile{display:none!important}}@media screen and (min-width:769px),print{.is-hidden-tablet{display:none!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-hidden-tablet-only{display:none!important}}@media screen and (max-width:1023px){.is-hidden-touch{display:none!important}}@media screen and (min-width:1024px){.is-hidden-desktop{display:none!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-hidden-desktop-only{display:none!important}}@media screen and (min-width:1216px){.is-hidden-widescreen{display:none!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-hidden-widescreen-only{display:none!important}}@media screen and (min-width:1408px){.is-hidden-fullhd{display:none!important}}.is-invisible{visibility:hidden!important}@media screen and (max-width:768px){.is-invisible-mobile{visibility:hidden!important}}@media screen and (min-width:769px),print{.is-invisible-tablet{visibility:hidden!important}}@media screen and (min-width:769px) and (max-width:1023px){.is-invisible-tablet-only{visibility:hidden!important}}@media screen and (max-width:1023px){.is-invisible-touch{visibility:hidden!important}}@media screen and (min-width:1024px){.is-invisible-desktop{visibility:hidden!important}}@media screen and (min-width:1024px) and (max-width:1215px){.is-invisible-desktop-only{visibility:hidden!important}}@media screen and (min-width:1216px){.is-invisible-widescreen{visibility:hidden!important}}@media screen and (min-width:1216px) and (max-width:1407px){.is-invisible-widescreen-only{visibility:hidden!important}}@media screen and (min-width:1408px){.is-invisible-fullhd{visibility:hidden!important}}.is-marginless{margin:0!important}.is-paddingless{padding:0!important}.is-radiusless{border-radius:0!important}.is-shadowless{box-shadow:none!important}.is-relative{position:relative!important}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-0{flex:none;width:0%}.columns.is-mobile>.column.is-offset-0{margin-left:0%}.columns.is-mobile>.column.is-1{flex:none;width:8.33333333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333333%}.columns.is-mobile>.column.is-2{flex:none;width:16.66666667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66666667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.33333333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333333%}.columns.is-mobile>.column.is-5{flex:none;width:41.66666667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66666667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.33333333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333333%}.columns.is-mobile>.column.is-8{flex:none;width:66.66666667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66666667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.33333333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333333%}.columns.is-mobile>.column.is-11{flex:none;width:91.66666667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66666667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width:768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-0-mobile{flex:none;width:0%}.column.is-offset-0-mobile{margin-left:0%}.column.is-1-mobile{flex:none;width:8.33333333%}.column.is-offset-1-mobile{margin-left:8.33333333%}.column.is-2-mobile{flex:none;width:16.66666667%}.column.is-offset-2-mobile{margin-left:16.66666667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.33333333%}.column.is-offset-4-mobile{margin-left:33.33333333%}.column.is-5-mobile{flex:none;width:41.66666667%}.column.is-offset-5-mobile{margin-left:41.66666667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.33333333%}.column.is-offset-7-mobile{margin-left:58.33333333%}.column.is-8-mobile{flex:none;width:66.66666667%}.column.is-offset-8-mobile{margin-left:66.66666667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.33333333%}.column.is-offset-10-mobile{margin-left:83.33333333%}.column.is-11-mobile{flex:none;width:91.66666667%}.column.is-offset-11-mobile{margin-left:91.66666667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width:769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-0,.column.is-0-tablet{flex:none;width:0%}.column.is-offset-0,.column.is-offset-0-tablet{margin-left:0%}.column.is-1,.column.is-1-tablet{flex:none;width:8.33333333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.66666667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66666667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.33333333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.66666667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66666667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.33333333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.66666667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66666667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.33333333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.66666667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66666667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width:1023px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-0-touch{flex:none;width:0%}.column.is-offset-0-touch{margin-left:0%}.column.is-1-touch{flex:none;width:8.33333333%}.column.is-offset-1-touch{margin-left:8.33333333%}.column.is-2-touch{flex:none;width:16.66666667%}.column.is-offset-2-touch{margin-left:16.66666667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.33333333%}.column.is-offset-4-touch{margin-left:33.33333333%}.column.is-5-touch{flex:none;width:41.66666667%}.column.is-offset-5-touch{margin-left:41.66666667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.33333333%}.column.is-offset-7-touch{margin-left:58.33333333%}.column.is-8-touch{flex:none;width:66.66666667%}.column.is-offset-8-touch{margin-left:66.66666667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.33333333%}.column.is-offset-10-touch{margin-left:83.33333333%}.column.is-11-touch{flex:none;width:91.66666667%}.column.is-offset-11-touch{margin-left:91.66666667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width:1024px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-0-desktop{flex:none;width:0%}.column.is-offset-0-desktop{margin-left:0%}.column.is-1-desktop{flex:none;width:8.33333333%}.column.is-offset-1-desktop{margin-left:8.33333333%}.column.is-2-desktop{flex:none;width:16.66666667%}.column.is-offset-2-desktop{margin-left:16.66666667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.33333333%}.column.is-offset-4-desktop{margin-left:33.33333333%}.column.is-5-desktop{flex:none;width:41.66666667%}.column.is-offset-5-desktop{margin-left:41.66666667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.33333333%}.column.is-offset-7-desktop{margin-left:58.33333333%}.column.is-8-desktop{flex:none;width:66.66666667%}.column.is-offset-8-desktop{margin-left:66.66666667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.33333333%}.column.is-offset-10-desktop{margin-left:83.33333333%}.column.is-11-desktop{flex:none;width:91.66666667%}.column.is-offset-11-desktop{margin-left:91.66666667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width:1216px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-0-widescreen{flex:none;width:0%}.column.is-offset-0-widescreen{margin-left:0%}.column.is-1-widescreen{flex:none;width:8.33333333%}.column.is-offset-1-widescreen{margin-left:8.33333333%}.column.is-2-widescreen{flex:none;width:16.66666667%}.column.is-offset-2-widescreen{margin-left:16.66666667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.33333333%}.column.is-offset-4-widescreen{margin-left:33.33333333%}.column.is-5-widescreen{flex:none;width:41.66666667%}.column.is-offset-5-widescreen{margin-left:41.66666667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.33333333%}.column.is-offset-7-widescreen{margin-left:58.33333333%}.column.is-8-widescreen{flex:none;width:66.66666667%}.column.is-offset-8-widescreen{margin-left:66.66666667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.33333333%}.column.is-offset-10-widescreen{margin-left:83.33333333%}.column.is-11-widescreen{flex:none;width:91.66666667%}.column.is-offset-11-widescreen{margin-left:91.66666667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width:1408px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-0-fullhd{flex:none;width:0%}.column.is-offset-0-fullhd{margin-left:0%}.column.is-1-fullhd{flex:none;width:8.33333333%}.column.is-offset-1-fullhd{margin-left:8.33333333%}.column.is-2-fullhd{flex:none;width:16.66666667%}.column.is-offset-2-fullhd{margin-left:16.66666667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.33333333%}.column.is-offset-4-fullhd{margin-left:33.33333333%}.column.is-5-fullhd{flex:none;width:41.66666667%}.column.is-offset-5-fullhd{margin-left:41.66666667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.33333333%}.column.is-offset-7-fullhd{margin-left:58.33333333%}.column.is-8-fullhd{flex:none;width:66.66666667%}.column.is-offset-8-fullhd{margin-left:66.66666667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.33333333%}.column.is-offset-10-fullhd{margin-left:83.33333333%}.column.is-11-fullhd{flex:none;width:91.66666667%}.column.is-offset-11-fullhd{margin-left:91.66666667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0!important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width:769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width:1024px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap:0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap:0rem}@media screen and (max-width:768px){.columns.is-variable.is-0-mobile{--columnGap:0rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-0-tablet{--columnGap:0rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-0-tablet-only{--columnGap:0rem}}@media screen and (max-width:1023px){.columns.is-variable.is-0-touch{--columnGap:0rem}}@media screen and (min-width:1024px){.columns.is-variable.is-0-desktop{--columnGap:0rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-0-desktop-only{--columnGap:0rem}}@media screen and (min-width:1216px){.columns.is-variable.is-0-widescreen{--columnGap:0rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-0-widescreen-only{--columnGap:0rem}}@media screen and (min-width:1408px){.columns.is-variable.is-0-fullhd{--columnGap:0rem}}.columns.is-variable.is-1{--columnGap:0.25rem}@media screen and (max-width:768px){.columns.is-variable.is-1-mobile{--columnGap:0.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-1-tablet{--columnGap:0.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-1-tablet-only{--columnGap:0.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-1-touch{--columnGap:0.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-1-desktop{--columnGap:0.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-1-desktop-only{--columnGap:0.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-1-widescreen{--columnGap:0.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-1-widescreen-only{--columnGap:0.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-1-fullhd{--columnGap:0.25rem}}.columns.is-variable.is-2{--columnGap:0.5rem}@media screen and (max-width:768px){.columns.is-variable.is-2-mobile{--columnGap:0.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-2-tablet{--columnGap:0.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-2-tablet-only{--columnGap:0.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-2-touch{--columnGap:0.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-2-desktop{--columnGap:0.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-2-desktop-only{--columnGap:0.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-2-widescreen{--columnGap:0.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-2-widescreen-only{--columnGap:0.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-2-fullhd{--columnGap:0.5rem}}.columns.is-variable.is-3{--columnGap:0.75rem}@media screen and (max-width:768px){.columns.is-variable.is-3-mobile{--columnGap:0.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-3-tablet{--columnGap:0.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-3-tablet-only{--columnGap:0.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-3-touch{--columnGap:0.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-3-desktop{--columnGap:0.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-3-desktop-only{--columnGap:0.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-3-widescreen{--columnGap:0.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-3-widescreen-only{--columnGap:0.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-3-fullhd{--columnGap:0.75rem}}.columns.is-variable.is-4{--columnGap:1rem}@media screen and (max-width:768px){.columns.is-variable.is-4-mobile{--columnGap:1rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-4-tablet{--columnGap:1rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-4-tablet-only{--columnGap:1rem}}@media screen and (max-width:1023px){.columns.is-variable.is-4-touch{--columnGap:1rem}}@media screen and (min-width:1024px){.columns.is-variable.is-4-desktop{--columnGap:1rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-4-desktop-only{--columnGap:1rem}}@media screen and (min-width:1216px){.columns.is-variable.is-4-widescreen{--columnGap:1rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-4-widescreen-only{--columnGap:1rem}}@media screen and (min-width:1408px){.columns.is-variable.is-4-fullhd{--columnGap:1rem}}.columns.is-variable.is-5{--columnGap:1.25rem}@media screen and (max-width:768px){.columns.is-variable.is-5-mobile{--columnGap:1.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-5-tablet{--columnGap:1.25rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-5-tablet-only{--columnGap:1.25rem}}@media screen and (max-width:1023px){.columns.is-variable.is-5-touch{--columnGap:1.25rem}}@media screen and (min-width:1024px){.columns.is-variable.is-5-desktop{--columnGap:1.25rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-5-desktop-only{--columnGap:1.25rem}}@media screen and (min-width:1216px){.columns.is-variable.is-5-widescreen{--columnGap:1.25rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-5-widescreen-only{--columnGap:1.25rem}}@media screen and (min-width:1408px){.columns.is-variable.is-5-fullhd{--columnGap:1.25rem}}.columns.is-variable.is-6{--columnGap:1.5rem}@media screen and (max-width:768px){.columns.is-variable.is-6-mobile{--columnGap:1.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-6-tablet{--columnGap:1.5rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-6-tablet-only{--columnGap:1.5rem}}@media screen and (max-width:1023px){.columns.is-variable.is-6-touch{--columnGap:1.5rem}}@media screen and (min-width:1024px){.columns.is-variable.is-6-desktop{--columnGap:1.5rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-6-desktop-only{--columnGap:1.5rem}}@media screen and (min-width:1216px){.columns.is-variable.is-6-widescreen{--columnGap:1.5rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-6-widescreen-only{--columnGap:1.5rem}}@media screen and (min-width:1408px){.columns.is-variable.is-6-fullhd{--columnGap:1.5rem}}.columns.is-variable.is-7{--columnGap:1.75rem}@media screen and (max-width:768px){.columns.is-variable.is-7-mobile{--columnGap:1.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-7-tablet{--columnGap:1.75rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-7-tablet-only{--columnGap:1.75rem}}@media screen and (max-width:1023px){.columns.is-variable.is-7-touch{--columnGap:1.75rem}}@media screen and (min-width:1024px){.columns.is-variable.is-7-desktop{--columnGap:1.75rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-7-desktop-only{--columnGap:1.75rem}}@media screen and (min-width:1216px){.columns.is-variable.is-7-widescreen{--columnGap:1.75rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-7-widescreen-only{--columnGap:1.75rem}}@media screen and (min-width:1408px){.columns.is-variable.is-7-fullhd{--columnGap:1.75rem}}.columns.is-variable.is-8{--columnGap:2rem}@media screen and (max-width:768px){.columns.is-variable.is-8-mobile{--columnGap:2rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-8-tablet{--columnGap:2rem}}@media screen and (min-width:769px) and (max-width:1023px){.columns.is-variable.is-8-tablet-only{--columnGap:2rem}}@media screen and (max-width:1023px){.columns.is-variable.is-8-touch{--columnGap:2rem}}@media screen and (min-width:1024px){.columns.is-variable.is-8-desktop{--columnGap:2rem}}@media screen and (min-width:1024px) and (max-width:1215px){.columns.is-variable.is-8-desktop-only{--columnGap:2rem}}@media screen and (min-width:1216px){.columns.is-variable.is-8-widescreen{--columnGap:2rem}}@media screen and (min-width:1216px) and (max-width:1407px){.columns.is-variable.is-8-widescreen-only{--columnGap:2rem}}@media screen and (min-width:1408px){.columns.is-variable.is-8-fullhd{--columnGap:2rem}}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:min-content}.tile.is-ancestor{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.tile.is-ancestor:last-child{margin-bottom:-.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0!important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem!important}@media screen and (min-width:769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.33333333%}.tile.is-2{flex:none;width:16.66666667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.33333333%}.tile.is-5{flex:none;width:41.66666667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.33333333%}.tile.is-8{flex:none;width:66.66666667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.33333333%}.tile.is-11{flex:none;width:91.66666667%}.tile.is-12{flex:none;width:100%}}.box{background-color:#fff;border-radius:6px;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);color:#4a4a4a;display:block;padding:1.25rem}a.box:hover,a.box:focus{box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px #3273dc}a.box:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2),0 0 0 1px #3273dc}.button{background-color:#fff;border-color:#dbdbdb;border-width:1px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(.5em - 1px);padding-left:1em;padding-right:1em;padding-top:calc(.5em - 1px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button .icon,.button .icon.is-small,.button .icon.is-medium,.button .icon.is-large{height:1.5em;width:1.5em}.button .icon:first-child:not(:last-child){margin-left:calc(-.5em - 1px);margin-right:.25em}.button .icon:last-child:not(:first-child){margin-left:.25em;margin-right:calc(-.5em - 1px)}.button .icon:first-child:last-child{margin-left:calc(-.5em - 1px);margin-right:calc(-.5em - 1px)}.button:hover,.button.is-hovered{border-color:#b5b5b5;color:#363636}.button:focus,.button.is-focused{border-color:#3273dc;color:#363636}.button:focus:not(:active),.button.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button:active,.button.is-active{border-color:#4a4a4a;color:#363636}.button.is-text{background-color:transparent;border-color:transparent;color:#4a4a4a;text-decoration:underline}.button.is-text:hover,.button.is-text.is-hovered,.button.is-text:focus,.button.is-text.is-focused{background-color:#f5f5f5;color:#363636}.button.is-text:active,.button.is-text.is-active{background-color:#e8e8e8;color:#363636}.button.is-text[disabled],fieldset[disabled] .button.is-text{background-color:transparent;border-color:transparent;box-shadow:none}.button.is-white{background-color:#fff;border-color:transparent;color:#0a0a0a}.button.is-white:hover,.button.is-white.is-hovered{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.button.is-white:focus,.button.is-white.is-focused{border-color:transparent;color:#0a0a0a}.button.is-white:focus:not(:active),.button.is-white.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.button.is-white:active,.button.is-white.is-active{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.button.is-white[disabled],fieldset[disabled] .button.is-white{background-color:#fff;border-color:transparent;box-shadow:none}.button.is-white.is-inverted{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted:hover,.button.is-white.is-inverted.is-hovered{background-color:#000}.button.is-white.is-inverted[disabled],fieldset[disabled] .button.is-white.is-inverted{background-color:#0a0a0a;border-color:transparent;box-shadow:none;color:#fff}.button.is-white.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-white.is-outlined:hover,.button.is-white.is-outlined.is-hovered,.button.is-white.is-outlined:focus,.button.is-white.is-outlined.is-focused{background-color:#fff;border-color:#fff;color:#0a0a0a}.button.is-white.is-outlined.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-outlined.is-loading:hover::after,.button.is-white.is-outlined.is-loading.is-hovered::after,.button.is-white.is-outlined.is-loading:focus::after,.button.is-white.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined[disabled],fieldset[disabled] .button.is-white.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-white.is-inverted.is-outlined:hover,.button.is-white.is-inverted.is-outlined.is-hovered,.button.is-white.is-inverted.is-outlined:focus,.button.is-white.is-inverted.is-outlined.is-focused{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-outlined.is-loading:hover::after,.button.is-white.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-white.is-inverted.is-outlined.is-loading:focus::after,.button.is-white.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black{background-color:#0a0a0a;border-color:transparent;color:#fff}.button.is-black:hover,.button.is-black.is-hovered{background-color:#040404;border-color:transparent;color:#fff}.button.is-black:focus,.button.is-black.is-focused{border-color:transparent;color:#fff}.button.is-black:focus:not(:active),.button.is-black.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.button.is-black:active,.button.is-black.is-active{background-color:#000;border-color:transparent;color:#fff}.button.is-black[disabled],fieldset[disabled] .button.is-black{background-color:#0a0a0a;border-color:transparent;box-shadow:none}.button.is-black.is-inverted{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted:hover,.button.is-black.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-black.is-inverted[disabled],fieldset[disabled] .button.is-black.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#0a0a0a}.button.is-black.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-black.is-outlined:hover,.button.is-black.is-outlined.is-hovered,.button.is-black.is-outlined:focus,.button.is-black.is-outlined.is-focused{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.button.is-black.is-outlined.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-outlined.is-loading:hover::after,.button.is-black.is-outlined.is-loading.is-hovered::after,.button.is-black.is-outlined.is-loading:focus::after,.button.is-black.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined[disabled],fieldset[disabled] .button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-black.is-inverted.is-outlined:hover,.button.is-black.is-inverted.is-outlined.is-hovered,.button.is-black.is-inverted.is-outlined:focus,.button.is-black.is-inverted.is-outlined.is-focused{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-outlined.is-loading:hover::after,.button.is-black.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-black.is-inverted.is-outlined.is-loading:focus::after,.button.is-black.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-light{background-color:#f5f5f5;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:hover,.button.is-light.is-hovered{background-color:#eee;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:focus,.button.is-light.is-focused{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:focus:not(:active),.button.is-light.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.button.is-light:active,.button.is-light.is-active{background-color:#e8e8e8;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light[disabled],fieldset[disabled] .button.is-light{background-color:#f5f5f5;border-color:transparent;box-shadow:none}.button.is-light.is-inverted{background-color:rgba(0,0,0,.7);color:#f5f5f5}.button.is-light.is-inverted:hover,.button.is-light.is-inverted.is-hovered{background-color:rgba(0,0,0,.7)}.button.is-light.is-inverted[disabled],fieldset[disabled] .button.is-light.is-inverted{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#f5f5f5}.button.is-light.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7)rgba(0,0,0,.7)!important}.button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-light.is-outlined:hover,.button.is-light.is-outlined.is-hovered,.button.is-light.is-outlined:focus,.button.is-light.is-outlined.is-focused{background-color:#f5f5f5;border-color:#f5f5f5;color:rgba(0,0,0,.7)}.button.is-light.is-outlined.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-outlined.is-loading:hover::after,.button.is-light.is-outlined.is-loading.is-hovered::after,.button.is-light.is-outlined.is-loading:focus::after,.button.is-light.is-outlined.is-loading.is-focused::after{border-color:transparent transparent rgba(0,0,0,.7)rgba(0,0,0,.7)!important}.button.is-light.is-outlined[disabled],fieldset[disabled] .button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-light.is-inverted.is-outlined:hover,.button.is-light.is-inverted.is-outlined.is-hovered,.button.is-light.is-inverted.is-outlined:focus,.button.is-light.is-inverted.is-outlined.is-focused{background-color:rgba(0,0,0,.7);color:#f5f5f5}.button.is-light.is-inverted.is-outlined.is-loading:hover::after,.button.is-light.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-light.is-inverted.is-outlined.is-loading:focus::after,.button.is-light.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-dark{background-color:#363636;border-color:transparent;color:#fff}.button.is-dark:hover,.button.is-dark.is-hovered{background-color:#2f2f2f;border-color:transparent;color:#fff}.button.is-dark:focus,.button.is-dark.is-focused{border-color:transparent;color:#fff}.button.is-dark:focus:not(:active),.button.is-dark.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.button.is-dark:active,.button.is-dark.is-active{background-color:#292929;border-color:transparent;color:#fff}.button.is-dark[disabled],fieldset[disabled] .button.is-dark{background-color:#363636;border-color:transparent;box-shadow:none}.button.is-dark.is-inverted{background-color:#fff;color:#363636}.button.is-dark.is-inverted:hover,.button.is-dark.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-dark.is-inverted[disabled],fieldset[disabled] .button.is-dark.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#363636}.button.is-dark.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-dark.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-dark.is-outlined:hover,.button.is-dark.is-outlined.is-hovered,.button.is-dark.is-outlined:focus,.button.is-dark.is-outlined.is-focused{background-color:#363636;border-color:#363636;color:#fff}.button.is-dark.is-outlined.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-outlined.is-loading:hover::after,.button.is-dark.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-outlined.is-loading:focus::after,.button.is-dark.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-dark.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-dark.is-inverted.is-outlined:hover,.button.is-dark.is-inverted.is-outlined.is-hovered,.button.is-dark.is-inverted.is-outlined:focus,.button.is-dark.is-inverted.is-outlined.is-focused{background-color:#fff;color:#363636}.button.is-dark.is-inverted.is-outlined.is-loading:hover::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-inverted.is-outlined.is-loading:focus::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-primary{background-color:#c93312;border-color:transparent;color:#fff}.button.is-primary:hover,.button.is-primary.is-hovered{background-color:#bd3011;border-color:transparent;color:#fff}.button.is-primary:focus,.button.is-primary.is-focused{border-color:transparent;color:#fff}.button.is-primary:focus:not(:active),.button.is-primary.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(201,51,18,.25)}.button.is-primary:active,.button.is-primary.is-active{background-color:#b22d10;border-color:transparent;color:#fff}.button.is-primary[disabled],fieldset[disabled] .button.is-primary{background-color:#c93312;border-color:transparent;box-shadow:none}.button.is-primary.is-inverted{background-color:#fff;color:#c93312}.button.is-primary.is-inverted:hover,.button.is-primary.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-primary.is-inverted[disabled],fieldset[disabled] .button.is-primary.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#c93312}.button.is-primary.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined{background-color:transparent;border-color:#c93312;color:#c93312}.button.is-primary.is-outlined:hover,.button.is-primary.is-outlined.is-hovered,.button.is-primary.is-outlined:focus,.button.is-primary.is-outlined.is-focused{background-color:#c93312;border-color:#c93312;color:#fff}.button.is-primary.is-outlined.is-loading::after{border-color:transparent transparent #c93312 #c93312!important}.button.is-primary.is-outlined.is-loading:hover::after,.button.is-primary.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-outlined.is-loading:focus::after,.button.is-primary.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-outlined{background-color:transparent;border-color:#c93312;box-shadow:none;color:#c93312}.button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-primary.is-inverted.is-outlined:hover,.button.is-primary.is-inverted.is-outlined.is-hovered,.button.is-primary.is-inverted.is-outlined:focus,.button.is-primary.is-inverted.is-outlined.is-focused{background-color:#fff;color:#c93312}.button.is-primary.is-inverted.is-outlined.is-loading:hover::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-inverted.is-outlined.is-loading:focus::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #c93312 #c93312!important}.button.is-primary.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-primary.is-light{background-color:#fdefec;color:#e13914}.button.is-primary.is-light:hover,.button.is-primary.is-light.is-hovered{background-color:#fce6e1;border-color:transparent;color:#e13914}.button.is-primary.is-light:active,.button.is-primary.is-light.is-active{background-color:#fbdcd5;border-color:transparent;color:#e13914}.button.is-link{background-color:#3273dc;border-color:transparent;color:#fff}.button.is-link:hover,.button.is-link.is-hovered{background-color:#276cda;border-color:transparent;color:#fff}.button.is-link:focus,.button.is-link.is-focused{border-color:transparent;color:#fff}.button.is-link:focus:not(:active),.button.is-link.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-link:active,.button.is-link.is-active{background-color:#2366d1;border-color:transparent;color:#fff}.button.is-link[disabled],fieldset[disabled] .button.is-link{background-color:#3273dc;border-color:transparent;box-shadow:none}.button.is-link.is-inverted{background-color:#fff;color:#3273dc}.button.is-link.is-inverted:hover,.button.is-link.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-link.is-inverted[disabled],fieldset[disabled] .button.is-link.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3273dc}.button.is-link.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;color:#3273dc}.button.is-link.is-outlined:hover,.button.is-link.is-outlined.is-hovered,.button.is-link.is-outlined:focus,.button.is-link.is-outlined.is-focused{background-color:#3273dc;border-color:#3273dc;color:#fff}.button.is-link.is-outlined.is-loading::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-outlined.is-loading:hover::after,.button.is-link.is-outlined.is-loading.is-hovered::after,.button.is-link.is-outlined.is-loading:focus::after,.button.is-link.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined[disabled],fieldset[disabled] .button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;box-shadow:none;color:#3273dc}.button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-link.is-inverted.is-outlined:hover,.button.is-link.is-inverted.is-outlined.is-hovered,.button.is-link.is-inverted.is-outlined:focus,.button.is-link.is-inverted.is-outlined.is-focused{background-color:#fff;color:#3273dc}.button.is-link.is-inverted.is-outlined.is-loading:hover::after,.button.is-link.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-link.is-inverted.is-outlined.is-loading:focus::after,.button.is-link.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #3273dc #3273dc!important}.button.is-link.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-link.is-light{background-color:#eef3fc;color:#2160c4}.button.is-link.is-light:hover,.button.is-link.is-light.is-hovered{background-color:#e3ecfa;border-color:transparent;color:#2160c4}.button.is-link.is-light:active,.button.is-link.is-light.is-active{background-color:#d8e4f8;border-color:transparent;color:#2160c4}.button.is-info{background-color:#3298dc;border-color:transparent;color:#fff}.button.is-info:hover,.button.is-info.is-hovered{background-color:#2793da;border-color:transparent;color:#fff}.button.is-info:focus,.button.is-info.is-focused{border-color:transparent;color:#fff}.button.is-info:focus:not(:active),.button.is-info.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,152,220,.25)}.button.is-info:active,.button.is-info.is-active{background-color:#238cd1;border-color:transparent;color:#fff}.button.is-info[disabled],fieldset[disabled] .button.is-info{background-color:#3298dc;border-color:transparent;box-shadow:none}.button.is-info.is-inverted{background-color:#fff;color:#3298dc}.button.is-info.is-inverted:hover,.button.is-info.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-info.is-inverted[disabled],fieldset[disabled] .button.is-info.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3298dc}.button.is-info.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined{background-color:transparent;border-color:#3298dc;color:#3298dc}.button.is-info.is-outlined:hover,.button.is-info.is-outlined.is-hovered,.button.is-info.is-outlined:focus,.button.is-info.is-outlined.is-focused{background-color:#3298dc;border-color:#3298dc;color:#fff}.button.is-info.is-outlined.is-loading::after{border-color:transparent transparent #3298dc #3298dc!important}.button.is-info.is-outlined.is-loading:hover::after,.button.is-info.is-outlined.is-loading.is-hovered::after,.button.is-info.is-outlined.is-loading:focus::after,.button.is-info.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined[disabled],fieldset[disabled] .button.is-info.is-outlined{background-color:transparent;border-color:#3298dc;box-shadow:none;color:#3298dc}.button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-info.is-inverted.is-outlined:hover,.button.is-info.is-inverted.is-outlined.is-hovered,.button.is-info.is-inverted.is-outlined:focus,.button.is-info.is-inverted.is-outlined.is-focused{background-color:#fff;color:#3298dc}.button.is-info.is-inverted.is-outlined.is-loading:hover::after,.button.is-info.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-info.is-inverted.is-outlined.is-loading:focus::after,.button.is-info.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #3298dc #3298dc!important}.button.is-info.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-info.is-light{background-color:#eef6fc;color:#1d72aa}.button.is-info.is-light:hover,.button.is-info.is-light.is-hovered{background-color:#e3f1fa;border-color:transparent;color:#1d72aa}.button.is-info.is-light:active,.button.is-info.is-light.is-active{background-color:#d8ebf8;border-color:transparent;color:#1d72aa}.button.is-success{background-color:#48c774;border-color:transparent;color:#fff}.button.is-success:hover,.button.is-success.is-hovered{background-color:#3ec46d;border-color:transparent;color:#fff}.button.is-success:focus,.button.is-success.is-focused{border-color:transparent;color:#fff}.button.is-success:focus:not(:active),.button.is-success.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(72,199,116,.25)}.button.is-success:active,.button.is-success.is-active{background-color:#3abb67;border-color:transparent;color:#fff}.button.is-success[disabled],fieldset[disabled] .button.is-success{background-color:#48c774;border-color:transparent;box-shadow:none}.button.is-success.is-inverted{background-color:#fff;color:#48c774}.button.is-success.is-inverted:hover,.button.is-success.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-success.is-inverted[disabled],fieldset[disabled] .button.is-success.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#48c774}.button.is-success.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined{background-color:transparent;border-color:#48c774;color:#48c774}.button.is-success.is-outlined:hover,.button.is-success.is-outlined.is-hovered,.button.is-success.is-outlined:focus,.button.is-success.is-outlined.is-focused{background-color:#48c774;border-color:#48c774;color:#fff}.button.is-success.is-outlined.is-loading::after{border-color:transparent transparent #48c774 #48c774!important}.button.is-success.is-outlined.is-loading:hover::after,.button.is-success.is-outlined.is-loading.is-hovered::after,.button.is-success.is-outlined.is-loading:focus::after,.button.is-success.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined[disabled],fieldset[disabled] .button.is-success.is-outlined{background-color:transparent;border-color:#48c774;box-shadow:none;color:#48c774}.button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-success.is-inverted.is-outlined:hover,.button.is-success.is-inverted.is-outlined.is-hovered,.button.is-success.is-inverted.is-outlined:focus,.button.is-success.is-inverted.is-outlined.is-focused{background-color:#fff;color:#48c774}.button.is-success.is-inverted.is-outlined.is-loading:hover::after,.button.is-success.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-success.is-inverted.is-outlined.is-loading:focus::after,.button.is-success.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #48c774 #48c774!important}.button.is-success.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-success.is-light{background-color:#effaf3;color:#257942}.button.is-success.is-light:hover,.button.is-success.is-light.is-hovered{background-color:#e6f7ec;border-color:transparent;color:#257942}.button.is-success.is-light:active,.button.is-success.is-light.is-active{background-color:#dcf4e4;border-color:transparent;color:#257942}.button.is-warning{background-color:#ffdd57;border-color:transparent;color:#fff}.button.is-warning:hover,.button.is-warning.is-hovered{background-color:#ffdb4a;border-color:transparent;color:#fff}.button.is-warning:focus,.button.is-warning.is-focused{border-color:transparent;color:#fff}.button.is-warning:focus:not(:active),.button.is-warning.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.button.is-warning:active,.button.is-warning.is-active{background-color:#ffd83d;border-color:transparent;color:#fff}.button.is-warning[disabled],fieldset[disabled] .button.is-warning{background-color:#ffdd57;border-color:transparent;box-shadow:none}.button.is-warning.is-inverted{background-color:#fff;color:#ffdd57}.button.is-warning.is-inverted:hover,.button.is-warning.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-warning.is-inverted[disabled],fieldset[disabled] .button.is-warning.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#ffdd57}.button.is-warning.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;color:#ffdd57}.button.is-warning.is-outlined:hover,.button.is-warning.is-outlined.is-hovered,.button.is-warning.is-outlined:focus,.button.is-warning.is-outlined.is-focused{background-color:#ffdd57;border-color:#ffdd57;color:#fff}.button.is-warning.is-outlined.is-loading::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-outlined.is-loading:hover::after,.button.is-warning.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-outlined.is-loading:focus::after,.button.is-warning.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-warning.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;box-shadow:none;color:#ffdd57}.button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-warning.is-inverted.is-outlined:hover,.button.is-warning.is-inverted.is-outlined.is-hovered,.button.is-warning.is-inverted.is-outlined:focus,.button.is-warning.is-inverted.is-outlined.is-focused{background-color:#fff;color:#ffdd57}.button.is-warning.is-inverted.is-outlined.is-loading:hover::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-inverted.is-outlined.is-loading:focus::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-warning.is-light{background-color:#fffbeb;color:#947600}.button.is-warning.is-light:hover,.button.is-warning.is-light.is-hovered{background-color:#fff8de;border-color:transparent;color:#947600}.button.is-warning.is-light:active,.button.is-warning.is-light.is-active{background-color:#fff6d1;border-color:transparent;color:#947600}.button.is-danger{background-color:#f14668;border-color:transparent;color:#fff}.button.is-danger:hover,.button.is-danger.is-hovered{background-color:#f03a5f;border-color:transparent;color:#fff}.button.is-danger:focus,.button.is-danger.is-focused{border-color:transparent;color:#fff}.button.is-danger:focus:not(:active),.button.is-danger.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(241,70,104,.25)}.button.is-danger:active,.button.is-danger.is-active{background-color:#ef2e55;border-color:transparent;color:#fff}.button.is-danger[disabled],fieldset[disabled] .button.is-danger{background-color:#f14668;border-color:transparent;box-shadow:none}.button.is-danger.is-inverted{background-color:#fff;color:#f14668}.button.is-danger.is-inverted:hover,.button.is-danger.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-danger.is-inverted[disabled],fieldset[disabled] .button.is-danger.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#f14668}.button.is-danger.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined{background-color:transparent;border-color:#f14668;color:#f14668}.button.is-danger.is-outlined:hover,.button.is-danger.is-outlined.is-hovered,.button.is-danger.is-outlined:focus,.button.is-danger.is-outlined.is-focused{background-color:#f14668;border-color:#f14668;color:#fff}.button.is-danger.is-outlined.is-loading::after{border-color:transparent transparent #f14668 #f14668!important}.button.is-danger.is-outlined.is-loading:hover::after,.button.is-danger.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-outlined.is-loading:focus::after,.button.is-danger.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-outlined{background-color:transparent;border-color:#f14668;box-shadow:none;color:#f14668}.button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-danger.is-inverted.is-outlined:hover,.button.is-danger.is-inverted.is-outlined.is-hovered,.button.is-danger.is-inverted.is-outlined:focus,.button.is-danger.is-inverted.is-outlined.is-focused{background-color:#fff;color:#f14668}.button.is-danger.is-inverted.is-outlined.is-loading:hover::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-inverted.is-outlined.is-loading:focus::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #f14668 #f14668!important}.button.is-danger.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-danger.is-light{background-color:#feecf0;color:#cc0f35}.button.is-danger.is-light:hover,.button.is-danger.is-light.is-hovered{background-color:#fde0e6;border-color:transparent;color:#cc0f35}.button.is-danger.is-light:active,.button.is-danger.is-light.is-active{background-color:#fcd4dc;border-color:transparent;color:#cc0f35}.button.is-small{border-radius:2px;font-size:.75rem}.button.is-normal{font-size:1rem}.button.is-medium{font-size:1.25rem}.button.is-large{font-size:1.5rem}.button[disabled],fieldset[disabled] .button{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.button.is-loading{color:transparent!important;pointer-events:none}.button.is-loading::after{position:absolute;left:calc(50% - (1em/2));top:calc(50% - (1em/2));position:absolute!important}.button.is-static{background-color:#f5f5f5;border-color:#dbdbdb;color:#7a7a7a;box-shadow:none;pointer-events:none}.button.is-rounded{border-radius:290486px;padding-left:calc(1em + .25em);padding-right:calc(1em + .25em)}.buttons{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.buttons .button{margin-bottom:.5rem}.buttons .button:not(:last-child):not(.is-fullwidth){margin-right:.5rem}.buttons:last-child{margin-bottom:-.5rem}.buttons:not(:last-child){margin-bottom:1rem}.buttons.are-small .button:not(.is-normal):not(.is-medium):not(.is-large){border-radius:2px;font-size:.75rem}.buttons.are-medium .button:not(.is-small):not(.is-normal):not(.is-large){font-size:1.25rem}.buttons.are-large .button:not(.is-small):not(.is-normal):not(.is-medium){font-size:1.5rem}.buttons.has-addons .button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.buttons.has-addons .button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.buttons.has-addons .button:last-child{margin-right:0}.buttons.has-addons .button:hover,.buttons.has-addons .button.is-hovered{z-index:2}.buttons.has-addons .button:focus,.buttons.has-addons .button.is-focused,.buttons.has-addons .button:active,.buttons.has-addons .button.is-active,.buttons.has-addons .button.is-selected{z-index:3}.buttons.has-addons .button:focus:hover,.buttons.has-addons .button.is-focused:hover,.buttons.has-addons .button:active:hover,.buttons.has-addons .button.is-active:hover,.buttons.has-addons .button.is-selected:hover{z-index:4}.buttons.has-addons .button.is-expanded{flex-grow:1;flex-shrink:1}.buttons.is-centered{justify-content:center}.buttons.is-centered:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.buttons.is-right{justify-content:flex-end}.buttons.is-right:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.container{flex-grow:1;margin:0 auto;position:relative;width:auto}.container.is-fluid{max-width:none;padding-left:32px;padding-right:32px;width:100%}@media screen and (min-width:1024px){.container{max-width:960px}}@media screen and (max-width:1215px){.container.is-widescreen{max-width:1152px}}@media screen and (max-width:1407px){.container.is-fullhd{max-width:1344px}}@media screen and (min-width:1216px){.container{max-width:1152px}}@media screen and (min-width:1408px){.container{max-width:1344px}}.content li+li{margin-top:.25em}.content p:not(:last-child),.content dl:not(:last-child),.content ol:not(:last-child),.content ul:not(:last-child),.content blockquote:not(:last-child),.content pre:not(:last-child),.content table:not(:last-child){margin-bottom:1em}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h2{font-size:1.75em;margin-bottom:.5714em}.content h2:not(:first-child){margin-top:1.1428em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content h6{font-size:1em;margin-bottom:1em}.content blockquote{background-color:#f5f5f5;border-left:5px solid #dbdbdb;padding:1.25em 1.5em}.content ol{list-style-position:outside;margin-left:2em;margin-top:1em}.content ol:not([type]){list-style-type:decimal}.content ol:not([type]).is-lower-alpha{list-style-type:lower-alpha}.content ol:not([type]).is-lower-roman{list-style-type:lower-roman}.content ol:not([type]).is-upper-alpha{list-style-type:upper-alpha}.content ol:not([type]).is-upper-roman{list-style-type:upper-roman}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content dd{margin-left:2em}.content figure{margin-left:2em;margin-right:2em;text-align:center}.content figure:not(:first-child){margin-top:2em}.content figure:not(:last-child){margin-bottom:2em}.content figure img{display:inline-block}.content figure figcaption{font-style:italic}.content pre{-webkit-overflow-scrolling:touch;overflow-x:auto;padding:1.25em 1.5em;white-space:pre;word-wrap:normal}.content sup,.content sub{font-size:75%}.content table{width:100%}.content table td,.content table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table th{color:#363636}.content table th:not([align]){text-align:left}.content table thead td,.content table thead th{border-width:0 0 2px;color:#363636}.content table tfoot td,.content table tfoot th{border-width:2px 0 0;color:#363636}.content table tbody tr:last-child td,.content table tbody tr:last-child th{border-bottom-width:0}.content .tabs li+li{margin-top:0}.content.is-small{font-size:.75rem}.content.is-medium{font-size:1.25rem}.content.is-large{font-size:1.5rem}.icon{align-items:center;display:inline-flex;justify-content:center;height:1.5rem;width:1.5rem}.icon.is-small{height:1rem;width:1rem}.icon.is-medium{height:2rem;width:2rem}.icon.is-large{height:3rem;width:3rem}.image{display:block;position:relative}.image img{display:block;height:auto;width:100%}.image img.is-rounded{border-radius:290486px}.image.is-fullwidth{width:100%}.image.is-square img,.image.is-square .has-ratio,.image.is-1by1 img,.image.is-1by1 .has-ratio,.image.is-5by4 img,.image.is-5by4 .has-ratio,.image.is-4by3 img,.image.is-4by3 .has-ratio,.image.is-3by2 img,.image.is-3by2 .has-ratio,.image.is-5by3 img,.image.is-5by3 .has-ratio,.image.is-16by9 img,.image.is-16by9 .has-ratio,.image.is-2by1 img,.image.is-2by1 .has-ratio,.image.is-3by1 img,.image.is-3by1 .has-ratio,.image.is-4by5 img,.image.is-4by5 .has-ratio,.image.is-3by4 img,.image.is-3by4 .has-ratio,.image.is-2by3 img,.image.is-2by3 .has-ratio,.image.is-3by5 img,.image.is-3by5 .has-ratio,.image.is-9by16 img,.image.is-9by16 .has-ratio,.image.is-1by2 img,.image.is-1by2 .has-ratio,.image.is-1by3 img,.image.is-1by3 .has-ratio{height:100%;width:100%}.image.is-square,.image.is-1by1{padding-top:100%}.image.is-5by4{padding-top:80%}.image.is-4by3{padding-top:75%}.image.is-3by2{padding-top:66.6666%}.image.is-5by3{padding-top:60%}.image.is-16by9{padding-top:56.25%}.image.is-2by1{padding-top:50%}.image.is-3by1{padding-top:33.3333%}.image.is-4by5{padding-top:125%}.image.is-3by4{padding-top:133.3333%}.image.is-2by3{padding-top:150%}.image.is-3by5{padding-top:166.6666%}.image.is-9by16{padding-top:177.7777%}.image.is-1by2{padding-top:200%}.image.is-1by3{padding-top:300%}.image.is-16x16{height:16px;width:16px}.image.is-24x24{height:24px;width:24px}.image.is-32x32{height:32px;width:32px}.image.is-48x48{height:48px;width:48px}.image.is-64x64{height:64px;width:64px}.image.is-96x96{height:96px;width:96px}.image.is-128x128{height:128px;width:128px}.notification{background-color:#f5f5f5;border-radius:4px;padding:1.25rem 2.5rem 1.25rem 1.5rem;position:relative}.notification a:not(.button):not(.dropdown-item){color:currentColor;text-decoration:underline}.notification strong{color:currentColor}.notification code,.notification pre{background:#fff}.notification pre code{background:0 0}.notification>.delete{position:absolute;right:.5rem;top:.5rem}.notification .title,.notification .subtitle,.notification .content{color:currentColor}.notification.is-white{background-color:#fff;color:#0a0a0a}.notification.is-black{background-color:#0a0a0a;color:#fff}.notification.is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.notification.is-dark{background-color:#363636;color:#fff}.notification.is-primary{background-color:#c93312;color:#fff}.notification.is-primary.is-light{background-color:#fdefec;color:#e13914}.notification.is-link{background-color:#3273dc;color:#fff}.notification.is-link.is-light{background-color:#eef3fc;color:#2160c4}.notification.is-info{background-color:#3298dc;color:#fff}.notification.is-info.is-light{background-color:#eef6fc;color:#1d72aa}.notification.is-success{background-color:#48c774;color:#fff}.notification.is-success.is-light{background-color:#effaf3;color:#257942}.notification.is-warning{background-color:#ffdd57;color:#fff}.notification.is-warning.is-light{background-color:#fffbeb;color:#947600}.notification.is-danger{background-color:#f14668;color:#fff}.notification.is-danger.is-light{background-color:#feecf0;color:#cc0f35}.progress{-moz-appearance:none;-webkit-appearance:none;border:none;border-radius:290486px;display:block;height:1rem;overflow:hidden;padding:0;width:100%}.progress::-webkit-progress-bar{background-color:#ededed}.progress::-webkit-progress-value{background-color:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;border:none}.progress.is-white::-webkit-progress-value{background-color:#fff}.progress.is-white::-moz-progress-bar{background-color:#fff}.progress.is-white::-ms-fill{background-color:#fff}.progress.is-white:indeterminate{background-image:linear-gradient(to right,white 30%,#ededed 30%)}.progress.is-black::-webkit-progress-value{background-color:#0a0a0a}.progress.is-black::-moz-progress-bar{background-color:#0a0a0a}.progress.is-black::-ms-fill{background-color:#0a0a0a}.progress.is-black:indeterminate{background-image:linear-gradient(to right,#0a0a0a 30%,#ededed 30%)}.progress.is-light::-webkit-progress-value{background-color:#f5f5f5}.progress.is-light::-moz-progress-bar{background-color:#f5f5f5}.progress.is-light::-ms-fill{background-color:#f5f5f5}.progress.is-light:indeterminate{background-image:linear-gradient(to right,whitesmoke 30%,#ededed 30%)}.progress.is-dark::-webkit-progress-value{background-color:#363636}.progress.is-dark::-moz-progress-bar{background-color:#363636}.progress.is-dark::-ms-fill{background-color:#363636}.progress.is-dark:indeterminate{background-image:linear-gradient(to right,#363636 30%,#ededed 30%)}.progress.is-primary::-webkit-progress-value{background-color:#c93312}.progress.is-primary::-moz-progress-bar{background-color:#c93312}.progress.is-primary::-ms-fill{background-color:#c93312}.progress.is-primary:indeterminate{background-image:linear-gradient(to right,#C93312 30%,#ededed 30%)}.progress.is-link::-webkit-progress-value{background-color:#3273dc}.progress.is-link::-moz-progress-bar{background-color:#3273dc}.progress.is-link::-ms-fill{background-color:#3273dc}.progress.is-link:indeterminate{background-image:linear-gradient(to right,#3273dc 30%,#ededed 30%)}.progress.is-info::-webkit-progress-value{background-color:#3298dc}.progress.is-info::-moz-progress-bar{background-color:#3298dc}.progress.is-info::-ms-fill{background-color:#3298dc}.progress.is-info:indeterminate{background-image:linear-gradient(to right,#3298dc 30%,#ededed 30%)}.progress.is-success::-webkit-progress-value{background-color:#48c774}.progress.is-success::-moz-progress-bar{background-color:#48c774}.progress.is-success::-ms-fill{background-color:#48c774}.progress.is-success:indeterminate{background-image:linear-gradient(to right,#48c774 30%,#ededed 30%)}.progress.is-warning::-webkit-progress-value{background-color:#ffdd57}.progress.is-warning::-moz-progress-bar{background-color:#ffdd57}.progress.is-warning::-ms-fill{background-color:#ffdd57}.progress.is-warning:indeterminate{background-image:linear-gradient(to right,#ffdd57 30%,#ededed 30%)}.progress.is-danger::-webkit-progress-value{background-color:#f14668}.progress.is-danger::-moz-progress-bar{background-color:#f14668}.progress.is-danger::-ms-fill{background-color:#f14668}.progress.is-danger:indeterminate{background-image:linear-gradient(to right,#f14668 30%,#ededed 30%)}.progress:indeterminate{animation-duration:1.5s;animation-iteration-count:infinite;animation-name:moveIndeterminate;animation-timing-function:linear;background-color:#ededed;background-image:linear-gradient(to right,#4a4a4a 30%,#ededed 30%);background-position:0 0;background-repeat:no-repeat;background-size:150% 150%}.progress:indeterminate::-webkit-progress-bar{background-color:transparent}.progress:indeterminate::-moz-progress-bar{background-color:transparent}.progress.is-small{height:.75rem}.progress.is-medium{height:1.25rem}.progress.is-large{height:1.5rem}@keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}.table{background-color:#fff;color:#363636}.table td,.table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-white,.table th.is-white{background-color:#fff;border-color:#fff;color:#0a0a0a}.table td.is-black,.table th.is-black{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.table td.is-light,.table th.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:rgba(0,0,0,.7)}.table td.is-dark,.table th.is-dark{background-color:#363636;border-color:#363636;color:#fff}.table td.is-primary,.table th.is-primary{background-color:#c93312;border-color:#c93312;color:#fff}.table td.is-link,.table th.is-link{background-color:#3273dc;border-color:#3273dc;color:#fff}.table td.is-info,.table th.is-info{background-color:#3298dc;border-color:#3298dc;color:#fff}.table td.is-success,.table th.is-success{background-color:#48c774;border-color:#48c774;color:#fff}.table td.is-warning,.table th.is-warning{background-color:#ffdd57;border-color:#ffdd57;color:#fff}.table td.is-danger,.table th.is-danger{background-color:#f14668;border-color:#f14668;color:#fff}.table td.is-narrow,.table th.is-narrow{white-space:nowrap;width:1%}.table td.is-selected,.table th.is-selected{background-color:#c93312;color:#fff}.table td.is-selected a,.table td.is-selected strong,.table th.is-selected a,.table th.is-selected strong{color:currentColor}.table th{color:#363636}.table th:not([align]){text-align:left}.table tr.is-selected{background-color:#c93312;color:#fff}.table tr.is-selected a,.table tr.is-selected strong{color:currentColor}.table tr.is-selected td,.table tr.is-selected th{border-color:#fff;color:currentColor}.table thead{background-color:transparent}.table thead td,.table thead th{border-width:0 0 2px;color:#363636}.table tfoot{background-color:transparent}.table tfoot td,.table tfoot th{border-width:2px 0 0;color:#363636}.table tbody{background-color:transparent}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:0}.table.is-bordered td,.table.is-bordered th{border-width:1px}.table.is-bordered tr:last-child td,.table.is-bordered tr:last-child th{border-bottom-width:1px}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover:nth-child(even){background-color:#f5f5f5}.table.is-narrow td,.table.is-narrow th{padding:.25em .5em}.table.is-striped tbody tr:not(.is-selected):nth-child(even){background-color:#fafafa}.table-container{-webkit-overflow-scrolling:touch;overflow:auto;overflow-y:hidden;max-width:100%}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags .tag{margin-bottom:.5rem}.tags .tag:not(:last-child){margin-right:.5rem}.tags:last-child{margin-bottom:-.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.are-medium .tag:not(.is-normal):not(.is-large){font-size:1rem}.tags.are-large .tag:not(.is-normal):not(.is-medium){font-size:1.25rem}.tags.is-centered{justify-content:center}.tags.is-centered .tag{margin-right:.25rem;margin-left:.25rem}.tags.is-right{justify-content:flex-end}.tags.is-right .tag:not(:first-child){margin-left:.5rem}.tags.is-right .tag:not(:last-child){margin-right:0}.tags.has-addons .tag{margin-right:0}.tags.has-addons .tag:not(:first-child){margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.tags.has-addons .tag:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.tag:not(body){align-items:center;background-color:#f5f5f5;border-radius:4px;color:#4a4a4a;display:inline-flex;font-size:.75rem;height:2em;justify-content:center;line-height:1.5;padding-left:.75em;padding-right:.75em;white-space:nowrap}.tag:not(body) .delete{margin-left:.25rem;margin-right:-.375rem}.tag:not(body).is-white{background-color:#fff;color:#0a0a0a}.tag:not(body).is-black{background-color:#0a0a0a;color:#fff}.tag:not(body).is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.tag:not(body).is-dark{background-color:#363636;color:#fff}.tag:not(body).is-primary{background-color:#c93312;color:#fff}.tag:not(body).is-primary.is-light{background-color:#fdefec;color:#e13914}.tag:not(body).is-link{background-color:#3273dc;color:#fff}.tag:not(body).is-link.is-light{background-color:#eef3fc;color:#2160c4}.tag:not(body).is-info{background-color:#3298dc;color:#fff}.tag:not(body).is-info.is-light{background-color:#eef6fc;color:#1d72aa}.tag:not(body).is-success{background-color:#48c774;color:#fff}.tag:not(body).is-success.is-light{background-color:#effaf3;color:#257942}.tag:not(body).is-warning{background-color:#ffdd57;color:#fff}.tag:not(body).is-warning.is-light{background-color:#fffbeb;color:#947600}.tag:not(body).is-danger{background-color:#f14668;color:#fff}.tag:not(body).is-danger.is-light{background-color:#feecf0;color:#cc0f35}.tag:not(body).is-normal{font-size:.75rem}.tag:not(body).is-medium{font-size:1rem}.tag:not(body).is-large{font-size:1.25rem}.tag:not(body) .icon:first-child:not(:last-child){margin-left:-.375em;margin-right:.1875em}.tag:not(body) .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:-.375em}.tag:not(body) .icon:first-child:last-child{margin-left:-.375em;margin-right:-.375em}.tag:not(body).is-delete{margin-left:1px;padding:0;position:relative;width:2em}.tag:not(body).is-delete::before,.tag:not(body).is-delete::after{background-color:currentColor;content:"";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%)translateY(-50%)rotate(45deg);transform-origin:center center}.tag:not(body).is-delete::before{height:1px;width:50%}.tag:not(body).is-delete::after{height:50%;width:1px}.tag:not(body).is-delete:hover,.tag:not(body).is-delete:focus{background-color:#e8e8e8}.tag:not(body).is-delete:active{background-color:#dbdbdb}.tag:not(body).is-rounded{border-radius:290486px}a.tag:hover{text-decoration:underline}.title,.subtitle{word-break:break-word}.title em,.title span,.subtitle em,.subtitle span{font-weight:inherit}.title sub,.subtitle sub{font-size:.75em}.title sup,.subtitle sup{font-size:.75em}.title .tag,.subtitle .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#4a4a4a;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.heading{display:block;font-size:11px;letter-spacing:1px;margin-bottom:5px;text-transform:uppercase}.highlight{font-weight:400;max-width:100%;overflow:hidden;padding:0}.highlight pre{overflow:auto;max-width:100%}.number{align-items:center;background-color:#f5f5f5;border-radius:290486px;display:inline-flex;font-size:1.25rem;height:2em;justify-content:center;margin-right:1.5rem;min-width:2.5em;padding:.25rem .5rem;text-align:center;vertical-align:top}.breadcrumb{font-size:1rem;white-space:nowrap}.breadcrumb a{align-items:center;color:#3273dc;display:flex;justify-content:center;padding:0 .75em}.breadcrumb a:hover{color:#363636}.breadcrumb li{align-items:center;display:flex}.breadcrumb li:first-child a{padding-left:0}.breadcrumb li.is-active a{color:#363636;cursor:default;pointer-events:none}.breadcrumb li+li::before{color:#b5b5b5;content:"\0002f"}.breadcrumb ul,.breadcrumb ol{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:flex-start}.breadcrumb .icon:first-child{margin-right:.5em}.breadcrumb .icon:last-child{margin-left:.5em}.breadcrumb.is-centered ol,.breadcrumb.is-centered ul{justify-content:center}.breadcrumb.is-right ol,.breadcrumb.is-right ul{justify-content:flex-end}.breadcrumb.is-small{font-size:.75rem}.breadcrumb.is-medium{font-size:1.25rem}.breadcrumb.is-large{font-size:1.5rem}.breadcrumb.has-arrow-separator li+li::before{content:"\02192"}.breadcrumb.has-bullet-separator li+li::before{content:"\02022"}.breadcrumb.has-dot-separator li+li::before{content:"\000b7"}.breadcrumb.has-succeeds-separator li+li::before{content:"\0227B"}.card{background-color:#fff;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);color:#4a4a4a;max-width:100%;position:relative}.card-header{background-color:transparent;align-items:stretch;box-shadow:0 .125em .25em rgba(10,10,10,.1);display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:700;padding:.75rem 1rem}.card-header-title.is-centered{justify-content:center}.card-header-icon{align-items:center;cursor:pointer;display:flex;justify-content:center;padding:.75rem 1rem}.card-image{display:block;position:relative}.card-content{background-color:transparent;padding:1.5rem}.card-footer{background-color:transparent;border-top:1px solid #ededed;align-items:stretch;display:flex}.card-footer-item{align-items:center;display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;padding:.75rem}.card-footer-item:not(:last-child){border-right:1px solid #ededed}.card .media:not(:last-child){margin-bottom:1.5rem}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-active .dropdown-menu,.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown.is-up .dropdown-menu{bottom:100%;padding-bottom:4px;padding-top:initial;top:auto}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#4a4a4a;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item,button.dropdown-item{padding-right:3rem;text-align:left;white-space:nowrap;width:100%}a.dropdown-item:hover,button.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}a.dropdown-item.is-active,button.dropdown-item.is-active{background-color:#3273dc;color:#fff}.dropdown-divider{background-color:#ededed;border:none;display:block;height:1px;margin:.5rem 0}.level{align-items:center;justify-content:space-between}.level code{border-radius:4px}.level img{display:inline-block;vertical-align:top}.level.is-mobile{display:flex}.level.is-mobile .level-left,.level.is-mobile .level-right{display:flex}.level.is-mobile .level-left+.level-right{margin-top:0}.level.is-mobile .level-item:not(:last-child){margin-bottom:0;margin-right:.75rem}.level.is-mobile .level-item:not(.is-narrow){flex-grow:1}@media screen and (min-width:769px),print{.level{display:flex}.level>.level-item:not(.is-narrow){flex-grow:1}}.level-item{align-items:center;display:flex;flex-basis:auto;flex-grow:0;flex-shrink:0;justify-content:center}.level-item .title,.level-item .subtitle{margin-bottom:0}@media screen and (max-width:768px){.level-item:not(:last-child){margin-bottom:.75rem}}.level-left,.level-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.level-left .level-item.is-flexible,.level-right .level-item.is-flexible{flex-grow:1}@media screen and (min-width:769px),print{.level-left .level-item:not(:last-child),.level-right .level-item:not(:last-child){margin-right:.75rem}}.level-left{align-items:center;justify-content:flex-start}@media screen and (max-width:768px){.level-left+.level-right{margin-top:1.5rem}}@media screen and (min-width:769px),print{.level-left{display:flex}}.level-right{align-items:center;justify-content:flex-end}@media screen and (min-width:769px),print{.level-right{display:flex}}.list{background-color:#fff;border-radius:4px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1)}.list-item{display:block;padding:.5em 1em}.list-item:not(a){color:#4a4a4a}.list-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-item:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px}.list-item:not(:last-child){border-bottom:1px solid #dbdbdb}.list-item.is-active{background-color:#3273dc;color:#fff}a.list-item{background-color:#f5f5f5;cursor:pointer}.media{align-items:flex-start;display:flex;text-align:left}.media .content:not(:last-child){margin-bottom:.75rem}.media .media{border-top:1px solid rgba(219,219,219,.5);display:flex;padding-top:.75rem}.media .media .content:not(:last-child),.media .media .control:not(:last-child){margin-bottom:.5rem}.media .media .media{padding-top:.5rem}.media .media .media+.media{margin-top:.5rem}.media+.media{border-top:1px solid rgba(219,219,219,.5);margin-top:1rem;padding-top:1rem}.media.is-large+.media{margin-top:1.5rem;padding-top:1.5rem}.media-left,.media-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.media-left{margin-right:1rem}.media-right{margin-left:1rem}.media-content{flex-basis:auto;flex-grow:1;flex-shrink:1;text-align:left}@media screen and (max-width:768px){.media-content{overflow-x:auto}}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-medium{font-size:1.25rem}.menu.is-large{font-size:1.5rem}.menu-list{line-height:1.25}.menu-list a{border-radius:2px;color:#4a4a4a;display:block;padding:.3em .75em}.menu-list a:hover{background-color:#f5f5f5;color:#363636}.menu-list a.is-active{background-color:#3273dc;color:#fff}.menu-list li ul{border-left:1px solid #dbdbdb;margin:.3em .75em;padding-left:.75em}.menu-label{color:#7a7a7a;font-size:.75em;letter-spacing:.1em;text-transform:uppercase}.menu-label:not(:first-child){margin-top:.7em}.menu-label:not(:last-child){margin-bottom:.7em}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag):not(.dropdown-item){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-medium{font-size:1.25rem}.message.is-large{font-size:1.5rem}.message.is-white{background-color:#fff}.message.is-white .message-header{background-color:#fff;color:#0a0a0a}.message.is-white .message-body{border-color:#fff}.message.is-black{background-color:#fafafa}.message.is-black .message-header{background-color:#0a0a0a;color:#fff}.message.is-black .message-body{border-color:#0a0a0a}.message.is-light{background-color:#fafafa}.message.is-light .message-header{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.message.is-light .message-body{border-color:#f5f5f5}.message.is-dark{background-color:#fafafa}.message.is-dark .message-header{background-color:#363636;color:#fff}.message.is-dark .message-body{border-color:#363636}.message.is-primary{background-color:#fdefec}.message.is-primary .message-header{background-color:#c93312;color:#fff}.message.is-primary .message-body{border-color:#c93312;color:#e13914}.message.is-link{background-color:#eef3fc}.message.is-link .message-header{background-color:#3273dc;color:#fff}.message.is-link .message-body{border-color:#3273dc;color:#2160c4}.message.is-info{background-color:#eef6fc}.message.is-info .message-header{background-color:#3298dc;color:#fff}.message.is-info .message-body{border-color:#3298dc;color:#1d72aa}.message.is-success{background-color:#effaf3}.message.is-success .message-header{background-color:#48c774;color:#fff}.message.is-success .message-body{border-color:#48c774;color:#257942}.message.is-warning{background-color:#fffbeb}.message.is-warning .message-header{background-color:#ffdd57;color:#fff}.message.is-warning .message-body{border-color:#ffdd57;color:#947600}.message.is-danger{background-color:#feecf0}.message.is-danger .message-header{background-color:#f14668;color:#fff}.message.is-danger .message-body{border-color:#f14668;color:#cc0f35}.message-header{align-items:center;background-color:#4a4a4a;border-radius:4px 4px 0 0;color:#fff;display:flex;font-weight:700;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative}.message-header .delete{flex-grow:0;flex-shrink:0;margin-left:.75em}.message-header+.message-body{border-width:0;border-top-left-radius:0;border-top-right-radius:0}.message-body{border-color:#dbdbdb;border-radius:4px;border-style:solid;border-width:0 0 0 4px;color:#4a4a4a;padding:1.25em 1.5em}.message-body code,.message-body pre{background-color:#fff}.message-body pre code{background-color:transparent}.modal{align-items:center;display:none;flex-direction:column;justify-content:center;overflow:hidden;position:fixed;z-index:40}.modal.is-active{display:flex}.modal-background{background-color:rgba(10,10,10,.86)}.modal-content,.modal-card{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}@media screen and (min-width:769px),print{.modal-content,.modal-card{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}.modal-close{background:0 0;height:40px;position:fixed;right:20px;top:20px;width:40px}.modal-card{display:flex;flex-direction:column;max-height:calc(100vh - 40px);overflow:hidden;-ms-overflow-y:visible}.modal-card-head,.modal-card-foot{align-items:center;background-color:#f5f5f5;display:flex;flex-shrink:0;justify-content:flex-start;padding:20px;position:relative}.modal-card-head{border-bottom:1px solid #dbdbdb;border-top-left-radius:6px;border-top-right-radius:6px}.modal-card-title{color:#363636;flex-grow:1;flex-shrink:0;font-size:1.5rem;line-height:1}.modal-card-foot{border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:1px solid #dbdbdb}.modal-card-foot .button:not(:last-child){margin-right:.5em}.modal-card-body{-webkit-overflow-scrolling:touch;background-color:#fff;flex-grow:1;flex-shrink:1;overflow:auto;padding:20px}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand>.navbar-item,.navbar.is-white .navbar-brand .navbar-link{color:#0a0a0a}.navbar.is-white .navbar-brand>a.navbar-item:focus,.navbar.is-white .navbar-brand>a.navbar-item:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand .navbar-link:focus,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-burger{color:#0a0a0a}@media screen and (min-width:1024px){.navbar.is-white .navbar-start>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-end .navbar-link{color:#0a0a0a}.navbar.is-white .navbar-start>a.navbar-item:focus,.navbar.is-white .navbar-start>a.navbar-item:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start .navbar-link:focus,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-end>a.navbar-item:focus,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end .navbar-link:focus,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-start .navbar-link::after,.navbar.is-white .navbar-end .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand>.navbar-item,.navbar.is-black .navbar-brand .navbar-link{color:#fff}.navbar.is-black .navbar-brand>a.navbar-item:focus,.navbar.is-black .navbar-brand>a.navbar-item:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand .navbar-link:focus,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand .navbar-link.is-active{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-black .navbar-start>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-end .navbar-link{color:#fff}.navbar.is-black .navbar-start>a.navbar-item:focus,.navbar.is-black .navbar-start>a.navbar-item:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start .navbar-link:focus,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-end>a.navbar-item:focus,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end .navbar-link:focus,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end .navbar-link.is-active{background-color:#000;color:#fff}.navbar.is-black .navbar-start .navbar-link::after,.navbar.is-black .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand>.navbar-item,.navbar.is-light .navbar-brand .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand>a.navbar-item:focus,.navbar.is-light .navbar-brand>a.navbar-item:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand .navbar-link:focus,.navbar.is-light .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-light .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width:1024px){.navbar.is-light .navbar-start>.navbar-item,.navbar.is-light .navbar-start .navbar-link,.navbar.is-light .navbar-end>.navbar-item,.navbar.is-light .navbar-end .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-light .navbar-start>a.navbar-item:focus,.navbar.is-light .navbar-start>a.navbar-item:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start .navbar-link:focus,.navbar.is-light .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-end>a.navbar-item:focus,.navbar.is-light .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end .navbar-link:focus,.navbar.is-light .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-start .navbar-link::after,.navbar.is-light .navbar-end .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-light .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:rgba(0,0,0,.7)}}.navbar.is-dark{background-color:#363636;color:#fff}.navbar.is-dark .navbar-brand>.navbar-item,.navbar.is-dark .navbar-brand .navbar-link{color:#fff}.navbar.is-dark .navbar-brand>a.navbar-item:focus,.navbar.is-dark .navbar-brand>a.navbar-item:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand .navbar-link:focus,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand .navbar-link.is-active{background-color:#292929;color:#fff}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-dark .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-dark .navbar-start>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-end .navbar-link{color:#fff}.navbar.is-dark .navbar-start>a.navbar-item:focus,.navbar.is-dark .navbar-start>a.navbar-item:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start .navbar-link:focus,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-end>a.navbar-item:focus,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end .navbar-link:focus,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end .navbar-link.is-active{background-color:#292929;color:#fff}.navbar.is-dark .navbar-start .navbar-link::after,.navbar.is-dark .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-dark .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link{background-color:#292929;color:#fff}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#fff}}.navbar.is-primary{background-color:#c93312;color:#fff}.navbar.is-primary .navbar-brand>.navbar-item,.navbar.is-primary .navbar-brand .navbar-link{color:#fff}.navbar.is-primary .navbar-brand>a.navbar-item:focus,.navbar.is-primary .navbar-brand>a.navbar-item:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand .navbar-link:focus,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand .navbar-link.is-active{background-color:#b22d10;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-primary .navbar-start>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-end .navbar-link{color:#fff}.navbar.is-primary .navbar-start>a.navbar-item:focus,.navbar.is-primary .navbar-start>a.navbar-item:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start .navbar-link:focus,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-end>a.navbar-item:focus,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end .navbar-link:focus,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end .navbar-link.is-active{background-color:#b22d10;color:#fff}.navbar.is-primary .navbar-start .navbar-link::after,.navbar.is-primary .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link{background-color:#b22d10;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#c93312;color:#fff}}.navbar.is-link{background-color:#3273dc;color:#fff}.navbar.is-link .navbar-brand>.navbar-item,.navbar.is-link .navbar-brand .navbar-link{color:#fff}.navbar.is-link .navbar-brand>a.navbar-item:focus,.navbar.is-link .navbar-brand>a.navbar-item:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand .navbar-link:focus,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand .navbar-link.is-active{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-link .navbar-start>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-end .navbar-link{color:#fff}.navbar.is-link .navbar-start>a.navbar-item:focus,.navbar.is-link .navbar-start>a.navbar-item:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start .navbar-link:focus,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-end>a.navbar-item:focus,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end .navbar-link:focus,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end .navbar-link.is-active{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-start .navbar-link::after,.navbar.is-link .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#3273dc;color:#fff}}.navbar.is-info{background-color:#3298dc;color:#fff}.navbar.is-info .navbar-brand>.navbar-item,.navbar.is-info .navbar-brand .navbar-link{color:#fff}.navbar.is-info .navbar-brand>a.navbar-item:focus,.navbar.is-info .navbar-brand>a.navbar-item:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand .navbar-link:focus,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand .navbar-link.is-active{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-info .navbar-start>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-end .navbar-link{color:#fff}.navbar.is-info .navbar-start>a.navbar-item:focus,.navbar.is-info .navbar-start>a.navbar-item:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start .navbar-link:focus,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-end>a.navbar-item:focus,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end .navbar-link:focus,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end .navbar-link.is-active{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-start .navbar-link::after,.navbar.is-info .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#3298dc;color:#fff}}.navbar.is-success{background-color:#48c774;color:#fff}.navbar.is-success .navbar-brand>.navbar-item,.navbar.is-success .navbar-brand .navbar-link{color:#fff}.navbar.is-success .navbar-brand>a.navbar-item:focus,.navbar.is-success .navbar-brand>a.navbar-item:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand .navbar-link:focus,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand .navbar-link.is-active{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-success .navbar-start>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-end .navbar-link{color:#fff}.navbar.is-success .navbar-start>a.navbar-item:focus,.navbar.is-success .navbar-start>a.navbar-item:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start .navbar-link:focus,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-end>a.navbar-item:focus,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end .navbar-link:focus,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end .navbar-link.is-active{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-start .navbar-link::after,.navbar.is-success .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#48c774;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:#fff}.navbar.is-warning .navbar-brand>.navbar-item,.navbar.is-warning .navbar-brand .navbar-link{color:#fff}.navbar.is-warning .navbar-brand>a.navbar-item:focus,.navbar.is-warning .navbar-brand>a.navbar-item:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand .navbar-link:focus,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand .navbar-link.is-active{background-color:#ffd83d;color:#fff}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-warning .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-warning .navbar-start>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-end .navbar-link{color:#fff}.navbar.is-warning .navbar-start>a.navbar-item:focus,.navbar.is-warning .navbar-start>a.navbar-item:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start .navbar-link:focus,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-end>a.navbar-item:focus,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end .navbar-link:focus,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end .navbar-link.is-active{background-color:#ffd83d;color:#fff}.navbar.is-warning .navbar-start .navbar-link::after,.navbar.is-warning .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-warning .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link{background-color:#ffd83d;color:#fff}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:#fff}}.navbar.is-danger{background-color:#f14668;color:#fff}.navbar.is-danger .navbar-brand>.navbar-item,.navbar.is-danger .navbar-brand .navbar-link{color:#fff}.navbar.is-danger .navbar-brand>a.navbar-item:focus,.navbar.is-danger .navbar-brand>a.navbar-item:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand .navbar-link:focus,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand .navbar-link.is-active{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-burger{color:#fff}@media screen and (min-width:1024px){.navbar.is-danger .navbar-start>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-end .navbar-link{color:#fff}.navbar.is-danger .navbar-start>a.navbar-item:focus,.navbar.is-danger .navbar-start>a.navbar-item:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start .navbar-link:focus,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-end>a.navbar-item:focus,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end .navbar-link:focus,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end .navbar-link.is-active{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-start .navbar-link::after,.navbar.is-danger .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#f14668;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px whitesmoke}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px whitesmoke}.navbar.is-fixed-top{top:0}html.has-navbar-fixed-top,body.has-navbar-fixed-top{padding-top:3.25rem}html.has-navbar-fixed-bottom,body.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:focus,.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{color:#4a4a4a;cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,5%)}.navbar-burger.is-active span:nth-child(1){transform:translateY(5px)rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){transform:translateY(-5px)rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#4a4a4a;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-.25rem;margin-right:-.25rem}a.navbar-item,.navbar-link{cursor:pointer}a.navbar-item:focus,a.navbar-item:focus-within,a.navbar-item:hover,a.navbar-item.is-active,.navbar-link:focus,.navbar-link:focus-within,.navbar-link:hover,.navbar-link.is-active{background-color:#fafafa;color:#3273dc}.navbar-item{display:block;flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(.5rem - 1px)}.navbar-item.is-tab:focus,.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#3273dc}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#3273dc;border-bottom-style:solid;border-bottom-width:3px;color:#3273dc;padding-bottom:calc(.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link:not(.is-arrowless){padding-right:2.5em}.navbar-link:not(.is-arrowless)::after{border-color:#3273dc;margin-top:-.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width:1023px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}html.has-navbar-fixed-top-touch,body.has-navbar-fixed-top-touch{padding-top:3.25rem}html.has-navbar-fixed-bottom-touch,body.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width:1024px){.navbar,.navbar-menu,.navbar-start,.navbar-end{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-start,.navbar.is-spaced .navbar-end{align-items:center}.navbar.is-spaced a.navbar-item,.navbar.is-spaced .navbar-link{border-radius:4px}.navbar.is-transparent a.navbar-item:focus,.navbar.is-transparent a.navbar-item:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent .navbar-link:focus,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent .navbar-link.is-active{background-color:transparent!important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus-within .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent!important}.navbar.is-transparent .navbar-dropdown a.navbar-item:focus,.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item{display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{transform:rotate(135deg)translate(.25em,-.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed{opacity:1;pointer-events:auto;transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:focus,.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar.is-spaced .navbar-dropdown,.navbar-dropdown.is-boxed{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.navbar>.container .navbar-brand,.container>.navbar .navbar-brand{margin-left:-.75rem}.navbar>.container .navbar-menu,.container>.navbar .navbar-menu{margin-right:-.75rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}html.has-navbar-fixed-top-desktop,body.has-navbar-fixed-top-desktop{padding-top:3.25rem}html.has-navbar-fixed-bottom-desktop,body.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}html.has-spaced-navbar-fixed-top,body.has-spaced-navbar-fixed-top{padding-top:5.25rem}html.has-spaced-navbar-fixed-bottom,body.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}a.navbar-item.is-active,.navbar-link.is-active{color:#0a0a0a}a.navbar-item.is-active:not(:focus):not(:hover),.navbar-link.is-active:not(:focus):not(:hover){background-color:transparent}.navbar-item.has-dropdown:focus .navbar-link,.navbar-item.has-dropdown:hover .navbar-link,.navbar-item.has-dropdown.is-active .navbar-link{background-color:#fafafa}}.hero.is-fullheight-with-navbar{min-height:calc(100vh - 3.25rem)}.pagination{font-size:1rem;margin:-.25rem}.pagination.is-small{font-size:.75rem}.pagination.is-medium{font-size:1.25rem}.pagination.is-large{font-size:1.5rem}.pagination.is-rounded .pagination-previous,.pagination.is-rounded .pagination-next{padding-left:1em;padding-right:1em;border-radius:290486px}.pagination.is-rounded .pagination-link{border-radius:290486px}.pagination,.pagination-list{align-items:center;display:flex;justify-content:center;text-align:center}.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis{font-size:1em;justify-content:center;margin:.25rem;padding-left:.5em;padding-right:.5em;text-align:center}.pagination-previous,.pagination-next,.pagination-link{border-color:#dbdbdb;color:#363636;min-width:2.5em}.pagination-previous:hover,.pagination-next:hover,.pagination-link:hover{border-color:#b5b5b5;color:#363636}.pagination-previous:focus,.pagination-next:focus,.pagination-link:focus{border-color:#3273dc}.pagination-previous:active,.pagination-next:active,.pagination-link:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2)}.pagination-previous[disabled],.pagination-next[disabled],.pagination-link[disabled]{background-color:#dbdbdb;border-color:#dbdbdb;box-shadow:none;color:#7a7a7a;opacity:.5}.pagination-previous,.pagination-next{padding-left:.75em;padding-right:.75em;white-space:nowrap}.pagination-link.is-current{background-color:#3273dc;border-color:#3273dc;color:#fff}.pagination-ellipsis{color:#b5b5b5;pointer-events:none}.pagination-list{flex-wrap:wrap}@media screen and (max-width:768px){.pagination{flex-wrap:wrap}.pagination-previous,.pagination-next{flex-grow:1;flex-shrink:1}.pagination-list li{flex-grow:1;flex-shrink:1}}@media screen and (min-width:769px),print{.pagination-list{flex-grow:1;flex-shrink:1;justify-content:flex-start;order:1}.pagination-previous{order:2}.pagination-next{order:3}.pagination{justify-content:space-between}.pagination.is-centered .pagination-previous{order:1}.pagination.is-centered .pagination-list{justify-content:center;order:2}.pagination.is-centered .pagination-next{order:3}.pagination.is-right .pagination-previous{order:1}.pagination.is-right .pagination-next{order:2}.pagination.is-right .pagination-list{justify-content:flex-end;order:3}}.panel{border-radius:6px;box-shadow:0 .5em 1em -.125em rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,2%);font-size:1rem}.panel:not(:last-child){margin-bottom:1.5rem}.panel.is-white .panel-heading{background-color:#fff;color:#0a0a0a}.panel.is-white .panel-tabs a.is-active{border-bottom-color:#fff}.panel.is-white .panel-block.is-active .panel-icon{color:#fff}.panel.is-black .panel-heading{background-color:#0a0a0a;color:#fff}.panel.is-black .panel-tabs a.is-active{border-bottom-color:#0a0a0a}.panel.is-black .panel-block.is-active .panel-icon{color:#0a0a0a}.panel.is-light .panel-heading{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.panel.is-light .panel-tabs a.is-active{border-bottom-color:#f5f5f5}.panel.is-light .panel-block.is-active .panel-icon{color:#f5f5f5}.panel.is-dark .panel-heading{background-color:#363636;color:#fff}.panel.is-dark .panel-tabs a.is-active{border-bottom-color:#363636}.panel.is-dark .panel-block.is-active .panel-icon{color:#363636}.panel.is-primary .panel-heading{background-color:#c93312;color:#fff}.panel.is-primary .panel-tabs a.is-active{border-bottom-color:#c93312}.panel.is-primary .panel-block.is-active .panel-icon{color:#c93312}.panel.is-link .panel-heading{background-color:#3273dc;color:#fff}.panel.is-link .panel-tabs a.is-active{border-bottom-color:#3273dc}.panel.is-link .panel-block.is-active .panel-icon{color:#3273dc}.panel.is-info .panel-heading{background-color:#3298dc;color:#fff}.panel.is-info .panel-tabs a.is-active{border-bottom-color:#3298dc}.panel.is-info .panel-block.is-active .panel-icon{color:#3298dc}.panel.is-success .panel-heading{background-color:#48c774;color:#fff}.panel.is-success .panel-tabs a.is-active{border-bottom-color:#48c774}.panel.is-success .panel-block.is-active .panel-icon{color:#48c774}.panel.is-warning .panel-heading{background-color:#ffdd57;color:#fff}.panel.is-warning .panel-tabs a.is-active{border-bottom-color:#ffdd57}.panel.is-warning .panel-block.is-active .panel-icon{color:#ffdd57}.panel.is-danger .panel-heading{background-color:#f14668;color:#fff}.panel.is-danger .panel-tabs a.is-active{border-bottom-color:#f14668}.panel.is-danger .panel-block.is-active .panel-icon{color:#f14668}.panel-tabs:not(:last-child),.panel-block:not(:last-child){border-bottom:1px solid #ededed}.panel-heading{background-color:#ededed;border-radius:6px 6px 0 0;color:#363636;font-size:1em;font-weight:600;line-height:1.25;padding:.75em 1em}.panel-tabs{align-items:flex-end;display:flex;font-size:.875em;justify-content:center}.panel-tabs a{border-bottom:1px solid #dbdbdb;margin-bottom:-1px;padding:.5em}.panel-tabs a.is-active{border-bottom-color:#4a4a4a;color:#363636}.panel-list a{color:#4a4a4a}.panel-list a:hover{color:#3273dc}.panel-block{align-items:center;color:#363636;display:flex;justify-content:flex-start;padding:.5em .75em}.panel-block input[type=checkbox]{margin-right:.75em}.panel-block>.control{flex-grow:1;flex-shrink:1;width:100%}.panel-block.is-wrapped{flex-wrap:wrap}.panel-block.is-active{border-left-color:#3273dc;color:#363636}.panel-block.is-active .panel-icon{color:#3273dc}.panel-block:last-child{border-bottom-left-radius:6px;border-bottom-right-radius:6px}a.panel-block,label.panel-block{cursor:pointer}a.panel-block:hover,label.panel-block:hover{background-color:#f5f5f5}.panel-icon{display:inline-block;font-size:14px;height:1em;line-height:1em;text-align:center;vertical-align:top;width:1em;color:#7a7a7a;margin-right:.75em}.panel-icon .fa{font-size:inherit;line-height:inherit}.tabs{-webkit-overflow-scrolling:touch;align-items:stretch;display:flex;font-size:1rem;justify-content:space-between;overflow:hidden;overflow-x:auto;white-space:nowrap}.tabs a{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;color:#4a4a4a;display:flex;justify-content:center;margin-bottom:-1px;padding:.5em 1em;vertical-align:top}.tabs a:hover{border-bottom-color:#363636;color:#363636}.tabs li{display:block}.tabs li.is-active a{border-bottom-color:#3273dc;color:#3273dc}.tabs ul{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;display:flex;flex-grow:1;flex-shrink:0;justify-content:flex-start}.tabs ul.is-left{padding-right:.75em}.tabs ul.is-center{flex:none;justify-content:center;padding-left:.75em;padding-right:.75em}.tabs ul.is-right{justify-content:flex-end;padding-left:.75em}.tabs .icon:first-child{margin-right:.5em}.tabs .icon:last-child{margin-left:.5em}.tabs.is-centered ul{justify-content:center}.tabs.is-right ul{justify-content:flex-end}.tabs.is-boxed a{border:1px solid transparent;border-radius:4px 4px 0 0}.tabs.is-boxed a:hover{background-color:#f5f5f5;border-bottom-color:#dbdbdb}.tabs.is-boxed li.is-active a{background-color:#fff;border-color:#dbdbdb;border-bottom-color:transparent!important}.tabs.is-fullwidth li{flex-grow:1;flex-shrink:0}.tabs.is-toggle a{border-color:#dbdbdb;border-style:solid;border-width:1px;margin-bottom:0;position:relative}.tabs.is-toggle a:hover{background-color:#f5f5f5;border-color:#b5b5b5;z-index:2}.tabs.is-toggle li+li{margin-left:-1px}.tabs.is-toggle li:first-child a{border-radius:4px 0 0 4px}.tabs.is-toggle li:last-child a{border-radius:0 4px 4px 0}.tabs.is-toggle li.is-active a{background-color:#3273dc;border-color:#3273dc;color:#fff;z-index:1}.tabs.is-toggle ul{border-bottom:none}.tabs.is-toggle.is-toggle-rounded li:first-child a{border-bottom-left-radius:290486px;border-top-left-radius:290486px;padding-left:1.25em}.tabs.is-toggle.is-toggle-rounded li:last-child a{border-bottom-right-radius:290486px;border-top-right-radius:290486px;padding-right:1.25em}.tabs.is-small{font-size:.75rem}.tabs.is-medium{font-size:1.25rem}.tabs.is-large{font-size:1.5rem}.hero{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar{background:0 0}.hero .tabs ul{border-bottom:none}.hero.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-white strong{color:inherit}.hero.is-white .title{color:#0a0a0a}.hero.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width:1023px){.hero.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,.hero.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white a.navbar-item:hover,.hero.is-white a.navbar-item.is-active,.hero.is-white .navbar-link:hover,.hero.is-white .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold{background-image:linear-gradient(141deg,#e8e3e4 0%,white 71%,white 100%)}@media screen and (max-width:768px){.hero.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg,#e8e3e4 0%,white 71%,white 100%)}}.hero.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-black strong{color:inherit}.hero.is-black .title{color:#fff}.hero.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,.hero.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black a.navbar-item:hover,.hero.is-black a.navbar-item.is-active,.hero.is-black .navbar-link:hover,.hero.is-black .navbar-link.is-active{background-color:#000;color:#fff}.hero.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold{background-image:linear-gradient(141deg,black 0%,#0a0a0a 71%,#181616 100%)}@media screen and (max-width:768px){.hero.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg,black 0%,#0a0a0a 71%,#181616 100%)}}.hero.is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-light strong{color:inherit}.hero.is-light .title{color:rgba(0,0,0,.7)}.hero.is-light .subtitle{color:rgba(0,0,0,.9)}.hero.is-light .subtitle a:not(.button),.hero.is-light .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:1023px){.hero.is-light .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,.hero.is-light .navbar-link{color:rgba(0,0,0,.7)}.hero.is-light a.navbar-item:hover,.hero.is-light a.navbar-item.is-active,.hero.is-light .navbar-link:hover,.hero.is-light .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.hero.is-light .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-light .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-light .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#f5f5f5}.hero.is-light.is-bold{background-image:linear-gradient(141deg,#dfd8d9 0%,whitesmoke 71%,white 100%)}@media screen and (max-width:768px){.hero.is-light.is-bold .navbar-menu{background-image:linear-gradient(141deg,#dfd8d9 0%,whitesmoke 71%,white 100%)}}.hero.is-dark{background-color:#363636;color:#fff}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-dark strong{color:inherit}.hero.is-dark .title{color:#fff}.hero.is-dark .subtitle{color:rgba(255,255,255,.9)}.hero.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,.hero.is-dark .navbar-link{color:rgba(255,255,255,.7)}.hero.is-dark a.navbar-item:hover,.hero.is-dark a.navbar-item.is-active,.hero.is-dark .navbar-link:hover,.hero.is-dark .navbar-link.is-active{background-color:#292929;color:#fff}.hero.is-dark .tabs a{color:#fff;opacity:.9}.hero.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a{color:#fff}.hero.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#363636}.hero.is-dark.is-bold{background-image:linear-gradient(141deg,#1f191a 0%,#363636 71%,#46403f 100%)}@media screen and (max-width:768px){.hero.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1f191a 0%,#363636 71%,#46403f 100%)}}.hero.is-primary{background-color:#c93312;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-primary strong{color:inherit}.hero.is-primary .title{color:#fff}.hero.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-primary .navbar-menu{background-color:#c93312}}.hero.is-primary .navbar-item,.hero.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary a.navbar-item:hover,.hero.is-primary a.navbar-item.is-active,.hero.is-primary .navbar-link:hover,.hero.is-primary .navbar-link.is-active{background-color:#b22d10;color:#fff}.hero.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#c93312}.hero.is-primary.is-bold{background-image:linear-gradient(141deg,#a30805 0%,#C93312 71%,#e7590e 100%)}@media screen and (max-width:768px){.hero.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg,#a30805 0%,#C93312 71%,#e7590e 100%)}}.hero.is-link{background-color:#3273dc;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-link strong{color:inherit}.hero.is-link .title{color:#fff}.hero.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-link .navbar-menu{background-color:#3273dc}}.hero.is-link .navbar-item,.hero.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link a.navbar-item:hover,.hero.is-link a.navbar-item.is-active,.hero.is-link .navbar-link:hover,.hero.is-link .navbar-link.is-active{background-color:#2366d1;color:#fff}.hero.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3273dc}.hero.is-link.is-bold{background-image:linear-gradient(141deg,#1577c6 0%,#3273dc 71%,#4366e5 100%)}@media screen and (max-width:768px){.hero.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1577c6 0%,#3273dc 71%,#4366e5 100%)}}.hero.is-info{background-color:#3298dc;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-info strong{color:inherit}.hero.is-info .title{color:#fff}.hero.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-info .navbar-menu{background-color:#3298dc}}.hero.is-info .navbar-item,.hero.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info a.navbar-item:hover,.hero.is-info a.navbar-item.is-active,.hero.is-info .navbar-link:hover,.hero.is-info .navbar-link.is-active{background-color:#238cd1;color:#fff}.hero.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3298dc}.hero.is-info.is-bold{background-image:linear-gradient(141deg,#159dc6 0%,#3298dc 71%,#4389e5 100%)}@media screen and (max-width:768px){.hero.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg,#159dc6 0%,#3298dc 71%,#4389e5 100%)}}.hero.is-success{background-color:#48c774;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-success strong{color:inherit}.hero.is-success .title{color:#fff}.hero.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-success .navbar-menu{background-color:#48c774}}.hero.is-success .navbar-item,.hero.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success a.navbar-item:hover,.hero.is-success a.navbar-item.is-active,.hero.is-success .navbar-link:hover,.hero.is-success .navbar-link.is-active{background-color:#3abb67;color:#fff}.hero.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#48c774}.hero.is-success.is-bold{background-image:linear-gradient(141deg,#29b342 0%,#48c774 71%,#56d296 100%)}@media screen and (max-width:768px){.hero.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg,#29b342 0%,#48c774 71%,#56d296 100%)}}.hero.is-warning{background-color:#ffdd57;color:#fff}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-warning strong{color:inherit}.hero.is-warning .title{color:#fff}.hero.is-warning .subtitle{color:rgba(255,255,255,.9)}.hero.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,.hero.is-warning .navbar-link{color:rgba(255,255,255,.7)}.hero.is-warning a.navbar-item:hover,.hero.is-warning a.navbar-item.is-active,.hero.is-warning .navbar-link:hover,.hero.is-warning .navbar-link.is-active{background-color:#ffd83d;color:#fff}.hero.is-warning .tabs a{color:#fff;opacity:.9}.hero.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a{color:#fff}.hero.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#ffdd57}.hero.is-warning.is-bold{background-image:linear-gradient(141deg,#ffaf24 0%,#ffdd57 71%,#fffa70 100%)}@media screen and (max-width:768px){.hero.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ffaf24 0%,#ffdd57 71%,#fffa70 100%)}}.hero.is-danger{background-color:#f14668;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-danger strong{color:inherit}.hero.is-danger .title{color:#fff}.hero.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong{color:#fff}@media screen and (max-width:1023px){.hero.is-danger .navbar-menu{background-color:#f14668}}.hero.is-danger .navbar-item,.hero.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger a.navbar-item:hover,.hero.is-danger a.navbar-item.is-active,.hero.is-danger .navbar-link:hover,.hero.is-danger .navbar-link.is-active{background-color:#ef2e55;color:#fff}.hero.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#f14668}.hero.is-danger.is-bold{background-image:linear-gradient(141deg,#fa0a62 0%,#f14668 71%,#f7595f 100%)}@media screen and (max-width:768px){.hero.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg,#fa0a62 0%,#f14668 71%,#f7595f 100%)}}.hero.is-small .hero-body{padding:1.5rem}@media screen and (min-width:769px),print{.hero.is-medium .hero-body{padding:9rem 1.5rem}}@media screen and (min-width:769px),print{.hero.is-large .hero-body{padding:18rem 1.5rem}}.hero.is-halfheight .hero-body,.hero.is-fullheight .hero-body,.hero.is-fullheight-with-navbar .hero-body{align-items:center;display:flex}.hero.is-halfheight .hero-body>.container,.hero.is-fullheight .hero-body>.container,.hero.is-fullheight-with-navbar .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight{min-height:50vh}.hero.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;transform:translate3d(-50%,-50%,0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width:768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width:768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width:769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-head,.hero-foot{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width:1024px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem}html{scroll-behavior:smooth}html,body{height:100%}body>div{min-height:100%}#sidebar{background-color:#eee;border-right:1px solid #c1c1c1;box-shadow:0 0 20px rgba(50,50,50,.2)inset;padding:1.75rem}#sidebar .brand{padding:1rem 0;text-align:center}#main{padding:3rem 3rem 9rem}.example{max-width:70vw;margin-bottom:1em}.example .highlight{margin:0}.example .path{font-style:italic;width:100%;text-align:right}code{color:#1a9f1a;font-size:.875em;font-weight:400}.content h2{padding-top:1em;border-top:1px solid silver}h1 .anchor,h2 .anchor,h3 .anchor,h4 .anchor,h5 .anchor,h6 .anchor,th .anchor{display:inline-block;width:0;margin-left:-1.5rem;margin-right:1.5rem;transition:all 100ms ease-in-out;opacity:0}h1:hover .anchor,h2:hover .anchor,h3:hover .anchor,h4:hover .anchor,h5:hover .anchor,h6:hover .anchor,th:hover .anchor{opacity:1}h1:target,h2:target,h3:target,h4:target,h5:target,h6:target,th:target{color:#c93312}h1:target .anchor,h2:target .anchor,h3:target .anchor,h4:target .anchor,h5:target .anchor,h6:target .anchor,th:target .anchor{opacity:1;color:#c93312}table code{white-space:pre}.footnotes p{display:inline}figure.has-border img{box-shadow:0 0 20px rgba(0,0,0,.25)}.asciicast-wrapper{margin:2rem 0}.asciicast-wrapper asciinema-player{display:block;margin-bottom:1rem}.asciicast-wrapper pre.asciinema-terminal{padding:0;overflow-x:hidden;-webkit-overflow-scrolling:auto}.asciicast-wrapper .panel-block{justify-content:space-between}.asciicast-wrapper .panel-block.is-active .tag{background-color:#3273dc;color:#fff} \ No newline at end of file diff --git a/docs/public/tags/index.html b/docs/public/tags/index.html new file mode 100644 index 00000000..456cf3d0 --- /dev/null +++ b/docs/public/tags/index.html @@ -0,0 +1,38 @@ + + + + + + + + + Tags + + + + + + + + + + + + + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + + diff --git a/docs/public/tags/index.xml b/docs/public/tags/index.xml new file mode 100644 index 00000000..5971d2f1 --- /dev/null +++ b/docs/public/tags/index.xml @@ -0,0 +1,10 @@ + + + + Tags on scalpel.org docs + /tags/ + Recent content in Tags on scalpel.org docs + Hugo -- gohugo.io + en-us + + diff --git a/docs/public/tute-aes/index.html b/docs/public/tute-aes/index.html new file mode 100644 index 00000000..60cc680b --- /dev/null +++ b/docs/public/tute-aes/index.html @@ -0,0 +1,497 @@ + + + + + + + + + Decrypting custom encryption + + + + + + + + + + + + +
    + +
    + + + + + Edit on GitHub + + + +

    #  Decrypting custom encryption

    +

    #  Context

    +

    An IOT appliance adds an obfuscation layer to its HTTP communications by encrypting the body of its requests and responses with a key.

    +

    On every HTTP request, the program sends two POST parameters:

    +
      +
    • secret (the encryption key)
    • +
    • encrypted (the ciphertext).
    • +
    +

    Let’s solve this problem by using Scalpel!

    +

    It will provide an additional tab in the Repeater which displays the plaintext for every request and response. The plaintext can also be edited. Scalpel will automatically encrypt it when the “Send” button is hit.

    +
    +

    💡 Find a mock API to test this case in Scalpel’s GitHub repository: test/server.js.

    +
    + +

    #  Table of content

    +
      +
    1. Take a look at the target
    2. +
    3. Reimplement the encryption / decryption
    4. +
    5. Create the script using Scalpel
    6. +
    7. Implement the encryption algorithm
    8. +
    9. Create custom editors
    10. +
    11. Filtering requests/responses sent to hooks
    12. +
    13. Conclusion
    14. +
    +

    #  1. Take a look at the target

    +

    Take the time to get familiar with the API code:

    +
    const { urlencoded } = require("express");
    +
    +const app = require("express")();
    +
    +app.use(urlencoded({ extended: true }));
    +
    +const crypto = require("crypto");
    +
    +const derive = (secret) => {
    +	const hasher = crypto.createHash("sha256");
    +	hasher.update(secret);
    +	const derived_aes_key = hasher.digest().slice(0, 32);
    +	return derived_aes_key;
    +};
    +
    +const get_cipher_decrypt = (secret, iv = Buffer.alloc(16, 0)) => {
    +	const derived_aes_key = derive(secret);
    +	const cipher = crypto.createDecipheriv("aes-256-cbc", derived_aes_key, iv);
    +	return cipher;
    +};
    +
    +const get_cipher_encrypt = (secret, iv = Buffer.alloc(16, 0)) => {
    +	const derived_aes_key = derive(secret);
    +	const cipher = crypto.createCipheriv("aes-256-cbc", derived_aes_key, iv);
    +	return cipher;
    +};
    +
    +const decrypt = (secret, data) => {
    +	const decipher = get_cipher_decrypt(secret);
    +	let decrypted = decipher.update(data, "base64", "utf8");
    +	decrypted += decipher.final("utf8");
    +	return decrypted;
    +};
    +
    +const encrypt = (secret, data) => {
    +	const cipher = get_cipher_encrypt(secret);
    +	let encrypted = cipher.update(data, "utf8", "base64");
    +	encrypted += cipher.final("base64");
    +	return encrypted;
    +};
    +
    +app.post("/encrypt", (req, res) => {
    +	const secret = req.body["secret"];
    +	const data = req.body["encrypted"];
    +
    +	if (data === undefined) {
    +		res.send("No content");
    +		return;
    +	}
    +
    +	const decrypted = decrypt(secret, data);
    +	const resContent = `You have sent "${decrypted}" using secret "${secret}"`;
    +	const encrypted = encrypt(secret, resContent);
    +
    +	res.send(encrypted);
    +});
    +
    +app.listen(3000, ["localhost"]);
    +

    As shown above, every request content is encrypted using AES, using a secret passed alongside the content, that also encrypt the response.

    +

    In vanilla Burp, editing the request would be very tedious (using copy to file). When faced against a case like this, users will either work with custom scripts outside of Burp, use tools like mitmproxy, write their own Burp Java extension, or give up.

    +

    Scalpel’s main objective is to make working around such cases trivial.

    +

    #  2. Reimplement the encryption / decryption

    +

    Before using Scalpel for handling this API’s encryption, the first thing to do is to implement the encryption process in Python.

    +

    #  Installing Python dependencies

    +

    To work with AES in Python, the pycryptodome module is required but not installed by default. All Scalpel Python scripts run in a virtual environment. Fortunately, Scalpel provides a way to switch between venvs and install packages through Burp GUI.

    +
      +
    1. Let’s jump to the Scalpel tab:
    2. +
    +
    +
    + +
      +
    1. Focus on the left part. You can use this interface to create and select new venvs.
    2. +
    +
    +
    + +
      +
    1. Let’s create a venv for this use case. Enter a name and press enter:
    2. +
    +

    +
    + +
    +
    +

    +
      +
    1. It is now possible to select it by clicking on its path:
    2. +
    +
    +
    + +
      +
    1. The central terminal is now activated in the selected venv and can be used to install packages using pip in the usual way:
    2. +
    +
    +
    + +
      +
    1. pycryptodome is now installed. Let’s create the Scalpel script!
    2. +
    +

    #  3. Create the script using Scalpel

    +

    You can create a new script for Scalpel using the GUI:

    +
      +
    1. Click the Create new script button (underlined in red below).
    2. +
    +
    +
    + +
      +
    1. Enter the desired filename.
    2. +
    +
    +
    + +
      +
    1. Once the file is created, this message will show up:
    2. +
    +
    +
    + +
      +
    1. After following this steps, the script should either be opened in your preferred graphical editor or in the terminal provided by Scalpel:
    2. +
    +
    +
    + +
      +
    1. It contains commented hooks declarations. Remove them, as you will rewrite them further in this tutorial.
    2. +
    +

    #  4. Implement the encryption algorithm

    +

    With pycryptodome, the encryption can be written in Python like this:

    +
    from Crypto.Cipher import AES
    +from Crypto.Hash import SHA256
    +from Crypto.Util.Padding import pad, unpad
    +from base64 import b64encode, b64decode
    +
    +def get_cipher(secret: bytes, iv=bytes(16)):
    +    hasher = SHA256.new()
    +    hasher.update(secret)
    +    derived_aes_key = hasher.digest()[:32]
    +    cipher = AES.new(derived_aes_key, AES.MODE_CBC, iv)
    +    return cipher
    +
    +
    +def decrypt(secret: bytes, data: bytes) -> bytes:
    +    data = b64decode(data)
    +    cipher = get_cipher(secret)
    +    decrypted = cipher.decrypt(data)
    +    return unpad(decrypted, AES.block_size)
    +
    +
    +def encrypt(secret: bytes, data: bytes) -> bytes:
    +    cipher = get_cipher(secret)
    +    padded_data = pad(data, AES.block_size)
    +    encrypted = cipher.encrypt(padded_data)
    +    return b64encode(encrypted)
    +

    #  5. Create custom editors

    +

    The above code can now be used to automatically decrypt your content to plaintext and re-encrypt a modified plaintext.

    +

    As explained in Editors, request editors are created by declaring the req_edit_in hook:

    +
    def req_edit_in_encrypted(req: Request) -> bytes | None:
    +    ...
    +

    Here, the _encrypted suffix was added to the hook name, creating a tab named “encrypted”.

    +
      +
    1. +

      Create a request editor.

      +

      This hook is called when Burp opens the request in an editor. It receives the request to edit and returns the bytes to display in the editor.

      +

      In order to display the plain text, the following must be done:

      +
        +
      • Get the secret and the encrypted content from the body.
      • +
      • Decrypt the content using the secret.
      • +
      • Return the decrypted bytes.
      • +
      +
      from pyscalpel import Request, Response, Flow
      +
      +def req_edit_in_encrypted(req: Request) -> bytes | None:
      +    secret = req.form[b"secret"]
      +    encrypted = req.form[b"encrypted"]
      +    if not encrypted:
      +        return b""
      +
      +    return decrypt(secret, encrypted)
      +

      Once this script is loaded with Scalpel, if you open an encrypted request in Burp, you will see a Scalpel tab along the Pretty, Raw, and Hex tabs:

      +

      +
      + +
      +
      +

      +

      But there is an issue. Right now, the additional tab cannot be edited since it has no way to encrypt the content back.

      +
    2. +
    +
    +
      +
    1. +

      To do so, the req_edit_out hook will be handful.

      +

      The req_edit_out hook has to implement the opposite behavior of req_edit_in, which means:

      +
        +
      • Encrypt the plain text using the secret.
      • +
      • Replace the old encrypted content in the request.
      • +
      • Return the new request.
      • +
      +
      def req_edit_out_encrypted(req: Request, text: bytes) -> Request:
      +    secret = req.form[b"secret"]
      +    req.form[b"encrypted"] = encrypt(secret, text)
      +    return req
      +
      +

      âš ï¸ When present, the req_edit_out suffix must match the req_edit_in suffix.
      +In this tutorial example, the suffix is: _encrypted

      +
      +
    2. +
    +
    +
      +
    1. +

      Add the hook. You should now be able to edit the plaintext. It will automatically be encrypted using req_edit_out_encrypted.

      +
      +
      + +
    2. +
    +
    +
      +
    1. +

      After that, it would be nice to decrypt the response to see if the changes were reflected.

      +

      The process is basically the same:

      +
      def res_edit_in_encrypted(res: Response) -> bytes | None:
      +    secret = res.request.form[b"secret"]
      +    encrypted = res.content
      +
      +    if not encrypted:
      +        return b""
      +
      +    return decrypt(secret, encrypted)
      +
      +# This is used to edit the response received by the browser in the proxy, but is useless in Repeater/Logger.
      +def res_edit_out_encrypted(res: Response, text: bytes) -> Response:
      +    secret = res.request.form[b"secret"]
      +    res.content = encrypt(secret, text)
      +    return res
      +
      +
      + +
    2. +
    +
    +
      +
    1. You can now edit the responses received by the browser as well.
    2. +
    +

    #  6. Filtering requests/responses sent to hooks

    +

    Scalpel provides a match() hook to filter unwanted requests from being treated by your hooks.

    +

    In this case, the encrypted requests are only sent to the /encrypt path and contain a secret. Thus, better not try to decrypt traffic that don’t match these conditions.

    +
    from pyscalpel import Request, Response, Flow
    +
    +def match(flow: Flow) -> bool:
    +    return flow.path_is("/encrypt*") and flow.request.form.get(b"secret") is not None
    +

    The above match hook receives a Flow object. It contains a request. When treating a response, it contains both the response and its initiating request.

    +

    It ensures the initiating request contained a secret field and was sent to a path matching /encrypt*

    +

    #  Conclusion

    +

    In this tutorial, you saw how to decrypt a custom encryption in IoT appliance communications using Scalpel. +This involved:

    +
      +
    • understanding the existing API encryption code
    • +
    • recreating the encryption process in Python
    • +
    • installing necessary Python dependencies
    • +
    • and creating custom editors to handle decryption and re-encryption of modified content.
    • +
    +

    This process was implemented for both request and response flows, allowing to view and manipulate the plaintext communication, then encrypt it again before sending. This approach greatly simplifies the process of analyzing and interacting with encrypted data, reducing the need for cumbersome work arounds or additional external tools.

    +

    While this tutorial covers a specific case of AES-256-CBC encryption, have in mind that the main concept and steps can be applied to various other encryption techniques as well. The only requirement is to understand the encryption process and be able to reproduce it in Python.

    +

    Scalpel is meant to be a versatile tool in scenarios where custom encryption is encountered. It aims to make data easier to analyze and modify for security testing purposes.

    + + +
    +
    + + + diff --git a/docs/public/tute-first-steps/index.html b/docs/public/tute-first-steps/index.html new file mode 100644 index 00000000..c32e34f4 --- /dev/null +++ b/docs/public/tute-first-steps/index.html @@ -0,0 +1,338 @@ + + + + + + + + + First steps + + + + + + + + + + + + +
    + +
    + + + + + Edit on GitHub + + + + +

    #  First Steps with Scalpel

    +

    #  Introduction

    +

    Welcome to your first steps with Scalpel! This beginner-friendly tutorial will walk you through basic steps to automatically and interactively modify HTTP headers using Scalpel. By the end of this tutorial, you’ll be able to edit the content of the User-Agent and Accept-Language headers using Scalpel’s hooks and custom editors.

    +

    #  Table of content

    +
      +
    1. Setting up Scalpel
    2. +
    3. Inspecting a GET request
    4. +
    5. Create a new script
    6. +
    7. Manipulating headers
    8. +
    9. Creating custom editors
    10. +
    11. Conclusion
    12. +
    +

    #  1. Setting up Scalpel

    +

    Before diving in, ensure Scalpel is installed. Once done, you should have a Scalpel tab within Burp Suite. +

    +
    +

    +

    #  2. Inspecting a GET request

    +

    Let’s start by inspecting a basic GET request. Open https://httpbin.org/get in your Burp suite’s browser. This site simply returns details of the requests it receives, making it perfect for this example case.

    +

    Then, get back to Burp Suite. The GET request should show in your HTTP history. +

    +
    +

    +

    Send it to Repeater using CTRL-R or right-click → Send to Repeater

    +

    #  3. Creating a new script

    +
      +
    1. +

      Select the Scalpel tab in the Burp GUI: +

      +
      +

      +
    2. +
    3. +

      Create a new script using the dedicated button: +

      +
      + +alt text

      +
    4. +
    5. +

      Name it appropriately: +

      +
      +

      +
    6. +
    7. +

      Open the new script in a text editor: +

      +
      + +
      +
      +

      +
      +

      💡 The commands ran when selecting a script or opening it can be configured in the Settings tab

      +
      +
    8. +
    +

    #  4. Manipulating headers

    +

    This step will focus on manipulating the User-Agent header of the GET request.

    +

    With Scalpel, this header can easily be changed to a custom value. Here’s how:

    +
    from pyscalpel import Request
    +
    +def request(req: Request) -> Request:
    +	user_agent = req.headers.get("User-Agent")
    +
    +	if user_agent:
    +	    req.headers["User-Agent"] = "My Custom User-Agent"
    +
    +	return req
    +
    +

    💡 The request() function modifies every requests going out of Burp.

    +

    This includes the requests from the proxy (browser) and the repeater.

    +
    +

    With the above code, every time you make a GET request, Scalpel will automatically change the User-Agent header to “My Custom User-Agentâ€.

    +

    To apply this effect:

    +
      +
    1. +

      Replace your script content with the snippet above.

      +
    2. +
    3. +

      Send the request to https://httpbin.org/get using Repeater.

      +
    4. +
    5. +

      You should see in the response that your User-Agent header was indeed replaced by My Custom User-Agent. +

      +
      +

      +
    6. +
    7. +

      The process for modifying a response is the same. Add this to your script:

      +
    8. +
    +
    from pyscalpel import Response
    +
    +def response(res: Response) -> Response:
    +	date = res.headers.get("Date")
    +
    +	if date:
    +		res.headers["Date"] = "My Custom Date"
    +
    +	return res
    +
      +
    1. The snippet above changed the Date header in response to My Custom Date. Send the request again and see the reflected changes: +
      +
      +
    2. +
    +

    You now know how to programmatically edit HTTP requests and responses.

    +

    Next, let’s see how to interactively edit parts of a request.

    +

    #  5. Creating custom editors

    +

    Custom editors in Scalpel allow you to interactively change specific parts of a request. Let’s create an editor to change the Accept-Language header manually:

    +
    def req_edit_in_accept_language(req: Request) -> bytes | None:
    +	return req.headers.get("Accept-Language", "").encode()
    +
    +def req_edit_out_accept_language(req: Request, edited_text: bytes) -> Request:
    +	req.headers["Accept-Language"] = edited_text.decode()
    +	return req
    +

    Thanks to these hooks, when you open a GET request in Burp Suite, you’ll see an additional Scalpel tab. This tab enables you to edit the Accept-Language header’s content directly. +

    +
    +

    +

    Once edited, Scalpel will replace the original Accept-Language value with your edited version. +

    +
    +

    +

    #  Conclusion

    +

    Congratulations! In this tutorial, you’ve taken your first steps with Scalpel. You’ve learned how to inspect GET requests, manipulate HTTP headers automatically, and create custom editors for interactive edits.

    +

    Remember, Scalpel is a powerful tool with a lot more capabilities. As you become more familiar with its features, you’ll discover its potential to significantly enhance your web security testing workflow.

    +
    +

    #  Further reading

    +

    Find example use-cases here.

    +

    Read the technical documentation.

    +

    See an advanced tutorial for a real use case in Decrypting custom encryption.

    + + +
    +
    + + + diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..ba116f1c --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +pdoc>=4.0.0 +requests +requests-toolbelt diff --git a/docs/scripts/api-render.py b/docs/scripts/api-render.py new file mode 100644 index 00000000..72f09f60 --- /dev/null +++ b/docs/scripts/api-render.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import os +import shutil +import textwrap +from pathlib import Path + +import pdoc.render_helpers + +here = Path(__file__).parent + +if os.environ.get("DOCS_ARCHIVE", False): + edit_url_map = {} +else: + edit_url_map = { + "scalpel": "https://REMOVED/scalpel", + } + +pdoc.render.configure( + template_directory=here / "pdoc-template", + edit_url_map=edit_url_map, +) + +# We can't configure Hugo, but we can configure pdoc. +pdoc.render_helpers.formatter.cssclass = "chroma pdoc-code" + +modules = [ + "pyscalpel.http", + "pyscalpel.http.body", + "pyscalpel.edit", + "pyscalpel.events", + "pyscalpel.venv", + "pyscalpel.utils", + "pyscalpel.encoding", + "pyscalpel.java", + "pyscalpel.java.burp", + here / ".." / "src" / "declarations" / "events.py", + here / ".." / "src" / "declarations" / "editors.py", +] + +pdoc.pdoc(*modules, output_directory=here / ".." / "src" / "generated" / "api") + +api_content = here / ".." / "src" / "content" / "api" +if api_content.exists(): + shutil.rmtree(api_content) + +api_content.mkdir() + +for weight, module in enumerate(modules): + if isinstance(module, Path): + continue + filename = f"api/{module.replace('.', '/')}.html" + (api_content / f"{module}.md").write_bytes( + textwrap.dedent( + f""" + --- + title: "{module}" + url: "{filename}" + + menu: + addons: + parent: 'Event Hooks & API' + weight: {weight + 1} + --- + + {{{{< readfile file="/generated/{filename}" >}}}} + """ + ).encode() + ) + +(here / ".." / "src" / "content" / "addons-api.md").touch() diff --git a/docs/scripts/pdoc-template/frame.html.jinja2 b/docs/scripts/pdoc-template/frame.html.jinja2 new file mode 100644 index 00000000..7b8a0823 --- /dev/null +++ b/docs/scripts/pdoc-template/frame.html.jinja2 @@ -0,0 +1,9 @@ +{% filter minify_css %} + {% block style %} + + + + {% endblock %} +{% endfilter %} +{% block content %}{% endblock %} + diff --git a/docs/src/.hugo_build.lock b/docs/src/.hugo_build.lock new file mode 100644 index 00000000..e69de29b diff --git a/docs/src/assets/badge.scss b/docs/src/assets/badge.scss new file mode 100644 index 00000000..8082f6c7 --- /dev/null +++ b/docs/src/assets/badge.scss @@ -0,0 +1,18 @@ +.badge { + color: #fff; + background-color: #6c757d; + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 1; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; + + // Empty badges collapse automatically + &:empty { + display: none; + } +} diff --git a/docs/src/assets/bulma/base/_all.sass b/docs/src/assets/bulma/base/_all.sass new file mode 100644 index 00000000..e913d6ba --- /dev/null +++ b/docs/src/assets/bulma/base/_all.sass @@ -0,0 +1,5 @@ +@charset "utf-8" + +@import "minireset.sass" +@import "generic.sass" +@import "helpers.sass" diff --git a/docs/src/assets/bulma/base/generic.sass b/docs/src/assets/bulma/base/generic.sass new file mode 100644 index 00000000..31eedfe9 --- /dev/null +++ b/docs/src/assets/bulma/base/generic.sass @@ -0,0 +1,142 @@ +$body-background-color: $scheme-main !default +$body-size: 16px !default +$body-min-width: 300px !default +$body-rendering: optimizeLegibility !default +$body-family: $family-primary !default +$body-overflow-x: hidden !default +$body-overflow-y: scroll !default + +$body-color: $text !default +$body-font-size: 1em !default +$body-weight: $weight-normal !default +$body-line-height: 1.5 !default + +$code-family: $family-code !default +$code-padding: 0.25em 0.5em 0.25em !default +$code-weight: normal !default +$code-size: 0.875em !default + +$small-font-size: 0.875em !default + +$hr-background-color: $background !default +$hr-height: 2px !default +$hr-margin: 1.5rem 0 !default + +$strong-color: $text-strong !default +$strong-weight: $weight-bold !default + +$pre-font-size: 0.875em !default +$pre-padding: 1.25rem 1.5rem !default +$pre-code-font-size: 1em !default + +html + background-color: $body-background-color + font-size: $body-size + -moz-osx-font-smoothing: grayscale + -webkit-font-smoothing: antialiased + min-width: $body-min-width + overflow-x: $body-overflow-x + overflow-y: $body-overflow-y + text-rendering: $body-rendering + text-size-adjust: 100% + +article, +aside, +figure, +footer, +header, +hgroup, +section + display: block + +body, +button, +input, +select, +textarea + font-family: $body-family + +code, +pre + -moz-osx-font-smoothing: auto + -webkit-font-smoothing: auto + font-family: $code-family + +body + color: $body-color + font-size: $body-font-size + font-weight: $body-weight + line-height: $body-line-height + +// Inline + +a + color: $link + cursor: pointer + text-decoration: none + strong + color: currentColor + &:hover + color: $link-hover + +code + background-color: $code-background + color: $code + font-size: $code-size + font-weight: $code-weight + padding: $code-padding + +hr + background-color: $hr-background-color + border: none + display: block + height: $hr-height + margin: $hr-margin + +img + height: auto + max-width: 100% + +input[type="checkbox"], +input[type="radio"] + vertical-align: baseline + +small + font-size: $small-font-size + +span + font-style: inherit + font-weight: inherit + +strong + color: $strong-color + font-weight: $strong-weight + +// Block + +fieldset + border: none + +pre + +overflow-touch + background-color: $pre-background + color: $pre + font-size: $pre-font-size + overflow-x: auto + padding: $pre-padding + white-space: pre + word-wrap: normal + code + background-color: transparent + color: currentColor + font-size: $pre-code-font-size + padding: 0 + +table + td, + th + vertical-align: top + &:not([align]) + text-align: left + th + color: $text-strong diff --git a/docs/src/assets/bulma/base/helpers.sass b/docs/src/assets/bulma/base/helpers.sass new file mode 100644 index 00000000..bbb489dd --- /dev/null +++ b/docs/src/assets/bulma/base/helpers.sass @@ -0,0 +1,281 @@ +// Float + +.is-clearfix + +clearfix + +.is-pulled-left + float: left !important + +.is-pulled-right + float: right !important + +// Overflow + +.is-clipped + overflow: hidden !important + +// Overlay + +.is-overlay + @extend %overlay + +// Typography + +=typography-size($target:'') + @each $size in $sizes + $i: index($sizes, $size) + .is-size-#{$i}#{if($target == '', '', '-' + $target)} + font-size: $size !important + ++typography-size() + ++mobile + +typography-size('mobile') + ++tablet + +typography-size('tablet') + ++touch + +typography-size('touch') + ++desktop + +typography-size('desktop') + ++widescreen + +typography-size('widescreen') + ++fullhd + +typography-size('fullhd') + +$alignments: ('centered': 'center', 'justified': 'justify', 'left': 'left', 'right': 'right') + +@each $alignment, $text-align in $alignments + .has-text-#{$alignment} + text-align: #{$text-align} !important + +@each $alignment, $text-align in $alignments + +mobile + .has-text-#{$alignment}-mobile + text-align: #{$text-align} !important + +tablet + .has-text-#{$alignment}-tablet + text-align: #{$text-align} !important + +tablet-only + .has-text-#{$alignment}-tablet-only + text-align: #{$text-align} !important + +touch + .has-text-#{$alignment}-touch + text-align: #{$text-align} !important + +desktop + .has-text-#{$alignment}-desktop + text-align: #{$text-align} !important + +desktop-only + .has-text-#{$alignment}-desktop-only + text-align: #{$text-align} !important + +widescreen + .has-text-#{$alignment}-widescreen + text-align: #{$text-align} !important + +widescreen-only + .has-text-#{$alignment}-widescreen-only + text-align: #{$text-align} !important + +fullhd + .has-text-#{$alignment}-fullhd + text-align: #{$text-align} !important + +.is-capitalized + text-transform: capitalize !important + +.is-lowercase + text-transform: lowercase !important + +.is-uppercase + text-transform: uppercase !important + +.is-italic + font-style: italic !important + +@each $name, $pair in $colors + $color: nth($pair, 1) + .has-text-#{$name} + color: $color !important + a.has-text-#{$name} + &:hover, + &:focus + color: darken($color, 10%) !important + .has-background-#{$name} + background-color: $color !important + +@each $name, $shade in $shades + .has-text-#{$name} + color: $shade !important + .has-background-#{$name} + background-color: $shade !important + +.has-text-weight-light + font-weight: $weight-light !important +.has-text-weight-normal + font-weight: $weight-normal !important +.has-text-weight-medium + font-weight: $weight-medium !important +.has-text-weight-semibold + font-weight: $weight-semibold !important +.has-text-weight-bold + font-weight: $weight-bold !important + +.is-family-primary + font-family: $family-primary !important + +.is-family-secondary + font-family: $family-secondary !important + +.is-family-sans-serif + font-family: $family-sans-serif !important + +.is-family-monospace + font-family: $family-monospace !important + +.is-family-code + font-family: $family-code !important + +// Visibility + +$displays: 'block' 'flex' 'inline' 'inline-block' 'inline-flex' + +@each $display in $displays + .is-#{$display} + display: #{$display} !important + +mobile + .is-#{$display}-mobile + display: #{$display} !important + +tablet + .is-#{$display}-tablet + display: #{$display} !important + +tablet-only + .is-#{$display}-tablet-only + display: #{$display} !important + +touch + .is-#{$display}-touch + display: #{$display} !important + +desktop + .is-#{$display}-desktop + display: #{$display} !important + +desktop-only + .is-#{$display}-desktop-only + display: #{$display} !important + +widescreen + .is-#{$display}-widescreen + display: #{$display} !important + +widescreen-only + .is-#{$display}-widescreen-only + display: #{$display} !important + +fullhd + .is-#{$display}-fullhd + display: #{$display} !important + +.is-hidden + display: none !important + +.is-sr-only + border: none !important + clip: rect(0, 0, 0, 0) !important + height: 0.01em !important + overflow: hidden !important + padding: 0 !important + position: absolute !important + white-space: nowrap !important + width: 0.01em !important + ++mobile + .is-hidden-mobile + display: none !important + ++tablet + .is-hidden-tablet + display: none !important + ++tablet-only + .is-hidden-tablet-only + display: none !important + ++touch + .is-hidden-touch + display: none !important + ++desktop + .is-hidden-desktop + display: none !important + ++desktop-only + .is-hidden-desktop-only + display: none !important + ++widescreen + .is-hidden-widescreen + display: none !important + ++widescreen-only + .is-hidden-widescreen-only + display: none !important + ++fullhd + .is-hidden-fullhd + display: none !important + +.is-invisible + visibility: hidden !important + ++mobile + .is-invisible-mobile + visibility: hidden !important + ++tablet + .is-invisible-tablet + visibility: hidden !important + ++tablet-only + .is-invisible-tablet-only + visibility: hidden !important + ++touch + .is-invisible-touch + visibility: hidden !important + ++desktop + .is-invisible-desktop + visibility: hidden !important + ++desktop-only + .is-invisible-desktop-only + visibility: hidden !important + ++widescreen + .is-invisible-widescreen + visibility: hidden !important + ++widescreen-only + .is-invisible-widescreen-only + visibility: hidden !important + ++fullhd + .is-invisible-fullhd + visibility: hidden !important + +// Other + +.is-marginless + margin: 0 !important + +.is-paddingless + padding: 0 !important + +.is-radiusless + border-radius: 0 !important + +.is-shadowless + box-shadow: none !important + +.is-unselectable + @extend %unselectable + +.is-relative + position: relative !important diff --git a/docs/src/assets/bulma/base/minireset.sass b/docs/src/assets/bulma/base/minireset.sass new file mode 100644 index 00000000..c5657ebd --- /dev/null +++ b/docs/src/assets/bulma/base/minireset.sass @@ -0,0 +1,79 @@ +/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */ +// Blocks +html, +body, +p, +ol, +ul, +li, +dl, +dt, +dd, +blockquote, +figure, +fieldset, +legend, +textarea, +pre, +iframe, +hr, +h1, +h2, +h3, +h4, +h5, +h6 + margin: 0 + padding: 0 + +// Headings +h1, +h2, +h3, +h4, +h5, +h6 + font-size: 100% + font-weight: normal + +// List +ul + list-style: none + +// Form +button, +input, +select, +textarea + margin: 0 + +// Box sizing +html + box-sizing: border-box + +* + &, + &::before, + &::after + box-sizing: inherit + +// Media +img, +video + height: auto + max-width: 100% + +// Iframe +iframe + border: 0 + +// Table +table + border-collapse: collapse + border-spacing: 0 + +td, +th + padding: 0 + &:not([align]) + text-align: left diff --git a/docs/src/assets/bulma/components/_all.sass b/docs/src/assets/bulma/components/_all.sass new file mode 100644 index 00000000..88fd45c5 --- /dev/null +++ b/docs/src/assets/bulma/components/_all.sass @@ -0,0 +1,15 @@ +@charset "utf-8" + +@import "breadcrumb.sass" +@import "card.sass" +@import "dropdown.sass" +@import "level.sass" +@import "list.sass" +@import "media.sass" +@import "menu.sass" +@import "message.sass" +@import "modal.sass" +@import "navbar.sass" +@import "pagination.sass" +@import "panel.sass" +@import "tabs.sass" diff --git a/docs/src/assets/bulma/components/breadcrumb.sass b/docs/src/assets/bulma/components/breadcrumb.sass new file mode 100644 index 00000000..3d7f4eb3 --- /dev/null +++ b/docs/src/assets/bulma/components/breadcrumb.sass @@ -0,0 +1,75 @@ +$breadcrumb-item-color: $link !default +$breadcrumb-item-hover-color: $link-hover !default +$breadcrumb-item-active-color: $text-strong !default + +$breadcrumb-item-padding-vertical: 0 !default +$breadcrumb-item-padding-horizontal: 0.75em !default + +$breadcrumb-item-separator-color: $border-hover !default + +.breadcrumb + @extend %block + @extend %unselectable + font-size: $size-normal + white-space: nowrap + a + align-items: center + color: $breadcrumb-item-color + display: flex + justify-content: center + padding: $breadcrumb-item-padding-vertical $breadcrumb-item-padding-horizontal + &:hover + color: $breadcrumb-item-hover-color + li + align-items: center + display: flex + &:first-child a + padding-left: 0 + &.is-active + a + color: $breadcrumb-item-active-color + cursor: default + pointer-events: none + & + li::before + color: $breadcrumb-item-separator-color + content: "\0002f" + ul, + ol + align-items: flex-start + display: flex + flex-wrap: wrap + justify-content: flex-start + .icon + &:first-child + margin-right: 0.5em + &:last-child + margin-left: 0.5em + // Alignment + &.is-centered + ol, + ul + justify-content: center + &.is-right + ol, + ul + justify-content: flex-end + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large + // Styles + &.has-arrow-separator + li + li::before + content: "\02192" + &.has-bullet-separator + li + li::before + content: "\02022" + &.has-dot-separator + li + li::before + content: "\000b7" + &.has-succeeds-separator + li + li::before + content: "\0227B" diff --git a/docs/src/assets/bulma/components/card.sass b/docs/src/assets/bulma/components/card.sass new file mode 100644 index 00000000..3cdf0008 --- /dev/null +++ b/docs/src/assets/bulma/components/card.sass @@ -0,0 +1,79 @@ +$card-color: $text !default +$card-background-color: $scheme-main !default +$card-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default + +$card-header-background-color: transparent !default +$card-header-color: $text-strong !default +$card-header-padding: 0.75rem 1rem !default +$card-header-shadow: 0 0.125em 0.25em rgba($scheme-invert, 0.1) !default +$card-header-weight: $weight-bold !default + +$card-content-background-color: transparent !default +$card-content-padding: 1.5rem !default + +$card-footer-background-color: transparent !default +$card-footer-border-top: 1px solid $border-light !default +$card-footer-padding: 0.75rem !default + +$card-media-margin: $block-spacing !default + +.card + background-color: $card-background-color + box-shadow: $card-shadow + color: $card-color + max-width: 100% + position: relative + +.card-header + background-color: $card-header-background-color + align-items: stretch + box-shadow: $card-header-shadow + display: flex + +.card-header-title + align-items: center + color: $card-header-color + display: flex + flex-grow: 1 + font-weight: $card-header-weight + padding: $card-header-padding + &.is-centered + justify-content: center + +.card-header-icon + align-items: center + cursor: pointer + display: flex + justify-content: center + padding: $card-header-padding + +.card-image + display: block + position: relative + +.card-content + background-color: $card-content-background-color + padding: $card-content-padding + +.card-footer + background-color: $card-footer-background-color + border-top: $card-footer-border-top + align-items: stretch + display: flex + +.card-footer-item + align-items: center + display: flex + flex-basis: 0 + flex-grow: 1 + flex-shrink: 0 + justify-content: center + padding: $card-footer-padding + &:not(:last-child) + border-right: $card-footer-border-top + +// Combinations + +.card + .media:not(:last-child) + margin-bottom: $card-media-margin diff --git a/docs/src/assets/bulma/components/dropdown.sass b/docs/src/assets/bulma/components/dropdown.sass new file mode 100644 index 00000000..d62a6d88 --- /dev/null +++ b/docs/src/assets/bulma/components/dropdown.sass @@ -0,0 +1,81 @@ +$dropdown-menu-min-width: 12rem !default + +$dropdown-content-background-color: $scheme-main !default +$dropdown-content-arrow: $link !default +$dropdown-content-offset: 4px !default +$dropdown-content-padding-bottom: 0.5rem !default +$dropdown-content-padding-top: 0.5rem !default +$dropdown-content-radius: $radius !default +$dropdown-content-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default +$dropdown-content-z: 20 !default + +$dropdown-item-color: $text !default +$dropdown-item-hover-color: $scheme-invert !default +$dropdown-item-hover-background-color: $background !default +$dropdown-item-active-color: $link-invert !default +$dropdown-item-active-background-color: $link !default + +$dropdown-divider-background-color: $border-light !default + +.dropdown + display: inline-flex + position: relative + vertical-align: top + &.is-active, + &.is-hoverable:hover + .dropdown-menu + display: block + &.is-right + .dropdown-menu + left: auto + right: 0 + &.is-up + .dropdown-menu + bottom: 100% + padding-bottom: $dropdown-content-offset + padding-top: initial + top: auto + +.dropdown-menu + display: none + left: 0 + min-width: $dropdown-menu-min-width + padding-top: $dropdown-content-offset + position: absolute + top: 100% + z-index: $dropdown-content-z + +.dropdown-content + background-color: $dropdown-content-background-color + border-radius: $dropdown-content-radius + box-shadow: $dropdown-content-shadow + padding-bottom: $dropdown-content-padding-bottom + padding-top: $dropdown-content-padding-top + +.dropdown-item + color: $dropdown-item-color + display: block + font-size: 0.875rem + line-height: 1.5 + padding: 0.375rem 1rem + position: relative + +a.dropdown-item, +button.dropdown-item + padding-right: 3rem + text-align: left + white-space: nowrap + width: 100% + &:hover + background-color: $dropdown-item-hover-background-color + color: $dropdown-item-hover-color + &.is-active + background-color: $dropdown-item-active-background-color + color: $dropdown-item-active-color + +.dropdown-divider + background-color: $dropdown-divider-background-color + border: none + display: block + height: 1px + margin: 0.5rem 0 diff --git a/docs/src/assets/bulma/components/level.sass b/docs/src/assets/bulma/components/level.sass new file mode 100644 index 00000000..608f291e --- /dev/null +++ b/docs/src/assets/bulma/components/level.sass @@ -0,0 +1,77 @@ +$level-item-spacing: ($block-spacing / 2) !default + +.level + @extend %block + align-items: center + justify-content: space-between + code + border-radius: $radius + img + display: inline-block + vertical-align: top + // Modifiers + &.is-mobile + display: flex + .level-left, + .level-right + display: flex + .level-left + .level-right + margin-top: 0 + .level-item + &:not(:last-child) + margin-bottom: 0 + margin-right: $level-item-spacing + &:not(.is-narrow) + flex-grow: 1 + // Responsiveness + +tablet + display: flex + & > .level-item + &:not(.is-narrow) + flex-grow: 1 + +.level-item + align-items: center + display: flex + flex-basis: auto + flex-grow: 0 + flex-shrink: 0 + justify-content: center + .title, + .subtitle + margin-bottom: 0 + // Responsiveness + +mobile + &:not(:last-child) + margin-bottom: $level-item-spacing + +.level-left, +.level-right + flex-basis: auto + flex-grow: 0 + flex-shrink: 0 + .level-item + // Modifiers + &.is-flexible + flex-grow: 1 + // Responsiveness + +tablet + &:not(:last-child) + margin-right: $level-item-spacing + +.level-left + align-items: center + justify-content: flex-start + // Responsiveness + +mobile + & + .level-right + margin-top: 1.5rem + +tablet + display: flex + +.level-right + align-items: center + justify-content: flex-end + // Responsiveness + +tablet + display: flex diff --git a/docs/src/assets/bulma/components/list.sass b/docs/src/assets/bulma/components/list.sass new file mode 100644 index 00000000..bc99428a --- /dev/null +++ b/docs/src/assets/bulma/components/list.sass @@ -0,0 +1,39 @@ +$list-background-color: $scheme-main !default +$list-shadow: 0 2px 3px rgba($scheme-invert, 0.1), 0 0 0 1px rgba($scheme-invert, 0.1) !default +$list-radius: $radius !default + +$list-item-border: 1px solid $border !default +$list-item-color: $text !default +$list-item-active-background-color: $link !default +$list-item-active-color: $link-invert !default +$list-item-hover-background-color: $background !default + +.list + @extend %block + background-color: $list-background-color + border-radius: $list-radius + box-shadow: $list-shadow + // &.is-hoverable > .list-item:hover:not(.is-active) + // background-color: $list-item-hover-background-color + // cursor: pointer + +.list-item + display: block + padding: 0.5em 1em + &:not(a) + color: $list-item-color + &:first-child + border-top-left-radius: $list-radius + border-top-right-radius: $list-radius + &:last-child + border-bottom-left-radius: $list-radius + border-bottom-right-radius: $list-radius + &:not(:last-child) + border-bottom: $list-item-border + &.is-active + background-color: $list-item-active-background-color + color: $list-item-active-color + +a.list-item + background-color: $list-item-hover-background-color + cursor: pointer diff --git a/docs/src/assets/bulma/components/media.sass b/docs/src/assets/bulma/components/media.sass new file mode 100644 index 00000000..a9ad114a --- /dev/null +++ b/docs/src/assets/bulma/components/media.sass @@ -0,0 +1,50 @@ +$media-border-color: bulmaRgba($border, 0.5) !default + +.media + align-items: flex-start + display: flex + text-align: left + .content:not(:last-child) + margin-bottom: 0.75rem + .media + border-top: 1px solid $media-border-color + display: flex + padding-top: 0.75rem + .content:not(:last-child), + .control:not(:last-child) + margin-bottom: 0.5rem + .media + padding-top: 0.5rem + & + .media + margin-top: 0.5rem + & + .media + border-top: 1px solid $media-border-color + margin-top: 1rem + padding-top: 1rem + // Sizes + &.is-large + & + .media + margin-top: 1.5rem + padding-top: 1.5rem + +.media-left, +.media-right + flex-basis: auto + flex-grow: 0 + flex-shrink: 0 + +.media-left + margin-right: 1rem + +.media-right + margin-left: 1rem + +.media-content + flex-basis: auto + flex-grow: 1 + flex-shrink: 1 + text-align: left + ++mobile + .media-content + overflow-x: auto diff --git a/docs/src/assets/bulma/components/menu.sass b/docs/src/assets/bulma/components/menu.sass new file mode 100644 index 00000000..3de7e18d --- /dev/null +++ b/docs/src/assets/bulma/components/menu.sass @@ -0,0 +1,57 @@ +$menu-item-color: $text !default +$menu-item-radius: $radius-small !default +$menu-item-hover-color: $text-strong !default +$menu-item-hover-background-color: $background !default +$menu-item-active-color: $link-invert !default +$menu-item-active-background-color: $link !default + +$menu-list-border-left: 1px solid $border !default +$menu-list-line-height: 1.25 !default +$menu-list-link-padding: 0.5em 0.75em !default +$menu-nested-list-margin: 0.75em !default +$menu-nested-list-padding-left: 0.75em !default + +$menu-label-color: $text-light !default +$menu-label-font-size: 0.75em !default +$menu-label-letter-spacing: 0.1em !default +$menu-label-spacing: 1em !default + +.menu + font-size: $size-normal + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large + +.menu-list + line-height: $menu-list-line-height + a + border-radius: $menu-item-radius + color: $menu-item-color + display: block + padding: $menu-list-link-padding + &:hover + background-color: $menu-item-hover-background-color + color: $menu-item-hover-color + // Modifiers + &.is-active + background-color: $menu-item-active-background-color + color: $menu-item-active-color + li + ul + border-left: $menu-list-border-left + margin: $menu-nested-list-margin + padding-left: $menu-nested-list-padding-left + +.menu-label + color: $menu-label-color + font-size: $menu-label-font-size + letter-spacing: $menu-label-letter-spacing + text-transform: uppercase + &:not(:first-child) + margin-top: $menu-label-spacing + &:not(:last-child) + margin-bottom: $menu-label-spacing diff --git a/docs/src/assets/bulma/components/message.sass b/docs/src/assets/bulma/components/message.sass new file mode 100644 index 00000000..89e4cc9a --- /dev/null +++ b/docs/src/assets/bulma/components/message.sass @@ -0,0 +1,99 @@ +$message-background-color: $background !default +$message-radius: $radius !default + +$message-header-background-color: $text !default +$message-header-color: $text-invert !default +$message-header-weight: $weight-bold !default +$message-header-padding: 0.75em 1em !default +$message-header-radius: $radius !default + +$message-body-border-color: $border !default +$message-body-border-width: 0 0 0 4px !default +$message-body-color: $text !default +$message-body-padding: 1.25em 1.5em !default +$message-body-radius: $radius !default + +$message-body-pre-background-color: $scheme-main !default +$message-body-pre-code-background-color: transparent !default + +$message-header-body-border-width: 0 !default +$message-colors: $colors !default + +.message + @extend %block + background-color: $message-background-color + border-radius: $message-radius + font-size: $size-normal + strong + color: currentColor + a:not(.button):not(.tag):not(.dropdown-item) + color: currentColor + text-decoration: underline + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large + // Colors + @each $name, $components in $message-colors + $color: nth($components, 1) + $color-invert: nth($components, 2) + $color-light: null + $color-dark: null + + @if length($components) >= 3 + $color-light: nth($components, 3) + @if length($components) >= 4 + $color-dark: nth($components, 4) + @else + $color-luminance: colorLuminance($color) + $darken-percentage: $color-luminance * 70% + $desaturate-percentage: $color-luminance * 30% + $color-dark: desaturate(darken($color, $darken-percentage), $desaturate-percentage) + @else + $color-lightning: max((100% - lightness($color)) - 2%, 0%) + $color-light: lighten($color, $color-lightning) + + &.is-#{$name} + background-color: $color-light + .message-header + background-color: $color + color: $color-invert + .message-body + border-color: $color + color: $color-dark + +.message-header + align-items: center + background-color: $message-header-background-color + border-radius: $message-header-radius $message-header-radius 0 0 + color: $message-header-color + display: flex + font-weight: $message-header-weight + justify-content: space-between + line-height: 1.25 + padding: $message-header-padding + position: relative + .delete + flex-grow: 0 + flex-shrink: 0 + margin-left: 0.75em + & + .message-body + border-width: $message-header-body-border-width + border-top-left-radius: 0 + border-top-right-radius: 0 + +.message-body + border-color: $message-body-border-color + border-radius: $message-body-radius + border-style: solid + border-width: $message-body-border-width + color: $message-body-color + padding: $message-body-padding + code, + pre + background-color: $message-body-pre-background-color + pre code + background-color: $message-body-pre-code-background-color diff --git a/docs/src/assets/bulma/components/modal.sass b/docs/src/assets/bulma/components/modal.sass new file mode 100644 index 00000000..377dfa78 --- /dev/null +++ b/docs/src/assets/bulma/components/modal.sass @@ -0,0 +1,113 @@ +$modal-z: 40 !default + +$modal-background-background-color: bulmaRgba($scheme-invert, 0.86) !default + +$modal-content-width: 640px !default +$modal-content-margin-mobile: 20px !default +$modal-content-spacing-mobile: 160px !default +$modal-content-spacing-tablet: 40px !default + +$modal-close-dimensions: 40px !default +$modal-close-right: 20px !default +$modal-close-top: 20px !default + +$modal-card-spacing: 40px !default + +$modal-card-head-background-color: $background !default +$modal-card-head-border-bottom: 1px solid $border !default +$modal-card-head-padding: 20px !default +$modal-card-head-radius: $radius-large !default + +$modal-card-title-color: $text-strong !default +$modal-card-title-line-height: 1 !default +$modal-card-title-size: $size-4 !default + +$modal-card-foot-radius: $radius-large !default +$modal-card-foot-border-top: 1px solid $border !default + +$modal-card-body-background-color: $scheme-main !default +$modal-card-body-padding: 20px !default + +.modal + @extend %overlay + align-items: center + display: none + flex-direction: column + justify-content: center + overflow: hidden + position: fixed + z-index: $modal-z + // Modifiers + &.is-active + display: flex + +.modal-background + @extend %overlay + background-color: $modal-background-background-color + +.modal-content, +.modal-card + margin: 0 $modal-content-margin-mobile + max-height: calc(100vh - #{$modal-content-spacing-mobile}) + overflow: auto + position: relative + width: 100% + // Responsiveness + +tablet + margin: 0 auto + max-height: calc(100vh - #{$modal-content-spacing-tablet}) + width: $modal-content-width + +.modal-close + @extend %delete + background: none + height: $modal-close-dimensions + position: fixed + right: $modal-close-right + top: $modal-close-top + width: $modal-close-dimensions + +.modal-card + display: flex + flex-direction: column + max-height: calc(100vh - #{$modal-card-spacing}) + overflow: hidden + -ms-overflow-y: visible + +.modal-card-head, +.modal-card-foot + align-items: center + background-color: $modal-card-head-background-color + display: flex + flex-shrink: 0 + justify-content: flex-start + padding: $modal-card-head-padding + position: relative + +.modal-card-head + border-bottom: $modal-card-head-border-bottom + border-top-left-radius: $modal-card-head-radius + border-top-right-radius: $modal-card-head-radius + +.modal-card-title + color: $modal-card-title-color + flex-grow: 1 + flex-shrink: 0 + font-size: $modal-card-title-size + line-height: $modal-card-title-line-height + +.modal-card-foot + border-bottom-left-radius: $modal-card-foot-radius + border-bottom-right-radius: $modal-card-foot-radius + border-top: $modal-card-foot-border-top + .button + &:not(:last-child) + margin-right: 0.5em + +.modal-card-body + +overflow-touch + background-color: $modal-card-body-background-color + flex-grow: 1 + flex-shrink: 1 + overflow: auto + padding: $modal-card-body-padding diff --git a/docs/src/assets/bulma/components/navbar.sass b/docs/src/assets/bulma/components/navbar.sass new file mode 100644 index 00000000..664558f7 --- /dev/null +++ b/docs/src/assets/bulma/components/navbar.sass @@ -0,0 +1,443 @@ +$navbar-background-color: $scheme-main !default +$navbar-box-shadow-size: 0 2px 0 0 !default +$navbar-box-shadow-color: $background !default +$navbar-height: 3.25rem !default +$navbar-padding-vertical: 1rem !default +$navbar-padding-horizontal: 2rem !default +$navbar-z: 30 !default +$navbar-fixed-z: 30 !default + +$navbar-item-color: $text !default +$navbar-item-hover-color: $link !default +$navbar-item-hover-background-color: $scheme-main-bis !default +$navbar-item-active-color: $scheme-invert !default +$navbar-item-active-background-color: transparent !default +$navbar-item-img-max-height: 1.75rem !default + +$navbar-burger-color: $navbar-item-color !default + +$navbar-tab-hover-background-color: transparent !default +$navbar-tab-hover-border-bottom-color: $link !default +$navbar-tab-active-color: $link !default +$navbar-tab-active-background-color: transparent !default +$navbar-tab-active-border-bottom-color: $link !default +$navbar-tab-active-border-bottom-style: solid !default +$navbar-tab-active-border-bottom-width: 3px !default + +$navbar-dropdown-background-color: $scheme-main !default +$navbar-dropdown-border-top: 2px solid $border !default +$navbar-dropdown-offset: -4px !default +$navbar-dropdown-arrow: $link !default +$navbar-dropdown-radius: $radius-large !default +$navbar-dropdown-z: 20 !default + +$navbar-dropdown-boxed-radius: $radius-large !default +$navbar-dropdown-boxed-shadow: 0 8px 8px rgba($scheme-invert, 0.1), 0 0 0 1px rgba($scheme-invert, 0.1) !default + +$navbar-dropdown-item-hover-color: $scheme-invert !default +$navbar-dropdown-item-hover-background-color: $background !default +$navbar-dropdown-item-active-color: $link !default +$navbar-dropdown-item-active-background-color: $background !default + +$navbar-divider-background-color: $background !default +$navbar-divider-height: 2px !default + +$navbar-bottom-box-shadow-size: 0 -2px 0 0 !default + +$navbar-breakpoint: $desktop !default + +=navbar-fixed + left: 0 + position: fixed + right: 0 + z-index: $navbar-fixed-z + +.navbar + background-color: $navbar-background-color + min-height: $navbar-height + position: relative + z-index: $navbar-z + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + background-color: $color + color: $color-invert + .navbar-brand + & > .navbar-item, + .navbar-link + color: $color-invert + & > a.navbar-item, + .navbar-link + &:focus, + &:hover, + &.is-active + background-color: darken($color, 5%) + color: $color-invert + .navbar-link + &::after + border-color: $color-invert + .navbar-burger + color: $color-invert + +from($navbar-breakpoint) + .navbar-start, + .navbar-end + & > .navbar-item, + .navbar-link + color: $color-invert + & > a.navbar-item, + .navbar-link + &:focus, + &:hover, + &.is-active + background-color: darken($color, 5%) + color: $color-invert + .navbar-link + &::after + border-color: $color-invert + .navbar-item.has-dropdown:focus .navbar-link, + .navbar-item.has-dropdown:hover .navbar-link, + .navbar-item.has-dropdown.is-active .navbar-link + background-color: darken($color, 5%) + color: $color-invert + .navbar-dropdown + a.navbar-item + &.is-active + background-color: $color + color: $color-invert + & > .container + align-items: stretch + display: flex + min-height: $navbar-height + width: 100% + &.has-shadow + box-shadow: $navbar-box-shadow-size $navbar-box-shadow-color + &.is-fixed-bottom, + &.is-fixed-top + +navbar-fixed + &.is-fixed-bottom + bottom: 0 + &.has-shadow + box-shadow: $navbar-bottom-box-shadow-size $navbar-box-shadow-color + &.is-fixed-top + top: 0 + +html, +body + &.has-navbar-fixed-top + padding-top: $navbar-height + &.has-navbar-fixed-bottom + padding-bottom: $navbar-height + +.navbar-brand, +.navbar-tabs + align-items: stretch + display: flex + flex-shrink: 0 + min-height: $navbar-height + +.navbar-brand + a.navbar-item + &:focus, + &:hover + background-color: transparent + +.navbar-tabs + +overflow-touch + max-width: 100vw + overflow-x: auto + overflow-y: hidden + +.navbar-burger + color: $navbar-burger-color + +hamburger($navbar-height) + margin-left: auto + +.navbar-menu + display: none + +.navbar-item, +.navbar-link + color: $navbar-item-color + display: block + line-height: 1.5 + padding: 0.5rem 0.75rem + position: relative + .icon + &:only-child + margin-left: -0.25rem + margin-right: -0.25rem + +a.navbar-item, +.navbar-link + cursor: pointer + &:focus, + &:focus-within, + &:hover, + &.is-active + background-color: $navbar-item-hover-background-color + color: $navbar-item-hover-color + +.navbar-item + display: block + flex-grow: 0 + flex-shrink: 0 + img + max-height: $navbar-item-img-max-height + &.has-dropdown + padding: 0 + &.is-expanded + flex-grow: 1 + flex-shrink: 1 + &.is-tab + border-bottom: 1px solid transparent + min-height: $navbar-height + padding-bottom: calc(0.5rem - 1px) + &:focus, + &:hover + background-color: $navbar-tab-hover-background-color + border-bottom-color: $navbar-tab-hover-border-bottom-color + &.is-active + background-color: $navbar-tab-active-background-color + border-bottom-color: $navbar-tab-active-border-bottom-color + border-bottom-style: $navbar-tab-active-border-bottom-style + border-bottom-width: $navbar-tab-active-border-bottom-width + color: $navbar-tab-active-color + padding-bottom: calc(0.5rem - #{$navbar-tab-active-border-bottom-width}) + +.navbar-content + flex-grow: 1 + flex-shrink: 1 + +.navbar-link:not(.is-arrowless) + padding-right: 2.5em + &::after + @extend %arrow + border-color: $navbar-dropdown-arrow + margin-top: -0.375em + right: 1.125em + +.navbar-dropdown + font-size: 0.875rem + padding-bottom: 0.5rem + padding-top: 0.5rem + .navbar-item + padding-left: 1.5rem + padding-right: 1.5rem + +.navbar-divider + background-color: $navbar-divider-background-color + border: none + display: none + height: $navbar-divider-height + margin: 0.5rem 0 + ++until($navbar-breakpoint) + .navbar > .container + display: block + .navbar-brand, + .navbar-tabs + .navbar-item + align-items: center + display: flex + .navbar-link + &::after + display: none + .navbar-menu + background-color: $navbar-background-color + box-shadow: 0 8px 16px rgba($scheme-invert, 0.1) + padding: 0.5rem 0 + &.is-active + display: block + // Fixed navbar + .navbar + &.is-fixed-bottom-touch, + &.is-fixed-top-touch + +navbar-fixed + &.is-fixed-bottom-touch + bottom: 0 + &.has-shadow + box-shadow: 0 -2px 3px rgba($scheme-invert, 0.1) + &.is-fixed-top-touch + top: 0 + &.is-fixed-top, + &.is-fixed-top-touch + .navbar-menu + +overflow-touch + max-height: calc(100vh - #{$navbar-height}) + overflow: auto + html, + body + &.has-navbar-fixed-top-touch + padding-top: $navbar-height + &.has-navbar-fixed-bottom-touch + padding-bottom: $navbar-height + ++from($navbar-breakpoint) + .navbar, + .navbar-menu, + .navbar-start, + .navbar-end + align-items: stretch + display: flex + .navbar + min-height: $navbar-height + &.is-spaced + padding: $navbar-padding-vertical $navbar-padding-horizontal + .navbar-start, + .navbar-end + align-items: center + a.navbar-item, + .navbar-link + border-radius: $radius + &.is-transparent + a.navbar-item, + .navbar-link + &:focus, + &:hover, + &.is-active + background-color: transparent !important + .navbar-item.has-dropdown + &.is-active, + &.is-hoverable:focus, + &.is-hoverable:focus-within, + &.is-hoverable:hover + .navbar-link + background-color: transparent !important + .navbar-dropdown + a.navbar-item + &:focus, + &:hover + background-color: $navbar-dropdown-item-hover-background-color + color: $navbar-dropdown-item-hover-color + &.is-active + background-color: $navbar-dropdown-item-active-background-color + color: $navbar-dropdown-item-active-color + .navbar-burger + display: none + .navbar-item, + .navbar-link + align-items: center + display: flex + .navbar-item + display: flex + &.has-dropdown + align-items: stretch + &.has-dropdown-up + .navbar-link::after + transform: rotate(135deg) translate(0.25em, -0.25em) + .navbar-dropdown + border-bottom: $navbar-dropdown-border-top + border-radius: $navbar-dropdown-radius $navbar-dropdown-radius 0 0 + border-top: none + bottom: 100% + box-shadow: 0 -8px 8px rgba($scheme-invert, 0.1) + top: auto + &.is-active, + &.is-hoverable:focus, + &.is-hoverable:focus-within, + &.is-hoverable:hover + .navbar-dropdown + display: block + .navbar.is-spaced &, + &.is-boxed + opacity: 1 + pointer-events: auto + transform: translateY(0) + .navbar-menu + flex-grow: 1 + flex-shrink: 0 + .navbar-start + justify-content: flex-start + margin-right: auto + .navbar-end + justify-content: flex-end + margin-left: auto + .navbar-dropdown + background-color: $navbar-dropdown-background-color + border-bottom-left-radius: $navbar-dropdown-radius + border-bottom-right-radius: $navbar-dropdown-radius + border-top: $navbar-dropdown-border-top + box-shadow: 0 8px 8px rgba($scheme-invert, 0.1) + display: none + font-size: 0.875rem + left: 0 + min-width: 100% + position: absolute + top: 100% + z-index: $navbar-dropdown-z + .navbar-item + padding: 0.375rem 1rem + white-space: nowrap + a.navbar-item + padding-right: 3rem + &:focus, + &:hover + background-color: $navbar-dropdown-item-hover-background-color + color: $navbar-dropdown-item-hover-color + &.is-active + background-color: $navbar-dropdown-item-active-background-color + color: $navbar-dropdown-item-active-color + .navbar.is-spaced &, + &.is-boxed + border-radius: $navbar-dropdown-boxed-radius + border-top: none + box-shadow: $navbar-dropdown-boxed-shadow + display: block + opacity: 0 + pointer-events: none + top: calc(100% + (#{$navbar-dropdown-offset})) + transform: translateY(-5px) + transition-duration: $speed + transition-property: opacity, transform + &.is-right + left: auto + right: 0 + .navbar-divider + display: block + .navbar > .container, + .container > .navbar + .navbar-brand + margin-left: -.75rem + .navbar-menu + margin-right: -.75rem + // Fixed navbar + .navbar + &.is-fixed-bottom-desktop, + &.is-fixed-top-desktop + +navbar-fixed + &.is-fixed-bottom-desktop + bottom: 0 + &.has-shadow + box-shadow: 0 -2px 3px rgba($scheme-invert, 0.1) + &.is-fixed-top-desktop + top: 0 + html, + body + &.has-navbar-fixed-top-desktop + padding-top: $navbar-height + &.has-navbar-fixed-bottom-desktop + padding-bottom: $navbar-height + &.has-spaced-navbar-fixed-top + padding-top: $navbar-height + ($navbar-padding-vertical * 2) + &.has-spaced-navbar-fixed-bottom + padding-bottom: $navbar-height + ($navbar-padding-vertical * 2) + // Hover/Active states + a.navbar-item, + .navbar-link + &.is-active + color: $navbar-item-active-color + &.is-active:not(:focus):not(:hover) + background-color: $navbar-item-active-background-color + .navbar-item.has-dropdown + &:focus, + &:hover, + &.is-active + .navbar-link + background-color: $navbar-item-hover-background-color + +// Combination + +.hero + &.is-fullheight-with-navbar + min-height: calc(100vh - #{$navbar-height}) diff --git a/docs/src/assets/bulma/components/pagination.sass b/docs/src/assets/bulma/components/pagination.sass new file mode 100644 index 00000000..822c2e81 --- /dev/null +++ b/docs/src/assets/bulma/components/pagination.sass @@ -0,0 +1,150 @@ +$pagination-color: $text-strong !default +$pagination-border-color: $border !default +$pagination-margin: -0.25rem !default +$pagination-min-width: $control-height !default + +$pagination-item-font-size: 1em !default +$pagination-item-margin: 0.25rem !default +$pagination-item-padding-left: 0.5em !default +$pagination-item-padding-right: 0.5em !default + +$pagination-hover-color: $link-hover !default +$pagination-hover-border-color: $link-hover-border !default + +$pagination-focus-color: $link-focus !default +$pagination-focus-border-color: $link-focus-border !default + +$pagination-active-color: $link-active !default +$pagination-active-border-color: $link-active-border !default + +$pagination-disabled-color: $text-light !default +$pagination-disabled-background-color: $border !default +$pagination-disabled-border-color: $border !default + +$pagination-current-color: $link-invert !default +$pagination-current-background-color: $link !default +$pagination-current-border-color: $link !default + +$pagination-ellipsis-color: $grey-light !default + +$pagination-shadow-inset: inset 0 1px 2px rgba($scheme-invert, 0.2) + +.pagination + @extend %block + font-size: $size-normal + margin: $pagination-margin + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large + &.is-rounded + .pagination-previous, + .pagination-next + padding-left: 1em + padding-right: 1em + border-radius: $radius-rounded + .pagination-link + border-radius: $radius-rounded + +.pagination, +.pagination-list + align-items: center + display: flex + justify-content: center + text-align: center + +.pagination-previous, +.pagination-next, +.pagination-link, +.pagination-ellipsis + @extend %control + @extend %unselectable + font-size: $pagination-item-font-size + justify-content: center + margin: $pagination-item-margin + padding-left: $pagination-item-padding-left + padding-right: $pagination-item-padding-right + text-align: center + +.pagination-previous, +.pagination-next, +.pagination-link + border-color: $pagination-border-color + color: $pagination-color + min-width: $pagination-min-width + &:hover + border-color: $pagination-hover-border-color + color: $pagination-hover-color + &:focus + border-color: $pagination-focus-border-color + &:active + box-shadow: $pagination-shadow-inset + &[disabled] + background-color: $pagination-disabled-background-color + border-color: $pagination-disabled-border-color + box-shadow: none + color: $pagination-disabled-color + opacity: 0.5 + +.pagination-previous, +.pagination-next + padding-left: 0.75em + padding-right: 0.75em + white-space: nowrap + +.pagination-link + &.is-current + background-color: $pagination-current-background-color + border-color: $pagination-current-border-color + color: $pagination-current-color + +.pagination-ellipsis + color: $pagination-ellipsis-color + pointer-events: none + +.pagination-list + flex-wrap: wrap + ++mobile + .pagination + flex-wrap: wrap + .pagination-previous, + .pagination-next + flex-grow: 1 + flex-shrink: 1 + .pagination-list + li + flex-grow: 1 + flex-shrink: 1 + ++tablet + .pagination-list + flex-grow: 1 + flex-shrink: 1 + justify-content: flex-start + order: 1 + .pagination-previous + order: 2 + .pagination-next + order: 3 + .pagination + justify-content: space-between + &.is-centered + .pagination-previous + order: 1 + .pagination-list + justify-content: center + order: 2 + .pagination-next + order: 3 + &.is-right + .pagination-previous + order: 1 + .pagination-next + order: 2 + .pagination-list + justify-content: flex-end + order: 3 diff --git a/docs/src/assets/bulma/components/panel.sass b/docs/src/assets/bulma/components/panel.sass new file mode 100644 index 00000000..c7b14877 --- /dev/null +++ b/docs/src/assets/bulma/components/panel.sass @@ -0,0 +1,119 @@ +$panel-margin: $block-spacing !default +$panel-item-border: 1px solid $border-light !default +$panel-radius: $radius-large !default +$panel-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default + +$panel-heading-background-color: $border-light !default +$panel-heading-color: $text-strong !default +$panel-heading-line-height: 1.25 !default +$panel-heading-padding: 0.75em 1em !default +$panel-heading-radius: $radius !default +$panel-heading-size: 1.25em !default +$panel-heading-weight: $weight-bold !default + +$panel-tabs-font-size: 0.875em !default +$panel-tab-border-bottom: 1px solid $border !default +$panel-tab-active-border-bottom-color: $link-active-border !default +$panel-tab-active-color: $link-active !default + +$panel-list-item-color: $text !default +$panel-list-item-hover-color: $link !default + +$panel-block-color: $text-strong !default +$panel-block-hover-background-color: $background !default +$panel-block-active-border-left-color: $link !default +$panel-block-active-color: $link-active !default +$panel-block-active-icon-color: $link !default + +$panel-icon-color: $text-light !default +$panel-colors: $colors !default + +.panel + border-radius: $panel-radius + box-shadow: $panel-shadow + font-size: $size-normal + &:not(:last-child) + margin-bottom: $panel-margin + // Colors + @each $name, $components in $panel-colors + $color: nth($components, 1) + $color-invert: nth($components, 2) + &.is-#{$name} + .panel-heading + background-color: $color + color: $color-invert + .panel-tabs a.is-active + border-bottom-color: $color + .panel-block.is-active .panel-icon + color: $color + +.panel-tabs, +.panel-block + &:not(:last-child) + border-bottom: $panel-item-border + +.panel-heading + background-color: $panel-heading-background-color + border-radius: $panel-radius $panel-radius 0 0 + color: $panel-heading-color + font-size: $panel-heading-size + font-weight: $panel-heading-weight + line-height: $panel-heading-line-height + padding: $panel-heading-padding + +.panel-tabs + align-items: flex-end + display: flex + font-size: $panel-tabs-font-size + justify-content: center + a + border-bottom: $panel-tab-border-bottom + margin-bottom: -1px + padding: 0.5em + // Modifiers + &.is-active + border-bottom-color: $panel-tab-active-border-bottom-color + color: $panel-tab-active-color + +.panel-list + a + color: $panel-list-item-color + &:hover + color: $panel-list-item-hover-color + +.panel-block + align-items: center + color: $panel-block-color + display: flex + justify-content: flex-start + padding: 0.5em 0.75em + input[type="checkbox"] + margin-right: 0.75em + & > .control + flex-grow: 1 + flex-shrink: 1 + width: 100% + &.is-wrapped + flex-wrap: wrap + &.is-active + border-left-color: $panel-block-active-border-left-color + color: $panel-block-active-color + .panel-icon + color: $panel-block-active-icon-color + &:last-child + border-bottom-left-radius: $panel-radius + border-bottom-right-radius: $panel-radius + +a.panel-block, +label.panel-block + cursor: pointer + &:hover + background-color: $panel-block-hover-background-color + +.panel-icon + +fa(14px, 1em) + color: $panel-icon-color + margin-right: 0.75em + .fa + font-size: inherit + line-height: inherit diff --git a/docs/src/assets/bulma/components/tabs.sass b/docs/src/assets/bulma/components/tabs.sass new file mode 100644 index 00000000..8c28c257 --- /dev/null +++ b/docs/src/assets/bulma/components/tabs.sass @@ -0,0 +1,151 @@ +$tabs-border-bottom-color: $border !default +$tabs-border-bottom-style: solid !default +$tabs-border-bottom-width: 1px !default +$tabs-link-color: $text !default +$tabs-link-hover-border-bottom-color: $text-strong !default +$tabs-link-hover-color: $text-strong !default +$tabs-link-active-border-bottom-color: $link !default +$tabs-link-active-color: $link !default +$tabs-link-padding: 0.5em 1em !default + +$tabs-boxed-link-radius: $radius !default +$tabs-boxed-link-hover-background-color: $background !default +$tabs-boxed-link-hover-border-bottom-color: $border !default + +$tabs-boxed-link-active-background-color: $scheme-main !default +$tabs-boxed-link-active-border-color: $border !default +$tabs-boxed-link-active-border-bottom-color: transparent !default + +$tabs-toggle-link-border-color: $border !default +$tabs-toggle-link-border-style: solid !default +$tabs-toggle-link-border-width: 1px !default +$tabs-toggle-link-hover-background-color: $background !default +$tabs-toggle-link-hover-border-color: $border-hover !default +$tabs-toggle-link-radius: $radius !default +$tabs-toggle-link-active-background-color: $link !default +$tabs-toggle-link-active-border-color: $link !default +$tabs-toggle-link-active-color: $link-invert !default + +.tabs + @extend %block + +overflow-touch + @extend %unselectable + align-items: stretch + display: flex + font-size: $size-normal + justify-content: space-between + overflow: hidden + overflow-x: auto + white-space: nowrap + a + align-items: center + border-bottom-color: $tabs-border-bottom-color + border-bottom-style: $tabs-border-bottom-style + border-bottom-width: $tabs-border-bottom-width + color: $tabs-link-color + display: flex + justify-content: center + margin-bottom: -#{$tabs-border-bottom-width} + padding: $tabs-link-padding + vertical-align: top + &:hover + border-bottom-color: $tabs-link-hover-border-bottom-color + color: $tabs-link-hover-color + li + display: block + &.is-active + a + border-bottom-color: $tabs-link-active-border-bottom-color + color: $tabs-link-active-color + ul + align-items: center + border-bottom-color: $tabs-border-bottom-color + border-bottom-style: $tabs-border-bottom-style + border-bottom-width: $tabs-border-bottom-width + display: flex + flex-grow: 1 + flex-shrink: 0 + justify-content: flex-start + &.is-left + padding-right: 0.75em + &.is-center + flex: none + justify-content: center + padding-left: 0.75em + padding-right: 0.75em + &.is-right + justify-content: flex-end + padding-left: 0.75em + .icon + &:first-child + margin-right: 0.5em + &:last-child + margin-left: 0.5em + // Alignment + &.is-centered + ul + justify-content: center + &.is-right + ul + justify-content: flex-end + // Styles + &.is-boxed + a + border: 1px solid transparent + border-radius: $tabs-boxed-link-radius $tabs-boxed-link-radius 0 0 + &:hover + background-color: $tabs-boxed-link-hover-background-color + border-bottom-color: $tabs-boxed-link-hover-border-bottom-color + li + &.is-active + a + background-color: $tabs-boxed-link-active-background-color + border-color: $tabs-boxed-link-active-border-color + border-bottom-color: $tabs-boxed-link-active-border-bottom-color !important + &.is-fullwidth + li + flex-grow: 1 + flex-shrink: 0 + &.is-toggle + a + border-color: $tabs-toggle-link-border-color + border-style: $tabs-toggle-link-border-style + border-width: $tabs-toggle-link-border-width + margin-bottom: 0 + position: relative + &:hover + background-color: $tabs-toggle-link-hover-background-color + border-color: $tabs-toggle-link-hover-border-color + z-index: 2 + li + & + li + margin-left: -#{$tabs-toggle-link-border-width} + &:first-child a + border-radius: $tabs-toggle-link-radius 0 0 $tabs-toggle-link-radius + &:last-child a + border-radius: 0 $tabs-toggle-link-radius $tabs-toggle-link-radius 0 + &.is-active + a + background-color: $tabs-toggle-link-active-background-color + border-color: $tabs-toggle-link-active-border-color + color: $tabs-toggle-link-active-color + z-index: 1 + ul + border-bottom: none + &.is-toggle-rounded + li + &:first-child a + border-bottom-left-radius: $radius-rounded + border-top-left-radius: $radius-rounded + padding-left: 1.25em + &:last-child a + border-bottom-right-radius: $radius-rounded + border-top-right-radius: $radius-rounded + padding-right: 1.25em + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large diff --git a/docs/src/assets/bulma/elements/_all.sass b/docs/src/assets/bulma/elements/_all.sass new file mode 100644 index 00000000..7490c00d --- /dev/null +++ b/docs/src/assets/bulma/elements/_all.sass @@ -0,0 +1,15 @@ +@charset "utf-8" + +@import "box.sass" +@import "button.sass" +@import "container.sass" +@import "content.sass" +@import "icon.sass" +@import "image.sass" +@import "notification.sass" +@import "progress.sass" +@import "table.sass" +@import "tag.sass" +@import "title.sass" + +@import "other.sass" diff --git a/docs/src/assets/bulma/elements/box.sass b/docs/src/assets/bulma/elements/box.sass new file mode 100644 index 00000000..2fd18d49 --- /dev/null +++ b/docs/src/assets/bulma/elements/box.sass @@ -0,0 +1,24 @@ +$box-color: $text !default +$box-background-color: $scheme-main !default +$box-radius: $radius-large !default +$box-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default +$box-padding: 1.25rem !default + +$box-link-hover-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0 0 1px $link !default +$box-link-active-shadow: inset 0 1px 2px rgba($scheme-invert, 0.2), 0 0 0 1px $link !default + +.box + @extend %block + background-color: $box-background-color + border-radius: $box-radius + box-shadow: $box-shadow + color: $box-color + display: block + padding: $box-padding + +a.box + &:hover, + &:focus + box-shadow: $box-link-hover-shadow + &:active + box-shadow: $box-link-active-shadow diff --git a/docs/src/assets/bulma/elements/button.sass b/docs/src/assets/bulma/elements/button.sass new file mode 100644 index 00000000..df5417c8 --- /dev/null +++ b/docs/src/assets/bulma/elements/button.sass @@ -0,0 +1,323 @@ +$button-color: $text-strong !default +$button-background-color: $scheme-main !default +$button-family: false !default + +$button-border-color: $border !default +$button-border-width: $control-border-width !default + +$button-padding-vertical: calc(0.5em - #{$button-border-width}) !default +$button-padding-horizontal: 1em !default + +$button-hover-color: $link-hover !default +$button-hover-border-color: $link-hover-border !default + +$button-focus-color: $link-focus !default +$button-focus-border-color: $link-focus-border !default +$button-focus-box-shadow-size: 0 0 0 0.125em !default +$button-focus-box-shadow-color: bulmaRgba($link, 0.25) !default + +$button-active-color: $link-active !default +$button-active-border-color: $link-active-border !default + +$button-text-color: $text !default +$button-text-decoration: underline !default +$button-text-hover-background-color: $background !default +$button-text-hover-color: $text-strong !default + +$button-disabled-background-color: $scheme-main !default +$button-disabled-border-color: $border !default +$button-disabled-shadow: none !default +$button-disabled-opacity: 0.5 !default + +$button-static-color: $text-light !default +$button-static-background-color: $scheme-main-ter !default +$button-static-border-color: $border !default + +// The button sizes use mixins so they can be used at different breakpoints +=button-small + border-radius: $radius-small + font-size: $size-small +=button-normal + font-size: $size-normal +=button-medium + font-size: $size-medium +=button-large + font-size: $size-large + +.button + @extend %control + @extend %unselectable + background-color: $button-background-color + border-color: $button-border-color + border-width: $button-border-width + color: $button-color + cursor: pointer + @if $button-family + font-family: $button-family + justify-content: center + padding-bottom: $button-padding-vertical + padding-left: $button-padding-horizontal + padding-right: $button-padding-horizontal + padding-top: $button-padding-vertical + text-align: center + white-space: nowrap + strong + color: inherit + .icon + &, + &.is-small, + &.is-medium, + &.is-large + height: 1.5em + width: 1.5em + &:first-child:not(:last-child) + margin-left: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}) + margin-right: $button-padding-horizontal / 4 + &:last-child:not(:first-child) + margin-left: $button-padding-horizontal / 4 + margin-right: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}) + &:first-child:last-child + margin-left: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}) + margin-right: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}) + // States + &:hover, + &.is-hovered + border-color: $button-hover-border-color + color: $button-hover-color + &:focus, + &.is-focused + border-color: $button-focus-border-color + color: $button-focus-color + &:not(:active) + box-shadow: $button-focus-box-shadow-size $button-focus-box-shadow-color + &:active, + &.is-active + border-color: $button-active-border-color + color: $button-active-color + // Colors + &.is-text + background-color: transparent + border-color: transparent + color: $button-text-color + text-decoration: $button-text-decoration + &:hover, + &.is-hovered, + &:focus, + &.is-focused + background-color: $button-text-hover-background-color + color: $button-text-hover-color + &:active, + &.is-active + background-color: darken($button-text-hover-background-color, 5%) + color: $button-text-hover-color + &[disabled], + fieldset[disabled] & + background-color: transparent + border-color: transparent + box-shadow: none + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + background-color: $color + border-color: transparent + color: $color-invert + &:hover, + &.is-hovered + background-color: darken($color, 2.5%) + border-color: transparent + color: $color-invert + &:focus, + &.is-focused + border-color: transparent + color: $color-invert + &:not(:active) + box-shadow: $button-focus-box-shadow-size rgba($color, 0.25) + &:active, + &.is-active + background-color: darken($color, 5%) + border-color: transparent + color: $color-invert + &[disabled], + fieldset[disabled] & + background-color: $color + border-color: transparent + box-shadow: none + &.is-inverted + background-color: $color-invert + color: $color + &:hover, + &.is-hovered + background-color: darken($color-invert, 5%) + &[disabled], + fieldset[disabled] & + background-color: $color-invert + border-color: transparent + box-shadow: none + color: $color + &.is-loading + &::after + border-color: transparent transparent $color-invert $color-invert !important + &.is-outlined + background-color: transparent + border-color: $color + color: $color + &:hover, + &.is-hovered, + &:focus, + &.is-focused + background-color: $color + border-color: $color + color: $color-invert + &.is-loading + &::after + border-color: transparent transparent $color $color !important + &:hover, + &.is-hovered, + &:focus, + &.is-focused + &::after + border-color: transparent transparent $color-invert $color-invert !important + &[disabled], + fieldset[disabled] & + background-color: transparent + border-color: $color + box-shadow: none + color: $color + &.is-inverted.is-outlined + background-color: transparent + border-color: $color-invert + color: $color-invert + &:hover, + &.is-hovered, + &:focus, + &.is-focused + background-color: $color-invert + color: $color + &.is-loading + &:hover, + &.is-hovered, + &:focus, + &.is-focused + &::after + border-color: transparent transparent $color $color !important + &[disabled], + fieldset[disabled] & + background-color: transparent + border-color: $color-invert + box-shadow: none + color: $color-invert + // If light and dark colors are provided + @if length($pair) >= 4 + $color-light: nth($pair, 3) + $color-dark: nth($pair, 4) + &.is-light + background-color: $color-light + color: $color-dark + &:hover, + &.is-hovered + background-color: darken($color-light, 2.5%) + border-color: transparent + color: $color-dark + &:active, + &.is-active + background-color: darken($color-light, 5%) + border-color: transparent + color: $color-dark + // Sizes + &.is-small + +button-small + &.is-normal + +button-normal + &.is-medium + +button-medium + &.is-large + +button-large + // Modifiers + &[disabled], + fieldset[disabled] & + background-color: $button-disabled-background-color + border-color: $button-disabled-border-color + box-shadow: $button-disabled-shadow + opacity: $button-disabled-opacity + &.is-fullwidth + display: flex + width: 100% + &.is-loading + color: transparent !important + pointer-events: none + &::after + @extend %loader + +center(1em) + position: absolute !important + &.is-static + background-color: $button-static-background-color + border-color: $button-static-border-color + color: $button-static-color + box-shadow: none + pointer-events: none + &.is-rounded + border-radius: $radius-rounded + padding-left: calc(#{$button-padding-horizontal} + 0.25em) + padding-right: calc(#{$button-padding-horizontal} + 0.25em) + +.buttons + align-items: center + display: flex + flex-wrap: wrap + justify-content: flex-start + .button + margin-bottom: 0.5rem + &:not(:last-child):not(.is-fullwidth) + margin-right: 0.5rem + &:last-child + margin-bottom: -0.5rem + &:not(:last-child) + margin-bottom: 1rem + // Sizes + &.are-small + .button:not(.is-normal):not(.is-medium):not(.is-large) + +button-small + &.are-medium + .button:not(.is-small):not(.is-normal):not(.is-large) + +button-medium + &.are-large + .button:not(.is-small):not(.is-normal):not(.is-medium) + +button-large + &.has-addons + .button + &:not(:first-child) + border-bottom-left-radius: 0 + border-top-left-radius: 0 + &:not(:last-child) + border-bottom-right-radius: 0 + border-top-right-radius: 0 + margin-right: -1px + &:last-child + margin-right: 0 + &:hover, + &.is-hovered + z-index: 2 + &:focus, + &.is-focused, + &:active, + &.is-active, + &.is-selected + z-index: 3 + &:hover + z-index: 4 + &.is-expanded + flex-grow: 1 + flex-shrink: 1 + &.is-centered + justify-content: center + &:not(.has-addons) + .button:not(.is-fullwidth) + margin-left: 0.25rem + margin-right: 0.25rem + &.is-right + justify-content: flex-end + &:not(.has-addons) + .button:not(.is-fullwidth) + margin-left: 0.25rem + margin-right: 0.25rem diff --git a/docs/src/assets/bulma/elements/container.sass b/docs/src/assets/bulma/elements/container.sass new file mode 100644 index 00000000..d88eb94a --- /dev/null +++ b/docs/src/assets/bulma/elements/container.sass @@ -0,0 +1,24 @@ +$container-offset: (2 * $gap) !default + +.container + flex-grow: 1 + margin: 0 auto + position: relative + width: auto + &.is-fluid + max-width: none + padding-left: $gap + padding-right: $gap + width: 100% + +desktop + max-width: $desktop - $container-offset + +until-widescreen + &.is-widescreen + max-width: $widescreen - $container-offset + +until-fullhd + &.is-fullhd + max-width: $fullhd - $container-offset + +widescreen + max-width: $widescreen - $container-offset + +fullhd + max-width: $fullhd - $container-offset diff --git a/docs/src/assets/bulma/elements/content.sass b/docs/src/assets/bulma/elements/content.sass new file mode 100644 index 00000000..001419ab --- /dev/null +++ b/docs/src/assets/bulma/elements/content.sass @@ -0,0 +1,155 @@ +$content-heading-color: $text-strong !default +$content-heading-weight: $weight-semibold !default +$content-heading-line-height: 1.125 !default + +$content-blockquote-background-color: $background !default +$content-blockquote-border-left: 5px solid $border !default +$content-blockquote-padding: 1.25em 1.5em !default + +$content-pre-padding: 1.25em 1.5em !default + +$content-table-cell-border: 1px solid $border !default +$content-table-cell-border-width: 0 0 1px !default +$content-table-cell-padding: 0.5em 0.75em !default +$content-table-cell-heading-color: $text-strong !default +$content-table-head-cell-border-width: 0 0 2px !default +$content-table-head-cell-color: $text-strong !default +$content-table-foot-cell-border-width: 2px 0 0 !default +$content-table-foot-cell-color: $text-strong !default + +.content + @extend %block + // Inline + li + li + margin-top: 0.25em + // Block + p, + dl, + ol, + ul, + blockquote, + pre, + table + &:not(:last-child) + margin-bottom: 1em + h1, + h2, + h3, + h4, + h5, + h6 + color: $content-heading-color + font-weight: $content-heading-weight + line-height: $content-heading-line-height + h1 + font-size: 2em + margin-bottom: 0.5em + &:not(:first-child) + margin-top: 1em + h2 + font-size: 1.75em + margin-bottom: 0.5714em + &:not(:first-child) + margin-top: 1.1428em + h3 + font-size: 1.5em + margin-bottom: 0.6666em + &:not(:first-child) + margin-top: 1.3333em + h4 + font-size: 1.25em + margin-bottom: 0.8em + h5 + font-size: 1.125em + margin-bottom: 0.8888em + h6 + font-size: 1em + margin-bottom: 1em + blockquote + background-color: $content-blockquote-background-color + border-left: $content-blockquote-border-left + padding: $content-blockquote-padding + ol + list-style-position: outside + margin-left: 2em + margin-top: 1em + &:not([type]) + list-style-type: decimal + &.is-lower-alpha + list-style-type: lower-alpha + &.is-lower-roman + list-style-type: lower-roman + &.is-upper-alpha + list-style-type: upper-alpha + &.is-upper-roman + list-style-type: upper-roman + ul + list-style: disc outside + margin-left: 2em + margin-top: 1em + ul + list-style-type: circle + margin-top: 0.5em + ul + list-style-type: square + dd + margin-left: 2em + figure + margin-left: 2em + margin-right: 2em + text-align: center + &:not(:first-child) + margin-top: 2em + &:not(:last-child) + margin-bottom: 2em + img + display: inline-block + figcaption + font-style: italic + pre + +overflow-touch + overflow-x: auto + padding: $content-pre-padding + white-space: pre + word-wrap: normal + sup, + sub + font-size: 75% + table + width: 100% + td, + th + border: $content-table-cell-border + border-width: $content-table-cell-border-width + padding: $content-table-cell-padding + vertical-align: top + th + color: $content-table-cell-heading-color + &:not([align]) + text-align: left + thead + td, + th + border-width: $content-table-head-cell-border-width + color: $content-table-head-cell-color + tfoot + td, + th + border-width: $content-table-foot-cell-border-width + color: $content-table-foot-cell-color + tbody + tr + &:last-child + td, + th + border-bottom-width: 0 + .tabs + li + li + margin-top: 0 + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large diff --git a/docs/src/assets/bulma/elements/form.sass b/docs/src/assets/bulma/elements/form.sass new file mode 100644 index 00000000..3122dc4c --- /dev/null +++ b/docs/src/assets/bulma/elements/form.sass @@ -0,0 +1 @@ +@warn "The form.sass file is DEPRECATED. It has moved into its own /form folder. Please import sass/form/_all instead." diff --git a/docs/src/assets/bulma/elements/icon.sass b/docs/src/assets/bulma/elements/icon.sass new file mode 100644 index 00000000..988546c7 --- /dev/null +++ b/docs/src/assets/bulma/elements/icon.sass @@ -0,0 +1,21 @@ +$icon-dimensions: 1.5rem !default +$icon-dimensions-small: 1rem !default +$icon-dimensions-medium: 2rem !default +$icon-dimensions-large: 3rem !default + +.icon + align-items: center + display: inline-flex + justify-content: center + height: $icon-dimensions + width: $icon-dimensions + // Sizes + &.is-small + height: $icon-dimensions-small + width: $icon-dimensions-small + &.is-medium + height: $icon-dimensions-medium + width: $icon-dimensions-medium + &.is-large + height: $icon-dimensions-large + width: $icon-dimensions-large diff --git a/docs/src/assets/bulma/elements/image.sass b/docs/src/assets/bulma/elements/image.sass new file mode 100644 index 00000000..7547abcf --- /dev/null +++ b/docs/src/assets/bulma/elements/image.sass @@ -0,0 +1,71 @@ +$dimensions: 16 24 32 48 64 96 128 !default + +.image + display: block + position: relative + img + display: block + height: auto + width: 100% + &.is-rounded + border-radius: $radius-rounded + &.is-fullwidth + width: 100% + // Ratio + &.is-square, + &.is-1by1, + &.is-5by4, + &.is-4by3, + &.is-3by2, + &.is-5by3, + &.is-16by9, + &.is-2by1, + &.is-3by1, + &.is-4by5, + &.is-3by4, + &.is-2by3, + &.is-3by5, + &.is-9by16, + &.is-1by2, + &.is-1by3 + img, + .has-ratio + @extend %overlay + height: 100% + width: 100% + &.is-square, + &.is-1by1 + padding-top: 100% + &.is-5by4 + padding-top: 80% + &.is-4by3 + padding-top: 75% + &.is-3by2 + padding-top: 66.6666% + &.is-5by3 + padding-top: 60% + &.is-16by9 + padding-top: 56.25% + &.is-2by1 + padding-top: 50% + &.is-3by1 + padding-top: 33.3333% + &.is-4by5 + padding-top: 125% + &.is-3by4 + padding-top: 133.3333% + &.is-2by3 + padding-top: 150% + &.is-3by5 + padding-top: 166.6666% + &.is-9by16 + padding-top: 177.7777% + &.is-1by2 + padding-top: 200% + &.is-1by3 + padding-top: 300% + // Sizes + @each $dimension in $dimensions + &.is-#{$dimension}x#{$dimension} + height: $dimension * 1px + width: $dimension * 1px diff --git a/docs/src/assets/bulma/elements/notification.sass b/docs/src/assets/bulma/elements/notification.sass new file mode 100644 index 00000000..32a0ee13 --- /dev/null +++ b/docs/src/assets/bulma/elements/notification.sass @@ -0,0 +1,43 @@ +$notification-background-color: $background !default +$notification-code-background-color: $scheme-main !default +$notification-radius: $radius !default +$notification-padding: 1.25rem 2.5rem 1.25rem 1.5rem !default + +.notification + @extend %block + background-color: $notification-background-color + border-radius: $notification-radius + padding: $notification-padding + position: relative + a:not(.button):not(.dropdown-item) + color: currentColor + text-decoration: underline + strong + color: currentColor + code, + pre + background: $notification-code-background-color + pre code + background: transparent + & > .delete + position: absolute + right: 0.5rem + top: 0.5rem + .title, + .subtitle, + .content + color: currentColor + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + background-color: $color + color: $color-invert + // If light and dark colors are provided + @if length($pair) >= 4 + $color-light: nth($pair, 3) + $color-dark: nth($pair, 4) + &.is-light + background-color: $color-light + color: $color-dark diff --git a/docs/src/assets/bulma/elements/other.sass b/docs/src/assets/bulma/elements/other.sass new file mode 100644 index 00000000..5725617c --- /dev/null +++ b/docs/src/assets/bulma/elements/other.sass @@ -0,0 +1,39 @@ +.block + @extend %block + +.delete + @extend %delete + +.heading + display: block + font-size: 11px + letter-spacing: 1px + margin-bottom: 5px + text-transform: uppercase + +.highlight + @extend %block + font-weight: $weight-normal + max-width: 100% + overflow: hidden + padding: 0 + pre + overflow: auto + max-width: 100% + +.loader + @extend %loader + +.number + align-items: center + background-color: $background + border-radius: $radius-rounded + display: inline-flex + font-size: $size-medium + height: 2em + justify-content: center + margin-right: 1.5rem + min-width: 2.5em + padding: 0.25rem 0.5rem + text-align: center + vertical-align: top diff --git a/docs/src/assets/bulma/elements/progress.sass b/docs/src/assets/bulma/elements/progress.sass new file mode 100644 index 00000000..bb43bb60 --- /dev/null +++ b/docs/src/assets/bulma/elements/progress.sass @@ -0,0 +1,67 @@ +$progress-bar-background-color: $border-light !default +$progress-value-background-color: $text !default +$progress-border-radius: $radius-rounded !default + +$progress-indeterminate-duration: 1.5s !default + +.progress + @extend %block + -moz-appearance: none + -webkit-appearance: none + border: none + border-radius: $progress-border-radius + display: block + height: $size-normal + overflow: hidden + padding: 0 + width: 100% + &::-webkit-progress-bar + background-color: $progress-bar-background-color + &::-webkit-progress-value + background-color: $progress-value-background-color + &::-moz-progress-bar + background-color: $progress-value-background-color + &::-ms-fill + background-color: $progress-value-background-color + border: none + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + &.is-#{$name} + &::-webkit-progress-value + background-color: $color + &::-moz-progress-bar + background-color: $color + &::-ms-fill + background-color: $color + &:indeterminate + background-image: linear-gradient(to right, $color 30%, $progress-bar-background-color 30%) + + &:indeterminate + animation-duration: $progress-indeterminate-duration + animation-iteration-count: infinite + animation-name: moveIndeterminate + animation-timing-function: linear + background-color: $progress-bar-background-color + background-image: linear-gradient(to right, $text 30%, $progress-bar-background-color 30%) + background-position: top left + background-repeat: no-repeat + background-size: 150% 150% + &::-webkit-progress-bar + background-color: transparent + &::-moz-progress-bar + background-color: transparent + + // Sizes + &.is-small + height: $size-small + &.is-medium + height: $size-medium + &.is-large + height: $size-large + +@keyframes moveIndeterminate + from + background-position: 200% 0 + to + background-position: -200% 0 diff --git a/docs/src/assets/bulma/elements/table.sass b/docs/src/assets/bulma/elements/table.sass new file mode 100644 index 00000000..2e6036ad --- /dev/null +++ b/docs/src/assets/bulma/elements/table.sass @@ -0,0 +1,127 @@ +$table-color: $text-strong !default +$table-background-color: $scheme-main !default + +$table-cell-border: 1px solid $border !default +$table-cell-border-width: 0 0 1px !default +$table-cell-padding: 0.5em 0.75em !default +$table-cell-heading-color: $text-strong !default + +$table-head-cell-border-width: 0 0 2px !default +$table-head-cell-color: $text-strong !default +$table-foot-cell-border-width: 2px 0 0 !default +$table-foot-cell-color: $text-strong !default + +$table-head-background-color: transparent !default +$table-body-background-color: transparent !default +$table-foot-background-color: transparent !default + +$table-row-hover-background-color: $scheme-main-bis !default + +$table-row-active-background-color: $primary !default +$table-row-active-color: $primary-invert !default + +$table-striped-row-even-background-color: $scheme-main-bis !default +$table-striped-row-even-hover-background-color: $scheme-main-ter !default + +.table + @extend %block + background-color: $table-background-color + color: $table-color + td, + th + border: $table-cell-border + border-width: $table-cell-border-width + padding: $table-cell-padding + vertical-align: top + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + background-color: $color + border-color: $color + color: $color-invert + // Modifiers + &.is-narrow + white-space: nowrap + width: 1% + &.is-selected + background-color: $table-row-active-background-color + color: $table-row-active-color + a, + strong + color: currentColor + th + color: $table-cell-heading-color + &:not([align]) + text-align: left + tr + &.is-selected + background-color: $table-row-active-background-color + color: $table-row-active-color + a, + strong + color: currentColor + td, + th + border-color: $table-row-active-color + color: currentColor + thead + background-color: $table-head-background-color + td, + th + border-width: $table-head-cell-border-width + color: $table-head-cell-color + tfoot + background-color: $table-foot-background-color + td, + th + border-width: $table-foot-cell-border-width + color: $table-foot-cell-color + tbody + background-color: $table-body-background-color + tr + &:last-child + td, + th + border-bottom-width: 0 + // Modifiers + &.is-bordered + td, + th + border-width: 1px + tr + &:last-child + td, + th + border-bottom-width: 1px + &.is-fullwidth + width: 100% + &.is-hoverable + tbody + tr:not(.is-selected) + &:hover + background-color: $table-row-hover-background-color + &.is-striped + tbody + tr:not(.is-selected) + &:hover + background-color: $table-row-hover-background-color + &:nth-child(even) + background-color: $table-striped-row-even-hover-background-color + &.is-narrow + td, + th + padding: 0.25em 0.5em + &.is-striped + tbody + tr:not(.is-selected) + &:nth-child(even) + background-color: $table-striped-row-even-background-color + +.table-container + @extend %block + +overflow-touch + overflow: auto + overflow-y: hidden + max-width: 100% diff --git a/docs/src/assets/bulma/elements/tag.sass b/docs/src/assets/bulma/elements/tag.sass new file mode 100644 index 00000000..e0fb89ef --- /dev/null +++ b/docs/src/assets/bulma/elements/tag.sass @@ -0,0 +1,128 @@ +$tag-background-color: $background !default +$tag-color: $text !default +$tag-radius: $radius !default +$tag-delete-margin: 1px !default + +.tags + align-items: center + display: flex + flex-wrap: wrap + justify-content: flex-start + .tag + margin-bottom: 0.5rem + &:not(:last-child) + margin-right: 0.5rem + &:last-child + margin-bottom: -0.5rem + &:not(:last-child) + margin-bottom: 1rem + // Sizes + &.are-medium + .tag:not(.is-normal):not(.is-large) + font-size: $size-normal + &.are-large + .tag:not(.is-normal):not(.is-medium) + font-size: $size-medium + &.is-centered + justify-content: center + .tag + margin-right: 0.25rem + margin-left: 0.25rem + &.is-right + justify-content: flex-end + .tag + &:not(:first-child) + margin-left: 0.5rem + &:not(:last-child) + margin-right: 0 + &.has-addons + .tag + margin-right: 0 + &:not(:first-child) + margin-left: 0 + border-bottom-left-radius: 0 + border-top-left-radius: 0 + &:not(:last-child) + border-bottom-right-radius: 0 + border-top-right-radius: 0 + +.tag:not(body) + align-items: center + background-color: $tag-background-color + border-radius: $tag-radius + color: $tag-color + display: inline-flex + font-size: $size-small + height: 2em + justify-content: center + line-height: 1.5 + padding-left: 0.75em + padding-right: 0.75em + white-space: nowrap + .delete + margin-left: 0.25rem + margin-right: -0.375rem + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + background-color: $color + color: $color-invert + // If a light and dark colors are provided + @if length($pair) > 3 + $color-light: nth($pair, 3) + $color-dark: nth($pair, 4) + &.is-light + background-color: $color-light + color: $color-dark + // Sizes + &.is-normal + font-size: $size-small + &.is-medium + font-size: $size-normal + &.is-large + font-size: $size-medium + .icon + &:first-child:not(:last-child) + margin-left: -0.375em + margin-right: 0.1875em + &:last-child:not(:first-child) + margin-left: 0.1875em + margin-right: -0.375em + &:first-child:last-child + margin-left: -0.375em + margin-right: -0.375em + // Modifiers + &.is-delete + margin-left: $tag-delete-margin + padding: 0 + position: relative + width: 2em + &::before, + &::after + background-color: currentColor + content: "" + display: block + left: 50% + position: absolute + top: 50% + transform: translateX(-50%) translateY(-50%) rotate(45deg) + transform-origin: center center + &::before + height: 1px + width: 50% + &::after + height: 50% + width: 1px + &:hover, + &:focus + background-color: darken($tag-background-color, 5%) + &:active + background-color: darken($tag-background-color, 10%) + &.is-rounded + border-radius: $radius-rounded + +a.tag + &:hover + text-decoration: underline diff --git a/docs/src/assets/bulma/elements/title.sass b/docs/src/assets/bulma/elements/title.sass new file mode 100644 index 00000000..fa9947dd --- /dev/null +++ b/docs/src/assets/bulma/elements/title.sass @@ -0,0 +1,70 @@ +$title-color: $text-strong !default +$title-family: false !default +$title-size: $size-3 !default +$title-weight: $weight-semibold !default +$title-line-height: 1.125 !default +$title-strong-color: inherit !default +$title-strong-weight: inherit !default +$title-sub-size: 0.75em !default +$title-sup-size: 0.75em !default + +$subtitle-color: $text !default +$subtitle-family: false !default +$subtitle-size: $size-5 !default +$subtitle-weight: $weight-normal !default +$subtitle-line-height: 1.25 !default +$subtitle-strong-color: $text-strong !default +$subtitle-strong-weight: $weight-semibold !default +$subtitle-negative-margin: -1.25rem !default + +.title, +.subtitle + @extend %block + word-break: break-word + em, + span + font-weight: inherit + sub + font-size: $title-sub-size + sup + font-size: $title-sup-size + .tag + vertical-align: middle + +.title + color: $title-color + @if $title-family + font-family: $title-family + font-size: $title-size + font-weight: $title-weight + line-height: $title-line-height + strong + color: $title-strong-color + font-weight: $title-strong-weight + & + .highlight + margin-top: -0.75rem + &:not(.is-spaced) + .subtitle + margin-top: $subtitle-negative-margin + // Sizes + @each $size in $sizes + $i: index($sizes, $size) + &.is-#{$i} + font-size: $size + +.subtitle + color: $subtitle-color + @if $subtitle-family + font-family: $subtitle-family + font-size: $subtitle-size + font-weight: $subtitle-weight + line-height: $subtitle-line-height + strong + color: $subtitle-strong-color + font-weight: $subtitle-strong-weight + &:not(.is-spaced) + .title + margin-top: $subtitle-negative-margin + // Sizes + @each $size in $sizes + $i: index($sizes, $size) + &.is-#{$i} + font-size: $size diff --git a/docs/src/assets/bulma/form/_all.sass b/docs/src/assets/bulma/form/_all.sass new file mode 100644 index 00000000..d9a2b955 --- /dev/null +++ b/docs/src/assets/bulma/form/_all.sass @@ -0,0 +1,8 @@ +@charset "utf-8" + +@import "shared.sass" +@import "input-textarea.sass" +@import "checkbox-radio.sass" +@import "select.sass" +@import "file.sass" +@import "tools.sass" diff --git a/docs/src/assets/bulma/form/checkbox-radio.sass b/docs/src/assets/bulma/form/checkbox-radio.sass new file mode 100644 index 00000000..d9f3ff0c --- /dev/null +++ b/docs/src/assets/bulma/form/checkbox-radio.sass @@ -0,0 +1,21 @@ +%checkbox-radio + cursor: pointer + display: inline-block + line-height: 1.25 + position: relative + input + cursor: pointer + &:hover + color: $input-hover-color + &[disabled], + fieldset[disabled] & + color: $input-disabled-color + cursor: not-allowed + +.checkbox + @extend %checkbox-radio + +.radio + @extend %checkbox-radio + & + .radio + margin-left: 0.5em diff --git a/docs/src/assets/bulma/form/file.sass b/docs/src/assets/bulma/form/file.sass new file mode 100644 index 00000000..41cc0215 --- /dev/null +++ b/docs/src/assets/bulma/form/file.sass @@ -0,0 +1,180 @@ +$file-border-color: $border !default +$file-radius: $radius !default + +$file-cta-background-color: $scheme-main-ter !default +$file-cta-color: $text !default +$file-cta-hover-color: $text-strong !default +$file-cta-active-color: $text-strong !default + +$file-name-border-color: $border !default +$file-name-border-style: solid !default +$file-name-border-width: 1px 1px 1px 0 !default +$file-name-max-width: 16em !default + +.file + @extend %unselectable + align-items: stretch + display: flex + justify-content: flex-start + position: relative + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + .file-cta + background-color: $color + border-color: transparent + color: $color-invert + &:hover, + &.is-hovered + .file-cta + background-color: darken($color, 2.5%) + border-color: transparent + color: $color-invert + &:focus, + &.is-focused + .file-cta + border-color: transparent + box-shadow: 0 0 0.5em rgba($color, 0.25) + color: $color-invert + &:active, + &.is-active + .file-cta + background-color: darken($color, 5%) + border-color: transparent + color: $color-invert + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + .file-icon + .fa + font-size: 21px + &.is-large + font-size: $size-large + .file-icon + .fa + font-size: 28px + // Modifiers + &.has-name + .file-cta + border-bottom-right-radius: 0 + border-top-right-radius: 0 + .file-name + border-bottom-left-radius: 0 + border-top-left-radius: 0 + &.is-empty + .file-cta + border-radius: $file-radius + .file-name + display: none + &.is-boxed + .file-label + flex-direction: column + .file-cta + flex-direction: column + height: auto + padding: 1em 3em + .file-name + border-width: 0 1px 1px + .file-icon + height: 1.5em + width: 1.5em + .fa + font-size: 21px + &.is-small + .file-icon .fa + font-size: 14px + &.is-medium + .file-icon .fa + font-size: 28px + &.is-large + .file-icon .fa + font-size: 35px + &.has-name + .file-cta + border-radius: $file-radius $file-radius 0 0 + .file-name + border-radius: 0 0 $file-radius $file-radius + border-width: 0 1px 1px + &.is-centered + justify-content: center + &.is-fullwidth + .file-label + width: 100% + .file-name + flex-grow: 1 + max-width: none + &.is-right + justify-content: flex-end + .file-cta + border-radius: 0 $file-radius $file-radius 0 + .file-name + border-radius: $file-radius 0 0 $file-radius + border-width: 1px 0 1px 1px + order: -1 + +.file-label + align-items: stretch + display: flex + cursor: pointer + justify-content: flex-start + overflow: hidden + position: relative + &:hover + .file-cta + background-color: darken($file-cta-background-color, 2.5%) + color: $file-cta-hover-color + .file-name + border-color: darken($file-name-border-color, 2.5%) + &:active + .file-cta + background-color: darken($file-cta-background-color, 5%) + color: $file-cta-active-color + .file-name + border-color: darken($file-name-border-color, 5%) + +.file-input + height: 100% + left: 0 + opacity: 0 + outline: none + position: absolute + top: 0 + width: 100% + +.file-cta, +.file-name + @extend %control + border-color: $file-border-color + border-radius: $file-radius + font-size: 1em + padding-left: 1em + padding-right: 1em + white-space: nowrap + +.file-cta + background-color: $file-cta-background-color + color: $file-cta-color + +.file-name + border-color: $file-name-border-color + border-style: $file-name-border-style + border-width: $file-name-border-width + display: block + max-width: $file-name-max-width + overflow: hidden + text-align: left + text-overflow: ellipsis + +.file-icon + align-items: center + display: flex + height: 1em + justify-content: center + margin-right: 0.5em + width: 1em + .fa + font-size: 14px diff --git a/docs/src/assets/bulma/form/input-textarea.sass b/docs/src/assets/bulma/form/input-textarea.sass new file mode 100644 index 00000000..7636cdae --- /dev/null +++ b/docs/src/assets/bulma/form/input-textarea.sass @@ -0,0 +1,64 @@ +$textarea-padding: $control-padding-horizontal !default +$textarea-max-height: 40em !default +$textarea-min-height: 8em !default + +%input-textarea + @extend %input + box-shadow: $input-shadow + max-width: 100% + width: 100% + &[readonly] + box-shadow: none + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + &.is-#{$name} + border-color: $color + &:focus, + &.is-focused, + &:active, + &.is-active + box-shadow: $input-focus-box-shadow-size rgba($color, 0.25) + // Sizes + &.is-small + +control-small + &.is-medium + +control-medium + &.is-large + +control-large + // Modifiers + &.is-fullwidth + display: block + width: 100% + &.is-inline + display: inline + width: auto + +.input + @extend %input-textarea + &.is-rounded + border-radius: $radius-rounded + padding-left: calc(#{$control-padding-horizontal} + 0.375em) + padding-right: calc(#{$control-padding-horizontal} + 0.375em) + &.is-static + background-color: transparent + border-color: transparent + box-shadow: none + padding-left: 0 + padding-right: 0 + +.textarea + @extend %input-textarea + display: block + max-width: 100% + min-width: 100% + padding: $textarea-padding + resize: vertical + &:not([rows]) + max-height: $textarea-max-height + min-height: $textarea-min-height + &[rows] + height: initial + // Modifiers + &.has-fixed-size + resize: none diff --git a/docs/src/assets/bulma/form/select.sass b/docs/src/assets/bulma/form/select.sass new file mode 100644 index 00000000..909b9d52 --- /dev/null +++ b/docs/src/assets/bulma/form/select.sass @@ -0,0 +1,85 @@ +.select + display: inline-block + max-width: 100% + position: relative + vertical-align: top + &:not(.is-multiple) + height: $input-height + &:not(.is-multiple):not(.is-loading) + &::after + @extend %arrow + border-color: $input-arrow + right: 1.125em + z-index: 4 + &.is-rounded + select + border-radius: $radius-rounded + padding-left: 1em + select + @extend %input + cursor: pointer + display: block + font-size: 1em + max-width: 100% + outline: none + &::-ms-expand + display: none + &[disabled]:hover, + fieldset[disabled] &:hover + border-color: $input-disabled-border-color + &:not([multiple]) + padding-right: 2.5em + &[multiple] + height: auto + padding: 0 + option + padding: 0.5em 1em + // States + &:not(.is-multiple):not(.is-loading):hover + &::after + border-color: $input-hover-color + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + &.is-#{$name} + &:not(:hover)::after + border-color: $color + select + border-color: $color + &:hover, + &.is-hovered + border-color: darken($color, 5%) + &:focus, + &.is-focused, + &:active, + &.is-active + box-shadow: $input-focus-box-shadow-size rgba($color, 0.25) + // Sizes + &.is-small + +control-small + &.is-medium + +control-medium + &.is-large + +control-large + // Modifiers + &.is-disabled + &::after + border-color: $input-disabled-color + &.is-fullwidth + width: 100% + select + width: 100% + &.is-loading + &::after + @extend %loader + margin-top: 0 + position: absolute + right: 0.625em + top: 0.625em + transform: none + &.is-small:after + font-size: $size-small + &.is-medium:after + font-size: $size-medium + &.is-large:after + font-size: $size-large diff --git a/docs/src/assets/bulma/form/shared.sass b/docs/src/assets/bulma/form/shared.sass new file mode 100644 index 00000000..3005587e --- /dev/null +++ b/docs/src/assets/bulma/form/shared.sass @@ -0,0 +1,55 @@ +$input-color: inherit !default +$input-background-color: $scheme-main !default +$input-border-color: $border !default +$input-height: $control-height !default +$input-shadow: inset 0 0.0625em 0.125em rgba($scheme-invert, 0.05) !default +$input-placeholder-color: bulmaRgba($input-color, 0.3) !default + +$input-hover-color: $text-strong !default +$input-hover-border-color: $border-hover !default + +$input-focus-color: $text-strong !default +$input-focus-border-color: $link !default +$input-focus-box-shadow-size: 0 0 0 0.125em !default +$input-focus-box-shadow-color: bulmaRgba($link, 0.25) !default + +$input-disabled-color: $text-light !default +$input-disabled-background-color: $background !default +$input-disabled-border-color: $background !default +$input-disabled-placeholder-color: bulmaRgba($input-disabled-color, 0.3) !default + +$input-arrow: $link !default + +$input-icon-color: $border !default +$input-icon-active-color: $text !default + +$input-radius: $radius !default + +=input + @extend %control + background-color: $input-background-color + border-color: $input-border-color + border-radius: $input-radius + color: $input-color + +placeholder + color: $input-placeholder-color + &:hover, + &.is-hovered + border-color: $input-hover-border-color + &:focus, + &.is-focused, + &:active, + &.is-active + border-color: $input-focus-border-color + box-shadow: $input-focus-box-shadow-size $input-focus-box-shadow-color + &[disabled], + fieldset[disabled] & + background-color: $input-disabled-background-color + border-color: $input-disabled-border-color + box-shadow: none + color: $input-disabled-color + +placeholder + color: $input-disabled-placeholder-color + +%input + +input diff --git a/docs/src/assets/bulma/form/tools.sass b/docs/src/assets/bulma/form/tools.sass new file mode 100644 index 00000000..ff2f8e20 --- /dev/null +++ b/docs/src/assets/bulma/form/tools.sass @@ -0,0 +1,205 @@ +$label-color: $text-strong !default +$label-weight: $weight-bold !default + +$help-size: $size-small !default + +.label + color: $label-color + display: block + font-size: $size-normal + font-weight: $label-weight + &:not(:last-child) + margin-bottom: 0.5em + // Sizes + &.is-small + font-size: $size-small + &.is-medium + font-size: $size-medium + &.is-large + font-size: $size-large + +.help + display: block + font-size: $help-size + margin-top: 0.25rem + @each $name, $pair in $colors + $color: nth($pair, 1) + &.is-#{$name} + color: $color + +// Containers + +.field + &:not(:last-child) + margin-bottom: 0.75rem + // Modifiers + &.has-addons + display: flex + justify-content: flex-start + .control + &:not(:last-child) + margin-right: -1px + &:not(:first-child):not(:last-child) + .button, + .input, + .select select + border-radius: 0 + &:first-child:not(:only-child) + .button, + .input, + .select select + border-bottom-right-radius: 0 + border-top-right-radius: 0 + &:last-child:not(:only-child) + .button, + .input, + .select select + border-bottom-left-radius: 0 + border-top-left-radius: 0 + .button, + .input, + .select select + &:not([disabled]) + &:hover, + &.is-hovered + z-index: 2 + &:focus, + &.is-focused, + &:active, + &.is-active + z-index: 3 + &:hover + z-index: 4 + &.is-expanded + flex-grow: 1 + flex-shrink: 1 + &.has-addons-centered + justify-content: center + &.has-addons-right + justify-content: flex-end + &.has-addons-fullwidth + .control + flex-grow: 1 + flex-shrink: 0 + &.is-grouped + display: flex + justify-content: flex-start + & > .control + flex-shrink: 0 + &:not(:last-child) + margin-bottom: 0 + margin-right: 0.75rem + &.is-expanded + flex-grow: 1 + flex-shrink: 1 + &.is-grouped-centered + justify-content: center + &.is-grouped-right + justify-content: flex-end + &.is-grouped-multiline + flex-wrap: wrap + & > .control + &:last-child, + &:not(:last-child) + margin-bottom: 0.75rem + &:last-child + margin-bottom: -0.75rem + &:not(:last-child) + margin-bottom: 0 + &.is-horizontal + +tablet + display: flex + +.field-label + .label + font-size: inherit + +mobile + margin-bottom: 0.5rem + +tablet + flex-basis: 0 + flex-grow: 1 + flex-shrink: 0 + margin-right: 1.5rem + text-align: right + &.is-small + font-size: $size-small + padding-top: 0.375em + &.is-normal + padding-top: 0.375em + &.is-medium + font-size: $size-medium + padding-top: 0.375em + &.is-large + font-size: $size-large + padding-top: 0.375em + +.field-body + .field .field + margin-bottom: 0 + +tablet + display: flex + flex-basis: 0 + flex-grow: 5 + flex-shrink: 1 + .field + margin-bottom: 0 + & > .field + flex-shrink: 1 + &:not(.is-narrow) + flex-grow: 1 + &:not(:last-child) + margin-right: 0.75rem + +.control + box-sizing: border-box + clear: both + font-size: $size-normal + position: relative + text-align: left + // Modifiers + &.has-icons-left, + &.has-icons-right + .input, + .select + &:focus + & ~ .icon + color: $input-icon-active-color + &.is-small ~ .icon + font-size: $size-small + &.is-medium ~ .icon + font-size: $size-medium + &.is-large ~ .icon + font-size: $size-large + .icon + color: $input-icon-color + height: $input-height + pointer-events: none + position: absolute + top: 0 + width: $input-height + z-index: 4 + &.has-icons-left + .input, + .select select + padding-left: $input-height + .icon.is-left + left: 0 + &.has-icons-right + .input, + .select select + padding-right: $input-height + .icon.is-right + right: 0 + &.is-loading + &::after + @extend %loader + position: absolute !important + right: 0.625em + top: 0.625em + z-index: 4 + &.is-small:after + font-size: $size-small + &.is-medium:after + font-size: $size-medium + &.is-large:after + font-size: $size-large diff --git a/docs/src/assets/bulma/grid/_all.sass b/docs/src/assets/bulma/grid/_all.sass new file mode 100644 index 00000000..e53070f6 --- /dev/null +++ b/docs/src/assets/bulma/grid/_all.sass @@ -0,0 +1,4 @@ +@charset "utf-8" + +@import "columns.sass" +@import "tiles.sass" diff --git a/docs/src/assets/bulma/grid/columns.sass b/docs/src/assets/bulma/grid/columns.sass new file mode 100644 index 00000000..34a83533 --- /dev/null +++ b/docs/src/assets/bulma/grid/columns.sass @@ -0,0 +1,504 @@ +$column-gap: 0.75rem !default + +.column + display: block + flex-basis: 0 + flex-grow: 1 + flex-shrink: 1 + padding: $column-gap + .columns.is-mobile > &.is-narrow + flex: none + .columns.is-mobile > &.is-full + flex: none + width: 100% + .columns.is-mobile > &.is-three-quarters + flex: none + width: 75% + .columns.is-mobile > &.is-two-thirds + flex: none + width: 66.6666% + .columns.is-mobile > &.is-half + flex: none + width: 50% + .columns.is-mobile > &.is-one-third + flex: none + width: 33.3333% + .columns.is-mobile > &.is-one-quarter + flex: none + width: 25% + .columns.is-mobile > &.is-one-fifth + flex: none + width: 20% + .columns.is-mobile > &.is-two-fifths + flex: none + width: 40% + .columns.is-mobile > &.is-three-fifths + flex: none + width: 60% + .columns.is-mobile > &.is-four-fifths + flex: none + width: 80% + .columns.is-mobile > &.is-offset-three-quarters + margin-left: 75% + .columns.is-mobile > &.is-offset-two-thirds + margin-left: 66.6666% + .columns.is-mobile > &.is-offset-half + margin-left: 50% + .columns.is-mobile > &.is-offset-one-third + margin-left: 33.3333% + .columns.is-mobile > &.is-offset-one-quarter + margin-left: 25% + .columns.is-mobile > &.is-offset-one-fifth + margin-left: 20% + .columns.is-mobile > &.is-offset-two-fifths + margin-left: 40% + .columns.is-mobile > &.is-offset-three-fifths + margin-left: 60% + .columns.is-mobile > &.is-offset-four-fifths + margin-left: 80% + @for $i from 0 through 12 + .columns.is-mobile > &.is-#{$i} + flex: none + width: percentage($i / 12) + .columns.is-mobile > &.is-offset-#{$i} + margin-left: percentage($i / 12) + +mobile + &.is-narrow-mobile + flex: none + &.is-full-mobile + flex: none + width: 100% + &.is-three-quarters-mobile + flex: none + width: 75% + &.is-two-thirds-mobile + flex: none + width: 66.6666% + &.is-half-mobile + flex: none + width: 50% + &.is-one-third-mobile + flex: none + width: 33.3333% + &.is-one-quarter-mobile + flex: none + width: 25% + &.is-one-fifth-mobile + flex: none + width: 20% + &.is-two-fifths-mobile + flex: none + width: 40% + &.is-three-fifths-mobile + flex: none + width: 60% + &.is-four-fifths-mobile + flex: none + width: 80% + &.is-offset-three-quarters-mobile + margin-left: 75% + &.is-offset-two-thirds-mobile + margin-left: 66.6666% + &.is-offset-half-mobile + margin-left: 50% + &.is-offset-one-third-mobile + margin-left: 33.3333% + &.is-offset-one-quarter-mobile + margin-left: 25% + &.is-offset-one-fifth-mobile + margin-left: 20% + &.is-offset-two-fifths-mobile + margin-left: 40% + &.is-offset-three-fifths-mobile + margin-left: 60% + &.is-offset-four-fifths-mobile + margin-left: 80% + @for $i from 0 through 12 + &.is-#{$i}-mobile + flex: none + width: percentage($i / 12) + &.is-offset-#{$i}-mobile + margin-left: percentage($i / 12) + +tablet + &.is-narrow, + &.is-narrow-tablet + flex: none + &.is-full, + &.is-full-tablet + flex: none + width: 100% + &.is-three-quarters, + &.is-three-quarters-tablet + flex: none + width: 75% + &.is-two-thirds, + &.is-two-thirds-tablet + flex: none + width: 66.6666% + &.is-half, + &.is-half-tablet + flex: none + width: 50% + &.is-one-third, + &.is-one-third-tablet + flex: none + width: 33.3333% + &.is-one-quarter, + &.is-one-quarter-tablet + flex: none + width: 25% + &.is-one-fifth, + &.is-one-fifth-tablet + flex: none + width: 20% + &.is-two-fifths, + &.is-two-fifths-tablet + flex: none + width: 40% + &.is-three-fifths, + &.is-three-fifths-tablet + flex: none + width: 60% + &.is-four-fifths, + &.is-four-fifths-tablet + flex: none + width: 80% + &.is-offset-three-quarters, + &.is-offset-three-quarters-tablet + margin-left: 75% + &.is-offset-two-thirds, + &.is-offset-two-thirds-tablet + margin-left: 66.6666% + &.is-offset-half, + &.is-offset-half-tablet + margin-left: 50% + &.is-offset-one-third, + &.is-offset-one-third-tablet + margin-left: 33.3333% + &.is-offset-one-quarter, + &.is-offset-one-quarter-tablet + margin-left: 25% + &.is-offset-one-fifth, + &.is-offset-one-fifth-tablet + margin-left: 20% + &.is-offset-two-fifths, + &.is-offset-two-fifths-tablet + margin-left: 40% + &.is-offset-three-fifths, + &.is-offset-three-fifths-tablet + margin-left: 60% + &.is-offset-four-fifths, + &.is-offset-four-fifths-tablet + margin-left: 80% + @for $i from 0 through 12 + &.is-#{$i}, + &.is-#{$i}-tablet + flex: none + width: percentage($i / 12) + &.is-offset-#{$i}, + &.is-offset-#{$i}-tablet + margin-left: percentage($i / 12) + +touch + &.is-narrow-touch + flex: none + &.is-full-touch + flex: none + width: 100% + &.is-three-quarters-touch + flex: none + width: 75% + &.is-two-thirds-touch + flex: none + width: 66.6666% + &.is-half-touch + flex: none + width: 50% + &.is-one-third-touch + flex: none + width: 33.3333% + &.is-one-quarter-touch + flex: none + width: 25% + &.is-one-fifth-touch + flex: none + width: 20% + &.is-two-fifths-touch + flex: none + width: 40% + &.is-three-fifths-touch + flex: none + width: 60% + &.is-four-fifths-touch + flex: none + width: 80% + &.is-offset-three-quarters-touch + margin-left: 75% + &.is-offset-two-thirds-touch + margin-left: 66.6666% + &.is-offset-half-touch + margin-left: 50% + &.is-offset-one-third-touch + margin-left: 33.3333% + &.is-offset-one-quarter-touch + margin-left: 25% + &.is-offset-one-fifth-touch + margin-left: 20% + &.is-offset-two-fifths-touch + margin-left: 40% + &.is-offset-three-fifths-touch + margin-left: 60% + &.is-offset-four-fifths-touch + margin-left: 80% + @for $i from 0 through 12 + &.is-#{$i}-touch + flex: none + width: percentage($i / 12) + &.is-offset-#{$i}-touch + margin-left: percentage($i / 12) + +desktop + &.is-narrow-desktop + flex: none + &.is-full-desktop + flex: none + width: 100% + &.is-three-quarters-desktop + flex: none + width: 75% + &.is-two-thirds-desktop + flex: none + width: 66.6666% + &.is-half-desktop + flex: none + width: 50% + &.is-one-third-desktop + flex: none + width: 33.3333% + &.is-one-quarter-desktop + flex: none + width: 25% + &.is-one-fifth-desktop + flex: none + width: 20% + &.is-two-fifths-desktop + flex: none + width: 40% + &.is-three-fifths-desktop + flex: none + width: 60% + &.is-four-fifths-desktop + flex: none + width: 80% + &.is-offset-three-quarters-desktop + margin-left: 75% + &.is-offset-two-thirds-desktop + margin-left: 66.6666% + &.is-offset-half-desktop + margin-left: 50% + &.is-offset-one-third-desktop + margin-left: 33.3333% + &.is-offset-one-quarter-desktop + margin-left: 25% + &.is-offset-one-fifth-desktop + margin-left: 20% + &.is-offset-two-fifths-desktop + margin-left: 40% + &.is-offset-three-fifths-desktop + margin-left: 60% + &.is-offset-four-fifths-desktop + margin-left: 80% + @for $i from 0 through 12 + &.is-#{$i}-desktop + flex: none + width: percentage($i / 12) + &.is-offset-#{$i}-desktop + margin-left: percentage($i / 12) + +widescreen + &.is-narrow-widescreen + flex: none + &.is-full-widescreen + flex: none + width: 100% + &.is-three-quarters-widescreen + flex: none + width: 75% + &.is-two-thirds-widescreen + flex: none + width: 66.6666% + &.is-half-widescreen + flex: none + width: 50% + &.is-one-third-widescreen + flex: none + width: 33.3333% + &.is-one-quarter-widescreen + flex: none + width: 25% + &.is-one-fifth-widescreen + flex: none + width: 20% + &.is-two-fifths-widescreen + flex: none + width: 40% + &.is-three-fifths-widescreen + flex: none + width: 60% + &.is-four-fifths-widescreen + flex: none + width: 80% + &.is-offset-three-quarters-widescreen + margin-left: 75% + &.is-offset-two-thirds-widescreen + margin-left: 66.6666% + &.is-offset-half-widescreen + margin-left: 50% + &.is-offset-one-third-widescreen + margin-left: 33.3333% + &.is-offset-one-quarter-widescreen + margin-left: 25% + &.is-offset-one-fifth-widescreen + margin-left: 20% + &.is-offset-two-fifths-widescreen + margin-left: 40% + &.is-offset-three-fifths-widescreen + margin-left: 60% + &.is-offset-four-fifths-widescreen + margin-left: 80% + @for $i from 0 through 12 + &.is-#{$i}-widescreen + flex: none + width: percentage($i / 12) + &.is-offset-#{$i}-widescreen + margin-left: percentage($i / 12) + +fullhd + &.is-narrow-fullhd + flex: none + &.is-full-fullhd + flex: none + width: 100% + &.is-three-quarters-fullhd + flex: none + width: 75% + &.is-two-thirds-fullhd + flex: none + width: 66.6666% + &.is-half-fullhd + flex: none + width: 50% + &.is-one-third-fullhd + flex: none + width: 33.3333% + &.is-one-quarter-fullhd + flex: none + width: 25% + &.is-one-fifth-fullhd + flex: none + width: 20% + &.is-two-fifths-fullhd + flex: none + width: 40% + &.is-three-fifths-fullhd + flex: none + width: 60% + &.is-four-fifths-fullhd + flex: none + width: 80% + &.is-offset-three-quarters-fullhd + margin-left: 75% + &.is-offset-two-thirds-fullhd + margin-left: 66.6666% + &.is-offset-half-fullhd + margin-left: 50% + &.is-offset-one-third-fullhd + margin-left: 33.3333% + &.is-offset-one-quarter-fullhd + margin-left: 25% + &.is-offset-one-fifth-fullhd + margin-left: 20% + &.is-offset-two-fifths-fullhd + margin-left: 40% + &.is-offset-three-fifths-fullhd + margin-left: 60% + &.is-offset-four-fifths-fullhd + margin-left: 80% + @for $i from 0 through 12 + &.is-#{$i}-fullhd + flex: none + width: percentage($i / 12) + &.is-offset-#{$i}-fullhd + margin-left: percentage($i / 12) + +.columns + margin-left: (-$column-gap) + margin-right: (-$column-gap) + margin-top: (-$column-gap) + &:last-child + margin-bottom: (-$column-gap) + &:not(:last-child) + margin-bottom: calc(1.5rem - #{$column-gap}) + // Modifiers + &.is-centered + justify-content: center + &.is-gapless + margin-left: 0 + margin-right: 0 + margin-top: 0 + & > .column + margin: 0 + padding: 0 !important + &:not(:last-child) + margin-bottom: 1.5rem + &:last-child + margin-bottom: 0 + &.is-mobile + display: flex + &.is-multiline + flex-wrap: wrap + &.is-vcentered + align-items: center + // Responsiveness + +tablet + &:not(.is-desktop) + display: flex + +desktop + // Modifiers + &.is-desktop + display: flex + +@if $variable-columns + .columns.is-variable + --columnGap: 0.75rem + margin-left: calc(-1 * var(--columnGap)) + margin-right: calc(-1 * var(--columnGap)) + .column + padding-left: var(--columnGap) + padding-right: var(--columnGap) + @for $i from 0 through 8 + &.is-#{$i} + --columnGap: #{$i * 0.25rem} + +mobile + &.is-#{$i}-mobile + --columnGap: #{$i * 0.25rem} + +tablet + &.is-#{$i}-tablet + --columnGap: #{$i * 0.25rem} + +tablet-only + &.is-#{$i}-tablet-only + --columnGap: #{$i * 0.25rem} + +touch + &.is-#{$i}-touch + --columnGap: #{$i * 0.25rem} + +desktop + &.is-#{$i}-desktop + --columnGap: #{$i * 0.25rem} + +desktop-only + &.is-#{$i}-desktop-only + --columnGap: #{$i * 0.25rem} + +widescreen + &.is-#{$i}-widescreen + --columnGap: #{$i * 0.25rem} + +widescreen-only + &.is-#{$i}-widescreen-only + --columnGap: #{$i * 0.25rem} + +fullhd + &.is-#{$i}-fullhd + --columnGap: #{$i * 0.25rem} diff --git a/docs/src/assets/bulma/grid/tiles.sass b/docs/src/assets/bulma/grid/tiles.sass new file mode 100644 index 00000000..15648c29 --- /dev/null +++ b/docs/src/assets/bulma/grid/tiles.sass @@ -0,0 +1,34 @@ +$tile-spacing: 0.75rem !default + +.tile + align-items: stretch + display: block + flex-basis: 0 + flex-grow: 1 + flex-shrink: 1 + min-height: min-content + // Modifiers + &.is-ancestor + margin-left: $tile-spacing * -1 + margin-right: $tile-spacing * -1 + margin-top: $tile-spacing * -1 + &:last-child + margin-bottom: $tile-spacing * -1 + &:not(:last-child) + margin-bottom: $tile-spacing + &.is-child + margin: 0 !important + &.is-parent + padding: $tile-spacing + &.is-vertical + flex-direction: column + & > .tile.is-child:not(:last-child) + margin-bottom: 1.5rem !important + // Responsiveness + +tablet + &:not(.is-child) + display: flex + @for $i from 1 through 12 + &.is-#{$i} + flex: none + width: ($i / 12) * 100% diff --git a/docs/src/assets/bulma/layout/_all.sass b/docs/src/assets/bulma/layout/_all.sass new file mode 100644 index 00000000..143ada35 --- /dev/null +++ b/docs/src/assets/bulma/layout/_all.sass @@ -0,0 +1,5 @@ +@charset "utf-8" + +@import "hero.sass" +@import "section.sass" +@import "footer.sass" diff --git a/docs/src/assets/bulma/layout/footer.sass b/docs/src/assets/bulma/layout/footer.sass new file mode 100644 index 00000000..8faa11ed --- /dev/null +++ b/docs/src/assets/bulma/layout/footer.sass @@ -0,0 +1,9 @@ +$footer-background-color: $scheme-main-bis !default +$footer-color: false !default +$footer-padding: 3rem 1.5rem 6rem !default + +.footer + background-color: $footer-background-color + padding: $footer-padding + @if $footer-color + color: $footer-color diff --git a/docs/src/assets/bulma/layout/hero.sass b/docs/src/assets/bulma/layout/hero.sass new file mode 100644 index 00000000..ae8ed086 --- /dev/null +++ b/docs/src/assets/bulma/layout/hero.sass @@ -0,0 +1,144 @@ +$hero-body-padding: 3rem 1.5rem !default +$hero-body-padding-small: 1.5rem !default +$hero-body-padding-medium: 9rem 1.5rem !default +$hero-body-padding-large: 18rem 1.5rem !default + +// Main container +.hero + align-items: stretch + display: flex + flex-direction: column + justify-content: space-between + .navbar + background: none + .tabs + ul + border-bottom: none + // Colors + @each $name, $pair in $colors + $color: nth($pair, 1) + $color-invert: nth($pair, 2) + &.is-#{$name} + background-color: $color + color: $color-invert + a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current), + strong + color: inherit + .title + color: $color-invert + .subtitle + color: bulmaRgba($color-invert, 0.9) + a:not(.button), + strong + color: $color-invert + .navbar-menu + +touch + background-color: $color + .navbar-item, + .navbar-link + color: bulmaRgba($color-invert, 0.7) + a.navbar-item, + .navbar-link + &:hover, + &.is-active + background-color: darken($color, 5%) + color: $color-invert + .tabs + a + color: $color-invert + opacity: 0.9 + &:hover + opacity: 1 + li + &.is-active a + opacity: 1 + &.is-boxed, + &.is-toggle + a + color: $color-invert + &:hover + background-color: bulmaRgba($scheme-invert, 0.1) + li.is-active a + &, + &:hover + background-color: $color-invert + border-color: $color-invert + color: $color + // Modifiers + &.is-bold + $gradient-top-left: darken(saturate(adjust-hue($color, -10deg), 10%), 10%) + $gradient-bottom-right: lighten(saturate(adjust-hue($color, 10deg), 5%), 5%) + background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%) + +mobile + .navbar-menu + background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%) + // Sizes + &.is-small + .hero-body + padding: $hero-body-padding-small + &.is-medium + +tablet + .hero-body + padding: $hero-body-padding-medium + &.is-large + +tablet + .hero-body + padding: $hero-body-padding-large + &.is-halfheight, + &.is-fullheight, + &.is-fullheight-with-navbar + .hero-body + align-items: center + display: flex + & > .container + flex-grow: 1 + flex-shrink: 1 + &.is-halfheight + min-height: 50vh + &.is-fullheight + min-height: 100vh + +// Components + +.hero-video + @extend %overlay + overflow: hidden + video + left: 50% + min-height: 100% + min-width: 100% + position: absolute + top: 50% + transform: translate3d(-50%, -50%, 0) + // Modifiers + &.is-transparent + opacity: 0.3 + // Responsiveness + +mobile + display: none + +.hero-buttons + margin-top: 1.5rem + // Responsiveness + +mobile + .button + display: flex + &:not(:last-child) + margin-bottom: 0.75rem + +tablet + display: flex + justify-content: center + .button:not(:last-child) + margin-right: 1.5rem + +// Containers + +.hero-head, +.hero-foot + flex-grow: 0 + flex-shrink: 0 + +.hero-body + flex-grow: 1 + flex-shrink: 0 + padding: $hero-body-padding diff --git a/docs/src/assets/bulma/layout/section.sass b/docs/src/assets/bulma/layout/section.sass new file mode 100644 index 00000000..6f2d3523 --- /dev/null +++ b/docs/src/assets/bulma/layout/section.sass @@ -0,0 +1,13 @@ +$section-padding: 3rem 1.5rem !default +$section-padding-medium: 9rem 1.5rem !default +$section-padding-large: 18rem 1.5rem !default + +.section + padding: $section-padding + // Responsiveness + +desktop + // Sizes + &.is-medium + padding: $section-padding-medium + &.is-large + padding: $section-padding-large diff --git a/docs/src/assets/bulma/utilities/_all.sass b/docs/src/assets/bulma/utilities/_all.sass new file mode 100644 index 00000000..bf4ecfe3 --- /dev/null +++ b/docs/src/assets/bulma/utilities/_all.sass @@ -0,0 +1,8 @@ +@charset "utf-8" + +@import "initial-variables.sass" +@import "functions.sass" +@import "derived-variables.sass" +@import "animations.sass" +@import "mixins.sass" +@import "controls.sass" diff --git a/docs/src/assets/bulma/utilities/animations.sass b/docs/src/assets/bulma/utilities/animations.sass new file mode 100644 index 00000000..a14525d7 --- /dev/null +++ b/docs/src/assets/bulma/utilities/animations.sass @@ -0,0 +1,5 @@ +@keyframes spinAround + from + transform: rotate(0deg) + to + transform: rotate(359deg) diff --git a/docs/src/assets/bulma/utilities/controls.sass b/docs/src/assets/bulma/utilities/controls.sass new file mode 100644 index 00000000..cc7672a1 --- /dev/null +++ b/docs/src/assets/bulma/utilities/controls.sass @@ -0,0 +1,50 @@ +$control-radius: $radius !default +$control-radius-small: $radius-small !default + +$control-border-width: 1px !default + +$control-height: 2.5em !default +$control-line-height: 1.5 !default + +$control-padding-vertical: calc(0.5em - #{$control-border-width}) !default +$control-padding-horizontal: calc(0.75em - #{$control-border-width}) !default + +=control + -moz-appearance: none + -webkit-appearance: none + align-items: center + border: $control-border-width solid transparent + border-radius: $control-radius + box-shadow: none + display: inline-flex + font-size: $size-normal + height: $control-height + justify-content: flex-start + line-height: $control-line-height + padding-bottom: $control-padding-vertical + padding-left: $control-padding-horizontal + padding-right: $control-padding-horizontal + padding-top: $control-padding-vertical + position: relative + vertical-align: top + // States + &:focus, + &.is-focused, + &:active, + &.is-active + outline: none + &[disabled], + fieldset[disabled] & + cursor: not-allowed + +%control + +control + +// The controls sizes use mixins so they can be used at different breakpoints +=control-small + border-radius: $control-radius-small + font-size: $size-small +=control-medium + font-size: $size-medium +=control-large + font-size: $size-large diff --git a/docs/src/assets/bulma/utilities/derived-variables.sass b/docs/src/assets/bulma/utilities/derived-variables.sass new file mode 100644 index 00000000..18a7f461 --- /dev/null +++ b/docs/src/assets/bulma/utilities/derived-variables.sass @@ -0,0 +1,106 @@ +$primary: $turquoise !default + +$info: $cyan !default +$success: $green !default +$warning: $yellow !default +$danger: $red !default + +$light: $white-ter !default +$dark: $grey-darker !default + +// Invert colors + +$orange-invert: findColorInvert($orange) !default +$yellow-invert: findColorInvert($yellow) !default +$green-invert: findColorInvert($green) !default +$turquoise-invert: findColorInvert($turquoise) !default +$cyan-invert: findColorInvert($cyan) !default +$blue-invert: findColorInvert($blue) !default +$purple-invert: findColorInvert($purple) !default +$red-invert: findColorInvert($red) !default + +$primary-invert: findColorInvert($primary) !default +$primary-light: findLightColor($primary) !default +$primary-dark: findDarkColor($primary) !default +$info-invert: findColorInvert($info) !default +$info-light: findLightColor($info) !default +$info-dark: findDarkColor($info) !default +$success-invert: findColorInvert($success) !default +$success-light: findLightColor($success) !default +$success-dark: findDarkColor($success) !default +$warning-invert: findColorInvert($warning) !default +$warning-light: findLightColor($warning) !default +$warning-dark: findDarkColor($warning) !default +$danger-invert: findColorInvert($danger) !default +$danger-light: findLightColor($danger) !default +$danger-dark: findDarkColor($danger) !default +$light-invert: findColorInvert($light) !default +$dark-invert: findColorInvert($dark) !default + +// General colors + +$scheme-main: $white !default +$scheme-main-bis: $white-bis !default +$scheme-main-ter: $white-ter !default +$scheme-invert: $black !default +$scheme-invert-bis: $black-bis !default +$scheme-invert-ter: $black-ter !default + +$background: $white-ter !default + +$border: $grey-lighter !default +$border-hover: $grey-light !default +$border-light: $grey-lightest !default +$border-light-hover: $grey-light !default + +// Text colors + +$text: $grey-dark !default +$text-invert: findColorInvert($text) !default +$text-light: $grey !default +$text-strong: $grey-darker !default + +// Code colors + +$code: $red !default +$code-background: $background !default + +$pre: $text !default +$pre-background: $background !default + +// Link colors + +$link: $blue !default +$link-invert: findColorInvert($link) !default +$link-light: findLightColor($link) !default +$link-dark: findDarkColor($link) !default +$link-visited: $purple !default + +$link-hover: $grey-darker !default +$link-hover-border: $grey-light !default + +$link-focus: $grey-darker !default +$link-focus-border: $blue !default + +$link-active: $grey-darker !default +$link-active-border: $grey-dark !default + +// Typography + +$family-primary: $family-sans-serif !default +$family-secondary: $family-sans-serif !default +$family-code: $family-monospace !default + +$size-small: $size-7 !default +$size-normal: $size-6 !default +$size-medium: $size-5 !default +$size-large: $size-4 !default + +// Lists and maps +$custom-colors: null !default +$custom-shades: null !default + +$colors: mergeColorMaps(("white": ($white, $black), "black": ($black, $white), "light": ($light, $light-invert), "dark": ($dark, $dark-invert), "primary": ($primary, $primary-invert, $primary-light, $primary-dark), "link": ($link, $link-invert, $link-light, $link-dark), "info": ($info, $info-invert, $info-light, $info-dark), "success": ($success, $success-invert, $success-light, $success-dark), "warning": ($warning, $warning-invert, $warning-light, $warning-dark), "danger": ($danger, $danger-invert, $danger-light, $danger-dark)), $custom-colors) !default +$shades: mergeColorMaps(("black-bis": $black-bis, "black-ter": $black-ter, "grey-darker": $grey-darker, "grey-dark": $grey-dark, "grey": $grey, "grey-light": $grey-light, "grey-lighter": $grey-lighter, "white-ter": $white-ter, "white-bis": $white-bis), $custom-shades) !default + +$sizes: $size-1 $size-2 $size-3 $size-4 $size-5 $size-6 $size-7 !default diff --git a/docs/src/assets/bulma/utilities/functions.sass b/docs/src/assets/bulma/utilities/functions.sass new file mode 100644 index 00000000..54225e21 --- /dev/null +++ b/docs/src/assets/bulma/utilities/functions.sass @@ -0,0 +1,103 @@ +@function mergeColorMaps($bulma-colors, $custom-colors) + // We return at least Bulma's hard-coded colors + $merged-colors: $bulma-colors + + // We want a map as input + @if type-of($custom-colors) == 'map' + @each $name, $components in $custom-colors + // The color name should be a string + // and the components either a single color + // or a colors list with at least one element + @if type-of($name) == 'string' and (type-of($components) == 'list' or type-of($components) == 'color') and length($components) >= 1 + $color-base: null + $color-invert: null + $color-light: null + $color-dark: null + $value: null + + // The param can either be a single color + // or a list of 2 colors + @if type-of($components) == 'color' + $color-base: $components + $color-invert: findColorInvert($color-base) + $color-light: findLightColor($color-base) + $color-dark: findDarkColor($color-base) + @else if type-of($components) == 'list' + $color-base: nth($components, 1) + // If Invert, Light and Dark are provided + @if length($components) > 3 + $color-invert: nth($components, 2) + $color-light: nth($components, 3) + $color-dark: nth($components, 4) + // If only Invert and Light are provided + @else if length($components) > 2 + $color-invert: nth($components, 2) + $color-light: nth($components, 3) + $color-dark: findDarkColor($color-base) + // If only Invert is provided + @else + $color-invert: nth($components, 2) + $color-light: findLightColor($color-base) + $color-dark: findDarkColor($color-base) + + $value: ($color-base, $color-invert, $color-light, $color-dark) + + // We only want to merge the map if the color base is an actual color + @if type-of($color-base) == 'color' + // We merge this colors elements as map with Bulma's colors map + // (we can override them this way, no multiple definition for the same name) + // $merged-colors: map_merge($merged-colors, ($name: ($color-base, $color-invert, $color-light, $color-dark))) + $merged-colors: map_merge($merged-colors, ($name: $value)) + + @return $merged-colors + +@function powerNumber($number, $exp) + $value: 1 + @if $exp > 0 + @for $i from 1 through $exp + $value: $value * $number + @else if $exp < 0 + @for $i from 1 through -$exp + $value: $value / $number + @return $value + +@function colorLuminance($color) + $color-rgb: ('red': red($color),'green': green($color),'blue': blue($color)) + @each $name, $value in $color-rgb + $adjusted: 0 + $value: $value / 255 + @if $value < 0.03928 + $value: $value / 12.92 + @else + $value: ($value + .055) / 1.055 + $value: powerNumber($value, 2) + $color-rgb: map-merge($color-rgb, ($name: $value)) + @return (map-get($color-rgb, 'red') * .2126) + (map-get($color-rgb, 'green') * .7152) + (map-get($color-rgb, 'blue') * .0722) + +@function findColorInvert($color) + @if (colorLuminance($color) > 0.55) + @return rgba(#000, 0.7) + @else + @return #fff + +@function findLightColor($color) + @if type-of($color) == 'color' + $l: 96% + @if lightness($color) > 96% + $l: lightness($color) + @return change-color($color, $lightness: $l) + @return $background + +@function findDarkColor($color) + @if type-of($color) == 'color' + $base-l: 29% + $luminance: colorLuminance($color) + $luminance-delta: (0.53 - $luminance) + $target-l: round($base-l + ($luminance-delta * 53)) + @return change-color($color, $lightness: max($base-l, $target-l)) + @return $text-strong + +@function bulmaRgba($color, $alpha) + @if type-of($color) == 'color' + @return rgba($color, $alpha) + @return $color diff --git a/docs/src/assets/bulma/utilities/initial-variables.sass b/docs/src/assets/bulma/utilities/initial-variables.sass new file mode 100644 index 00000000..03bbc128 --- /dev/null +++ b/docs/src/assets/bulma/utilities/initial-variables.sass @@ -0,0 +1,77 @@ +// Colors + +$black: hsl(0, 0%, 4%) !default +$black-bis: hsl(0, 0%, 7%) !default +$black-ter: hsl(0, 0%, 14%) !default + +$grey-darker: hsl(0, 0%, 21%) !default +$grey-dark: hsl(0, 0%, 29%) !default +$grey: hsl(0, 0%, 48%) !default +$grey-light: hsl(0, 0%, 71%) !default +$grey-lighter: hsl(0, 0%, 86%) !default +$grey-lightest: hsl(0, 0%, 93%) !default + +$white-ter: hsl(0, 0%, 96%) !default +$white-bis: hsl(0, 0%, 98%) !default +$white: hsl(0, 0%, 100%) !default + +$orange: hsl(14, 100%, 53%) !default +$yellow: hsl(48, 100%, 67%) !default +$green: hsl(141, 53%, 53%) !default +$turquoise: hsl(171, 100%, 41%) !default +$cyan: hsl(204, 71%, 53%) !default +$blue: hsl(217, 71%, 53%) !default +$purple: hsl(271, 100%, 71%) !default +$red: hsl(348, 86%, 61%) !default + +// Typography + +$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default +$family-monospace: monospace !default +$render-mode: optimizeLegibility !default + +$size-1: 3rem !default +$size-2: 2.5rem !default +$size-3: 2rem !default +$size-4: 1.5rem !default +$size-5: 1.25rem !default +$size-6: 1rem !default +$size-7: 0.75rem !default + +$weight-light: 300 !default +$weight-normal: 400 !default +$weight-medium: 500 !default +$weight-semibold: 600 !default +$weight-bold: 700 !default + +// Spacing + +$block-spacing: 1.5rem !default + +// Responsiveness + +// The container horizontal gap, which acts as the offset for breakpoints +$gap: 32px !default +// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16 +$tablet: 769px !default +// 960px container + 4rem +$desktop: 960px + (2 * $gap) !default +// 1152px container + 4rem +$widescreen: 1152px + (2 * $gap) !default +$widescreen-enabled: true !default +// 1344px container + 4rem +$fullhd: 1344px + (2 * $gap) !default +$fullhd-enabled: true !default + +// Miscellaneous + +$easing: ease-out !default +$radius-small: 2px !default +$radius: 4px !default +$radius-large: 6px !default +$radius-rounded: 290486px !default +$speed: 86ms !default + +// Flags + +$variable-columns: true !default diff --git a/docs/src/assets/bulma/utilities/mixins.sass b/docs/src/assets/bulma/utilities/mixins.sass new file mode 100644 index 00000000..27d74673 --- /dev/null +++ b/docs/src/assets/bulma/utilities/mixins.sass @@ -0,0 +1,261 @@ +@import "initial-variables" + +=clearfix + &::after + clear: both + content: " " + display: table + +=center($width, $height: 0) + position: absolute + @if $height != 0 + left: calc(50% - (#{$width} / 2)) + top: calc(50% - (#{$height} / 2)) + @else + left: calc(50% - (#{$width} / 2)) + top: calc(50% - (#{$width} / 2)) + +=fa($size, $dimensions) + display: inline-block + font-size: $size + height: $dimensions + line-height: $dimensions + text-align: center + vertical-align: top + width: $dimensions + +=hamburger($dimensions) + cursor: pointer + display: block + height: $dimensions + position: relative + width: $dimensions + span + background-color: currentColor + display: block + height: 1px + left: calc(50% - 8px) + position: absolute + transform-origin: center + transition-duration: $speed + transition-property: background-color, opacity, transform + transition-timing-function: $easing + width: 16px + &:nth-child(1) + top: calc(50% - 6px) + &:nth-child(2) + top: calc(50% - 1px) + &:nth-child(3) + top: calc(50% + 4px) + &:hover + background-color: bulmaRgba(black, 0.05) + // Modifers + &.is-active + span + &:nth-child(1) + transform: translateY(5px) rotate(45deg) + &:nth-child(2) + opacity: 0 + &:nth-child(3) + transform: translateY(-5px) rotate(-45deg) + +=overflow-touch + -webkit-overflow-scrolling: touch + +=placeholder + $placeholders: ':-moz' ':-webkit-input' '-moz' '-ms-input' + @each $placeholder in $placeholders + &:#{$placeholder}-placeholder + @content + +// Responsiveness + +=from($device) + @media screen and (min-width: $device) + @content + +=until($device) + @media screen and (max-width: $device - 1px) + @content + +=mobile + @media screen and (max-width: $tablet - 1px) + @content + +=tablet + @media screen and (min-width: $tablet), print + @content + +=tablet-only + @media screen and (min-width: $tablet) and (max-width: $desktop - 1px) + @content + +=touch + @media screen and (max-width: $desktop - 1px) + @content + +=desktop + @media screen and (min-width: $desktop) + @content + +=desktop-only + @if $widescreen-enabled + @media screen and (min-width: $desktop) and (max-width: $widescreen - 1px) + @content + +=until-widescreen + @if $widescreen-enabled + @media screen and (max-width: $widescreen - 1px) + @content + +=widescreen + @if $widescreen-enabled + @media screen and (min-width: $widescreen) + @content + +=widescreen-only + @if $widescreen-enabled and $fullhd-enabled + @media screen and (min-width: $widescreen) and (max-width: $fullhd - 1px) + @content + +=until-fullhd + @if $fullhd-enabled + @media screen and (max-width: $fullhd - 1px) + @content + +=fullhd + @if $fullhd-enabled + @media screen and (min-width: $fullhd) + @content + +// Placeholders + +=unselectable + -webkit-touch-callout: none + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + +%unselectable + +unselectable + +=arrow($color: transparent) + border: 3px solid $color + border-radius: 2px + border-right: 0 + border-top: 0 + content: " " + display: block + height: 0.625em + margin-top: -0.4375em + pointer-events: none + position: absolute + top: 50% + transform: rotate(-45deg) + transform-origin: center + width: 0.625em + +%arrow + +arrow + +=block($spacing: $block-spacing) + &:not(:last-child) + margin-bottom: $spacing + +%block + +block + +=delete + @extend %unselectable + -moz-appearance: none + -webkit-appearance: none + background-color: bulmaRgba($scheme-invert, 0.2) + border: none + border-radius: $radius-rounded + cursor: pointer + pointer-events: auto + display: inline-block + flex-grow: 0 + flex-shrink: 0 + font-size: 0 + height: 20px + max-height: 20px + max-width: 20px + min-height: 20px + min-width: 20px + outline: none + position: relative + vertical-align: top + width: 20px + &::before, + &::after + background-color: $scheme-main + content: "" + display: block + left: 50% + position: absolute + top: 50% + transform: translateX(-50%) translateY(-50%) rotate(45deg) + transform-origin: center center + &::before + height: 2px + width: 50% + &::after + height: 50% + width: 2px + &:hover, + &:focus + background-color: bulmaRgba($scheme-invert, 0.3) + &:active + background-color: bulmaRgba($scheme-invert, 0.4) + // Sizes + &.is-small + height: 16px + max-height: 16px + max-width: 16px + min-height: 16px + min-width: 16px + width: 16px + &.is-medium + height: 24px + max-height: 24px + max-width: 24px + min-height: 24px + min-width: 24px + width: 24px + &.is-large + height: 32px + max-height: 32px + max-width: 32px + min-height: 32px + min-width: 32px + width: 32px + +%delete + +delete + +=loader + animation: spinAround 500ms infinite linear + border: 2px solid $grey-lighter + border-radius: $radius-rounded + border-right-color: transparent + border-top-color: transparent + content: "" + display: block + height: 1em + position: relative + width: 1em + +%loader + +loader + +=overlay($offset: 0) + bottom: $offset + left: $offset + position: absolute + right: $offset + top: $offset + +%overlay + +overlay diff --git a/docs/src/assets/fa/_animated.scss b/docs/src/assets/fa/_animated.scss new file mode 100644 index 00000000..7c7c0e17 --- /dev/null +++ b/docs/src/assets/fa/_animated.scss @@ -0,0 +1,20 @@ +// Animated Icons +// -------------------------- + +.#{$fa-css-prefix}-spin { + animation: fa-spin 2s infinite linear; +} + +.#{$fa-css-prefix}-pulse { + animation: fa-spin 1s infinite steps(8); +} + +@keyframes fa-spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/docs/src/assets/fa/_bordered-pulled.scss b/docs/src/assets/fa/_bordered-pulled.scss new file mode 100644 index 00000000..c8c4274c --- /dev/null +++ b/docs/src/assets/fa/_bordered-pulled.scss @@ -0,0 +1,20 @@ +// Bordered & Pulled +// ------------------------- + +.#{$fa-css-prefix}-border { + border: solid .08em $fa-border-color; + border-radius: .1em; + padding: .2em .25em .15em; +} + +.#{$fa-css-prefix}-pull-left { float: left; } +.#{$fa-css-prefix}-pull-right { float: right; } + +.#{$fa-css-prefix}, +.fas, +.far, +.fal, +.fab { + &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } + &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } +} diff --git a/docs/src/assets/fa/_core.scss b/docs/src/assets/fa/_core.scss new file mode 100644 index 00000000..7fd37f85 --- /dev/null +++ b/docs/src/assets/fa/_core.scss @@ -0,0 +1,16 @@ +// Base Class Definition +// ------------------------- + +.#{$fa-css-prefix}, +.fas, +.far, +.fal, +.fab { + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; +} diff --git a/docs/src/assets/fa/_fixed-width.scss b/docs/src/assets/fa/_fixed-width.scss new file mode 100644 index 00000000..5b33eb49 --- /dev/null +++ b/docs/src/assets/fa/_fixed-width.scss @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.#{$fa-css-prefix}-fw { + text-align: center; + width: (20em / 16); +} diff --git a/docs/src/assets/fa/_icons.scss b/docs/src/assets/fa/_icons.scss new file mode 100644 index 00000000..bfd2b469 --- /dev/null +++ b/docs/src/assets/fa/_icons.scss @@ -0,0 +1,792 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen +readers do not read off random characters that represent icons */ + +.#{$fa-css-prefix}-500px:before { content: fa-content($fa-var-500px); } +.#{$fa-css-prefix}-accessible-icon:before { content: fa-content($fa-var-accessible-icon); } +.#{$fa-css-prefix}-accusoft:before { content: fa-content($fa-var-accusoft); } +.#{$fa-css-prefix}-address-book:before { content: fa-content($fa-var-address-book); } +.#{$fa-css-prefix}-address-card:before { content: fa-content($fa-var-address-card); } +.#{$fa-css-prefix}-adjust:before { content: fa-content($fa-var-adjust); } +.#{$fa-css-prefix}-adn:before { content: fa-content($fa-var-adn); } +.#{$fa-css-prefix}-adversal:before { content: fa-content($fa-var-adversal); } +.#{$fa-css-prefix}-affiliatetheme:before { content: fa-content($fa-var-affiliatetheme); } +.#{$fa-css-prefix}-algolia:before { content: fa-content($fa-var-algolia); } +.#{$fa-css-prefix}-align-center:before { content: fa-content($fa-var-align-center); } +.#{$fa-css-prefix}-align-justify:before { content: fa-content($fa-var-align-justify); } +.#{$fa-css-prefix}-align-left:before { content: fa-content($fa-var-align-left); } +.#{$fa-css-prefix}-align-right:before { content: fa-content($fa-var-align-right); } +.#{$fa-css-prefix}-amazon:before { content: fa-content($fa-var-amazon); } +.#{$fa-css-prefix}-amazon-pay:before { content: fa-content($fa-var-amazon-pay); } +.#{$fa-css-prefix}-ambulance:before { content: fa-content($fa-var-ambulance); } +.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: fa-content($fa-var-american-sign-language-interpreting); } +.#{$fa-css-prefix}-amilia:before { content: fa-content($fa-var-amilia); } +.#{$fa-css-prefix}-anchor:before { content: fa-content($fa-var-anchor); } +.#{$fa-css-prefix}-android:before { content: fa-content($fa-var-android); } +.#{$fa-css-prefix}-angellist:before { content: fa-content($fa-var-angellist); } +.#{$fa-css-prefix}-angle-double-down:before { content: fa-content($fa-var-angle-double-down); } +.#{$fa-css-prefix}-angle-double-left:before { content: fa-content($fa-var-angle-double-left); } +.#{$fa-css-prefix}-angle-double-right:before { content: fa-content($fa-var-angle-double-right); } +.#{$fa-css-prefix}-angle-double-up:before { content: fa-content($fa-var-angle-double-up); } +.#{$fa-css-prefix}-angle-down:before { content: fa-content($fa-var-angle-down); } +.#{$fa-css-prefix}-angle-left:before { content: fa-content($fa-var-angle-left); } +.#{$fa-css-prefix}-angle-right:before { content: fa-content($fa-var-angle-right); } +.#{$fa-css-prefix}-angle-up:before { content: fa-content($fa-var-angle-up); } +.#{$fa-css-prefix}-angrycreative:before { content: fa-content($fa-var-angrycreative); } +.#{$fa-css-prefix}-angular:before { content: fa-content($fa-var-angular); } +.#{$fa-css-prefix}-app-store:before { content: fa-content($fa-var-app-store); } +.#{$fa-css-prefix}-app-store-ios:before { content: fa-content($fa-var-app-store-ios); } +.#{$fa-css-prefix}-apper:before { content: fa-content($fa-var-apper); } +.#{$fa-css-prefix}-apple:before { content: fa-content($fa-var-apple); } +.#{$fa-css-prefix}-apple-pay:before { content: fa-content($fa-var-apple-pay); } +.#{$fa-css-prefix}-archive:before { content: fa-content($fa-var-archive); } +.#{$fa-css-prefix}-arrow-alt-circle-down:before { content: fa-content($fa-var-arrow-alt-circle-down); } +.#{$fa-css-prefix}-arrow-alt-circle-left:before { content: fa-content($fa-var-arrow-alt-circle-left); } +.#{$fa-css-prefix}-arrow-alt-circle-right:before { content: fa-content($fa-var-arrow-alt-circle-right); } +.#{$fa-css-prefix}-arrow-alt-circle-up:before { content: fa-content($fa-var-arrow-alt-circle-up); } +.#{$fa-css-prefix}-arrow-circle-down:before { content: fa-content($fa-var-arrow-circle-down); } +.#{$fa-css-prefix}-arrow-circle-left:before { content: fa-content($fa-var-arrow-circle-left); } +.#{$fa-css-prefix}-arrow-circle-right:before { content: fa-content($fa-var-arrow-circle-right); } +.#{$fa-css-prefix}-arrow-circle-up:before { content: fa-content($fa-var-arrow-circle-up); } +.#{$fa-css-prefix}-arrow-down:before { content: fa-content($fa-var-arrow-down); } +.#{$fa-css-prefix}-arrow-left:before { content: fa-content($fa-var-arrow-left); } +.#{$fa-css-prefix}-arrow-right:before { content: fa-content($fa-var-arrow-right); } +.#{$fa-css-prefix}-arrow-up:before { content: fa-content($fa-var-arrow-up); } +.#{$fa-css-prefix}-arrows-alt:before { content: fa-content($fa-var-arrows-alt); } +.#{$fa-css-prefix}-arrows-alt-h:before { content: fa-content($fa-var-arrows-alt-h); } +.#{$fa-css-prefix}-arrows-alt-v:before { content: fa-content($fa-var-arrows-alt-v); } +.#{$fa-css-prefix}-assistive-listening-systems:before { content: fa-content($fa-var-assistive-listening-systems); } +.#{$fa-css-prefix}-asterisk:before { content: fa-content($fa-var-asterisk); } +.#{$fa-css-prefix}-asymmetrik:before { content: fa-content($fa-var-asymmetrik); } +.#{$fa-css-prefix}-at:before { content: fa-content($fa-var-at); } +.#{$fa-css-prefix}-audible:before { content: fa-content($fa-var-audible); } +.#{$fa-css-prefix}-audio-description:before { content: fa-content($fa-var-audio-description); } +.#{$fa-css-prefix}-autoprefixer:before { content: fa-content($fa-var-autoprefixer); } +.#{$fa-css-prefix}-avianex:before { content: fa-content($fa-var-avianex); } +.#{$fa-css-prefix}-aviato:before { content: fa-content($fa-var-aviato); } +.#{$fa-css-prefix}-aws:before { content: fa-content($fa-var-aws); } +.#{$fa-css-prefix}-backward:before { content: fa-content($fa-var-backward); } +.#{$fa-css-prefix}-balance-scale:before { content: fa-content($fa-var-balance-scale); } +.#{$fa-css-prefix}-ban:before { content: fa-content($fa-var-ban); } +.#{$fa-css-prefix}-bandcamp:before { content: fa-content($fa-var-bandcamp); } +.#{$fa-css-prefix}-barcode:before { content: fa-content($fa-var-barcode); } +.#{$fa-css-prefix}-bars:before { content: fa-content($fa-var-bars); } +.#{$fa-css-prefix}-bath:before { content: fa-content($fa-var-bath); } +.#{$fa-css-prefix}-battery-empty:before { content: fa-content($fa-var-battery-empty); } +.#{$fa-css-prefix}-battery-full:before { content: fa-content($fa-var-battery-full); } +.#{$fa-css-prefix}-battery-half:before { content: fa-content($fa-var-battery-half); } +.#{$fa-css-prefix}-battery-quarter:before { content: fa-content($fa-var-battery-quarter); } +.#{$fa-css-prefix}-battery-three-quarters:before { content: fa-content($fa-var-battery-three-quarters); } +.#{$fa-css-prefix}-bed:before { content: fa-content($fa-var-bed); } +.#{$fa-css-prefix}-beer:before { content: fa-content($fa-var-beer); } +.#{$fa-css-prefix}-behance:before { content: fa-content($fa-var-behance); } +.#{$fa-css-prefix}-behance-square:before { content: fa-content($fa-var-behance-square); } +.#{$fa-css-prefix}-bell:before { content: fa-content($fa-var-bell); } +.#{$fa-css-prefix}-bell-slash:before { content: fa-content($fa-var-bell-slash); } +.#{$fa-css-prefix}-bicycle:before { content: fa-content($fa-var-bicycle); } +.#{$fa-css-prefix}-bimobject:before { content: fa-content($fa-var-bimobject); } +.#{$fa-css-prefix}-binoculars:before { content: fa-content($fa-var-binoculars); } +.#{$fa-css-prefix}-birthday-cake:before { content: fa-content($fa-var-birthday-cake); } +.#{$fa-css-prefix}-bitbucket:before { content: fa-content($fa-var-bitbucket); } +.#{$fa-css-prefix}-bitcoin:before { content: fa-content($fa-var-bitcoin); } +.#{$fa-css-prefix}-bity:before { content: fa-content($fa-var-bity); } +.#{$fa-css-prefix}-black-tie:before { content: fa-content($fa-var-black-tie); } +.#{$fa-css-prefix}-blackberry:before { content: fa-content($fa-var-blackberry); } +.#{$fa-css-prefix}-blind:before { content: fa-content($fa-var-blind); } +.#{$fa-css-prefix}-blogger:before { content: fa-content($fa-var-blogger); } +.#{$fa-css-prefix}-blogger-b:before { content: fa-content($fa-var-blogger-b); } +.#{$fa-css-prefix}-bluetooth:before { content: fa-content($fa-var-bluetooth); } +.#{$fa-css-prefix}-bluetooth-b:before { content: fa-content($fa-var-bluetooth-b); } +.#{$fa-css-prefix}-bold:before { content: fa-content($fa-var-bold); } +.#{$fa-css-prefix}-bolt:before { content: fa-content($fa-var-bolt); } +.#{$fa-css-prefix}-bomb:before { content: fa-content($fa-var-bomb); } +.#{$fa-css-prefix}-book:before { content: fa-content($fa-var-book); } +.#{$fa-css-prefix}-bookmark:before { content: fa-content($fa-var-bookmark); } +.#{$fa-css-prefix}-braille:before { content: fa-content($fa-var-braille); } +.#{$fa-css-prefix}-briefcase:before { content: fa-content($fa-var-briefcase); } +.#{$fa-css-prefix}-btc:before { content: fa-content($fa-var-btc); } +.#{$fa-css-prefix}-bug:before { content: fa-content($fa-var-bug); } +.#{$fa-css-prefix}-building:before { content: fa-content($fa-var-building); } +.#{$fa-css-prefix}-bullhorn:before { content: fa-content($fa-var-bullhorn); } +.#{$fa-css-prefix}-bullseye:before { content: fa-content($fa-var-bullseye); } +.#{$fa-css-prefix}-buromobelexperte:before { content: fa-content($fa-var-buromobelexperte); } +.#{$fa-css-prefix}-bus:before { content: fa-content($fa-var-bus); } +.#{$fa-css-prefix}-buysellads:before { content: fa-content($fa-var-buysellads); } +.#{$fa-css-prefix}-calculator:before { content: fa-content($fa-var-calculator); } +.#{$fa-css-prefix}-calendar:before { content: fa-content($fa-var-calendar); } +.#{$fa-css-prefix}-calendar-alt:before { content: fa-content($fa-var-calendar-alt); } +.#{$fa-css-prefix}-calendar-check:before { content: fa-content($fa-var-calendar-check); } +.#{$fa-css-prefix}-calendar-minus:before { content: fa-content($fa-var-calendar-minus); } +.#{$fa-css-prefix}-calendar-plus:before { content: fa-content($fa-var-calendar-plus); } +.#{$fa-css-prefix}-calendar-times:before { content: fa-content($fa-var-calendar-times); } +.#{$fa-css-prefix}-camera:before { content: fa-content($fa-var-camera); } +.#{$fa-css-prefix}-camera-retro:before { content: fa-content($fa-var-camera-retro); } +.#{$fa-css-prefix}-car:before { content: fa-content($fa-var-car); } +.#{$fa-css-prefix}-caret-down:before { content: fa-content($fa-var-caret-down); } +.#{$fa-css-prefix}-caret-left:before { content: fa-content($fa-var-caret-left); } +.#{$fa-css-prefix}-caret-right:before { content: fa-content($fa-var-caret-right); } +.#{$fa-css-prefix}-caret-square-down:before { content: fa-content($fa-var-caret-square-down); } +.#{$fa-css-prefix}-caret-square-left:before { content: fa-content($fa-var-caret-square-left); } +.#{$fa-css-prefix}-caret-square-right:before { content: fa-content($fa-var-caret-square-right); } +.#{$fa-css-prefix}-caret-square-up:before { content: fa-content($fa-var-caret-square-up); } +.#{$fa-css-prefix}-caret-up:before { content: fa-content($fa-var-caret-up); } +.#{$fa-css-prefix}-cart-arrow-down:before { content: fa-content($fa-var-cart-arrow-down); } +.#{$fa-css-prefix}-cart-plus:before { content: fa-content($fa-var-cart-plus); } +.#{$fa-css-prefix}-cc-amazon-pay:before { content: fa-content($fa-var-cc-amazon-pay); } +.#{$fa-css-prefix}-cc-amex:before { content: fa-content($fa-var-cc-amex); } +.#{$fa-css-prefix}-cc-apple-pay:before { content: fa-content($fa-var-cc-apple-pay); } +.#{$fa-css-prefix}-cc-diners-club:before { content: fa-content($fa-var-cc-diners-club); } +.#{$fa-css-prefix}-cc-discover:before { content: fa-content($fa-var-cc-discover); } +.#{$fa-css-prefix}-cc-jcb:before { content: fa-content($fa-var-cc-jcb); } +.#{$fa-css-prefix}-cc-mastercard:before { content: fa-content($fa-var-cc-mastercard); } +.#{$fa-css-prefix}-cc-paypal:before { content: fa-content($fa-var-cc-paypal); } +.#{$fa-css-prefix}-cc-stripe:before { content: fa-content($fa-var-cc-stripe); } +.#{$fa-css-prefix}-cc-visa:before { content: fa-content($fa-var-cc-visa); } +.#{$fa-css-prefix}-centercode:before { content: fa-content($fa-var-centercode); } +.#{$fa-css-prefix}-certificate:before { content: fa-content($fa-var-certificate); } +.#{$fa-css-prefix}-chart-area:before { content: fa-content($fa-var-chart-area); } +.#{$fa-css-prefix}-chart-bar:before { content: fa-content($fa-var-chart-bar); } +.#{$fa-css-prefix}-chart-line:before { content: fa-content($fa-var-chart-line); } +.#{$fa-css-prefix}-chart-pie:before { content: fa-content($fa-var-chart-pie); } +.#{$fa-css-prefix}-check:before { content: fa-content($fa-var-check); } +.#{$fa-css-prefix}-check-circle:before { content: fa-content($fa-var-check-circle); } +.#{$fa-css-prefix}-check-square:before { content: fa-content($fa-var-check-square); } +.#{$fa-css-prefix}-chevron-circle-down:before { content: fa-content($fa-var-chevron-circle-down); } +.#{$fa-css-prefix}-chevron-circle-left:before { content: fa-content($fa-var-chevron-circle-left); } +.#{$fa-css-prefix}-chevron-circle-right:before { content: fa-content($fa-var-chevron-circle-right); } +.#{$fa-css-prefix}-chevron-circle-up:before { content: fa-content($fa-var-chevron-circle-up); } +.#{$fa-css-prefix}-chevron-down:before { content: fa-content($fa-var-chevron-down); } +.#{$fa-css-prefix}-chevron-left:before { content: fa-content($fa-var-chevron-left); } +.#{$fa-css-prefix}-chevron-right:before { content: fa-content($fa-var-chevron-right); } +.#{$fa-css-prefix}-chevron-up:before { content: fa-content($fa-var-chevron-up); } +.#{$fa-css-prefix}-child:before { content: fa-content($fa-var-child); } +.#{$fa-css-prefix}-chrome:before { content: fa-content($fa-var-chrome); } +.#{$fa-css-prefix}-circle:before { content: fa-content($fa-var-circle); } +.#{$fa-css-prefix}-circle-notch:before { content: fa-content($fa-var-circle-notch); } +.#{$fa-css-prefix}-clipboard:before { content: fa-content($fa-var-clipboard); } +.#{$fa-css-prefix}-clock:before { content: fa-content($fa-var-clock); } +.#{$fa-css-prefix}-clone:before { content: fa-content($fa-var-clone); } +.#{$fa-css-prefix}-closed-captioning:before { content: fa-content($fa-var-closed-captioning); } +.#{$fa-css-prefix}-cloud:before { content: fa-content($fa-var-cloud); } +.#{$fa-css-prefix}-cloud-download-alt:before { content: fa-content($fa-var-cloud-download-alt); } +.#{$fa-css-prefix}-cloud-upload-alt:before { content: fa-content($fa-var-cloud-upload-alt); } +.#{$fa-css-prefix}-cloudscale:before { content: fa-content($fa-var-cloudscale); } +.#{$fa-css-prefix}-cloudsmith:before { content: fa-content($fa-var-cloudsmith); } +.#{$fa-css-prefix}-cloudversify:before { content: fa-content($fa-var-cloudversify); } +.#{$fa-css-prefix}-code:before { content: fa-content($fa-var-code); } +.#{$fa-css-prefix}-code-branch:before { content: fa-content($fa-var-code-branch); } +.#{$fa-css-prefix}-codepen:before { content: fa-content($fa-var-codepen); } +.#{$fa-css-prefix}-codiepie:before { content: fa-content($fa-var-codiepie); } +.#{$fa-css-prefix}-coffee:before { content: fa-content($fa-var-coffee); } +.#{$fa-css-prefix}-cog:before { content: fa-content($fa-var-cog); } +.#{$fa-css-prefix}-cogs:before { content: fa-content($fa-var-cogs); } +.#{$fa-css-prefix}-columns:before { content: fa-content($fa-var-columns); } +.#{$fa-css-prefix}-comment:before { content: fa-content($fa-var-comment); } +.#{$fa-css-prefix}-comment-alt:before { content: fa-content($fa-var-comment-alt); } +.#{$fa-css-prefix}-comments:before { content: fa-content($fa-var-comments); } +.#{$fa-css-prefix}-compass:before { content: fa-content($fa-var-compass); } +.#{$fa-css-prefix}-compress:before { content: fa-content($fa-var-compress); } +.#{$fa-css-prefix}-connectdevelop:before { content: fa-content($fa-var-connectdevelop); } +.#{$fa-css-prefix}-contao:before { content: fa-content($fa-var-contao); } +.#{$fa-css-prefix}-copy:before { content: fa-content($fa-var-copy); } +.#{$fa-css-prefix}-copyright:before { content: fa-content($fa-var-copyright); } +.#{$fa-css-prefix}-cpanel:before { content: fa-content($fa-var-cpanel); } +.#{$fa-css-prefix}-creative-commons:before { content: fa-content($fa-var-creative-commons); } +.#{$fa-css-prefix}-credit-card:before { content: fa-content($fa-var-credit-card); } +.#{$fa-css-prefix}-crop:before { content: fa-content($fa-var-crop); } +.#{$fa-css-prefix}-crosshairs:before { content: fa-content($fa-var-crosshairs); } +.#{$fa-css-prefix}-css3:before { content: fa-content($fa-var-css3); } +.#{$fa-css-prefix}-css3-alt:before { content: fa-content($fa-var-css3-alt); } +.#{$fa-css-prefix}-cube:before { content: fa-content($fa-var-cube); } +.#{$fa-css-prefix}-cubes:before { content: fa-content($fa-var-cubes); } +.#{$fa-css-prefix}-cut:before { content: fa-content($fa-var-cut); } +.#{$fa-css-prefix}-cuttlefish:before { content: fa-content($fa-var-cuttlefish); } +.#{$fa-css-prefix}-d-and-d:before { content: fa-content($fa-var-d-and-d); } +.#{$fa-css-prefix}-dashcube:before { content: fa-content($fa-var-dashcube); } +.#{$fa-css-prefix}-database:before { content: fa-content($fa-var-database); } +.#{$fa-css-prefix}-deaf:before { content: fa-content($fa-var-deaf); } +.#{$fa-css-prefix}-delicious:before { content: fa-content($fa-var-delicious); } +.#{$fa-css-prefix}-deploydog:before { content: fa-content($fa-var-deploydog); } +.#{$fa-css-prefix}-deskpro:before { content: fa-content($fa-var-deskpro); } +.#{$fa-css-prefix}-desktop:before { content: fa-content($fa-var-desktop); } +.#{$fa-css-prefix}-deviantart:before { content: fa-content($fa-var-deviantart); } +.#{$fa-css-prefix}-digg:before { content: fa-content($fa-var-digg); } +.#{$fa-css-prefix}-digital-ocean:before { content: fa-content($fa-var-digital-ocean); } +.#{$fa-css-prefix}-discord:before { content: fa-content($fa-var-discord); } +.#{$fa-css-prefix}-discourse:before { content: fa-content($fa-var-discourse); } +.#{$fa-css-prefix}-dochub:before { content: fa-content($fa-var-dochub); } +.#{$fa-css-prefix}-docker:before { content: fa-content($fa-var-docker); } +.#{$fa-css-prefix}-dollar-sign:before { content: fa-content($fa-var-dollar-sign); } +.#{$fa-css-prefix}-dot-circle:before { content: fa-content($fa-var-dot-circle); } +.#{$fa-css-prefix}-download:before { content: fa-content($fa-var-download); } +.#{$fa-css-prefix}-draft2digital:before { content: fa-content($fa-var-draft2digital); } +.#{$fa-css-prefix}-dribbble:before { content: fa-content($fa-var-dribbble); } +.#{$fa-css-prefix}-dribbble-square:before { content: fa-content($fa-var-dribbble-square); } +.#{$fa-css-prefix}-dropbox:before { content: fa-content($fa-var-dropbox); } +.#{$fa-css-prefix}-drupal:before { content: fa-content($fa-var-drupal); } +.#{$fa-css-prefix}-dyalog:before { content: fa-content($fa-var-dyalog); } +.#{$fa-css-prefix}-earlybirds:before { content: fa-content($fa-var-earlybirds); } +.#{$fa-css-prefix}-edge:before { content: fa-content($fa-var-edge); } +.#{$fa-css-prefix}-edit:before { content: fa-content($fa-var-edit); } +.#{$fa-css-prefix}-eject:before { content: fa-content($fa-var-eject); } +.#{$fa-css-prefix}-ellipsis-h:before { content: fa-content($fa-var-ellipsis-h); } +.#{$fa-css-prefix}-ellipsis-v:before { content: fa-content($fa-var-ellipsis-v); } +.#{$fa-css-prefix}-ember:before { content: fa-content($fa-var-ember); } +.#{$fa-css-prefix}-empire:before { content: fa-content($fa-var-empire); } +.#{$fa-css-prefix}-envelope:before { content: fa-content($fa-var-envelope); } +.#{$fa-css-prefix}-envelope-open:before { content: fa-content($fa-var-envelope-open); } +.#{$fa-css-prefix}-envelope-square:before { content: fa-content($fa-var-envelope-square); } +.#{$fa-css-prefix}-envira:before { content: fa-content($fa-var-envira); } +.#{$fa-css-prefix}-eraser:before { content: fa-content($fa-var-eraser); } +.#{$fa-css-prefix}-erlang:before { content: fa-content($fa-var-erlang); } +.#{$fa-css-prefix}-ethereum:before { content: fa-content($fa-var-ethereum); } +.#{$fa-css-prefix}-etsy:before { content: fa-content($fa-var-etsy); } +.#{$fa-css-prefix}-euro-sign:before { content: fa-content($fa-var-euro-sign); } +.#{$fa-css-prefix}-exchange-alt:before { content: fa-content($fa-var-exchange-alt); } +.#{$fa-css-prefix}-exclamation:before { content: fa-content($fa-var-exclamation); } +.#{$fa-css-prefix}-exclamation-circle:before { content: fa-content($fa-var-exclamation-circle); } +.#{$fa-css-prefix}-exclamation-triangle:before { content: fa-content($fa-var-exclamation-triangle); } +.#{$fa-css-prefix}-expand:before { content: fa-content($fa-var-expand); } +.#{$fa-css-prefix}-expand-arrows-alt:before { content: fa-content($fa-var-expand-arrows-alt); } +.#{$fa-css-prefix}-expeditedssl:before { content: fa-content($fa-var-expeditedssl); } +.#{$fa-css-prefix}-external-link-alt:before { content: fa-content($fa-var-external-link-alt); } +.#{$fa-css-prefix}-external-link-square-alt:before { content: fa-content($fa-var-external-link-square-alt); } +.#{$fa-css-prefix}-eye:before { content: fa-content($fa-var-eye); } +.#{$fa-css-prefix}-eye-dropper:before { content: fa-content($fa-var-eye-dropper); } +.#{$fa-css-prefix}-eye-slash:before { content: fa-content($fa-var-eye-slash); } +.#{$fa-css-prefix}-facebook:before { content: fa-content($fa-var-facebook); } +.#{$fa-css-prefix}-facebook-f:before { content: fa-content($fa-var-facebook-f); } +.#{$fa-css-prefix}-facebook-messenger:before { content: fa-content($fa-var-facebook-messenger); } +.#{$fa-css-prefix}-facebook-square:before { content: fa-content($fa-var-facebook-square); } +.#{$fa-css-prefix}-fast-backward:before { content: fa-content($fa-var-fast-backward); } +.#{$fa-css-prefix}-fast-forward:before { content: fa-content($fa-var-fast-forward); } +.#{$fa-css-prefix}-fax:before { content: fa-content($fa-var-fax); } +.#{$fa-css-prefix}-female:before { content: fa-content($fa-var-female); } +.#{$fa-css-prefix}-fighter-jet:before { content: fa-content($fa-var-fighter-jet); } +.#{$fa-css-prefix}-file:before { content: fa-content($fa-var-file); } +.#{$fa-css-prefix}-file-alt:before { content: fa-content($fa-var-file-alt); } +.#{$fa-css-prefix}-file-archive:before { content: fa-content($fa-var-file-archive); } +.#{$fa-css-prefix}-file-audio:before { content: fa-content($fa-var-file-audio); } +.#{$fa-css-prefix}-file-code:before { content: fa-content($fa-var-file-code); } +.#{$fa-css-prefix}-file-excel:before { content: fa-content($fa-var-file-excel); } +.#{$fa-css-prefix}-file-image:before { content: fa-content($fa-var-file-image); } +.#{$fa-css-prefix}-file-pdf:before { content: fa-content($fa-var-file-pdf); } +.#{$fa-css-prefix}-file-powerpoint:before { content: fa-content($fa-var-file-powerpoint); } +.#{$fa-css-prefix}-file-video:before { content: fa-content($fa-var-file-video); } +.#{$fa-css-prefix}-file-word:before { content: fa-content($fa-var-file-word); } +.#{$fa-css-prefix}-film:before { content: fa-content($fa-var-film); } +.#{$fa-css-prefix}-filter:before { content: fa-content($fa-var-filter); } +.#{$fa-css-prefix}-fire:before { content: fa-content($fa-var-fire); } +.#{$fa-css-prefix}-fire-extinguisher:before { content: fa-content($fa-var-fire-extinguisher); } +.#{$fa-css-prefix}-firefox:before { content: fa-content($fa-var-firefox); } +.#{$fa-css-prefix}-first-order:before { content: fa-content($fa-var-first-order); } +.#{$fa-css-prefix}-firstdraft:before { content: fa-content($fa-var-firstdraft); } +.#{$fa-css-prefix}-flag:before { content: fa-content($fa-var-flag); } +.#{$fa-css-prefix}-flag-checkered:before { content: fa-content($fa-var-flag-checkered); } +.#{$fa-css-prefix}-flask:before { content: fa-content($fa-var-flask); } +.#{$fa-css-prefix}-flickr:before { content: fa-content($fa-var-flickr); } +.#{$fa-css-prefix}-fly:before { content: fa-content($fa-var-fly); } +.#{$fa-css-prefix}-folder:before { content: fa-content($fa-var-folder); } +.#{$fa-css-prefix}-folder-open:before { content: fa-content($fa-var-folder-open); } +.#{$fa-css-prefix}-font:before { content: fa-content($fa-var-font); } +.#{$fa-css-prefix}-font-awesome:before { content: fa-content($fa-var-font-awesome); } +.#{$fa-css-prefix}-font-awesome-alt:before { content: fa-content($fa-var-font-awesome-alt); } +.#{$fa-css-prefix}-font-awesome-flag:before { content: fa-content($fa-var-font-awesome-flag); } +.#{$fa-css-prefix}-fonticons:before { content: fa-content($fa-var-fonticons); } +.#{$fa-css-prefix}-fonticons-fi:before { content: fa-content($fa-var-fonticons-fi); } +.#{$fa-css-prefix}-fort-awesome:before { content: fa-content($fa-var-fort-awesome); } +.#{$fa-css-prefix}-fort-awesome-alt:before { content: fa-content($fa-var-fort-awesome-alt); } +.#{$fa-css-prefix}-forumbee:before { content: fa-content($fa-var-forumbee); } +.#{$fa-css-prefix}-forward:before { content: fa-content($fa-var-forward); } +.#{$fa-css-prefix}-foursquare:before { content: fa-content($fa-var-foursquare); } +.#{$fa-css-prefix}-free-code-camp:before { content: fa-content($fa-var-free-code-camp); } +.#{$fa-css-prefix}-freebsd:before { content: fa-content($fa-var-freebsd); } +.#{$fa-css-prefix}-frown:before { content: fa-content($fa-var-frown); } +.#{$fa-css-prefix}-futbol:before { content: fa-content($fa-var-futbol); } +.#{$fa-css-prefix}-gamepad:before { content: fa-content($fa-var-gamepad); } +.#{$fa-css-prefix}-gavel:before { content: fa-content($fa-var-gavel); } +.#{$fa-css-prefix}-gem:before { content: fa-content($fa-var-gem); } +.#{$fa-css-prefix}-genderless:before { content: fa-content($fa-var-genderless); } +.#{$fa-css-prefix}-get-pocket:before { content: fa-content($fa-var-get-pocket); } +.#{$fa-css-prefix}-gg:before { content: fa-content($fa-var-gg); } +.#{$fa-css-prefix}-gg-circle:before { content: fa-content($fa-var-gg-circle); } +.#{$fa-css-prefix}-gift:before { content: fa-content($fa-var-gift); } +.#{$fa-css-prefix}-git:before { content: fa-content($fa-var-git); } +.#{$fa-css-prefix}-git-square:before { content: fa-content($fa-var-git-square); } +.#{$fa-css-prefix}-github:before { content: fa-content($fa-var-github); } +.#{$fa-css-prefix}-github-alt:before { content: fa-content($fa-var-github-alt); } +.#{$fa-css-prefix}-github-square:before { content: fa-content($fa-var-github-square); } +.#{$fa-css-prefix}-gitkraken:before { content: fa-content($fa-var-gitkraken); } +.#{$fa-css-prefix}-gitlab:before { content: fa-content($fa-var-gitlab); } +.#{$fa-css-prefix}-gitter:before { content: fa-content($fa-var-gitter); } +.#{$fa-css-prefix}-glass-martini:before { content: fa-content($fa-var-glass-martini); } +.#{$fa-css-prefix}-glide:before { content: fa-content($fa-var-glide); } +.#{$fa-css-prefix}-glide-g:before { content: fa-content($fa-var-glide-g); } +.#{$fa-css-prefix}-globe:before { content: fa-content($fa-var-globe); } +.#{$fa-css-prefix}-gofore:before { content: fa-content($fa-var-gofore); } +.#{$fa-css-prefix}-goodreads:before { content: fa-content($fa-var-goodreads); } +.#{$fa-css-prefix}-goodreads-g:before { content: fa-content($fa-var-goodreads-g); } +.#{$fa-css-prefix}-google:before { content: fa-content($fa-var-google); } +.#{$fa-css-prefix}-google-drive:before { content: fa-content($fa-var-google-drive); } +.#{$fa-css-prefix}-google-play:before { content: fa-content($fa-var-google-play); } +.#{$fa-css-prefix}-google-plus:before { content: fa-content($fa-var-google-plus); } +.#{$fa-css-prefix}-google-plus-g:before { content: fa-content($fa-var-google-plus-g); } +.#{$fa-css-prefix}-google-plus-square:before { content: fa-content($fa-var-google-plus-square); } +.#{$fa-css-prefix}-google-wallet:before { content: fa-content($fa-var-google-wallet); } +.#{$fa-css-prefix}-graduation-cap:before { content: fa-content($fa-var-graduation-cap); } +.#{$fa-css-prefix}-gratipay:before { content: fa-content($fa-var-gratipay); } +.#{$fa-css-prefix}-grav:before { content: fa-content($fa-var-grav); } +.#{$fa-css-prefix}-gripfire:before { content: fa-content($fa-var-gripfire); } +.#{$fa-css-prefix}-grunt:before { content: fa-content($fa-var-grunt); } +.#{$fa-css-prefix}-gulp:before { content: fa-content($fa-var-gulp); } +.#{$fa-css-prefix}-h-square:before { content: fa-content($fa-var-h-square); } +.#{$fa-css-prefix}-hacker-news:before { content: fa-content($fa-var-hacker-news); } +.#{$fa-css-prefix}-hacker-news-square:before { content: fa-content($fa-var-hacker-news-square); } +.#{$fa-css-prefix}-hand-lizard:before { content: fa-content($fa-var-hand-lizard); } +.#{$fa-css-prefix}-hand-paper:before { content: fa-content($fa-var-hand-paper); } +.#{$fa-css-prefix}-hand-peace:before { content: fa-content($fa-var-hand-peace); } +.#{$fa-css-prefix}-hand-point-down:before { content: fa-content($fa-var-hand-point-down); } +.#{$fa-css-prefix}-hand-point-left:before { content: fa-content($fa-var-hand-point-left); } +.#{$fa-css-prefix}-hand-point-right:before { content: fa-content($fa-var-hand-point-right); } +.#{$fa-css-prefix}-hand-point-up:before { content: fa-content($fa-var-hand-point-up); } +.#{$fa-css-prefix}-hand-pointer:before { content: fa-content($fa-var-hand-pointer); } +.#{$fa-css-prefix}-hand-rock:before { content: fa-content($fa-var-hand-rock); } +.#{$fa-css-prefix}-hand-scissors:before { content: fa-content($fa-var-hand-scissors); } +.#{$fa-css-prefix}-hand-spock:before { content: fa-content($fa-var-hand-spock); } +.#{$fa-css-prefix}-handshake:before { content: fa-content($fa-var-handshake); } +.#{$fa-css-prefix}-hashtag:before { content: fa-content($fa-var-hashtag); } +.#{$fa-css-prefix}-hdd:before { content: fa-content($fa-var-hdd); } +.#{$fa-css-prefix}-heading:before { content: fa-content($fa-var-heading); } +.#{$fa-css-prefix}-headphones:before { content: fa-content($fa-var-headphones); } +.#{$fa-css-prefix}-heart:before { content: fa-content($fa-var-heart); } +.#{$fa-css-prefix}-heartbeat:before { content: fa-content($fa-var-heartbeat); } +.#{$fa-css-prefix}-hire-a-helper:before { content: fa-content($fa-var-hire-a-helper); } +.#{$fa-css-prefix}-history:before { content: fa-content($fa-var-history); } +.#{$fa-css-prefix}-home:before { content: fa-content($fa-var-home); } +.#{$fa-css-prefix}-hooli:before { content: fa-content($fa-var-hooli); } +.#{$fa-css-prefix}-hospital:before { content: fa-content($fa-var-hospital); } +.#{$fa-css-prefix}-hotjar:before { content: fa-content($fa-var-hotjar); } +.#{$fa-css-prefix}-hourglass:before { content: fa-content($fa-var-hourglass); } +.#{$fa-css-prefix}-hourglass-end:before { content: fa-content($fa-var-hourglass-end); } +.#{$fa-css-prefix}-hourglass-half:before { content: fa-content($fa-var-hourglass-half); } +.#{$fa-css-prefix}-hourglass-start:before { content: fa-content($fa-var-hourglass-start); } +.#{$fa-css-prefix}-houzz:before { content: fa-content($fa-var-houzz); } +.#{$fa-css-prefix}-html5:before { content: fa-content($fa-var-html5); } +.#{$fa-css-prefix}-hubspot:before { content: fa-content($fa-var-hubspot); } +.#{$fa-css-prefix}-i-cursor:before { content: fa-content($fa-var-i-cursor); } +.#{$fa-css-prefix}-id-badge:before { content: fa-content($fa-var-id-badge); } +.#{$fa-css-prefix}-id-card:before { content: fa-content($fa-var-id-card); } +.#{$fa-css-prefix}-image:before { content: fa-content($fa-var-image); } +.#{$fa-css-prefix}-images:before { content: fa-content($fa-var-images); } +.#{$fa-css-prefix}-imdb:before { content: fa-content($fa-var-imdb); } +.#{$fa-css-prefix}-inbox:before { content: fa-content($fa-var-inbox); } +.#{$fa-css-prefix}-indent:before { content: fa-content($fa-var-indent); } +.#{$fa-css-prefix}-industry:before { content: fa-content($fa-var-industry); } +.#{$fa-css-prefix}-info:before { content: fa-content($fa-var-info); } +.#{$fa-css-prefix}-info-circle:before { content: fa-content($fa-var-info-circle); } +.#{$fa-css-prefix}-instagram:before { content: fa-content($fa-var-instagram); } +.#{$fa-css-prefix}-internet-explorer:before { content: fa-content($fa-var-internet-explorer); } +.#{$fa-css-prefix}-ioxhost:before { content: fa-content($fa-var-ioxhost); } +.#{$fa-css-prefix}-italic:before { content: fa-content($fa-var-italic); } +.#{$fa-css-prefix}-itunes:before { content: fa-content($fa-var-itunes); } +.#{$fa-css-prefix}-itunes-note:before { content: fa-content($fa-var-itunes-note); } +.#{$fa-css-prefix}-jenkins:before { content: fa-content($fa-var-jenkins); } +.#{$fa-css-prefix}-joget:before { content: fa-content($fa-var-joget); } +.#{$fa-css-prefix}-joomla:before { content: fa-content($fa-var-joomla); } +.#{$fa-css-prefix}-js:before { content: fa-content($fa-var-js); } +.#{$fa-css-prefix}-js-square:before { content: fa-content($fa-var-js-square); } +.#{$fa-css-prefix}-jsfiddle:before { content: fa-content($fa-var-jsfiddle); } +.#{$fa-css-prefix}-key:before { content: fa-content($fa-var-key); } +.#{$fa-css-prefix}-keyboard:before { content: fa-content($fa-var-keyboard); } +.#{$fa-css-prefix}-keycdn:before { content: fa-content($fa-var-keycdn); } +.#{$fa-css-prefix}-kickstarter:before { content: fa-content($fa-var-kickstarter); } +.#{$fa-css-prefix}-kickstarter-k:before { content: fa-content($fa-var-kickstarter-k); } +.#{$fa-css-prefix}-korvue:before { content: fa-content($fa-var-korvue); } +.#{$fa-css-prefix}-language:before { content: fa-content($fa-var-language); } +.#{$fa-css-prefix}-laptop:before { content: fa-content($fa-var-laptop); } +.#{$fa-css-prefix}-laravel:before { content: fa-content($fa-var-laravel); } +.#{$fa-css-prefix}-lastfm:before { content: fa-content($fa-var-lastfm); } +.#{$fa-css-prefix}-lastfm-square:before { content: fa-content($fa-var-lastfm-square); } +.#{$fa-css-prefix}-leaf:before { content: fa-content($fa-var-leaf); } +.#{$fa-css-prefix}-leanpub:before { content: fa-content($fa-var-leanpub); } +.#{$fa-css-prefix}-lemon:before { content: fa-content($fa-var-lemon); } +.#{$fa-css-prefix}-less:before { content: fa-content($fa-var-less); } +.#{$fa-css-prefix}-level-down-alt:before { content: fa-content($fa-var-level-down-alt); } +.#{$fa-css-prefix}-level-up-alt:before { content: fa-content($fa-var-level-up-alt); } +.#{$fa-css-prefix}-life-ring:before { content: fa-content($fa-var-life-ring); } +.#{$fa-css-prefix}-lightbulb:before { content: fa-content($fa-var-lightbulb); } +.#{$fa-css-prefix}-line:before { content: fa-content($fa-var-line); } +.#{$fa-css-prefix}-link:before { content: fa-content($fa-var-link); } +.#{$fa-css-prefix}-linkedin:before { content: fa-content($fa-var-linkedin); } +.#{$fa-css-prefix}-linkedin-in:before { content: fa-content($fa-var-linkedin-in); } +.#{$fa-css-prefix}-linode:before { content: fa-content($fa-var-linode); } +.#{$fa-css-prefix}-linux:before { content: fa-content($fa-var-linux); } +.#{$fa-css-prefix}-lira-sign:before { content: fa-content($fa-var-lira-sign); } +.#{$fa-css-prefix}-list:before { content: fa-content($fa-var-list); } +.#{$fa-css-prefix}-list-alt:before { content: fa-content($fa-var-list-alt); } +.#{$fa-css-prefix}-list-ol:before { content: fa-content($fa-var-list-ol); } +.#{$fa-css-prefix}-list-ul:before { content: fa-content($fa-var-list-ul); } +.#{$fa-css-prefix}-location-arrow:before { content: fa-content($fa-var-location-arrow); } +.#{$fa-css-prefix}-lock:before { content: fa-content($fa-var-lock); } +.#{$fa-css-prefix}-lock-open:before { content: fa-content($fa-var-lock-open); } +.#{$fa-css-prefix}-long-arrow-alt-down:before { content: fa-content($fa-var-long-arrow-alt-down); } +.#{$fa-css-prefix}-long-arrow-alt-left:before { content: fa-content($fa-var-long-arrow-alt-left); } +.#{$fa-css-prefix}-long-arrow-alt-right:before { content: fa-content($fa-var-long-arrow-alt-right); } +.#{$fa-css-prefix}-long-arrow-alt-up:before { content: fa-content($fa-var-long-arrow-alt-up); } +.#{$fa-css-prefix}-low-vision:before { content: fa-content($fa-var-low-vision); } +.#{$fa-css-prefix}-lyft:before { content: fa-content($fa-var-lyft); } +.#{$fa-css-prefix}-magento:before { content: fa-content($fa-var-magento); } +.#{$fa-css-prefix}-magic:before { content: fa-content($fa-var-magic); } +.#{$fa-css-prefix}-magnet:before { content: fa-content($fa-var-magnet); } +.#{$fa-css-prefix}-male:before { content: fa-content($fa-var-male); } +.#{$fa-css-prefix}-map:before { content: fa-content($fa-var-map); } +.#{$fa-css-prefix}-map-marker:before { content: fa-content($fa-var-map-marker); } +.#{$fa-css-prefix}-map-marker-alt:before { content: fa-content($fa-var-map-marker-alt); } +.#{$fa-css-prefix}-map-pin:before { content: fa-content($fa-var-map-pin); } +.#{$fa-css-prefix}-map-signs:before { content: fa-content($fa-var-map-signs); } +.#{$fa-css-prefix}-mars:before { content: fa-content($fa-var-mars); } +.#{$fa-css-prefix}-mars-double:before { content: fa-content($fa-var-mars-double); } +.#{$fa-css-prefix}-mars-stroke:before { content: fa-content($fa-var-mars-stroke); } +.#{$fa-css-prefix}-mars-stroke-h:before { content: fa-content($fa-var-mars-stroke-h); } +.#{$fa-css-prefix}-mars-stroke-v:before { content: fa-content($fa-var-mars-stroke-v); } +.#{$fa-css-prefix}-maxcdn:before { content: fa-content($fa-var-maxcdn); } +.#{$fa-css-prefix}-medapps:before { content: fa-content($fa-var-medapps); } +.#{$fa-css-prefix}-medium:before { content: fa-content($fa-var-medium); } +.#{$fa-css-prefix}-medium-m:before { content: fa-content($fa-var-medium-m); } +.#{$fa-css-prefix}-medkit:before { content: fa-content($fa-var-medkit); } +.#{$fa-css-prefix}-medrt:before { content: fa-content($fa-var-medrt); } +.#{$fa-css-prefix}-meetup:before { content: fa-content($fa-var-meetup); } +.#{$fa-css-prefix}-meh:before { content: fa-content($fa-var-meh); } +.#{$fa-css-prefix}-mercury:before { content: fa-content($fa-var-mercury); } +.#{$fa-css-prefix}-microchip:before { content: fa-content($fa-var-microchip); } +.#{$fa-css-prefix}-microphone:before { content: fa-content($fa-var-microphone); } +.#{$fa-css-prefix}-microphone-slash:before { content: fa-content($fa-var-microphone-slash); } +.#{$fa-css-prefix}-microsoft:before { content: fa-content($fa-var-microsoft); } +.#{$fa-css-prefix}-minus:before { content: fa-content($fa-var-minus); } +.#{$fa-css-prefix}-minus-circle:before { content: fa-content($fa-var-minus-circle); } +.#{$fa-css-prefix}-minus-square:before { content: fa-content($fa-var-minus-square); } +.#{$fa-css-prefix}-mix:before { content: fa-content($fa-var-mix); } +.#{$fa-css-prefix}-mixcloud:before { content: fa-content($fa-var-mixcloud); } +.#{$fa-css-prefix}-mizuni:before { content: fa-content($fa-var-mizuni); } +.#{$fa-css-prefix}-mobile:before { content: fa-content($fa-var-mobile); } +.#{$fa-css-prefix}-mobile-alt:before { content: fa-content($fa-var-mobile-alt); } +.#{$fa-css-prefix}-modx:before { content: fa-content($fa-var-modx); } +.#{$fa-css-prefix}-monero:before { content: fa-content($fa-var-monero); } +.#{$fa-css-prefix}-money-bill-alt:before { content: fa-content($fa-var-money-bill-alt); } +.#{$fa-css-prefix}-moon:before { content: fa-content($fa-var-moon); } +.#{$fa-css-prefix}-motorcycle:before { content: fa-content($fa-var-motorcycle); } +.#{$fa-css-prefix}-mouse-pointer:before { content: fa-content($fa-var-mouse-pointer); } +.#{$fa-css-prefix}-music:before { content: fa-content($fa-var-music); } +.#{$fa-css-prefix}-napster:before { content: fa-content($fa-var-napster); } +.#{$fa-css-prefix}-neuter:before { content: fa-content($fa-var-neuter); } +.#{$fa-css-prefix}-newspaper:before { content: fa-content($fa-var-newspaper); } +.#{$fa-css-prefix}-nintendo-switch:before { content: fa-content($fa-var-nintendo-switch); } +.#{$fa-css-prefix}-node:before { content: fa-content($fa-var-node); } +.#{$fa-css-prefix}-node-js:before { content: fa-content($fa-var-node-js); } +.#{$fa-css-prefix}-npm:before { content: fa-content($fa-var-npm); } +.#{$fa-css-prefix}-ns8:before { content: fa-content($fa-var-ns8); } +.#{$fa-css-prefix}-nutritionix:before { content: fa-content($fa-var-nutritionix); } +.#{$fa-css-prefix}-object-group:before { content: fa-content($fa-var-object-group); } +.#{$fa-css-prefix}-object-ungroup:before { content: fa-content($fa-var-object-ungroup); } +.#{$fa-css-prefix}-odnoklassniki:before { content: fa-content($fa-var-odnoklassniki); } +.#{$fa-css-prefix}-odnoklassniki-square:before { content: fa-content($fa-var-odnoklassniki-square); } +.#{$fa-css-prefix}-opencart:before { content: fa-content($fa-var-opencart); } +.#{$fa-css-prefix}-openid:before { content: fa-content($fa-var-openid); } +.#{$fa-css-prefix}-opera:before { content: fa-content($fa-var-opera); } +.#{$fa-css-prefix}-optin-monster:before { content: fa-content($fa-var-optin-monster); } +.#{$fa-css-prefix}-osi:before { content: fa-content($fa-var-osi); } +.#{$fa-css-prefix}-outdent:before { content: fa-content($fa-var-outdent); } +.#{$fa-css-prefix}-page4:before { content: fa-content($fa-var-page4); } +.#{$fa-css-prefix}-pagelines:before { content: fa-content($fa-var-pagelines); } +.#{$fa-css-prefix}-paint-brush:before { content: fa-content($fa-var-paint-brush); } +.#{$fa-css-prefix}-palfed:before { content: fa-content($fa-var-palfed); } +.#{$fa-css-prefix}-paper-plane:before { content: fa-content($fa-var-paper-plane); } +.#{$fa-css-prefix}-paperclip:before { content: fa-content($fa-var-paperclip); } +.#{$fa-css-prefix}-paragraph:before { content: fa-content($fa-var-paragraph); } +.#{$fa-css-prefix}-paste:before { content: fa-content($fa-var-paste); } +.#{$fa-css-prefix}-patreon:before { content: fa-content($fa-var-patreon); } +.#{$fa-css-prefix}-pause:before { content: fa-content($fa-var-pause); } +.#{$fa-css-prefix}-pause-circle:before { content: fa-content($fa-var-pause-circle); } +.#{$fa-css-prefix}-paw:before { content: fa-content($fa-var-paw); } +.#{$fa-css-prefix}-paypal:before { content: fa-content($fa-var-paypal); } +.#{$fa-css-prefix}-pen-square:before { content: fa-content($fa-var-pen-square); } +.#{$fa-css-prefix}-pencil-alt:before { content: fa-content($fa-var-pencil-alt); } +.#{$fa-css-prefix}-percent:before { content: fa-content($fa-var-percent); } +.#{$fa-css-prefix}-periscope:before { content: fa-content($fa-var-periscope); } +.#{$fa-css-prefix}-phabricator:before { content: fa-content($fa-var-phabricator); } +.#{$fa-css-prefix}-phoenix-framework:before { content: fa-content($fa-var-phoenix-framework); } +.#{$fa-css-prefix}-phone:before { content: fa-content($fa-var-phone); } +.#{$fa-css-prefix}-phone-square:before { content: fa-content($fa-var-phone-square); } +.#{$fa-css-prefix}-phone-volume:before { content: fa-content($fa-var-phone-volume); } +.#{$fa-css-prefix}-pied-piper:before { content: fa-content($fa-var-pied-piper); } +.#{$fa-css-prefix}-pied-piper-alt:before { content: fa-content($fa-var-pied-piper-alt); } +.#{$fa-css-prefix}-pied-piper-pp:before { content: fa-content($fa-var-pied-piper-pp); } +.#{$fa-css-prefix}-pinterest:before { content: fa-content($fa-var-pinterest); } +.#{$fa-css-prefix}-pinterest-p:before { content: fa-content($fa-var-pinterest-p); } +.#{$fa-css-prefix}-pinterest-square:before { content: fa-content($fa-var-pinterest-square); } +.#{$fa-css-prefix}-plane:before { content: fa-content($fa-var-plane); } +.#{$fa-css-prefix}-play:before { content: fa-content($fa-var-play); } +.#{$fa-css-prefix}-play-circle:before { content: fa-content($fa-var-play-circle); } +.#{$fa-css-prefix}-playstation:before { content: fa-content($fa-var-playstation); } +.#{$fa-css-prefix}-plug:before { content: fa-content($fa-var-plug); } +.#{$fa-css-prefix}-plus:before { content: fa-content($fa-var-plus); } +.#{$fa-css-prefix}-plus-circle:before { content: fa-content($fa-var-plus-circle); } +.#{$fa-css-prefix}-plus-square:before { content: fa-content($fa-var-plus-square); } +.#{$fa-css-prefix}-podcast:before { content: fa-content($fa-var-podcast); } +.#{$fa-css-prefix}-pound-sign:before { content: fa-content($fa-var-pound-sign); } +.#{$fa-css-prefix}-power-off:before { content: fa-content($fa-var-power-off); } +.#{$fa-css-prefix}-print:before { content: fa-content($fa-var-print); } +.#{$fa-css-prefix}-product-hunt:before { content: fa-content($fa-var-product-hunt); } +.#{$fa-css-prefix}-pushed:before { content: fa-content($fa-var-pushed); } +.#{$fa-css-prefix}-puzzle-piece:before { content: fa-content($fa-var-puzzle-piece); } +.#{$fa-css-prefix}-python:before { content: fa-content($fa-var-python); } +.#{$fa-css-prefix}-qq:before { content: fa-content($fa-var-qq); } +.#{$fa-css-prefix}-qrcode:before { content: fa-content($fa-var-qrcode); } +.#{$fa-css-prefix}-question:before { content: fa-content($fa-var-question); } +.#{$fa-css-prefix}-question-circle:before { content: fa-content($fa-var-question-circle); } +.#{$fa-css-prefix}-quora:before { content: fa-content($fa-var-quora); } +.#{$fa-css-prefix}-quote-left:before { content: fa-content($fa-var-quote-left); } +.#{$fa-css-prefix}-quote-right:before { content: fa-content($fa-var-quote-right); } +.#{$fa-css-prefix}-random:before { content: fa-content($fa-var-random); } +.#{$fa-css-prefix}-ravelry:before { content: fa-content($fa-var-ravelry); } +.#{$fa-css-prefix}-react:before { content: fa-content($fa-var-react); } +.#{$fa-css-prefix}-rebel:before { content: fa-content($fa-var-rebel); } +.#{$fa-css-prefix}-recycle:before { content: fa-content($fa-var-recycle); } +.#{$fa-css-prefix}-red-river:before { content: fa-content($fa-var-red-river); } +.#{$fa-css-prefix}-reddit:before { content: fa-content($fa-var-reddit); } +.#{$fa-css-prefix}-reddit-alien:before { content: fa-content($fa-var-reddit-alien); } +.#{$fa-css-prefix}-reddit-square:before { content: fa-content($fa-var-reddit-square); } +.#{$fa-css-prefix}-redo:before { content: fa-content($fa-var-redo); } +.#{$fa-css-prefix}-redo-alt:before { content: fa-content($fa-var-redo-alt); } +.#{$fa-css-prefix}-registered:before { content: fa-content($fa-var-registered); } +.#{$fa-css-prefix}-rendact:before { content: fa-content($fa-var-rendact); } +.#{$fa-css-prefix}-renren:before { content: fa-content($fa-var-renren); } +.#{$fa-css-prefix}-reply:before { content: fa-content($fa-var-reply); } +.#{$fa-css-prefix}-reply-all:before { content: fa-content($fa-var-reply-all); } +.#{$fa-css-prefix}-replyd:before { content: fa-content($fa-var-replyd); } +.#{$fa-css-prefix}-resolving:before { content: fa-content($fa-var-resolving); } +.#{$fa-css-prefix}-retweet:before { content: fa-content($fa-var-retweet); } +.#{$fa-css-prefix}-road:before { content: fa-content($fa-var-road); } +.#{$fa-css-prefix}-rocket:before { content: fa-content($fa-var-rocket); } +.#{$fa-css-prefix}-rocketchat:before { content: fa-content($fa-var-rocketchat); } +.#{$fa-css-prefix}-rockrms:before { content: fa-content($fa-var-rockrms); } +.#{$fa-css-prefix}-rss:before { content: fa-content($fa-var-rss); } +.#{$fa-css-prefix}-rss-square:before { content: fa-content($fa-var-rss-square); } +.#{$fa-css-prefix}-ruble-sign:before { content: fa-content($fa-var-ruble-sign); } +.#{$fa-css-prefix}-rupee-sign:before { content: fa-content($fa-var-rupee-sign); } +.#{$fa-css-prefix}-safari:before { content: fa-content($fa-var-safari); } +.#{$fa-css-prefix}-sass:before { content: fa-content($fa-var-sass); } +.#{$fa-css-prefix}-save:before { content: fa-content($fa-var-save); } +.#{$fa-css-prefix}-schlix:before { content: fa-content($fa-var-schlix); } +.#{$fa-css-prefix}-scribd:before { content: fa-content($fa-var-scribd); } +.#{$fa-css-prefix}-search:before { content: fa-content($fa-var-search); } +.#{$fa-css-prefix}-search-minus:before { content: fa-content($fa-var-search-minus); } +.#{$fa-css-prefix}-search-plus:before { content: fa-content($fa-var-search-plus); } +.#{$fa-css-prefix}-searchengin:before { content: fa-content($fa-var-searchengin); } +.#{$fa-css-prefix}-sellcast:before { content: fa-content($fa-var-sellcast); } +.#{$fa-css-prefix}-sellsy:before { content: fa-content($fa-var-sellsy); } +.#{$fa-css-prefix}-server:before { content: fa-content($fa-var-server); } +.#{$fa-css-prefix}-servicestack:before { content: fa-content($fa-var-servicestack); } +.#{$fa-css-prefix}-share:before { content: fa-content($fa-var-share); } +.#{$fa-css-prefix}-share-alt:before { content: fa-content($fa-var-share-alt); } +.#{$fa-css-prefix}-share-alt-square:before { content: fa-content($fa-var-share-alt-square); } +.#{$fa-css-prefix}-share-square:before { content: fa-content($fa-var-share-square); } +.#{$fa-css-prefix}-shekel-sign:before { content: fa-content($fa-var-shekel-sign); } +.#{$fa-css-prefix}-shield-alt:before { content: fa-content($fa-var-shield-alt); } +.#{$fa-css-prefix}-ship:before { content: fa-content($fa-var-ship); } +.#{$fa-css-prefix}-shirtsinbulk:before { content: fa-content($fa-var-shirtsinbulk); } +.#{$fa-css-prefix}-shopping-bag:before { content: fa-content($fa-var-shopping-bag); } +.#{$fa-css-prefix}-shopping-basket:before { content: fa-content($fa-var-shopping-basket); } +.#{$fa-css-prefix}-shopping-cart:before { content: fa-content($fa-var-shopping-cart); } +.#{$fa-css-prefix}-shower:before { content: fa-content($fa-var-shower); } +.#{$fa-css-prefix}-sign-in-alt:before { content: fa-content($fa-var-sign-in-alt); } +.#{$fa-css-prefix}-sign-language:before { content: fa-content($fa-var-sign-language); } +.#{$fa-css-prefix}-sign-out-alt:before { content: fa-content($fa-var-sign-out-alt); } +.#{$fa-css-prefix}-signal:before { content: fa-content($fa-var-signal); } +.#{$fa-css-prefix}-simplybuilt:before { content: fa-content($fa-var-simplybuilt); } +.#{$fa-css-prefix}-sistrix:before { content: fa-content($fa-var-sistrix); } +.#{$fa-css-prefix}-sitemap:before { content: fa-content($fa-var-sitemap); } +.#{$fa-css-prefix}-skyatlas:before { content: fa-content($fa-var-skyatlas); } +.#{$fa-css-prefix}-skype:before { content: fa-content($fa-var-skype); } +.#{$fa-css-prefix}-slack:before { content: fa-content($fa-var-slack); } +.#{$fa-css-prefix}-slack-hash:before { content: fa-content($fa-var-slack-hash); } +.#{$fa-css-prefix}-sliders-h:before { content: fa-content($fa-var-sliders-h); } +.#{$fa-css-prefix}-slideshare:before { content: fa-content($fa-var-slideshare); } +.#{$fa-css-prefix}-smile:before { content: fa-content($fa-var-smile); } +.#{$fa-css-prefix}-snapchat:before { content: fa-content($fa-var-snapchat); } +.#{$fa-css-prefix}-snapchat-ghost:before { content: fa-content($fa-var-snapchat-ghost); } +.#{$fa-css-prefix}-snapchat-square:before { content: fa-content($fa-var-snapchat-square); } +.#{$fa-css-prefix}-snowflake:before { content: fa-content($fa-var-snowflake); } +.#{$fa-css-prefix}-sort:before { content: fa-content($fa-var-sort); } +.#{$fa-css-prefix}-sort-alpha-down:before { content: fa-content($fa-var-sort-alpha-down); } +.#{$fa-css-prefix}-sort-alpha-up:before { content: fa-content($fa-var-sort-alpha-up); } +.#{$fa-css-prefix}-sort-amount-down:before { content: fa-content($fa-var-sort-amount-down); } +.#{$fa-css-prefix}-sort-amount-up:before { content: fa-content($fa-var-sort-amount-up); } +.#{$fa-css-prefix}-sort-down:before { content: fa-content($fa-var-sort-down); } +.#{$fa-css-prefix}-sort-numeric-down:before { content: fa-content($fa-var-sort-numeric-down); } +.#{$fa-css-prefix}-sort-numeric-up:before { content: fa-content($fa-var-sort-numeric-up); } +.#{$fa-css-prefix}-sort-up:before { content: fa-content($fa-var-sort-up); } +.#{$fa-css-prefix}-soundcloud:before { content: fa-content($fa-var-soundcloud); } +.#{$fa-css-prefix}-space-shuttle:before { content: fa-content($fa-var-space-shuttle); } +.#{$fa-css-prefix}-speakap:before { content: fa-content($fa-var-speakap); } +.#{$fa-css-prefix}-spinner:before { content: fa-content($fa-var-spinner); } +.#{$fa-css-prefix}-spotify:before { content: fa-content($fa-var-spotify); } +.#{$fa-css-prefix}-square:before { content: fa-content($fa-var-square); } +.#{$fa-css-prefix}-stack-exchange:before { content: fa-content($fa-var-stack-exchange); } +.#{$fa-css-prefix}-stack-overflow:before { content: fa-content($fa-var-stack-overflow); } +.#{$fa-css-prefix}-star:before { content: fa-content($fa-var-star); } +.#{$fa-css-prefix}-star-half:before { content: fa-content($fa-var-star-half); } +.#{$fa-css-prefix}-staylinked:before { content: fa-content($fa-var-staylinked); } +.#{$fa-css-prefix}-steam:before { content: fa-content($fa-var-steam); } +.#{$fa-css-prefix}-steam-square:before { content: fa-content($fa-var-steam-square); } +.#{$fa-css-prefix}-steam-symbol:before { content: fa-content($fa-var-steam-symbol); } +.#{$fa-css-prefix}-step-backward:before { content: fa-content($fa-var-step-backward); } +.#{$fa-css-prefix}-step-forward:before { content: fa-content($fa-var-step-forward); } +.#{$fa-css-prefix}-stethoscope:before { content: fa-content($fa-var-stethoscope); } +.#{$fa-css-prefix}-sticker-mule:before { content: fa-content($fa-var-sticker-mule); } +.#{$fa-css-prefix}-sticky-note:before { content: fa-content($fa-var-sticky-note); } +.#{$fa-css-prefix}-stop:before { content: fa-content($fa-var-stop); } +.#{$fa-css-prefix}-stop-circle:before { content: fa-content($fa-var-stop-circle); } +.#{$fa-css-prefix}-stopwatch:before { content: fa-content($fa-var-stopwatch); } +.#{$fa-css-prefix}-strava:before { content: fa-content($fa-var-strava); } +.#{$fa-css-prefix}-street-view:before { content: fa-content($fa-var-street-view); } +.#{$fa-css-prefix}-strikethrough:before { content: fa-content($fa-var-strikethrough); } +.#{$fa-css-prefix}-stripe:before { content: fa-content($fa-var-stripe); } +.#{$fa-css-prefix}-stripe-s:before { content: fa-content($fa-var-stripe-s); } +.#{$fa-css-prefix}-studiovinari:before { content: fa-content($fa-var-studiovinari); } +.#{$fa-css-prefix}-stumbleupon:before { content: fa-content($fa-var-stumbleupon); } +.#{$fa-css-prefix}-stumbleupon-circle:before { content: fa-content($fa-var-stumbleupon-circle); } +.#{$fa-css-prefix}-subscript:before { content: fa-content($fa-var-subscript); } +.#{$fa-css-prefix}-subway:before { content: fa-content($fa-var-subway); } +.#{$fa-css-prefix}-suitcase:before { content: fa-content($fa-var-suitcase); } +.#{$fa-css-prefix}-sun:before { content: fa-content($fa-var-sun); } +.#{$fa-css-prefix}-superpowers:before { content: fa-content($fa-var-superpowers); } +.#{$fa-css-prefix}-superscript:before { content: fa-content($fa-var-superscript); } +.#{$fa-css-prefix}-supple:before { content: fa-content($fa-var-supple); } +.#{$fa-css-prefix}-sync:before { content: fa-content($fa-var-sync); } +.#{$fa-css-prefix}-sync-alt:before { content: fa-content($fa-var-sync-alt); } +.#{$fa-css-prefix}-table:before { content: fa-content($fa-var-table); } +.#{$fa-css-prefix}-tablet:before { content: fa-content($fa-var-tablet); } +.#{$fa-css-prefix}-tablet-alt:before { content: fa-content($fa-var-tablet-alt); } +.#{$fa-css-prefix}-tachometer-alt:before { content: fa-content($fa-var-tachometer-alt); } +.#{$fa-css-prefix}-tag:before { content: fa-content($fa-var-tag); } +.#{$fa-css-prefix}-tags:before { content: fa-content($fa-var-tags); } +.#{$fa-css-prefix}-tasks:before { content: fa-content($fa-var-tasks); } +.#{$fa-css-prefix}-taxi:before { content: fa-content($fa-var-taxi); } +.#{$fa-css-prefix}-telegram:before { content: fa-content($fa-var-telegram); } +.#{$fa-css-prefix}-telegram-plane:before { content: fa-content($fa-var-telegram-plane); } +.#{$fa-css-prefix}-tencent-weibo:before { content: fa-content($fa-var-tencent-weibo); } +.#{$fa-css-prefix}-terminal:before { content: fa-content($fa-var-terminal); } +.#{$fa-css-prefix}-text-height:before { content: fa-content($fa-var-text-height); } +.#{$fa-css-prefix}-text-width:before { content: fa-content($fa-var-text-width); } +.#{$fa-css-prefix}-th:before { content: fa-content($fa-var-th); } +.#{$fa-css-prefix}-th-large:before { content: fa-content($fa-var-th-large); } +.#{$fa-css-prefix}-th-list:before { content: fa-content($fa-var-th-list); } +.#{$fa-css-prefix}-themeisle:before { content: fa-content($fa-var-themeisle); } +.#{$fa-css-prefix}-thermometer-empty:before { content: fa-content($fa-var-thermometer-empty); } +.#{$fa-css-prefix}-thermometer-full:before { content: fa-content($fa-var-thermometer-full); } +.#{$fa-css-prefix}-thermometer-half:before { content: fa-content($fa-var-thermometer-half); } +.#{$fa-css-prefix}-thermometer-quarter:before { content: fa-content($fa-var-thermometer-quarter); } +.#{$fa-css-prefix}-thermometer-three-quarters:before { content: fa-content($fa-var-thermometer-three-quarters); } +.#{$fa-css-prefix}-thumbs-down:before { content: fa-content($fa-var-thumbs-down); } +.#{$fa-css-prefix}-thumbs-up:before { content: fa-content($fa-var-thumbs-up); } +.#{$fa-css-prefix}-thumbtack:before { content: fa-content($fa-var-thumbtack); } +.#{$fa-css-prefix}-ticket-alt:before { content: fa-content($fa-var-ticket-alt); } +.#{$fa-css-prefix}-times:before { content: fa-content($fa-var-times); } +.#{$fa-css-prefix}-times-circle:before { content: fa-content($fa-var-times-circle); } +.#{$fa-css-prefix}-tint:before { content: fa-content($fa-var-tint); } +.#{$fa-css-prefix}-toggle-off:before { content: fa-content($fa-var-toggle-off); } +.#{$fa-css-prefix}-toggle-on:before { content: fa-content($fa-var-toggle-on); } +.#{$fa-css-prefix}-trademark:before { content: fa-content($fa-var-trademark); } +.#{$fa-css-prefix}-train:before { content: fa-content($fa-var-train); } +.#{$fa-css-prefix}-transgender:before { content: fa-content($fa-var-transgender); } +.#{$fa-css-prefix}-transgender-alt:before { content: fa-content($fa-var-transgender-alt); } +.#{$fa-css-prefix}-trash:before { content: fa-content($fa-var-trash); } +.#{$fa-css-prefix}-trash-alt:before { content: fa-content($fa-var-trash-alt); } +.#{$fa-css-prefix}-tree:before { content: fa-content($fa-var-tree); } +.#{$fa-css-prefix}-trello:before { content: fa-content($fa-var-trello); } +.#{$fa-css-prefix}-tripadvisor:before { content: fa-content($fa-var-tripadvisor); } +.#{$fa-css-prefix}-trophy:before { content: fa-content($fa-var-trophy); } +.#{$fa-css-prefix}-truck:before { content: fa-content($fa-var-truck); } +.#{$fa-css-prefix}-tty:before { content: fa-content($fa-var-tty); } +.#{$fa-css-prefix}-tumblr:before { content: fa-content($fa-var-tumblr); } +.#{$fa-css-prefix}-tumblr-square:before { content: fa-content($fa-var-tumblr-square); } +.#{$fa-css-prefix}-tv:before { content: fa-content($fa-var-tv); } +.#{$fa-css-prefix}-twitch:before { content: fa-content($fa-var-twitch); } +.#{$fa-css-prefix}-twitter:before { content: fa-content($fa-var-twitter); } +.#{$fa-css-prefix}-twitter-square:before { content: fa-content($fa-var-twitter-square); } +.#{$fa-css-prefix}-typo3:before { content: fa-content($fa-var-typo3); } +.#{$fa-css-prefix}-uber:before { content: fa-content($fa-var-uber); } +.#{$fa-css-prefix}-uikit:before { content: fa-content($fa-var-uikit); } +.#{$fa-css-prefix}-umbrella:before { content: fa-content($fa-var-umbrella); } +.#{$fa-css-prefix}-underline:before { content: fa-content($fa-var-underline); } +.#{$fa-css-prefix}-undo:before { content: fa-content($fa-var-undo); } +.#{$fa-css-prefix}-undo-alt:before { content: fa-content($fa-var-undo-alt); } +.#{$fa-css-prefix}-uniregistry:before { content: fa-content($fa-var-uniregistry); } +.#{$fa-css-prefix}-universal-access:before { content: fa-content($fa-var-universal-access); } +.#{$fa-css-prefix}-university:before { content: fa-content($fa-var-university); } +.#{$fa-css-prefix}-unlink:before { content: fa-content($fa-var-unlink); } +.#{$fa-css-prefix}-unlock:before { content: fa-content($fa-var-unlock); } +.#{$fa-css-prefix}-unlock-alt:before { content: fa-content($fa-var-unlock-alt); } +.#{$fa-css-prefix}-untappd:before { content: fa-content($fa-var-untappd); } +.#{$fa-css-prefix}-upload:before { content: fa-content($fa-var-upload); } +.#{$fa-css-prefix}-usb:before { content: fa-content($fa-var-usb); } +.#{$fa-css-prefix}-user:before { content: fa-content($fa-var-user); } +.#{$fa-css-prefix}-user-circle:before { content: fa-content($fa-var-user-circle); } +.#{$fa-css-prefix}-user-md:before { content: fa-content($fa-var-user-md); } +.#{$fa-css-prefix}-user-plus:before { content: fa-content($fa-var-user-plus); } +.#{$fa-css-prefix}-user-secret:before { content: fa-content($fa-var-user-secret); } +.#{$fa-css-prefix}-user-times:before { content: fa-content($fa-var-user-times); } +.#{$fa-css-prefix}-users:before { content: fa-content($fa-var-users); } +.#{$fa-css-prefix}-ussunnah:before { content: fa-content($fa-var-ussunnah); } +.#{$fa-css-prefix}-utensil-spoon:before { content: fa-content($fa-var-utensil-spoon); } +.#{$fa-css-prefix}-utensils:before { content: fa-content($fa-var-utensils); } +.#{$fa-css-prefix}-vaadin:before { content: fa-content($fa-var-vaadin); } +.#{$fa-css-prefix}-venus:before { content: fa-content($fa-var-venus); } +.#{$fa-css-prefix}-venus-double:before { content: fa-content($fa-var-venus-double); } +.#{$fa-css-prefix}-venus-mars:before { content: fa-content($fa-var-venus-mars); } +.#{$fa-css-prefix}-viacoin:before { content: fa-content($fa-var-viacoin); } +.#{$fa-css-prefix}-viadeo:before { content: fa-content($fa-var-viadeo); } +.#{$fa-css-prefix}-viadeo-square:before { content: fa-content($fa-var-viadeo-square); } +.#{$fa-css-prefix}-viber:before { content: fa-content($fa-var-viber); } +.#{$fa-css-prefix}-video:before { content: fa-content($fa-var-video); } +.#{$fa-css-prefix}-vimeo:before { content: fa-content($fa-var-vimeo); } +.#{$fa-css-prefix}-vimeo-square:before { content: fa-content($fa-var-vimeo-square); } +.#{$fa-css-prefix}-vimeo-v:before { content: fa-content($fa-var-vimeo-v); } +.#{$fa-css-prefix}-vine:before { content: fa-content($fa-var-vine); } +.#{$fa-css-prefix}-vk:before { content: fa-content($fa-var-vk); } +.#{$fa-css-prefix}-vnv:before { content: fa-content($fa-var-vnv); } +.#{$fa-css-prefix}-volume-down:before { content: fa-content($fa-var-volume-down); } +.#{$fa-css-prefix}-volume-off:before { content: fa-content($fa-var-volume-off); } +.#{$fa-css-prefix}-volume-up:before { content: fa-content($fa-var-volume-up); } +.#{$fa-css-prefix}-vuejs:before { content: fa-content($fa-var-vuejs); } +.#{$fa-css-prefix}-weibo:before { content: fa-content($fa-var-weibo); } +.#{$fa-css-prefix}-weixin:before { content: fa-content($fa-var-weixin); } +.#{$fa-css-prefix}-whatsapp:before { content: fa-content($fa-var-whatsapp); } +.#{$fa-css-prefix}-whatsapp-square:before { content: fa-content($fa-var-whatsapp-square); } +.#{$fa-css-prefix}-wheelchair:before { content: fa-content($fa-var-wheelchair); } +.#{$fa-css-prefix}-whmcs:before { content: fa-content($fa-var-whmcs); } +.#{$fa-css-prefix}-wifi:before { content: fa-content($fa-var-wifi); } +.#{$fa-css-prefix}-wikipedia-w:before { content: fa-content($fa-var-wikipedia-w); } +.#{$fa-css-prefix}-window-close:before { content: fa-content($fa-var-window-close); } +.#{$fa-css-prefix}-window-maximize:before { content: fa-content($fa-var-window-maximize); } +.#{$fa-css-prefix}-window-minimize:before { content: fa-content($fa-var-window-minimize); } +.#{$fa-css-prefix}-window-restore:before { content: fa-content($fa-var-window-restore); } +.#{$fa-css-prefix}-windows:before { content: fa-content($fa-var-windows); } +.#{$fa-css-prefix}-won-sign:before { content: fa-content($fa-var-won-sign); } +.#{$fa-css-prefix}-wordpress:before { content: fa-content($fa-var-wordpress); } +.#{$fa-css-prefix}-wordpress-simple:before { content: fa-content($fa-var-wordpress-simple); } +.#{$fa-css-prefix}-wpbeginner:before { content: fa-content($fa-var-wpbeginner); } +.#{$fa-css-prefix}-wpexplorer:before { content: fa-content($fa-var-wpexplorer); } +.#{$fa-css-prefix}-wpforms:before { content: fa-content($fa-var-wpforms); } +.#{$fa-css-prefix}-wrench:before { content: fa-content($fa-var-wrench); } +.#{$fa-css-prefix}-xbox:before { content: fa-content($fa-var-xbox); } +.#{$fa-css-prefix}-xing:before { content: fa-content($fa-var-xing); } +.#{$fa-css-prefix}-xing-square:before { content: fa-content($fa-var-xing-square); } +.#{$fa-css-prefix}-y-combinator:before { content: fa-content($fa-var-y-combinator); } +.#{$fa-css-prefix}-yahoo:before { content: fa-content($fa-var-yahoo); } +.#{$fa-css-prefix}-yandex:before { content: fa-content($fa-var-yandex); } +.#{$fa-css-prefix}-yandex-international:before { content: fa-content($fa-var-yandex-international); } +.#{$fa-css-prefix}-yelp:before { content: fa-content($fa-var-yelp); } +.#{$fa-css-prefix}-yen-sign:before { content: fa-content($fa-var-yen-sign); } +.#{$fa-css-prefix}-yoast:before { content: fa-content($fa-var-yoast); } +.#{$fa-css-prefix}-youtube:before { content: fa-content($fa-var-youtube); } diff --git a/docs/src/assets/fa/_larger.scss b/docs/src/assets/fa/_larger.scss new file mode 100644 index 00000000..27c2ad5f --- /dev/null +++ b/docs/src/assets/fa/_larger.scss @@ -0,0 +1,23 @@ +// Icon Sizes +// ------------------------- + +// makes the font 33% larger relative to the icon container +.#{$fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -.0667em; +} + +.#{$fa-css-prefix}-xs { + font-size: .75em; +} + +.#{$fa-css-prefix}-sm { + font-size: .875em; +} + +@for $i from 1 through 10 { + .#{$fa-css-prefix}-#{$i}x { + font-size: $i * 1em; + } +} diff --git a/docs/src/assets/fa/_list.scss b/docs/src/assets/fa/_list.scss new file mode 100644 index 00000000..8ebf3333 --- /dev/null +++ b/docs/src/assets/fa/_list.scss @@ -0,0 +1,18 @@ +// List Icons +// ------------------------- + +.#{$fa-css-prefix}-ul { + list-style-type: none; + margin-left: $fa-li-width * 5/4; + padding-left: 0; + + > li { position: relative; } +} + +.#{$fa-css-prefix}-li { + left: -$fa-li-width; + position: absolute; + text-align: center; + width: $fa-li-width; + line-height: inherit; +} diff --git a/docs/src/assets/fa/_mixins.scss b/docs/src/assets/fa/_mixins.scss new file mode 100644 index 00000000..06e549b6 --- /dev/null +++ b/docs/src/assets/fa/_mixins.scss @@ -0,0 +1,57 @@ +// Mixins +// -------------------------- + +@mixin fa-icon { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + display: inline-block; + font-style: normal; + font-variant: normal; + font-weight: normal; + line-height: 1; + vertical-align: -15%; +} + +@mixin fa-icon-rotate($degrees, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; + transform: rotate($degrees); +} + +@mixin fa-icon-flip($horiz, $vert, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; + transform: scale($horiz, $vert); +} + + +// Only display content to screen readers. A la Bootstrap 4. +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +@mixin sr-only { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// +// Credit: HTML5 Boilerplate + +@mixin sr-only-focusable { + &:active, + &:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; + } +} diff --git a/docs/src/assets/fa/_rotated-flipped.scss b/docs/src/assets/fa/_rotated-flipped.scss new file mode 100644 index 00000000..995bc4cc --- /dev/null +++ b/docs/src/assets/fa/_rotated-flipped.scss @@ -0,0 +1,23 @@ +// Rotated & Flipped Icons +// ------------------------- + +.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } +.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } +.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } + +.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } +.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } +.#{$fa-css-prefix}-flip-horizontal.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(-1, -1, 2); } + +// Hook for IE8-9 +// ------------------------- + +:root { + .#{$fa-css-prefix}-rotate-90, + .#{$fa-css-prefix}-rotate-180, + .#{$fa-css-prefix}-rotate-270, + .#{$fa-css-prefix}-flip-horizontal, + .#{$fa-css-prefix}-flip-vertical { + filter: none; + } +} diff --git a/docs/src/assets/fa/_screen-reader.scss b/docs/src/assets/fa/_screen-reader.scss new file mode 100644 index 00000000..5d0ab262 --- /dev/null +++ b/docs/src/assets/fa/_screen-reader.scss @@ -0,0 +1,5 @@ +// Screen Readers +// ------------------------- + +.sr-only { @include sr-only; } +.sr-only-focusable { @include sr-only-focusable; } diff --git a/docs/src/assets/fa/_stacked.scss b/docs/src/assets/fa/_stacked.scss new file mode 100644 index 00000000..6c09d84c --- /dev/null +++ b/docs/src/assets/fa/_stacked.scss @@ -0,0 +1,31 @@ +// Stacked Icons +// ------------------------- + +.#{$fa-css-prefix}-stack { + display: inline-block; + height: 2em; + line-height: 2em; + position: relative; + vertical-align: middle; + width: 2em; +} + +.#{$fa-css-prefix}-stack-1x, +.#{$fa-css-prefix}-stack-2x { + left: 0; + position: absolute; + text-align: center; + width: 100%; +} + +.#{$fa-css-prefix}-stack-1x { + line-height: inherit; +} + +.#{$fa-css-prefix}-stack-2x { + font-size: 2em; +} + +.#{$fa-css-prefix}-inverse { + color: $fa-inverse; +} diff --git a/docs/src/assets/fa/_variables.scss b/docs/src/assets/fa/_variables.scss new file mode 100644 index 00000000..d2c4d474 --- /dev/null +++ b/docs/src/assets/fa/_variables.scss @@ -0,0 +1,805 @@ +// Variables +// -------------------------- + +$fa-font-path: "../webfonts" !default; +$fa-font-size-base: 16px !default; +$fa-css-prefix: fa !default; +$fa-version: "5.0.2" !default; +$fa-border-color: #eee !default; +$fa-inverse: #fff !default; +$fa-li-width: 2em !default; + +// Convenience function used to set content property +@function fa-content($fa-var) { + @return unquote("\"#{ $fa-var }\""); +} + +$fa-var-500px: \f26e; +$fa-var-accessible-icon: \f368; +$fa-var-accusoft: \f369; +$fa-var-address-book: \f2b9; +$fa-var-address-card: \f2bb; +$fa-var-adjust: \f042; +$fa-var-adn: \f170; +$fa-var-adversal: \f36a; +$fa-var-affiliatetheme: \f36b; +$fa-var-algolia: \f36c; +$fa-var-align-center: \f037; +$fa-var-align-justify: \f039; +$fa-var-align-left: \f036; +$fa-var-align-right: \f038; +$fa-var-amazon: \f270; +$fa-var-amazon-pay: \f42c; +$fa-var-ambulance: \f0f9; +$fa-var-american-sign-language-interpreting: \f2a3; +$fa-var-amilia: \f36d; +$fa-var-anchor: \f13d; +$fa-var-android: \f17b; +$fa-var-angellist: \f209; +$fa-var-angle-double-down: \f103; +$fa-var-angle-double-left: \f100; +$fa-var-angle-double-right: \f101; +$fa-var-angle-double-up: \f102; +$fa-var-angle-down: \f107; +$fa-var-angle-left: \f104; +$fa-var-angle-right: \f105; +$fa-var-angle-up: \f106; +$fa-var-angrycreative: \f36e; +$fa-var-angular: \f420; +$fa-var-app-store: \f36f; +$fa-var-app-store-ios: \f370; +$fa-var-apper: \f371; +$fa-var-apple: \f179; +$fa-var-apple-pay: \f415; +$fa-var-archive: \f187; +$fa-var-arrow-alt-circle-down: \f358; +$fa-var-arrow-alt-circle-left: \f359; +$fa-var-arrow-alt-circle-right: \f35a; +$fa-var-arrow-alt-circle-up: \f35b; +$fa-var-arrow-circle-down: \f0ab; +$fa-var-arrow-circle-left: \f0a8; +$fa-var-arrow-circle-right: \f0a9; +$fa-var-arrow-circle-up: \f0aa; +$fa-var-arrow-down: \f063; +$fa-var-arrow-left: \f060; +$fa-var-arrow-right: \f061; +$fa-var-arrow-up: \f062; +$fa-var-arrows-alt: \f0b2; +$fa-var-arrows-alt-h: \f337; +$fa-var-arrows-alt-v: \f338; +$fa-var-assistive-listening-systems: \f2a2; +$fa-var-asterisk: \f069; +$fa-var-asymmetrik: \f372; +$fa-var-at: \f1fa; +$fa-var-audible: \f373; +$fa-var-audio-description: \f29e; +$fa-var-autoprefixer: \f41c; +$fa-var-avianex: \f374; +$fa-var-aviato: \f421; +$fa-var-aws: \f375; +$fa-var-backward: \f04a; +$fa-var-balance-scale: \f24e; +$fa-var-ban: \f05e; +$fa-var-bandcamp: \f2d5; +$fa-var-barcode: \f02a; +$fa-var-bars: \f0c9; +$fa-var-bath: \f2cd; +$fa-var-battery-empty: \f244; +$fa-var-battery-full: \f240; +$fa-var-battery-half: \f242; +$fa-var-battery-quarter: \f243; +$fa-var-battery-three-quarters: \f241; +$fa-var-bed: \f236; +$fa-var-beer: \f0fc; +$fa-var-behance: \f1b4; +$fa-var-behance-square: \f1b5; +$fa-var-bell: \f0f3; +$fa-var-bell-slash: \f1f6; +$fa-var-bicycle: \f206; +$fa-var-bimobject: \f378; +$fa-var-binoculars: \f1e5; +$fa-var-birthday-cake: \f1fd; +$fa-var-bitbucket: \f171; +$fa-var-bitcoin: \f379; +$fa-var-bity: \f37a; +$fa-var-black-tie: \f27e; +$fa-var-blackberry: \f37b; +$fa-var-blind: \f29d; +$fa-var-blogger: \f37c; +$fa-var-blogger-b: \f37d; +$fa-var-bluetooth: \f293; +$fa-var-bluetooth-b: \f294; +$fa-var-bold: \f032; +$fa-var-bolt: \f0e7; +$fa-var-bomb: \f1e2; +$fa-var-book: \f02d; +$fa-var-bookmark: \f02e; +$fa-var-braille: \f2a1; +$fa-var-briefcase: \f0b1; +$fa-var-btc: \f15a; +$fa-var-bug: \f188; +$fa-var-building: \f1ad; +$fa-var-bullhorn: \f0a1; +$fa-var-bullseye: \f140; +$fa-var-buromobelexperte: \f37f; +$fa-var-bus: \f207; +$fa-var-buysellads: \f20d; +$fa-var-calculator: \f1ec; +$fa-var-calendar: \f133; +$fa-var-calendar-alt: \f073; +$fa-var-calendar-check: \f274; +$fa-var-calendar-minus: \f272; +$fa-var-calendar-plus: \f271; +$fa-var-calendar-times: \f273; +$fa-var-camera: \f030; +$fa-var-camera-retro: \f083; +$fa-var-car: \f1b9; +$fa-var-caret-down: \f0d7; +$fa-var-caret-left: \f0d9; +$fa-var-caret-right: \f0da; +$fa-var-caret-square-down: \f150; +$fa-var-caret-square-left: \f191; +$fa-var-caret-square-right: \f152; +$fa-var-caret-square-up: \f151; +$fa-var-caret-up: \f0d8; +$fa-var-cart-arrow-down: \f218; +$fa-var-cart-plus: \f217; +$fa-var-cc-amazon-pay: \f42d; +$fa-var-cc-amex: \f1f3; +$fa-var-cc-apple-pay: \f416; +$fa-var-cc-diners-club: \f24c; +$fa-var-cc-discover: \f1f2; +$fa-var-cc-jcb: \f24b; +$fa-var-cc-mastercard: \f1f1; +$fa-var-cc-paypal: \f1f4; +$fa-var-cc-stripe: \f1f5; +$fa-var-cc-visa: \f1f0; +$fa-var-centercode: \f380; +$fa-var-certificate: \f0a3; +$fa-var-chart-area: \f1fe; +$fa-var-chart-bar: \f080; +$fa-var-chart-line: \f201; +$fa-var-chart-pie: \f200; +$fa-var-check: \f00c; +$fa-var-check-circle: \f058; +$fa-var-check-square: \f14a; +$fa-var-chevron-circle-down: \f13a; +$fa-var-chevron-circle-left: \f137; +$fa-var-chevron-circle-right: \f138; +$fa-var-chevron-circle-up: \f139; +$fa-var-chevron-down: \f078; +$fa-var-chevron-left: \f053; +$fa-var-chevron-right: \f054; +$fa-var-chevron-up: \f077; +$fa-var-child: \f1ae; +$fa-var-chrome: \f268; +$fa-var-circle: \f111; +$fa-var-circle-notch: \f1ce; +$fa-var-clipboard: \f328; +$fa-var-clock: \f017; +$fa-var-clone: \f24d; +$fa-var-closed-captioning: \f20a; +$fa-var-cloud: \f0c2; +$fa-var-cloud-download-alt: \f381; +$fa-var-cloud-upload-alt: \f382; +$fa-var-cloudscale: \f383; +$fa-var-cloudsmith: \f384; +$fa-var-cloudversify: \f385; +$fa-var-code: \f121; +$fa-var-code-branch: \f126; +$fa-var-codepen: \f1cb; +$fa-var-codiepie: \f284; +$fa-var-coffee: \f0f4; +$fa-var-cog: \f013; +$fa-var-cogs: \f085; +$fa-var-columns: \f0db; +$fa-var-comment: \f075; +$fa-var-comment-alt: \f27a; +$fa-var-comments: \f086; +$fa-var-compass: \f14e; +$fa-var-compress: \f066; +$fa-var-connectdevelop: \f20e; +$fa-var-contao: \f26d; +$fa-var-copy: \f0c5; +$fa-var-copyright: \f1f9; +$fa-var-cpanel: \f388; +$fa-var-creative-commons: \f25e; +$fa-var-credit-card: \f09d; +$fa-var-crop: \f125; +$fa-var-crosshairs: \f05b; +$fa-var-css3: \f13c; +$fa-var-css3-alt: \f38b; +$fa-var-cube: \f1b2; +$fa-var-cubes: \f1b3; +$fa-var-cut: \f0c4; +$fa-var-cuttlefish: \f38c; +$fa-var-d-and-d: \f38d; +$fa-var-dashcube: \f210; +$fa-var-database: \f1c0; +$fa-var-deaf: \f2a4; +$fa-var-delicious: \f1a5; +$fa-var-deploydog: \f38e; +$fa-var-deskpro: \f38f; +$fa-var-desktop: \f108; +$fa-var-deviantart: \f1bd; +$fa-var-digg: \f1a6; +$fa-var-digital-ocean: \f391; +$fa-var-discord: \f392; +$fa-var-discourse: \f393; +$fa-var-dochub: \f394; +$fa-var-docker: \f395; +$fa-var-dollar-sign: \f155; +$fa-var-dot-circle: \f192; +$fa-var-download: \f019; +$fa-var-draft2digital: \f396; +$fa-var-dribbble: \f17d; +$fa-var-dribbble-square: \f397; +$fa-var-dropbox: \f16b; +$fa-var-drupal: \f1a9; +$fa-var-dyalog: \f399; +$fa-var-earlybirds: \f39a; +$fa-var-edge: \f282; +$fa-var-edit: \f044; +$fa-var-eject: \f052; +$fa-var-ellipsis-h: \f141; +$fa-var-ellipsis-v: \f142; +$fa-var-ember: \f423; +$fa-var-empire: \f1d1; +$fa-var-envelope: \f0e0; +$fa-var-envelope-open: \f2b6; +$fa-var-envelope-square: \f199; +$fa-var-envira: \f299; +$fa-var-eraser: \f12d; +$fa-var-erlang: \f39d; +$fa-var-ethereum: \f42e; +$fa-var-etsy: \f2d7; +$fa-var-euro-sign: \f153; +$fa-var-exchange-alt: \f362; +$fa-var-exclamation: \f12a; +$fa-var-exclamation-circle: \f06a; +$fa-var-exclamation-triangle: \f071; +$fa-var-expand: \f065; +$fa-var-expand-arrows-alt: \f31e; +$fa-var-expeditedssl: \f23e; +$fa-var-external-link-alt: \f35d; +$fa-var-external-link-square-alt: \f360; +$fa-var-eye: \f06e; +$fa-var-eye-dropper: \f1fb; +$fa-var-eye-slash: \f070; +$fa-var-facebook: \f09a; +$fa-var-facebook-f: \f39e; +$fa-var-facebook-messenger: \f39f; +$fa-var-facebook-square: \f082; +$fa-var-fast-backward: \f049; +$fa-var-fast-forward: \f050; +$fa-var-fax: \f1ac; +$fa-var-female: \f182; +$fa-var-fighter-jet: \f0fb; +$fa-var-file: \f15b; +$fa-var-file-alt: \f15c; +$fa-var-file-archive: \f1c6; +$fa-var-file-audio: \f1c7; +$fa-var-file-code: \f1c9; +$fa-var-file-excel: \f1c3; +$fa-var-file-image: \f1c5; +$fa-var-file-pdf: \f1c1; +$fa-var-file-powerpoint: \f1c4; +$fa-var-file-video: \f1c8; +$fa-var-file-word: \f1c2; +$fa-var-film: \f008; +$fa-var-filter: \f0b0; +$fa-var-fire: \f06d; +$fa-var-fire-extinguisher: \f134; +$fa-var-firefox: \f269; +$fa-var-first-order: \f2b0; +$fa-var-firstdraft: \f3a1; +$fa-var-flag: \f024; +$fa-var-flag-checkered: \f11e; +$fa-var-flask: \f0c3; +$fa-var-flickr: \f16e; +$fa-var-fly: \f417; +$fa-var-folder: \f07b; +$fa-var-folder-open: \f07c; +$fa-var-font: \f031; +$fa-var-font-awesome: \f2b4; +$fa-var-font-awesome-alt: \f35c; +$fa-var-font-awesome-flag: \f425; +$fa-var-fonticons: \f280; +$fa-var-fonticons-fi: \f3a2; +$fa-var-fort-awesome: \f286; +$fa-var-fort-awesome-alt: \f3a3; +$fa-var-forumbee: \f211; +$fa-var-forward: \f04e; +$fa-var-foursquare: \f180; +$fa-var-free-code-camp: \f2c5; +$fa-var-freebsd: \f3a4; +$fa-var-frown: \f119; +$fa-var-futbol: \f1e3; +$fa-var-gamepad: \f11b; +$fa-var-gavel: \f0e3; +$fa-var-gem: \f3a5; +$fa-var-genderless: \f22d; +$fa-var-get-pocket: \f265; +$fa-var-gg: \f260; +$fa-var-gg-circle: \f261; +$fa-var-gift: \f06b; +$fa-var-git: \f1d3; +$fa-var-git-square: \f1d2; +$fa-var-github: \f09b; +$fa-var-github-alt: \f113; +$fa-var-github-square: \f092; +$fa-var-gitkraken: \f3a6; +$fa-var-gitlab: \f296; +$fa-var-gitter: \f426; +$fa-var-glass-martini: \f000; +$fa-var-glide: \f2a5; +$fa-var-glide-g: \f2a6; +$fa-var-globe: \f0ac; +$fa-var-gofore: \f3a7; +$fa-var-goodreads: \f3a8; +$fa-var-goodreads-g: \f3a9; +$fa-var-google: \f1a0; +$fa-var-google-drive: \f3aa; +$fa-var-google-play: \f3ab; +$fa-var-google-plus: \f2b3; +$fa-var-google-plus-g: \f0d5; +$fa-var-google-plus-square: \f0d4; +$fa-var-google-wallet: \f1ee; +$fa-var-graduation-cap: \f19d; +$fa-var-gratipay: \f184; +$fa-var-grav: \f2d6; +$fa-var-gripfire: \f3ac; +$fa-var-grunt: \f3ad; +$fa-var-gulp: \f3ae; +$fa-var-h-square: \f0fd; +$fa-var-hacker-news: \f1d4; +$fa-var-hacker-news-square: \f3af; +$fa-var-hand-lizard: \f258; +$fa-var-hand-paper: \f256; +$fa-var-hand-peace: \f25b; +$fa-var-hand-point-down: \f0a7; +$fa-var-hand-point-left: \f0a5; +$fa-var-hand-point-right: \f0a4; +$fa-var-hand-point-up: \f0a6; +$fa-var-hand-pointer: \f25a; +$fa-var-hand-rock: \f255; +$fa-var-hand-scissors: \f257; +$fa-var-hand-spock: \f259; +$fa-var-handshake: \f2b5; +$fa-var-hashtag: \f292; +$fa-var-hdd: \f0a0; +$fa-var-heading: \f1dc; +$fa-var-headphones: \f025; +$fa-var-heart: \f004; +$fa-var-heartbeat: \f21e; +$fa-var-hire-a-helper: \f3b0; +$fa-var-history: \f1da; +$fa-var-home: \f015; +$fa-var-hooli: \f427; +$fa-var-hospital: \f0f8; +$fa-var-hotjar: \f3b1; +$fa-var-hourglass: \f254; +$fa-var-hourglass-end: \f253; +$fa-var-hourglass-half: \f252; +$fa-var-hourglass-start: \f251; +$fa-var-houzz: \f27c; +$fa-var-html5: \f13b; +$fa-var-hubspot: \f3b2; +$fa-var-i-cursor: \f246; +$fa-var-id-badge: \f2c1; +$fa-var-id-card: \f2c2; +$fa-var-image: \f03e; +$fa-var-images: \f302; +$fa-var-imdb: \f2d8; +$fa-var-inbox: \f01c; +$fa-var-indent: \f03c; +$fa-var-industry: \f275; +$fa-var-info: \f129; +$fa-var-info-circle: \f05a; +$fa-var-instagram: \f16d; +$fa-var-internet-explorer: \f26b; +$fa-var-ioxhost: \f208; +$fa-var-italic: \f033; +$fa-var-itunes: \f3b4; +$fa-var-itunes-note: \f3b5; +$fa-var-jenkins: \f3b6; +$fa-var-joget: \f3b7; +$fa-var-joomla: \f1aa; +$fa-var-js: \f3b8; +$fa-var-js-square: \f3b9; +$fa-var-jsfiddle: \f1cc; +$fa-var-key: \f084; +$fa-var-keyboard: \f11c; +$fa-var-keycdn: \f3ba; +$fa-var-kickstarter: \f3bb; +$fa-var-kickstarter-k: \f3bc; +$fa-var-korvue: \f42f; +$fa-var-language: \f1ab; +$fa-var-laptop: \f109; +$fa-var-laravel: \f3bd; +$fa-var-lastfm: \f202; +$fa-var-lastfm-square: \f203; +$fa-var-leaf: \f06c; +$fa-var-leanpub: \f212; +$fa-var-lemon: \f094; +$fa-var-less: \f41d; +$fa-var-level-down-alt: \f3be; +$fa-var-level-up-alt: \f3bf; +$fa-var-life-ring: \f1cd; +$fa-var-lightbulb: \f0eb; +$fa-var-line: \f3c0; +$fa-var-link: \f0c1; +$fa-var-linkedin: \f08c; +$fa-var-linkedin-in: \f0e1; +$fa-var-linode: \f2b8; +$fa-var-linux: \f17c; +$fa-var-lira-sign: \f195; +$fa-var-list: \f03a; +$fa-var-list-alt: \f022; +$fa-var-list-ol: \f0cb; +$fa-var-list-ul: \f0ca; +$fa-var-location-arrow: \f124; +$fa-var-lock: \f023; +$fa-var-lock-open: \f3c1; +$fa-var-long-arrow-alt-down: \f309; +$fa-var-long-arrow-alt-left: \f30a; +$fa-var-long-arrow-alt-right: \f30b; +$fa-var-long-arrow-alt-up: \f30c; +$fa-var-low-vision: \f2a8; +$fa-var-lyft: \f3c3; +$fa-var-magento: \f3c4; +$fa-var-magic: \f0d0; +$fa-var-magnet: \f076; +$fa-var-male: \f183; +$fa-var-map: \f279; +$fa-var-map-marker: \f041; +$fa-var-map-marker-alt: \f3c5; +$fa-var-map-pin: \f276; +$fa-var-map-signs: \f277; +$fa-var-mars: \f222; +$fa-var-mars-double: \f227; +$fa-var-mars-stroke: \f229; +$fa-var-mars-stroke-h: \f22b; +$fa-var-mars-stroke-v: \f22a; +$fa-var-maxcdn: \f136; +$fa-var-medapps: \f3c6; +$fa-var-medium: \f23a; +$fa-var-medium-m: \f3c7; +$fa-var-medkit: \f0fa; +$fa-var-medrt: \f3c8; +$fa-var-meetup: \f2e0; +$fa-var-meh: \f11a; +$fa-var-mercury: \f223; +$fa-var-microchip: \f2db; +$fa-var-microphone: \f130; +$fa-var-microphone-slash: \f131; +$fa-var-microsoft: \f3ca; +$fa-var-minus: \f068; +$fa-var-minus-circle: \f056; +$fa-var-minus-square: \f146; +$fa-var-mix: \f3cb; +$fa-var-mixcloud: \f289; +$fa-var-mizuni: \f3cc; +$fa-var-mobile: \f10b; +$fa-var-mobile-alt: \f3cd; +$fa-var-modx: \f285; +$fa-var-monero: \f3d0; +$fa-var-money-bill-alt: \f3d1; +$fa-var-moon: \f186; +$fa-var-motorcycle: \f21c; +$fa-var-mouse-pointer: \f245; +$fa-var-music: \f001; +$fa-var-napster: \f3d2; +$fa-var-neuter: \f22c; +$fa-var-newspaper: \f1ea; +$fa-var-nintendo-switch: \f418; +$fa-var-node: \f419; +$fa-var-node-js: \f3d3; +$fa-var-npm: \f3d4; +$fa-var-ns8: \f3d5; +$fa-var-nutritionix: \f3d6; +$fa-var-object-group: \f247; +$fa-var-object-ungroup: \f248; +$fa-var-odnoklassniki: \f263; +$fa-var-odnoklassniki-square: \f264; +$fa-var-opencart: \f23d; +$fa-var-openid: \f19b; +$fa-var-opera: \f26a; +$fa-var-optin-monster: \f23c; +$fa-var-osi: \f41a; +$fa-var-outdent: \f03b; +$fa-var-page4: \f3d7; +$fa-var-pagelines: \f18c; +$fa-var-paint-brush: \f1fc; +$fa-var-palfed: \f3d8; +$fa-var-paper-plane: \f1d8; +$fa-var-paperclip: \f0c6; +$fa-var-paragraph: \f1dd; +$fa-var-paste: \f0ea; +$fa-var-patreon: \f3d9; +$fa-var-pause: \f04c; +$fa-var-pause-circle: \f28b; +$fa-var-paw: \f1b0; +$fa-var-paypal: \f1ed; +$fa-var-pen-square: \f14b; +$fa-var-pencil-alt: \f303; +$fa-var-percent: \f295; +$fa-var-periscope: \f3da; +$fa-var-phabricator: \f3db; +$fa-var-phoenix-framework: \f3dc; +$fa-var-phone: \f095; +$fa-var-phone-square: \f098; +$fa-var-phone-volume: \f2a0; +$fa-var-pied-piper: \f2ae; +$fa-var-pied-piper-alt: \f1a8; +$fa-var-pied-piper-pp: \f1a7; +$fa-var-pinterest: \f0d2; +$fa-var-pinterest-p: \f231; +$fa-var-pinterest-square: \f0d3; +$fa-var-plane: \f072; +$fa-var-play: \f04b; +$fa-var-play-circle: \f144; +$fa-var-playstation: \f3df; +$fa-var-plug: \f1e6; +$fa-var-plus: \f067; +$fa-var-plus-circle: \f055; +$fa-var-plus-square: \f0fe; +$fa-var-podcast: \f2ce; +$fa-var-pound-sign: \f154; +$fa-var-power-off: \f011; +$fa-var-print: \f02f; +$fa-var-product-hunt: \f288; +$fa-var-pushed: \f3e1; +$fa-var-puzzle-piece: \f12e; +$fa-var-python: \f3e2; +$fa-var-qq: \f1d6; +$fa-var-qrcode: \f029; +$fa-var-question: \f128; +$fa-var-question-circle: \f059; +$fa-var-quora: \f2c4; +$fa-var-quote-left: \f10d; +$fa-var-quote-right: \f10e; +$fa-var-random: \f074; +$fa-var-ravelry: \f2d9; +$fa-var-react: \f41b; +$fa-var-rebel: \f1d0; +$fa-var-recycle: \f1b8; +$fa-var-red-river: \f3e3; +$fa-var-reddit: \f1a1; +$fa-var-reddit-alien: \f281; +$fa-var-reddit-square: \f1a2; +$fa-var-redo: \f01e; +$fa-var-redo-alt: \f2f9; +$fa-var-registered: \f25d; +$fa-var-rendact: \f3e4; +$fa-var-renren: \f18b; +$fa-var-reply: \f3e5; +$fa-var-reply-all: \f122; +$fa-var-replyd: \f3e6; +$fa-var-resolving: \f3e7; +$fa-var-retweet: \f079; +$fa-var-road: \f018; +$fa-var-rocket: \f135; +$fa-var-rocketchat: \f3e8; +$fa-var-rockrms: \f3e9; +$fa-var-rss: \f09e; +$fa-var-rss-square: \f143; +$fa-var-ruble-sign: \f158; +$fa-var-rupee-sign: \f156; +$fa-var-safari: \f267; +$fa-var-sass: \f41e; +$fa-var-save: \f0c7; +$fa-var-schlix: \f3ea; +$fa-var-scribd: \f28a; +$fa-var-search: \f002; +$fa-var-search-minus: \f010; +$fa-var-search-plus: \f00e; +$fa-var-searchengin: \f3eb; +$fa-var-sellcast: \f2da; +$fa-var-sellsy: \f213; +$fa-var-server: \f233; +$fa-var-servicestack: \f3ec; +$fa-var-share: \f064; +$fa-var-share-alt: \f1e0; +$fa-var-share-alt-square: \f1e1; +$fa-var-share-square: \f14d; +$fa-var-shekel-sign: \f20b; +$fa-var-shield-alt: \f3ed; +$fa-var-ship: \f21a; +$fa-var-shirtsinbulk: \f214; +$fa-var-shopping-bag: \f290; +$fa-var-shopping-basket: \f291; +$fa-var-shopping-cart: \f07a; +$fa-var-shower: \f2cc; +$fa-var-sign-in-alt: \f2f6; +$fa-var-sign-language: \f2a7; +$fa-var-sign-out-alt: \f2f5; +$fa-var-signal: \f012; +$fa-var-simplybuilt: \f215; +$fa-var-sistrix: \f3ee; +$fa-var-sitemap: \f0e8; +$fa-var-skyatlas: \f216; +$fa-var-skype: \f17e; +$fa-var-slack: \f198; +$fa-var-slack-hash: \f3ef; +$fa-var-sliders-h: \f1de; +$fa-var-slideshare: \f1e7; +$fa-var-smile: \f118; +$fa-var-snapchat: \f2ab; +$fa-var-snapchat-ghost: \f2ac; +$fa-var-snapchat-square: \f2ad; +$fa-var-snowflake: \f2dc; +$fa-var-sort: \f0dc; +$fa-var-sort-alpha-down: \f15d; +$fa-var-sort-alpha-up: \f15e; +$fa-var-sort-amount-down: \f160; +$fa-var-sort-amount-up: \f161; +$fa-var-sort-down: \f0dd; +$fa-var-sort-numeric-down: \f162; +$fa-var-sort-numeric-up: \f163; +$fa-var-sort-up: \f0de; +$fa-var-soundcloud: \f1be; +$fa-var-space-shuttle: \f197; +$fa-var-speakap: \f3f3; +$fa-var-spinner: \f110; +$fa-var-spotify: \f1bc; +$fa-var-square: \f0c8; +$fa-var-stack-exchange: \f18d; +$fa-var-stack-overflow: \f16c; +$fa-var-star: \f005; +$fa-var-star-half: \f089; +$fa-var-staylinked: \f3f5; +$fa-var-steam: \f1b6; +$fa-var-steam-square: \f1b7; +$fa-var-steam-symbol: \f3f6; +$fa-var-step-backward: \f048; +$fa-var-step-forward: \f051; +$fa-var-stethoscope: \f0f1; +$fa-var-sticker-mule: \f3f7; +$fa-var-sticky-note: \f249; +$fa-var-stop: \f04d; +$fa-var-stop-circle: \f28d; +$fa-var-stopwatch: \f2f2; +$fa-var-strava: \f428; +$fa-var-street-view: \f21d; +$fa-var-strikethrough: \f0cc; +$fa-var-stripe: \f429; +$fa-var-stripe-s: \f42a; +$fa-var-studiovinari: \f3f8; +$fa-var-stumbleupon: \f1a4; +$fa-var-stumbleupon-circle: \f1a3; +$fa-var-subscript: \f12c; +$fa-var-subway: \f239; +$fa-var-suitcase: \f0f2; +$fa-var-sun: \f185; +$fa-var-superpowers: \f2dd; +$fa-var-superscript: \f12b; +$fa-var-supple: \f3f9; +$fa-var-sync: \f021; +$fa-var-sync-alt: \f2f1; +$fa-var-table: \f0ce; +$fa-var-tablet: \f10a; +$fa-var-tablet-alt: \f3fa; +$fa-var-tachometer-alt: \f3fd; +$fa-var-tag: \f02b; +$fa-var-tags: \f02c; +$fa-var-tasks: \f0ae; +$fa-var-taxi: \f1ba; +$fa-var-telegram: \f2c6; +$fa-var-telegram-plane: \f3fe; +$fa-var-tencent-weibo: \f1d5; +$fa-var-terminal: \f120; +$fa-var-text-height: \f034; +$fa-var-text-width: \f035; +$fa-var-th: \f00a; +$fa-var-th-large: \f009; +$fa-var-th-list: \f00b; +$fa-var-themeisle: \f2b2; +$fa-var-thermometer-empty: \f2cb; +$fa-var-thermometer-full: \f2c7; +$fa-var-thermometer-half: \f2c9; +$fa-var-thermometer-quarter: \f2ca; +$fa-var-thermometer-three-quarters: \f2c8; +$fa-var-thumbs-down: \f165; +$fa-var-thumbs-up: \f164; +$fa-var-thumbtack: \f08d; +$fa-var-ticket-alt: \f3ff; +$fa-var-times: \f00d; +$fa-var-times-circle: \f057; +$fa-var-tint: \f043; +$fa-var-toggle-off: \f204; +$fa-var-toggle-on: \f205; +$fa-var-trademark: \f25c; +$fa-var-train: \f238; +$fa-var-transgender: \f224; +$fa-var-transgender-alt: \f225; +$fa-var-trash: \f1f8; +$fa-var-trash-alt: \f2ed; +$fa-var-tree: \f1bb; +$fa-var-trello: \f181; +$fa-var-tripadvisor: \f262; +$fa-var-trophy: \f091; +$fa-var-truck: \f0d1; +$fa-var-tty: \f1e4; +$fa-var-tumblr: \f173; +$fa-var-tumblr-square: \f174; +$fa-var-tv: \f26c; +$fa-var-twitch: \f1e8; +$fa-var-twitter: \f099; +$fa-var-twitter-square: \f081; +$fa-var-typo3: \f42b; +$fa-var-uber: \f402; +$fa-var-uikit: \f403; +$fa-var-umbrella: \f0e9; +$fa-var-underline: \f0cd; +$fa-var-undo: \f0e2; +$fa-var-undo-alt: \f2ea; +$fa-var-uniregistry: \f404; +$fa-var-universal-access: \f29a; +$fa-var-university: \f19c; +$fa-var-unlink: \f127; +$fa-var-unlock: \f09c; +$fa-var-unlock-alt: \f13e; +$fa-var-untappd: \f405; +$fa-var-upload: \f093; +$fa-var-usb: \f287; +$fa-var-user: \f007; +$fa-var-user-circle: \f2bd; +$fa-var-user-md: \f0f0; +$fa-var-user-plus: \f234; +$fa-var-user-secret: \f21b; +$fa-var-user-times: \f235; +$fa-var-users: \f0c0; +$fa-var-ussunnah: \f407; +$fa-var-utensil-spoon: \f2e5; +$fa-var-utensils: \f2e7; +$fa-var-vaadin: \f408; +$fa-var-venus: \f221; +$fa-var-venus-double: \f226; +$fa-var-venus-mars: \f228; +$fa-var-viacoin: \f237; +$fa-var-viadeo: \f2a9; +$fa-var-viadeo-square: \f2aa; +$fa-var-viber: \f409; +$fa-var-video: \f03d; +$fa-var-vimeo: \f40a; +$fa-var-vimeo-square: \f194; +$fa-var-vimeo-v: \f27d; +$fa-var-vine: \f1ca; +$fa-var-vk: \f189; +$fa-var-vnv: \f40b; +$fa-var-volume-down: \f027; +$fa-var-volume-off: \f026; +$fa-var-volume-up: \f028; +$fa-var-vuejs: \f41f; +$fa-var-weibo: \f18a; +$fa-var-weixin: \f1d7; +$fa-var-whatsapp: \f232; +$fa-var-whatsapp-square: \f40c; +$fa-var-wheelchair: \f193; +$fa-var-whmcs: \f40d; +$fa-var-wifi: \f1eb; +$fa-var-wikipedia-w: \f266; +$fa-var-window-close: \f410; +$fa-var-window-maximize: \f2d0; +$fa-var-window-minimize: \f2d1; +$fa-var-window-restore: \f2d2; +$fa-var-windows: \f17a; +$fa-var-won-sign: \f159; +$fa-var-wordpress: \f19a; +$fa-var-wordpress-simple: \f411; +$fa-var-wpbeginner: \f297; +$fa-var-wpexplorer: \f2de; +$fa-var-wpforms: \f298; +$fa-var-wrench: \f0ad; +$fa-var-xbox: \f412; +$fa-var-xing: \f168; +$fa-var-xing-square: \f169; +$fa-var-y-combinator: \f23b; +$fa-var-yahoo: \f19e; +$fa-var-yandex: \f413; +$fa-var-yandex-international: \f414; +$fa-var-yelp: \f1e9; +$fa-var-yen-sign: \f157; +$fa-var-yoast: \f2b1; +$fa-var-youtube: \f167; diff --git a/docs/src/assets/fa/fa-brands.scss b/docs/src/assets/fa/fa-brands.scss new file mode 100644 index 00000000..ab89c020 --- /dev/null +++ b/docs/src/assets/fa/fa-brands.scss @@ -0,0 +1,21 @@ +/*! + * Font Awesome Free 5.0.2 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; + +@font-face { + font-family: 'Font Awesome 5 Brands'; + font-style: normal; + font-weight: normal; + src: url('#{$fa-font-path}/fa-brands-400.eot'); + src: url('#{$fa-font-path}/fa-brands-400.eot?#iefix') format('embedded-opentype'), + url('#{$fa-font-path}/fa-brands-400.woff2') format('woff2'), + url('#{$fa-font-path}/fa-brands-400.woff') format('woff'), + url('#{$fa-font-path}/fa-brands-400.ttf') format('truetype'), + url('#{$fa-font-path}/fa-brands-400.svg#fontawesome') format('svg'); +} + +.fab { + font-family: 'Font Awesome 5 Brands'; +} diff --git a/docs/src/assets/fa/fa-regular.scss b/docs/src/assets/fa/fa-regular.scss new file mode 100644 index 00000000..4f288292 --- /dev/null +++ b/docs/src/assets/fa/fa-regular.scss @@ -0,0 +1,22 @@ +/*! + * Font Awesome Free 5.0.2 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 400; + src: url('#{$fa-font-path}/fa-regular-400.eot'); + src: url('#{$fa-font-path}/fa-regular-400.eot?#iefix') format('embedded-opentype'), + url('#{$fa-font-path}/fa-regular-400.woff2') format('woff2'), + url('#{$fa-font-path}/fa-regular-400.woff') format('woff'), + url('#{$fa-font-path}/fa-regular-400.ttf') format('truetype'), + url('#{$fa-font-path}/fa-regular-400.svg#fontawesome') format('svg'); +} + +.far { + font-family: 'Font Awesome 5 Free'; + font-weight: 400; +} diff --git a/docs/src/assets/fa/fa-solid.scss b/docs/src/assets/fa/fa-solid.scss new file mode 100644 index 00000000..bb0df52a --- /dev/null +++ b/docs/src/assets/fa/fa-solid.scss @@ -0,0 +1,23 @@ +/*! + * Font Awesome Free 5.0.2 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; + +@font-face { + font-family: 'Font Awesome 5 Free'; + font-style: normal; + font-weight: 900; + src: url('#{$fa-font-path}/fa-solid-900.eot'); + src: url('#{$fa-font-path}/fa-solid-900.eot?#iefix') format('embedded-opentype'), + url('#{$fa-font-path}/fa-solid-900.woff2') format('woff2'), + url('#{$fa-font-path}/fa-solid-900.woff') format('woff'), + url('#{$fa-font-path}/fa-solid-900.ttf') format('truetype'), + url('#{$fa-font-path}/fa-solid-900.svg#fontawesome') format('svg'); +} + +.fa, +.fas { + font-family: 'Font Awesome 5 Free'; + font-weight: 900; +} diff --git a/docs/src/assets/fa/fontawesome.scss b/docs/src/assets/fa/fontawesome.scss new file mode 100644 index 00000000..04eb879a --- /dev/null +++ b/docs/src/assets/fa/fontawesome.scss @@ -0,0 +1,16 @@ +/*! + * Font Awesome Free 5.0.2 by @fontawesome - http://fontawesome.com + * License - http://fontawesome.com/license (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) + */ +@import 'variables'; +@import 'mixins'; +@import 'core'; +@import 'larger'; +@import 'fixed-width'; +@import 'list'; +@import 'bordered-pulled'; +@import 'animated'; +@import 'rotated-flipped'; +@import 'stacked'; +@import 'icons'; +@import 'screen-reader'; diff --git a/docs/src/assets/style.scss b/docs/src/assets/style.scss new file mode 100644 index 00000000..ec46d374 --- /dev/null +++ b/docs/src/assets/style.scss @@ -0,0 +1,141 @@ +@import "./syntax"; +/* background for both hugo *and* pdoc. */ +.chroma pre, pre.chroma { + background-color: #f7f7f7; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + padding: .5rem 0 .5rem .5rem; +} + +@import "./badge"; + +$primary: #C93312; +$warning-invert: #FFFFFF; +$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif, 'Font Awesome 5 Free', 'Font Awesome 5 Brands' !default; + +$panel-heading-size: 1em; +$panel-heading-weight: 600; +$menu-list-link-padding: .3em .75em; +$menu-label-spacing: .7em; +$menu-nested-list-margin: .3em .75em; + +/*!* +bulma.io v0.8.0 | MIT License | github.com/jgthms/bulma */ +@import "./bulma/utilities/_all"; +@import "./bulma/base/_all"; +@import "./bulma/grid/_all"; +@import "./bulma/elements/_all"; +@import "./bulma/components/_all"; +@import "./bulma/layout/_all"; + +html { + scroll-behavior: smooth; +} + +html, body { + height: 100%; +} + +body > div { + min-height: 100%; +} + +#sidebar { + background-color: #eee; + border-right: 1px solid #c1c1c1; + box-shadow: 0 0 20px rgba(50, 50, 50, .2) inset; + padding: $column-gap + 1rem; + + .brand { + padding: 1rem 0; + text-align: center; + } +} + +#main { + padding: 3rem 3rem 9rem; +} + +.example { + .highlight { + margin: 0; + } + .path { + font-style: italic; + width: 100%; + text-align: right; + } + max-width: 70vw; + margin-bottom: 1em; +} + +code { + color: #1a9f1a; + font-size: 0.875em; + font-weight: normal; +} + +.content { + h2 { + padding-top: 1em; + border-top: 1px solid #c0c0c0; + } +} + +h1, h2, h3, h4, h5, h6, th { + .anchor { + display: inline-block; + width: 0; + margin-left: -1.5rem; + margin-right: 1.5rem; + transition: all 100ms ease-in-out; + opacity: 0; + } + &:hover .anchor { + opacity: 1; + } + &:target { + color: $primary; + .anchor { + opacity: 1; + color: $primary + } + } +} + +table code { + white-space: pre; +} + +.footnotes p { + display: inline; +} + +figure.has-border img { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); +} + +.asciicast-wrapper { + margin: 2rem 0; + + asciinema-player { + display: block; + margin-bottom: 1rem; + } + + // reset bulma pre styles + pre.asciinema-terminal { + padding: 0; + overflow-x: hidden; + -webkit-overflow-scrolling: auto; + } + + .panel-block { + justify-content: space-between; + } + + .panel-block.is-active .tag { + background-color: $link; + color: $white; + } +} diff --git a/docs/src/assets/syntax.css b/docs/src/assets/syntax.css new file mode 100644 index 00000000..86c307fa --- /dev/null +++ b/docs/src/assets/syntax.css @@ -0,0 +1,83 @@ +/* Line Numbers */ .chroma span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 20px; } +/* Background */ .chroma { background-color: #ffffff } +/* Other */ .chroma .x { } +/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } +/* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } +/* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Keyword */ .chroma .k { color: #000000; font-weight: bold } +/* KeywordConstant */ .chroma .kc { color: #000000; font-weight: bold } +/* KeywordDeclaration */ .chroma .kd { color: #000000; font-weight: bold } +/* KeywordNamespace */ .chroma .kn { color: #000000; font-weight: bold } +/* KeywordPseudo */ .chroma .kp { color: #000000; font-weight: bold } +/* KeywordReserved */ .chroma .kr { color: #000000; font-weight: bold } +/* KeywordType */ .chroma .kt { color: #445588; font-weight: bold } +/* Name */ .chroma .n { } +/* NameAttribute */ .chroma .na { color: #008080 } +/* NameBuiltin */ .chroma .nb { color: #0086b3 } +/* NameBuiltinPseudo */ .chroma .bp { color: #999999 } +/* NameClass */ .chroma .nc { color: #445588; font-weight: bold } +/* NameConstant */ .chroma .no { color: #008080 } +/* NameDecorator */ .chroma .nd { color: #3c5d5d; font-weight: bold } +/* NameEntity */ .chroma .ni { color: #800080 } +/* NameException */ .chroma .ne { color: #990000; font-weight: bold } +/* NameFunction */ .chroma .nf { color: #990000; font-weight: bold } +/* NameFunctionMagic */ .chroma .fm { } +/* NameLabel */ .chroma .nl { color: #990000; font-weight: bold } +/* NameNamespace */ .chroma .nn { color: #555555 } +/* NameOther */ .chroma .nx { } +/* NameProperty */ .chroma .py { } +/* NameTag */ .chroma .nt { color: #000080 } +/* NameVariable */ .chroma .nv { color: #008080 } +/* NameVariableClass */ .chroma .vc { color: #008080 } +/* NameVariableGlobal */ .chroma .vg { color: #008080 } +/* NameVariableInstance */ .chroma .vi { color: #008080 } +/* NameVariableMagic */ .chroma .vm { } +/* Literal */ .chroma .l { } +/* LiteralDate */ .chroma .ld { } +/* LiteralString */ .chroma .s { color: #dd1144 } +/* LiteralStringAffix */ .chroma .sa { color: #dd1144 } +/* LiteralStringBacktick */ .chroma .sb { color: #dd1144 } +/* LiteralStringChar */ .chroma .sc { color: #dd1144 } +/* LiteralStringDelimiter */ .chroma .dl { color: #dd1144 } +/* LiteralStringDoc */ .chroma .sd { color: #dd1144 } +/* LiteralStringDouble */ .chroma .s2 { color: #dd1144 } +/* LiteralStringEscape */ .chroma .se { color: #dd1144 } +/* LiteralStringHeredoc */ .chroma .sh { color: #dd1144 } +/* LiteralStringInterpol */ .chroma .si { color: #dd1144 } +/* LiteralStringOther */ .chroma .sx { color: #dd1144 } +/* LiteralStringRegex */ .chroma .sr { color: #009926 } +/* LiteralStringSingle */ .chroma .s1 { color: #dd1144 } +/* LiteralStringSymbol */ .chroma .ss { color: #990073 } +/* LiteralNumber */ .chroma .m { color: #009999 } +/* LiteralNumberBin */ .chroma .mb { color: #009999 } +/* LiteralNumberFloat */ .chroma .mf { color: #009999 } +/* LiteralNumberHex */ .chroma .mh { color: #009999 } +/* LiteralNumberInteger */ .chroma .mi { color: #009999 } +/* LiteralNumberIntegerLong */ .chroma .il { color: #009999 } +/* LiteralNumberOct */ .chroma .mo { color: #009999 } +/* Operator */ .chroma .o { color: #000000; font-weight: bold } +/* OperatorWord */ .chroma .ow { color: #000000; font-weight: bold } +/* Punctuation */ .chroma .p { } +/* Comment */ .chroma .c { color: #999988; font-style: italic } +/* CommentHashbang */ .chroma .ch { color: #999988; font-style: italic } +/* CommentMultiline */ .chroma .cm { color: #999988; font-style: italic } +/* CommentSingle */ .chroma .c1 { color: #999988; font-style: italic } +/* CommentSpecial */ .chroma .cs { color: #999999; font-weight: bold; font-style: italic } +/* CommentPreproc */ .chroma .cp { color: #999999; font-weight: bold; font-style: italic } +/* CommentPreprocFile */ .chroma .cpf { color: #999999; font-weight: bold; font-style: italic } +/* Generic */ .chroma .g { } +/* GenericDeleted */ .chroma .gd { color: #000000; background-color: #ffdddd } +/* GenericEmph */ .chroma .ge { color: #000000; font-style: italic } +/* GenericError */ .chroma .gr { color: #aa0000 } +/* GenericHeading */ .chroma .gh { color: #999999 } +/* GenericInserted */ .chroma .gi { color: #000000; background-color: #ddffdd } +/* GenericOutput */ .chroma .go { color: #888888 } +/* GenericPrompt */ .chroma .gp { color: #555555 } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #aaaaaa } +/* GenericTraceback */ .chroma .gt { color: #aa0000 } +/* GenericUnderline */ .chroma .gl { text-decoration: underline } +/* TextWhitespace */ .chroma .w { color: #bbbbbb } diff --git a/docs/src/config.toml b/docs/src/config.toml new file mode 100644 index 00000000..d56ecf86 --- /dev/null +++ b/docs/src/config.toml @@ -0,0 +1,17 @@ +baseURL = "" +languageCode = "en-us" +title = "scalpel.org docs" +theme = "mitmproxydocs" +publishDir = "../public" +RelativeURLs = true +pygmentsCodefences = true +pygmentsUseClasses = true + +[indexes] +tag = "tags" + +[markup.goldmark.renderer] +unsafe = true + +[security.funcs] +getenv = ["DOCS_ARCHIVE"] diff --git a/docs/src/content/_index.md b/docs/src/content/_index.md new file mode 100644 index 00000000..f430004e --- /dev/null +++ b/docs/src/content/_index.md @@ -0,0 +1,38 @@ +--- +title: "Introduction" +layout: single +menu: + overview: + weight: 1 +--- + +# Introduction + +Scalpel is a powerful **Burp Suite** extension that allows you to script Burp in order to intercept, rewrite HTTP traffic on the fly, and program custom Burp editors in Python 3. + +It provides an interactive way to edit encoded/encrypted data as plaintext and offers an easy-to-use Python library as an alternative to Burp's Java API. + +# Index + +- [Installation]({{< relref "overview-installation" >}}) +- [Usage]({{< relref "overview-usage" >}}) +- [First steps]({{< relref "tute-first-steps" >}}) +- [FAQ]({{< relref "overview-faq" >}}) +- [Technical documentation for script development]({{< relref "addons-api" >}}) +- [Example use-case]({{< relref "tute-aes" >}}) +- [How scalpel works]({{< relref "concepts-howscalpelworks" >}}) + +## Features + +- [**Python Library**]({{< relref "addons-api" >}}): Easy-to-use Python library, especially welcome for non-Java developers. +- [**Intercept and Rewrite HTTP Traffic**]({{< relref "feature-http" >}}): Scalpel provides a set of predefined function names that can be implemented to intercept and modify HTTP requests and responses. +- [**Custom Burp Editors**]({{< relref "feature-editors" >}}): Program your own Burp editors in Python. Encoded/encrypted data can be handled as plaintext. + - [**Hex Editors**]({{< relref "feature-editors#binary-editors" >}}): Ability to create improved hex editors. + +## Use cases + +- [Decrypting custom encryption]({{< relref "tute-aes" >}}) +- [Editing encoded requests/responses]({{< relref "addons-examples#GZIP" >}}) + +> Note: One might think existing Burp extensions like `Piper` can handle such cases. But actually they can't. +> For example, when intercepting a response, `Piper` cannot get information from the initiating request, which is required in the above use cases. Scalpel generally allows you to manage complex cases that are not handled by other Burp extensions like `Piper` or `Hackvertor`. diff --git a/docs/src/content/addons-api.md b/docs/src/content/addons-api.md new file mode 100644 index 00000000..0d4d18ba --- /dev/null +++ b/docs/src/content/addons-api.md @@ -0,0 +1,34 @@ +--- +title: "Event Hooks & API" +url: "api/events.html" +aliases: + - /addons-events/ +layout: single +menu: + addons: + weight: 1 +--- + +## Available Hooks + +The following list all available event hooks. + +The full Python documentation is available **[here](../../pdoc/python3-10/pyscalpel.html)** + +{{< readfile file="/generated/api/events.html" >}} + +## âš ï¸ Good to know + +- If your hooks return `None`, they will follow these behaviors: + + - `request()` or `response()`: The original request is be **forwarded without any modifications**. + - `req_edit_in()` or `res_edit_in()`: The editor tab is **not displayed**. + - `req_edit_out()` or `res_edit_out()`: The request is **not modified**. + +- If `req_edit_out()` or `res_edit_out()` isn't declared but `req_edit_in()` or `res_edit_in()` is, the corresponding editor will be **read-only**. + +- You do not have to declare every hook if you don't need them, if you only want to modify requests, you can declare the `request()` hook only. + +## Further reading + +Check out the [Custom Burp Editors]({{< relref "feature-editors" >}}) section. diff --git a/docs/src/content/addons-debugging.md b/docs/src/content/addons-debugging.md new file mode 100644 index 00000000..150673c0 --- /dev/null +++ b/docs/src/content/addons-debugging.md @@ -0,0 +1,45 @@ +--- +title: "Debugging" +menu: addons +weight: 3 +--- + +# Debugging + +Scalpel scripts can be **hard to debug**, as you cannot run them outside of Burp. + +Also it is difficult to know if a bug is related to Scalpel/Burp context or to the user's implementation. + +Here are a few advices for debugging Scalpel errors. + +## Finding stacktraces + +Errors that occur in scripts can be found in different places: + +### 1. The **Output** tab + +In the **Scalpel** tab, there is a sub-tab named **Script Output**, it shows all the standard output and error contents outputted by the current script +{{< figure src="/screenshots/output.png" >}} + +### 2. The error popus + +When an error happens in the hooks **request()** and **response()**, a popup is displayed. +{{< figure src="/screenshots/error-popup.png" >}} + +This popup can be disabled in the Settings tab + +### 3. The dashboard event log + +{{< figure src="/screenshots/debug-image.png" >}} +The user may click on the events to get the full error message: +{{< figure src="/screenshots/debug-image-1.png" >}} + +### 4. The extensions logs + +{{< figure src="/screenshots/debug-image-2.png" >}} + +### 5. The command line output **(best)** + +{{< figure src="/screenshots/debug-image-3.png" >}} + +> 💡 When debugging, it is best to launch Burp in CLI, as the CLI output will contain absolutely all errors and logs, which is not always the case in the Burp GUI (e.g: In case of deadlocks, crashes and other tricky issues). diff --git a/docs/src/content/addons-examples.md b/docs/src/content/addons-examples.md new file mode 100644 index 00000000..67816299 --- /dev/null +++ b/docs/src/content/addons-examples.md @@ -0,0 +1,169 @@ +--- +title: "Examples" +menu: + addons: + weight: 6 +--- + +# Script examples + +This page provides example scripts to get familiar with Scalpel's Python library. They are designed for real use cases. + +## Table of content + +- [GZIP-ed API](#gzip-ed-api) +- [Cryptography using a session as a secret](#cryptography-using-a-session-as-a-secret) + +## GZIP-ed API + +Let's assume you encountered an API using a custom protocol that gzips multiple form-data fields. + +Quick-and-dirty Scalpel script to directly edit the unzipped data and find hidden secrets: + +```python +from pyscalpel import Request, Response, logger +import gzip + + +def unzip_bytes(data): + try: + # Create a GzipFile object with the input data + with gzip.GzipFile(fileobj=data) as gz_file: + # Read the uncompressed data + uncompressed_data = gz_file.read() + return uncompressed_data + except OSError as e: + logger.error(f"Error: Failed to unzip the data - {e}") + + +def req_edit_in_fs(req: Request) -> bytes | None: + gz = req.multipart_form["fs"].content + + # Decode utf-16 and re-encoding to get rid of null bytes in the editor + content = gzip.decompress(gz).decode("utf-16le").encode("latin-1") + return content + + +def req_edit_out_fs(req: Request, text: bytes) -> Request | None: + data = text.decode("latin-1").encode("utf-16le") + content = gzip.compress(data, mtime=0) + req.multipart_form["fs"].content = content + return req + + +def req_edit_in_filetosend(req: Request) -> bytes | None: + gz = req.multipart_form["filetosend"].content + content = gzip.decompress(gz) + return content + + +def req_edit_out_filetosend(req: Request, text: bytes) -> Request | None: + data = text + content = gzip.compress(data, mtime=0) + req.multipart_form["filetosend"].content = content + return req + + +def res_edit_in(res: Response) -> bytes | None: + gz = res.content + if not gz: + return + + content = gzip.decompress(gz) + content.decode("utf-16le").encode("utf-8") + return content + + +def res_edit_out(res: Response, text: bytes) -> Response | None: + res.content = text + return res +``` + +## Cryptography using a session as a secret + +In this case, the client encrypted its form data using a session token obtained upon authentication. + +This script demonstrates that Scalpel can be easily used to deal with **stateful behaviors**: + +> 💡 Find a mock API to test this case in Scalpel's GitHub repository: [`test/server.js`](https://github.com/ambionics/scalpel/blob/4b935cb29b496f3627a319d963a009dda79a1aa7/test/server.js#L117C1-L118C1). + +```python +from pyscalpel import Request, Response, Flow +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Util.Padding import pad, unpad +from base64 import b64encode, b64decode + + +session: bytes = b"" + + +def match(flow: Flow) -> bool: + return flow.path_is("/encrypt-session*") and bool( + session or flow.request.method != "POST" + ) + + +def get_cipher(secret: bytes, iv=bytes(16)): + hasher = SHA256.new() + hasher.update(secret) + derived_aes_key = hasher.digest()[:32] + cipher = AES.new(derived_aes_key, AES.MODE_CBC, iv) + return cipher + + +def decrypt(secret: bytes, data: bytes) -> bytes: + data = b64decode(data) + cipher = get_cipher(secret) + decrypted = cipher.decrypt(data) + return unpad(decrypted, AES.block_size) + + +def encrypt(secret: bytes, data: bytes) -> bytes: + cipher = get_cipher(secret) + padded_data = pad(data, AES.block_size) + encrypted = cipher.encrypt(padded_data) + return b64encode(encrypted) + + +def response(res: Response) -> Response | None: + if res.request.method == "GET": + global session + session = res.content or b"" + return + + +def req_edit_in_encrypted(req: Request) -> bytes: + secret = session + encrypted = req.form[b"encrypted"] + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + +def req_edit_out_encrypted(req: Request, text: bytes) -> Request: + secret = session + req.form[b"encrypted"] = encrypt(secret, text) + return req + + +def res_edit_in_encrypted(res: Response) -> bytes: + secret = session + encrypted = res.content + + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + +def res_edit_out_encrypted(res: Response, text: bytes) -> Response: + secret = session + res.content = encrypt(secret, text) + return res +``` + +--- + +> If you encountered an interesting case, feel free to contact us to add it! diff --git a/docs/src/content/addons-java.md b/docs/src/content/addons-java.md new file mode 100644 index 00000000..20b9b1ed --- /dev/null +++ b/docs/src/content/addons-java.md @@ -0,0 +1,64 @@ +--- +title: "Using the Burp API" +menu: "addons" +menu: + addons: + weight: 2 +--- + +# Using the Burp API + +Scalpel communicates with Burp through its Java API. Then, it provides the user with an execution context in which they should **only use Python objects**. + +However, since Scalpel focuses on HTTP objects, it **does not provide utilities for all the Burp API features** (like the ability to generate Collaborator payloads). + +If the user must deal with unhandled cases, they can directly access the [MontoyaApi Java object](https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html) to search for appropriate objects. + +## Examples + +_A script that spoofs the Host header with a collaborator payload:_ + +```python +from pyscalpel import Request, ctx + +# Spoof the Host header to a Burp collaborator payload to detect out-of-band interactions and HTTP SSRFs + +# Directly access the Montoya API Java object to generate a payload +PAYLOAD = str(ctx["API"].collaborator().defaultPayloadGenerator().generatePayload()) + + +def request(req: Request) -> Request | None: + req.host_header = PAYLOAD + return req +``` + +> 💡 [PortSwigger's documentation for the Collaborator Generator](). + +
    + +_A script that sends every request that has the `cmd` param to Burp Repeater:_ + +```python +from pyscalpel import Request, ctx +from threading import Thread + +# Send every request that contains the "cmd" param to repeater + +# Ensure added request are unique by using a set +seen = set() + + +def request(req: Request) -> None: + cmd = req.query.get("cmd") + if cmd is not None and cmd not in seen: + # Convert request to Burp format + breq = req.to_burp() + + # Directly access the Montoya API Java object to send the request to repeater + repeater = ctx["API"].repeater() + + # Wait for sendToRepeater() while intercepting a request causes a Burp deadlock + Thread(target=lambda: repeater.sendToRepeater(breq, f"cmd={cmd}")).start() +``` + +> 💡 [PortSwigger's documentation for Burp repeater](https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/repeater/Repeater.html) diff --git a/docs/src/content/api/pyscalpel.edit.md b/docs/src/content/api/pyscalpel.edit.md new file mode 100644 index 00000000..38cd47de --- /dev/null +++ b/docs/src/content/api/pyscalpel.edit.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.edit" +url: "api/pyscalpel/edit.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 3 +--- + +{{< readfile file="/generated/api/pyscalpel/edit.html" >}} diff --git a/docs/src/content/api/pyscalpel.encoding.md b/docs/src/content/api/pyscalpel.encoding.md new file mode 100644 index 00000000..69a99405 --- /dev/null +++ b/docs/src/content/api/pyscalpel.encoding.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.encoding" +url: "api/pyscalpel/encoding.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 7 +--- + +{{< readfile file="/generated/api/pyscalpel/encoding.html" >}} diff --git a/docs/src/content/api/pyscalpel.events.md b/docs/src/content/api/pyscalpel.events.md new file mode 100644 index 00000000..9e81e62e --- /dev/null +++ b/docs/src/content/api/pyscalpel.events.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.events" +url: "api/pyscalpel/events.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 4 +--- + +{{< readfile file="/generated/api/pyscalpel/events.html" >}} diff --git a/docs/src/content/api/pyscalpel.http.body.md b/docs/src/content/api/pyscalpel.http.body.md new file mode 100644 index 00000000..177783dc --- /dev/null +++ b/docs/src/content/api/pyscalpel.http.body.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.http.body" +url: "api/pyscalpel/http/body.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 2 +--- + +{{< readfile file="/generated/api/pyscalpel/http/body.html" >}} diff --git a/docs/src/content/api/pyscalpel.http.md b/docs/src/content/api/pyscalpel.http.md new file mode 100644 index 00000000..849046d0 --- /dev/null +++ b/docs/src/content/api/pyscalpel.http.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.http" +url: "api/pyscalpel/http.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 1 +--- + +{{< readfile file="/generated/api/pyscalpel/http.html" >}} diff --git a/docs/src/content/api/pyscalpel.java.burp.md b/docs/src/content/api/pyscalpel.java.burp.md new file mode 100644 index 00000000..d0d1c554 --- /dev/null +++ b/docs/src/content/api/pyscalpel.java.burp.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.java.burp" +url: "api/pyscalpel/java/burp.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 9 +--- + +{{< readfile file="/generated/api/pyscalpel/java/burp.html" >}} diff --git a/docs/src/content/api/pyscalpel.java.md b/docs/src/content/api/pyscalpel.java.md new file mode 100644 index 00000000..bbdb7ecc --- /dev/null +++ b/docs/src/content/api/pyscalpel.java.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.java" +url: "api/pyscalpel/java.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 8 +--- + +{{< readfile file="/generated/api/pyscalpel/java.html" >}} diff --git a/docs/src/content/api/pyscalpel.utils.md b/docs/src/content/api/pyscalpel.utils.md new file mode 100644 index 00000000..19d92a84 --- /dev/null +++ b/docs/src/content/api/pyscalpel.utils.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.utils" +url: "api/pyscalpel/utils.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 6 +--- + +{{< readfile file="/generated/api/pyscalpel/utils.html" >}} diff --git a/docs/src/content/api/pyscalpel.venv.md b/docs/src/content/api/pyscalpel.venv.md new file mode 100644 index 00000000..626eaa9e --- /dev/null +++ b/docs/src/content/api/pyscalpel.venv.md @@ -0,0 +1,12 @@ + +--- +title: "pyscalpel.venv" +url: "api/pyscalpel/venv.html" + +menu: + addons: + parent: 'Event Hooks & API' + weight: 5 +--- + +{{< readfile file="/generated/api/pyscalpel/venv.html" >}} diff --git a/docs/src/content/concepts-howscalpelworks.md b/docs/src/content/concepts-howscalpelworks.md new file mode 100644 index 00000000..81754466 --- /dev/null +++ b/docs/src/content/concepts-howscalpelworks.md @@ -0,0 +1,47 @@ +--- +title: "How scalpel works" +menu: + concepts: + weight: 1 +--- + +# How Scalpel works + +## Table of content + +- [Dependencies](#dependencies) +- [Behavior](#behavior) +- [Python scripting](#python-scripting) +- [Diagram](#diagram) + +## Dependencies + +- Scalpel's Python library is embedded in a JAR file and is unzipped when Burp loads the extension. +- Scalpel requires external dependencies and will install them using `pip` when needed. +- Scalpel will always use a virtual environment for every action. Hence, it will never modify the user's global Python installation. +- Scalpel relies on [Jep](https://github.com/ninia/jep/) to communicate with Python. It requires to have a JDK installed on your machine. +- User scripts are executed in a virtual environment selected from the `Scalpel` tab. +- Scalpel provides a terminal with a shell running in the selected virtual environment to easily install packages. +- Creating new virtual environments or adding existing ones can be done via the dedicated GUI. +- All data is stored in the `~/.scalpel` directory. + +## Behavior + +- Scalpel uses the Java [Burp Montoya API](https://portswigger.net/burp/documentation/desktop/extensions) to interact with Burp. +- Scalpel uses Java to handle the dependencies installation, HTTP and GUI for Burp, and communication with Python. +- Scalpel uses [Jep](https://github.com/ninia/jep/) to execute Python from Java. +- Python execution is handled through a task queue in a dedicated thread that will execute one Python task at a time in a thread-safe way. +- All Python hooks are executed through a `_framework.py` file that will activate the selected venv, load the user script file, look for callable objects matching the hooks names (`match, request, response, req_edit_in, res_edit_in, req_edit_out, res_edit_out, req_edit_in_, res_edit_in_, req_edit_out_, res_edit_out_`). +- The `_framework.py` declares callbacks that receive Java objects, convert them to custom easy-to-use Python objects, pass the Python objects to the corresponding user hook, get back the modified Python objects and convert them back to Java objects. +- Java code receives the hook's result and interact with Burp to apply its effects. +- At each task, Scalpel checks whether the user script file changed. If so, it reloads and restarts the interpreter. + +## Python scripting + +- Scalpel uses a single shared interpreter. Then, if any global variables are changed in a hook, their values remain changed in the next hook calls. +- For easy Python scripting, Scalpel provides many utilities described in the [Event Hooks & API]({{< relref "addons-api" >}}) section. + +## Diagram + +Here is a diagram illustating the points above: +{{< figure src="/schematics/scalpel-diagram.svg" >}} diff --git a/docs/src/content/feature-editors.md b/docs/src/content/feature-editors.md new file mode 100644 index 00000000..05173bf5 --- /dev/null +++ b/docs/src/content/feature-editors.md @@ -0,0 +1,137 @@ +--- +title: "Custom Burp editors" +menu: "features" +menu: + features: + weight: 2 +--- + +# Custom Burp Editors + +Scalpel's main _killer feature_ is the ability to **program your own editors** using Python. + +## Table of content + +- [Event hooks](#event-hooks) + 1. [Edit a request](#1-edit-a-request) + 2. [Edit a response](#1-edit-a-response) + 3. [Multiple tabs example](#3-multiple-tabs-example) +- [Binary editors](#binary-editors) + +## Event hooks + +#### 1. Edit a request + +_E.g: A simple script to edit a fully URL encoded query string parameter in a request:_ + +```python +from pyscalpel import Request +from pyscalpel.utils import urldecode, urlencode_all + + +# Hook to initialize the editor's content +def req_edit_in(req: Request) -> bytes | None: + param = req.query.get("filename") + if param is not None: + return urldecode(param) + + # Do not modify the request + return None + +# Hook to update the request from the editor's modified content +def req_edit_out(req: Request, modified_content: bytes) -> Request: + req.query["filename"] = urlencode_all(modified_content) + return req +``` + +- If you open a request with a `filename` query parameter, a `Scalpel` tab should appear in the editor like shown below: {{< figure src="/screenshots/urlencode.png" >}} +- Once your [`req_edit_in()`]({{< relref "addons-api#req_edit_in" >}}) Python hook is invoked, the tab should contain the `filename` parameter's URL decoded content. {{< figure src="/screenshots/decoded.png" >}} +- You can modify it to update the request and thus, include anything you want (e.g: path traversal sequences). {{< figure src="/screenshots/traversal.png" >}} +- When you send the request or switch to another editor tab, your Python hook [`req_edit_out()`]({{< relref "addons-api#req_edit_out" >}}) will be invoked to update the parameter. {{< figure src="/screenshots/updated.png" >}} + + +#### 2. Edit a response + +It is the same process for editing responses: +```py +def res_edit_in(res: Response) -> bytes | None: + # Displays an additional header in the editor + res.headers["X-Python-In-Response-Editor"] = "res_edit_in" + return bytes(res) + + +def res_edit_out(_: Response, text: bytes) -> Response | None: + # Recreate a response from the editor's content + res = Response.from_raw(text) + return res +``` + +
    + +#### 3. Multiple tabs example + +You can have multiple tabs open at the same time. Just **suffix** your function names: + +_E.g: Same script as above but for two parameters: "filename" and "directory"._ + +```python +from pyscalpel import Request +from pyscalpel.utils import urldecode, urlencode_all + +def req_edit_in_filename(req: Request): + param = req.query.get("filename") + if param is not None: + return urldecode(param) + +def req_edit_out_filename(req: Request, text: bytes): + req.query["filename"] = urlencode_all(text) + return req + + +def req_edit_in_directory(req: Request): + param = req.query.get("directory") + if param is not None: + return urldecode(param) + + +def req_edit_out_directory(req: Request, text: bytes): + req.query["directory"] = urlencode_all(text) + return req +``` + +This will result in two open tabs. One for the `filename` parameter and one for the `directory` parameter (see the second image below). +{{< figure src="/screenshots/multiple_params.png" >}} +{{< figure src="/screenshots/multiple_tabs.png" >}} + +
    + +## Binary editors + +{{< readfile file="/generated/api/editors.html" >}} + + +### Example +_E.g.: A simple script displaying requests in a hexadecimal editor and responses in a binary editor:_ +```py +from pyscalpel import Request, Response, editor + + +@editor("hex") +def req_edit_in(req: Request) -> bytes | None: + return bytes(req) + +@editor("binary") +def res_edit_in(res: Response) -> bytes | None: + return bytes(res) +``` +_The hexadecimal editor:_ +{{< figure src="/screenshots/bin-request.png" >}} + +_The binary editor:_ +{{< figure src="/screenshots/bin-response.png" >}} + +
    + +## Further reading + +Learn more about the available hooks in the technical documentation's [Event Hooks & API]({{< relref "addons-api" >}}) section. diff --git a/docs/src/content/feature-http.md b/docs/src/content/feature-http.md new file mode 100644 index 00000000..b799309c --- /dev/null +++ b/docs/src/content/feature-http.md @@ -0,0 +1,77 @@ +--- +title: "Intercept and rewrite HTTP traffic" +menu: "features" +menu: + features: + weight: 1 +--- + +# Event Hooks + +Scalpel scripts hook into Burps's internal mechanisms through [**event hooks**]({{< relref "addons-api" >}}). + +These are implemented as methods with a set of well-known names. +Events receive [`Request`](../pdoc/python/pyscalpel.html#Request), [`Response`](../pdoc/python/pyscalpel.html#Response), [`Flow`](../pdoc/python/pyscalpel.html#Flow) and `bytes` objects as arguments. By modifying these objects, scripts can +change traffic on the fly and program custom request/response editors. + +For instance, here is an script that adds a response +header with the number of seen responses: + +```python +from pyscalpel import Response + +count = 0 + +def response(res: Response) -> Response: + global count + + count += 1 + res.headers["count"] = count + return res +``` + +# Intercept and Rewrite HTTP Traffic + +#### Request / Response + +To intercept requests/responses, implement the [`request()`]({{< relref "addons-api#request" >}}) and [`response()`]({{< relref "addons-api#response" >}}) functions in your script: + +_E.g: Hooks that add an arbitrary header to every request and response:_ + +```python +from pyscalpel import Request, Response + +# Intercept the request +def request(req: Request) -> Request: + # Add an header + req.headers["X-Python-Intercept-Request"] = "request" + # Return the modified request + return req + +# Same for response +def response(res: Response) -> Response: + res.headers["X-Python-Intercept-Response"] = "response" + return res +``` + +
    + +#### Match + +Decide whether to intercept an HTTP message with the [`match()`]({{< relref "addons-api#match" >}}) function: + +_E.g: A match intercepting requests to `localhost` and `127.0.0.1` only:_ + +```python +from pyscalpel import Flow + +# If match() returns true, request(), response(), req_edit_in(), [...] callbacks will be used. +def match(flow: Flow) -> bool: + # True if host is localhost or 127.0.0.1 + return flow.host_is("localhost", "127.0.0.1") +``` + +## Further reading + +- Learn more about the available hooks in the technical documentation's [Event Hooks & API]({{< relref "addons-api" >}}) section. +- Or check out the [Custom Burp Editors]({{< relref "feature-editors" >}}). diff --git a/docs/src/content/overview-faq.md b/docs/src/content/overview-faq.md new file mode 100644 index 00000000..fa52b78c --- /dev/null +++ b/docs/src/content/overview-faq.md @@ -0,0 +1,85 @@ +--- +title: "FAQ" +menu: "overview" +menu: + overview: + weight: 3 +--- + +# FAQ + +## Table of Contents + +1. [Why does Scalpel depend on JDK whereas Burp comes with its own JRE?](#why-does-scalpel-depend-on-jdk-whereas-burp-comes-with-its-own-jre) +2. [Why using Java with Jep to execute Python whereas Burp already supports Python extensions with Jython?](#why-using-java-with-jep-to-execute-python-whereas-burp-already-supports-python-extensions-with-jython) +3. [Once the .jar is loaded, no additional request shows up in the editor](#once-the-jar-is-loaded-no-additional-request-shows-up-in-the-editor) +4. [My distribution/OS comes with an outdated python.](#scalpel-requires-python-310-but-my-distribution-is-outdated-and-i-cant-install-such-recent-python-versions-using-the-package-manager) +5. [Configuring my editor for Python](#how-can-i-configure-my-editor-to-recognize-the-python-library) +6. [I installed Python using the Microsoft Store and Scalpel doesn't work.](#i-installed-python-using-the-microsoft-store-and-scalpel-doesnt-work) + +--- + +### Why does Scalpel depend on JDK whereas Burp comes with its own JRE? + +- Scalpel uses a project called [`jep`](https://github.com/ninia/jep/wiki/) to call Python from Java. `jep` needs a JDK to function. +- If you are curious or need more technical information about Scalpel's implementation, read [How scalpel works]({{< relref "concepts-howscalpelworks" >}}). + +### Why using Java with Jep to execute Python whereas Burp already supports Python extensions with [Jython](https://www.jython.org/)? + +- Jython supports up to Python 2.7. Unfortunately, Python 3 is not handled at all. Python 2.7 is basically a dead language and nobody should still be using it. +- Burp's developers released a [new API](https://portswigger.net/burp/documentation/desktop/extensions/creating) for extensions and deprecated the former one. The new version only supports Java. That's why the most appropriate choice was to reimplement a partial Python scripting support for Burp. + +### Once the .jar is loaded, no additional request shows up in the editor! + +- When first installing Scalpel, the installation of all its dependencies may take a while. Look at the "Output" logs in the Burp "Extension" tab to ensure that the extension has completed. +- Examine the "Errors" logs in the Burp "Extension" tab. There should be an explicit error message with some tips to solve the problem. +- Make sure you followed the [installation guidelines](../install.md). In case you didn't, remove the `~/.scalpel` directory and try one more time. +- If the error message doesn't help, please open a GitHub issue including the "Output" and "Errors" logs, and your system information (OS / Distribution version, CPU architecture, JDK and Python version and installation path, environment variables which Burp runs with, and so forth). + +### Scalpel requires python >=3.8 but my distribution is outdated and I can't install such recent Python versions using the package manager. + +- Try updating your distribution. +- If that is not possible, you must setup a separate Python >=3.8 installation and run Burp with the appropriate environment so this separate installation is used. + > 💡 Tip: Use [`pyenv`](https://github.com/pyenv/pyenv) to easily install different Python versions and switch between them. + +### How can I configure my editor to recognize the Python library + +- Python modules are extracted in `~/.scalpel/.extracted/python`, adding this to your PYTHONPATH should do it. +- For **VSCode users**, Scalpel extracts a .vscode containing the correct settings in the scripts directory, so you can simply open the folder and it should work out of the box. + + - Alternatively, you can use the following `settings.json`: + + ```JSON + { + "terminal.integrated.env.linux": { + "PYTHONPATH": "${env:HOME}/.scalpel/extracted/python:${env:PYTHONPATH}", + "PATH": "${env:HOME}/.scalpel/extracted/python:${env:PATH}" + }, + + // '~' or ${env:HOME} is not supported by this setting, it must be replaced manually. + "python.analysis.extraPaths": ["/.extracted/python"] + } + ``` + +### I installed Python using the Microsoft Store and Scalpel doesn't work. + +- The Microsoft Store Python is a sandboxed version designed for educational purposes. Many of its behaviors are incompatible with Scalpel. To use Scalpel on Windows, it is required to install Python from the [official source](https://www.python.org/downloads/windows/). + +### error: `command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1` + +- Some users encouter this error when the python developpement libraries are missing: + +``` +x86_64-linux-gnu-gcc -pthread -Wno-unused-result -Wsign-compare -DNDEBUG -g -fwrapv -O2 -Wall -g -fstack-protector-strong -Wformat -Werror=format-security -g -fwrapv -O2 -fPIC -DPACKAGE=jep -DUSE_DEALLOC=1 -DJEP_NUMPY_ENABLED=0 -DVERSION=\"4.1.1\" -DPYTHON_LDLIBRARY=\"libpython3.10.so\" -I/usr/lib/jvm/java-11-openjdk-amd64/include -I/usr/lib/jvm/java-11-openjdk-amd64/include/linux -Isrc/main/c/Include -Ibuild/include -I/home//.scalpel/venvs/default/.venv/include -I/usr/include/python3.10 -c src/main/c/Jep/convert_j2p.c -o build/temp.linux-x86_64-cpython-310/src/main/c/Jep/convert_j2p.o + In file included from src/main/c/Include/Jep.h:35, + from src/main/c/Jep/convert_j2p.c:28: + src/main/c/Include/jep_platform.h:35:10: fatal error: Python.h: Aucun fichier ou dossier de ce type + 35 | #include + | ^~~~~~~~~~ + compilation terminated. + error: command '/usr/bin/x86_64-linux-gnu-gcc' failed with exit code 1 + [end of output] +``` + +- Make sure you installed the python3-dev libraries for your python version + https://stackoverflow.com/a/57698471 diff --git a/docs/src/content/overview-installation.md b/docs/src/content/overview-installation.md new file mode 100644 index 00000000..c58f53d5 --- /dev/null +++ b/docs/src/content/overview-installation.md @@ -0,0 +1,74 @@ +--- +title: "Installation" +menu: "overview" +menu: + overview: + weight: 2 +--- + +# Installation + +## Requirements + +- OpenJDK >= `17` +- Python >= `3.8` +- pip +- python-virtualenv + +### Debian-based distributions + +The following packages are required: + +```sh +sudo apt install build-essential python3 python3-dev python3-venv openjdk-17-jdk +``` + +### Fedora / RHEL / CentOS + +The following packages are required: + +```sh +sudo dnf install @development-tools python3 python3-devel python3-virtualenv java-17-openjdk-devel +``` + +### Arch-based distributions + +The following packages are required: + +```sh +sudo pacman -S base-devel python python-pip python-virtualenv jdk-openjdk +``` + +### Windows + +Microsoft Visual C++ >=14.0 is required: +https://visualstudio.microsoft.com/visual-cpp-build-tools/ + +## Step-by-step instructions + +1. Download the latest [JAR release](https://github.com/ambionics/scalpel/releases). + {{< figure src="/screenshots/release.png" >}} + +2. Import the `.jar` to Burp. + {{< figure src="/screenshots/import.png" >}} + +3. Wait for the dependencies to install. + {{< figure src="/screenshots/wait.png" >}} + +4. Once Scalpel is properly initialized, you should get the following. + {{< figure src="/screenshots/init.png" >}} + +5. If the installation was successful, a `Scalpel` tab should show in the Request/Response editor as follows: + {{< figure src="/screenshots/tabs.png" >}} + +6. And also a `Scalpel` tab for configuration to install additional packages via terminal. + {{< figure src="/screenshots/terminal.png" >}} + +Scalpel is now properly installed and initialized! + +> ### 💡 To unload and reload Scalpel, you must restart Burp, simply disabling and re-enabling the extension will **not** work + +## What's next + +- Check the [Usage]({{< relref "overview-usage" >}}) page to get a glimpse of how to use the tool. +- Read this [tutorial]({{< relref "tute-aes" >}}) to see Scalpel in a real use case context. diff --git a/docs/src/content/overview-usage.md b/docs/src/content/overview-usage.md new file mode 100644 index 00000000..e50f5917 --- /dev/null +++ b/docs/src/content/overview-usage.md @@ -0,0 +1,29 @@ +--- +title: "Usage" +menu: "overview" +menu: + overview: + weight: 2 +--- + +# Usage + +Scalpel allows you to programmatically intercept and modify HTTP requests/responses going through Burp, as well as creating custom request/response editors with Python. + +To do so, Scalpel provides a **Burp extension GUI** for scripting and a set of **predefined function names** corresponding to specific actions: + +- [`match`]({{< relref "addons-api#match" >}}): Determine whether an event should be handled by a hook. +- [`request`]({{< relref "addons-api#request" >}}): Intercept and rewrite a request. +- [`response`]({{< relref "addons-api#response" >}}): Intercept and rewrite a response. +- [`req_edit_in`]({{< relref "addons-api#req_edit_in" >}}): Create or update a request editor's content from a request. +- [`req_edit_out`]({{< relref "addons-api#req_edit_out" >}}): Update a request from an editor's modified content. +- [`res_edit_in`]({{< relref "addons-api#res_edit_in" >}}): Create or update a response editor's content from a response. +- [`res_edit_out`]({{< relref "addons-api#res_edit_out" >}}): Update a response from an editor's modified content. + +Simply write a Python script implementing the ones you need and load the file with Scalpel Burp GUI: {{< figure src="/screenshots//choose_script.png" >}} + + +> ### 💡 To get started with Scalpel, see [First steps]({{< relref "tute-first-steps" >}}) +## Further reading + +Learn more about the predefined function names and find examples in the [Features]({{< relref "feature-http" >}}) category. diff --git a/docs/src/content/tute-aes.md b/docs/src/content/tute-aes.md new file mode 100644 index 00000000..8b13e505 --- /dev/null +++ b/docs/src/content/tute-aes.md @@ -0,0 +1,321 @@ +--- +title: "Decrypting custom encryption" +menu: + tutes: + weight: 2 +--- + +# Decrypting custom encryption + +## Context + +An IOT appliance adds an obfuscation layer to its HTTP communications by encrypting the body of its requests and responses with a key. + +On every HTTP request, the program sends two POST parameters: + +- `secret` (the encryption key) +- `encrypted` (the ciphertext). + +Let's solve this problem by using Scalpel! + +It will provide an additional tab in the Repeater which displays the plaintext for every request and response. The plaintext can also be edited. Scalpel will automatically encrypt it when the "Send" button is hit. + +> 💡 Find a mock API to test this case in Scalpel's GitHub repository: [`test/server.js`](https://github.com/ambionics/scalpel/blob/4b935cb29b496f3627a319d963a009dda79a1aa7/test/server.js#L117C1-L118C1). + + + +## Table of content + +1. [Take a look at the target](#1-take-a-look-at-the-target) +2. [Reimplement the encryption / decryption](#2-reimplement-the-encryption--decryption) +3. [Create the script using Scalpel](#3-create-the-script-using-scalpel) +4. [Implement the encryption algorithm](#4-implement-the-encryption-algorithm) +5. [Create custom editors](#5-create-custom-editors) +6. [Filtering requests/responses sent to hooks](#6-filtering-requestsresponses-sent-to-hooks) +7. [Conclusion](#conclusion) + +## 1. Take a look at the target + +Take the time to get familiar with the API code: + +```ts +const { urlencoded } = require("express"); + +const app = require("express")(); + +app.use(urlencoded({ extended: true })); + +const crypto = require("crypto"); + +const derive = (secret) => { + const hasher = crypto.createHash("sha256"); + hasher.update(secret); + const derived_aes_key = hasher.digest().slice(0, 32); + return derived_aes_key; +}; + +const get_cipher_decrypt = (secret, iv = Buffer.alloc(16, 0)) => { + const derived_aes_key = derive(secret); + const cipher = crypto.createDecipheriv("aes-256-cbc", derived_aes_key, iv); + return cipher; +}; + +const get_cipher_encrypt = (secret, iv = Buffer.alloc(16, 0)) => { + const derived_aes_key = derive(secret); + const cipher = crypto.createCipheriv("aes-256-cbc", derived_aes_key, iv); + return cipher; +}; + +const decrypt = (secret, data) => { + const decipher = get_cipher_decrypt(secret); + let decrypted = decipher.update(data, "base64", "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; +}; + +const encrypt = (secret, data) => { + const cipher = get_cipher_encrypt(secret); + let encrypted = cipher.update(data, "utf8", "base64"); + encrypted += cipher.final("base64"); + return encrypted; +}; + +app.post("/encrypt", (req, res) => { + const secret = req.body["secret"]; + const data = req.body["encrypted"]; + + if (data === undefined) { + res.send("No content"); + return; + } + + const decrypted = decrypt(secret, data); + const resContent = `You have sent "${decrypted}" using secret "${secret}"`; + const encrypted = encrypt(secret, resContent); + + res.send(encrypted); +}); + +app.listen(3000, ["localhost"]); +``` + +As shown above, every request content is encrypted using AES, using a secret passed alongside the content, that also encrypt the response. + +In vanilla Burp, editing the request would be very tedious (using `copy to file`). When faced against a case like this, users will either work with custom scripts outside of Burp, use tools like [mitmproxy](https://docs.mitmproxy.org/stable/), write their own Burp Java extension, or give up. + +Scalpel's main objective is to make working around such cases trivial. + +## 2. Reimplement the encryption / decryption + +Before using Scalpel for handling this API's encryption, the first thing to do is to implement the encryption process in Python. + +### Installing Python dependencies + +To work with AES in Python, the `pycryptodome` module is required but not installed by default. All Scalpel Python scripts run in a virtual environment. Fortunately, Scalpel provides a way to switch between venvs and install packages through Burp GUI. + +1. Let's jump to the `Scalpel` tab: + +{{< figure src="/screenshots/terminal.png" >}} + +2. Focus on the left part. You can use this interface to create and select new venvs. + +{{< figure src="/screenshots/venv.png" >}} + +3. Let's create a venv for this use case. Enter a name and press enter: + +{{< figure src="/screenshots/aes-venv.png" >}} +{{< figure src="/screenshots/venv-installing.png" >}} + +4. It is now possible to select it by clicking on its path: + +{{< figure src="/screenshots/select-venv.png" >}} + +5. The central terminal is now activated in the selected venv and can be used to install packages using `pip` in the usual way: + +{{< figure src="/screenshots/venv-pycryptodome.png" >}} + +6. `pycryptodome` is now installed. Let's create the Scalpel script! + +## 3. Create the script using Scalpel + +You can create a new script for Scalpel using the GUI: + +1. Click the `Create new script` button (underlined in red below). + +{{< figure src="/screenshots/create-script.png" >}} + +2. Enter the desired filename. + +{{< figure src="/screenshots/create-script-prompt.png" >}} + +3. Once the file is created, this message will show up: + +{{< figure src="/screenshots/create-script-success.png" >}} + +4. After following this steps, the script should either be opened in your preferred graphical editor or in the terminal provided by Scalpel: + +{{< figure src="/screenshots/create-script-edit.png" >}} + +5. It contains commented hooks declarations. Remove them, as you will rewrite them further in this tutorial. + +## 4. Implement the encryption algorithm + +With `pycryptodome`, the encryption can be written in Python like this: + +```python +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Util.Padding import pad, unpad +from base64 import b64encode, b64decode + +def get_cipher(secret: bytes, iv=bytes(16)): + hasher = SHA256.new() + hasher.update(secret) + derived_aes_key = hasher.digest()[:32] + cipher = AES.new(derived_aes_key, AES.MODE_CBC, iv) + return cipher + + +def decrypt(secret: bytes, data: bytes) -> bytes: + data = b64decode(data) + cipher = get_cipher(secret) + decrypted = cipher.decrypt(data) + return unpad(decrypted, AES.block_size) + + +def encrypt(secret: bytes, data: bytes) -> bytes: + cipher = get_cipher(secret) + padded_data = pad(data, AES.block_size) + encrypted = cipher.encrypt(padded_data) + return b64encode(encrypted) +``` + +## 5. Create custom editors + +The above code can now be used to automatically decrypt your content to plaintext and re-encrypt a modified plaintext. + +As explained in [Editors]({{< relref "feature-editors#1-edit-a-request" >}}), request editors are created by declaring the `req_edit_in` hook: + +```python +def req_edit_in_encrypted(req: Request) -> bytes | None: + ... +``` + +Here, the `_encrypted` suffix was added to the hook name, creating a tab named "encrypted". + +1. Create a request editor. + + This hook is called when Burp opens the request in an editor. It receives the request to edit and returns the bytes to display in the editor. + + In order to display the plain text, the following must be done: + + - Get the secret and the encrypted content from the body. + - Decrypt the content using the secret. + - Return the decrypted bytes. + + ```python + from pyscalpel import Request, Response, Flow + + def req_edit_in_encrypted(req: Request) -> bytes | None: + secret = req.form[b"secret"] + encrypted = req.form[b"encrypted"] + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + ``` + + Once this script is loaded with Scalpel, if you open an encrypted request in Burp, you will see a `Scalpel` tab along the `Pretty`, `Raw`, and `Hex` tabs: + + {{< figure src="/screenshots/encrypty-scalpel-tab.png" >}} + {{< figure src="/screenshots/encrypt-tab-selected.png" >}} + + But there is an issue. Right now, the additional tab cannot be edited since it has no way to encrypt the content back. + +
    + +2. To do so, the `req_edit_out` hook will be handful. + + The `req_edit_out` hook has to implement the opposite behavior of `req_edit_in`, which means: + + - Encrypt the plain text using the secret. + - Replace the old encrypted content in the request. + - Return the new request. + + ```python + def req_edit_out_encrypted(req: Request, text: bytes) -> Request: + secret = req.form[b"secret"] + req.form[b"encrypted"] = encrypt(secret, text) + return req + ``` + + > âš ï¸ When present, the `req_edit_out` suffix **must match** the `req_edit_in` suffix. + > In this tutorial example, the suffix is: `_encrypted` + +
    + +3. Add the hook. You should now be able to edit the plaintext. It will automatically be encrypted using `req_edit_out_encrypted`. + + {{< figure src="/screenshots/encrypt-edited.png" >}} + +
    + +4. After that, it would be nice to decrypt the response to see if the changes were reflected. + + The process is basically the same: + + ```python + def res_edit_in_encrypted(res: Response) -> bytes | None: + secret = res.request.form[b"secret"] + encrypted = res.content + + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + # This is used to edit the response received by the browser in the proxy, but is useless in Repeater/Logger. + def res_edit_out_encrypted(res: Response, text: bytes) -> Response: + secret = res.request.form[b"secret"] + res.content = encrypt(secret, text) + return res + ``` + + {{< figure src="/screenshots/decrypted-response.png" >}} + +
    + +5. You can now edit the responses received by the browser as well. + +## 6. Filtering requests/responses sent to hooks + +Scalpel provides a [match()]({{< relref "addons-api#match" >}}) hook to filter unwanted requests from being treated by your hooks. + +In this case, the encrypted requests are only sent to the `/encrypt` path and contain a `secret`. Thus, better not try to decrypt traffic that don't match these conditions. + +```python +from pyscalpel import Request, Response, Flow + +def match(flow: Flow) -> bool: + return flow.path_is("/encrypt*") and flow.request.form.get(b"secret") is not None +``` + +The above `match` hook receives a [Flow](/api/pyscalpel/http.html#Flow) object. It contains a request. When treating a response, it contains both the response and its initiating request. + +It ensures the initiating request contained a `secret` field and was sent to a path matching `/encrypt*` + +# Conclusion + +In this tutorial, you saw how to decrypt a custom encryption in IoT appliance communications using Scalpel. +This involved: + +- understanding the existing API encryption code +- recreating the encryption process in Python +- installing necessary Python dependencies +- and creating custom editors to handle decryption and re-encryption of modified content. + +This process was implemented for both request and response flows, allowing to view and manipulate the plaintext communication, then encrypt it again before sending. This approach greatly simplifies the process of analyzing and interacting with encrypted data, reducing the need for cumbersome work arounds or additional external tools. + +While this tutorial covers a specific case of AES-256-CBC encryption, have in mind that the main concept and steps can be applied to various other encryption techniques as well. **The only requirement is to understand the encryption process and be able to reproduce it in Python.** + +Scalpel is meant to be a versatile tool in scenarios where custom encryption is encountered. It aims to make data easier to analyze and modify for security testing purposes. diff --git a/docs/src/content/tute-first-steps.md b/docs/src/content/tute-first-steps.md new file mode 100644 index 00000000..5c806a50 --- /dev/null +++ b/docs/src/content/tute-first-steps.md @@ -0,0 +1,140 @@ +--- +title: "First steps" +menu: + tutes: + weight: 1 +--- + + + +# First Steps with Scalpel + +## Introduction + +Welcome to your first steps with Scalpel! This beginner-friendly tutorial will walk you through basic steps to automatically and interactively modify HTTP headers using Scalpel. By the end of this tutorial, you’ll be able to edit the content of the `User-Agent` and `Accept-Language` headers using Scalpel’s hooks and custom editors. + +## Table of content + +1. [Setting up Scalpel](#1-setting-up-scalpel) +2. [Inspecting a GET request](#2-inspecting-a-get-request) +3. [Create a new script](#3-creating-a-new-script) +4. [Manipulating headers](#4-manipulating-headers) +5. [Creating custom editors](#5-creating-custom-editors) +6. [Conclusion](#conclusion) + +## 1. Setting up Scalpel + +Before diving in, ensure Scalpel is [installed]({{< relref "overview-installation" >}}). Once done, you should have a `Scalpel` tab within Burp Suite. +{{< figure src="/screenshots/first-steps-0.png" >}} + +## 2. Inspecting a GET request + +Let’s start by inspecting a basic GET request. Open [https://httpbin.org/get](https://httpbin.org/get) in your Burp suite’s browser. This site simply returns details of the requests it receives, making it perfect for this example case. + +Then, get back to Burp Suite. The GET request should show in your HTTP history. +{{< figure src="/screenshots/first-steps-1.png" >}} + +Send it to Repeater using CTRL-R or right-click → `Send to Repeater` + +## 3. Creating a new script + +1. Select the `Scalpel` tab in the Burp GUI: + {{< figure src="/screenshots/first-steps-2.png" >}} + +2. Create a new script using the dedicated button: + {{< figure src="/screenshots/first-steps-3.png" >}} + ![alt text](error-popup.png) +3. Name it appropriately: + {{< figure src="/screenshots/first-steps-4.png" >}} + +4. Open the new script in a text editor: + {{< figure src="/screenshots/first-steps-5.png" >}} + {{< figure src="/screenshots/first-steps-6.png" >}} + > 💡 The commands ran when selecting a script or opening it can be configured in the **_Settings_ tab** + +## 4. Manipulating headers + +This step will focus on manipulating the `User-Agent` header of the GET request. + +With Scalpel, this header can easily be changed to a custom value. Here’s how: + +```python +from pyscalpel import Request + +def request(req: Request) -> Request: + user_agent = req.headers.get("User-Agent") + + if user_agent: + req.headers["User-Agent"] = "My Custom User-Agent" + + return req +``` + +> 💡 The `request()` function modifies every requests going out of Burp. +> +> This includes the requests from the proxy (browser) and the repeater. + +With the above code, every time you make a GET request, Scalpel will automatically change the `User-Agent` header to “My Custom User-Agentâ€. + +To apply this effect: + +1. Replace your script content with the snippet above. +2. Send the request to [https://httpbin.org/get](https://httpbin.org/get) using Repeater. +3. You should see in the response that your User-Agent header was indeed replaced by `My Custom User-Agent`. + {{< figure src="/screenshots/first-steps-7.png" >}} + +4. The process for modifying a response is the same. Add this to your script: + +```python +from pyscalpel import Response + +def response(res: Response) -> Response: + date = res.headers.get("Date") + + if date: + res.headers["Date"] = "My Custom Date" + + return res +``` + +5. The snippet above changed the `Date` header in response to `My Custom Date`. Send the request again and see the reflected changes: + {{< figure src="/screenshots/first-steps-8.png" >}} + +You now know how to programmatically edit HTTP requests and responses. + +Next, let’s see how to interactively edit parts of a request. + +## 5. Creating custom editors + +Custom editors in Scalpel allow you to interactively change specific parts of a request. Let’s create an editor to change the `Accept-Language` header manually: + +```python +def req_edit_in_accept_language(req: Request) -> bytes | None: + return req.headers.get("Accept-Language", "").encode() + +def req_edit_out_accept_language(req: Request, edited_text: bytes) -> Request: + req.headers["Accept-Language"] = edited_text.decode() + return req +``` + +Thanks to these hooks, when you open a GET request in Burp Suite, you’ll see an additional `Scalpel` tab. This tab enables you to edit the `Accept-Language` header’s content directly. +{{< figure src="/screenshots/first-steps-9.png" >}} + +Once edited, Scalpel will replace the original `Accept-Language` value with your edited version. +{{< figure src="/screenshots/first-steps-10.png" >}} + +## Conclusion + +Congratulations! In this tutorial, you’ve taken your first steps with Scalpel. You’ve learned how to inspect GET requests, manipulate HTTP headers automatically, and create custom editors for interactive edits. + +Remember, Scalpel is a powerful tool with a lot more capabilities. As you become more familiar with its features, you’ll discover its potential to significantly enhance your web security testing workflow. + +--- + +# Further reading + +Find **example use-cases [here]({{< relref "addons-examples" >}})**. + +Read the [**technical documentation**](/pdoc/python/pyscalpel.html). + +See an **advanced tutorial** for a real use case in [**Decrypting custom encryption**]({{< relref "tute-aes" >}}). diff --git a/docs/src/declarations/editors.py b/docs/src/declarations/editors.py new file mode 100644 index 00000000..44d8a81b --- /dev/null +++ b/docs/src/declarations/editors.py @@ -0,0 +1,8 @@ +""" + To display the contents of your tab in a hexadecimal, binary, octal or decimal editor, + the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_in` hook: +""" +from pyscalpel.edit import editor + + +__all__ = ["editor"] diff --git a/docs/src/declarations/events.py b/docs/src/declarations/events.py new file mode 100644 index 00000000..969b87d8 --- /dev/null +++ b/docs/src/declarations/events.py @@ -0,0 +1,85 @@ +from pyscalpel import Request, Response, Flow, MatchEvent + + +def match(flow: Flow, events: MatchEvent) -> bool: + """- Determine whether an event should be handled by a hook. + + - Args: + - flow ([Flow](../pdoc/python3-10/pyscalpel.html#Flow)): The event context (contains request and optional response). + - events ([MatchEvent](../pdoc/python3-10/pyscalpel.html#MatchEvent)): The hook type (request, response, req_edit_in, ...). + + - Returns: + - bool: True if the event must be handled. Otherwise, False. + """ + + +def request(req: Request) -> Request | None: + """- Intercept and rewrite a request. + + - Args: + - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The intercepted request + + - Returns: + - [Request](../pdoc/python3-10/pyscalpel.html#Request) or None: The modified request. Otherwise, None to ignore the request. + """ + + +def response(res: Response) -> Response | None: + """- Intercept and rewrite a response. + + - Args: + - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The intercepted response. + + - Returns: + - [Response](../pdoc/python3-10/pyscalpel.html#Response) or None: The modified response. Otherwise, None to ignore the response. + """ + + +def req_edit_in(req: Request) -> bytes | None: + """- Create or update a request editor's content from a request. + - May be used to decode a request to plaintext. + + - Args: + - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The HTTP request. + + - Returns: + - bytes or None: The editor's contents. + """ + + +def req_edit_out(req: Request, modified_content: bytes) -> Request | None: + """- Update a request from an editor's modified content. + - May be used to encode a request from plaintext (modified_content). + + - Args: + - req ([Request](../pdoc/python3-10/pyscalpel.html#Request)): The original request. + - modified_content (bytes): The editor's content. + + - Returns: + - [Request](../pdoc/python3-10/pyscalpel.html#Request) or None: The new request. + """ + + +def res_edit_in(res: Response) -> bytes | None: + """- Create or update a response editor's content from a response. + - May be used to decode a response to plaintext. + + - Args: + - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The HTTP response. + + - Returns: + - bytes or None: The editor contents. + """ + + +def res_edit_out(res: Response, modified_content: bytes) -> Response | None: + """- Update a response from an editor's modified content. + - May be used to encode a response from plaintext (modified_content). + + - Args: + - res ([Response](../pdoc/python3-10/pyscalpel.html#Response)): The original response. + - modified_content (bytes): The editor's content. + + - Returns: + - [Response](../pdoc/python3-10/pyscalpel.html#Response) or None: The new response. + """ diff --git a/docs/src/examples b/docs/src/examples new file mode 120000 index 00000000..d15735c1 --- /dev/null +++ b/docs/src/examples @@ -0,0 +1 @@ +../../examples \ No newline at end of file diff --git a/docs/src/layouts/_default/single.html b/docs/src/layouts/_default/single.html new file mode 100644 index 00000000..801b6341 --- /dev/null +++ b/docs/src/layouts/_default/single.html @@ -0,0 +1,12 @@ +{{ partial "header" . }} +
    + +
    + {{ partial "outdated" . }} + {{ partial "edit-on-github" . }} + {{ partial "add-anchors" .Content}} +
    +
    +{{ partial "footer.html" . }} diff --git a/docs/src/layouts/partials/add-anchors.html b/docs/src/layouts/partials/add-anchors.html new file mode 100644 index 00000000..f7050f7f --- /dev/null +++ b/docs/src/layouts/partials/add-anchors.html @@ -0,0 +1 @@ +{{ . | replaceRE "()(.+?)" "${1}#  ${3}" | safeHTML }} diff --git a/docs/src/layouts/partials/edit-on-github.html b/docs/src/layouts/partials/edit-on-github.html new file mode 100644 index 00000000..bf73129b --- /dev/null +++ b/docs/src/layouts/partials/edit-on-github.html @@ -0,0 +1,10 @@ +{{ if and .IsPage (ne .Type "api") (not (getenv "DOCS_ARCHIVE")) }} + + + Edit on GitHub + +{{ end }} diff --git a/docs/src/layouts/partials/outdated.html b/docs/src/layouts/partials/outdated.html new file mode 100644 index 00000000..c95b8da3 --- /dev/null +++ b/docs/src/layouts/partials/outdated.html @@ -0,0 +1,9 @@ +{{- if (getenv "DOCS_ARCHIVE") -}} +
    +
    + You are not viewing the most up to date version of the documentation. + Click here + to view the latest version. +
    +
    +{{- end -}} diff --git a/docs/src/layouts/partials/sidebar.html b/docs/src/layouts/partials/sidebar.html new file mode 100644 index 00000000..5f5b0926 --- /dev/null +++ b/docs/src/layouts/partials/sidebar.html @@ -0,0 +1,48 @@ + + + diff --git a/docs/src/layouts/partials/sidemenu.html b/docs/src/layouts/partials/sidemenu.html new file mode 100644 index 00000000..919abf3c --- /dev/null +++ b/docs/src/layouts/partials/sidemenu.html @@ -0,0 +1,21 @@ + diff --git a/docs/src/layouts/shortcodes/asciicast.html b/docs/src/layouts/shortcodes/asciicast.html new file mode 100644 index 00000000..4f78a2a2 --- /dev/null +++ b/docs/src/layouts/shortcodes/asciicast.html @@ -0,0 +1,19 @@ +{{ $file := .Get "file" }} +
    + + {{- if .Get "instructions" -}} + {{- $instructions_file := print "static/recordings/" $file "_instructions.json" -}} + {{ $data := getJSON $instructions_file }} + + {{- end -}} +
    diff --git a/docs/src/layouts/shortcodes/example.html b/docs/src/layouts/shortcodes/example.html new file mode 100644 index 00000000..83a6075d --- /dev/null +++ b/docs/src/layouts/shortcodes/example.html @@ -0,0 +1,4 @@ +
    +{{ highlight (trim (readFile (.Get "src")) "\n\r") (.Get "lang") "" }} +
    {{ (.Get "src" )}}
    +
    diff --git a/docs/src/layouts/shortcodes/note.html b/docs/src/layouts/shortcodes/note.html new file mode 100644 index 00000000..b7c34511 --- /dev/null +++ b/docs/src/layouts/shortcodes/note.html @@ -0,0 +1 @@ +
    {{.Inner | markdownify }}
    diff --git a/docs/src/layouts/shortcodes/readfile.html b/docs/src/layouts/shortcodes/readfile.html new file mode 100644 index 00000000..6860b0f1 --- /dev/null +++ b/docs/src/layouts/shortcodes/readfile.html @@ -0,0 +1,6 @@ +{{$file := .Get "file"}} +{{- if eq (.Get "markdown") "true" -}} +{{- $file | readFile | markdownify -}} +{{- else -}} +{{ $file | readFile | safeHTML }} +{{- end -}} diff --git a/docs/src/static/favicon.ico b/docs/src/static/favicon.ico new file mode 100644 index 00000000..425913c3 Binary files /dev/null and b/docs/src/static/favicon.ico differ diff --git a/docs/src/static/github.svg b/docs/src/static/github.svg new file mode 100644 index 00000000..a8d11740 --- /dev/null +++ b/docs/src/static/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/src/static/logo-docs.svg b/docs/src/static/logo-docs.svg new file mode 100644 index 00000000..05b61f9d --- /dev/null +++ b/docs/src/static/logo-docs.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/src/static/schematics/scalpel-diagram.dark.svg b/docs/src/static/schematics/scalpel-diagram.dark.svg new file mode 100644 index 00000000..d9f143dc --- /dev/null +++ b/docs/src/static/schematics/scalpel-diagram.dark.svg @@ -0,0 +1,4 @@ + + + +
    Burp
    Burp
    Scalpel (Java Extension)
    Scalpel (Java Extens...
    _framework.py
    _framework.py
    Jep
    Jep
    <user script>
    def request ..
    def response ...
    def req_edit_in ...
    def req_edit_out ...
    <user script>...
    Loads and calls
    Loads and calls
    Handles the Python interpreter through native libraries
    Handles the Python interpreter...
    CPython
    CPython
    Activates user virtual env
    Activates user virtual env
    Registers HTTP messqge handlers, editors, returns modified HTTP request/response
    Registers HTTP messqge handlers, editors...
    Calls the extension initializing method

    Provides listeners for HTTP messages interception and messages editors.

    Calls extension
     callbacks 
    Calls the extension initi...
    Provides a Java API to execute Python and automatically convert objects to their Java/Python equivalents.
    Provides a Java API to execute Pytho...
    Convert Burp objects to Pythonic custom objects 
    Convert Burp objects to Pythonic custo...
    Pass Burp objects
    Pass Burp object...
    Pass Burp objects
    Pass Burp object...
    Returns modified Python objects
    Returns modified Python objects
    Convert Pythonic objects to Burp objects
    Convert Pythonic objects to Burp o...
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/docs/src/static/schematics/scalpel-diagram.svg b/docs/src/static/schematics/scalpel-diagram.svg new file mode 100644 index 00000000..40978c00 --- /dev/null +++ b/docs/src/static/schematics/scalpel-diagram.svg @@ -0,0 +1,4 @@ + + + +
    Burp
    Burp
    Scalpel (Java Extension)
    Scalpel (Java Extens...
    _framework.py
    _framework.py
    Jep
    Jep
    <user script>
    def request ..
    def response ...
    def req_edit_in ...
    def req_edit_out ...
    <user script>...
    Loads and calls
    Loads and calls
    Handles the Python interpreter through native libraries
    Handles the Python interpreter...
    CPython
    CPython
    Activates user virtual env
    Activates user virtual env
    Registers HTTP message handlers, editors, returns modified HTTP request/response
    Registers HTTP message handlers, editors...
    Calls the extension initializing method

    Provides listeners for HTTP messages interception and messages editors.

    Calls extension
     callbacks 
    Calls the extension initi...
    Provides a Java API to execute Python and automatically convert objects to their Java/Python equivalents.
    Provides a Java API to execute Pytho...
    Convert Burp objects to Pythonic custom objects 
    Convert Burp objects to Pythonic custo...
    Pass Burp objects
    Pass Burp object...
    Pass Burp objects
    Pass Burp object...
    Returns modified Python objects
    Returns modified Python objects
    Convert Pythonic objects to Burp objects
    Convert Pythonic objects to Burp o...
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/docs/src/static/screenshots/aes-venv.png b/docs/src/static/screenshots/aes-venv.png new file mode 100644 index 00000000..e69e932a Binary files /dev/null and b/docs/src/static/screenshots/aes-venv.png differ diff --git a/docs/src/static/screenshots/bin-request.png b/docs/src/static/screenshots/bin-request.png new file mode 100644 index 00000000..fb20f13d Binary files /dev/null and b/docs/src/static/screenshots/bin-request.png differ diff --git a/docs/src/static/screenshots/bin-response.png b/docs/src/static/screenshots/bin-response.png new file mode 100644 index 00000000..227d00a7 Binary files /dev/null and b/docs/src/static/screenshots/bin-response.png differ diff --git a/docs/src/static/screenshots/choose_script.png b/docs/src/static/screenshots/choose_script.png new file mode 100644 index 00000000..40051eb0 Binary files /dev/null and b/docs/src/static/screenshots/choose_script.png differ diff --git a/docs/src/static/screenshots/create-script-edit.png b/docs/src/static/screenshots/create-script-edit.png new file mode 100644 index 00000000..35f80a81 Binary files /dev/null and b/docs/src/static/screenshots/create-script-edit.png differ diff --git a/docs/src/static/screenshots/create-script-prompt.png b/docs/src/static/screenshots/create-script-prompt.png new file mode 100644 index 00000000..e4a36415 Binary files /dev/null and b/docs/src/static/screenshots/create-script-prompt.png differ diff --git a/docs/src/static/screenshots/create-script-success.png b/docs/src/static/screenshots/create-script-success.png new file mode 100644 index 00000000..6d8a20b9 Binary files /dev/null and b/docs/src/static/screenshots/create-script-success.png differ diff --git a/docs/src/static/screenshots/create-script.png b/docs/src/static/screenshots/create-script.png new file mode 100644 index 00000000..b26607e9 Binary files /dev/null and b/docs/src/static/screenshots/create-script.png differ diff --git a/docs/src/static/screenshots/debug-image-1.png b/docs/src/static/screenshots/debug-image-1.png new file mode 100644 index 00000000..e017b08b Binary files /dev/null and b/docs/src/static/screenshots/debug-image-1.png differ diff --git a/docs/src/static/screenshots/debug-image-2.png b/docs/src/static/screenshots/debug-image-2.png new file mode 100644 index 00000000..383bf381 Binary files /dev/null and b/docs/src/static/screenshots/debug-image-2.png differ diff --git a/docs/src/static/screenshots/debug-image-3.png b/docs/src/static/screenshots/debug-image-3.png new file mode 100644 index 00000000..1128d586 Binary files /dev/null and b/docs/src/static/screenshots/debug-image-3.png differ diff --git a/docs/src/static/screenshots/debug-image.png b/docs/src/static/screenshots/debug-image.png new file mode 100644 index 00000000..4a996c5d Binary files /dev/null and b/docs/src/static/screenshots/debug-image.png differ diff --git a/docs/src/static/screenshots/decoded.png b/docs/src/static/screenshots/decoded.png new file mode 100644 index 00000000..d82f7089 Binary files /dev/null and b/docs/src/static/screenshots/decoded.png differ diff --git a/docs/src/static/screenshots/decrypted-response.png b/docs/src/static/screenshots/decrypted-response.png new file mode 100644 index 00000000..c7a76218 Binary files /dev/null and b/docs/src/static/screenshots/decrypted-response.png differ diff --git a/docs/src/static/screenshots/encrypt-edited.png b/docs/src/static/screenshots/encrypt-edited.png new file mode 100644 index 00000000..2a3bfbdc Binary files /dev/null and b/docs/src/static/screenshots/encrypt-edited.png differ diff --git a/docs/src/static/screenshots/encrypt-tab-selected.png b/docs/src/static/screenshots/encrypt-tab-selected.png new file mode 100644 index 00000000..9e1180bb Binary files /dev/null and b/docs/src/static/screenshots/encrypt-tab-selected.png differ diff --git a/docs/src/static/screenshots/encrypty-scalpel-tab.png b/docs/src/static/screenshots/encrypty-scalpel-tab.png new file mode 100644 index 00000000..852fc478 Binary files /dev/null and b/docs/src/static/screenshots/encrypty-scalpel-tab.png differ diff --git a/docs/src/static/screenshots/error-popup.png b/docs/src/static/screenshots/error-popup.png new file mode 100644 index 00000000..17a68a4f Binary files /dev/null and b/docs/src/static/screenshots/error-popup.png differ diff --git a/docs/src/static/screenshots/first-steps-0.png b/docs/src/static/screenshots/first-steps-0.png new file mode 100644 index 00000000..8c41184d Binary files /dev/null and b/docs/src/static/screenshots/first-steps-0.png differ diff --git a/docs/src/static/screenshots/first-steps-1.png b/docs/src/static/screenshots/first-steps-1.png new file mode 100644 index 00000000..82f9ddcf Binary files /dev/null and b/docs/src/static/screenshots/first-steps-1.png differ diff --git a/docs/src/static/screenshots/first-steps-10.png b/docs/src/static/screenshots/first-steps-10.png new file mode 100644 index 00000000..04e23e04 Binary files /dev/null and b/docs/src/static/screenshots/first-steps-10.png differ diff --git a/docs/src/static/screenshots/first-steps-2.png b/docs/src/static/screenshots/first-steps-2.png new file mode 100644 index 00000000..8c41184d Binary files /dev/null and b/docs/src/static/screenshots/first-steps-2.png differ diff --git a/docs/src/static/screenshots/first-steps-3.png b/docs/src/static/screenshots/first-steps-3.png new file mode 100644 index 00000000..3024b8aa Binary files /dev/null and b/docs/src/static/screenshots/first-steps-3.png differ diff --git a/docs/src/static/screenshots/first-steps-4.png b/docs/src/static/screenshots/first-steps-4.png new file mode 100644 index 00000000..16c95cf4 Binary files /dev/null and b/docs/src/static/screenshots/first-steps-4.png differ diff --git a/docs/src/static/screenshots/first-steps-5.png b/docs/src/static/screenshots/first-steps-5.png new file mode 100644 index 00000000..c6a55c03 Binary files /dev/null and b/docs/src/static/screenshots/first-steps-5.png differ diff --git a/docs/src/static/screenshots/first-steps-6.png b/docs/src/static/screenshots/first-steps-6.png new file mode 100644 index 00000000..9aa8399a Binary files /dev/null and b/docs/src/static/screenshots/first-steps-6.png differ diff --git a/docs/src/static/screenshots/first-steps-7.png b/docs/src/static/screenshots/first-steps-7.png new file mode 100644 index 00000000..c396b2fc Binary files /dev/null and b/docs/src/static/screenshots/first-steps-7.png differ diff --git a/docs/src/static/screenshots/first-steps-8.png b/docs/src/static/screenshots/first-steps-8.png new file mode 100644 index 00000000..6165106b Binary files /dev/null and b/docs/src/static/screenshots/first-steps-8.png differ diff --git a/docs/src/static/screenshots/first-steps-9.png b/docs/src/static/screenshots/first-steps-9.png new file mode 100644 index 00000000..b660ee9b Binary files /dev/null and b/docs/src/static/screenshots/first-steps-9.png differ diff --git a/docs/src/static/screenshots/import.png b/docs/src/static/screenshots/import.png new file mode 100644 index 00000000..1edb1cb1 Binary files /dev/null and b/docs/src/static/screenshots/import.png differ diff --git a/docs/src/static/screenshots/init.png b/docs/src/static/screenshots/init.png new file mode 100644 index 00000000..b1145640 Binary files /dev/null and b/docs/src/static/screenshots/init.png differ diff --git a/docs/src/static/screenshots/mitmproxy.png b/docs/src/static/screenshots/mitmproxy.png new file mode 100644 index 00000000..3fd14b9d Binary files /dev/null and b/docs/src/static/screenshots/mitmproxy.png differ diff --git a/docs/src/static/screenshots/mitmweb.png b/docs/src/static/screenshots/mitmweb.png new file mode 100644 index 00000000..5f0cc925 Binary files /dev/null and b/docs/src/static/screenshots/mitmweb.png differ diff --git a/docs/src/static/screenshots/multiple_params.png b/docs/src/static/screenshots/multiple_params.png new file mode 100644 index 00000000..772df9d6 Binary files /dev/null and b/docs/src/static/screenshots/multiple_params.png differ diff --git a/docs/src/static/screenshots/multiple_tabs.png b/docs/src/static/screenshots/multiple_tabs.png new file mode 100644 index 00000000..4d64b8d0 Binary files /dev/null and b/docs/src/static/screenshots/multiple_tabs.png differ diff --git a/docs/src/static/screenshots/output.png b/docs/src/static/screenshots/output.png new file mode 100644 index 00000000..398e5600 Binary files /dev/null and b/docs/src/static/screenshots/output.png differ diff --git a/docs/src/static/screenshots/release.png b/docs/src/static/screenshots/release.png new file mode 100644 index 00000000..8e9cb937 Binary files /dev/null and b/docs/src/static/screenshots/release.png differ diff --git a/docs/src/static/screenshots/select-venv.png b/docs/src/static/screenshots/select-venv.png new file mode 100644 index 00000000..e13fb41b Binary files /dev/null and b/docs/src/static/screenshots/select-venv.png differ diff --git a/docs/src/static/screenshots/tabs.png b/docs/src/static/screenshots/tabs.png new file mode 100644 index 00000000..e81a3d87 Binary files /dev/null and b/docs/src/static/screenshots/tabs.png differ diff --git a/docs/src/static/screenshots/terminal.png b/docs/src/static/screenshots/terminal.png new file mode 100644 index 00000000..9a0747ab Binary files /dev/null and b/docs/src/static/screenshots/terminal.png differ diff --git a/docs/src/static/screenshots/traversal.png b/docs/src/static/screenshots/traversal.png new file mode 100644 index 00000000..cf4c5a07 Binary files /dev/null and b/docs/src/static/screenshots/traversal.png differ diff --git a/docs/src/static/screenshots/updated.png b/docs/src/static/screenshots/updated.png new file mode 100644 index 00000000..668f1272 Binary files /dev/null and b/docs/src/static/screenshots/updated.png differ diff --git a/docs/src/static/screenshots/urlencode.png b/docs/src/static/screenshots/urlencode.png new file mode 100644 index 00000000..9f3b25df Binary files /dev/null and b/docs/src/static/screenshots/urlencode.png differ diff --git a/docs/src/static/screenshots/venv-installing.png b/docs/src/static/screenshots/venv-installing.png new file mode 100644 index 00000000..d7cc8ebb Binary files /dev/null and b/docs/src/static/screenshots/venv-installing.png differ diff --git a/docs/src/static/screenshots/venv-pycryptodome.png b/docs/src/static/screenshots/venv-pycryptodome.png new file mode 100644 index 00000000..46cd621b Binary files /dev/null and b/docs/src/static/screenshots/venv-pycryptodome.png differ diff --git a/docs/src/static/screenshots/venv.png b/docs/src/static/screenshots/venv.png new file mode 100644 index 00000000..367f3af5 Binary files /dev/null and b/docs/src/static/screenshots/venv.png differ diff --git a/docs/src/static/screenshots/wait.png b/docs/src/static/screenshots/wait.png new file mode 100644 index 00000000..c34aa899 Binary files /dev/null and b/docs/src/static/screenshots/wait.png differ diff --git a/docs/src/themes/mitmproxydocs/archetypes/default.md b/docs/src/themes/mitmproxydocs/archetypes/default.md new file mode 100644 index 00000000..03855e35 --- /dev/null +++ b/docs/src/themes/mitmproxydocs/archetypes/default.md @@ -0,0 +1,4 @@ ++++ +title = "" +date = "" ++++ \ No newline at end of file diff --git a/docs/src/themes/mitmproxydocs/layouts/_default/list.html b/docs/src/themes/mitmproxydocs/layouts/_default/list.html new file mode 100644 index 00000000..b88c94ad --- /dev/null +++ b/docs/src/themes/mitmproxydocs/layouts/_default/list.html @@ -0,0 +1,25 @@ +{{ partial "header.html" . }} +
    +
    +
    +
    +
    +
    + {{ range .Data.Pages.GroupByDate "2006" }} +

    {{ .Key }}

    +
      + {{ range .Pages }} +
    • + - + {{ .Title }} +
    • + {{ end }} +
    + {{ end }} +
    +
    +
    +
    +
    +
    +{{ partial "footer.html" . }} diff --git a/docs/src/themes/mitmproxydocs/layouts/_default/single.html b/docs/src/themes/mitmproxydocs/layouts/_default/single.html new file mode 100644 index 00000000..83016450 --- /dev/null +++ b/docs/src/themes/mitmproxydocs/layouts/_default/single.html @@ -0,0 +1,7 @@ +{{ partial "header.html" . }} +
    +
    + {{ .Content }} +
    +
    +{{ partial "footer.html" . }} diff --git a/docs/src/themes/mitmproxydocs/layouts/partials/footer.html b/docs/src/themes/mitmproxydocs/layouts/partials/footer.html new file mode 100644 index 00000000..308b1d01 --- /dev/null +++ b/docs/src/themes/mitmproxydocs/layouts/partials/footer.html @@ -0,0 +1,2 @@ + + diff --git a/docs/src/themes/mitmproxydocs/layouts/partials/header.html b/docs/src/themes/mitmproxydocs/layouts/partials/header.html new file mode 100644 index 00000000..a665f6a6 --- /dev/null +++ b/docs/src/themes/mitmproxydocs/layouts/partials/header.html @@ -0,0 +1,36 @@ + + + + + + + + + {{ .Title }} + {{ with .Site.Params.description }} + + {{ end }} + {{ with .Site.Params.author }} + + {{ end }} + + {{ $style := resources.Get "style.scss" | toCSS | minify }} + + + {{ if .Params.has_asciinema }} + {{- $styles := resources.Get "asciinema-player.css" | minify | fingerprint }} + + + {{- $styles := resources.Get "asciinema-player.js" | minify | fingerprint }} + + + {{- $styles := resources.Get "asciinema-tutorial.js" | minify | fingerprint }} + + {{ end }} + + {{ range .AlternativeOutputFormats -}} + {{ printf `` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }} + {{ end -}} + {{ hugo.Generator }} + + diff --git a/docs/src/themes/mitmproxydocs/static/css/style.css b/docs/src/themes/mitmproxydocs/static/css/style.css new file mode 100644 index 00000000..d42e307e --- /dev/null +++ b/docs/src/themes/mitmproxydocs/static/css/style.css @@ -0,0 +1,6786 @@ +/* Background */ +.chroma { + color: #f8f8f2; + background-color: #272822; } + +/* Error */ +.chroma .err { + color: #960050; + background-color: #1e0010; } + +/* LineTableTD */ +.chroma .lntd { + vertical-align: top; + padding: 0; + margin: 0; + border: 0; } + +/* LineTable */ +.chroma .lntable { + border-spacing: 0; + padding: 0; + margin: 0; + border: 0; + width: 100%; + overflow: auto; + display: block; } + +/* LineHighlight */ +.chroma .hl { + display: block; + width: 100%; + background-color: #ffffcc; } + +/* LineNumbersTable */ +.chroma .lnt { + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; + display: block; } + +/* LineNumbers */ +.chroma .ln { + margin-right: 0.4em; + padding: 0 0.4em 0 0.4em; } + +/* Keyword */ +.chroma .k { + color: #66d9ef; } + +/* KeywordConstant */ +.chroma .kc { + color: #66d9ef; } + +/* KeywordDeclaration */ +.chroma .kd { + color: #66d9ef; } + +/* KeywordNamespace */ +.chroma .kn { + color: #f92672; } + +/* KeywordPseudo */ +.chroma .kp { + color: #66d9ef; } + +/* KeywordReserved */ +.chroma .kr { + color: #66d9ef; } + +/* KeywordType */ +.chroma .kt { + color: #66d9ef; } + +/* NameAttribute */ +.chroma .na { + color: #a6e22e; } + +/* NameClass */ +.chroma .nc { + color: #a6e22e; } + +/* NameConstant */ +.chroma .no { + color: #66d9ef; } + +/* NameDecorator */ +.chroma .nd { + color: #a6e22e; } + +/* NameException */ +.chroma .ne { + color: #a6e22e; } + +/* NameFunction */ +.chroma .nf { + color: #a6e22e; } + +/* NameOther */ +.chroma .nx { + color: #a6e22e; } + +/* NameTag */ +.chroma .nt { + color: #f92672; } + +/* Literal */ +.chroma .l { + color: #ae81ff; } + +/* LiteralDate */ +.chroma .ld { + color: #e6db74; } + +/* LiteralString */ +.chroma .s { + color: #e6db74; } + +/* LiteralStringAffix */ +.chroma .sa { + color: #e6db74; } + +/* LiteralStringBacktick */ +.chroma .sb { + color: #e6db74; } + +/* LiteralStringChar */ +.chroma .sc { + color: #e6db74; } + +/* LiteralStringDelimiter */ +.chroma .dl { + color: #e6db74; } + +/* LiteralStringDoc */ +.chroma .sd { + color: #e6db74; } + +/* LiteralStringDouble */ +.chroma .s2 { + color: #e6db74; } + +/* LiteralStringEscape */ +.chroma .se { + color: #ae81ff; } + +/* LiteralStringHeredoc */ +.chroma .sh { + color: #e6db74; } + +/* LiteralStringInterpol */ +.chroma .si { + color: #e6db74; } + +/* LiteralStringOther */ +.chroma .sx { + color: #e6db74; } + +/* LiteralStringRegex */ +.chroma .sr { + color: #e6db74; } + +/* LiteralStringSingle */ +.chroma .s1 { + color: #e6db74; } + +/* LiteralStringSymbol */ +.chroma .ss { + color: #e6db74; } + +/* LiteralNumber */ +.chroma .m { + color: #ae81ff; } + +/* LiteralNumberBin */ +.chroma .mb { + color: #ae81ff; } + +/* LiteralNumberFloat */ +.chroma .mf { + color: #ae81ff; } + +/* LiteralNumberHex */ +.chroma .mh { + color: #ae81ff; } + +/* LiteralNumberInteger */ +.chroma .mi { + color: #ae81ff; } + +/* LiteralNumberIntegerLong */ +.chroma .il { + color: #ae81ff; } + +/* LiteralNumberOct */ +.chroma .mo { + color: #ae81ff; } + +/* Operator */ +.chroma .o { + color: #f92672; } + +/* OperatorWord */ +.chroma .ow { + color: #f92672; } + +/* Comment */ +.chroma .c { + color: #75715e; } + +/* CommentHashbang */ +.chroma .ch { + color: #75715e; } + +/* CommentMultiline */ +.chroma .cm { + color: #75715e; } + +/* CommentSingle */ +.chroma .c1 { + color: #75715e; } + +/* CommentSpecial */ +.chroma .cs { + color: #75715e; } + +/* CommentPreproc */ +.chroma .cp { + color: #75715e; } + +/* CommentPreprocFile */ +.chroma .cpf { + color: #75715e; } + +/* GenericDeleted */ +.chroma .gd { + color: #f92672; } + +/* GenericEmph */ +.chroma .ge { + font-style: italic; } + +/* GenericInserted */ +.chroma .gi { + color: #a6e22e; } + +/* GenericStrong */ +.chroma .gs { + font-weight: bold; } + +/* GenericSubheading */ +.chroma .gu { + color: #75715e; } + +.badge { + color: #fff; + background-color: #6c757d; + display: inline-block; + padding: .25em .4em; + font-size: 75%; + font-weight: 1; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.25rem; } + .badge:empty { + display: none; } + +@keyframes spinAround { + from { + transform: rotate(0deg); } + to { + transform: rotate(359deg); } } + +/*! minireset.css v0.0.2 | MIT License | github.com/jgthms/minireset.css */ +html, +body, +p, +ol, +ul, +li, +dl, +dt, +dd, +blockquote, +figure, +fieldset, +legend, +textarea, +pre, +iframe, +hr, +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + padding: 0; } + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: 100%; + font-weight: normal; } + +ul { + list-style: none; } + +button, +input, +select, +textarea { + margin: 0; } + +html { + box-sizing: border-box; } + +* { + box-sizing: inherit; } + *:before, *:after { + box-sizing: inherit; } + +img, +embed, +object, +audio, +video { + max-width: 100%; } + +iframe { + border: 0; } + +table { + border-collapse: collapse; + border-spacing: 0; } + +td, +th { + padding: 0; + text-align: left; } + +html { + background-color: white; + font-size: 16px; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + min-width: 300px; + overflow-x: hidden; + overflow-y: scroll; + text-rendering: optimizeLegibility; + text-size-adjust: 100%; } + +article, +aside, +figure, +footer, +header, +hgroup, +section { + display: block; } + +body, +button, +input, +select, +textarea { + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif, "Font Awesome 5 Free", "Font Awesome 5 Brands"; } + +code, +pre { + -moz-osx-font-smoothing: auto; + -webkit-font-smoothing: auto; + font-family: monospace; } + +body { + color: #4a4a4a; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; } + +a { + color: #3273dc; + cursor: pointer; + text-decoration: none; } + a strong { + color: currentColor; } + a:hover { + color: #363636; } + +code { + background-color: whitesmoke; + color: #ff3860; + font-size: 0.875em; + font-weight: normal; + padding: 0.25em 0.5em 0.25em; } + +hr { + background-color: #dbdbdb; + border: none; + display: block; + height: 1px; + margin: 1.5rem 0; } + +img { + height: auto; + max-width: 100%; } + +input[type="checkbox"], +input[type="radio"] { + vertical-align: baseline; } + +small { + font-size: 0.875em; } + +span { + font-style: inherit; + font-weight: inherit; } + +strong { + color: #363636; + font-weight: 700; } + +pre { + -webkit-overflow-scrolling: touch; + background-color: whitesmoke; + color: #4a4a4a; + font-size: 0.875em; + overflow-x: auto; + padding: 1.25rem 1.5rem; + white-space: pre; + word-wrap: normal; } + pre code { + background-color: transparent; + color: currentColor; + font-size: 1em; + padding: 0; } + +table td, +table th { + text-align: left; + vertical-align: top; } + +table th { + color: #363636; } + +.is-clearfix:after { + clear: both; + content: " "; + display: table; } + +.is-pulled-left { + float: left !important; } + +.is-pulled-right { + float: right !important; } + +.is-clipped { + overflow: hidden !important; } + +.is-overlay { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; } + +.is-size-1 { + font-size: 3rem !important; } + +.is-size-2 { + font-size: 2.5rem !important; } + +.is-size-3 { + font-size: 2rem !important; } + +.is-size-4 { + font-size: 1.5rem !important; } + +.is-size-5 { + font-size: 1.25rem !important; } + +.is-size-6 { + font-size: 1rem !important; } + +.is-size-7 { + font-size: 0.75rem !important; } + +@media screen and (max-width: 768px) { + .is-size-1-mobile { + font-size: 3rem !important; } + .is-size-2-mobile { + font-size: 2.5rem !important; } + .is-size-3-mobile { + font-size: 2rem !important; } + .is-size-4-mobile { + font-size: 1.5rem !important; } + .is-size-5-mobile { + font-size: 1.25rem !important; } + .is-size-6-mobile { + font-size: 1rem !important; } + .is-size-7-mobile { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 769px), print { + .is-size-1-tablet { + font-size: 3rem !important; } + .is-size-2-tablet { + font-size: 2.5rem !important; } + .is-size-3-tablet { + font-size: 2rem !important; } + .is-size-4-tablet { + font-size: 1.5rem !important; } + .is-size-5-tablet { + font-size: 1.25rem !important; } + .is-size-6-tablet { + font-size: 1rem !important; } + .is-size-7-tablet { + font-size: 0.75rem !important; } } + +@media screen and (max-width: 1023px) { + .is-size-1-touch { + font-size: 3rem !important; } + .is-size-2-touch { + font-size: 2.5rem !important; } + .is-size-3-touch { + font-size: 2rem !important; } + .is-size-4-touch { + font-size: 1.5rem !important; } + .is-size-5-touch { + font-size: 1.25rem !important; } + .is-size-6-touch { + font-size: 1rem !important; } + .is-size-7-touch { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 1024px) { + .is-size-1-desktop { + font-size: 3rem !important; } + .is-size-2-desktop { + font-size: 2.5rem !important; } + .is-size-3-desktop { + font-size: 2rem !important; } + .is-size-4-desktop { + font-size: 1.5rem !important; } + .is-size-5-desktop { + font-size: 1.25rem !important; } + .is-size-6-desktop { + font-size: 1rem !important; } + .is-size-7-desktop { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 1216px) { + .is-size-1-widescreen { + font-size: 3rem !important; } + .is-size-2-widescreen { + font-size: 2.5rem !important; } + .is-size-3-widescreen { + font-size: 2rem !important; } + .is-size-4-widescreen { + font-size: 1.5rem !important; } + .is-size-5-widescreen { + font-size: 1.25rem !important; } + .is-size-6-widescreen { + font-size: 1rem !important; } + .is-size-7-widescreen { + font-size: 0.75rem !important; } } + +@media screen and (min-width: 1408px) { + .is-size-1-fullhd { + font-size: 3rem !important; } + .is-size-2-fullhd { + font-size: 2.5rem !important; } + .is-size-3-fullhd { + font-size: 2rem !important; } + .is-size-4-fullhd { + font-size: 1.5rem !important; } + .is-size-5-fullhd { + font-size: 1.25rem !important; } + .is-size-6-fullhd { + font-size: 1rem !important; } + .is-size-7-fullhd { + font-size: 0.75rem !important; } } + +.has-text-centered { + text-align: center !important; } + +@media screen and (max-width: 768px) { + .has-text-centered-mobile { + text-align: center !important; } } + +@media screen and (min-width: 769px), print { + .has-text-centered-tablet { + text-align: center !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-centered-tablet-only { + text-align: center !important; } } + +@media screen and (max-width: 1023px) { + .has-text-centered-touch { + text-align: center !important; } } + +@media screen and (min-width: 1024px) { + .has-text-centered-desktop { + text-align: center !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-centered-desktop-only { + text-align: center !important; } } + +@media screen and (min-width: 1216px) { + .has-text-centered-widescreen { + text-align: center !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-centered-widescreen-only { + text-align: center !important; } } + +@media screen and (min-width: 1408px) { + .has-text-centered-fullhd { + text-align: center !important; } } + +.has-text-justified { + text-align: justify !important; } + +@media screen and (max-width: 768px) { + .has-text-justified-mobile { + text-align: justify !important; } } + +@media screen and (min-width: 769px), print { + .has-text-justified-tablet { + text-align: justify !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-justified-tablet-only { + text-align: justify !important; } } + +@media screen and (max-width: 1023px) { + .has-text-justified-touch { + text-align: justify !important; } } + +@media screen and (min-width: 1024px) { + .has-text-justified-desktop { + text-align: justify !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-justified-desktop-only { + text-align: justify !important; } } + +@media screen and (min-width: 1216px) { + .has-text-justified-widescreen { + text-align: justify !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-justified-widescreen-only { + text-align: justify !important; } } + +@media screen and (min-width: 1408px) { + .has-text-justified-fullhd { + text-align: justify !important; } } + +.has-text-left { + text-align: left !important; } + +@media screen and (max-width: 768px) { + .has-text-left-mobile { + text-align: left !important; } } + +@media screen and (min-width: 769px), print { + .has-text-left-tablet { + text-align: left !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-left-tablet-only { + text-align: left !important; } } + +@media screen and (max-width: 1023px) { + .has-text-left-touch { + text-align: left !important; } } + +@media screen and (min-width: 1024px) { + .has-text-left-desktop { + text-align: left !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-left-desktop-only { + text-align: left !important; } } + +@media screen and (min-width: 1216px) { + .has-text-left-widescreen { + text-align: left !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-left-widescreen-only { + text-align: left !important; } } + +@media screen and (min-width: 1408px) { + .has-text-left-fullhd { + text-align: left !important; } } + +.has-text-right { + text-align: right !important; } + +@media screen and (max-width: 768px) { + .has-text-right-mobile { + text-align: right !important; } } + +@media screen and (min-width: 769px), print { + .has-text-right-tablet { + text-align: right !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .has-text-right-tablet-only { + text-align: right !important; } } + +@media screen and (max-width: 1023px) { + .has-text-right-touch { + text-align: right !important; } } + +@media screen and (min-width: 1024px) { + .has-text-right-desktop { + text-align: right !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .has-text-right-desktop-only { + text-align: right !important; } } + +@media screen and (min-width: 1216px) { + .has-text-right-widescreen { + text-align: right !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .has-text-right-widescreen-only { + text-align: right !important; } } + +@media screen and (min-width: 1408px) { + .has-text-right-fullhd { + text-align: right !important; } } + +.is-capitalized { + text-transform: capitalize !important; } + +.is-lowercase { + text-transform: lowercase !important; } + +.is-uppercase { + text-transform: uppercase !important; } + +.has-text-white { + color: white !important; } + +a.has-text-white:hover, a.has-text-white:focus { + color: #e6e6e6 !important; } + +.has-text-black { + color: #0a0a0a !important; } + +a.has-text-black:hover, a.has-text-black:focus { + color: black !important; } + +.has-text-light { + color: whitesmoke !important; } + +a.has-text-light:hover, a.has-text-light:focus { + color: #dbdbdb !important; } + +.has-text-dark { + color: #363636 !important; } + +a.has-text-dark:hover, a.has-text-dark:focus { + color: #1c1c1c !important; } + +.has-text-primary { + color: #C93312 !important; } + +a.has-text-primary:hover, a.has-text-primary:focus { + color: #9a270e !important; } + +.has-text-link { + color: #3273dc !important; } + +a.has-text-link:hover, a.has-text-link:focus { + color: #205bbc !important; } + +.has-text-info { + color: #209cee !important; } + +a.has-text-info:hover, a.has-text-info:focus { + color: #0f81cc !important; } + +.has-text-success { + color: #23d160 !important; } + +a.has-text-success:hover, a.has-text-success:focus { + color: #1ca64c !important; } + +.has-text-warning { + color: #ffdd57 !important; } + +a.has-text-warning:hover, a.has-text-warning:focus { + color: #ffd324 !important; } + +.has-text-danger { + color: #ff3860 !important; } + +a.has-text-danger:hover, a.has-text-danger:focus { + color: #ff0537 !important; } + +.has-text-black-bis { + color: #121212 !important; } + +.has-text-black-ter { + color: #242424 !important; } + +.has-text-grey-darker { + color: #363636 !important; } + +.has-text-grey-dark { + color: #4a4a4a !important; } + +.has-text-grey { + color: #7a7a7a !important; } + +.has-text-grey-light { + color: #b5b5b5 !important; } + +.has-text-grey-lighter { + color: #dbdbdb !important; } + +.has-text-white-ter { + color: whitesmoke !important; } + +.has-text-white-bis { + color: #fafafa !important; } + +.has-text-weight-light { + font-weight: 300 !important; } + +.has-text-weight-normal { + font-weight: 400 !important; } + +.has-text-weight-semibold { + font-weight: 600 !important; } + +.has-text-weight-bold { + font-weight: 700 !important; } + +.is-block { + display: block !important; } + +@media screen and (max-width: 768px) { + .is-block-mobile { + display: block !important; } } + +@media screen and (min-width: 769px), print { + .is-block-tablet { + display: block !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-block-tablet-only { + display: block !important; } } + +@media screen and (max-width: 1023px) { + .is-block-touch { + display: block !important; } } + +@media screen and (min-width: 1024px) { + .is-block-desktop { + display: block !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-block-desktop-only { + display: block !important; } } + +@media screen and (min-width: 1216px) { + .is-block-widescreen { + display: block !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-block-widescreen-only { + display: block !important; } } + +@media screen and (min-width: 1408px) { + .is-block-fullhd { + display: block !important; } } + +.is-flex { + display: flex !important; } + +@media screen and (max-width: 768px) { + .is-flex-mobile { + display: flex !important; } } + +@media screen and (min-width: 769px), print { + .is-flex-tablet { + display: flex !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-flex-tablet-only { + display: flex !important; } } + +@media screen and (max-width: 1023px) { + .is-flex-touch { + display: flex !important; } } + +@media screen and (min-width: 1024px) { + .is-flex-desktop { + display: flex !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-flex-desktop-only { + display: flex !important; } } + +@media screen and (min-width: 1216px) { + .is-flex-widescreen { + display: flex !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-flex-widescreen-only { + display: flex !important; } } + +@media screen and (min-width: 1408px) { + .is-flex-fullhd { + display: flex !important; } } + +.is-inline { + display: inline !important; } + +@media screen and (max-width: 768px) { + .is-inline-mobile { + display: inline !important; } } + +@media screen and (min-width: 769px), print { + .is-inline-tablet { + display: inline !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-inline-tablet-only { + display: inline !important; } } + +@media screen and (max-width: 1023px) { + .is-inline-touch { + display: inline !important; } } + +@media screen and (min-width: 1024px) { + .is-inline-desktop { + display: inline !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-inline-desktop-only { + display: inline !important; } } + +@media screen and (min-width: 1216px) { + .is-inline-widescreen { + display: inline !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-inline-widescreen-only { + display: inline !important; } } + +@media screen and (min-width: 1408px) { + .is-inline-fullhd { + display: inline !important; } } + +.is-inline-block { + display: inline-block !important; } + +@media screen and (max-width: 768px) { + .is-inline-block-mobile { + display: inline-block !important; } } + +@media screen and (min-width: 769px), print { + .is-inline-block-tablet { + display: inline-block !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-inline-block-tablet-only { + display: inline-block !important; } } + +@media screen and (max-width: 1023px) { + .is-inline-block-touch { + display: inline-block !important; } } + +@media screen and (min-width: 1024px) { + .is-inline-block-desktop { + display: inline-block !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-inline-block-desktop-only { + display: inline-block !important; } } + +@media screen and (min-width: 1216px) { + .is-inline-block-widescreen { + display: inline-block !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-inline-block-widescreen-only { + display: inline-block !important; } } + +@media screen and (min-width: 1408px) { + .is-inline-block-fullhd { + display: inline-block !important; } } + +.is-inline-flex { + display: inline-flex !important; } + +@media screen and (max-width: 768px) { + .is-inline-flex-mobile { + display: inline-flex !important; } } + +@media screen and (min-width: 769px), print { + .is-inline-flex-tablet { + display: inline-flex !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-inline-flex-tablet-only { + display: inline-flex !important; } } + +@media screen and (max-width: 1023px) { + .is-inline-flex-touch { + display: inline-flex !important; } } + +@media screen and (min-width: 1024px) { + .is-inline-flex-desktop { + display: inline-flex !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-inline-flex-desktop-only { + display: inline-flex !important; } } + +@media screen and (min-width: 1216px) { + .is-inline-flex-widescreen { + display: inline-flex !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-inline-flex-widescreen-only { + display: inline-flex !important; } } + +@media screen and (min-width: 1408px) { + .is-inline-flex-fullhd { + display: inline-flex !important; } } + +.is-hidden { + display: none !important; } + +@media screen and (max-width: 768px) { + .is-hidden-mobile { + display: none !important; } } + +@media screen and (min-width: 769px), print { + .is-hidden-tablet { + display: none !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-hidden-tablet-only { + display: none !important; } } + +@media screen and (max-width: 1023px) { + .is-hidden-touch { + display: none !important; } } + +@media screen and (min-width: 1024px) { + .is-hidden-desktop { + display: none !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-hidden-desktop-only { + display: none !important; } } + +@media screen and (min-width: 1216px) { + .is-hidden-widescreen { + display: none !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-hidden-widescreen-only { + display: none !important; } } + +@media screen and (min-width: 1408px) { + .is-hidden-fullhd { + display: none !important; } } + +.is-invisible { + visibility: hidden !important; } + +@media screen and (max-width: 768px) { + .is-invisible-mobile { + visibility: hidden !important; } } + +@media screen and (min-width: 769px), print { + .is-invisible-tablet { + visibility: hidden !important; } } + +@media screen and (min-width: 769px) and (max-width: 1023px) { + .is-invisible-tablet-only { + visibility: hidden !important; } } + +@media screen and (max-width: 1023px) { + .is-invisible-touch { + visibility: hidden !important; } } + +@media screen and (min-width: 1024px) { + .is-invisible-desktop { + visibility: hidden !important; } } + +@media screen and (min-width: 1024px) and (max-width: 1215px) { + .is-invisible-desktop-only { + visibility: hidden !important; } } + +@media screen and (min-width: 1216px) { + .is-invisible-widescreen { + visibility: hidden !important; } } + +@media screen and (min-width: 1216px) and (max-width: 1407px) { + .is-invisible-widescreen-only { + visibility: hidden !important; } } + +@media screen and (min-width: 1408px) { + .is-invisible-fullhd { + visibility: hidden !important; } } + +.is-marginless { + margin: 0 !important; } + +.is-paddingless { + padding: 0 !important; } + +.is-radiusless { + border-radius: 0 !important; } + +.is-shadowless { + box-shadow: none !important; } + +.is-unselectable { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + +.column { + display: block; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 1; + padding: 0.75rem; } + .columns.is-mobile > .column.is-narrow { + flex: none; } + .columns.is-mobile > .column.is-full { + flex: none; + width: 100%; } + .columns.is-mobile > .column.is-three-quarters { + flex: none; + width: 75%; } + .columns.is-mobile > .column.is-two-thirds { + flex: none; + width: 66.6666%; } + .columns.is-mobile > .column.is-half { + flex: none; + width: 50%; } + .columns.is-mobile > .column.is-one-third { + flex: none; + width: 33.3333%; } + .columns.is-mobile > .column.is-one-quarter { + flex: none; + width: 25%; } + .columns.is-mobile > .column.is-one-fifth { + flex: none; + width: 20%; } + .columns.is-mobile > .column.is-two-fifths { + flex: none; + width: 40%; } + .columns.is-mobile > .column.is-three-fifths { + flex: none; + width: 60%; } + .columns.is-mobile > .column.is-four-fifths { + flex: none; + width: 80%; } + .columns.is-mobile > .column.is-offset-three-quarters { + margin-left: 75%; } + .columns.is-mobile > .column.is-offset-two-thirds { + margin-left: 66.6666%; } + .columns.is-mobile > .column.is-offset-half { + margin-left: 50%; } + .columns.is-mobile > .column.is-offset-one-third { + margin-left: 33.3333%; } + .columns.is-mobile > .column.is-offset-one-quarter { + margin-left: 25%; } + .columns.is-mobile > .column.is-offset-one-fifth { + margin-left: 20%; } + .columns.is-mobile > .column.is-offset-two-fifths { + margin-left: 40%; } + .columns.is-mobile > .column.is-offset-three-fifths { + margin-left: 60%; } + .columns.is-mobile > .column.is-offset-four-fifths { + margin-left: 80%; } + .columns.is-mobile > .column.is-1 { + flex: none; + width: 8.33333%; } + .columns.is-mobile > .column.is-offset-1 { + margin-left: 8.33333%; } + .columns.is-mobile > .column.is-2 { + flex: none; + width: 16.66667%; } + .columns.is-mobile > .column.is-offset-2 { + margin-left: 16.66667%; } + .columns.is-mobile > .column.is-3 { + flex: none; + width: 25%; } + .columns.is-mobile > .column.is-offset-3 { + margin-left: 25%; } + .columns.is-mobile > .column.is-4 { + flex: none; + width: 33.33333%; } + .columns.is-mobile > .column.is-offset-4 { + margin-left: 33.33333%; } + .columns.is-mobile > .column.is-5 { + flex: none; + width: 41.66667%; } + .columns.is-mobile > .column.is-offset-5 { + margin-left: 41.66667%; } + .columns.is-mobile > .column.is-6 { + flex: none; + width: 50%; } + .columns.is-mobile > .column.is-offset-6 { + margin-left: 50%; } + .columns.is-mobile > .column.is-7 { + flex: none; + width: 58.33333%; } + .columns.is-mobile > .column.is-offset-7 { + margin-left: 58.33333%; } + .columns.is-mobile > .column.is-8 { + flex: none; + width: 66.66667%; } + .columns.is-mobile > .column.is-offset-8 { + margin-left: 66.66667%; } + .columns.is-mobile > .column.is-9 { + flex: none; + width: 75%; } + .columns.is-mobile > .column.is-offset-9 { + margin-left: 75%; } + .columns.is-mobile > .column.is-10 { + flex: none; + width: 83.33333%; } + .columns.is-mobile > .column.is-offset-10 { + margin-left: 83.33333%; } + .columns.is-mobile > .column.is-11 { + flex: none; + width: 91.66667%; } + .columns.is-mobile > .column.is-offset-11 { + margin-left: 91.66667%; } + .columns.is-mobile > .column.is-12 { + flex: none; + width: 100%; } + .columns.is-mobile > .column.is-offset-12 { + margin-left: 100%; } + @media screen and (max-width: 768px) { + .column.is-narrow-mobile { + flex: none; } + .column.is-full-mobile { + flex: none; + width: 100%; } + .column.is-three-quarters-mobile { + flex: none; + width: 75%; } + .column.is-two-thirds-mobile { + flex: none; + width: 66.6666%; } + .column.is-half-mobile { + flex: none; + width: 50%; } + .column.is-one-third-mobile { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-mobile { + flex: none; + width: 25%; } + .column.is-one-fifth-mobile { + flex: none; + width: 20%; } + .column.is-two-fifths-mobile { + flex: none; + width: 40%; } + .column.is-three-fifths-mobile { + flex: none; + width: 60%; } + .column.is-four-fifths-mobile { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-mobile { + margin-left: 75%; } + .column.is-offset-two-thirds-mobile { + margin-left: 66.6666%; } + .column.is-offset-half-mobile { + margin-left: 50%; } + .column.is-offset-one-third-mobile { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-mobile { + margin-left: 25%; } + .column.is-offset-one-fifth-mobile { + margin-left: 20%; } + .column.is-offset-two-fifths-mobile { + margin-left: 40%; } + .column.is-offset-three-fifths-mobile { + margin-left: 60%; } + .column.is-offset-four-fifths-mobile { + margin-left: 80%; } + .column.is-1-mobile { + flex: none; + width: 8.33333%; } + .column.is-offset-1-mobile { + margin-left: 8.33333%; } + .column.is-2-mobile { + flex: none; + width: 16.66667%; } + .column.is-offset-2-mobile { + margin-left: 16.66667%; } + .column.is-3-mobile { + flex: none; + width: 25%; } + .column.is-offset-3-mobile { + margin-left: 25%; } + .column.is-4-mobile { + flex: none; + width: 33.33333%; } + .column.is-offset-4-mobile { + margin-left: 33.33333%; } + .column.is-5-mobile { + flex: none; + width: 41.66667%; } + .column.is-offset-5-mobile { + margin-left: 41.66667%; } + .column.is-6-mobile { + flex: none; + width: 50%; } + .column.is-offset-6-mobile { + margin-left: 50%; } + .column.is-7-mobile { + flex: none; + width: 58.33333%; } + .column.is-offset-7-mobile { + margin-left: 58.33333%; } + .column.is-8-mobile { + flex: none; + width: 66.66667%; } + .column.is-offset-8-mobile { + margin-left: 66.66667%; } + .column.is-9-mobile { + flex: none; + width: 75%; } + .column.is-offset-9-mobile { + margin-left: 75%; } + .column.is-10-mobile { + flex: none; + width: 83.33333%; } + .column.is-offset-10-mobile { + margin-left: 83.33333%; } + .column.is-11-mobile { + flex: none; + width: 91.66667%; } + .column.is-offset-11-mobile { + margin-left: 91.66667%; } + .column.is-12-mobile { + flex: none; + width: 100%; } + .column.is-offset-12-mobile { + margin-left: 100%; } } + @media screen and (min-width: 769px), print { + .column.is-narrow, .column.is-narrow-tablet { + flex: none; } + .column.is-full, .column.is-full-tablet { + flex: none; + width: 100%; } + .column.is-three-quarters, .column.is-three-quarters-tablet { + flex: none; + width: 75%; } + .column.is-two-thirds, .column.is-two-thirds-tablet { + flex: none; + width: 66.6666%; } + .column.is-half, .column.is-half-tablet { + flex: none; + width: 50%; } + .column.is-one-third, .column.is-one-third-tablet { + flex: none; + width: 33.3333%; } + .column.is-one-quarter, .column.is-one-quarter-tablet { + flex: none; + width: 25%; } + .column.is-one-fifth, .column.is-one-fifth-tablet { + flex: none; + width: 20%; } + .column.is-two-fifths, .column.is-two-fifths-tablet { + flex: none; + width: 40%; } + .column.is-three-fifths, .column.is-three-fifths-tablet { + flex: none; + width: 60%; } + .column.is-four-fifths, .column.is-four-fifths-tablet { + flex: none; + width: 80%; } + .column.is-offset-three-quarters, .column.is-offset-three-quarters-tablet { + margin-left: 75%; } + .column.is-offset-two-thirds, .column.is-offset-two-thirds-tablet { + margin-left: 66.6666%; } + .column.is-offset-half, .column.is-offset-half-tablet { + margin-left: 50%; } + .column.is-offset-one-third, .column.is-offset-one-third-tablet { + margin-left: 33.3333%; } + .column.is-offset-one-quarter, .column.is-offset-one-quarter-tablet { + margin-left: 25%; } + .column.is-offset-one-fifth, .column.is-offset-one-fifth-tablet { + margin-left: 20%; } + .column.is-offset-two-fifths, .column.is-offset-two-fifths-tablet { + margin-left: 40%; } + .column.is-offset-three-fifths, .column.is-offset-three-fifths-tablet { + margin-left: 60%; } + .column.is-offset-four-fifths, .column.is-offset-four-fifths-tablet { + margin-left: 80%; } + .column.is-1, .column.is-1-tablet { + flex: none; + width: 8.33333%; } + .column.is-offset-1, .column.is-offset-1-tablet { + margin-left: 8.33333%; } + .column.is-2, .column.is-2-tablet { + flex: none; + width: 16.66667%; } + .column.is-offset-2, .column.is-offset-2-tablet { + margin-left: 16.66667%; } + .column.is-3, .column.is-3-tablet { + flex: none; + width: 25%; } + .column.is-offset-3, .column.is-offset-3-tablet { + margin-left: 25%; } + .column.is-4, .column.is-4-tablet { + flex: none; + width: 33.33333%; } + .column.is-offset-4, .column.is-offset-4-tablet { + margin-left: 33.33333%; } + .column.is-5, .column.is-5-tablet { + flex: none; + width: 41.66667%; } + .column.is-offset-5, .column.is-offset-5-tablet { + margin-left: 41.66667%; } + .column.is-6, .column.is-6-tablet { + flex: none; + width: 50%; } + .column.is-offset-6, .column.is-offset-6-tablet { + margin-left: 50%; } + .column.is-7, .column.is-7-tablet { + flex: none; + width: 58.33333%; } + .column.is-offset-7, .column.is-offset-7-tablet { + margin-left: 58.33333%; } + .column.is-8, .column.is-8-tablet { + flex: none; + width: 66.66667%; } + .column.is-offset-8, .column.is-offset-8-tablet { + margin-left: 66.66667%; } + .column.is-9, .column.is-9-tablet { + flex: none; + width: 75%; } + .column.is-offset-9, .column.is-offset-9-tablet { + margin-left: 75%; } + .column.is-10, .column.is-10-tablet { + flex: none; + width: 83.33333%; } + .column.is-offset-10, .column.is-offset-10-tablet { + margin-left: 83.33333%; } + .column.is-11, .column.is-11-tablet { + flex: none; + width: 91.66667%; } + .column.is-offset-11, .column.is-offset-11-tablet { + margin-left: 91.66667%; } + .column.is-12, .column.is-12-tablet { + flex: none; + width: 100%; } + .column.is-offset-12, .column.is-offset-12-tablet { + margin-left: 100%; } } + @media screen and (max-width: 1023px) { + .column.is-narrow-touch { + flex: none; } + .column.is-full-touch { + flex: none; + width: 100%; } + .column.is-three-quarters-touch { + flex: none; + width: 75%; } + .column.is-two-thirds-touch { + flex: none; + width: 66.6666%; } + .column.is-half-touch { + flex: none; + width: 50%; } + .column.is-one-third-touch { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-touch { + flex: none; + width: 25%; } + .column.is-one-fifth-touch { + flex: none; + width: 20%; } + .column.is-two-fifths-touch { + flex: none; + width: 40%; } + .column.is-three-fifths-touch { + flex: none; + width: 60%; } + .column.is-four-fifths-touch { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-touch { + margin-left: 75%; } + .column.is-offset-two-thirds-touch { + margin-left: 66.6666%; } + .column.is-offset-half-touch { + margin-left: 50%; } + .column.is-offset-one-third-touch { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-touch { + margin-left: 25%; } + .column.is-offset-one-fifth-touch { + margin-left: 20%; } + .column.is-offset-two-fifths-touch { + margin-left: 40%; } + .column.is-offset-three-fifths-touch { + margin-left: 60%; } + .column.is-offset-four-fifths-touch { + margin-left: 80%; } + .column.is-1-touch { + flex: none; + width: 8.33333%; } + .column.is-offset-1-touch { + margin-left: 8.33333%; } + .column.is-2-touch { + flex: none; + width: 16.66667%; } + .column.is-offset-2-touch { + margin-left: 16.66667%; } + .column.is-3-touch { + flex: none; + width: 25%; } + .column.is-offset-3-touch { + margin-left: 25%; } + .column.is-4-touch { + flex: none; + width: 33.33333%; } + .column.is-offset-4-touch { + margin-left: 33.33333%; } + .column.is-5-touch { + flex: none; + width: 41.66667%; } + .column.is-offset-5-touch { + margin-left: 41.66667%; } + .column.is-6-touch { + flex: none; + width: 50%; } + .column.is-offset-6-touch { + margin-left: 50%; } + .column.is-7-touch { + flex: none; + width: 58.33333%; } + .column.is-offset-7-touch { + margin-left: 58.33333%; } + .column.is-8-touch { + flex: none; + width: 66.66667%; } + .column.is-offset-8-touch { + margin-left: 66.66667%; } + .column.is-9-touch { + flex: none; + width: 75%; } + .column.is-offset-9-touch { + margin-left: 75%; } + .column.is-10-touch { + flex: none; + width: 83.33333%; } + .column.is-offset-10-touch { + margin-left: 83.33333%; } + .column.is-11-touch { + flex: none; + width: 91.66667%; } + .column.is-offset-11-touch { + margin-left: 91.66667%; } + .column.is-12-touch { + flex: none; + width: 100%; } + .column.is-offset-12-touch { + margin-left: 100%; } } + @media screen and (min-width: 1024px) { + .column.is-narrow-desktop { + flex: none; } + .column.is-full-desktop { + flex: none; + width: 100%; } + .column.is-three-quarters-desktop { + flex: none; + width: 75%; } + .column.is-two-thirds-desktop { + flex: none; + width: 66.6666%; } + .column.is-half-desktop { + flex: none; + width: 50%; } + .column.is-one-third-desktop { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-desktop { + flex: none; + width: 25%; } + .column.is-one-fifth-desktop { + flex: none; + width: 20%; } + .column.is-two-fifths-desktop { + flex: none; + width: 40%; } + .column.is-three-fifths-desktop { + flex: none; + width: 60%; } + .column.is-four-fifths-desktop { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-desktop { + margin-left: 75%; } + .column.is-offset-two-thirds-desktop { + margin-left: 66.6666%; } + .column.is-offset-half-desktop { + margin-left: 50%; } + .column.is-offset-one-third-desktop { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-desktop { + margin-left: 25%; } + .column.is-offset-one-fifth-desktop { + margin-left: 20%; } + .column.is-offset-two-fifths-desktop { + margin-left: 40%; } + .column.is-offset-three-fifths-desktop { + margin-left: 60%; } + .column.is-offset-four-fifths-desktop { + margin-left: 80%; } + .column.is-1-desktop { + flex: none; + width: 8.33333%; } + .column.is-offset-1-desktop { + margin-left: 8.33333%; } + .column.is-2-desktop { + flex: none; + width: 16.66667%; } + .column.is-offset-2-desktop { + margin-left: 16.66667%; } + .column.is-3-desktop { + flex: none; + width: 25%; } + .column.is-offset-3-desktop { + margin-left: 25%; } + .column.is-4-desktop { + flex: none; + width: 33.33333%; } + .column.is-offset-4-desktop { + margin-left: 33.33333%; } + .column.is-5-desktop { + flex: none; + width: 41.66667%; } + .column.is-offset-5-desktop { + margin-left: 41.66667%; } + .column.is-6-desktop { + flex: none; + width: 50%; } + .column.is-offset-6-desktop { + margin-left: 50%; } + .column.is-7-desktop { + flex: none; + width: 58.33333%; } + .column.is-offset-7-desktop { + margin-left: 58.33333%; } + .column.is-8-desktop { + flex: none; + width: 66.66667%; } + .column.is-offset-8-desktop { + margin-left: 66.66667%; } + .column.is-9-desktop { + flex: none; + width: 75%; } + .column.is-offset-9-desktop { + margin-left: 75%; } + .column.is-10-desktop { + flex: none; + width: 83.33333%; } + .column.is-offset-10-desktop { + margin-left: 83.33333%; } + .column.is-11-desktop { + flex: none; + width: 91.66667%; } + .column.is-offset-11-desktop { + margin-left: 91.66667%; } + .column.is-12-desktop { + flex: none; + width: 100%; } + .column.is-offset-12-desktop { + margin-left: 100%; } } + @media screen and (min-width: 1216px) { + .column.is-narrow-widescreen { + flex: none; } + .column.is-full-widescreen { + flex: none; + width: 100%; } + .column.is-three-quarters-widescreen { + flex: none; + width: 75%; } + .column.is-two-thirds-widescreen { + flex: none; + width: 66.6666%; } + .column.is-half-widescreen { + flex: none; + width: 50%; } + .column.is-one-third-widescreen { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-widescreen { + flex: none; + width: 25%; } + .column.is-one-fifth-widescreen { + flex: none; + width: 20%; } + .column.is-two-fifths-widescreen { + flex: none; + width: 40%; } + .column.is-three-fifths-widescreen { + flex: none; + width: 60%; } + .column.is-four-fifths-widescreen { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-widescreen { + margin-left: 75%; } + .column.is-offset-two-thirds-widescreen { + margin-left: 66.6666%; } + .column.is-offset-half-widescreen { + margin-left: 50%; } + .column.is-offset-one-third-widescreen { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-widescreen { + margin-left: 25%; } + .column.is-offset-one-fifth-widescreen { + margin-left: 20%; } + .column.is-offset-two-fifths-widescreen { + margin-left: 40%; } + .column.is-offset-three-fifths-widescreen { + margin-left: 60%; } + .column.is-offset-four-fifths-widescreen { + margin-left: 80%; } + .column.is-1-widescreen { + flex: none; + width: 8.33333%; } + .column.is-offset-1-widescreen { + margin-left: 8.33333%; } + .column.is-2-widescreen { + flex: none; + width: 16.66667%; } + .column.is-offset-2-widescreen { + margin-left: 16.66667%; } + .column.is-3-widescreen { + flex: none; + width: 25%; } + .column.is-offset-3-widescreen { + margin-left: 25%; } + .column.is-4-widescreen { + flex: none; + width: 33.33333%; } + .column.is-offset-4-widescreen { + margin-left: 33.33333%; } + .column.is-5-widescreen { + flex: none; + width: 41.66667%; } + .column.is-offset-5-widescreen { + margin-left: 41.66667%; } + .column.is-6-widescreen { + flex: none; + width: 50%; } + .column.is-offset-6-widescreen { + margin-left: 50%; } + .column.is-7-widescreen { + flex: none; + width: 58.33333%; } + .column.is-offset-7-widescreen { + margin-left: 58.33333%; } + .column.is-8-widescreen { + flex: none; + width: 66.66667%; } + .column.is-offset-8-widescreen { + margin-left: 66.66667%; } + .column.is-9-widescreen { + flex: none; + width: 75%; } + .column.is-offset-9-widescreen { + margin-left: 75%; } + .column.is-10-widescreen { + flex: none; + width: 83.33333%; } + .column.is-offset-10-widescreen { + margin-left: 83.33333%; } + .column.is-11-widescreen { + flex: none; + width: 91.66667%; } + .column.is-offset-11-widescreen { + margin-left: 91.66667%; } + .column.is-12-widescreen { + flex: none; + width: 100%; } + .column.is-offset-12-widescreen { + margin-left: 100%; } } + @media screen and (min-width: 1408px) { + .column.is-narrow-fullhd { + flex: none; } + .column.is-full-fullhd { + flex: none; + width: 100%; } + .column.is-three-quarters-fullhd { + flex: none; + width: 75%; } + .column.is-two-thirds-fullhd { + flex: none; + width: 66.6666%; } + .column.is-half-fullhd { + flex: none; + width: 50%; } + .column.is-one-third-fullhd { + flex: none; + width: 33.3333%; } + .column.is-one-quarter-fullhd { + flex: none; + width: 25%; } + .column.is-one-fifth-fullhd { + flex: none; + width: 20%; } + .column.is-two-fifths-fullhd { + flex: none; + width: 40%; } + .column.is-three-fifths-fullhd { + flex: none; + width: 60%; } + .column.is-four-fifths-fullhd { + flex: none; + width: 80%; } + .column.is-offset-three-quarters-fullhd { + margin-left: 75%; } + .column.is-offset-two-thirds-fullhd { + margin-left: 66.6666%; } + .column.is-offset-half-fullhd { + margin-left: 50%; } + .column.is-offset-one-third-fullhd { + margin-left: 33.3333%; } + .column.is-offset-one-quarter-fullhd { + margin-left: 25%; } + .column.is-offset-one-fifth-fullhd { + margin-left: 20%; } + .column.is-offset-two-fifths-fullhd { + margin-left: 40%; } + .column.is-offset-three-fifths-fullhd { + margin-left: 60%; } + .column.is-offset-four-fifths-fullhd { + margin-left: 80%; } + .column.is-1-fullhd { + flex: none; + width: 8.33333%; } + .column.is-offset-1-fullhd { + margin-left: 8.33333%; } + .column.is-2-fullhd { + flex: none; + width: 16.66667%; } + .column.is-offset-2-fullhd { + margin-left: 16.66667%; } + .column.is-3-fullhd { + flex: none; + width: 25%; } + .column.is-offset-3-fullhd { + margin-left: 25%; } + .column.is-4-fullhd { + flex: none; + width: 33.33333%; } + .column.is-offset-4-fullhd { + margin-left: 33.33333%; } + .column.is-5-fullhd { + flex: none; + width: 41.66667%; } + .column.is-offset-5-fullhd { + margin-left: 41.66667%; } + .column.is-6-fullhd { + flex: none; + width: 50%; } + .column.is-offset-6-fullhd { + margin-left: 50%; } + .column.is-7-fullhd { + flex: none; + width: 58.33333%; } + .column.is-offset-7-fullhd { + margin-left: 58.33333%; } + .column.is-8-fullhd { + flex: none; + width: 66.66667%; } + .column.is-offset-8-fullhd { + margin-left: 66.66667%; } + .column.is-9-fullhd { + flex: none; + width: 75%; } + .column.is-offset-9-fullhd { + margin-left: 75%; } + .column.is-10-fullhd { + flex: none; + width: 83.33333%; } + .column.is-offset-10-fullhd { + margin-left: 83.33333%; } + .column.is-11-fullhd { + flex: none; + width: 91.66667%; } + .column.is-offset-11-fullhd { + margin-left: 91.66667%; } + .column.is-12-fullhd { + flex: none; + width: 100%; } + .column.is-offset-12-fullhd { + margin-left: 100%; } } + +.columns { + margin-left: -0.75rem; + margin-right: -0.75rem; + margin-top: -0.75rem; } + .columns:last-child { + margin-bottom: -0.75rem; } + .columns:not(:last-child) { + margin-bottom: calc(1.5rem - 0.75rem); } + .columns.is-centered { + justify-content: center; } + .columns.is-gapless { + margin-left: 0; + margin-right: 0; + margin-top: 0; } + .columns.is-gapless > .column { + margin: 0; + padding: 0 !important; } + .columns.is-gapless:not(:last-child) { + margin-bottom: 1.5rem; } + .columns.is-gapless:last-child { + margin-bottom: 0; } + .columns.is-mobile { + display: flex; } + .columns.is-multiline { + flex-wrap: wrap; } + .columns.is-vcentered { + align-items: center; } + @media screen and (min-width: 769px), print { + .columns:not(.is-desktop) { + display: flex; } } + @media screen and (min-width: 1024px) { + .columns.is-desktop { + display: flex; } } + +.columns.is-variable { + --columnGap: 0.75rem; + margin-left: calc(-1 * var(--columnGap)); + margin-right: calc(-1 * var(--columnGap)); } + .columns.is-variable .column { + padding-left: var(--columnGap); + padding-right: var(--columnGap); } + .columns.is-variable.is-0 { + --columnGap: 0rem; } + .columns.is-variable.is-1 { + --columnGap: 0.25rem; } + .columns.is-variable.is-2 { + --columnGap: 0.5rem; } + .columns.is-variable.is-3 { + --columnGap: 0.75rem; } + .columns.is-variable.is-4 { + --columnGap: 1rem; } + .columns.is-variable.is-5 { + --columnGap: 1.25rem; } + .columns.is-variable.is-6 { + --columnGap: 1.5rem; } + .columns.is-variable.is-7 { + --columnGap: 1.75rem; } + .columns.is-variable.is-8 { + --columnGap: 2rem; } + +.tile { + align-items: stretch; + display: block; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 1; + min-height: min-content; } + .tile.is-ancestor { + margin-left: -0.75rem; + margin-right: -0.75rem; + margin-top: -0.75rem; } + .tile.is-ancestor:last-child { + margin-bottom: -0.75rem; } + .tile.is-ancestor:not(:last-child) { + margin-bottom: 0.75rem; } + .tile.is-child { + margin: 0 !important; } + .tile.is-parent { + padding: 0.75rem; } + .tile.is-vertical { + flex-direction: column; } + .tile.is-vertical > .tile.is-child:not(:last-child) { + margin-bottom: 1.5rem !important; } + @media screen and (min-width: 769px), print { + .tile:not(.is-child) { + display: flex; } + .tile.is-1 { + flex: none; + width: 8.33333%; } + .tile.is-2 { + flex: none; + width: 16.66667%; } + .tile.is-3 { + flex: none; + width: 25%; } + .tile.is-4 { + flex: none; + width: 33.33333%; } + .tile.is-5 { + flex: none; + width: 41.66667%; } + .tile.is-6 { + flex: none; + width: 50%; } + .tile.is-7 { + flex: none; + width: 58.33333%; } + .tile.is-8 { + flex: none; + width: 66.66667%; } + .tile.is-9 { + flex: none; + width: 75%; } + .tile.is-10 { + flex: none; + width: 83.33333%; } + .tile.is-11 { + flex: none; + width: 91.66667%; } + .tile.is-12 { + flex: none; + width: 100%; } } + +.box { + background-color: white; + border-radius: 5px; + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + color: #4a4a4a; + display: block; + padding: 1.25rem; } + .box:not(:last-child) { + margin-bottom: 1.5rem; } + +a.box:hover, a.box:focus { + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px #3273dc; } + +a.box:active { + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.2), 0 0 0 1px #3273dc; } + +.button { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-color: white; + border-color: #dbdbdb; + color: #363636; + cursor: pointer; + justify-content: center; + padding-left: 0.75em; + padding-right: 0.75em; + text-align: center; + white-space: nowrap; } + .button:focus, .button.is-focused, .button:active, .button.is-active { + outline: none; } + .button[disabled] { + cursor: not-allowed; } + .button strong { + color: inherit; } + .button .icon, .button .icon.is-small, .button .icon.is-medium, .button .icon.is-large { + height: 1.5em; + width: 1.5em; } + .button .icon:first-child:not(:last-child) { + margin-left: calc(-0.375em - 1px); + margin-right: 0.1875em; } + .button .icon:last-child:not(:first-child) { + margin-left: 0.1875em; + margin-right: calc(-0.375em - 1px); } + .button .icon:first-child:last-child { + margin-left: calc(-0.375em - 1px); + margin-right: calc(-0.375em - 1px); } + .button:hover, .button.is-hovered { + border-color: #b5b5b5; + color: #363636; } + .button:focus, .button.is-focused { + border-color: #3273dc; + color: #363636; } + .button:focus:not(:active), .button.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .button:active, .button.is-active { + border-color: #4a4a4a; + color: #363636; } + .button.is-text { + background-color: transparent; + border-color: transparent; + color: #4a4a4a; + text-decoration: underline; } + .button.is-text:hover, .button.is-text.is-hovered, .button.is-text:focus, .button.is-text.is-focused { + background-color: whitesmoke; + color: #363636; } + .button.is-text:active, .button.is-text.is-active { + background-color: #e8e8e8; + color: #363636; } + .button.is-text[disabled] { + background-color: transparent; + border-color: transparent; + box-shadow: none; } + .button.is-white { + background-color: white; + border-color: transparent; + color: #0a0a0a; } + .button.is-white:hover, .button.is-white.is-hovered { + background-color: #f9f9f9; + border-color: transparent; + color: #0a0a0a; } + .button.is-white:focus, .button.is-white.is-focused { + border-color: transparent; + color: #0a0a0a; } + .button.is-white:focus:not(:active), .button.is-white.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); } + .button.is-white:active, .button.is-white.is-active { + background-color: #f2f2f2; + border-color: transparent; + color: #0a0a0a; } + .button.is-white[disabled] { + background-color: white; + border-color: transparent; + box-shadow: none; } + .button.is-white.is-inverted { + background-color: #0a0a0a; + color: white; } + .button.is-white.is-inverted:hover { + background-color: black; } + .button.is-white.is-inverted[disabled] { + background-color: #0a0a0a; + border-color: transparent; + box-shadow: none; + color: white; } + .button.is-white.is-loading:after { + border-color: transparent transparent #0a0a0a #0a0a0a !important; } + .button.is-white.is-outlined { + background-color: transparent; + border-color: white; + color: white; } + .button.is-white.is-outlined:hover, .button.is-white.is-outlined:focus { + background-color: white; + border-color: white; + color: #0a0a0a; } + .button.is-white.is-outlined.is-loading:after { + border-color: transparent transparent white white !important; } + .button.is-white.is-outlined[disabled] { + background-color: transparent; + border-color: white; + box-shadow: none; + color: white; } + .button.is-white.is-inverted.is-outlined { + background-color: transparent; + border-color: #0a0a0a; + color: #0a0a0a; } + .button.is-white.is-inverted.is-outlined:hover, .button.is-white.is-inverted.is-outlined:focus { + background-color: #0a0a0a; + color: white; } + .button.is-white.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #0a0a0a; + box-shadow: none; + color: #0a0a0a; } + .button.is-black { + background-color: #0a0a0a; + border-color: transparent; + color: white; } + .button.is-black:hover, .button.is-black.is-hovered { + background-color: #040404; + border-color: transparent; + color: white; } + .button.is-black:focus, .button.is-black.is-focused { + border-color: transparent; + color: white; } + .button.is-black:focus:not(:active), .button.is-black.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); } + .button.is-black:active, .button.is-black.is-active { + background-color: black; + border-color: transparent; + color: white; } + .button.is-black[disabled] { + background-color: #0a0a0a; + border-color: transparent; + box-shadow: none; } + .button.is-black.is-inverted { + background-color: white; + color: #0a0a0a; } + .button.is-black.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-black.is-inverted[disabled] { + background-color: white; + border-color: transparent; + box-shadow: none; + color: #0a0a0a; } + .button.is-black.is-loading:after { + border-color: transparent transparent white white !important; } + .button.is-black.is-outlined { + background-color: transparent; + border-color: #0a0a0a; + color: #0a0a0a; } + .button.is-black.is-outlined:hover, .button.is-black.is-outlined:focus { + background-color: #0a0a0a; + border-color: #0a0a0a; + color: white; } + .button.is-black.is-outlined.is-loading:after { + border-color: transparent transparent #0a0a0a #0a0a0a !important; } + .button.is-black.is-outlined[disabled] { + background-color: transparent; + border-color: #0a0a0a; + box-shadow: none; + color: #0a0a0a; } + .button.is-black.is-inverted.is-outlined { + background-color: transparent; + border-color: white; + color: white; } + .button.is-black.is-inverted.is-outlined:hover, .button.is-black.is-inverted.is-outlined:focus { + background-color: white; + color: #0a0a0a; } + .button.is-black.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: white; + box-shadow: none; + color: white; } + .button.is-light { + background-color: whitesmoke; + border-color: transparent; + color: #363636; } + .button.is-light:hover, .button.is-light.is-hovered { + background-color: #eeeeee; + border-color: transparent; + color: #363636; } + .button.is-light:focus, .button.is-light.is-focused { + border-color: transparent; + color: #363636; } + .button.is-light:focus:not(:active), .button.is-light.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); } + .button.is-light:active, .button.is-light.is-active { + background-color: #e8e8e8; + border-color: transparent; + color: #363636; } + .button.is-light[disabled] { + background-color: whitesmoke; + border-color: transparent; + box-shadow: none; } + .button.is-light.is-inverted { + background-color: #363636; + color: whitesmoke; } + .button.is-light.is-inverted:hover { + background-color: #292929; } + .button.is-light.is-inverted[disabled] { + background-color: #363636; + border-color: transparent; + box-shadow: none; + color: whitesmoke; } + .button.is-light.is-loading:after { + border-color: transparent transparent #363636 #363636 !important; } + .button.is-light.is-outlined { + background-color: transparent; + border-color: whitesmoke; + color: whitesmoke; } + .button.is-light.is-outlined:hover, .button.is-light.is-outlined:focus { + background-color: whitesmoke; + border-color: whitesmoke; + color: #363636; } + .button.is-light.is-outlined.is-loading:after { + border-color: transparent transparent whitesmoke whitesmoke !important; } + .button.is-light.is-outlined[disabled] { + background-color: transparent; + border-color: whitesmoke; + box-shadow: none; + color: whitesmoke; } + .button.is-light.is-inverted.is-outlined { + background-color: transparent; + border-color: #363636; + color: #363636; } + .button.is-light.is-inverted.is-outlined:hover, .button.is-light.is-inverted.is-outlined:focus { + background-color: #363636; + color: whitesmoke; } + .button.is-light.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #363636; + box-shadow: none; + color: #363636; } + .button.is-dark { + background-color: #363636; + border-color: transparent; + color: whitesmoke; } + .button.is-dark:hover, .button.is-dark.is-hovered { + background-color: #2f2f2f; + border-color: transparent; + color: whitesmoke; } + .button.is-dark:focus, .button.is-dark.is-focused { + border-color: transparent; + color: whitesmoke; } + .button.is-dark:focus:not(:active), .button.is-dark.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); } + .button.is-dark:active, .button.is-dark.is-active { + background-color: #292929; + border-color: transparent; + color: whitesmoke; } + .button.is-dark[disabled] { + background-color: #363636; + border-color: transparent; + box-shadow: none; } + .button.is-dark.is-inverted { + background-color: whitesmoke; + color: #363636; } + .button.is-dark.is-inverted:hover { + background-color: #e8e8e8; } + .button.is-dark.is-inverted[disabled] { + background-color: whitesmoke; + border-color: transparent; + box-shadow: none; + color: #363636; } + .button.is-dark.is-loading:after { + border-color: transparent transparent whitesmoke whitesmoke !important; } + .button.is-dark.is-outlined { + background-color: transparent; + border-color: #363636; + color: #363636; } + .button.is-dark.is-outlined:hover, .button.is-dark.is-outlined:focus { + background-color: #363636; + border-color: #363636; + color: whitesmoke; } + .button.is-dark.is-outlined.is-loading:after { + border-color: transparent transparent #363636 #363636 !important; } + .button.is-dark.is-outlined[disabled] { + background-color: transparent; + border-color: #363636; + box-shadow: none; + color: #363636; } + .button.is-dark.is-inverted.is-outlined { + background-color: transparent; + border-color: whitesmoke; + color: whitesmoke; } + .button.is-dark.is-inverted.is-outlined:hover, .button.is-dark.is-inverted.is-outlined:focus { + background-color: whitesmoke; + color: #363636; } + .button.is-dark.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: whitesmoke; + box-shadow: none; + color: whitesmoke; } + .button.is-primary { + background-color: #C93312; + border-color: transparent; + color: #fff; } + .button.is-primary:hover, .button.is-primary.is-hovered { + background-color: #bd3011; + border-color: transparent; + color: #fff; } + .button.is-primary:focus, .button.is-primary.is-focused { + border-color: transparent; + color: #fff; } + .button.is-primary:focus:not(:active), .button.is-primary.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(201, 51, 18, 0.25); } + .button.is-primary:active, .button.is-primary.is-active { + background-color: #b22d10; + border-color: transparent; + color: #fff; } + .button.is-primary[disabled] { + background-color: #C93312; + border-color: transparent; + box-shadow: none; } + .button.is-primary.is-inverted { + background-color: #fff; + color: #C93312; } + .button.is-primary.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-primary.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #C93312; } + .button.is-primary.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-primary.is-outlined { + background-color: transparent; + border-color: #C93312; + color: #C93312; } + .button.is-primary.is-outlined:hover, .button.is-primary.is-outlined:focus { + background-color: #C93312; + border-color: #C93312; + color: #fff; } + .button.is-primary.is-outlined.is-loading:after { + border-color: transparent transparent #C93312 #C93312 !important; } + .button.is-primary.is-outlined[disabled] { + background-color: transparent; + border-color: #C93312; + box-shadow: none; + color: #C93312; } + .button.is-primary.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-primary.is-inverted.is-outlined:hover, .button.is-primary.is-inverted.is-outlined:focus { + background-color: #fff; + color: #C93312; } + .button.is-primary.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-link { + background-color: #3273dc; + border-color: transparent; + color: #fff; } + .button.is-link:hover, .button.is-link.is-hovered { + background-color: #276cda; + border-color: transparent; + color: #fff; } + .button.is-link:focus, .button.is-link.is-focused { + border-color: transparent; + color: #fff; } + .button.is-link:focus:not(:active), .button.is-link.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .button.is-link:active, .button.is-link.is-active { + background-color: #2366d1; + border-color: transparent; + color: #fff; } + .button.is-link[disabled] { + background-color: #3273dc; + border-color: transparent; + box-shadow: none; } + .button.is-link.is-inverted { + background-color: #fff; + color: #3273dc; } + .button.is-link.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-link.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #3273dc; } + .button.is-link.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-link.is-outlined { + background-color: transparent; + border-color: #3273dc; + color: #3273dc; } + .button.is-link.is-outlined:hover, .button.is-link.is-outlined:focus { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; } + .button.is-link.is-outlined.is-loading:after { + border-color: transparent transparent #3273dc #3273dc !important; } + .button.is-link.is-outlined[disabled] { + background-color: transparent; + border-color: #3273dc; + box-shadow: none; + color: #3273dc; } + .button.is-link.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-link.is-inverted.is-outlined:hover, .button.is-link.is-inverted.is-outlined:focus { + background-color: #fff; + color: #3273dc; } + .button.is-link.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-info { + background-color: #209cee; + border-color: transparent; + color: #fff; } + .button.is-info:hover, .button.is-info.is-hovered { + background-color: #1496ed; + border-color: transparent; + color: #fff; } + .button.is-info:focus, .button.is-info.is-focused { + border-color: transparent; + color: #fff; } + .button.is-info:focus:not(:active), .button.is-info.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(32, 156, 238, 0.25); } + .button.is-info:active, .button.is-info.is-active { + background-color: #118fe4; + border-color: transparent; + color: #fff; } + .button.is-info[disabled] { + background-color: #209cee; + border-color: transparent; + box-shadow: none; } + .button.is-info.is-inverted { + background-color: #fff; + color: #209cee; } + .button.is-info.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-info.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #209cee; } + .button.is-info.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-info.is-outlined { + background-color: transparent; + border-color: #209cee; + color: #209cee; } + .button.is-info.is-outlined:hover, .button.is-info.is-outlined:focus { + background-color: #209cee; + border-color: #209cee; + color: #fff; } + .button.is-info.is-outlined.is-loading:after { + border-color: transparent transparent #209cee #209cee !important; } + .button.is-info.is-outlined[disabled] { + background-color: transparent; + border-color: #209cee; + box-shadow: none; + color: #209cee; } + .button.is-info.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-info.is-inverted.is-outlined:hover, .button.is-info.is-inverted.is-outlined:focus { + background-color: #fff; + color: #209cee; } + .button.is-info.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-success { + background-color: #23d160; + border-color: transparent; + color: #fff; } + .button.is-success:hover, .button.is-success.is-hovered { + background-color: #22c65b; + border-color: transparent; + color: #fff; } + .button.is-success:focus, .button.is-success.is-focused { + border-color: transparent; + color: #fff; } + .button.is-success:focus:not(:active), .button.is-success.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(35, 209, 96, 0.25); } + .button.is-success:active, .button.is-success.is-active { + background-color: #20bc56; + border-color: transparent; + color: #fff; } + .button.is-success[disabled] { + background-color: #23d160; + border-color: transparent; + box-shadow: none; } + .button.is-success.is-inverted { + background-color: #fff; + color: #23d160; } + .button.is-success.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-success.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #23d160; } + .button.is-success.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-success.is-outlined { + background-color: transparent; + border-color: #23d160; + color: #23d160; } + .button.is-success.is-outlined:hover, .button.is-success.is-outlined:focus { + background-color: #23d160; + border-color: #23d160; + color: #fff; } + .button.is-success.is-outlined.is-loading:after { + border-color: transparent transparent #23d160 #23d160 !important; } + .button.is-success.is-outlined[disabled] { + background-color: transparent; + border-color: #23d160; + box-shadow: none; + color: #23d160; } + .button.is-success.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-success.is-inverted.is-outlined:hover, .button.is-success.is-inverted.is-outlined:focus { + background-color: #fff; + color: #23d160; } + .button.is-success.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-warning { + background-color: #ffdd57; + border-color: transparent; + color: #FFFFFF; } + .button.is-warning:hover, .button.is-warning.is-hovered { + background-color: #ffdb4a; + border-color: transparent; + color: #FFFFFF; } + .button.is-warning:focus, .button.is-warning.is-focused { + border-color: transparent; + color: #FFFFFF; } + .button.is-warning:focus:not(:active), .button.is-warning.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(255, 221, 87, 0.25); } + .button.is-warning:active, .button.is-warning.is-active { + background-color: #ffd83d; + border-color: transparent; + color: #FFFFFF; } + .button.is-warning[disabled] { + background-color: #ffdd57; + border-color: transparent; + box-shadow: none; } + .button.is-warning.is-inverted { + background-color: #FFFFFF; + color: #ffdd57; } + .button.is-warning.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-warning.is-inverted[disabled] { + background-color: #FFFFFF; + border-color: transparent; + box-shadow: none; + color: #ffdd57; } + .button.is-warning.is-loading:after { + border-color: transparent transparent #FFFFFF #FFFFFF !important; } + .button.is-warning.is-outlined { + background-color: transparent; + border-color: #ffdd57; + color: #ffdd57; } + .button.is-warning.is-outlined:hover, .button.is-warning.is-outlined:focus { + background-color: #ffdd57; + border-color: #ffdd57; + color: #FFFFFF; } + .button.is-warning.is-outlined.is-loading:after { + border-color: transparent transparent #ffdd57 #ffdd57 !important; } + .button.is-warning.is-outlined[disabled] { + background-color: transparent; + border-color: #ffdd57; + box-shadow: none; + color: #ffdd57; } + .button.is-warning.is-inverted.is-outlined { + background-color: transparent; + border-color: #FFFFFF; + color: #FFFFFF; } + .button.is-warning.is-inverted.is-outlined:hover, .button.is-warning.is-inverted.is-outlined:focus { + background-color: #FFFFFF; + color: #ffdd57; } + .button.is-warning.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #FFFFFF; + box-shadow: none; + color: #FFFFFF; } + .button.is-danger { + background-color: #ff3860; + border-color: transparent; + color: #fff; } + .button.is-danger:hover, .button.is-danger.is-hovered { + background-color: #ff2b56; + border-color: transparent; + color: #fff; } + .button.is-danger:focus, .button.is-danger.is-focused { + border-color: transparent; + color: #fff; } + .button.is-danger:focus:not(:active), .button.is-danger.is-focused:not(:active) { + box-shadow: 0 0 0 0.125em rgba(255, 56, 96, 0.25); } + .button.is-danger:active, .button.is-danger.is-active { + background-color: #ff1f4b; + border-color: transparent; + color: #fff; } + .button.is-danger[disabled] { + background-color: #ff3860; + border-color: transparent; + box-shadow: none; } + .button.is-danger.is-inverted { + background-color: #fff; + color: #ff3860; } + .button.is-danger.is-inverted:hover { + background-color: #f2f2f2; } + .button.is-danger.is-inverted[disabled] { + background-color: #fff; + border-color: transparent; + box-shadow: none; + color: #ff3860; } + .button.is-danger.is-loading:after { + border-color: transparent transparent #fff #fff !important; } + .button.is-danger.is-outlined { + background-color: transparent; + border-color: #ff3860; + color: #ff3860; } + .button.is-danger.is-outlined:hover, .button.is-danger.is-outlined:focus { + background-color: #ff3860; + border-color: #ff3860; + color: #fff; } + .button.is-danger.is-outlined.is-loading:after { + border-color: transparent transparent #ff3860 #ff3860 !important; } + .button.is-danger.is-outlined[disabled] { + background-color: transparent; + border-color: #ff3860; + box-shadow: none; + color: #ff3860; } + .button.is-danger.is-inverted.is-outlined { + background-color: transparent; + border-color: #fff; + color: #fff; } + .button.is-danger.is-inverted.is-outlined:hover, .button.is-danger.is-inverted.is-outlined:focus { + background-color: #fff; + color: #ff3860; } + .button.is-danger.is-inverted.is-outlined[disabled] { + background-color: transparent; + border-color: #fff; + box-shadow: none; + color: #fff; } + .button.is-small { + border-radius: 2px; + font-size: 0.75rem; } + .button.is-medium { + font-size: 1.25rem; } + .button.is-large { + font-size: 1.5rem; } + .button[disabled] { + background-color: white; + border-color: #dbdbdb; + box-shadow: none; + opacity: 0.5; } + .button.is-fullwidth { + display: flex; + width: 100%; } + .button.is-loading { + color: transparent !important; + pointer-events: none; } + .button.is-loading:after { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; + position: absolute; + left: calc(50% - (1em / 2)); + top: calc(50% - (1em / 2)); + position: absolute !important; } + .button.is-static { + background-color: whitesmoke; + border-color: #dbdbdb; + color: #7a7a7a; + box-shadow: none; + pointer-events: none; } + +.buttons { + align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; } + .buttons .button { + margin-bottom: 0.5rem; } + .buttons .button:not(:last-child) { + margin-right: 0.5rem; } + .buttons:last-child { + margin-bottom: -0.5rem; } + .buttons:not(:last-child) { + margin-bottom: 1rem; } + .buttons.has-addons .button:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .buttons.has-addons .button:not(:last-child) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; + margin-right: -1px; } + .buttons.has-addons .button:last-child { + margin-right: 0; } + .buttons.has-addons .button:hover, .buttons.has-addons .button.is-hovered { + z-index: 2; } + .buttons.has-addons .button:focus, .buttons.has-addons .button.is-focused, .buttons.has-addons .button:active, .buttons.has-addons .button.is-active, .buttons.has-addons .button.is-selected { + z-index: 3; } + .buttons.has-addons .button:focus:hover, .buttons.has-addons .button.is-focused:hover, .buttons.has-addons .button:active:hover, .buttons.has-addons .button.is-active:hover, .buttons.has-addons .button.is-selected:hover { + z-index: 4; } + .buttons.is-centered { + justify-content: center; } + .buttons.is-right { + justify-content: flex-end; } + +.container { + margin: 0 auto; + position: relative; } + @media screen and (min-width: 1024px) { + .container { + max-width: 960px; + width: 960px; } + .container.is-fluid { + margin-left: 32px; + margin-right: 32px; + max-width: none; + width: auto; } } + @media screen and (max-width: 1215px) { + .container.is-widescreen { + max-width: 1152px; + width: auto; } } + @media screen and (max-width: 1407px) { + .container.is-fullhd { + max-width: 1344px; + width: auto; } } + @media screen and (min-width: 1216px) { + .container { + max-width: 1152px; + width: 1152px; } } + @media screen and (min-width: 1408px) { + .container { + max-width: 1344px; + width: 1344px; } } + +.content:not(:last-child) { + margin-bottom: 1.5rem; } + +.content li + li { + margin-top: 0.25em; } + +.content p:not(:last-child), +.content dl:not(:last-child), +.content ol:not(:last-child), +.content ul:not(:last-child), +.content blockquote:not(:last-child), +.content pre:not(:last-child), +.content table:not(:last-child) { + margin-bottom: 1em; } + +.content h1, +.content h2, +.content h3, +.content h4, +.content h5, +.content h6 { + color: #363636; + font-weight: 400; + line-height: 1.125; } + +.content h1 { + font-size: 2em; + margin-bottom: 0.5em; } + .content h1:not(:first-child) { + margin-top: 1em; } + +.content h2 { + font-size: 1.75em; + margin-bottom: 0.5714em; } + .content h2:not(:first-child) { + margin-top: 1.1428em; } + +.content h3 { + font-size: 1.5em; + margin-bottom: 0.6666em; } + .content h3:not(:first-child) { + margin-top: 1.3333em; } + +.content h4 { + font-size: 1.25em; + margin-bottom: 0.8em; } + +.content h5 { + font-size: 1.125em; + margin-bottom: 0.8888em; } + +.content h6 { + font-size: 1em; + margin-bottom: 1em; } + +.content blockquote { + background-color: whitesmoke; + border-left: 5px solid #dbdbdb; + padding: 1.25em 1.5em; } + +.content ol { + list-style: decimal outside; + margin-left: 2em; + margin-top: 1em; } + +.content ul { + list-style: disc outside; + margin-left: 2em; + margin-top: 1em; } + .content ul ul { + list-style-type: circle; + margin-top: 0.5em; } + .content ul ul ul { + list-style-type: square; } + +.content dd { + margin-left: 2em; } + +.content figure { + margin-left: 2em; + margin-right: 2em; + text-align: center; } + .content figure:not(:first-child) { + margin-top: 2em; } + .content figure:not(:last-child) { + margin-bottom: 2em; } + .content figure img { + display: inline-block; } + .content figure figcaption { + font-style: italic; } + +.content pre { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + padding: 1.25em 1.5em; + white-space: pre; + word-wrap: normal; } + +.content sup, +.content sub { + font-size: 75%; } + +.content table { + width: 100%; } + .content table td, + .content table th { + border: 1px solid #dbdbdb; + border-width: 0 0 1px; + padding: 0.5em 0.75em; + vertical-align: top; } + .content table th { + color: #363636; + text-align: left; } + .content table tr:hover { + background-color: whitesmoke; } + .content table thead td, + .content table thead th { + border-width: 0 0 2px; + color: #363636; } + .content table tfoot td, + .content table tfoot th { + border-width: 2px 0 0; + color: #363636; } + .content table tbody tr:last-child td, + .content table tbody tr:last-child th { + border-bottom-width: 0; } + +.content.is-small { + font-size: 0.75rem; } + +.content.is-medium { + font-size: 1.25rem; } + +.content.is-large { + font-size: 1.5rem; } + +.input, +.textarea { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + background-color: white; + border-color: #dbdbdb; + color: #363636; + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); + max-width: 100%; + width: 100%; } + .input:focus, .input.is-focused, .input:active, .input.is-active, + .textarea:focus, + .textarea.is-focused, + .textarea:active, + .textarea.is-active { + outline: none; } + .input[disabled], + .textarea[disabled] { + cursor: not-allowed; } + .input::-moz-placeholder, + .textarea::-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input::-webkit-input-placeholder, + .textarea::-webkit-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input:-moz-placeholder, + .textarea:-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input:-ms-input-placeholder, + .textarea:-ms-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .input:hover, .input.is-hovered, + .textarea:hover, + .textarea.is-hovered { + border-color: #b5b5b5; } + .input:focus, .input.is-focused, .input:active, .input.is-active, + .textarea:focus, + .textarea.is-focused, + .textarea:active, + .textarea.is-active { + border-color: #3273dc; + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .input[disabled], + .textarea[disabled] { + background-color: whitesmoke; + border-color: whitesmoke; + box-shadow: none; + color: #7a7a7a; } + .input[disabled]::-moz-placeholder, + .textarea[disabled]::-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[disabled]::-webkit-input-placeholder, + .textarea[disabled]::-webkit-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[disabled]:-moz-placeholder, + .textarea[disabled]:-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[disabled]:-ms-input-placeholder, + .textarea[disabled]:-ms-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .input[type="search"], + .textarea[type="search"] { + border-radius: 290486px; } + .input[readonly], + .textarea[readonly] { + box-shadow: none; } + .input.is-white, + .textarea.is-white { + border-color: white; } + .input.is-white:focus, .input.is-white.is-focused, .input.is-white:active, .input.is-white.is-active, + .textarea.is-white:focus, + .textarea.is-white.is-focused, + .textarea.is-white:active, + .textarea.is-white.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); } + .input.is-black, + .textarea.is-black { + border-color: #0a0a0a; } + .input.is-black:focus, .input.is-black.is-focused, .input.is-black:active, .input.is-black.is-active, + .textarea.is-black:focus, + .textarea.is-black.is-focused, + .textarea.is-black:active, + .textarea.is-black.is-active { + box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); } + .input.is-light, + .textarea.is-light { + border-color: whitesmoke; } + .input.is-light:focus, .input.is-light.is-focused, .input.is-light:active, .input.is-light.is-active, + .textarea.is-light:focus, + .textarea.is-light.is-focused, + .textarea.is-light:active, + .textarea.is-light.is-active { + box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); } + .input.is-dark, + .textarea.is-dark { + border-color: #363636; } + .input.is-dark:focus, .input.is-dark.is-focused, .input.is-dark:active, .input.is-dark.is-active, + .textarea.is-dark:focus, + .textarea.is-dark.is-focused, + .textarea.is-dark:active, + .textarea.is-dark.is-active { + box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); } + .input.is-primary, + .textarea.is-primary { + border-color: #C93312; } + .input.is-primary:focus, .input.is-primary.is-focused, .input.is-primary:active, .input.is-primary.is-active, + .textarea.is-primary:focus, + .textarea.is-primary.is-focused, + .textarea.is-primary:active, + .textarea.is-primary.is-active { + box-shadow: 0 0 0 0.125em rgba(201, 51, 18, 0.25); } + .input.is-link, + .textarea.is-link { + border-color: #3273dc; } + .input.is-link:focus, .input.is-link.is-focused, .input.is-link:active, .input.is-link.is-active, + .textarea.is-link:focus, + .textarea.is-link.is-focused, + .textarea.is-link:active, + .textarea.is-link.is-active { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .input.is-info, + .textarea.is-info { + border-color: #209cee; } + .input.is-info:focus, .input.is-info.is-focused, .input.is-info:active, .input.is-info.is-active, + .textarea.is-info:focus, + .textarea.is-info.is-focused, + .textarea.is-info:active, + .textarea.is-info.is-active { + box-shadow: 0 0 0 0.125em rgba(32, 156, 238, 0.25); } + .input.is-success, + .textarea.is-success { + border-color: #23d160; } + .input.is-success:focus, .input.is-success.is-focused, .input.is-success:active, .input.is-success.is-active, + .textarea.is-success:focus, + .textarea.is-success.is-focused, + .textarea.is-success:active, + .textarea.is-success.is-active { + box-shadow: 0 0 0 0.125em rgba(35, 209, 96, 0.25); } + .input.is-warning, + .textarea.is-warning { + border-color: #ffdd57; } + .input.is-warning:focus, .input.is-warning.is-focused, .input.is-warning:active, .input.is-warning.is-active, + .textarea.is-warning:focus, + .textarea.is-warning.is-focused, + .textarea.is-warning:active, + .textarea.is-warning.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 221, 87, 0.25); } + .input.is-danger, + .textarea.is-danger { + border-color: #ff3860; } + .input.is-danger:focus, .input.is-danger.is-focused, .input.is-danger:active, .input.is-danger.is-active, + .textarea.is-danger:focus, + .textarea.is-danger.is-focused, + .textarea.is-danger:active, + .textarea.is-danger.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 56, 96, 0.25); } + .input.is-small, + .textarea.is-small { + border-radius: 2px; + font-size: 0.75rem; } + .input.is-medium, + .textarea.is-medium { + font-size: 1.25rem; } + .input.is-large, + .textarea.is-large { + font-size: 1.5rem; } + .input.is-fullwidth, + .textarea.is-fullwidth { + display: block; + width: 100%; } + .input.is-inline, + .textarea.is-inline { + display: inline; + width: auto; } + +.input.is-static { + background-color: transparent; + border-color: transparent; + box-shadow: none; + padding-left: 0; + padding-right: 0; } + +.textarea { + display: block; + max-width: 100%; + min-width: 100%; + padding: 0.625em; + resize: vertical; } + .textarea:not([rows]) { + max-height: 600px; + min-height: 120px; } + .textarea[rows] { + height: unset; } + .textarea.has-fixed-size { + resize: none; } + +.checkbox, +.radio { + cursor: pointer; + display: inline-block; + line-height: 1.25; + position: relative; } + .checkbox input, + .radio input { + cursor: pointer; } + .checkbox:hover, + .radio:hover { + color: #363636; } + .checkbox[disabled], + .radio[disabled] { + color: #7a7a7a; + cursor: not-allowed; } + +.radio + .radio { + margin-left: 0.5em; } + +.select { + display: inline-block; + max-width: 100%; + position: relative; + vertical-align: top; } + .select:not(.is-multiple) { + height: 2.25em; } + .select:not(.is-multiple)::after { + border: 1px solid #3273dc; + border-right: 0; + border-top: 0; + content: " "; + display: block; + height: 0.5em; + pointer-events: none; + position: absolute; + transform: rotate(-45deg); + transform-origin: center; + width: 0.5em; + margin-top: -0.375em; + right: 1.125em; + top: 50%; + z-index: 4; } + .select select { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + background-color: white; + border-color: #dbdbdb; + color: #363636; + cursor: pointer; + display: block; + font-size: 1em; + max-width: 100%; + outline: none; } + .select select:focus, .select select.is-focused, .select select:active, .select select.is-active { + outline: none; } + .select select[disabled] { + cursor: not-allowed; } + .select select::-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select::-webkit-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select:-moz-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select:-ms-input-placeholder { + color: rgba(54, 54, 54, 0.3); } + .select select:hover, .select select.is-hovered { + border-color: #b5b5b5; } + .select select:focus, .select select.is-focused, .select select:active, .select select.is-active { + border-color: #3273dc; + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .select select[disabled] { + background-color: whitesmoke; + border-color: whitesmoke; + box-shadow: none; + color: #7a7a7a; } + .select select[disabled]::-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select[disabled]::-webkit-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select[disabled]:-moz-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select[disabled]:-ms-input-placeholder { + color: rgba(122, 122, 122, 0.3); } + .select select::-ms-expand { + display: none; } + .select select[disabled]:hover { + border-color: whitesmoke; } + .select select:not([multiple]) { + padding-right: 2.5em; } + .select select[multiple] { + height: unset; + padding: 0; } + .select select[multiple] option { + padding: 0.5em 1em; } + .select:hover::after { + border-color: #363636; } + .select.is-white select { + border-color: white; } + .select.is-white select:focus, .select.is-white select.is-focused, .select.is-white select:active, .select.is-white select.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); } + .select.is-black select { + border-color: #0a0a0a; } + .select.is-black select:focus, .select.is-black select.is-focused, .select.is-black select:active, .select.is-black select.is-active { + box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); } + .select.is-light select { + border-color: whitesmoke; } + .select.is-light select:focus, .select.is-light select.is-focused, .select.is-light select:active, .select.is-light select.is-active { + box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); } + .select.is-dark select { + border-color: #363636; } + .select.is-dark select:focus, .select.is-dark select.is-focused, .select.is-dark select:active, .select.is-dark select.is-active { + box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); } + .select.is-primary select { + border-color: #C93312; } + .select.is-primary select:focus, .select.is-primary select.is-focused, .select.is-primary select:active, .select.is-primary select.is-active { + box-shadow: 0 0 0 0.125em rgba(201, 51, 18, 0.25); } + .select.is-link select { + border-color: #3273dc; } + .select.is-link select:focus, .select.is-link select.is-focused, .select.is-link select:active, .select.is-link select.is-active { + box-shadow: 0 0 0 0.125em rgba(50, 115, 220, 0.25); } + .select.is-info select { + border-color: #209cee; } + .select.is-info select:focus, .select.is-info select.is-focused, .select.is-info select:active, .select.is-info select.is-active { + box-shadow: 0 0 0 0.125em rgba(32, 156, 238, 0.25); } + .select.is-success select { + border-color: #23d160; } + .select.is-success select:focus, .select.is-success select.is-focused, .select.is-success select:active, .select.is-success select.is-active { + box-shadow: 0 0 0 0.125em rgba(35, 209, 96, 0.25); } + .select.is-warning select { + border-color: #ffdd57; } + .select.is-warning select:focus, .select.is-warning select.is-focused, .select.is-warning select:active, .select.is-warning select.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 221, 87, 0.25); } + .select.is-danger select { + border-color: #ff3860; } + .select.is-danger select:focus, .select.is-danger select.is-focused, .select.is-danger select:active, .select.is-danger select.is-active { + box-shadow: 0 0 0 0.125em rgba(255, 56, 96, 0.25); } + .select.is-small { + border-radius: 2px; + font-size: 0.75rem; } + .select.is-medium { + font-size: 1.25rem; } + .select.is-large { + font-size: 1.5rem; } + .select.is-disabled::after { + border-color: #7a7a7a; } + .select.is-fullwidth { + width: 100%; } + .select.is-fullwidth select { + width: 100%; } + .select.is-loading::after { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; + margin-top: 0; + position: absolute; + right: 0.625em; + top: 0.625em; + transform: none; } + .select.is-loading.is-small:after { + font-size: 0.75rem; } + .select.is-loading.is-medium:after { + font-size: 1.25rem; } + .select.is-loading.is-large:after { + font-size: 1.5rem; } + +.file { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: stretch; + display: flex; + justify-content: flex-start; + position: relative; } + .file.is-white .file-cta { + background-color: white; + border-color: transparent; + color: #0a0a0a; } + .file.is-white:hover .file-cta, .file.is-white.is-hovered .file-cta { + background-color: #f9f9f9; + border-color: transparent; + color: #0a0a0a; } + .file.is-white:focus .file-cta, .file.is-white.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.25); + color: #0a0a0a; } + .file.is-white:active .file-cta, .file.is-white.is-active .file-cta { + background-color: #f2f2f2; + border-color: transparent; + color: #0a0a0a; } + .file.is-black .file-cta { + background-color: #0a0a0a; + border-color: transparent; + color: white; } + .file.is-black:hover .file-cta, .file.is-black.is-hovered .file-cta { + background-color: #040404; + border-color: transparent; + color: white; } + .file.is-black:focus .file-cta, .file.is-black.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(10, 10, 10, 0.25); + color: white; } + .file.is-black:active .file-cta, .file.is-black.is-active .file-cta { + background-color: black; + border-color: transparent; + color: white; } + .file.is-light .file-cta { + background-color: whitesmoke; + border-color: transparent; + color: #363636; } + .file.is-light:hover .file-cta, .file.is-light.is-hovered .file-cta { + background-color: #eeeeee; + border-color: transparent; + color: #363636; } + .file.is-light:focus .file-cta, .file.is-light.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(245, 245, 245, 0.25); + color: #363636; } + .file.is-light:active .file-cta, .file.is-light.is-active .file-cta { + background-color: #e8e8e8; + border-color: transparent; + color: #363636; } + .file.is-dark .file-cta { + background-color: #363636; + border-color: transparent; + color: whitesmoke; } + .file.is-dark:hover .file-cta, .file.is-dark.is-hovered .file-cta { + background-color: #2f2f2f; + border-color: transparent; + color: whitesmoke; } + .file.is-dark:focus .file-cta, .file.is-dark.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(54, 54, 54, 0.25); + color: whitesmoke; } + .file.is-dark:active .file-cta, .file.is-dark.is-active .file-cta { + background-color: #292929; + border-color: transparent; + color: whitesmoke; } + .file.is-primary .file-cta { + background-color: #C93312; + border-color: transparent; + color: #fff; } + .file.is-primary:hover .file-cta, .file.is-primary.is-hovered .file-cta { + background-color: #bd3011; + border-color: transparent; + color: #fff; } + .file.is-primary:focus .file-cta, .file.is-primary.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(201, 51, 18, 0.25); + color: #fff; } + .file.is-primary:active .file-cta, .file.is-primary.is-active .file-cta { + background-color: #b22d10; + border-color: transparent; + color: #fff; } + .file.is-link .file-cta { + background-color: #3273dc; + border-color: transparent; + color: #fff; } + .file.is-link:hover .file-cta, .file.is-link.is-hovered .file-cta { + background-color: #276cda; + border-color: transparent; + color: #fff; } + .file.is-link:focus .file-cta, .file.is-link.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(50, 115, 220, 0.25); + color: #fff; } + .file.is-link:active .file-cta, .file.is-link.is-active .file-cta { + background-color: #2366d1; + border-color: transparent; + color: #fff; } + .file.is-info .file-cta { + background-color: #209cee; + border-color: transparent; + color: #fff; } + .file.is-info:hover .file-cta, .file.is-info.is-hovered .file-cta { + background-color: #1496ed; + border-color: transparent; + color: #fff; } + .file.is-info:focus .file-cta, .file.is-info.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(32, 156, 238, 0.25); + color: #fff; } + .file.is-info:active .file-cta, .file.is-info.is-active .file-cta { + background-color: #118fe4; + border-color: transparent; + color: #fff; } + .file.is-success .file-cta { + background-color: #23d160; + border-color: transparent; + color: #fff; } + .file.is-success:hover .file-cta, .file.is-success.is-hovered .file-cta { + background-color: #22c65b; + border-color: transparent; + color: #fff; } + .file.is-success:focus .file-cta, .file.is-success.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(35, 209, 96, 0.25); + color: #fff; } + .file.is-success:active .file-cta, .file.is-success.is-active .file-cta { + background-color: #20bc56; + border-color: transparent; + color: #fff; } + .file.is-warning .file-cta { + background-color: #ffdd57; + border-color: transparent; + color: #FFFFFF; } + .file.is-warning:hover .file-cta, .file.is-warning.is-hovered .file-cta { + background-color: #ffdb4a; + border-color: transparent; + color: #FFFFFF; } + .file.is-warning:focus .file-cta, .file.is-warning.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(255, 221, 87, 0.25); + color: #FFFFFF; } + .file.is-warning:active .file-cta, .file.is-warning.is-active .file-cta { + background-color: #ffd83d; + border-color: transparent; + color: #FFFFFF; } + .file.is-danger .file-cta { + background-color: #ff3860; + border-color: transparent; + color: #fff; } + .file.is-danger:hover .file-cta, .file.is-danger.is-hovered .file-cta { + background-color: #ff2b56; + border-color: transparent; + color: #fff; } + .file.is-danger:focus .file-cta, .file.is-danger.is-focused .file-cta { + border-color: transparent; + box-shadow: 0 0 0.5em rgba(255, 56, 96, 0.25); + color: #fff; } + .file.is-danger:active .file-cta, .file.is-danger.is-active .file-cta { + background-color: #ff1f4b; + border-color: transparent; + color: #fff; } + .file.is-small { + font-size: 0.75rem; } + .file.is-medium { + font-size: 1.25rem; } + .file.is-medium .file-icon .fa { + font-size: 21px; } + .file.is-large { + font-size: 1.5rem; } + .file.is-large .file-icon .fa { + font-size: 28px; } + .file.has-name .file-cta { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + .file.has-name .file-name { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .file.has-name.is-empty .file-cta { + border-radius: 3px; } + .file.has-name.is-empty .file-name { + display: none; } + .file.is-centered { + justify-content: center; } + .file.is-right { + justify-content: flex-end; } + .file.is-boxed .file-label { + flex-direction: column; } + .file.is-boxed .file-cta { + flex-direction: column; + height: auto; + padding: 1em 3em; } + .file.is-boxed .file-name { + border-width: 0 1px 1px; } + .file.is-boxed .file-icon { + height: 1.5em; + width: 1.5em; } + .file.is-boxed .file-icon .fa { + font-size: 21px; } + .file.is-boxed.is-small .file-icon .fa { + font-size: 14px; } + .file.is-boxed.is-medium .file-icon .fa { + font-size: 28px; } + .file.is-boxed.is-large .file-icon .fa { + font-size: 35px; } + .file.is-boxed.has-name .file-cta { + border-radius: 3px 3px 0 0; } + .file.is-boxed.has-name .file-name { + border-radius: 0 0 3px 3px; + border-width: 0 1px 1px; } + .file.is-right .file-cta { + border-radius: 0 3px 3px 0; } + .file.is-right .file-name { + border-radius: 3px 0 0 3px; + border-width: 1px 0 1px 1px; + order: -1; } + .file.is-fullwidth .file-label { + width: 100%; } + .file.is-fullwidth .file-name { + flex-grow: 1; + max-width: none; } + +.file-label { + align-items: stretch; + display: flex; + cursor: pointer; + justify-content: flex-start; + overflow: hidden; + position: relative; } + .file-label:hover .file-cta { + background-color: #eeeeee; + color: #363636; } + .file-label:hover .file-name { + border-color: #d5d5d5; } + .file-label:active .file-cta { + background-color: #e8e8e8; + color: #363636; } + .file-label:active .file-name { + border-color: #cfcfcf; } + +.file-input { + height: 0.01em; + left: 0; + outline: none; + position: absolute; + top: 0; + width: 0.01em; } + +.file-cta, +.file-name { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + border-color: #dbdbdb; + border-radius: 3px; + font-size: 1em; + padding-left: 1em; + padding-right: 1em; + white-space: nowrap; } + .file-cta:focus, .file-cta.is-focused, .file-cta:active, .file-cta.is-active, + .file-name:focus, + .file-name.is-focused, + .file-name:active, + .file-name.is-active { + outline: none; } + .file-cta[disabled], + .file-name[disabled] { + cursor: not-allowed; } + +.file-cta { + background-color: whitesmoke; + color: #4a4a4a; } + +.file-name { + border-color: #dbdbdb; + border-style: solid; + border-width: 1px 1px 1px 0; + display: block; + max-width: 16em; + overflow: hidden; + text-align: left; + text-overflow: ellipsis; } + +.file-icon { + align-items: center; + display: flex; + height: 1em; + justify-content: center; + margin-right: 0.5em; + width: 1em; } + .file-icon .fa { + font-size: 14px; } + +.label { + color: #363636; + display: block; + font-size: 1rem; + font-weight: 700; } + .label:not(:last-child) { + margin-bottom: 0.5em; } + .label.is-small { + font-size: 0.75rem; } + .label.is-medium { + font-size: 1.25rem; } + .label.is-large { + font-size: 1.5rem; } + +.help { + display: block; + font-size: 0.75rem; + margin-top: 0.25rem; } + .help.is-white { + color: white; } + .help.is-black { + color: #0a0a0a; } + .help.is-light { + color: whitesmoke; } + .help.is-dark { + color: #363636; } + .help.is-primary { + color: #C93312; } + .help.is-link { + color: #3273dc; } + .help.is-info { + color: #209cee; } + .help.is-success { + color: #23d160; } + .help.is-warning { + color: #ffdd57; } + .help.is-danger { + color: #ff3860; } + +.field:not(:last-child) { + margin-bottom: 0.75rem; } + +.field.has-addons { + display: flex; + justify-content: flex-start; } + .field.has-addons .control:not(:last-child) { + margin-right: -1px; } + .field.has-addons .control:first-child .button, + .field.has-addons .control:first-child .input, + .field.has-addons .control:first-child .select select { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; } + .field.has-addons .control:last-child .button, + .field.has-addons .control:last-child .input, + .field.has-addons .control:last-child .select select { + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; } + .field.has-addons .control .button, + .field.has-addons .control .input, + .field.has-addons .control .select select { + border-radius: 0; } + .field.has-addons .control .button:hover, .field.has-addons .control .button.is-hovered, + .field.has-addons .control .input:hover, + .field.has-addons .control .input.is-hovered, + .field.has-addons .control .select select:hover, + .field.has-addons .control .select select.is-hovered { + z-index: 2; } + .field.has-addons .control .button:focus, .field.has-addons .control .button.is-focused, .field.has-addons .control .button:active, .field.has-addons .control .button.is-active, + .field.has-addons .control .input:focus, + .field.has-addons .control .input.is-focused, + .field.has-addons .control .input:active, + .field.has-addons .control .input.is-active, + .field.has-addons .control .select select:focus, + .field.has-addons .control .select select.is-focused, + .field.has-addons .control .select select:active, + .field.has-addons .control .select select.is-active { + z-index: 3; } + .field.has-addons .control .button:focus:hover, .field.has-addons .control .button.is-focused:hover, .field.has-addons .control .button:active:hover, .field.has-addons .control .button.is-active:hover, + .field.has-addons .control .input:focus:hover, + .field.has-addons .control .input.is-focused:hover, + .field.has-addons .control .input:active:hover, + .field.has-addons .control .input.is-active:hover, + .field.has-addons .control .select select:focus:hover, + .field.has-addons .control .select select.is-focused:hover, + .field.has-addons .control .select select:active:hover, + .field.has-addons .control .select select.is-active:hover { + z-index: 4; } + .field.has-addons .control.is-expanded { + flex-grow: 1; } + .field.has-addons.has-addons-centered { + justify-content: center; } + .field.has-addons.has-addons-right { + justify-content: flex-end; } + .field.has-addons.has-addons-fullwidth .control { + flex-grow: 1; + flex-shrink: 0; } + +.field.is-grouped { + display: flex; + justify-content: flex-start; } + .field.is-grouped > .control { + flex-shrink: 0; } + .field.is-grouped > .control:not(:last-child) { + margin-bottom: 0; + margin-right: 0.75rem; } + .field.is-grouped > .control.is-expanded { + flex-grow: 1; + flex-shrink: 1; } + .field.is-grouped.is-grouped-centered { + justify-content: center; } + .field.is-grouped.is-grouped-right { + justify-content: flex-end; } + .field.is-grouped.is-grouped-multiline { + flex-wrap: wrap; } + .field.is-grouped.is-grouped-multiline > .control:last-child, .field.is-grouped.is-grouped-multiline > .control:not(:last-child) { + margin-bottom: 0.75rem; } + .field.is-grouped.is-grouped-multiline:last-child { + margin-bottom: -0.75rem; } + .field.is-grouped.is-grouped-multiline:not(:last-child) { + margin-bottom: 0; } + +@media screen and (min-width: 769px), print { + .field.is-horizontal { + display: flex; } } + +.field-label .label { + font-size: inherit; } + +@media screen and (max-width: 768px) { + .field-label { + margin-bottom: 0.5rem; } } + +@media screen and (min-width: 769px), print { + .field-label { + flex-basis: 0; + flex-grow: 1; + flex-shrink: 0; + margin-right: 1.5rem; + text-align: right; } + .field-label.is-small { + font-size: 0.75rem; + padding-top: 0.375em; } + .field-label.is-normal { + padding-top: 0.375em; } + .field-label.is-medium { + font-size: 1.25rem; + padding-top: 0.375em; } + .field-label.is-large { + font-size: 1.5rem; + padding-top: 0.375em; } } + +.field-body .field .field { + margin-bottom: 0; } + +@media screen and (min-width: 769px), print { + .field-body { + display: flex; + flex-basis: 0; + flex-grow: 5; + flex-shrink: 1; } + .field-body .field { + margin-bottom: 0; } + .field-body > .field { + flex-shrink: 1; } + .field-body > .field:not(.is-narrow) { + flex-grow: 1; } + .field-body > .field:not(:last-child) { + margin-right: 0.75rem; } } + +.control { + font-size: 1rem; + position: relative; + text-align: left; } + .control.has-icon .icon { + color: #dbdbdb; + height: 2.25em; + pointer-events: none; + position: absolute; + top: 0; + width: 2.25em; + z-index: 4; } + .control.has-icon .input:focus + .icon { + color: #7a7a7a; } + .control.has-icon .input.is-small + .icon { + font-size: 0.75rem; } + .control.has-icon .input.is-medium + .icon { + font-size: 1.25rem; } + .control.has-icon .input.is-large + .icon { + font-size: 1.5rem; } + .control.has-icon:not(.has-icon-right) .icon { + left: 0; } + .control.has-icon:not(.has-icon-right) .input { + padding-left: 2.25em; } + .control.has-icon.has-icon-right .icon { + right: 0; } + .control.has-icon.has-icon-right .input { + padding-right: 2.25em; } + .control.has-icons-left .input:focus ~ .icon, + .control.has-icons-left .select:focus ~ .icon, .control.has-icons-right .input:focus ~ .icon, + .control.has-icons-right .select:focus ~ .icon { + color: #7a7a7a; } + .control.has-icons-left .input.is-small ~ .icon, + .control.has-icons-left .select.is-small ~ .icon, .control.has-icons-right .input.is-small ~ .icon, + .control.has-icons-right .select.is-small ~ .icon { + font-size: 0.75rem; } + .control.has-icons-left .input.is-medium ~ .icon, + .control.has-icons-left .select.is-medium ~ .icon, .control.has-icons-right .input.is-medium ~ .icon, + .control.has-icons-right .select.is-medium ~ .icon { + font-size: 1.25rem; } + .control.has-icons-left .input.is-large ~ .icon, + .control.has-icons-left .select.is-large ~ .icon, .control.has-icons-right .input.is-large ~ .icon, + .control.has-icons-right .select.is-large ~ .icon { + font-size: 1.5rem; } + .control.has-icons-left .icon, .control.has-icons-right .icon { + color: #dbdbdb; + height: 2.25em; + pointer-events: none; + position: absolute; + top: 0; + width: 2.25em; + z-index: 4; } + .control.has-icons-left .input, + .control.has-icons-left .select select { + padding-left: 2.25em; } + .control.has-icons-left .icon.is-left { + left: 0; } + .control.has-icons-right .input, + .control.has-icons-right .select select { + padding-right: 2.25em; } + .control.has-icons-right .icon.is-right { + right: 0; } + .control.is-loading::after { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; + position: absolute !important; + right: 0.625em; + top: 0.625em; } + .control.is-loading.is-small:after { + font-size: 0.75rem; } + .control.is-loading.is-medium:after { + font-size: 1.25rem; } + .control.is-loading.is-large:after { + font-size: 1.5rem; } + +.icon { + align-items: center; + display: inline-flex; + justify-content: center; + height: 1.5rem; + width: 1.5rem; } + .icon.is-small { + height: 1rem; + width: 1rem; } + .icon.is-medium { + height: 2rem; + width: 2rem; } + .icon.is-large { + height: 3rem; + width: 3rem; } + +.image { + display: block; + position: relative; } + .image img { + display: block; + height: auto; + width: 100%; } + .image.is-square img, .image.is-1by1 img, .image.is-4by3 img, .image.is-3by2 img, .image.is-16by9 img, .image.is-2by1 img { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + height: 100%; + width: 100%; } + .image.is-square, .image.is-1by1 { + padding-top: 100%; } + .image.is-4by3 { + padding-top: 75%; } + .image.is-3by2 { + padding-top: 66.6666%; } + .image.is-16by9 { + padding-top: 56.25%; } + .image.is-2by1 { + padding-top: 50%; } + .image.is-16x16 { + height: 16px; + width: 16px; } + .image.is-24x24 { + height: 24px; + width: 24px; } + .image.is-32x32 { + height: 32px; + width: 32px; } + .image.is-48x48 { + height: 48px; + width: 48px; } + .image.is-64x64 { + height: 64px; + width: 64px; } + .image.is-96x96 { + height: 96px; + width: 96px; } + .image.is-128x128 { + height: 128px; + width: 128px; } + +.notification { + background-color: whitesmoke; + border-radius: 3px; + padding: 1.25rem 2.5rem 1.25rem 1.5rem; + position: relative; } + .notification:not(:last-child) { + margin-bottom: 1.5rem; } + .notification a:not(.button) { + color: currentColor; + text-decoration: underline; } + .notification strong { + color: currentColor; } + .notification code, + .notification pre { + background: white; } + .notification pre code { + background: transparent; } + .notification > .delete { + position: absolute; + right: 0.5em; + top: 0.5em; } + .notification .title, + .notification .subtitle, + .notification .content { + color: currentColor; } + .notification.is-white { + background-color: white; + color: #0a0a0a; } + .notification.is-black { + background-color: #0a0a0a; + color: white; } + .notification.is-light { + background-color: whitesmoke; + color: #363636; } + .notification.is-dark { + background-color: #363636; + color: whitesmoke; } + .notification.is-primary { + background-color: #C93312; + color: #fff; } + .notification.is-link { + background-color: #3273dc; + color: #fff; } + .notification.is-info { + background-color: #209cee; + color: #fff; } + .notification.is-success { + background-color: #23d160; + color: #fff; } + .notification.is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .notification.is-danger { + background-color: #ff3860; + color: #fff; } + +.progress { + -moz-appearance: none; + -webkit-appearance: none; + border: none; + border-radius: 290486px; + display: block; + height: 1rem; + overflow: hidden; + padding: 0; + width: 100%; } + .progress:not(:last-child) { + margin-bottom: 1.5rem; } + .progress::-webkit-progress-bar { + background-color: #dbdbdb; } + .progress::-webkit-progress-value { + background-color: #4a4a4a; } + .progress::-moz-progress-bar { + background-color: #4a4a4a; } + .progress::-ms-fill { + background-color: #4a4a4a; + border: none; } + .progress.is-white::-webkit-progress-value { + background-color: white; } + .progress.is-white::-moz-progress-bar { + background-color: white; } + .progress.is-white::-ms-fill { + background-color: white; } + .progress.is-black::-webkit-progress-value { + background-color: #0a0a0a; } + .progress.is-black::-moz-progress-bar { + background-color: #0a0a0a; } + .progress.is-black::-ms-fill { + background-color: #0a0a0a; } + .progress.is-light::-webkit-progress-value { + background-color: whitesmoke; } + .progress.is-light::-moz-progress-bar { + background-color: whitesmoke; } + .progress.is-light::-ms-fill { + background-color: whitesmoke; } + .progress.is-dark::-webkit-progress-value { + background-color: #363636; } + .progress.is-dark::-moz-progress-bar { + background-color: #363636; } + .progress.is-dark::-ms-fill { + background-color: #363636; } + .progress.is-primary::-webkit-progress-value { + background-color: #C93312; } + .progress.is-primary::-moz-progress-bar { + background-color: #C93312; } + .progress.is-primary::-ms-fill { + background-color: #C93312; } + .progress.is-link::-webkit-progress-value { + background-color: #3273dc; } + .progress.is-link::-moz-progress-bar { + background-color: #3273dc; } + .progress.is-link::-ms-fill { + background-color: #3273dc; } + .progress.is-info::-webkit-progress-value { + background-color: #209cee; } + .progress.is-info::-moz-progress-bar { + background-color: #209cee; } + .progress.is-info::-ms-fill { + background-color: #209cee; } + .progress.is-success::-webkit-progress-value { + background-color: #23d160; } + .progress.is-success::-moz-progress-bar { + background-color: #23d160; } + .progress.is-success::-ms-fill { + background-color: #23d160; } + .progress.is-warning::-webkit-progress-value { + background-color: #ffdd57; } + .progress.is-warning::-moz-progress-bar { + background-color: #ffdd57; } + .progress.is-warning::-ms-fill { + background-color: #ffdd57; } + .progress.is-danger::-webkit-progress-value { + background-color: #ff3860; } + .progress.is-danger::-moz-progress-bar { + background-color: #ff3860; } + .progress.is-danger::-ms-fill { + background-color: #ff3860; } + .progress.is-small { + height: 0.75rem; } + .progress.is-medium { + height: 1.25rem; } + .progress.is-large { + height: 1.5rem; } + +.table { + background-color: white; + color: #363636; + margin-bottom: 1.5rem; } + .table td, + .table th { + border: 1px solid #dbdbdb; + border-width: 0 0 1px; + padding: 0.5em 0.75em; + vertical-align: top; } + .table td.is-white, + .table th.is-white { + background-color: white; + border-color: white; + color: #0a0a0a; } + .table td.is-black, + .table th.is-black { + background-color: #0a0a0a; + border-color: #0a0a0a; + color: white; } + .table td.is-light, + .table th.is-light { + background-color: whitesmoke; + border-color: whitesmoke; + color: #363636; } + .table td.is-dark, + .table th.is-dark { + background-color: #363636; + border-color: #363636; + color: whitesmoke; } + .table td.is-primary, + .table th.is-primary { + background-color: #C93312; + border-color: #C93312; + color: #fff; } + .table td.is-link, + .table th.is-link { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; } + .table td.is-info, + .table th.is-info { + background-color: #209cee; + border-color: #209cee; + color: #fff; } + .table td.is-success, + .table th.is-success { + background-color: #23d160; + border-color: #23d160; + color: #fff; } + .table td.is-warning, + .table th.is-warning { + background-color: #ffdd57; + border-color: #ffdd57; + color: #FFFFFF; } + .table td.is-danger, + .table th.is-danger { + background-color: #ff3860; + border-color: #ff3860; + color: #fff; } + .table td.is-narrow, + .table th.is-narrow { + white-space: nowrap; + width: 1%; } + .table td.is-selected, + .table th.is-selected { + background-color: #C93312; + color: #fff; } + .table td.is-selected a, + .table td.is-selected strong, + .table th.is-selected a, + .table th.is-selected strong { + color: currentColor; } + .table th { + color: #363636; + text-align: left; } + .table tr.is-selected { + background-color: #C93312; + color: #fff; } + .table tr.is-selected a, + .table tr.is-selected strong { + color: currentColor; } + .table tr.is-selected td, + .table tr.is-selected th { + border-color: #fff; + color: currentColor; } + .table thead td, + .table thead th { + border-width: 0 0 2px; + color: #363636; } + .table tfoot td, + .table tfoot th { + border-width: 2px 0 0; + color: #363636; } + .table tbody tr:last-child td, + .table tbody tr:last-child th { + border-bottom-width: 0; } + .table.is-bordered td, + .table.is-bordered th { + border-width: 1px; } + .table.is-bordered tr:last-child td, + .table.is-bordered tr:last-child th { + border-bottom-width: 1px; } + .table.is-fullwidth { + width: 100%; } + .table.is-hoverable tbody tr:not(.is-selected):hover { + background-color: #fafafa; } + .table.is-hoverable.is-striped tbody tr:not(.is-selected):hover { + background-color: whitesmoke; } + .table.is-narrow td, + .table.is-narrow th { + padding: 0.25em 0.5em; } + .table.is-striped tbody tr:not(.is-selected):nth-child(even) { + background-color: #fafafa; } + +.tags { + align-items: center; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; } + .tags .tag { + margin-bottom: 0.5rem; } + .tags .tag:not(:last-child) { + margin-right: 0.5rem; } + .tags:last-child { + margin-bottom: -0.5rem; } + .tags:not(:last-child) { + margin-bottom: 1rem; } + .tags.has-addons .tag { + margin-right: 0; } + .tags.has-addons .tag:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; } + .tags.has-addons .tag:not(:last-child) { + border-bottom-right-radius: 0; + border-top-right-radius: 0; } + .tags.is-centered { + justify-content: center; } + .tags.is-centered .tag { + margin-right: 0.25rem; + margin-left: 0.25rem; } + .tags.is-right { + justify-content: flex-end; } + .tags.is-right .tag:not(:first-child) { + margin-left: 0.5rem; } + .tags.is-right .tag:not(:last-child) { + margin-right: 0; } + +.tag:not(body) { + align-items: center; + background-color: whitesmoke; + border-radius: 3px; + color: #4a4a4a; + display: inline-flex; + font-size: 0.75rem; + height: 2em; + justify-content: center; + line-height: 1.5; + padding-left: 0.75em; + padding-right: 0.75em; + white-space: nowrap; } + .tag:not(body) .delete { + margin-left: 0.25em; + margin-right: -0.375em; } + .tag:not(body).is-white { + background-color: white; + color: #0a0a0a; } + .tag:not(body).is-black { + background-color: #0a0a0a; + color: white; } + .tag:not(body).is-light { + background-color: whitesmoke; + color: #363636; } + .tag:not(body).is-dark { + background-color: #363636; + color: whitesmoke; } + .tag:not(body).is-primary { + background-color: #C93312; + color: #fff; } + .tag:not(body).is-link { + background-color: #3273dc; + color: #fff; } + .tag:not(body).is-info { + background-color: #209cee; + color: #fff; } + .tag:not(body).is-success { + background-color: #23d160; + color: #fff; } + .tag:not(body).is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .tag:not(body).is-danger { + background-color: #ff3860; + color: #fff; } + .tag:not(body).is-medium { + font-size: 1rem; } + .tag:not(body).is-large { + font-size: 1.25rem; } + .tag:not(body) .icon:first-child:not(:last-child) { + margin-left: -0.375em; + margin-right: 0.1875em; } + .tag:not(body) .icon:last-child:not(:first-child) { + margin-left: 0.1875em; + margin-right: -0.375em; } + .tag:not(body) .icon:first-child:last-child { + margin-left: -0.375em; + margin-right: -0.375em; } + .tag:not(body).is-delete { + margin-left: 1px; + padding: 0; + position: relative; + width: 2em; } + .tag:not(body).is-delete:before, .tag:not(body).is-delete:after { + background-color: currentColor; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } + .tag:not(body).is-delete:before { + height: 1px; + width: 50%; } + .tag:not(body).is-delete:after { + height: 50%; + width: 1px; } + .tag:not(body).is-delete:hover, .tag:not(body).is-delete:focus { + background-color: #e8e8e8; } + .tag:not(body).is-delete:active { + background-color: #dbdbdb; } + .tag:not(body).is-rounded { + border-radius: 290486px; } + +a.tag:hover { + text-decoration: underline; } + +.title, +.subtitle { + word-break: break-word; } + .title:not(:last-child), + .subtitle:not(:last-child) { + margin-bottom: 1.5rem; } + .title em, + .title span, + .subtitle em, + .subtitle span { + font-weight: inherit; } + .title .tag, + .subtitle .tag { + vertical-align: middle; } + +.title { + color: #363636; + font-size: 2rem; + font-weight: 600; + line-height: 1.125; } + .title strong { + color: inherit; + font-weight: inherit; } + .title + .highlight { + margin-top: -0.75rem; } + .title:not(.is-spaced) + .subtitle { + margin-top: -1.5rem; } + .title.is-1 { + font-size: 3rem; } + .title.is-2 { + font-size: 2.5rem; } + .title.is-3 { + font-size: 2rem; } + .title.is-4 { + font-size: 1.5rem; } + .title.is-5 { + font-size: 1.25rem; } + .title.is-6 { + font-size: 1rem; } + .title.is-7 { + font-size: 0.75rem; } + +.subtitle { + color: #4a4a4a; + font-size: 1.25rem; + font-weight: 400; + line-height: 1.25; } + .subtitle strong { + color: #363636; + font-weight: 600; } + .subtitle:not(.is-spaced) + .title { + margin-top: -1.5rem; } + .subtitle.is-1 { + font-size: 3rem; } + .subtitle.is-2 { + font-size: 2.5rem; } + .subtitle.is-3 { + font-size: 2rem; } + .subtitle.is-4 { + font-size: 1.5rem; } + .subtitle.is-5 { + font-size: 1.25rem; } + .subtitle.is-6 { + font-size: 1rem; } + .subtitle.is-7 { + font-size: 0.75rem; } + +.block:not(:last-child) { + margin-bottom: 1.5rem; } + +.delete { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -moz-appearance: none; + -webkit-appearance: none; + background-color: rgba(10, 10, 10, 0.2); + border: none; + border-radius: 290486px; + cursor: pointer; + display: inline-block; + flex-grow: 0; + flex-shrink: 0; + font-size: 0; + height: 20px; + max-height: 20px; + max-width: 20px; + min-height: 20px; + min-width: 20px; + outline: none; + position: relative; + vertical-align: top; + width: 20px; } + .delete:before, .delete:after { + background-color: white; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } + .delete:before { + height: 2px; + width: 50%; } + .delete:after { + height: 50%; + width: 2px; } + .delete:hover, .delete:focus { + background-color: rgba(10, 10, 10, 0.3); } + .delete:active { + background-color: rgba(10, 10, 10, 0.4); } + .delete.is-small { + height: 16px; + max-height: 16px; + max-width: 16px; + min-height: 16px; + min-width: 16px; + width: 16px; } + .delete.is-medium { + height: 24px; + max-height: 24px; + max-width: 24px; + min-height: 24px; + min-width: 24px; + width: 24px; } + .delete.is-large { + height: 32px; + max-height: 32px; + max-width: 32px; + min-height: 32px; + min-width: 32px; + width: 32px; } + +.heading { + display: block; + font-size: 11px; + letter-spacing: 1px; + margin-bottom: 5px; + text-transform: uppercase; } + +.highlight { + font-weight: 400; + max-width: 100%; + overflow: hidden; + padding: 0; } + .highlight:not(:last-child) { + margin-bottom: 1.5rem; } + .highlight pre { + overflow: auto; + max-width: 100%; } + +.loader { + animation: spinAround 500ms infinite linear; + border: 2px solid #dbdbdb; + border-radius: 290486px; + border-right-color: transparent; + border-top-color: transparent; + content: ""; + display: block; + height: 1em; + position: relative; + width: 1em; } + +.number { + align-items: center; + background-color: whitesmoke; + border-radius: 290486px; + display: inline-flex; + font-size: 1.25rem; + height: 2em; + justify-content: center; + margin-right: 1.5rem; + min-width: 2.5em; + padding: 0.25rem 0.5rem; + text-align: center; + vertical-align: top; } + +.breadcrumb { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: stretch; + display: flex; + font-size: 1rem; + overflow: hidden; + overflow-x: auto; + white-space: nowrap; } + .breadcrumb:not(:last-child) { + margin-bottom: 1.5rem; } + .breadcrumb a { + align-items: center; + color: #3273dc; + display: flex; + justify-content: center; + padding: 0.5em 0.75em; } + .breadcrumb a:hover { + color: #363636; } + .breadcrumb li { + align-items: center; + display: flex; } + .breadcrumb li:first-child a { + padding-left: 0; } + .breadcrumb li.is-active a { + color: #363636; + cursor: default; + pointer-events: none; } + .breadcrumb li + li::before { + color: #4a4a4a; + content: "\0002f"; } + .breadcrumb ul, .breadcrumb ol { + align-items: center; + display: flex; + flex-grow: 1; + flex-shrink: 0; + justify-content: flex-start; } + .breadcrumb .icon:first-child { + margin-right: 0.5em; } + .breadcrumb .icon:last-child { + margin-left: 0.5em; } + .breadcrumb.is-centered ol, .breadcrumb.is-centered ul { + justify-content: center; } + .breadcrumb.is-right ol, .breadcrumb.is-right ul { + justify-content: flex-end; } + .breadcrumb.is-small { + font-size: 0.75rem; } + .breadcrumb.is-medium { + font-size: 1.25rem; } + .breadcrumb.is-large { + font-size: 1.5rem; } + .breadcrumb.has-arrow-separator li + li::before { + content: "\02192"; } + .breadcrumb.has-bullet-separator li + li::before { + content: "\02022"; } + .breadcrumb.has-dot-separator li + li::before { + content: "\000b7"; } + .breadcrumb.has-succeeds-separator li + li::before { + content: "\0227B"; } + +.card { + background-color: white; + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + color: #4a4a4a; + max-width: 100%; + position: relative; } + +.card-header { + align-items: stretch; + box-shadow: 0 1px 2px rgba(10, 10, 10, 0.1); + display: flex; } + +.card-header-title { + align-items: center; + color: #363636; + display: flex; + flex-grow: 1; + font-weight: 700; + padding: 0.75rem; } + .card-header-title.is-centered { + justify-content: center; } + +.card-header-icon { + align-items: center; + cursor: pointer; + display: flex; + justify-content: center; + padding: 0.75rem; } + +.card-image { + display: block; + position: relative; } + +.card-content { + padding: 1.5rem; } + +.card-footer { + border-top: 1px solid #dbdbdb; + align-items: stretch; + display: flex; } + +.card-footer-item { + align-items: center; + display: flex; + flex-basis: 0; + flex-grow: 1; + flex-shrink: 0; + justify-content: center; + padding: 0.75rem; } + .card-footer-item:not(:last-child) { + border-right: 1px solid #dbdbdb; } + +.card .media:not(:last-child) { + margin-bottom: 0.75rem; } + +.dropdown { + display: inline-flex; + position: relative; + vertical-align: top; } + .dropdown.is-active .dropdown-menu, .dropdown.is-hoverable:hover .dropdown-menu { + display: block; } + .dropdown.is-right .dropdown-menu { + left: auto; + right: 0; } + .dropdown.is-up .dropdown-menu { + bottom: 100%; + padding-bottom: 4px; + padding-top: unset; + top: auto; } + +.dropdown-menu { + display: none; + left: 0; + min-width: 12rem; + padding-top: 4px; + position: absolute; + top: 100%; + z-index: 20; } + +.dropdown-content { + background-color: white; + border-radius: 3px; + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + padding-bottom: 0.5rem; + padding-top: 0.5rem; } + +.dropdown-item { + color: #4a4a4a; + display: block; + font-size: 0.875rem; + line-height: 1.5; + padding: 0.375rem 1rem; + position: relative; } + +a.dropdown-item { + padding-right: 3rem; + white-space: nowrap; } + a.dropdown-item:hover { + background-color: whitesmoke; + color: #0a0a0a; } + a.dropdown-item.is-active { + background-color: #3273dc; + color: #fff; } + +.dropdown-divider { + background-color: #dbdbdb; + border: none; + display: block; + height: 1px; + margin: 0.5rem 0; } + +.level { + align-items: center; + justify-content: space-between; } + .level:not(:last-child) { + margin-bottom: 1.5rem; } + .level code { + border-radius: 3px; } + .level img { + display: inline-block; + vertical-align: top; } + .level.is-mobile { + display: flex; } + .level.is-mobile .level-left, + .level.is-mobile .level-right { + display: flex; } + .level.is-mobile .level-left + .level-right { + margin-top: 0; } + .level.is-mobile .level-item { + margin-right: 0.75rem; } + .level.is-mobile .level-item:not(:last-child) { + margin-bottom: 0; } + .level.is-mobile .level-item:not(.is-narrow) { + flex-grow: 1; } + @media screen and (min-width: 769px), print { + .level { + display: flex; } + .level > .level-item:not(.is-narrow) { + flex-grow: 1; } } + +.level-item { + align-items: center; + display: flex; + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; + justify-content: center; } + .level-item .title, + .level-item .subtitle { + margin-bottom: 0; } + @media screen and (max-width: 768px) { + .level-item:not(:last-child) { + margin-bottom: 0.75rem; } } + +.level-left, +.level-right { + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; } + .level-left .level-item.is-flexible, + .level-right .level-item.is-flexible { + flex-grow: 1; } + @media screen and (min-width: 769px), print { + .level-left .level-item:not(:last-child), + .level-right .level-item:not(:last-child) { + margin-right: 0.75rem; } } + +.level-left { + align-items: center; + justify-content: flex-start; } + @media screen and (max-width: 768px) { + .level-left + .level-right { + margin-top: 1.5rem; } } + @media screen and (min-width: 769px), print { + .level-left { + display: flex; } } + +.level-right { + align-items: center; + justify-content: flex-end; } + @media screen and (min-width: 769px), print { + .level-right { + display: flex; } } + +.media { + align-items: flex-start; + display: flex; + text-align: left; } + .media .content:not(:last-child) { + margin-bottom: 0.75rem; } + .media .media { + border-top: 1px solid rgba(219, 219, 219, 0.5); + display: flex; + padding-top: 0.75rem; } + .media .media .content:not(:last-child), + .media .media .control:not(:last-child) { + margin-bottom: 0.5rem; } + .media .media .media { + padding-top: 0.5rem; } + .media .media .media + .media { + margin-top: 0.5rem; } + .media + .media { + border-top: 1px solid rgba(219, 219, 219, 0.5); + margin-top: 1rem; + padding-top: 1rem; } + .media.is-large + .media { + margin-top: 1.5rem; + padding-top: 1.5rem; } + +.media-left, +.media-right { + flex-basis: auto; + flex-grow: 0; + flex-shrink: 0; } + +.media-left { + margin-right: 1rem; } + +.media-right { + margin-left: 1rem; } + +.media-content { + flex-basis: auto; + flex-grow: 1; + flex-shrink: 1; + text-align: left; } + +.menu { + font-size: 1rem; } + .menu.is-small { + font-size: 0.75rem; } + .menu.is-medium { + font-size: 1.25rem; } + .menu.is-large { + font-size: 1.5rem; } + +.menu-list { + line-height: 1.25; } + .menu-list a { + border-radius: 2px; + color: #4a4a4a; + display: block; + padding: 0.5em 0.75em; } + .menu-list a:hover { + background-color: whitesmoke; + color: #363636; } + .menu-list a.is-active { + background-color: #3273dc; + color: #fff; } + .menu-list li ul { + border-left: 1px solid #dbdbdb; + margin: 0.75em; + padding-left: 0.75em; } + +.menu-label { + color: #7a7a7a; + font-size: 0.75em; + letter-spacing: 0.1em; + text-transform: uppercase; } + .menu-label:not(:first-child) { + margin-top: 1em; } + .menu-label:not(:last-child) { + margin-bottom: 1em; } + +.message { + background-color: whitesmoke; + border-radius: 3px; + font-size: 1rem; } + .message:not(:last-child) { + margin-bottom: 1.5rem; } + .message strong { + color: currentColor; } + .message a:not(.button):not(.tag) { + color: currentColor; + text-decoration: underline; } + .message.is-small { + font-size: 0.75rem; } + .message.is-medium { + font-size: 1.25rem; } + .message.is-large { + font-size: 1.5rem; } + .message.is-white { + background-color: white; } + .message.is-white .message-header { + background-color: white; + color: #0a0a0a; } + .message.is-white .message-body { + border-color: white; + color: #4d4d4d; } + .message.is-black { + background-color: #fafafa; } + .message.is-black .message-header { + background-color: #0a0a0a; + color: white; } + .message.is-black .message-body { + border-color: #0a0a0a; + color: #090909; } + .message.is-light { + background-color: #fafafa; } + .message.is-light .message-header { + background-color: whitesmoke; + color: #363636; } + .message.is-light .message-body { + border-color: whitesmoke; + color: #505050; } + .message.is-dark { + background-color: #fafafa; } + .message.is-dark .message-header { + background-color: #363636; + color: whitesmoke; } + .message.is-dark .message-body { + border-color: #363636; + color: #2a2a2a; } + .message.is-primary { + background-color: #fef7f6; } + .message.is-primary .message-header { + background-color: #C93312; + color: #fff; } + .message.is-primary .message-body { + border-color: #C93312; + color: #8a2711; } + .message.is-link { + background-color: #f6f9fe; } + .message.is-link .message-header { + background-color: #3273dc; + color: #fff; } + .message.is-link .message-body { + border-color: #3273dc; + color: #22509a; } + .message.is-info { + background-color: #f6fbfe; } + .message.is-info .message-header { + background-color: #209cee; + color: #fff; } + .message.is-info .message-body { + border-color: #209cee; + color: #12537e; } + .message.is-success { + background-color: #f6fef9; } + .message.is-success .message-header { + background-color: #23d160; + color: #fff; } + .message.is-success .message-body { + border-color: #23d160; + color: #0e301a; } + .message.is-warning { + background-color: #fffdf5; } + .message.is-warning .message-header { + background-color: #ffdd57; + color: #FFFFFF; } + .message.is-warning .message-body { + border-color: #ffdd57; + color: #3b3108; } + .message.is-danger { + background-color: #fff5f7; } + .message.is-danger .message-header { + background-color: #ff3860; + color: #fff; } + .message.is-danger .message-body { + border-color: #ff3860; + color: #cd0930; } + +.message-header { + align-items: center; + background-color: #4a4a4a; + border-radius: 3px 3px 0 0; + color: #fff; + display: flex; + justify-content: space-between; + line-height: 1.25; + padding: 0.5em 0.75em; + position: relative; } + .message-header .delete { + flex-grow: 0; + flex-shrink: 0; + margin-left: 0.75em; } + .message-header + .message-body { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: none; } + +.message-body { + border: 1px solid #dbdbdb; + border-radius: 3px; + color: #4a4a4a; + padding: 1em 1.25em; } + .message-body code, + .message-body pre { + background-color: white; } + .message-body pre code { + background-color: transparent; } + +.modal { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + align-items: center; + display: none; + justify-content: center; + overflow: hidden; + position: fixed; + z-index: 20; } + .modal.is-active { + display: flex; } + +.modal-background { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + background-color: rgba(10, 10, 10, 0.86); } + +.modal-content, +.modal-card { + margin: 0 20px; + max-height: calc(100vh - 160px); + overflow: auto; + position: relative; + width: 100%; } + @media screen and (min-width: 769px), print { + .modal-content, + .modal-card { + margin: 0 auto; + max-height: calc(100vh - 40px); + width: 640px; } } + +.modal-close { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -moz-appearance: none; + -webkit-appearance: none; + background-color: rgba(10, 10, 10, 0.2); + border: none; + border-radius: 290486px; + cursor: pointer; + display: inline-block; + flex-grow: 0; + flex-shrink: 0; + font-size: 0; + height: 20px; + max-height: 20px; + max-width: 20px; + min-height: 20px; + min-width: 20px; + outline: none; + position: relative; + vertical-align: top; + width: 20px; + background: none; + height: 40px; + position: fixed; + right: 20px; + top: 20px; + width: 40px; } + .modal-close:before, .modal-close:after { + background-color: white; + content: ""; + display: block; + left: 50%; + position: absolute; + top: 50%; + transform: translateX(-50%) translateY(-50%) rotate(45deg); + transform-origin: center center; } + .modal-close:before { + height: 2px; + width: 50%; } + .modal-close:after { + height: 50%; + width: 2px; } + .modal-close:hover, .modal-close:focus { + background-color: rgba(10, 10, 10, 0.3); } + .modal-close:active { + background-color: rgba(10, 10, 10, 0.4); } + .modal-close.is-small { + height: 16px; + max-height: 16px; + max-width: 16px; + min-height: 16px; + min-width: 16px; + width: 16px; } + .modal-close.is-medium { + height: 24px; + max-height: 24px; + max-width: 24px; + min-height: 24px; + min-width: 24px; + width: 24px; } + .modal-close.is-large { + height: 32px; + max-height: 32px; + max-width: 32px; + min-height: 32px; + min-width: 32px; + width: 32px; } + +.modal-card { + display: flex; + flex-direction: column; + max-height: calc(100vh - 40px); + overflow: hidden; } + +.modal-card-head, +.modal-card-foot { + align-items: center; + background-color: whitesmoke; + display: flex; + flex-shrink: 0; + justify-content: flex-start; + padding: 20px; + position: relative; } + +.modal-card-head { + border-bottom: 1px solid #dbdbdb; + border-top-left-radius: 5px; + border-top-right-radius: 5px; } + +.modal-card-title { + color: #363636; + flex-grow: 1; + flex-shrink: 0; + font-size: 1.5rem; + line-height: 1; } + +.modal-card-foot { + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-top: 1px solid #dbdbdb; } + .modal-card-foot .button:not(:last-child) { + margin-right: 10px; } + +.modal-card-body { + -webkit-overflow-scrolling: touch; + background-color: white; + flex-grow: 1; + flex-shrink: 1; + overflow: auto; + padding: 20px; } + +.navbar { + background-color: white; + min-height: 3.25rem; + position: relative; } + .navbar.is-white { + background-color: white; + color: #0a0a0a; } + .navbar.is-white .navbar-brand > .navbar-item, + .navbar.is-white .navbar-brand .navbar-link { + color: #0a0a0a; } + .navbar.is-white .navbar-brand > a.navbar-item:hover, .navbar.is-white .navbar-brand > a.navbar-item.is-active, + .navbar.is-white .navbar-brand .navbar-link:hover, + .navbar.is-white .navbar-brand .navbar-link.is-active { + background-color: #f2f2f2; + color: #0a0a0a; } + .navbar.is-white .navbar-brand .navbar-link::after { + border-color: #0a0a0a; } + @media screen and (min-width: 1024px) { + .navbar.is-white .navbar-start > .navbar-item, + .navbar.is-white .navbar-start .navbar-link, + .navbar.is-white .navbar-end > .navbar-item, + .navbar.is-white .navbar-end .navbar-link { + color: #0a0a0a; } + .navbar.is-white .navbar-start > a.navbar-item:hover, .navbar.is-white .navbar-start > a.navbar-item.is-active, + .navbar.is-white .navbar-start .navbar-link:hover, + .navbar.is-white .navbar-start .navbar-link.is-active, + .navbar.is-white .navbar-end > a.navbar-item:hover, + .navbar.is-white .navbar-end > a.navbar-item.is-active, + .navbar.is-white .navbar-end .navbar-link:hover, + .navbar.is-white .navbar-end .navbar-link.is-active { + background-color: #f2f2f2; + color: #0a0a0a; } + .navbar.is-white .navbar-start .navbar-link::after, + .navbar.is-white .navbar-end .navbar-link::after { + border-color: #0a0a0a; } + .navbar.is-white .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #f2f2f2; + color: #0a0a0a; } + .navbar.is-white .navbar-dropdown a.navbar-item.is-active { + background-color: white; + color: #0a0a0a; } } + .navbar.is-black { + background-color: #0a0a0a; + color: white; } + .navbar.is-black .navbar-brand > .navbar-item, + .navbar.is-black .navbar-brand .navbar-link { + color: white; } + .navbar.is-black .navbar-brand > a.navbar-item:hover, .navbar.is-black .navbar-brand > a.navbar-item.is-active, + .navbar.is-black .navbar-brand .navbar-link:hover, + .navbar.is-black .navbar-brand .navbar-link.is-active { + background-color: black; + color: white; } + .navbar.is-black .navbar-brand .navbar-link::after { + border-color: white; } + @media screen and (min-width: 1024px) { + .navbar.is-black .navbar-start > .navbar-item, + .navbar.is-black .navbar-start .navbar-link, + .navbar.is-black .navbar-end > .navbar-item, + .navbar.is-black .navbar-end .navbar-link { + color: white; } + .navbar.is-black .navbar-start > a.navbar-item:hover, .navbar.is-black .navbar-start > a.navbar-item.is-active, + .navbar.is-black .navbar-start .navbar-link:hover, + .navbar.is-black .navbar-start .navbar-link.is-active, + .navbar.is-black .navbar-end > a.navbar-item:hover, + .navbar.is-black .navbar-end > a.navbar-item.is-active, + .navbar.is-black .navbar-end .navbar-link:hover, + .navbar.is-black .navbar-end .navbar-link.is-active { + background-color: black; + color: white; } + .navbar.is-black .navbar-start .navbar-link::after, + .navbar.is-black .navbar-end .navbar-link::after { + border-color: white; } + .navbar.is-black .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link { + background-color: black; + color: white; } + .navbar.is-black .navbar-dropdown a.navbar-item.is-active { + background-color: #0a0a0a; + color: white; } } + .navbar.is-light { + background-color: whitesmoke; + color: #363636; } + .navbar.is-light .navbar-brand > .navbar-item, + .navbar.is-light .navbar-brand .navbar-link { + color: #363636; } + .navbar.is-light .navbar-brand > a.navbar-item:hover, .navbar.is-light .navbar-brand > a.navbar-item.is-active, + .navbar.is-light .navbar-brand .navbar-link:hover, + .navbar.is-light .navbar-brand .navbar-link.is-active { + background-color: #e8e8e8; + color: #363636; } + .navbar.is-light .navbar-brand .navbar-link::after { + border-color: #363636; } + @media screen and (min-width: 1024px) { + .navbar.is-light .navbar-start > .navbar-item, + .navbar.is-light .navbar-start .navbar-link, + .navbar.is-light .navbar-end > .navbar-item, + .navbar.is-light .navbar-end .navbar-link { + color: #363636; } + .navbar.is-light .navbar-start > a.navbar-item:hover, .navbar.is-light .navbar-start > a.navbar-item.is-active, + .navbar.is-light .navbar-start .navbar-link:hover, + .navbar.is-light .navbar-start .navbar-link.is-active, + .navbar.is-light .navbar-end > a.navbar-item:hover, + .navbar.is-light .navbar-end > a.navbar-item.is-active, + .navbar.is-light .navbar-end .navbar-link:hover, + .navbar.is-light .navbar-end .navbar-link.is-active { + background-color: #e8e8e8; + color: #363636; } + .navbar.is-light .navbar-start .navbar-link::after, + .navbar.is-light .navbar-end .navbar-link::after { + border-color: #363636; } + .navbar.is-light .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #e8e8e8; + color: #363636; } + .navbar.is-light .navbar-dropdown a.navbar-item.is-active { + background-color: whitesmoke; + color: #363636; } } + .navbar.is-dark { + background-color: #363636; + color: whitesmoke; } + .navbar.is-dark .navbar-brand > .navbar-item, + .navbar.is-dark .navbar-brand .navbar-link { + color: whitesmoke; } + .navbar.is-dark .navbar-brand > a.navbar-item:hover, .navbar.is-dark .navbar-brand > a.navbar-item.is-active, + .navbar.is-dark .navbar-brand .navbar-link:hover, + .navbar.is-dark .navbar-brand .navbar-link.is-active { + background-color: #292929; + color: whitesmoke; } + .navbar.is-dark .navbar-brand .navbar-link::after { + border-color: whitesmoke; } + @media screen and (min-width: 1024px) { + .navbar.is-dark .navbar-start > .navbar-item, + .navbar.is-dark .navbar-start .navbar-link, + .navbar.is-dark .navbar-end > .navbar-item, + .navbar.is-dark .navbar-end .navbar-link { + color: whitesmoke; } + .navbar.is-dark .navbar-start > a.navbar-item:hover, .navbar.is-dark .navbar-start > a.navbar-item.is-active, + .navbar.is-dark .navbar-start .navbar-link:hover, + .navbar.is-dark .navbar-start .navbar-link.is-active, + .navbar.is-dark .navbar-end > a.navbar-item:hover, + .navbar.is-dark .navbar-end > a.navbar-item.is-active, + .navbar.is-dark .navbar-end .navbar-link:hover, + .navbar.is-dark .navbar-end .navbar-link.is-active { + background-color: #292929; + color: whitesmoke; } + .navbar.is-dark .navbar-start .navbar-link::after, + .navbar.is-dark .navbar-end .navbar-link::after { + border-color: whitesmoke; } + .navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #292929; + color: whitesmoke; } + .navbar.is-dark .navbar-dropdown a.navbar-item.is-active { + background-color: #363636; + color: whitesmoke; } } + .navbar.is-primary { + background-color: #C93312; + color: #fff; } + .navbar.is-primary .navbar-brand > .navbar-item, + .navbar.is-primary .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-primary .navbar-brand > a.navbar-item:hover, .navbar.is-primary .navbar-brand > a.navbar-item.is-active, + .navbar.is-primary .navbar-brand .navbar-link:hover, + .navbar.is-primary .navbar-brand .navbar-link.is-active { + background-color: #b22d10; + color: #fff; } + .navbar.is-primary .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-primary .navbar-start > .navbar-item, + .navbar.is-primary .navbar-start .navbar-link, + .navbar.is-primary .navbar-end > .navbar-item, + .navbar.is-primary .navbar-end .navbar-link { + color: #fff; } + .navbar.is-primary .navbar-start > a.navbar-item:hover, .navbar.is-primary .navbar-start > a.navbar-item.is-active, + .navbar.is-primary .navbar-start .navbar-link:hover, + .navbar.is-primary .navbar-start .navbar-link.is-active, + .navbar.is-primary .navbar-end > a.navbar-item:hover, + .navbar.is-primary .navbar-end > a.navbar-item.is-active, + .navbar.is-primary .navbar-end .navbar-link:hover, + .navbar.is-primary .navbar-end .navbar-link.is-active { + background-color: #b22d10; + color: #fff; } + .navbar.is-primary .navbar-start .navbar-link::after, + .navbar.is-primary .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #b22d10; + color: #fff; } + .navbar.is-primary .navbar-dropdown a.navbar-item.is-active { + background-color: #C93312; + color: #fff; } } + .navbar.is-link { + background-color: #3273dc; + color: #fff; } + .navbar.is-link .navbar-brand > .navbar-item, + .navbar.is-link .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-link .navbar-brand > a.navbar-item:hover, .navbar.is-link .navbar-brand > a.navbar-item.is-active, + .navbar.is-link .navbar-brand .navbar-link:hover, + .navbar.is-link .navbar-brand .navbar-link.is-active { + background-color: #2366d1; + color: #fff; } + .navbar.is-link .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-link .navbar-start > .navbar-item, + .navbar.is-link .navbar-start .navbar-link, + .navbar.is-link .navbar-end > .navbar-item, + .navbar.is-link .navbar-end .navbar-link { + color: #fff; } + .navbar.is-link .navbar-start > a.navbar-item:hover, .navbar.is-link .navbar-start > a.navbar-item.is-active, + .navbar.is-link .navbar-start .navbar-link:hover, + .navbar.is-link .navbar-start .navbar-link.is-active, + .navbar.is-link .navbar-end > a.navbar-item:hover, + .navbar.is-link .navbar-end > a.navbar-item.is-active, + .navbar.is-link .navbar-end .navbar-link:hover, + .navbar.is-link .navbar-end .navbar-link.is-active { + background-color: #2366d1; + color: #fff; } + .navbar.is-link .navbar-start .navbar-link::after, + .navbar.is-link .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-link .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #2366d1; + color: #fff; } + .navbar.is-link .navbar-dropdown a.navbar-item.is-active { + background-color: #3273dc; + color: #fff; } } + .navbar.is-info { + background-color: #209cee; + color: #fff; } + .navbar.is-info .navbar-brand > .navbar-item, + .navbar.is-info .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-info .navbar-brand > a.navbar-item:hover, .navbar.is-info .navbar-brand > a.navbar-item.is-active, + .navbar.is-info .navbar-brand .navbar-link:hover, + .navbar.is-info .navbar-brand .navbar-link.is-active { + background-color: #118fe4; + color: #fff; } + .navbar.is-info .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-info .navbar-start > .navbar-item, + .navbar.is-info .navbar-start .navbar-link, + .navbar.is-info .navbar-end > .navbar-item, + .navbar.is-info .navbar-end .navbar-link { + color: #fff; } + .navbar.is-info .navbar-start > a.navbar-item:hover, .navbar.is-info .navbar-start > a.navbar-item.is-active, + .navbar.is-info .navbar-start .navbar-link:hover, + .navbar.is-info .navbar-start .navbar-link.is-active, + .navbar.is-info .navbar-end > a.navbar-item:hover, + .navbar.is-info .navbar-end > a.navbar-item.is-active, + .navbar.is-info .navbar-end .navbar-link:hover, + .navbar.is-info .navbar-end .navbar-link.is-active { + background-color: #118fe4; + color: #fff; } + .navbar.is-info .navbar-start .navbar-link::after, + .navbar.is-info .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-info .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #118fe4; + color: #fff; } + .navbar.is-info .navbar-dropdown a.navbar-item.is-active { + background-color: #209cee; + color: #fff; } } + .navbar.is-success { + background-color: #23d160; + color: #fff; } + .navbar.is-success .navbar-brand > .navbar-item, + .navbar.is-success .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-success .navbar-brand > a.navbar-item:hover, .navbar.is-success .navbar-brand > a.navbar-item.is-active, + .navbar.is-success .navbar-brand .navbar-link:hover, + .navbar.is-success .navbar-brand .navbar-link.is-active { + background-color: #20bc56; + color: #fff; } + .navbar.is-success .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-success .navbar-start > .navbar-item, + .navbar.is-success .navbar-start .navbar-link, + .navbar.is-success .navbar-end > .navbar-item, + .navbar.is-success .navbar-end .navbar-link { + color: #fff; } + .navbar.is-success .navbar-start > a.navbar-item:hover, .navbar.is-success .navbar-start > a.navbar-item.is-active, + .navbar.is-success .navbar-start .navbar-link:hover, + .navbar.is-success .navbar-start .navbar-link.is-active, + .navbar.is-success .navbar-end > a.navbar-item:hover, + .navbar.is-success .navbar-end > a.navbar-item.is-active, + .navbar.is-success .navbar-end .navbar-link:hover, + .navbar.is-success .navbar-end .navbar-link.is-active { + background-color: #20bc56; + color: #fff; } + .navbar.is-success .navbar-start .navbar-link::after, + .navbar.is-success .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-success .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #20bc56; + color: #fff; } + .navbar.is-success .navbar-dropdown a.navbar-item.is-active { + background-color: #23d160; + color: #fff; } } + .navbar.is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .navbar.is-warning .navbar-brand > .navbar-item, + .navbar.is-warning .navbar-brand .navbar-link { + color: #FFFFFF; } + .navbar.is-warning .navbar-brand > a.navbar-item:hover, .navbar.is-warning .navbar-brand > a.navbar-item.is-active, + .navbar.is-warning .navbar-brand .navbar-link:hover, + .navbar.is-warning .navbar-brand .navbar-link.is-active { + background-color: #ffd83d; + color: #FFFFFF; } + .navbar.is-warning .navbar-brand .navbar-link::after { + border-color: #FFFFFF; } + @media screen and (min-width: 1024px) { + .navbar.is-warning .navbar-start > .navbar-item, + .navbar.is-warning .navbar-start .navbar-link, + .navbar.is-warning .navbar-end > .navbar-item, + .navbar.is-warning .navbar-end .navbar-link { + color: #FFFFFF; } + .navbar.is-warning .navbar-start > a.navbar-item:hover, .navbar.is-warning .navbar-start > a.navbar-item.is-active, + .navbar.is-warning .navbar-start .navbar-link:hover, + .navbar.is-warning .navbar-start .navbar-link.is-active, + .navbar.is-warning .navbar-end > a.navbar-item:hover, + .navbar.is-warning .navbar-end > a.navbar-item.is-active, + .navbar.is-warning .navbar-end .navbar-link:hover, + .navbar.is-warning .navbar-end .navbar-link.is-active { + background-color: #ffd83d; + color: #FFFFFF; } + .navbar.is-warning .navbar-start .navbar-link::after, + .navbar.is-warning .navbar-end .navbar-link::after { + border-color: #FFFFFF; } + .navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #ffd83d; + color: #FFFFFF; } + .navbar.is-warning .navbar-dropdown a.navbar-item.is-active { + background-color: #ffdd57; + color: #FFFFFF; } } + .navbar.is-danger { + background-color: #ff3860; + color: #fff; } + .navbar.is-danger .navbar-brand > .navbar-item, + .navbar.is-danger .navbar-brand .navbar-link { + color: #fff; } + .navbar.is-danger .navbar-brand > a.navbar-item:hover, .navbar.is-danger .navbar-brand > a.navbar-item.is-active, + .navbar.is-danger .navbar-brand .navbar-link:hover, + .navbar.is-danger .navbar-brand .navbar-link.is-active { + background-color: #ff1f4b; + color: #fff; } + .navbar.is-danger .navbar-brand .navbar-link::after { + border-color: #fff; } + @media screen and (min-width: 1024px) { + .navbar.is-danger .navbar-start > .navbar-item, + .navbar.is-danger .navbar-start .navbar-link, + .navbar.is-danger .navbar-end > .navbar-item, + .navbar.is-danger .navbar-end .navbar-link { + color: #fff; } + .navbar.is-danger .navbar-start > a.navbar-item:hover, .navbar.is-danger .navbar-start > a.navbar-item.is-active, + .navbar.is-danger .navbar-start .navbar-link:hover, + .navbar.is-danger .navbar-start .navbar-link.is-active, + .navbar.is-danger .navbar-end > a.navbar-item:hover, + .navbar.is-danger .navbar-end > a.navbar-item.is-active, + .navbar.is-danger .navbar-end .navbar-link:hover, + .navbar.is-danger .navbar-end .navbar-link.is-active { + background-color: #ff1f4b; + color: #fff; } + .navbar.is-danger .navbar-start .navbar-link::after, + .navbar.is-danger .navbar-end .navbar-link::after { + border-color: #fff; } + .navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link, + .navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link { + background-color: #ff1f4b; + color: #fff; } + .navbar.is-danger .navbar-dropdown a.navbar-item.is-active { + background-color: #ff3860; + color: #fff; } } + .navbar > .container { + align-items: stretch; + display: flex; + min-height: 3.25rem; + width: 100%; } + .navbar.has-shadow { + box-shadow: 0 2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-bottom, .navbar.is-fixed-top { + left: 0; + position: fixed; + right: 0; + z-index: 30; } + .navbar.is-fixed-bottom { + bottom: 0; } + .navbar.is-fixed-bottom.has-shadow { + box-shadow: 0 -2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-top { + top: 0; } + +html.has-navbar-fixed-top { + padding-top: 3.25rem; } + +html.has-navbar-fixed-bottom { + padding-bottom: 3.25rem; } + +.navbar-brand, +.navbar-tabs { + align-items: stretch; + display: flex; + flex-shrink: 0; + min-height: 3.25rem; } + +.navbar-tabs { + -webkit-overflow-scrolling: touch; + max-width: 100vw; + overflow-x: auto; + overflow-y: hidden; } + +.navbar-burger { + cursor: pointer; + display: block; + height: 3.25rem; + position: relative; + width: 3.25rem; + margin-left: auto; } + .navbar-burger span { + background-color: currentColor; + display: block; + height: 1px; + left: calc(50% - 8px); + position: absolute; + transform-origin: center; + transition-duration: 86ms; + transition-property: background-color, opacity, transform; + transition-timing-function: ease-out; + width: 16px; } + .navbar-burger span:nth-child(1) { + top: calc(50% - 6px); } + .navbar-burger span:nth-child(2) { + top: calc(50% - 1px); } + .navbar-burger span:nth-child(3) { + top: calc(50% + 4px); } + .navbar-burger:hover { + background-color: rgba(0, 0, 0, 0.05); } + .navbar-burger.is-active span:nth-child(1) { + transform: translateY(5px) rotate(45deg); } + .navbar-burger.is-active span:nth-child(2) { + opacity: 0; } + .navbar-burger.is-active span:nth-child(3) { + transform: translateY(-5px) rotate(-45deg); } + +.navbar-menu { + display: none; } + +.navbar-item, +.navbar-link { + color: #4a4a4a; + display: block; + line-height: 1.5; + padding: 0.5rem 1rem; + position: relative; } + +a.navbar-item:hover, a.navbar-item.is-active, +a.navbar-link:hover, +a.navbar-link.is-active { + background-color: whitesmoke; + color: #3273dc; } + +.navbar-item { + flex-grow: 0; + flex-shrink: 0; } + .navbar-item img { + max-height: 1.75rem; } + .navbar-item.has-dropdown { + padding: 0; } + .navbar-item.is-expanded { + flex-grow: 1; + flex-shrink: 1; } + .navbar-item.is-tab { + border-bottom: 1px solid transparent; + min-height: 3.25rem; + padding-bottom: calc(0.5rem - 1px); } + .navbar-item.is-tab:hover { + background-color: transparent; + border-bottom-color: #3273dc; } + .navbar-item.is-tab.is-active { + background-color: transparent; + border-bottom-color: #3273dc; + border-bottom-style: solid; + border-bottom-width: 3px; + color: #3273dc; + padding-bottom: calc(0.5rem - 3px); } + +.navbar-content { + flex-grow: 1; + flex-shrink: 1; } + +.navbar-link { + padding-right: 2.5em; } + +.navbar-dropdown { + font-size: 0.875rem; + padding-bottom: 0.5rem; + padding-top: 0.5rem; } + .navbar-dropdown .navbar-item { + padding-left: 1.5rem; + padding-right: 1.5rem; } + +.navbar-divider { + background-color: #dbdbdb; + border: none; + display: none; + height: 1px; + margin: 0.5rem 0; } + +@media screen and (max-width: 1023px) { + .navbar > .container { + display: block; } + .navbar-brand .navbar-item, + .navbar-tabs .navbar-item { + align-items: center; + display: flex; } + .navbar-menu { + background-color: white; + box-shadow: 0 8px 16px rgba(10, 10, 10, 0.1); + padding: 0.5rem 0; } + .navbar-menu.is-active { + display: block; } + .navbar.is-fixed-bottom-touch, .navbar.is-fixed-top-touch { + left: 0; + position: fixed; + right: 0; + z-index: 30; } + .navbar.is-fixed-bottom-touch { + bottom: 0; } + .navbar.is-fixed-bottom-touch.has-shadow { + box-shadow: 0 -2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-top-touch { + top: 0; } + .navbar.is-fixed-top .navbar-menu, .navbar.is-fixed-top-touch .navbar-menu { + -webkit-overflow-scrolling: touch; + max-height: calc(100vh - 3.25rem); + overflow: auto; } + html.has-navbar-fixed-top-touch { + padding-top: 3.25rem; } + html.has-navbar-fixed-bottom-touch { + padding-bottom: 3.25rem; } } + +@media screen and (min-width: 1024px) { + .navbar, + .navbar-menu, + .navbar-start, + .navbar-end { + align-items: stretch; + display: flex; } + .navbar { + min-height: 3.25rem; } + .navbar.is-transparent a.navbar-item:hover, .navbar.is-transparent a.navbar-item.is-active, + .navbar.is-transparent a.navbar-link:hover, + .navbar.is-transparent a.navbar-link.is-active { + background-color: transparent !important; } + .navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link, .navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link { + background-color: transparent !important; } + .navbar.is-transparent .navbar-dropdown a.navbar-item:hover { + background-color: whitesmoke; + color: #0a0a0a; } + .navbar.is-transparent .navbar-dropdown a.navbar-item.is-active { + background-color: whitesmoke; + color: #3273dc; } + .navbar-burger { + display: none; } + .navbar-item, + .navbar-link { + align-items: center; + display: flex; } + .navbar-item.has-dropdown { + align-items: stretch; } + .navbar-item.has-dropdown-up .navbar-link::after { + transform: rotate(135deg) translate(0.25em, -0.25em); } + .navbar-item.has-dropdown-up .navbar-dropdown { + border-bottom: 1px solid #dbdbdb; + border-radius: 5px 5px 0 0; + border-top: none; + bottom: 100%; + box-shadow: 0 -8px 8px rgba(10, 10, 10, 0.1); + top: auto; } + .navbar-item.is-active .navbar-dropdown, .navbar-item.is-hoverable:hover .navbar-dropdown { + display: block; } + .navbar-item.is-active .navbar-dropdown.is-boxed, .navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed { + opacity: 1; + pointer-events: auto; + transform: translateY(0); } + .navbar-link::after { + border: 1px solid #3273dc; + border-right: 0; + border-top: 0; + content: " "; + display: block; + height: 0.5em; + pointer-events: none; + position: absolute; + transform: rotate(-45deg); + transform-origin: center; + width: 0.5em; + margin-top: -0.375em; + right: 1.125em; + top: 50%; } + .navbar-menu { + flex-grow: 1; + flex-shrink: 0; } + .navbar-start { + justify-content: flex-start; + margin-right: auto; } + .navbar-end { + justify-content: flex-end; + margin-left: auto; } + .navbar-dropdown { + background-color: white; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + border-top: 1px solid #dbdbdb; + box-shadow: 0 8px 8px rgba(10, 10, 10, 0.1); + display: none; + font-size: 0.875rem; + left: 0; + min-width: 100%; + position: absolute; + top: 100%; + z-index: 20; } + .navbar-dropdown .navbar-item { + padding: 0.375rem 1rem; + white-space: nowrap; } + .navbar-dropdown a.navbar-item { + padding-right: 3rem; } + .navbar-dropdown a.navbar-item:hover { + background-color: whitesmoke; + color: #0a0a0a; } + .navbar-dropdown a.navbar-item.is-active { + background-color: whitesmoke; + color: #3273dc; } + .navbar-dropdown.is-boxed { + border-radius: 5px; + border-top: none; + box-shadow: 0 8px 8px rgba(10, 10, 10, 0.1), 0 0 0 1px rgba(10, 10, 10, 0.1); + display: block; + opacity: 0; + pointer-events: none; + top: calc(100% + (-4px)); + transform: translateY(-5px); + transition-duration: 86ms; + transition-property: opacity, transform; } + .navbar-dropdown.is-right { + left: auto; + right: 0; } + .navbar-divider { + display: block; } + .navbar > .container .navbar-brand, + .container > .navbar .navbar-brand { + margin-left: -1rem; } + .navbar > .container .navbar-menu, + .container > .navbar .navbar-menu { + margin-right: -1rem; } + .navbar.is-fixed-bottom-desktop, .navbar.is-fixed-top-desktop { + left: 0; + position: fixed; + right: 0; + z-index: 30; } + .navbar.is-fixed-bottom-desktop { + bottom: 0; } + .navbar.is-fixed-bottom-desktop.has-shadow { + box-shadow: 0 -2px 3px rgba(10, 10, 10, 0.1); } + .navbar.is-fixed-top-desktop { + top: 0; } + html.has-navbar-fixed-top-desktop { + padding-top: 3.25rem; } + html.has-navbar-fixed-bottom-desktop { + padding-bottom: 3.25rem; } + a.navbar-item.is-active, + a.navbar-link.is-active { + color: #0a0a0a; } + a.navbar-item.is-active:not(:hover), + a.navbar-link.is-active:not(:hover) { + background-color: transparent; } + .navbar-item.has-dropdown:hover .navbar-link, .navbar-item.has-dropdown.is-active .navbar-link { + background-color: whitesmoke; } } + +.pagination { + font-size: 1rem; + margin: -0.25rem; } + .pagination.is-small { + font-size: 0.75rem; } + .pagination.is-medium { + font-size: 1.25rem; } + .pagination.is-large { + font-size: 1.5rem; } + +.pagination, +.pagination-list { + align-items: center; + display: flex; + justify-content: center; + text-align: center; } + +.pagination-previous, +.pagination-next, +.pagination-link, +.pagination-ellipsis { + -moz-appearance: none; + -webkit-appearance: none; + align-items: center; + border: 1px solid transparent; + border-radius: 3px; + box-shadow: none; + display: inline-flex; + font-size: 1rem; + height: 2.25em; + justify-content: flex-start; + line-height: 1.5; + padding-bottom: calc(0.375em - 1px); + padding-left: calc(0.625em - 1px); + padding-right: calc(0.625em - 1px); + padding-top: calc(0.375em - 1px); + position: relative; + vertical-align: top; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-size: 1em; + padding-left: 0.5em; + padding-right: 0.5em; + justify-content: center; + margin: 0.25rem; + text-align: center; } + .pagination-previous:focus, .pagination-previous.is-focused, .pagination-previous:active, .pagination-previous.is-active, + .pagination-next:focus, + .pagination-next.is-focused, + .pagination-next:active, + .pagination-next.is-active, + .pagination-link:focus, + .pagination-link.is-focused, + .pagination-link:active, + .pagination-link.is-active, + .pagination-ellipsis:focus, + .pagination-ellipsis.is-focused, + .pagination-ellipsis:active, + .pagination-ellipsis.is-active { + outline: none; } + .pagination-previous[disabled], + .pagination-next[disabled], + .pagination-link[disabled], + .pagination-ellipsis[disabled] { + cursor: not-allowed; } + +.pagination-previous, +.pagination-next, +.pagination-link { + border-color: #dbdbdb; + min-width: 2.25em; } + .pagination-previous:hover, + .pagination-next:hover, + .pagination-link:hover { + border-color: #b5b5b5; + color: #363636; } + .pagination-previous:focus, + .pagination-next:focus, + .pagination-link:focus { + border-color: #3273dc; } + .pagination-previous:active, + .pagination-next:active, + .pagination-link:active { + box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.2); } + .pagination-previous[disabled], + .pagination-next[disabled], + .pagination-link[disabled] { + background-color: #dbdbdb; + border-color: #dbdbdb; + box-shadow: none; + color: #7a7a7a; + opacity: 0.5; } + +.pagination-previous, +.pagination-next { + padding-left: 0.75em; + padding-right: 0.75em; + white-space: nowrap; } + +.pagination-link.is-current { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; } + +.pagination-ellipsis { + color: #b5b5b5; + pointer-events: none; } + +.pagination-list { + flex-wrap: wrap; } + +@media screen and (max-width: 768px) { + .pagination { + flex-wrap: wrap; } + .pagination-previous, + .pagination-next { + flex-grow: 1; + flex-shrink: 1; } + .pagination-list li { + flex-grow: 1; + flex-shrink: 1; } } + +@media screen and (min-width: 769px), print { + .pagination-list { + flex-grow: 1; + flex-shrink: 1; + justify-content: flex-start; + order: 1; } + .pagination-previous { + order: 2; } + .pagination-next { + order: 3; } + .pagination { + justify-content: space-between; } + .pagination.is-centered .pagination-previous { + order: 1; } + .pagination.is-centered .pagination-list { + justify-content: center; + order: 2; } + .pagination.is-centered .pagination-next { + order: 3; } + .pagination.is-right .pagination-previous { + order: 1; } + .pagination.is-right .pagination-next { + order: 2; } + .pagination.is-right .pagination-list { + justify-content: flex-end; + order: 3; } } + +.panel { + font-size: 1rem; } + .panel:not(:last-child) { + margin-bottom: 1.5rem; } + +.panel-heading, +.panel-tabs, +.panel-block { + border-bottom: 1px solid #dbdbdb; + border-left: 1px solid #dbdbdb; + border-right: 1px solid #dbdbdb; } + .panel-heading:first-child, + .panel-tabs:first-child, + .panel-block:first-child { + border-top: 1px solid #dbdbdb; } + +.panel-heading { + background-color: whitesmoke; + border-radius: 3px 3px 0 0; + color: #363636; + font-size: 1.25em; + font-weight: 300; + line-height: 1.25; + padding: 0.5em 0.75em; } + +.panel-tabs { + align-items: flex-end; + display: flex; + font-size: 0.875em; + justify-content: center; } + .panel-tabs a { + border-bottom: 1px solid #dbdbdb; + margin-bottom: -1px; + padding: 0.5em; } + .panel-tabs a.is-active { + border-bottom-color: #4a4a4a; + color: #363636; } + +.panel-list a { + color: #4a4a4a; } + .panel-list a:hover { + color: #3273dc; } + +.panel-block { + align-items: center; + color: #363636; + display: flex; + justify-content: flex-start; + padding: 0.5em 0.75em; } + .panel-block input[type="checkbox"] { + margin-right: 0.75em; } + .panel-block > .control { + flex-grow: 1; + flex-shrink: 1; + width: 100%; } + .panel-block.is-wrapped { + flex-wrap: wrap; } + .panel-block.is-active { + border-left-color: #3273dc; + color: #363636; } + .panel-block.is-active .panel-icon { + color: #3273dc; } + +a.panel-block, +label.panel-block { + cursor: pointer; } + a.panel-block:hover, + label.panel-block:hover { + background-color: whitesmoke; } + +.panel-icon { + display: inline-block; + font-size: 14px; + height: 1em; + line-height: 1em; + text-align: center; + vertical-align: top; + width: 1em; + color: #7a7a7a; + margin-right: 0.75em; } + .panel-icon .fa { + font-size: inherit; + line-height: inherit; } + +.tabs { + -webkit-overflow-scrolling: touch; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: stretch; + display: flex; + font-size: 1rem; + justify-content: space-between; + overflow: hidden; + overflow-x: auto; + white-space: nowrap; } + .tabs:not(:last-child) { + margin-bottom: 1.5rem; } + .tabs a { + align-items: center; + border-bottom-color: #dbdbdb; + border-bottom-style: solid; + border-bottom-width: 1px; + color: #4a4a4a; + display: flex; + justify-content: center; + margin-bottom: -1px; + padding: 0.5em 1em; + vertical-align: top; } + .tabs a:hover { + border-bottom-color: #363636; + color: #363636; } + .tabs li { + display: block; } + .tabs li.is-active a { + border-bottom-color: #3273dc; + color: #3273dc; } + .tabs ul { + align-items: center; + border-bottom-color: #dbdbdb; + border-bottom-style: solid; + border-bottom-width: 1px; + display: flex; + flex-grow: 1; + flex-shrink: 0; + justify-content: flex-start; } + .tabs ul.is-left { + padding-right: 0.75em; } + .tabs ul.is-center { + flex: none; + justify-content: center; + padding-left: 0.75em; + padding-right: 0.75em; } + .tabs ul.is-right { + justify-content: flex-end; + padding-left: 0.75em; } + .tabs .icon:first-child { + margin-right: 0.5em; } + .tabs .icon:last-child { + margin-left: 0.5em; } + .tabs.is-centered ul { + justify-content: center; } + .tabs.is-right ul { + justify-content: flex-end; } + .tabs.is-boxed a { + border: 1px solid transparent; + border-radius: 3px 3px 0 0; } + .tabs.is-boxed a:hover { + background-color: whitesmoke; + border-bottom-color: #dbdbdb; } + .tabs.is-boxed li.is-active a { + background-color: white; + border-color: #dbdbdb; + border-bottom-color: transparent !important; } + .tabs.is-fullwidth li { + flex-grow: 1; + flex-shrink: 0; } + .tabs.is-toggle a { + border-color: #dbdbdb; + border-style: solid; + border-width: 1px; + margin-bottom: 0; + position: relative; } + .tabs.is-toggle a:hover { + background-color: whitesmoke; + border-color: #b5b5b5; + z-index: 2; } + .tabs.is-toggle li + li { + margin-left: -1px; } + .tabs.is-toggle li:first-child a { + border-radius: 3px 0 0 3px; } + .tabs.is-toggle li:last-child a { + border-radius: 0 3px 3px 0; } + .tabs.is-toggle li.is-active a { + background-color: #3273dc; + border-color: #3273dc; + color: #fff; + z-index: 1; } + .tabs.is-toggle ul { + border-bottom: none; } + .tabs.is-small { + font-size: 0.75rem; } + .tabs.is-medium { + font-size: 1.25rem; } + .tabs.is-large { + font-size: 1.5rem; } + +.hero { + align-items: stretch; + display: flex; + flex-direction: column; + justify-content: space-between; } + .hero .navbar { + background: none; } + .hero .tabs ul { + border-bottom: none; } + .hero.is-white { + background-color: white; + color: #0a0a0a; } + .hero.is-white a:not(.button), + .hero.is-white strong { + color: inherit; } + .hero.is-white .title { + color: #0a0a0a; } + .hero.is-white .subtitle { + color: rgba(10, 10, 10, 0.9); } + .hero.is-white .subtitle a:not(.button), + .hero.is-white .subtitle strong { + color: #0a0a0a; } + @media screen and (max-width: 1023px) { + .hero.is-white .navbar-menu { + background-color: white; } } + .hero.is-white .navbar-item, + .hero.is-white .navbar-link { + color: rgba(10, 10, 10, 0.7); } + .hero.is-white a.navbar-item:hover, .hero.is-white a.navbar-item.is-active, + .hero.is-white .navbar-link:hover, + .hero.is-white .navbar-link.is-active { + background-color: #f2f2f2; + color: #0a0a0a; } + .hero.is-white .tabs a { + color: #0a0a0a; + opacity: 0.9; } + .hero.is-white .tabs a:hover { + opacity: 1; } + .hero.is-white .tabs li.is-active a { + opacity: 1; } + .hero.is-white .tabs.is-boxed a, .hero.is-white .tabs.is-toggle a { + color: #0a0a0a; } + .hero.is-white .tabs.is-boxed a:hover, .hero.is-white .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-white .tabs.is-boxed li.is-active a, .hero.is-white .tabs.is-boxed li.is-active a:hover, .hero.is-white .tabs.is-toggle li.is-active a, .hero.is-white .tabs.is-toggle li.is-active a:hover { + background-color: #0a0a0a; + border-color: #0a0a0a; + color: white; } + .hero.is-white.is-bold { + background-image: linear-gradient(141deg, #e6e6e6 0%, white 71%, white 100%); } + @media screen and (max-width: 768px) { + .hero.is-white.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #e6e6e6 0%, white 71%, white 100%); } } + .hero.is-black { + background-color: #0a0a0a; + color: white; } + .hero.is-black a:not(.button), + .hero.is-black strong { + color: inherit; } + .hero.is-black .title { + color: white; } + .hero.is-black .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-black .subtitle a:not(.button), + .hero.is-black .subtitle strong { + color: white; } + @media screen and (max-width: 1023px) { + .hero.is-black .navbar-menu { + background-color: #0a0a0a; } } + .hero.is-black .navbar-item, + .hero.is-black .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-black a.navbar-item:hover, .hero.is-black a.navbar-item.is-active, + .hero.is-black .navbar-link:hover, + .hero.is-black .navbar-link.is-active { + background-color: black; + color: white; } + .hero.is-black .tabs a { + color: white; + opacity: 0.9; } + .hero.is-black .tabs a:hover { + opacity: 1; } + .hero.is-black .tabs li.is-active a { + opacity: 1; } + .hero.is-black .tabs.is-boxed a, .hero.is-black .tabs.is-toggle a { + color: white; } + .hero.is-black .tabs.is-boxed a:hover, .hero.is-black .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-black .tabs.is-boxed li.is-active a, .hero.is-black .tabs.is-boxed li.is-active a:hover, .hero.is-black .tabs.is-toggle li.is-active a, .hero.is-black .tabs.is-toggle li.is-active a:hover { + background-color: white; + border-color: white; + color: #0a0a0a; } + .hero.is-black.is-bold { + background-image: linear-gradient(141deg, black 0%, #0a0a0a 71%, #181616 100%); } + @media screen and (max-width: 768px) { + .hero.is-black.is-bold .navbar-menu { + background-image: linear-gradient(141deg, black 0%, #0a0a0a 71%, #181616 100%); } } + .hero.is-light { + background-color: whitesmoke; + color: #363636; } + .hero.is-light a:not(.button), + .hero.is-light strong { + color: inherit; } + .hero.is-light .title { + color: #363636; } + .hero.is-light .subtitle { + color: rgba(54, 54, 54, 0.9); } + .hero.is-light .subtitle a:not(.button), + .hero.is-light .subtitle strong { + color: #363636; } + @media screen and (max-width: 1023px) { + .hero.is-light .navbar-menu { + background-color: whitesmoke; } } + .hero.is-light .navbar-item, + .hero.is-light .navbar-link { + color: rgba(54, 54, 54, 0.7); } + .hero.is-light a.navbar-item:hover, .hero.is-light a.navbar-item.is-active, + .hero.is-light .navbar-link:hover, + .hero.is-light .navbar-link.is-active { + background-color: #e8e8e8; + color: #363636; } + .hero.is-light .tabs a { + color: #363636; + opacity: 0.9; } + .hero.is-light .tabs a:hover { + opacity: 1; } + .hero.is-light .tabs li.is-active a { + opacity: 1; } + .hero.is-light .tabs.is-boxed a, .hero.is-light .tabs.is-toggle a { + color: #363636; } + .hero.is-light .tabs.is-boxed a:hover, .hero.is-light .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-light .tabs.is-boxed li.is-active a, .hero.is-light .tabs.is-boxed li.is-active a:hover, .hero.is-light .tabs.is-toggle li.is-active a, .hero.is-light .tabs.is-toggle li.is-active a:hover { + background-color: #363636; + border-color: #363636; + color: whitesmoke; } + .hero.is-light.is-bold { + background-image: linear-gradient(141deg, #dfd8d9 0%, whitesmoke 71%, white 100%); } + @media screen and (max-width: 768px) { + .hero.is-light.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #dfd8d9 0%, whitesmoke 71%, white 100%); } } + .hero.is-dark { + background-color: #363636; + color: whitesmoke; } + .hero.is-dark a:not(.button), + .hero.is-dark strong { + color: inherit; } + .hero.is-dark .title { + color: whitesmoke; } + .hero.is-dark .subtitle { + color: rgba(245, 245, 245, 0.9); } + .hero.is-dark .subtitle a:not(.button), + .hero.is-dark .subtitle strong { + color: whitesmoke; } + @media screen and (max-width: 1023px) { + .hero.is-dark .navbar-menu { + background-color: #363636; } } + .hero.is-dark .navbar-item, + .hero.is-dark .navbar-link { + color: rgba(245, 245, 245, 0.7); } + .hero.is-dark a.navbar-item:hover, .hero.is-dark a.navbar-item.is-active, + .hero.is-dark .navbar-link:hover, + .hero.is-dark .navbar-link.is-active { + background-color: #292929; + color: whitesmoke; } + .hero.is-dark .tabs a { + color: whitesmoke; + opacity: 0.9; } + .hero.is-dark .tabs a:hover { + opacity: 1; } + .hero.is-dark .tabs li.is-active a { + opacity: 1; } + .hero.is-dark .tabs.is-boxed a, .hero.is-dark .tabs.is-toggle a { + color: whitesmoke; } + .hero.is-dark .tabs.is-boxed a:hover, .hero.is-dark .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-dark .tabs.is-boxed li.is-active a, .hero.is-dark .tabs.is-boxed li.is-active a:hover, .hero.is-dark .tabs.is-toggle li.is-active a, .hero.is-dark .tabs.is-toggle li.is-active a:hover { + background-color: whitesmoke; + border-color: whitesmoke; + color: #363636; } + .hero.is-dark.is-bold { + background-image: linear-gradient(141deg, #1f191a 0%, #363636 71%, #46403f 100%); } + @media screen and (max-width: 768px) { + .hero.is-dark.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #1f191a 0%, #363636 71%, #46403f 100%); } } + .hero.is-primary { + background-color: #C93312; + color: #fff; } + .hero.is-primary a:not(.button), + .hero.is-primary strong { + color: inherit; } + .hero.is-primary .title { + color: #fff; } + .hero.is-primary .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-primary .subtitle a:not(.button), + .hero.is-primary .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-primary .navbar-menu { + background-color: #C93312; } } + .hero.is-primary .navbar-item, + .hero.is-primary .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-primary a.navbar-item:hover, .hero.is-primary a.navbar-item.is-active, + .hero.is-primary .navbar-link:hover, + .hero.is-primary .navbar-link.is-active { + background-color: #b22d10; + color: #fff; } + .hero.is-primary .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-primary .tabs a:hover { + opacity: 1; } + .hero.is-primary .tabs li.is-active a { + opacity: 1; } + .hero.is-primary .tabs.is-boxed a, .hero.is-primary .tabs.is-toggle a { + color: #fff; } + .hero.is-primary .tabs.is-boxed a:hover, .hero.is-primary .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-primary .tabs.is-boxed li.is-active a, .hero.is-primary .tabs.is-boxed li.is-active a:hover, .hero.is-primary .tabs.is-toggle li.is-active a, .hero.is-primary .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #C93312; } + .hero.is-primary.is-bold { + background-image: linear-gradient(141deg, #a30805 0%, #C93312 71%, #e7590e 100%); } + @media screen and (max-width: 768px) { + .hero.is-primary.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #a30805 0%, #C93312 71%, #e7590e 100%); } } + .hero.is-link { + background-color: #3273dc; + color: #fff; } + .hero.is-link a:not(.button), + .hero.is-link strong { + color: inherit; } + .hero.is-link .title { + color: #fff; } + .hero.is-link .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-link .subtitle a:not(.button), + .hero.is-link .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-link .navbar-menu { + background-color: #3273dc; } } + .hero.is-link .navbar-item, + .hero.is-link .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-link a.navbar-item:hover, .hero.is-link a.navbar-item.is-active, + .hero.is-link .navbar-link:hover, + .hero.is-link .navbar-link.is-active { + background-color: #2366d1; + color: #fff; } + .hero.is-link .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-link .tabs a:hover { + opacity: 1; } + .hero.is-link .tabs li.is-active a { + opacity: 1; } + .hero.is-link .tabs.is-boxed a, .hero.is-link .tabs.is-toggle a { + color: #fff; } + .hero.is-link .tabs.is-boxed a:hover, .hero.is-link .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-link .tabs.is-boxed li.is-active a, .hero.is-link .tabs.is-boxed li.is-active a:hover, .hero.is-link .tabs.is-toggle li.is-active a, .hero.is-link .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #3273dc; } + .hero.is-link.is-bold { + background-image: linear-gradient(141deg, #1577c6 0%, #3273dc 71%, #4366e5 100%); } + @media screen and (max-width: 768px) { + .hero.is-link.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #1577c6 0%, #3273dc 71%, #4366e5 100%); } } + .hero.is-info { + background-color: #209cee; + color: #fff; } + .hero.is-info a:not(.button), + .hero.is-info strong { + color: inherit; } + .hero.is-info .title { + color: #fff; } + .hero.is-info .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-info .subtitle a:not(.button), + .hero.is-info .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-info .navbar-menu { + background-color: #209cee; } } + .hero.is-info .navbar-item, + .hero.is-info .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-info a.navbar-item:hover, .hero.is-info a.navbar-item.is-active, + .hero.is-info .navbar-link:hover, + .hero.is-info .navbar-link.is-active { + background-color: #118fe4; + color: #fff; } + .hero.is-info .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-info .tabs a:hover { + opacity: 1; } + .hero.is-info .tabs li.is-active a { + opacity: 1; } + .hero.is-info .tabs.is-boxed a, .hero.is-info .tabs.is-toggle a { + color: #fff; } + .hero.is-info .tabs.is-boxed a:hover, .hero.is-info .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-info .tabs.is-boxed li.is-active a, .hero.is-info .tabs.is-boxed li.is-active a:hover, .hero.is-info .tabs.is-toggle li.is-active a, .hero.is-info .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #209cee; } + .hero.is-info.is-bold { + background-image: linear-gradient(141deg, #04a6d7 0%, #209cee 71%, #3287f5 100%); } + @media screen and (max-width: 768px) { + .hero.is-info.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #04a6d7 0%, #209cee 71%, #3287f5 100%); } } + .hero.is-success { + background-color: #23d160; + color: #fff; } + .hero.is-success a:not(.button), + .hero.is-success strong { + color: inherit; } + .hero.is-success .title { + color: #fff; } + .hero.is-success .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-success .subtitle a:not(.button), + .hero.is-success .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-success .navbar-menu { + background-color: #23d160; } } + .hero.is-success .navbar-item, + .hero.is-success .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-success a.navbar-item:hover, .hero.is-success a.navbar-item.is-active, + .hero.is-success .navbar-link:hover, + .hero.is-success .navbar-link.is-active { + background-color: #20bc56; + color: #fff; } + .hero.is-success .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-success .tabs a:hover { + opacity: 1; } + .hero.is-success .tabs li.is-active a { + opacity: 1; } + .hero.is-success .tabs.is-boxed a, .hero.is-success .tabs.is-toggle a { + color: #fff; } + .hero.is-success .tabs.is-boxed a:hover, .hero.is-success .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-success .tabs.is-boxed li.is-active a, .hero.is-success .tabs.is-boxed li.is-active a:hover, .hero.is-success .tabs.is-toggle li.is-active a, .hero.is-success .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #23d160; } + .hero.is-success.is-bold { + background-image: linear-gradient(141deg, #12af2f 0%, #23d160 71%, #2ce28a 100%); } + @media screen and (max-width: 768px) { + .hero.is-success.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #12af2f 0%, #23d160 71%, #2ce28a 100%); } } + .hero.is-warning { + background-color: #ffdd57; + color: #FFFFFF; } + .hero.is-warning a:not(.button), + .hero.is-warning strong { + color: inherit; } + .hero.is-warning .title { + color: #FFFFFF; } + .hero.is-warning .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-warning .subtitle a:not(.button), + .hero.is-warning .subtitle strong { + color: #FFFFFF; } + @media screen and (max-width: 1023px) { + .hero.is-warning .navbar-menu { + background-color: #ffdd57; } } + .hero.is-warning .navbar-item, + .hero.is-warning .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-warning a.navbar-item:hover, .hero.is-warning a.navbar-item.is-active, + .hero.is-warning .navbar-link:hover, + .hero.is-warning .navbar-link.is-active { + background-color: #ffd83d; + color: #FFFFFF; } + .hero.is-warning .tabs a { + color: #FFFFFF; + opacity: 0.9; } + .hero.is-warning .tabs a:hover { + opacity: 1; } + .hero.is-warning .tabs li.is-active a { + opacity: 1; } + .hero.is-warning .tabs.is-boxed a, .hero.is-warning .tabs.is-toggle a { + color: #FFFFFF; } + .hero.is-warning .tabs.is-boxed a:hover, .hero.is-warning .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-warning .tabs.is-boxed li.is-active a, .hero.is-warning .tabs.is-boxed li.is-active a:hover, .hero.is-warning .tabs.is-toggle li.is-active a, .hero.is-warning .tabs.is-toggle li.is-active a:hover { + background-color: #FFFFFF; + border-color: #FFFFFF; + color: #ffdd57; } + .hero.is-warning.is-bold { + background-image: linear-gradient(141deg, #ffaf24 0%, #ffdd57 71%, #fffa70 100%); } + @media screen and (max-width: 768px) { + .hero.is-warning.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #ffaf24 0%, #ffdd57 71%, #fffa70 100%); } } + .hero.is-danger { + background-color: #ff3860; + color: #fff; } + .hero.is-danger a:not(.button), + .hero.is-danger strong { + color: inherit; } + .hero.is-danger .title { + color: #fff; } + .hero.is-danger .subtitle { + color: rgba(255, 255, 255, 0.9); } + .hero.is-danger .subtitle a:not(.button), + .hero.is-danger .subtitle strong { + color: #fff; } + @media screen and (max-width: 1023px) { + .hero.is-danger .navbar-menu { + background-color: #ff3860; } } + .hero.is-danger .navbar-item, + .hero.is-danger .navbar-link { + color: rgba(255, 255, 255, 0.7); } + .hero.is-danger a.navbar-item:hover, .hero.is-danger a.navbar-item.is-active, + .hero.is-danger .navbar-link:hover, + .hero.is-danger .navbar-link.is-active { + background-color: #ff1f4b; + color: #fff; } + .hero.is-danger .tabs a { + color: #fff; + opacity: 0.9; } + .hero.is-danger .tabs a:hover { + opacity: 1; } + .hero.is-danger .tabs li.is-active a { + opacity: 1; } + .hero.is-danger .tabs.is-boxed a, .hero.is-danger .tabs.is-toggle a { + color: #fff; } + .hero.is-danger .tabs.is-boxed a:hover, .hero.is-danger .tabs.is-toggle a:hover { + background-color: rgba(10, 10, 10, 0.1); } + .hero.is-danger .tabs.is-boxed li.is-active a, .hero.is-danger .tabs.is-boxed li.is-active a:hover, .hero.is-danger .tabs.is-toggle li.is-active a, .hero.is-danger .tabs.is-toggle li.is-active a:hover { + background-color: #fff; + border-color: #fff; + color: #ff3860; } + .hero.is-danger.is-bold { + background-image: linear-gradient(141deg, #ff0561 0%, #ff3860 71%, #ff5257 100%); } + @media screen and (max-width: 768px) { + .hero.is-danger.is-bold .navbar-menu { + background-image: linear-gradient(141deg, #ff0561 0%, #ff3860 71%, #ff5257 100%); } } + .hero.is-small .hero-body { + padding-bottom: 1.5rem; + padding-top: 1.5rem; } + @media screen and (min-width: 769px), print { + .hero.is-medium .hero-body { + padding-bottom: 9rem; + padding-top: 9rem; } } + @media screen and (min-width: 769px), print { + .hero.is-large .hero-body { + padding-bottom: 18rem; + padding-top: 18rem; } } + .hero.is-halfheight .hero-body, .hero.is-fullheight .hero-body { + align-items: center; + display: flex; } + .hero.is-halfheight .hero-body > .container, .hero.is-fullheight .hero-body > .container { + flex-grow: 1; + flex-shrink: 1; } + .hero.is-halfheight { + min-height: 50vh; } + .hero.is-fullheight { + min-height: 100vh; } + +.hero-video { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + overflow: hidden; } + .hero-video video { + left: 50%; + min-height: 100%; + min-width: 100%; + position: absolute; + top: 50%; + transform: translate3d(-50%, -50%, 0); } + .hero-video.is-transparent { + opacity: 0.3; } + @media screen and (max-width: 768px) { + .hero-video { + display: none; } } + +.hero-buttons { + margin-top: 1.5rem; } + @media screen and (max-width: 768px) { + .hero-buttons .button { + display: flex; } + .hero-buttons .button:not(:last-child) { + margin-bottom: 0.75rem; } } + @media screen and (min-width: 769px), print { + .hero-buttons { + display: flex; + justify-content: center; } + .hero-buttons .button:not(:last-child) { + margin-right: 1.5rem; } } + +.hero-head, +.hero-foot { + flex-grow: 0; + flex-shrink: 0; } + +.hero-body { + flex-grow: 1; + flex-shrink: 0; + padding: 3rem 1.5rem; } + +.section { + padding: 3rem 1.5rem; } + @media screen and (min-width: 1024px) { + .section.is-medium { + padding: 9rem 1.5rem; } + .section.is-large { + padding: 18rem 1.5rem; } } + +.footer { + background-color: whitesmoke; + padding: 3rem 1.5rem 6rem; } + +#sidebar { + background-color: #eee; + border-right: 1px solid #c1c1c1; + box-shadow: 0 0 20px rgba(50, 50, 50, 0.2) inset; + padding: 1.75rem; } + #sidebar .brand { + padding: 1rem 0; + text-align: center; } + +#main { + padding: 3rem; } + +.example { + margin-bottom: 1em; } + .example .highlight { + margin: 0; } + .example .path { + font-style: italic; + width: 100%; + text-align: right; } + +code { + color: #1a9f1a; + font-size: 0.875em; + font-weight: normal; } + +.content h2 { + padding-top: 1em; + border-top: 1px solid #c0c0c0; } + +h1 .anchor, h2 .anchor, h3 .anchor, h4 .anchor, h5 .anchor, h6 .anchor { + display: inline-block; + width: 0; + margin-left: -1.5rem; + margin-right: 1.5rem; + transition: all 100ms ease-in-out; + opacity: 0; } + +h1:hover .anchor, h2:hover .anchor, h3:hover .anchor, h4:hover .anchor, h5:hover .anchor, h6:hover .anchor { + opacity: 1; } + +h1:target, h2:target, h3:target, h4:target, h5:target, h6:target { + color: #C93312; } + h1:target .anchor, h2:target .anchor, h3:target .anchor, h4:target .anchor, h5:target .anchor, h6:target .anchor { + opacity: 1; + color: #C93312; } + +.footnotes p { + display: inline; } + +figure.has-border img { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.25); } diff --git a/docs/src/themes/mitmproxydocs/theme.toml b/docs/src/themes/mitmproxydocs/theme.toml new file mode 100644 index 00000000..5909676b --- /dev/null +++ b/docs/src/themes/mitmproxydocs/theme.toml @@ -0,0 +1,2 @@ +name = "mitmproxy" +description = "mitmproxy's internal theme" \ No newline at end of file diff --git a/examples b/examples new file mode 120000 index 00000000..c9b1c1d2 --- /dev/null +++ b/examples @@ -0,0 +1 @@ +scalpel/src/main/resources/python3-10/samples/ \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..f398c33c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..65dcd68d --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..6689b85b --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/run_tests.sh b/run_tests.sh new file mode 100644 index 00000000..2bcfbf3f --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,10 @@ +export _DO_NOT_IMPORT_JAVA=1 + +VERSION='3-10' +if [ $(python3 --version | grep -Eo '\.(.*)\.') = ".8." ]; then + VERSION='3-8' +fi + +PREFIX="scalpel/src/main/resources/python$VERSION" +cd $PREFIX +python3 -m unittest pyscalpel/tests/test_*.py qs/tests.py diff --git a/scalpel/build.gradle b/scalpel/build.gradle new file mode 100644 index 00000000..7c4af708 --- /dev/null +++ b/scalpel/build.gradle @@ -0,0 +1,127 @@ +import org.gradle.internal.os.OperatingSystem +import org.gradle.api.tasks.Exec +import org.gradle.api.tasks.testing.Test + +plugins { + id 'java' +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +group 'lexfo' +version '1.0.0' + +repositories { + mavenLocal() + mavenCentral() + jcenter() +} + + + +dependencies { + implementation 'net.portswigger.burp.extensions:montoya-api:2023.10.1' + + // https://mvnrepository.com/artifact/black.ninia/jep + implementation 'black.ninia:jep:4.2.0' + implementation 'commons-io:commons-io:2.6' + // https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation 'com.intellij:forms_rt:7.0.3' + + implementation 'org.jetbrains.jediterm:jediterm-pty:2.42' + implementation 'com.google.guava:guava:31.0.1-jre' + implementation 'log4j:log4j:1.2.17' + implementation 'org.jetbrains.pty4j:pty4j:0.12.11' + + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0-rc2' + + // Hex editor + // https://bined.exbin.org/library/ + // https://mvnrepository.com/artifact/org.exbin.bined/bined-swing + implementation 'org.exbin.bined:bined-swing:0.2.0' + implementation 'org.exbin.auxiliary:paged_data:0.2.0' +} + +jar { + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + exclude '__pycache__' + exclude '**/*.pyc' // Exclude python cache + exclude '**/*.html' + duplicatesStrategy = 'exclude' +} + +tasks.withType(Copy).all { duplicatesStrategy 'exclude' } + +configurations { + javadocImplementation.extendsFrom implementation +} + +javadoc { + source = sourceSets.main.allJava + classpath += configurations.javadocImplementation + options.links( + "https://portswigger.github.io/burp-extensions-montoya-api/javadoc/", + "https://bined.exbin.org/library/javadoc/bined-core-0.2.0/", + "https://bined.exbin.org/library/javadoc/paged_data-0.2.0/", + "https://bined.exbin.org/library/javadoc/bined-swing-0.2.0/", + "https://ninia.github.io/jep/javadoc/4.2/" + ) + options.memberLevel = JavadocMemberLevel.PRIVATE + options.addStringOption('Xdoclint:none', '-quiet') +} + + +task deletePythonResources(type: Delete) { + delete 'src/main/resources/python3-8' +} + +task transpile_to_python_3_8(type: Exec) { + dependsOn deletePythonResources + workingDir "${projectDir}" + + // Execute the Python script to transpile the code + commandLine 'python3', '../transpile_tools/3.10_to_3.8.py', '--exclude', "internal_mitmproxy", 'src/main/resources/python3-10', 'src/main/resources/python3-8' +} + +// Specify that the build task should run after the transpile_to_python_3_8 task +tasks.named('build') { + dependsOn transpile_to_python_3_8 +} + +// Add the python 3.8 compatibility files to the jar +build.dependsOn transpile_to_python_3_8 + + +// Function to create Python test tasks +void createPythonTestTask(String taskName, String workingDirPath) { + task "${taskName}"(type: Exec) { + mustRunAfter transpile_to_python_3_8 + + // Disable jep imports + environment '_DO_NOT_IMPORT_JAVA', '1' + workingDir "${projectDir}/${workingDirPath}" + + if (OperatingSystem.current().isWindows()) { + commandLine 'cmd', '/c', 'set "_DO_NOT_IMPORT_JAVA=1" && python3 -m unittest pyscalpel/tests/test_*.py qs/tests.py' + } else { + commandLine 'sh', '-c', 'export _DO_NOT_IMPORT_JAVA=1; python3 -m unittest pyscalpel/tests/test_*.py qs/tests.py' + } + } +} + +createPythonTestTask('runPythonTestsBeforeTranspiling', 'src/main/resources/python3-10') +createPythonTestTask('runPythonTestsAfterTranspiling', 'src/main/resources/python3-8') + +task runPythonTests { + dependsOn runPythonTestsBeforeTranspiling, runPythonTestsAfterTranspiling +} + + +build.mustRunAfter test diff --git a/scalpel/src/main/java/lexfo/scalpel/Async.java b/scalpel/src/main/java/lexfo/scalpel/Async.java new file mode 100644 index 00000000..7c96c8f5 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Async.java @@ -0,0 +1,14 @@ +package lexfo.scalpel; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class Async { + + private static final Executor executor = Executors.newFixedThreadPool(10); + + public static CompletableFuture run(Runnable runnable) { + return CompletableFuture.runAsync(runnable, executor); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/CommandChecker.java b/scalpel/src/main/java/lexfo/scalpel/CommandChecker.java new file mode 100644 index 00000000..c4ceb524 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/CommandChecker.java @@ -0,0 +1,57 @@ +package lexfo.scalpel; + +import java.io.File; + +/** + * Provides utilities to get default commands. + */ +public class CommandChecker { + + public static String getAvailableCommand(String... commands) { + for (int i = 0; i < commands.length - 1; i++) { + try { + final String cmd = commands[i]; + if (cmd == null) { + continue; + } + + final String binary = extractBinary(cmd); + if (isCommandAvailable(binary)) { + return cmd; + } + } catch (Throwable e) { + ScalpelLogger.logStackTrace(e); + } + } + // If no command matched, return the last one + return commands[commands.length - 1]; + } + + private static String extractBinary(String command) { + final int spaceIndex = command.indexOf(' '); + if (spaceIndex != -1) { + return command.substring(0, spaceIndex); + } + return command; + } + + private static boolean isCommandAvailable(String command) { + // Check if the command is an absolute path + final File commandFile = new File(command); + if (commandFile.isAbsolute()) { + return commandFile.exists() && commandFile.canExecute(); + } + + // Check in each directory listed in PATH + final String path = System.getenv("PATH"); + if (path != null) { + for (final String dir : path.split(File.pathSeparator)) { + final File file = new File(dir, command); + if (file.exists() && file.canExecute()) { + return true; + } + } + } + return false; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Config.java b/scalpel/src/main/java/lexfo/scalpel/Config.java new file mode 100644 index 00000000..687a23de --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Config.java @@ -0,0 +1,705 @@ +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.persistence.PersistedObject; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.jediterm.terminal.ui.UIUtil; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import lexfo.scalpel.ScalpelLogger.Level; + +/** + * Scalpel configuration. + * + * + * By default, the project configuration file is located in the $HOME/.scalpel directory. + * + * The file name is the project id with the .json extension. + * The project ID is an UUID stored in the extension data: + * https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/persistence/Persistence.html#extensionData() + * + * The configuration file looks something like this: + * { + * "workspacePaths": [ + * "/path/to/workspace1", + * "/path/to/workspace2" + * ], + * "scriptPath": "/path/to/script.py", + * } + * + * The file is not really designed to be directly edited by the user, but rather by the extension itself. + * + * A configuration file is needed because we need to store global persistent data arrays. (e.g. workspacePaths) + * Which can't be done with the Java Preferences API. + * Furthermore, it's simply more convenient to store as JSON and we already have a scalpel directory to store 'ad-hoc' python workspaces. + */ +public class Config { + + /** + * Global configuration. + * + * This is the configuration that is shared between all projects. + * It contains the list of venvs and the default values. + * + * The default values are inferred from the user behavior. + * For a new project, the default venv, script and framework paths are the last ones selected by the user in any different project. + * If the user has never selected a venv, script or framework, it is set to default values. + */ + public static class _GlobalData { + + /** + * List of registered venv paths. + */ + public ArrayList workspacePaths = new ArrayList<>(); + public String defaultWorkspacePath = ""; + public String defaultScriptPath = ""; + public String jdkPath = null; + public String logLevel = "INFO"; + public String openScriptCommand = Constants.DEFAULT_OPEN_FILE_CMD; + public String editScriptCommand = Constants.DEFAULT_TERM_EDIT_CMD; + public String openFolderCommand = Constants.DEFAULT_OPEN_DIR_CMD; + public boolean enabled = true; + } + + // Persistent data for a specific project. + public static class _ProjectData { + + /* + * The venv to run the script in. + */ + public String workspacePath = ""; + + /* + * The script to run. + */ + public String userScriptPath = ""; + + public String displayProxyErrorPopup = "True"; + } + + private final _GlobalData globalConfig; + private final _ProjectData projectConfig; + private long lastModified = System.currentTimeMillis(); + + // Scalpel configuration file extension + private static final String CONFIG_EXT = ".json"; + + // UUID generated to identify the project (because the project name cannot be fetched) + // This is stored in the extension data which is specific to the project. + public final String projectID; + + // Path to the project configuration file + private final File projectScalpelConfig; + + // Prefix for the extension data keys + private static final String DATA_PREFIX = "scalpel."; + + // Key for the project ID + private static final String DATA_PROJECT_ID_KEY = DATA_PREFIX + "projectID"; + + private Path _jdkPath = null; + + private static Config instance = null; + + private final MontoyaApi API; + + private Config(final MontoyaApi API) throws IOException { + instance = this; + + this.API = API; + + final PersistedObject extensionData = API.persistence().extensionData(); + this.projectID = + Optional + .ofNullable(extensionData.getString(DATA_PROJECT_ID_KEY)) + .orElseGet(() -> { + String id = UUID.randomUUID().toString(); + extensionData.setString(DATA_PROJECT_ID_KEY, id); + return id; + }); + + // Set the path to the project configuration file + this.projectScalpelConfig = + RessourcesUnpacker.DATA_DIR_PATH + .resolve(projectID + CONFIG_EXT) + .toFile(); + + this.globalConfig = initGlobalConfig(); + this._jdkPath = Path.of(this.globalConfig.jdkPath); + + // Load project config or create a new one on failure. (e.g. file doesn't exist) + this.projectConfig = this.initProjectConfig(); + saveAllConfig(); + } + + /** + * Provides access to the singleton instance of the Config class. + * + * @return The single instance of the Config class. + */ + private static synchronized Config getInstance( + final Optional optAPI + ) { + if (instance == null) { + try { + final MontoyaApi api = optAPI.orElseThrow(() -> + new RuntimeException( + "Config was not initialized with the MontoyaAPI" + ) + ); + + instance = new Config(api); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return instance; + } + + /** + * Provides access to the singleton instance of the Config class. + * The config must already be initialized to use this. + * + * @return The single instance of the Config class. + */ + public static synchronized Config getInstance() { + return getInstance(Optional.empty()); + } + + /** + * Provides access to the singleton instance of the Config class. + * + * @return The single instance of the Config class. + */ + public static synchronized Config getInstance(final MontoyaApi API) { + return getInstance(Optional.of(API)); + } + + private static T readConfigFile(File file, Class clazz) { + return IO.readJSON( + file, + clazz, + e -> + ScalpelLogger.logStackTrace( + "/!\\ Invalid JSON config file /!\\" + + ", try re-installing Scalpel by removing ~/.scalpel and restarting Burp.", + e + ) + ); + } + + private _GlobalData initGlobalConfig() throws IOException { + // Load global config file + final File globalConfigFile = getGlobalConfigFile(); + + // If config file does not exist, return default global data + if (!globalConfigFile.exists()) { + return getDefaultGlobalData(); + } + + // Read existing configuration + final _GlobalData globalData = ConfigUtil.readConfigFile( + globalConfigFile, + _GlobalData.class + ); + + // Remove workspace paths that do not exist anymore + globalData.workspacePaths.removeIf(path -> !new File(path).exists()); + + // Set JDK path if it's not set + if (globalData.jdkPath == null) { + globalData.jdkPath = findJdkPath().toString(); + } + + // Ensure there is at least one workspace path + if (globalData.workspacePaths.isEmpty()) { + globalData.workspacePaths.add( + Workspace + .getOrCreateDefaultWorkspace(Path.of(globalData.jdkPath)) + .toString() + ); + } + + // Set the default workspace path + if ( + globalData.defaultWorkspacePath == null || + !new File(globalData.defaultWorkspacePath).exists() + ) { + globalData.defaultWorkspacePath = globalData.workspacePaths.get(0); + } + + // Set log level + API + .persistence() + .preferences() + .setString("logLevel", globalData.logLevel); + ScalpelLogger.setLogLevel(globalData.logLevel); + + return globalData; + } + + private _ProjectData initProjectConfig() { + return Optional + .of(projectScalpelConfig) + .filter(File::exists) + .map(file -> ConfigUtil.readConfigFile(file, _ProjectData.class)) + .map(d -> { + d.workspacePath = + Optional + .ofNullable(d.workspacePath) // Ensure the venv path is set. + .filter(p -> globalConfig.workspacePaths.contains(p)) // Ensure the selected venv is registered. + .orElse(globalConfig.defaultWorkspacePath); // Otherwise, use the default venv. + return d; + }) + .orElseGet(this::getDefaultProjectData); + } + + /** + * Write the global configuration to the global configuration file. + */ + private synchronized void saveGlobalConfig() { + IO.writeJSON(getGlobalConfigFile(), globalConfig); + } + + /** + * Write the project configuration to the project configuration file. + */ + private synchronized void saveProjectConfig() { + this.lastModified = System.currentTimeMillis(); + IO.writeJSON(projectScalpelConfig, projectConfig); + } + + /** + * Write the global and project configuration to their respective files. + */ + private synchronized void saveAllConfig() { + saveGlobalConfig(); + saveProjectConfig(); + } + + /** + * Get the last modification time of the project configuration file. + * + * This is used to reload the execution configuration when the project configuration file is modified. + * @return The last modification time of the project configuration file. + */ + public long getLastModified() { + return lastModified; + } + + /** + * Get the global configuration file. + * + * @return The global configuration file. (default: $HOME/.scalpel/global.json) + */ + public static File getGlobalConfigFile() { + return RessourcesUnpacker.DATA_DIR_PATH + .resolve("global" + CONFIG_EXT) + .toFile(); + } + + private static boolean hasIncludeDir(Path jdkPath) { + File inc = jdkPath.resolve("include").toFile(); + return inc.exists() && inc.isDirectory(); + } + + private static Optional guessJdkPath() throws IOException { + if (UIUtil.isWindows) { + // Official JDK usually gets installed in 'C:\\Program Files\\Java\\jdk-' + final Path winJdkPath = Path.of("C:\\Program Files\\Java\\"); + try (Stream files = Files.walk(winJdkPath)) { + return files + .filter(f -> f.toFile().getName().contains("jdk")) + .map(Path::toAbsolutePath) + .filter(Config::hasIncludeDir) + .findFirst(); + } catch (NoSuchFileException e) { + ScalpelLogger.warn( + "Could not find JDK in common Windows location (" + + winJdkPath + + "), prompting the user to select it instead.." + ); + return Optional.empty(); + } + } + + if (UIUtil.isMac) { + // Get output of /usr/libexec/java_home + final String javaHomeCommand = "/usr/libexec/java_home"; + try { + final Process process = Runtime + .getRuntime() + .exec(javaHomeCommand); + + try ( + final BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()) + ) + ) { + final String jdkPathStr = reader.readLine(); + if (jdkPathStr != null && !jdkPathStr.isBlank()) { + final Path macJdkPath = Path.of(jdkPathStr); + if (hasIncludeDir(macJdkPath)) { + return Optional.of(macJdkPath); + } + } + } + } catch (IOException e) { + ScalpelLogger.error( + "Could not execute " + javaHomeCommand + " :" + ); + ScalpelLogger.logStackTrace(e); + } + } + + // We try to find the JDK from the javac binary path + final String binaryName = "javac"; + final Stream matchingBinaries = findBinaryInPath(binaryName); + final Stream potentialJdkPaths = matchingBinaries + .map(binaryPath -> { + try { + final Path absolutePath = binaryPath.toRealPath(); + return absolutePath.getParent().getParent(); + } catch (IOException e) { + return null; + } + }) + .filter(Objects::nonNull); + + // Some distributions (e.g. Kali) come with an incomplete JDK and requires installing a package for the complete one. + // This filter prevents selecting those. + final Stream validJavaHomes = potentialJdkPaths.filter( + Config::hasIncludeDir + ); + + return validJavaHomes.findFirst(); + } + + /** + * Tries to get the JDK path from PATH, usual install locations, or by prompting the user. + * @return The JDK path. + * @throws IOException + */ + public Path findJdkPath() throws IOException { + if (_jdkPath != null) { + // Return memoized path + return _jdkPath; + } + + final Path javaHome = guessJdkPath() + .orElseGet(() -> { + // Display popup telling the user that JDK was not found and needs to select it manually + JOptionPane.showMessageDialog( + null, + "JDK not found. Please select JDK path manually.", + "JDK not found", + JOptionPane.INFORMATION_MESSAGE + ); + + // Include a filechooser to choose the path + final JFileChooser fileChooser = new JFileChooser(); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + final int option = fileChooser.showOpenDialog(null); + if (option == JFileChooser.APPROVE_OPTION) { + final File file = fileChooser.getSelectedFile(); + return file.toPath(); + } + return null; + }); + + if (javaHome == null) { + throw new RuntimeException( + "No JDK was found nor manually selected, please install a JDK in a common place" + ); + } + // Memoize path + _jdkPath = javaHome; + + return javaHome; + } + + private static Stream findBinaryInPath(String binaryName) { + final String systemPath = System.getenv("PATH"); + final String[] pathDirs = systemPath.split( + System.getProperty("path.separator") + ); + + return Arrays + .stream(pathDirs) + .map(pathDir -> Paths.get(pathDir, binaryName)) + .filter(Files::exists) + .map(path -> { + try { + return path.toRealPath(); + } catch (IOException e) { + return path; + } + }); + } + + /** + * Get the global configuration. + * + * @return The global configuration. + */ + private _GlobalData getDefaultGlobalData() throws IOException { + final _GlobalData data = new _GlobalData(); + + data.jdkPath = IO.ioWrap(this::findJdkPath).toString(); + + data.defaultScriptPath = + RessourcesUnpacker.DEFAULT_SCRIPT_PATH.toString(); + + RessourcesUnpacker.FRAMEWORK_PATH.toString(); + + data.workspacePaths = new ArrayList<>(); + + data.workspacePaths.add( + Workspace + .getOrCreateDefaultWorkspace(Path.of(data.jdkPath)) + .toString() + ); + + data.defaultWorkspacePath = data.workspacePaths.get(0); + return data; + } + + /** + * Get the project configuration. + * + * @return The project configuration. + */ + private _ProjectData getDefaultProjectData() { + final _ProjectData data = new _ProjectData(); + + data.userScriptPath = globalConfig.defaultScriptPath; + data.workspacePath = globalConfig.defaultWorkspacePath; + return data; + } + + // Getters + + /* + * Get the venv paths list. + * + * @return The venv paths list. + */ + public String[] getVenvPaths() { + return globalConfig.workspacePaths.toArray(new String[0]); + } + + /* + * Get the selected user script path. + * + * @return The selected user script path. + */ + public Path getUserScriptPath() { + return Path.of(projectConfig.userScriptPath); + } + + /* + * Get the selected framework path. + * + * @return The selected framework path. + */ + public Path getFrameworkPath() { + return RessourcesUnpacker.FRAMEWORK_PATH; + } + + public Path getJdkPath() { + return Path.of(globalConfig.jdkPath); + } + + /* + * Get the selected venv path. + * + * @return The selected venv path. + */ + public Path getSelectedWorkspacePath() { + return Path.of(projectConfig.workspacePath); + } + + // Setters + + public void setJdkPath(Path path) { + this.globalConfig.jdkPath = path.toString(); + this.saveGlobalConfig(); + } + + /* + * Set the venv paths list. + * Saves the new list to the global configuration file. + * + * @param venvPaths The new venv paths list. + */ + public void setVenvPaths(ArrayList venvPaths) { + this.globalConfig.workspacePaths = venvPaths; + this.saveGlobalConfig(); + } + + /* + * Set the selected user script path. + * Saves the new path to the global and project configuration files. + * + * @param scriptPath The new user script path. + */ + public void setUserScriptPath(Path scriptPath) { + this.projectConfig.userScriptPath = scriptPath.toString(); + this.globalConfig.defaultScriptPath = scriptPath.toString(); + this.saveAllConfig(); + } + + /* + * Set the selected venv path. + * Saves the new path to the global and project configuration files. + * + * @param venvPath The new venv path. + */ + public void setSelectedVenvPath(Path venvPath) { + this.projectConfig.workspacePath = venvPath.toString(); + this.globalConfig.defaultWorkspacePath = venvPath.toString(); + this.saveAllConfig(); + } + + // Methods + + /* + * Add a venv path to the list. + * Saves the new list to the global configuration file. + * + * @param venvPath The venv path to add. + */ + public void addVenvPath(Path venvPath) { + globalConfig.workspacePaths.add(venvPath.toString()); + this.saveGlobalConfig(); + } + + /* + * Remove a venv path from the list. + * Saves the new list to the global configuration file. + * + * @param venvPath The venv path to remove. + */ + public void removeVenvPath(Path venvPath) { + globalConfig.workspacePaths.remove(venvPath.toString()); + this.saveGlobalConfig(); + } + + public String getLogLevel() { + return globalConfig.logLevel; + } + + public void setLogLevel(String logLevel) { + API.persistence().preferences().setString("logLevel", logLevel); + ScalpelLogger.setLogLevel(Level.nameToLevel.get(logLevel)); + + this.globalConfig.logLevel = logLevel; + saveGlobalConfig(); + } + + public String getOpenScriptCommand() { + return globalConfig.openScriptCommand; + } + + public void setOpenScriptCommand(String openScriptCommand) { + this.globalConfig.openScriptCommand = openScriptCommand; + saveGlobalConfig(); + } + + public String getEditScriptCommand() { + return globalConfig.editScriptCommand; + } + + public void setEditScriptCommand(String editScriptCommand) { + this.globalConfig.editScriptCommand = editScriptCommand; + saveGlobalConfig(); + } + + public String getOpenFolderCommand() { + return globalConfig.openFolderCommand; + } + + public void setOpenFolderCommand(String openFolderCommand) { + this.globalConfig.openFolderCommand = openFolderCommand; + saveGlobalConfig(); + } + + /* + * Get the display proxy error popup status. (enabled/disabled) + * + * @return The current status of proxy error popup display. + */ + public String getDisplayProxyErrorPopup() { + return projectConfig.displayProxyErrorPopup; + } + + /* + * Set the display proxy error popup status. + * Saves the new status to the project configuration file. + * + * @param displayProxyErrorPopup The new status for displaying proxy error popups. + */ + public void setDisplayProxyErrorPopup(String displayProxyErrorPopup) { + this.projectConfig.displayProxyErrorPopup = displayProxyErrorPopup; + saveProjectConfig(); + } + + /* + * Get the enabled status. + * + * @return The current enabled status. + */ + public boolean isEnabled() { + return globalConfig.enabled; + } + + /* + * Set the enabled status. + * Saves the new status to the global configuration file. + * + * @param enabled The new enabled status. + */ + public void setEnabled(boolean enabled) { + this.globalConfig.enabled = enabled; + saveGlobalConfig(); + } + + public String dumpConfig() { + // Dump whole config as string for debugging + final ObjectWriter writer = new ObjectMapper() + .writerWithDefaultPrettyPrinter(); + + try { + String globalConfigJSON = writer.writeValueAsString(globalConfig); + String projectConfigJSON = writer.writeValueAsString(projectConfig); + + return ( + "Global config: \n" + + globalConfigJSON + + "\n------------------------\n" + + "Project config: \n" + + projectConfigJSON + ); + } catch (JsonProcessingException e) { + ScalpelLogger.logStackTrace(e); + } + return ""; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ConfigTab.form b/scalpel/src/main/java/lexfo/scalpel/ConfigTab.form new file mode 100644 index 00000000..ab7d2adf --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ConfigTab.form @@ -0,0 +1,719 @@ + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    diff --git a/scalpel/src/main/java/lexfo/scalpel/ConfigTab.java b/scalpel/src/main/java/lexfo/scalpel/ConfigTab.java new file mode 100644 index 00000000..81fb3c04 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ConfigTab.java @@ -0,0 +1,2306 @@ +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.Theme; +import com.intellij.uiDesigner.core.GridConstraints; +import com.intellij.uiDesigner.core.GridLayoutManager; +import com.intellij.uiDesigner.core.Spacer; +import com.jediterm.terminal.TtyConnector; +import com.jediterm.terminal.ui.JediTermWidget; +import com.jgoodies.forms.layout.CellConstraints; +import com.jgoodies.forms.layout.FormLayout; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Supplier; +import javax.swing.*; +import javax.swing.border.TitledBorder; +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.DefaultTableModel; +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import lexfo.scalpel.ScalpelLogger.Level; +import lexfo.scalpel.Venv.PackageInfo; +import lexfo.scalpel.components.PlaceholderTextField; +import lexfo.scalpel.components.SettingsPanel; +import lexfo.scalpel.components.WorkingPopup; + +/** + * Burp tab handling Scalpel configuration + * IntelliJ's GUI designer is needed to edit most components. + */ +public class ConfigTab extends JFrame { + + private JPanel rootPanel; + private JButton frameworkBrowseButton; + private JTextField frameworkPathField; + private JPanel browsePanel; + private JPanel frameworkConfigPanel; + private JTextArea frameworkPathTextArea; + private JPanel scriptConfigPanel; + private JButton scriptBrowseButton; + private JLabel scriptPathTextArea; + private JediTermWidget terminalForVenvConfig; + private JList venvListComponent; + private JTable packagesTable; + private PlaceholderTextField addVentText = new PlaceholderTextField( + "Enter a virtualenv path or name here to import or create one." + ); + private JButton addVenvButton; + private JPanel venvSelectPanel; + private JButton openScriptButton; + private JButton createButton; + private JList venvScriptList; + private JPanel listPannel; + private JButton openFolderButton; + private JButton scalpelIsENABLEDButton; + private JTextArea stderrTextArea; + private JTextArea stdoutTextArea; + private JTextPane helpTextPane; + private JPanel outputTabPanel; + private JScrollPane stdoutScrollPane; + private JScrollPane stderrScrollPane; + private JTextPane debugInfoTextPane; + private JLabel selectedScriptLabel; + private JButton resetTerminalButton; + private JButton copyToClipboardButton; + private JPanel settingsTab; + private JButton openIssueOnGitHubButton; + private final transient ScalpelExecutor scalpelExecutor; + private final transient Config config; + private final transient MontoyaApi API; + private final Theme theme; + private final Frame burpFrame; + private final SettingsPanel settingsPanel = new SettingsPanel(); + + private static ConfigTab instance = null; + + public static ConfigTab getInstance() { + if (instance == null) { + throw new IllegalStateException("ConfigTab was never initialized."); + } + return instance; + } + + public ConfigTab( + MontoyaApi API, + ScalpelExecutor executor, + Config config, + Theme theme + ) { + // Set the singleton instance, throw an exception if it's already set. + if (instance != null) { + throw new IllegalStateException( + "More than one instance of ConfigTab is not allowed." + ); + } + + this.config = config; + this.scalpelExecutor = executor; + this.theme = theme; + this.API = API; + this.burpFrame = API.userInterface().swingUtils().suiteFrame(); + + $$$setupUI$$$(); + + instance = this; + setupVenvTab(); + setupHelpTab(); + setupLogsTab(); + setupDebugInfoTab(); + setupSettingsTab(); + } + + private void setupVenvTab() { + // Open file browser to select the script to execute. + scriptBrowseButton.addActionListener(e -> + handleBrowseButtonClick( + () -> RessourcesUnpacker.DEFAULT_SCRIPT_PATH, + this::setAndStoreScript + ) + ); + + // Fill the venv list component. + venvListComponent.setListData(config.getVenvPaths()); + + // Update the displayed packages + updatePackagesTable(); + updateScriptList(); + + // Change the venv, terminal and package table when the user selects a venv. + venvListComponent.addListSelectionListener( + this::handleVenvListSelectionEvent + ); + + addListDoubleClickListener( + venvListComponent, + this::handleVenvListSelectionEvent + ); + + // Add a new venv when the user clicks the button. + addVenvButton.addActionListener(__ -> handleVenvButton()); + + // Add a new venv when the user presses enter in the text field. + addVentText.addActionListener(__ -> handleVenvButton()); + + openScriptButton.addActionListener(__ -> handleOpenScriptButton()); + + createButton.addActionListener(__ -> handleNewScriptButton()); + + openFolderButton.addActionListener(__ -> handleOpenScriptFolderButton() + ); + + this.scalpelIsENABLEDButton.addActionListener(__ -> handleEnableButton() + ); + + if (!config.isEnabled()) { + this.scalpelIsENABLEDButton.setText("Scalpel is DISABLED"); + } + + // Implement the terminal reset button + resetTerminalButton.addActionListener(__ -> { + final String selectedVenvPath = config + .getSelectedWorkspacePath() + .toString(); + updateTerminal(selectedVenvPath); + }); + + venvScriptList.addListSelectionListener(e -> { + if (!e.getValueIsAdjusting()) { + this.handleScriptListSelectionEvent(); + } + }); + } + + private void setupHelpTab() { + // Make HTML links clickable in the help text pane. + helpTextPane.addHyperlinkListener(e -> { + if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) { + try { + Desktop.getDesktop().browse(e.getURL().toURI()); // Open link in the default browser. + } catch (IOException | URISyntaxException ex) { + ex.printStackTrace(); + } + } + }); + } + + private void setupLogsTab() { + // For some reason IntelliJ's GUI designer doesn't let you set a simple fixed + // GridLayout. + // So we do it manually here. + outputTabPanel.setLayout(new GridLayout(1, 2)); // 1 row, 2 columns + + // Adjusting autoScroll for stdoutTextArea + UIUtils.setupAutoScroll(stdoutScrollPane, stdoutTextArea); + // Adjusting autoScroll for stderrTextArea + UIUtils.setupAutoScroll(stderrScrollPane, stderrTextArea); + + selectedScriptLabel.setText( + config.getUserScriptPath().getFileName().toString() + ); + } + + private void setupCopyButton() { + // Implement the copy to clipboard button + copyToClipboardButton.addActionListener(__ -> { + final String text = debugInfoTextPane.getText(); + final Clipboard clipboard = Toolkit + .getDefaultToolkit() + .getSystemClipboard(); + + clipboard.setContents(new StringSelection(text), null); + + // Change the button text to "Copied!" for 1s + copyToClipboardButton.setText("Copied!"); + Async.run(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + ScalpelLogger.logStackTrace(e); + } + copyToClipboardButton.setText("Copy to clipboard"); + }); + + // Focus the debug info text + debugInfoTextPane.requestFocus(); + + // Select the whole debug text for visual feedback + debugInfoTextPane.selectAll(); + }); + } + + public void setSettings(Map settings) { + this.settingsPanel.setSettingsValues(settings); + } + + private void setupSettingsTab() { + this.settingsPanel.addDropdownSetting( + "logLevel", + "Log level", + Level.names, + config.getLogLevel() + ); + + this.settingsPanel.addTextFieldSetting( + "openScriptCommand", + "\"Open script\" button command", + config.getOpenScriptCommand() + ); + + this.settingsPanel.addTextFieldSetting( + "editScriptCommand", + "Edit script in terminal command", + config.getEditScriptCommand() + ); + + this.settingsPanel.addTextFieldSetting( + "openFolderCommand", + "\"Open folder\" button command", + config.getOpenFolderCommand() + ); + + this.settingsPanel.addCheckboxSetting( + "displayProxyErrorPopup", + "Display error popup", + "True".equals(config.getDisplayProxyErrorPopup()) + ); + + // Padding + this.settingsPanel.addInformationText(""); + this.settingsPanel.addInformationText("Available placeholders:"); + this.settingsPanel.addInformationText( + " - {file}: The absolute path to the selected script" + ); + this.settingsPanel.addInformationText( + " - {dir}: The absolute path to the selected script containing directory" + ); + + this.settingsPanel.addListener(settings -> { + ScalpelLogger.info("Settings changed: " + settings); + final String logLevel = settings.get("logLevel"); + ScalpelLogger.setLogLevel(logLevel); + config.setLogLevel(logLevel); + config.setOpenScriptCommand(settings.get("openScriptCommand")); + config.setEditScriptCommand(settings.get("editScriptCommand")); + config.setOpenFolderCommand(settings.get("openFolderCommand")); + config.setDisplayProxyErrorPopup( + settings.get("displayProxyErrorPopup") + ); + }); + + this.settingsTab.add(this.settingsPanel, BorderLayout.CENTER); + } + + private void setupGitHubIssueButton() { + openIssueOnGitHubButton.addActionListener(__ -> { + try { + final String title = URLEncoder.encode( + "Failed to install Scalpel", + "UTF-8" + ); + final String text = this.debugInfoTextPane.getText(); + + // Truncate at 6500 chars + final Integer maxSize = 6000; + + final String truncatedText = text.length() < maxSize + ? text + : "/!\\ The debug report is too long to be fully included in the URL" + + ", please copy it manually from the \"Debug Info\" tab /!\\\n" + + text.substring(0, maxSize) + + "\n[TRUNCATED]"; + final String body = URLEncoder.encode( + "Please describe the issue here\n\n# Debug report\n```\n" + + truncatedText + + "\n\n```", + "UTF-8" + ); + final String URL = + "https://github.com/ambionics/scalpel/issues/new?title=" + + title + + "&body=" + + body; + + Desktop.getDesktop().browse(new URI(URL)); + } catch (IOException | URISyntaxException e) { + ScalpelLogger.logStackTrace(e); + } + }); + } + + private void setupDebugInfoTab() { + setupCopyButton(); + setupGitHubIssueButton(); + + final String timestamp = new SimpleDateFormat("dd-MM-yyyy HH:mm") + .format(new Date()); + + final String configDump = config.dumpConfig(); + final String pythonVersion = PythonSetup.executePythonCommand( + "import sys; print('.'.join(map(str, sys.version_info[:3])))" + ); + final Path jdkPath = config.getJdkPath(); + + final StringBuilder out = new StringBuilder(); + out + .append("Debug report generated on ") + .append(timestamp) + .append("\n\n"); + + final String[] properties = { + "os.name", + "os.version", + "os.arch", + "java.version", + "user.name", + "user.home", + "user.dir", + }; + for (final String property : properties) { + out + .append(property) + .append(": ") + .append(System.getProperty(property)) + .append("\n"); + } + + out.append("Python version: ").append(pythonVersion).append("\n"); + out.append("JDK path: ").append(jdkPath).append("\n"); + + out + .append("\n------------------------\n") + .append(configDump) + .append("\n------------------------\n"); + out.append("Installed packages in venv:\n"); + + final PackageInfo[] installedPackages; + try { + installedPackages = + Venv.getInstalledPackages(config.getSelectedWorkspacePath()); + + for (PackageInfo packageInfo : installedPackages) { + out.append( + packageInfo.name + " : " + packageInfo.version + "\n" + ); + } + } catch (IOException e) { + out.append( + ScalpelLogger.exceptionToErrorMsg( + e, + "Failed to get installed packages" + ) + ); + } + + final File selectedScript = config.getUserScriptPath().toFile(); + try { + final String content = new String( + Files.readAllBytes(selectedScript.toPath()) + ); + out.append("\n---- Script content -----\n"); + out.append(content); + out.append("\n------------------------\n"); + } catch (IOException e) { + out.append( + ScalpelLogger.exceptionToErrorMsg( + e, + "Failed to read script file" + ) + ); + } + + out.append("---- Full debug log ----\n\n"); + this.debugInfoTextPane.setText(out.toString()); + } + + public static void appendToDebugInfo(String info) { + if (instance == null) { + return; + } + final JTextPane pane = instance.debugInfoTextPane; + + // Safely append text to the JTextPane + SwingUtilities.invokeLater(() -> { + final Document doc = pane.getDocument(); + try { + // Append new info as a new line at the end of the Document + doc.insertString(doc.getLength(), "\n" + info, null); + } catch (BadLocationException e) { + ScalpelLogger.logStackTrace(e); + } + }); + } + + /** + * Push a character to a stdout or stderr text area. + * + * @param c The character to push. + * @param isStdout Whether the character is from stdout or stderr. + */ + + public static void pushCharToOutput(int c, boolean isStdout) { + if (instance == null) { + return; + } + final JTextArea textArea = isStdout + ? instance.stdoutTextArea + : instance.stderrTextArea; + textArea.append(String.valueOf((char) c)); + } + + public static void putStringToOutput(String s, boolean isStdout) { + if (instance == null) { + return; + } + final JTextArea textArea = isStdout + ? instance.stdoutTextArea + : instance.stderrTextArea; + textArea.append(s); + textArea.append("\n\n"); + } + + public static void clearOutputs(String msg) { + if (instance == null) { + return; + } + final String clearedTimestamp = new SimpleDateFormat("HH:mm:ss") + .format(new Date()); + + final String clearedMsg = + "--- CLEARED at " + + clearedTimestamp + + " ---\n" + + msg + + "\n-------------------------------------------------------------------\n\n"; + + instance.stdoutTextArea.setText(clearedMsg); + instance.stderrTextArea.setText(clearedMsg); + } + + /** + * JList doesn't natively support double click events, so we implment it + * ourselves. + * + * @param + * @param list The list to add the listener to. + * @param handler The listener handler callback. + */ + private void addListDoubleClickListener( + JList list, + Consumer handler + ) { + list.addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + if (evt.getClickCount() != 2) { + // Not a double click + return; + } + + // Get the selected list elem from the click coordinates + final int selectedIndex = list.locationToIndex( + evt.getPoint() + ); + + // Convert the MouseEvent into a corresponding ListSelectionEvent + final ListSelectionEvent passedEvent = new ListSelectionEvent( + evt.getSource(), + selectedIndex, + selectedIndex, + false + ); + + handler.accept(passedEvent); + } + } + ); + } + + private void handleOpenScriptFolderButton() { + final File script = config.getUserScriptPath().toFile(); + final File folder = script.getParentFile(); + final Map settings = settingsPanel.getSettingsValues(); + final String command = settings.get("openFolderCommand"); + final String cmd = cmdFormat( + command, + folder.getAbsolutePath(), + script.getAbsolutePath() + ); + + updateTerminal( + config.getSelectedWorkspacePath().toString(), + folder.getAbsolutePath(), + cmd + ); + } + + private void handleScriptListSelectionEvent() { + // Get the selected script name. + final Optional selected = Optional.ofNullable( + venvScriptList.getSelectedValue() + ); + + selected.ifPresent(s -> { + final Path path = config + .getSelectedWorkspacePath() + .resolve(s) + .toAbsolutePath(); + + selectScript(path); + }); + } + + private void updateScriptList() { + Async.run(() -> { + final JList list = this.venvScriptList; + final File selectedVenv = config + .getSelectedWorkspacePath() + .toFile(); + final File[] files = selectedVenv.listFiles(f -> + f.getName().endsWith(".py") + ); + + final String selected = config + .getUserScriptPath() + .getFileName() + .toString(); + + if (files != null) { + Arrays.sort( + files, + (f1, f2) -> { + // Ensure selected script comes up on top + if (selected.equalsIgnoreCase(f1.getName())) { + return -1; + } else if (selected.equalsIgnoreCase(f2.getName())) { + return 1; + } else { + return f1 + .getName() + .compareToIgnoreCase(f2.getName()); + } + } + ); + } + + final DefaultListModel listModel = new DefaultListModel<>(); + + // Fill the model with the file names + if (files != null) { + for (File file : files) { + listModel.addElement(file.getName()); + } + } + + list.setModel(listModel); + }); + } + + private void selectScript(Path path) { + // Select the script + config.setUserScriptPath(path); + + // Reload the executor + Async.run(scalpelExecutor::notifyEventLoop); + + this.selectedScriptLabel.setText(path.getFileName().toString()); + + // Display the script in the terminal. + openEditorInTerminal(path); + } + + private void handleNewScriptButton() { + final File venv = config.getSelectedWorkspacePath().toFile(); + + // Prompt the user for a name + String fileName = JOptionPane.showInputDialog( + burpFrame, + "Enter the name for the new script" + ); + + if (fileName == null || fileName.trim().isEmpty()) { + // The user didn't enter a name + JOptionPane.showMessageDialog( + burpFrame, + "You must provide a name for the file." + ); + return; + } + + // Append .py extension if it's not there + if (!fileName.endsWith(".py")) { + fileName += ".py"; + } + + // Define the source file + Path source = Path.of( + System.getProperty("user.home"), + ".scalpel", + "extracted", + "templates", + "default.py" + ); + + // Define the destination file + final Path destination = venv.toPath().resolve(fileName); + + // Copy the file + try { + Files.copy( + source, + destination, + StandardCopyOption.REPLACE_EXISTING + ); + JOptionPane.showMessageDialog( + burpFrame, + "File was successfully created!" + ); + + final Path absolutePath = destination.toAbsolutePath(); + + selectScript(absolutePath); + updateScriptList(); + } catch (IOException e) { + JOptionPane.showMessageDialog( + burpFrame, + "Error copying file: " + e.getMessage() + ); + } + } + + /** + * Opens the script in a terminal editor + *

    + * Tries to use the EDITOR env var + * Falls back to vi if EDITOR is missing + * + * @param fileToEdit + */ + private void openEditorInTerminal(Path fileToEdit) { + final Path dir = config.getSelectedWorkspacePath(); + final String cmd = cmdFormat( + config.getEditScriptCommand(), + dir, + fileToEdit + ); + + final String cwd = fileToEdit.getParent().toString(); + + this.updateTerminal( + config.getSelectedWorkspacePath().toString(), + cwd, + cmd + ); + } + + private String cmdFormat(String fmt, Object dir, Object file) { + return fmt + .replace("{dir}", Terminal.escapeshellarg(dir.toString())) + .replace("{file}", Terminal.escapeshellarg(file.toString())); + } + + private void handleOpenScriptButton() { + final Path script = config.getUserScriptPath(); + launchOpenScriptCommand(script); + } + + private void launchOpenScriptCommand(Path script) { + final Path venvDir = config.getSelectedWorkspacePath(); + final Map settings = settingsPanel.getSettingsValues(); + final String cmdFmt = settings.get("openScriptCommand"); + final String cmd = cmdFormat(cmdFmt, venvDir, script); + + updateTerminal( + config.getSelectedWorkspacePath().toString(), + script.getParent().toString(), + cmd + ); + } + + private void handleEnableButton() { + if (this.scalpelExecutor.isEnabled()) { + this.scalpelIsENABLEDButton.setText("Scalpel is DISABLED"); + this.config.setEnabled(false); + this.scalpelExecutor.disable(); + } else { + this.scalpelIsENABLEDButton.setText("Scalpel is ENABLED"); + this.config.setEnabled(true); + this.scalpelExecutor.enable(); + } + } + + private void handleVenvButton() { + final String value = addVentText.getText().trim(); + + if (value.isEmpty()) { + return; + } + + final Path path; + try { + if ((new File(value).isAbsolute())) { + // The user provided an absolute path, use it as is. + path = Path.of(value); + } else if (value.contains(File.separator)) { + // The user provided a relative path, forbid it. + throw new IllegalArgumentException( + "Venv name cannot contain " + + File.separator + + "\n" + + "Please provide a venv name or an absolute path." + ); + } else { + // The user provided a name, use it to create a venv in the default venvs dir. + path = + Paths.get( + Workspace.getWorkspacesDir().getAbsolutePath(), + value + ); + } + } catch (IllegalArgumentException e) { + JOptionPane.showMessageDialog( + burpFrame, + e.getMessage(), + "Invalid venv name or absolute path", + JOptionPane.ERROR_MESSAGE + ); + return; + } + + WorkingPopup.showBlockingWaitDialog( + "Creating venv and installing required packages...", + label -> { + // Clear the text field. + addVentText.setEditable(false); + addVentText.setText("Please wait ..."); + + // Create the venv and installed required packages. (i.e. mitmproxy) + try { + Workspace.createAndInitWorkspace( + path, + Optional.of(config.getJdkPath()), + Optional.of(terminalForVenvConfig.getTerminal()) + ); + + // Add the venv to the config. + config.addVenvPath(path); + + // Clear the text field. + addVentText.setText(""); + + // Display the venv in the list. + venvListComponent.setListData(config.getVenvPaths()); + + venvListComponent.setSelectedIndex( + config.getVenvPaths().length - 1 + ); + } catch (RuntimeException e) { + final String msg = + "Failed to create venv: \n" + e.getMessage(); + ScalpelLogger.error(msg); + ScalpelLogger.logStackTrace(e); + JOptionPane.showMessageDialog( + burpFrame, + msg, + "Failed to create venv", + JOptionPane.ERROR_MESSAGE + ); + } + addVentText.setEditable(true); + } + ); + } + + private synchronized void updateTerminal( + String selectedVenvPath, + String cwd, + String cmd + ) { + final JediTermWidget termWidget = this.terminalForVenvConfig; + final TtyConnector oldConnector = termWidget.getTtyConnector(); + + // Close asynchronously to avoid losing time. + termWidget.stop(); + // Kill the old process. + oldConnector.close(); + + final com.jediterm.terminal.Terminal term = termWidget.getTerminal(); + final int width = term.getTerminalWidth(); + final int height = term.getTerminalHeight(); + final Dimension dimension = new Dimension(width, height); + + // Start the process while the terminal is closing + final TtyConnector connector = Terminal.createTtyConnector( + selectedVenvPath, + Optional.of(dimension), + Optional.ofNullable(cwd), + Optional.ofNullable(cmd) + ); + + // Connect the terminal to the new process in the new venv. + termWidget.setTtyConnector(connector); + + term.reset(); + term.cursorPosition(0, 0); + + // Start the terminal. + termWidget.start(); + } + + private void updateTerminal(String selectedVenvPath) { + updateTerminal(selectedVenvPath, null, null); + } + + private void handleVenvListSelectionEvent(ListSelectionEvent e) { + // Ignore intermediate events. + if (e.getValueIsAdjusting()) { + return; + } + + // Get the selected venv path. + final String selectedVenvPath = venvListComponent.getSelectedValue(); + + if (selectedVenvPath == null) { + return; + } + + config.setSelectedVenvPath(Path.of(selectedVenvPath)); + + Async.run(scalpelExecutor::notifyEventLoop); + + // Update the package table. + Async.run(this::updatePackagesTable); + Async.run(() -> updateTerminal(selectedVenvPath)); + Async.run(this::updateScriptList); + } + + private CompletableFuture handleBrowseButtonClick( + Supplier getter, + Consumer setter + ) { + return Async.run(() -> { + final JFileChooser fileChooser = new JFileChooser(); + + // Allow the user to only select files. + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + + // Set default path to the path in the text field. + fileChooser.setCurrentDirectory(getter.get().toFile()); + + final int result = fileChooser.showOpenDialog(this); + + // When the user selects a file, set the text field to the selected file. + if (result == JFileChooser.APPROVE_OPTION) { + setter.accept( + fileChooser.getSelectedFile().toPath().toAbsolutePath() + ); + } + }); + } + + private CompletableFuture updatePackagesTable( + Consumer onSuccess, + Runnable onFail + ) { + return Async.run(() -> { + final PackageInfo[] installedPackages; + try { + installedPackages = + Venv.getInstalledPackages( + config.getSelectedWorkspacePath() + ); + } catch (IOException e) { + JOptionPane.showMessageDialog( + burpFrame, + "Failed to get installed packages: \n" + e.getMessage(), + "Failed to get installed packages", + JOptionPane.ERROR_MESSAGE + ); + onFail.run(); + return; + } + + // Create a table model with the appropriate column names + final DefaultTableModel tableModel = new DefaultTableModel( + new Object[] { "Package", "Version" }, + 0 + ); + + // Parse with jackson and add to the table model + Arrays + .stream(installedPackages) + .map(p -> new Object[] { p.name, p.version }) + .forEach(tableModel::addRow); + + // Set the table model + packagesTable.setModel(tableModel); + + // make the table uneditable + packagesTable.setDefaultEditor(Object.class, null); + + onSuccess.accept(packagesTable); + }); + } + + private void updatePackagesTable(Consumer onSuccess) { + updatePackagesTable(onSuccess, () -> {}); + } + + private void updatePackagesTable() { + updatePackagesTable(__ -> {}); + } + + private void setAndStoreScript(final Path path) { + final Path copied; + try { + copied = + Workspace.copyScriptToWorkspace( + config.getSelectedWorkspacePath(), + path + ); + } catch (RuntimeException e) { + // Error popup + JOptionPane.showMessageDialog( + burpFrame, + e.getMessage(), + "Could not copy script to venv.", + JOptionPane.ERROR_MESSAGE + ); + return; + } + + // Store the path in the config. (writes to disk) + config.setUserScriptPath(copied); + Async.run(scalpelExecutor::notifyEventLoop); + + Async.run(this::updateScriptList); + Async.run(() -> selectScript(copied)); + } + + /** + * Returns the UI component to display. + * + * @return the UI component to display + */ + public Component uiComponent() { + return rootPanel; + } + + private void createUIComponents() { + rootPanel = new JPanel(); + + // Create the TtyConnector + terminalForVenvConfig = + Terminal.createTerminal( + theme, + config.getSelectedWorkspacePath().toString() + ); + } + + /** + * Method generated by IntelliJ IDEA GUI Designer + * >>> IMPORTANT!! <<< + * DO NOT edit this method OR call it in your code! + * + * @noinspection ALL + */ + private void $$$setupUI$$$() { + createUIComponents(); + rootPanel.setLayout( + new GridLayoutManager(2, 3, new Insets(0, 0, 0, 0), -1, -1) + ); + rootPanel.setBorder( + BorderFactory.createTitledBorder( + null, + "", + TitledBorder.DEFAULT_JUSTIFICATION, + TitledBorder.DEFAULT_POSITION, + null, + null + ) + ); + final JTabbedPane tabbedPane1 = new JTabbedPane(); + tabbedPane1.setToolTipText(""); + rootPanel.add( + tabbedPane1, + new GridConstraints( + 0, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + new Dimension(200, 200), + null, + 0, + false + ) + ); + final JPanel panel1 = new JPanel(); + panel1.setLayout( + new GridLayoutManager(1, 3, new Insets(0, 0, 0, 0), -1, -1) + ); + tabbedPane1.addTab("Scripts and Venv", panel1); + venvSelectPanel = new JPanel(); + venvSelectPanel.setLayout( + new GridLayoutManager(4, 2, new Insets(5, 5, 5, 0), -1, -1) + ); + panel1.add( + venvSelectPanel, + new GridConstraints( + 0, + 0, + 1, + 2, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + venvSelectPanel.setBorder( + BorderFactory.createTitledBorder( + null, + "", + TitledBorder.DEFAULT_JUSTIFICATION, + TitledBorder.DEFAULT_POSITION, + null, + null + ) + ); + final JPanel panel2 = new JPanel(); + panel2.setLayout(new BorderLayout(0, 0)); + venvSelectPanel.add( + panel2, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_SOUTH, + GridConstraints.FILL_HORIZONTAL, + 1, + 1, + null, + new Dimension(100, -1), + null, + 0, + false + ) + ); + panel2.add(addVentText, BorderLayout.CENTER); + addVenvButton = new JButton(); + addVenvButton.setText("+"); + addVenvButton.setToolTipText("Add/Create the virtualenv"); + panel2.add(addVenvButton, BorderLayout.EAST); + terminalForVenvConfig.setToolTipText( + "A terminal to install new packages and edit your script" + ); + venvSelectPanel.add( + terminalForVenvConfig, + new GridConstraints( + 0, + 1, + 4, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + final JPanel panel3 = new JPanel(); + panel3.setLayout( + new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1) + ); + venvSelectPanel.add( + panel3, + new GridConstraints( + 1, + 0, + 3, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + final JScrollPane scrollPane1 = new JScrollPane(); + panel3.add( + scrollPane1, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + 1, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + venvListComponent = new JList(); + final DefaultListModel defaultListModel1 = new DefaultListModel(); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("loremaucupatum"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatiolorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatiolorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatiolorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatiolorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatiolorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + defaultListModel1.addElement("lorem"); + defaultListModel1.addElement("ipsum"); + defaultListModel1.addElement("aucupatum"); + defaultListModel1.addElement("versatio"); + venvListComponent.setModel(defaultListModel1); + venvListComponent.setToolTipText(""); + scrollPane1.setViewportView(venvListComponent); + final JScrollPane scrollPane2 = new JScrollPane(); + scrollPane2.setToolTipText(""); + panel3.add( + scrollPane2, + new GridConstraints( + 1, + 0, + 2, + 1, + GridConstraints.ANCHOR_WEST, + GridConstraints.FILL_VERTICAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + scrollPane2.setBorder( + BorderFactory.createTitledBorder( + null, + "", + TitledBorder.DEFAULT_JUSTIFICATION, + TitledBorder.DEFAULT_POSITION, + null, + null + ) + ); + packagesTable = new JTable(); + packagesTable.setToolTipText( + "Packages installed in the current virtualenv" + ); + scrollPane2.setViewportView(packagesTable); + final JPanel panel4 = new JPanel(); + panel4.setLayout( + new GridLayoutManager(1, 1, new Insets(0, 0, 0, 0), -1, -1) + ); + panel1.add( + panel4, + new GridConstraints( + 0, + 2, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + browsePanel = new JPanel(); + browsePanel.setLayout( + new GridLayoutManager(11, 3, new Insets(3, 3, 3, 3), 0, -1) + ); + panel4.add( + browsePanel, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_NORTH, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + new Dimension(300, -1), + 0, + false + ) + ); + frameworkConfigPanel = new JPanel(); + frameworkConfigPanel.setLayout( + new GridLayoutManager(2, 3, new Insets(0, 0, 10, 10), -1, -1) + ); + frameworkConfigPanel.setEnabled(false); + frameworkConfigPanel.setVisible(false); + browsePanel.add( + frameworkConfigPanel, + new GridConstraints( + 1, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + frameworkBrowseButton = new JButton(); + frameworkBrowseButton.setText("Browse"); + frameworkConfigPanel.add( + frameworkBrowseButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 1, + false + ) + ); + final Spacer spacer1 = new Spacer(); + frameworkConfigPanel.add( + spacer1, + new GridConstraints( + 1, + 2, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_WANT_GROW, + 1, + null, + null, + null, + 0, + false + ) + ); + frameworkPathTextArea = new JTextArea(); + frameworkPathTextArea.setText("framework path"); + frameworkConfigPanel.add( + frameworkPathTextArea, + new GridConstraints( + 0, + 1, + 1, + 1, + GridConstraints.ANCHOR_SOUTH, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + new Dimension(150, 10), + null, + 0, + false + ) + ); + frameworkPathField = new JTextField(); + frameworkPathField.setHorizontalAlignment(4); + frameworkPathField.setText(""); + frameworkConfigPanel.add( + frameworkPathField, + new GridConstraints( + 1, + 1, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + new Dimension(50, 10), + null, + 0, + false + ) + ); + scriptConfigPanel = new JPanel(); + scriptConfigPanel.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 10, 10), -1, -1) + ); + scriptConfigPanel.setToolTipText(""); + browsePanel.add( + scriptConfigPanel, + new GridConstraints( + 5, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + scriptBrowseButton = new JButton(); + scriptBrowseButton.setText("Browse"); + scriptConfigPanel.add( + scriptBrowseButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 1, + false + ) + ); + scriptPathTextArea = new JLabel(); + scriptPathTextArea.setText("Add an existing python script"); + scriptConfigPanel.add( + scriptPathTextArea, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_SOUTH, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + new Dimension(-1, 10), + null, + 0, + false + ) + ); + final JPanel panel5 = new JPanel(); + panel5.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 10, 10), -1, -1) + ); + browsePanel.add( + panel5, + new GridConstraints( + 7, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + createButton = new JButton(); + createButton.setText("Create new script"); + panel5.add( + createButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 1, + false + ) + ); + final Spacer spacer2 = new Spacer(); + panel5.add( + spacer2, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + new Dimension(-1, 10), + null, + 0, + false + ) + ); + final JPanel panel6 = new JPanel(); + panel6.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 10, 10), -1, -1) + ); + panel6.setToolTipText(""); + browsePanel.add( + panel6, + new GridConstraints( + 6, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + openScriptButton = new JButton(); + openScriptButton.setText("Open selected script"); + openScriptButton.setToolTipText( + "Open the script with the command defined in \"Settings\"" + ); + panel6.add( + openScriptButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 1, + false + ) + ); + final Spacer spacer3 = new Spacer(); + panel6.add( + spacer3, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + new Dimension(-1, 10), + null, + 0, + false + ) + ); + listPannel = new JPanel(); + listPannel.setLayout( + new GridLayoutManager(3, 1, new Insets(0, 0, 0, 0), -1, -1) + ); + listPannel.setToolTipText(""); + browsePanel.add( + listPannel, + new GridConstraints( + 10, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + listPannel.setBorder( + BorderFactory.createTitledBorder( + null, + "", + TitledBorder.DEFAULT_JUSTIFICATION, + TitledBorder.DEFAULT_POSITION, + null, + null + ) + ); + final JScrollPane scrollPane3 = new JScrollPane(); + listPannel.add( + scrollPane3, + new GridConstraints( + 2, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + 1, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + venvScriptList = new JList(); + venvScriptList.setMaximumSize(new Dimension(65, 150)); + final DefaultListModel defaultListModel2 = new DefaultListModel(); + defaultListModel2.addElement("default.py"); + defaultListModel2.addElement("crypto.py"); + defaultListModel2.addElement("recon.py"); + venvScriptList.setModel(defaultListModel2); + venvScriptList.setPreferredSize(new Dimension(65, 300)); + venvScriptList.setToolTipText(""); + venvScriptList.putClientProperty("List.isFileList", Boolean.TRUE); + scrollPane3.setViewportView(venvScriptList); + final JLabel label1 = new JLabel(); + label1.setHorizontalAlignment(0); + label1.setHorizontalTextPosition(0); + label1.setText("Selected script: "); + listPannel.add( + label1, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + selectedScriptLabel = new JLabel(); + selectedScriptLabel.setText(""); + listPannel.add( + selectedScriptLabel, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + final JPanel panel7 = new JPanel(); + panel7.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 10, 10), -1, -1) + ); + panel7.setToolTipText(""); + browsePanel.add( + panel7, + new GridConstraints( + 8, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + panel7.setBorder( + BorderFactory.createTitledBorder( + null, + "", + TitledBorder.DEFAULT_JUSTIFICATION, + TitledBorder.DEFAULT_POSITION, + null, + null + ) + ); + openFolderButton = new JButton(); + openFolderButton.setText("Open script folder"); + openFolderButton.setToolTipText( + "Open the containing folder using the command defined in \"Settings\"" + ); + panel7.add( + openFolderButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 1, + false + ) + ); + final Spacer spacer4 = new Spacer(); + panel7.add( + spacer4, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + new Dimension(-1, 10), + null, + 0, + false + ) + ); + scalpelIsENABLEDButton = new JButton(); + scalpelIsENABLEDButton.setForeground(new Color(-4473925)); + scalpelIsENABLEDButton.setHideActionText(false); + scalpelIsENABLEDButton.setText("Scalpel is ENABLED"); + scalpelIsENABLEDButton.setToolTipText( + "Button to enable or disable Scalpel hooks" + ); + browsePanel.add( + scalpelIsENABLEDButton, + new GridConstraints( + 3, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + final Spacer spacer5 = new Spacer(); + browsePanel.add( + spacer5, + new GridConstraints( + 4, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + new Dimension(-1, 15), + null, + null, + 0, + false + ) + ); + final Spacer spacer6 = new Spacer(); + browsePanel.add( + spacer6, + new GridConstraints( + 2, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + new Dimension(-1, 5), + null, + null, + 0, + false + ) + ); + final JPanel panel8 = new JPanel(); + panel8.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 10, 10), -1, -1) + ); + panel8.setToolTipText(""); + browsePanel.add( + panel8, + new GridConstraints( + 9, + 0, + 1, + 3, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + panel8.setBorder( + BorderFactory.createTitledBorder( + null, + "", + TitledBorder.DEFAULT_JUSTIFICATION, + TitledBorder.DEFAULT_POSITION, + null, + null + ) + ); + resetTerminalButton = new JButton(); + resetTerminalButton.setText("Reset terminal"); + resetTerminalButton.setToolTipText( + "Reset the terminal in case it gets broken" + ); + panel8.add( + resetTerminalButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 1, + false + ) + ); + final Spacer spacer7 = new Spacer(); + panel8.add( + spacer7, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_VERTICAL, + 1, + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + new Dimension(-1, 10), + null, + 0, + false + ) + ); + outputTabPanel = new JPanel(); + outputTabPanel.setLayout( + new GridLayoutManager(2, 2, new Insets(0, 0, 0, 0), -1, -1) + ); + tabbedPane1.addTab("Script output", outputTabPanel); + final JPanel panel9 = new JPanel(); + panel9.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1) + ); + outputTabPanel.add( + panel9, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + stdoutScrollPane = new JScrollPane(); + panel9.add( + stdoutScrollPane, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + stdoutTextArea = new JTextArea(); + stdoutTextArea.setEditable(false); + stdoutTextArea.setLineWrap(true); + stdoutTextArea.setText(""); + stdoutTextArea.setWrapStyleWord(true); + stdoutTextArea.putClientProperty("html.disable", Boolean.TRUE); + stdoutScrollPane.setViewportView(stdoutTextArea); + final JLabel label2 = new JLabel(); + label2.setText("Output"); + panel9.add( + label2, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + final JPanel panel10 = new JPanel(); + panel10.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1) + ); + outputTabPanel.add( + panel10, + new GridConstraints( + 0, + 1, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + null, + null, + null, + 0, + false + ) + ); + stderrScrollPane = new JScrollPane(); + panel10.add( + stderrScrollPane, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + stderrTextArea = new JTextArea(); + stderrTextArea.setEditable(false); + stderrTextArea.setLineWrap(true); + stderrTextArea.setText(""); + stderrTextArea.putClientProperty("html.disable", Boolean.TRUE); + stderrScrollPane.setViewportView(stderrTextArea); + final JLabel label3 = new JLabel(); + label3.setText("Error"); + panel10.add( + label3, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_NONE, + GridConstraints.SIZEPOLICY_FIXED, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + settingsTab = new JPanel(); + settingsTab.setLayout(new BorderLayout(0, 0)); + settingsTab.putClientProperty("html.disable", Boolean.TRUE); + tabbedPane1.addTab("Settings", settingsTab); + final JPanel panel11 = new JPanel(); + panel11.setLayout( + new FormLayout( + "fill:d:grow", + "center:d:noGrow,top:4dlu:noGrow,center:max(d;4px):noGrow" + ) + ); + tabbedPane1.addTab("Help", panel11); + helpTextPane = new JTextPane(); + helpTextPane.setContentType("text/html"); + helpTextPane.setEditable(false); + helpTextPane.setEnabled(true); + helpTextPane.setText( + "\n \n \n \n \n

    \n To access the documentation: Click \n here\n

    \n

    \n To access the FAQ and common issues: Click \n Here \n

    \n

    \n To fully uninstall Scalpel, remove the ~/.scalpel/ folder in your home \n directory and make sure to remove the extension from Burp\n

    \n

    \n To reinstall Scalpel simply restart Burp and reload the extension\n

    \n

    \n If you have previously installed Scalpel and left the installation in a \n broken state, you must fully uninstall Scalpel before reinstalling it\n

    \n

    \n If Scalpel fails to load properly, make you sure you have installed all \n the dependencies specified in the Github \n README, and that your Python version is supported\n

    \n

    \n If you fail to troubleshoot a failed install, please open an issue on \n the GitHub page and \n include the content of the "Debug Info" tab\n

    \n

    \n Note: To reload Scalpel, you MUST restart Burp entirely\n

    \n \n\n" + ); + CellConstraints cc = new CellConstraints(); + panel11.add(helpTextPane, cc.xy(1, 1)); + openIssueOnGitHubButton = new JButton(); + openIssueOnGitHubButton.setText("Open an issue on GitHub"); + panel11.add( + openIssueOnGitHubButton, + new CellConstraints( + 1, + 3, + 1, + 1, + CellConstraints.LEFT, + CellConstraints.DEFAULT, + new Insets(0, 5, 0, 0) + ) + ); + final JPanel panel12 = new JPanel(); + panel12.setLayout( + new GridLayoutManager(2, 1, new Insets(0, 0, 0, 0), -1, -1) + ); + tabbedPane1.addTab("Debug Info", panel12); + final JScrollPane scrollPane4 = new JScrollPane(); + scrollPane4.setHorizontalScrollBarPolicy(30); + panel12.add( + scrollPane4, + new GridConstraints( + 0, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_BOTH, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_WANT_GROW, + null, + null, + null, + 0, + false + ) + ); + debugInfoTextPane = new JTextPane(); + debugInfoTextPane.setEditable(false); + debugInfoTextPane.setText(""); + debugInfoTextPane.setToolTipText( + "Transmit this when asking for support." + ); + debugInfoTextPane.putClientProperty("html.disable", Boolean.TRUE); + scrollPane4.setViewportView(debugInfoTextPane); + copyToClipboardButton = new JButton(); + copyToClipboardButton.setText("Copy to clipboard"); + panel12.add( + copyToClipboardButton, + new GridConstraints( + 1, + 0, + 1, + 1, + GridConstraints.ANCHOR_CENTER, + GridConstraints.FILL_HORIZONTAL, + GridConstraints.SIZEPOLICY_CAN_SHRINK | + GridConstraints.SIZEPOLICY_CAN_GROW, + GridConstraints.SIZEPOLICY_FIXED, + null, + null, + null, + 0, + false + ) + ); + } + + /** + * @noinspection ALL + */ + public JComponent $$$getRootComponent$$$() { + return rootPanel; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ConfigUtil.java b/scalpel/src/main/java/lexfo/scalpel/ConfigUtil.java new file mode 100644 index 00000000..043f4412 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ConfigUtil.java @@ -0,0 +1,51 @@ +package lexfo.scalpel; + +import com.fasterxml.jackson.core.type.TypeReference; +import java.io.File; +import java.lang.reflect.Field; +import java.util.Map; + +public class ConfigUtil { + + public static T readConfigFile(File file, Class clazz) { + final T defaultInstance; + try { + defaultInstance = clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + final Map map = IO.readJSON( + file, + new TypeReference>() {}, + e -> + ScalpelLogger.logStackTrace( + "/!\\ Invalid JSON config file /!\\" + + ", try re-installing Scalpel by removing ~/.scalpel and restarting Burp.", + e + ) + ); + + if (map != null) { + mergeWithDefaults(defaultInstance, map); + } + + return defaultInstance; + } + + private static void mergeWithDefaults( + T instance, + Map map + ) { + for (final Field field : instance.getClass().getDeclaredFields()) { + field.setAccessible(true); + if (map.containsKey(field.getName())) { + try { + field.set(instance, map.get(field.getName())); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Constants.java b/scalpel/src/main/java/lexfo/scalpel/Constants.java new file mode 100644 index 00000000..66d96db9 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Constants.java @@ -0,0 +1,187 @@ +package lexfo.scalpel; + +import com.google.common.collect.ImmutableSet; +import com.jediterm.terminal.ui.UIUtil; + +/** + Contains constants used by the extension. +*/ +public class Constants { + + public static final String REQ_EDIT_PREFIX = "req_edit_"; + + /** + Callback prefix for request editors. + */ + public static final String FRAMEWORK_REQ_EDIT_PREFIX = + "_" + REQ_EDIT_PREFIX; + + public static final String RES_EDIT_PREFIX = "res_edit_"; + + /** + Callback prefix for response editors. + */ + public static final String FRAMEWORK_RES_EDIT_PREFIX = + "_" + RES_EDIT_PREFIX; + + /** + Callback suffix for HttpMessage-to-bytes convertion. + */ + public static final String IN_SUFFIX = "in"; + + /** + Callback suffix for bytes to HttpMessage convertion. + */ + public static final String OUT_SUFFIX = "out"; + + public static final String REQ_CB_NAME = "request"; + + /** + Callback prefix for request intercepters. + */ + public static final String FRAMEWORK_REQ_CB_NAME = "_" + REQ_CB_NAME; + + public static final String RES_CB_NAME = "response"; + + public static final ImmutableSet VALID_HOOK_PREFIXES = ImmutableSet.of( + REQ_EDIT_PREFIX, + RES_EDIT_PREFIX, + REQ_CB_NAME, + RES_CB_NAME + ); + + /** + Callback prefix for response intercepters. + */ + public static final String FRAMEWORK_RES_CB_NAME = "_" + RES_CB_NAME; + + /** + Scalpel prefix for the persistence databases. + + @see burp.api.montoya.persistence.Persistence + */ + public static final String PERSISTENCE_PREFIX = "scalpel:"; + + /** + Persistence key for the cached user script path. + */ + public static final String PERSISTED_SCRIPT = + PERSISTENCE_PREFIX + "script_path"; + + /** + Persistence key for the cached framework path. + */ + public static final String PERSISTED_FRAMEWORK = + PERSISTENCE_PREFIX + "framework_path"; + + public static final String GET_CB_NAME = "_get_callables"; + + /** + * Required python packages + */ + public static final String[] DEFAULT_VENV_DEPENDENCIES = new String[] { + "jep==4.2.0", + }; + + public static final String[] PYTHON_DEPENDENCIES = new String[] { + "requests", + "requests-toolbelt", + }; + + /** + * Venv dir containing site-packages + */ + public static final String VENV_LIB_DIR = UIUtil.isWindows ? "Lib" : "lib"; + + /** + * JEP native library filename + */ + public static final String NATIVE_LIBJEP_FILE = UIUtil.isWindows + ? "jep.dll" + : UIUtil.isMac ? "libjep.jnilib" : "libjep.so"; + + /** + * Python 3 executable filename + */ + public static final String PYTHON_BIN = UIUtil.isWindows + ? "python.exe" + : "python3"; + + public static final String PIP_BIN = UIUtil.isWindows ? "pip.exe" : "pip"; + public static final String VENV_BIN_DIR = UIUtil.isWindows + ? "Scripts" + : "bin"; + + public static final String DEFAULT_TERMINAL_EDITOR = "vi"; + + public static final String DEFAULT_WINDOWS_EDITOR = "notepad.exe"; + + public static final String EDITOR_MODE_ANNOTATION_KEY = + "scalpel_editor_mode"; + public static final String HEX_EDITOR_MODE = "hex"; + public static final String RAW_EDITOR_MODE = "raw"; + public static final String DEFAULT_EDITOR_MODE = RAW_EDITOR_MODE; + + public static final int MIN_SUPPORTED_PYTHON_VERSION = 8; + public static final int PREFERRED_PYTHON_VERSION = 10; + + public static final String DEFAULT_UNIX_SHELL = "/bin/bash"; + + public static final String DEFAULT_LINUX_TERM_EDIT_CMD = + CommandChecker.getAvailableCommand( + System.getenv("EDITOR"), + "micro", + "vim", + "nano", + "vi", + "emacs", + "bat", + "pager", + "less", + "more", + "/bin/cat" + ) + + " {file}"; + + public static final String DEFAULT_LINUX_OPEN_FILE_CMD = + CommandChecker.getAvailableCommand( + "code {dir}", + "xdg-open", + "gnome-open", + "kde-open", + "exo-open", + "mimeopen", + "gvfs-open", + "open", + DEFAULT_LINUX_TERM_EDIT_CMD + ) + + " {file}"; + + public static final String DEFAULT_LINUX_OPEN_DIR_CMD = + CommandChecker.getAvailableCommand( + "xdg-open", + "gnome-open", + "kde-open", + "exo-open", + "mimeopen", + "gvfs-open", + "open" + ) + + " {dir}"; + + public static final String DEFAULT_WINDOWS_TERM_EDIT_CMD = "type {file}"; + public static final String DEFAULT_WINDOWS_OPEN_FILE_CMD = + "explorer.exe {file}"; + public static final String DEFAULT_WINDOWS_OPEN_DIR_CMD = + "explorer.exe {dir}"; + + public static final String DEFAULT_TERM_EDIT_CMD = UIUtil.isWindows + ? DEFAULT_WINDOWS_TERM_EDIT_CMD + : DEFAULT_LINUX_TERM_EDIT_CMD; + public static final String DEFAULT_OPEN_FILE_CMD = UIUtil.isWindows + ? DEFAULT_WINDOWS_OPEN_FILE_CMD + : DEFAULT_LINUX_OPEN_FILE_CMD; + public static final String DEFAULT_OPEN_DIR_CMD = UIUtil.isWindows + ? DEFAULT_WINDOWS_OPEN_DIR_CMD + : DEFAULT_LINUX_OPEN_DIR_CMD; +} diff --git a/scalpel/src/main/java/lexfo/scalpel/EditorType.java b/scalpel/src/main/java/lexfo/scalpel/EditorType.java new file mode 100644 index 00000000..fe362691 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/EditorType.java @@ -0,0 +1,16 @@ +package lexfo.scalpel; + +/** + ** Enum used by editors to identify themselves + */ +public enum EditorType { + /** + Indicates an editor for an HTTP request. + */ + REQUEST, + + /** + Indicates an editor for an HTTP response. + */ + RESPONSE, +} diff --git a/scalpel/src/main/java/lexfo/scalpel/IO.java b/scalpel/src/main/java/lexfo/scalpel/IO.java new file mode 100644 index 00000000..d749ec20 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/IO.java @@ -0,0 +1,115 @@ +package lexfo.scalpel; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class IO { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @FunctionalInterface + public interface IOSupplier { + T call() throws IOException, InterruptedException, ExecutionException; + } + + @FunctionalInterface + public interface IORunnable { + void run() throws IOException, InterruptedException, ExecutionException; + } + + public static T ioWrap(IOSupplier supplier) { + try { + return supplier.call(); + } catch (IOException | InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public static void run(IORunnable supplier) { + try { + supplier.run(); + } catch (IOException | InterruptedException | ExecutionException e) {} + } + + public static T ioWrap( + IOSupplier supplier, + Supplier defaultSupplier + ) { + try { + return supplier.call(); + } catch (IOException | InterruptedException | ExecutionException e) { + return defaultSupplier.get(); + } + } + + public static T readJSON(File file, TypeReference typeRef) { + return ioWrap(() -> mapper.readValue(file, typeRef)); + } + + public static T readJSON( + File file, + TypeReference typeRef, + Consumer errorHandler + ) { + return ioWrap( + () -> mapper.readValue(file, typeRef), + () -> { + errorHandler.accept(new IOException()); + return null; + } + ); + } + + public static T readJSON(File file, Class clazz) { + return ioWrap(() -> mapper.readValue(file, clazz)); + } + + public static T readJSON( + File file, + Class clazz, + Consumer errorHandler + ) { + return ioWrap( + () -> mapper.readValue(file, clazz), + () -> { + errorHandler.accept(new IOException()); + return null; + } + ); + } + + public static T readJSON(String json, Class clazz) { + return ioWrap(() -> mapper.readValue(json, clazz)); + } + + public static void writeJSON(File file, Object obj) { + ioWrap(() -> { + mapper.writerWithDefaultPrettyPrinter().writeValue(file, obj); + + new FileWriter(file, true).append('\n').close(); + return null; + }); + } + + public static void writeFile(String path, String content) { + ioWrap(() -> { + FileWriter writer = new FileWriter(path); + writer.write(content); + writer.close(); + return null; + }); + } + + public static void sleep(Integer ms) { + ioWrap(() -> { + Thread.sleep(ms); + return null; + }); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Palette.java b/scalpel/src/main/java/lexfo/scalpel/Palette.java new file mode 100644 index 00000000..7b8ce256 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Palette.java @@ -0,0 +1,97 @@ +package lexfo.scalpel; + +import com.jediterm.terminal.emulator.ColorPalette; +import java.awt.*; + +/** + * Color palette for the embedded terminal + * Contains colors for both light and dark theme + */ +public class Palette extends ColorPalette { + + private static final Color[] DARK_COLORS = new Color[] { + new Color(0x2c2c2c), // Slightly lighter dark blue + new Color(0xe60000), // Slightly lighter red + new Color(0x00e600), // Slightly lighter green + new Color(0xe6e600), // Slightly lighter yellow + new Color(0x2e9afe), // Slightly lighter blue + new Color(0xe600e6), // Slightly lighter magenta + new Color(0x00e6e6), // Slightly lighter cyan + new Color(0xf2f2f2), // Slightly lighter white + + // Bright versions of the ISO colors + new Color(0x5f5f5f), // Bright black + new Color(0xff5f5f), // Bright red + new Color(0x5fff5f), // Bright green + new Color(0xffff5f), // Bright yellow + new Color(0x7da7ff), // Bright blue + new Color(0xff5fff), // Bright magenta + new Color(0x5fffff), // Bright cyan + new Color(0xffffff), // Bright white + }; + + public static final ColorPalette DARK_PALETTE = new Palette(DARK_COLORS); + + private static final Color[] LIGHT_COLORS = new Color[] { + new Color(0x1c1c1c), // Dark blue + new Color(0xcd0000), // Red + new Color(0x00cd00), // Green + new Color(0xcdcd00), // Yellow + new Color(0x1e90ff), // Blue + new Color(0xcd00cd), // Magenta + new Color(0x00cdcd), // Cyan + new Color(0xd4d4d4), // Grayish white + + // Bright versions of the ISO colors + new Color(0x555555), // Bright black + new Color(0xff0000), // Bright red + new Color(0x00ff00), // Bright green + new Color(0xffff00), // Bright yellow + new Color(0x6495ed), // Bright blue + new Color(0xff00ff), // Bright magenta + new Color(0x00ffff), // Bright cyan + new Color(0xe5e5e5), // Bright grayish white + }; + + public static final ColorPalette LIGHT_PALETTE = new Palette(LIGHT_COLORS); + + private static final Color[] WINDOWS_COLORS = new Color[] { + new Color(0x000000), //Black + new Color(0x800000), //Red + new Color(0x008000), //Green + new Color(0x808000), //Yellow + new Color(0x000080), //Blue + new Color(0x800080), //Magenta + new Color(0x008080), //Cyan + new Color(0xc0c0c0), //White + //Bright versions of the ISO colors + new Color(0x808080), //Black + new Color(0xff0000), //Red + new Color(0x00ff00), //Green + new Color(0xffff00), //Yellow + new Color(0x4682b4), //Blue + new Color(0xff00ff), //Magenta + new Color(0x00ffff), //Cyan + new Color(0xffffff), //White + }; + + public static final ColorPalette WINDOWS_PALETTE = new Palette( + WINDOWS_COLORS + ); + + private final Color[] myColors; + + private Palette(Color[] colors) { + myColors = colors; + } + + @Override + public Color getForegroundByColorIndex(int colorIndex) { + return myColors[colorIndex]; + } + + @Override + protected Color getBackgroundByColorIndex(int colorIndex) { + return myColors[colorIndex]; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/PythonSetup.java b/scalpel/src/main/java/lexfo/scalpel/PythonSetup.java new file mode 100644 index 00000000..721243aa --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/PythonSetup.java @@ -0,0 +1,110 @@ +package lexfo.scalpel; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Utilities to initialize Java Embedded Python (jep) + */ +public class PythonSetup { + + public static void loadLibPython3() { + final String libPath = executePythonCommand( + "import sysconfig; print('/'.join([sysconfig.get_config_var('LIBDIR'), 'libpython' + sysconfig.get_config_var('VERSION') + '.dylib']))" + ); + + ScalpelLogger.all("Loading Python library from " + libPath); + try { + System.load(libPath); + } catch (Throwable e) { + throw new RuntimeException( + "Failed loading" + + libPath + + "\nIf you are using an ARM/M1 macOS, make sure you installed the ARM/M1 Burp package and not the Intel one:\n" + + "https://portswigger.net/burp/releases/professional-community-2023-10-1?requestededition=professional&requestedplatform=macos%20(arm/m1)", + e + ); + } + ScalpelLogger.all("Successfully loaded Python library from " + libPath); + } + + public static int getPythonVersion() { + final String version = executePythonCommand( + "import sys; print('.'.join(map(str, sys.version_info[:3])))" + ); + + if (version != null) { + final String[] versionParts = version.split("\\."); + final int major = Integer.parseInt(versionParts[0]); + final int minor = Integer.parseInt(versionParts[1]); + + if ( + major < 3 || + (major == 3 && minor < Constants.MIN_SUPPORTED_PYTHON_VERSION) + ) { + throw new RuntimeException( + "Detected Python version " + + version + + ". Requires Python version 3." + + Constants.MIN_SUPPORTED_PYTHON_VERSION + + " or greater." + ); + } + + if (minor >= Constants.PREFERRED_PYTHON_VERSION) { + return Constants.PREFERRED_PYTHON_VERSION; + } + return Constants.MIN_SUPPORTED_PYTHON_VERSION; + } else { + throw new RuntimeException("Failed to retrieve Python version."); + } + } + + public static String getUsedPythonBin() { + // Get ~/.scalpel/venvs/default/.venv/bin/python if it exists, else, the Python in PATH + // This is useful for cases where someone previously installed Scalpel with an older python version, and is now running Burp with a newer one. + // The version that will actually be used will always be the default venv one.bu + try { + return Venv + .getExecutablePath( + Workspace.getVenvDir(Workspace.getDefaultWorkspace()), + Constants.PYTHON_BIN + ) + .toAbsolutePath() + .toString(); + } catch (IOException e) { + return Constants.PYTHON_BIN; + } + } + + public static String executePythonCommand(final String command) { + try { + final String[] cmd = { getUsedPythonBin(), "-c", command }; + + final ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectErrorStream(true); // Redirect stderr to stdout + + final Process process = pb.start(); + final String output; + try ( + BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()) + ) + ) { + output = reader.readLine(); + } + + final int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException( + "Python command failed with exit code " + exitCode + ); + } + + return output; + } catch (Throwable e) { + throw new RuntimeException(e); + } + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/PythonUtils.java b/scalpel/src/main/java/lexfo/scalpel/PythonUtils.java new file mode 100644 index 00000000..c4f04c0f --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/PythonUtils.java @@ -0,0 +1,101 @@ +package lexfo.scalpel; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.HttpHeader; +import burp.api.montoya.http.message.HttpMessage; +import java.lang.reflect.Method; +import java.util.stream.IntStream; + +/** + Utility class for Python scripts. +*/ +public class PythonUtils { + + /** + * Convert Java signed bytes to corresponding unsigned values + * Convertions issues occur when passing Java bytes to Python because Java's are signed and Python's are unsigned. + * Passing an unsigned int array solves this problem. + * + * + * @param javaBytes the bytes to convert + * @return the corresponding unsigned values as int + */ + public static int[] toPythonBytes(byte[] javaBytes) { + return IntStream + .range(0, javaBytes.length) + .map(i -> Byte.toUnsignedInt(javaBytes[i])) + .toArray(); + } + + /** + * Convert Python bytes to Java bytes + * + * It is not possible to explicitely convert to Java bytes Python side without a Java helper like this one, + * because Jep doesn't natively support the convertion: + * https://github.com/ninia/jep/wiki/How-Jep-Works#objects + * + * When returning byte[], + * Python receives a PyJArray of integer-like objects which will be mapped back to byte[] by Jep. + * + * Some errors this solves are for example when there is both an overload for byte[] and int[] and Jep chooses the wrong one. + * This can be used to avoid type errors by avoding Jep's conversion by passing a native Java object. + * + * @param pythonBytes the unsigned values to convert + * @return the corresponding signed bytes + */ + public static byte[] toJavaBytes(byte[] pythonBytes) { + return pythonBytes; + } + + /** + * Convert Python bytes to a Burp ByteArray + * + * @param pythonBytes the unsigned values to convert + * @return the corresponding Burp ByteArray + */ + public static ByteArray toByteArray(byte[] pythonBytes) { + return ByteArray.byteArray(pythonBytes); + } + + /** + * Updates the specified HttpMessage object's header with the specified name and value. + * Creates the header when it doesn't exist. + *

    (Burp's withUpdatedHeader() method does not create the header.) + * + * @param The type of the HttpMessage object. + * @param msg The HttpMessage object to update. + * @param name The name of the header to update. + * @param value The value of the header to update. + * @return The updated HttpMessage object. + */ + @SuppressWarnings({ "unchecked" }) + public static T updateHeader( + T msg, + String name, + String value + ) { + final String methName = msg + .headers() + .stream() + .map(HttpHeader::name) + .anyMatch(name::equalsIgnoreCase) + ? "withUpdatedHeader" + : "withAddedHeader"; + + try { + final Method meth = msg + .getClass() + .getMethod(methName, String.class, String.class); + + return (T) meth.invoke(msg, name, value); + } catch (Throwable e) { + e.printStackTrace(); + } + + throw new RuntimeException( + "Wrong type " + + msg.getClass().getSimpleName() + + " was passed to updateHeader()" + ); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/RessourcesUnpacker.java b/scalpel/src/main/java/lexfo/scalpel/RessourcesUnpacker.java new file mode 100644 index 00000000..3f029ae6 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/RessourcesUnpacker.java @@ -0,0 +1,207 @@ +package lexfo.scalpel; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Enumeration; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + Provides methods for unpacking the Scalpel resources. +*/ +public class RessourcesUnpacker { + + // Scalpel configuration directory basename + public static final String DATA_DIRNAME = ".scalpel"; + + public static final Path DATA_DIR_PATH = Path.of( + System.getProperty("user.home"), + DATA_DIRNAME + ); + // Directory to copy ressources embed in .jar + public static final String RESSOURCES_DIRNAME = "extracted"; + + // Directory to copy the Python stuff + public static final String PYTHON_DIRNAME = + "python3-" + PythonSetup.getPythonVersion(); + + // Directory to copy the base init script + public static final String SHELL_DIRNAME = "shell"; + + // Directory to copy the script template to open when creating a new script + public static final String TEMPLATES_DIRNAME = "templates"; + + // Directory where user venvs will be created + public static final String WORKSPACE_DIRNAME = "venv"; + + // Whitelist for the ressources to extract + private static final Set RESSOURCES_TO_COPY = Set.of( + PYTHON_DIRNAME, + SHELL_DIRNAME, + TEMPLATES_DIRNAME, + WORKSPACE_DIRNAME + ); + + // Directory containing example scripts + public static final String SAMPLES_DIRNAME = "samples"; + + public static final String DEFAULT_SCRIPT_FILENAME = "default.py"; + + // The absolute path to the Scalpel resources directory. + public static final Path RESSOURCES_PATH = DATA_DIR_PATH.resolve( + RESSOURCES_DIRNAME + ); + + // Actual paths for directories defined aboves + + public static final Path PYTHON_PATH = RESSOURCES_PATH.resolve( + PYTHON_DIRNAME + ); + public static final Path WORKSPACE_PATH = RESSOURCES_PATH.resolve( + WORKSPACE_DIRNAME + ); + + // Path to the Pyscalpel module + public static final Path PYSCALPEL_PATH = PYTHON_PATH.resolve("pyscalpel"); + + // Path to the framework script + public static final Path FRAMEWORK_PATH = PYSCALPEL_PATH.resolve( + "_framework.py" + ); + + public static final Path SAMPLES_PATH = PYTHON_PATH.resolve( + SAMPLES_DIRNAME + ); + + public static final Path DEFAULT_SCRIPT_PATH = SAMPLES_PATH.resolve( + DEFAULT_SCRIPT_FILENAME + ); + + public static final Path BASH_INIT_FILE_PATH = RESSOURCES_PATH + .resolve(SHELL_DIRNAME) + .resolve("init-venv.sh"); + + // https://stackoverflow.com/questions/320542/how-to-get-the-path-of-a-running-jar-file#:~:text=return%20new%20File(MyClass.class.getProtectionDomain().getCodeSource().getLocation()%0A%20%20%20%20.toURI()).getPath()%3B + /** + Returns the path to the Scalpel JAR file. + @return The path to the Scalpel JAR file. + */ + private static String getRunningJarPath() { + try { + return Scalpel.class.getProtectionDomain() + .getCodeSource() + .getLocation() + .toURI() + .getPath(); + } catch (Throwable e) { + return "err"; + } + } + + // https://stackoverflow.com/questions/9324933/what-is-a-good-java-library-to-zip-unzip-files#:~:text=Extract%20zip%20file%20and%20all%20its%20subfolders%2C%20using%20only%20the%20JDK%3A + /** + Extracts the Scalpel python resources from the Scalpel JAR file. + + @param zipFile The path to the Scalpel JAR file. + @param extractFolder The path to the Scalpel resources directory. + */ + private static void extractRessources( + String zipFile, + String extractFolder, + Set entriesWhitelist + ) { + ZipFile zip = null; + try { + final int BUFFER = 2048; + final File file = new File(zipFile); + zip = new ZipFile(file); + + final String newPath = extractFolder; + + new File(newPath).mkdirs(); + final Enumeration zipFileEntries = zip.entries(); + + // Process each entry + while (zipFileEntries.hasMoreElements()) { + // grab a zip file entry + final ZipEntry entry = zipFileEntries.nextElement(); + + final String currentEntry = entry.getName(); + final long size = entry.getSize(); + + if ( + entriesWhitelist + .stream() + .noneMatch(currentEntry::startsWith) + ) { + continue; + } + + ScalpelLogger.info( + "Extracting " + currentEntry + " (" + size + " bytes)" + ); + + final File destFile = new File(newPath, currentEntry); + final File destinationParent = destFile.getParentFile(); + + // create the parent directory structure if needed + destinationParent.mkdirs(); + + if (!entry.isDirectory()) { + final InputStream is = zip.getInputStream(entry); + + int currentByte; + // establish buffer for writing file + final byte[] data = new byte[BUFFER]; + + // write the current file to disk + final FileOutputStream dest = new FileOutputStream( + destFile + ); + + // read and write until last byte is encountered + while ((currentByte = is.read(data)) != -1) { + dest.write(data, 0, currentByte); + } + dest.flush(); + dest.close(); + is.close(); + } + } + } catch (Throwable e) { + ScalpelLogger.logStackTrace(e); + } finally { + try { + if (zip != null) zip.close(); + } catch (Throwable e) { + ScalpelLogger.logStackTrace(e); + } + } + } + + /** + Initializes the Scalpel resources directory. + */ + public static void extractRessourcesToHome() { + try { + // Create a $HOME/.scalpel/extracted directory. + ScalpelLogger.all("Extracting to " + RESSOURCES_PATH); + + extractRessources( + getRunningJarPath(), + RESSOURCES_PATH.toString(), + RESSOURCES_TO_COPY + ); + + ScalpelLogger.all( + "Successfully extracted running .jar to " + RESSOURCES_PATH + ); + } catch (Throwable e) { + ScalpelLogger.error("extractRessourcesToHome() failed."); + ScalpelLogger.logStackTrace(e); + } + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Result.java b/scalpel/src/main/java/lexfo/scalpel/Result.java new file mode 100644 index 00000000..d5dc1ff5 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Result.java @@ -0,0 +1,135 @@ +package lexfo.scalpel; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Optional-style class for handling python task results + * + * A completed python task can have multiple outcomes: + * - The task completes successfully and returns a value + * - The task completes successfully but returns no value + * - The task throws an exception + * + * Result allows us to handle returned values and errors uniformly to handle them when needed. + */ +public class Result { + + private final T value; + private final E error; + private final boolean isEmpty; + + private Result(T value, E error, boolean isEmpty) { + this.value = value; + this.error = error; + this.isEmpty = isEmpty; + } + + public static Result success(T value) { + return new Result<>(value, null, false); + } + + public static Result empty() { + return new Result<>(null, null, true); + } + + public static Result error(E error) { + return new Result<>(null, error, false); + } + + public boolean isSuccess() { + return error == null; + } + + public boolean hasValue() { + return isSuccess() && !isEmpty(); + } + + public T getValue() { + if (!isSuccess()) { + throw new RuntimeException("Result is in error state", error); + } + if (isEmpty) { + throw new IllegalStateException("Result is empty"); + } + return value; + } + + public E getError() { + return error; + } + + public boolean isEmpty() { + return isEmpty && isSuccess(); + } + + @Override + public String toString() { + if (isSuccess()) { + if (isEmpty) { + return ""; + } else { + return String.valueOf(value); + } + } else { + // Convert stacktrace to string + final StringWriter sw = new StringWriter(); + final PrintWriter pw = new PrintWriter(sw); + error.printStackTrace(pw); + return sw.toString(); + } + } + + public Result map(Function mapper) { + if (isSuccess() && !isEmpty) { + return new Result<>(mapper.apply(value), null, false); + } else if (isEmpty) { + return empty(); + } else { + return error(error); + } + } + + public Result flatMap(Function> mapper) { + if (isSuccess() && !isEmpty) { + return mapper.apply(value); + } else if (isEmpty) { + return empty(); + } else { + return error(error); + } + } + + public Result or(Result other) { + return isSuccess() ? this : other; + } + + public T orElse(T other) { + return isSuccess() && !isEmpty ? value : other; + } + + public T orElseGet(Supplier other) { + return isSuccess() && !isEmpty ? value : other.get(); + } + + public void ifSuccess(Consumer action) { + if (isSuccess() && !isEmpty) { + action.accept(value); + } + } + + public void ifError(Consumer action) { + if (!isSuccess()) { + action.accept(error); + } + } + + public void ifEmpty(Runnable action) { + if (isEmpty) { + action.run(); + } + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Scalpel.java b/scalpel/src/main/java/lexfo/scalpel/Scalpel.java new file mode 100644 index 00000000..74df7bb8 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Scalpel.java @@ -0,0 +1,211 @@ +package lexfo.scalpel; + +import burp.api.montoya.BurpExtension; +import burp.api.montoya.MontoyaApi; +import com.jediterm.terminal.ui.UIUtil; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.stream.Collectors; +import jep.MainInterpreter; + +// Burp will auto-detect and load any class that extends BurpExtension. +/** + The main class of the extension. + This class is instantiated by Burp Suite and is used to initialize the extension. +*/ +public class Scalpel implements BurpExtension { + + /** + * The ScalpelExecutor object used to execute Python scripts. + */ + private ScalpelExecutor executor; + + /** + * The MontoyaApi object used to interact with Burp Suite. + */ + private MontoyaApi API; + + private Config config; + + private static void logConfig(final Config config) { + ScalpelLogger.all("Config:"); + ScalpelLogger.all("Framework: " + config.getFrameworkPath()); + ScalpelLogger.all("Script: " + config.getUserScriptPath()); + ScalpelLogger.all( + "Venvs: " + + Arrays + .stream(config.getVenvPaths()) + .collect(Collectors.joining("\",\"", "[\"", "\"]")) + ); + ScalpelLogger.all("Default venv: " + Workspace.getDefaultWorkspace()); + ScalpelLogger.all( + "Selected venv: " + config.getSelectedWorkspacePath() + ); + } + + private static void setupJepFromConfig(Config config) throws IOException { + final Path venvPath = Workspace + .getOrCreateDefaultWorkspace(config.getJdkPath()) + .resolve(Workspace.VENV_DIR); + + final File dir = Venv.getSitePackagesPath(venvPath).toFile(); + + final File[] jepDirs = dir.listFiles((__, name) -> name.matches("jep")); + + if (jepDirs.length == 0) { + throw new IOException( + "FATAL: Could not find jep directory in " + + dir + + "\nIf the install failed previously, remove the ~/.scalpel directory and reload the extension" + ); + } + + final String jepDir = jepDirs[0].getAbsolutePath(); + + // Adding path to java.library.path is necessary for Windows + final String oldLibPath = System.getProperty("java.library.path"); + final String newLibPath = jepDir + File.pathSeparator + oldLibPath; + System.setProperty("java.library.path", newLibPath); + + final String libjepFile = Constants.NATIVE_LIBJEP_FILE; + final String jepLib = Paths.get(jepDir, libjepFile).toString(); + + // Load the library ourselves to catch errors right away. + ScalpelLogger.all("Loading Jep native library from " + jepLib); + System.load(jepLib); + MainInterpreter.setJepLibraryPath(jepLib); + } + + private static void waitForExecutor( + MontoyaApi API, + ScalpelEditorProvider provider, + ScalpelExecutor executor + ) { + while (executor.isStarting()) { + IO.sleep(100); + } + if (executor.isRunning()) { + ScalpelLogger.all( + "SUCCESS: Initialized Scalpel's embedded Python successfully!" + ); + } else { + ScalpelLogger.error( + "ERROR: Failed to initialize Scalpel's embedded Python." + ); + } + + // Init may fail if the user script is invalid. + // Wait for the user to fix their script before registering Burp hooks. + while (!executor.isRunning()) { + IO.sleep(100); + } + // Add editor tabs to Burp + API.userInterface().registerHttpRequestEditorProvider(provider); + API.userInterface().registerHttpResponseEditorProvider(provider); + + // Intercept HTTP requests + API + .http() + .registerHttpHandler( + new ScalpelHttpRequestHandler(API, provider, executor) + ); + } + + /** + * Initializes the extension. + @param API The MontoyaApi object to use. + */ + @Override + public void initialize(MontoyaApi API) { + this.API = API; + + // Set displayed extension name. + API.extension().setName("Scalpel"); + + // Create a logger that will display messages in Burp extension logs. + ScalpelLogger.setLogger(API.logging()); + final String logLevel = API + .persistence() + .preferences() + .getString("logLevel"); + + if (logLevel != null) { + ScalpelLogger.setLogLevel(logLevel); + } + + try { + ScalpelLogger.all("Initializing..."); + + if (UIUtil.isMac) { + // It may be required to manually load libpython on MacOS for jep not to break + // https://github.com/ninia/jep/issues/432#issuecomment-1317590878 + PythonSetup.loadLibPython3(); + } + + // Extract embeded ressources. + ScalpelLogger.all("Extracting ressources..."); + RessourcesUnpacker.extractRessourcesToHome(); + + ScalpelLogger.all("Reading config and initializing venvs..."); + ScalpelLogger.all( + "(This might take a minute, Scalpel is installing dependencies...)" + ); + + config = Config.getInstance(API); + logConfig(config); + + setupJepFromConfig(config); + + // Initialize Python task queue. + executor = new ScalpelExecutor(API, config); + + // Add the configuration tab to Burp UI. + API + .userInterface() + .registerSuiteTab( + "Scalpel", + UIBuilder.constructConfigTab( + API, + executor, + config, + API.userInterface().currentTheme() + ) + ); + + // Create the provider responsible for creating the request/response editors for Burp. + final ScalpelEditorProvider provider = new ScalpelEditorProvider( + API, + executor + ); + + // Inject dependency to solve circular dependency. + executor.setEditorsProvider(provider); + + // Extension is fully loaded. + ScalpelLogger.all("Successfully loaded scalpel."); + + // Log an error or success message when executor has finished initializing. + Async.run(() -> waitForExecutor(API, provider, executor)); + } catch (Throwable e) { + ScalpelLogger.all("Failed to initialize Scalpel:"); + if ( + e instanceof ExceptionInInitializerError && e.getCause() != null + ) { + // Log the original error and stacktrace + // This happens when an Exception is thrown in the static initialization of RessourcesUnpacker + e = e.getCause(); + } + + ScalpelLogger.logFatalStackTrace(e); + + // Burp race-condition: cannot unload an extension while in initialize() + Async.run(() -> { + IO.sleep(500); + API.extension().unload(); + }); + } + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ScalpelEditorProvider.java b/scalpel/src/main/java/lexfo/scalpel/ScalpelEditorProvider.java new file mode 100644 index 00000000..1c14fec8 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ScalpelEditorProvider.java @@ -0,0 +1,123 @@ +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor; +import burp.api.montoya.ui.editor.extension.HttpRequestEditorProvider; +import burp.api.montoya.ui.editor.extension.HttpResponseEditorProvider; +import java.lang.ref.WeakReference; +import java.util.LinkedList; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import javax.swing.SwingUtilities; + +/** + Provides a new ScalpelProvidedEditor object for editing HTTP requests or responses. +

    Calls Python scripts to initialize the editor and update the requests or responses. +*/ +public class ScalpelEditorProvider + implements HttpRequestEditorProvider, HttpResponseEditorProvider { + + /** + The MontoyaApi object used to interact with Burp Suite. + */ + private final MontoyaApi API; + + /** + The ScalpelExecutor object used to execute Python scripts. + */ + private final ScalpelExecutor executor; + + private LinkedList> editorsRefs = new LinkedList<>(); + + /** + Constructs a new ScalpelEditorProvider object with the specified MontoyaApi object and ScalpelExecutor object. + + @param API The MontoyaApi object to use. + @param executor The ScalpelExecutor object to use. + */ + public ScalpelEditorProvider(MontoyaApi API, ScalpelExecutor executor) { + this.API = API; + this.executor = executor; + } + + /** + Provides a new ExtensionProvidedHttpRequestEditor object for editing an HTTP request. + + @param creationContext The EditorCreationContext object containing information about the request editor. + @return A new ScalpelProvidedEditor object for editing the HTTP request. + */ + @Override + public ExtensionProvidedHttpRequestEditor provideHttpRequestEditor( + EditorCreationContext creationContext + ) { + final ScalpelEditorTabbedPane editor = new ScalpelEditorTabbedPane( + API, + creationContext, + EditorType.REQUEST, + this, + executor + ); + editorsRefs.add(new WeakReference<>(editor)); + return editor; + } + + /** + Provides a new ExtensionProvidedHttpResponseEditor object for editing an HTTP response. + + @param creationContext The EditorCreationContext object containing information about the response editor. + @return A new ScalpelProvidedEditor object for editing the HTTP response. + */ + @Override + public ExtensionProvidedHttpResponseEditor provideHttpResponseEditor( + EditorCreationContext creationContext + ) { + final ScalpelEditorTabbedPane editor = new ScalpelEditorTabbedPane( + API, + creationContext, + EditorType.RESPONSE, + this, + executor + ); + editorsRefs.add(new WeakReference<>(editor)); + return editor; + } + + private void forceGarbageCollection() { + final WeakReference ref = new WeakReference<>(new Object()); + // The above object may now be garbage collected. + + while (ref.get() != null) { + System.gc(); + } + } + + public synchronized void resetEditors() { + ScalpelLogger.debug("Resetting editors..."); + // Destroy all unused editors to avoid useless expensive callbacks. + // TODO: Improve this by using ReferenceQueue + // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ref/ReferenceQueue.html + // https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/ref/Reference.html#isEnqueued() + forceGarbageCollection(); + + // Clean the list + this.editorsRefs = + this.editorsRefs.parallelStream() + .filter(weakRef -> weakRef.get() != null) + .collect(Collectors.toCollection(LinkedList::new)); + + SwingUtilities.invokeLater(() -> { + this.editorsRefs.parallelStream() + .map(WeakReference::get) + .map(ScalpelEditorTabbedPane::recreateEditorsAsync) + .forEach(CompletableFuture::join); + + ScalpelLogger.info("Editors reset."); + }); + } + + public CompletableFuture resetEditorsAsync() { + return Async.run(this::resetEditors); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ScalpelEditorTabbedPane.java b/scalpel/src/main/java/lexfo/scalpel/ScalpelEditorTabbedPane.java new file mode 100644 index 00000000..4c35f30c --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ScalpelEditorTabbedPane.java @@ -0,0 +1,653 @@ +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor; +import java.awt.Component; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.JTabbedPane; +import javax.swing.UIManager; +import javax.swing.plaf.basic.BasicTabbedPaneUI; +import lexfo.scalpel.ScalpelExecutor.CallableData; +import lexfo.scalpel.editors.AbstractEditor; +import lexfo.scalpel.editors.IMessageEditor; +import lexfo.scalpel.editors.ScalpelBinaryEditor; +import lexfo.scalpel.editors.ScalpelDecimalEditor; +import lexfo.scalpel.editors.ScalpelHexEditor; +import lexfo.scalpel.editors.ScalpelOctalEditor; +import lexfo.scalpel.editors.ScalpelRawEditor; + +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.html +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.html +/** + Provides an UI text editor component for editing HTTP requests or responses. + Calls Python scripts to initialize the editor and update the requests or responses. +*/ +public class ScalpelEditorTabbedPane + implements + ExtensionProvidedHttpRequestEditor, + ExtensionProvidedHttpResponseEditor { + + /** + The editor swing UI component. + */ + private final JTabbedPane pane = new JTabbedPane(); + /** + The HTTP request or response being edited. + */ + private HttpRequestResponse _requestResponse; + + /** + The Montoya API object. + */ + private final MontoyaApi API; + + /** + The editor creation context. + */ + private final EditorCreationContext ctx; + + /** + The editor type (REQUEST or RESPONSE). + */ + private final EditorType type; + + /** + The editor ID. (unused) + */ + private final String id; + + /** + The editor provider that instantiated this editor. (unused) + */ + private final ScalpelEditorProvider provider; + + /** + The executor responsible for interacting with Python. + */ + private final ScalpelExecutor executor; + + private final ArrayList editors = new ArrayList<>(); + + /** + req_edit_ or res_edit + */ + private final String hookPrefix; + + /** + req_edit_in_ or res_edit_in_ + */ + private final String hookInPrefix; + + /** + req_edit_out_ or res_edit_out_ + */ + private final String hookOutPrefix; + + /** + Constructs a new Scalpel editor. + + @param API The Montoya API object. + @param creationContext The EditorCreationContext object containing information about the editor. + @param type The editor type (REQUEST or RESPONSE). + @param provider The ScalpelEditorProvider object that instantiated this editor. + @param executor The executor to use. + */ + ScalpelEditorTabbedPane( + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorProvider provider, + ScalpelExecutor executor + ) { + // Keep a reference to the Montoya API + this.API = API; + + // Associate the editor with an unique ID (obsolete) + this.id = UUID.randomUUID().toString(); + + // Keep a reference to the provider. + this.provider = provider; + + // Store the context (e.g.: Tool origin, HTTP message type,...) + this.ctx = creationContext; + + // Reference the executor to be able to call Python callbacks. + this.executor = executor; + + // Set the editor type (REQUEST or RESPONSE). + this.type = type; + + this.hookPrefix = + ( + type == EditorType.REQUEST + ? Constants.REQ_EDIT_PREFIX + : Constants.RES_EDIT_PREFIX + ); + + // req_edit_in / res_edit_in + this.hookInPrefix = this.hookPrefix + Constants.IN_SUFFIX; + // req_edit_out / res_edit_out + this.hookOutPrefix = this.hookPrefix + Constants.OUT_SUFFIX; + + try { + this.recreateEditors(); + ScalpelLogger.debug( + "Successfully initialized ScalpelProvidedEditor for " + + type.name() + ); + } catch (Throwable e) { + // Log the stack trace. + ScalpelLogger.error("Couldn't instantiate new editor:"); + ScalpelLogger.logStackTrace(e); + + // Throw the error again. + throw new RuntimeException(e); + } + } + + private int getTabNameOffsetInHookName(String hookName) { + return hookName.startsWith(hookInPrefix) + ? hookInPrefix.length() + : hookOutPrefix.length(); + } + + private String getHookSuffix(String hookName) { + return hookName + .substring(getTabNameOffsetInHookName(hookName)) + .replaceFirst("^_", ""); + } + + private String getHookPrefix(String hookName) { + return hookName.substring(0, getTabNameOffsetInHookName(hookName)); + } + + public static final Map> modeToEditorMap = Map.of( + "raw", + ScalpelRawEditor.class, + "hex", + ScalpelHexEditor.class, + "octal", + ScalpelOctalEditor.class, + "decimal", + ScalpelDecimalEditor.class, + "binary", + ScalpelBinaryEditor.class + ); + + /** + * A tab can be associated with at most two hooks + * (e.g req_edit_in and req_edit_out) + * + * This stores the informations related to only one hook and is later merged with the second hook information into a HookTabInfo + */ + private record PartialHookTabInfo( + String name, + String mode, + String direction + ) {} + + /** + * This stores all the informations required to create a tab. + * .directions contains the whole prefix and not just "in" or "out" + */ + private record HookTabInfo( + String name, // for req_edit_in_tab1 -> tab1 + String mode, // hex / raw + Set directions // re[qs]_edit_in / re[qs]_edit_out + ) {} + + private List getCallables() { + // List Python callbacks. + try { + return executor.getCallables(); + } catch (RuntimeException e) { + // This will fail if the script is invalid or empty. + ScalpelLogger.trace( + "recreateEditors(): Could not call get_callables" + ); + ScalpelLogger.trace(e.toString()); + return null; + } + } + + /** + * Retain hooks for editing a request / response and parses them. + * @param callables All the Python callable objects that were found. + * @return Parsed hook infos + */ + private Stream filterEditorHooks( + List callables + ) { + return callables + .parallelStream() + .filter(c -> + c.name().startsWith(this.hookInPrefix) || + c.name().startsWith(this.hookOutPrefix) + ) + .map(c -> + new PartialHookTabInfo( + this.getHookSuffix(c.name()), + c + .annotations() + .getOrDefault( + Constants.EDITOR_MODE_ANNOTATION_KEY, + Constants.DEFAULT_EDITOR_MODE + ), + this.getHookPrefix(c.name()) + ) + ); + } + + /** + * Takes all the hooks infos and merge the corresponding ones + * E.g: + * Given the hook req_edit_in_tab1 + * To create a tab, we need to know if req_edit_in_tab1 has a corresponding req_edit_out_tab1 + * The editor mode (raw or hex) must be taken from the req_edit_in_tab1 annotations (@edit("hex")) + * + * @param infos The hooks individual infos. + * @return The hook informations required to create a Scalpel editor tab. + */ + private Stream mergeHookTabInfo( + Stream infos + ) { + // Group the hooks individual infos by their tab name (as in req_edit_in_) + final Map> grouped = infos.collect( + Collectors.groupingBy(PartialHookTabInfo::name) + ); + + // Merge the grouped individual infos into a single object. + return grouped + .entrySet() + .parallelStream() + .map(entry -> { + final String name = entry.getKey(); + final List partials = entry.getValue(); + final Set directions = partials + .parallelStream() + .map(PartialHookTabInfo::direction) + .collect(Collectors.toSet()); + + // Discard the "out" hook editor mode and only account for the "in" hook. + final Optional inHook = partials + .parallelStream() + .filter(p -> p.direction().equals(this.hookInPrefix)) + .findFirst(); + + // inHook can be empty in the case where the user specified an "out" hook + // but not an "in" hook, which is a noop case, we handle this for safety. + final String mode = inHook + .map(PartialHookTabInfo::mode) + .orElse(Constants.DEFAULT_EDITOR_MODE); + + return new HookTabInfo(name, mode, directions); + }); + } + + /** + Recreates the editors tabs. + + Calls Python to get the tabs name. + */ + public synchronized void recreateEditors() { + // Destroy existing editors + this.pane.removeAll(); + this.editors.clear(); + + final List callables = getCallables(); + if (callables == null) { + return; + } + + // Retain only correct prefixes and parse hook name + final Stream hooks = filterEditorHooks(callables); + + // Merge the individual hooks infos + final Stream mergedTabInfo = mergeHookTabInfo(hooks); + + // Create the editors + mergedTabInfo.forEach(tabInfo -> { + ScalpelLogger.debug("Creating tab for " + tabInfo); + + // Get editor implementation corresponding to mode. + final Class dispatchedEditor = modeToEditorMap.getOrDefault( + tabInfo.mode(), + ScalpelRawEditor.class + ); + + final AbstractEditor editor; + try { + // There should be a better way to do this.. + final Constructor construcor = dispatchedEditor.getConstructor( + String.class, + Boolean.class, + MontoyaApi.class, + EditorCreationContext.class, + EditorType.class, + ScalpelEditorTabbedPane.class, + ScalpelExecutor.class + ); + + editor = + construcor.newInstance( + tabInfo.name(), + tabInfo.directions.contains(this.hookOutPrefix), // Read-only tab if no "out" hook. + API, + ctx, + type, + this, + executor + ); + } catch (Throwable ex) { + ScalpelLogger.fatal("FATAL: Invalid editor constructor"); + // Should never happen as long as the constructor has not been overriden by an abstract declaration. + throw new RuntimeException(ex); + } + ScalpelLogger.debug("Successfully created tab for " + tabInfo); + + this.editors.add(editor); + + if ( + this._requestResponse != null && + editor.setRequestResponseInternal(_requestResponse) + ) { + this.addEditorToDisplayedTabs(editor); + } + }); + } + + /** + Recreates the editors tabs asynchronously. + + Calls Python to get the tabs name. + + Might cause deadlocks or other weird issues if used in constructors directly called by Burp. + */ + public synchronized CompletableFuture recreateEditorsAsync() { + return Async.run(this::recreateEditors); + } + + /** + Returns the editor type (REQUEST or RESPONSE). + + @return The editor type (REQUEST or RESPONSE). + */ + public EditorType getEditorType() { + return type; + } + + /** + Returns the editor's unique ID. (unused) + @return The editor's unique ID. + */ + public String getId() { + return id; + } + + /** + Returns the editor's creation context. + @return The editor's creation context. + */ + public EditorCreationContext getCtx() { + return ctx; + } + + /** + Returns the Burp editor object. + @return The Burp editor object. + */ + public JTabbedPane getPane() { + return pane; + } + + /** + * Select the most suited editor for updating Burp message data. + * + * @return + */ + public IMessageEditor selectEditor() { + final IMessageEditor selectedEditor = editors.get( + pane.getSelectedIndex() + ); + if (selectedEditor.isModified()) { + return selectedEditor; + } + + // TODO: Mimic burp update behaviour. + final Stream modifiedEditors = editors + .stream() + .filter(IMessageEditor::isModified); + + return modifiedEditors.findFirst().orElse(selectedEditor); + } + + /** + * Returns the HTTP message being edited. + * @return The HTTP message being edited. + */ + public HttpMessage getMessage() { + // Ensure request response exists. + if (_requestResponse == null) { + return null; + } + + // Safely extract the message from the requestResponse. + return type == EditorType.REQUEST + ? _requestResponse.request() + : _requestResponse.response(); + } + + /** + * Creates a new HTTP message by passing the editor's contents through a Python callback. + * + * @return The new HTTP message. + */ + private HttpMessage processOutboundMessage() { + return selectEditor().processOutboundMessage(); + } + + /** + * Creates a new HTTP request by passing the editor's contents through a Python callback. + * (called by Burp) + * + * @return The new HTTP request. + */ + @Override + public HttpRequest getRequest() { + ScalpelLogger.trace("getRequest called"); + // Cast the generic HttpMessage interface back to it's concrete type. + return (HttpRequest) processOutboundMessage(); + } + + /** + * Creates a new HTTP response by passing the editor's contents through a Python callback. + * (called by Burp) + * + * @return The new HTTP response. + */ + @Override + public HttpResponse getResponse() { + ScalpelLogger.trace("getResponse called"); + // Cast the generic HttpMessage interface back to it's concrete type. + return (HttpResponse) processOutboundMessage(); + } + + /** + Returns the stored HttpRequestResponse. + + @return The stored HttpRequestResponse. + */ + public HttpRequestResponse getRequestResponse() { + return this._requestResponse; + } + + /** + * Adds the editor to the tabbed pane + * If the editor caption is blank, the displayed name will be the tab index. + * @param editor The editor to add + */ + private void addEditorToDisplayedTabs(IMessageEditor editor) { + final String displayedName; + if (editor.caption().isBlank()) { + // Make indexes start at 1 instead of 0 + displayedName = Integer.toString(this.pane.getTabCount() + 1); + } else { + displayedName = editor.caption(); + } + + final Component component = editor.uiComponent(); + pane.addTab(displayedName, component); + } + + /** + Sets the HttpRequestResponse to be edited. + (called by Burp) + + @param requestResponse The HttpRequestResponse to be edited. + */ + @Override + public void setRequestResponse(HttpRequestResponse requestResponse) { + ScalpelLogger.trace("TabbedPane: setRequestResponse()"); + + this._requestResponse = requestResponse; + + // Hide disabled tabs + this.pane.removeAll(); + editors + .parallelStream() + .filter(e -> e.setRequestResponseInternal(requestResponse)) + .forEach(this::addEditorToDisplayedTabs); + } + + /** + * Get the network informations associated with the editor + * + * Gets the HttpService from requestResponse and falls back to request if it is null + * + * @return An HttpService if found, else null + */ + public HttpService getHttpService() { + final HttpRequestResponse reqRes = this._requestResponse; + + // Ensure editor is initialized + if (reqRes == null) return null; + + // Check if networking infos are available in the requestRespone + if (reqRes.httpService() != null) { + return reqRes.httpService(); + } + + // Fall back to the initiating request + final HttpRequest req = reqRes.request(); + if (req != null) { + return req.httpService(); + } + + return null; + } + + /** + Determines whether the editor should be enabled for the provided HttpRequestResponse. + Also initializes the editor with Python callbacks output of the inputted HTTP message. + (called by Burp) + + @param requestResponse The HttpRequestResponse to be edited. + */ + @Override + public boolean isEnabledFor(HttpRequestResponse requestResponse) { + ScalpelLogger.trace("TabbedPane: isEnabledFor()"); + try { + return editors + .parallelStream() + .anyMatch(e -> e.isEnabledFor(requestResponse)); + } catch (Throwable e) { + ScalpelLogger.logStackTrace(e); + } + return false; + } + + /** + Returns the name of the tab. + (called by Burp) + + @return The name of the tab. + */ + @Override + public String caption() { + // Return the tab name. + return "Scalpel"; + } + + // Hide tab bar when there is only 1 tab + private void adjustTabBarVisibility() { + if (pane.getTabCount() == 1) { + pane.setUI( + new BasicTabbedPaneUI() { + @Override + protected int calculateTabAreaHeight( + int tabPlacement, + int horizRunCount, + int maxTabHeight + ) { + return 0; + } + } + ); + } else { + // Reset to the default UI if more than one tab is present + UIManager.getLookAndFeel().provideErrorFeedback(pane); + pane.updateUI(); + } + } + + /** + Returns the underlying UI component. + (called by Burp) + + @return The underlying UI component. + */ + @Override + public Component uiComponent() { + adjustTabBarVisibility(); + return pane; + } + + /** + Returns the selected data. + (called by Burp) + + @return The selected data. + */ + @Override + public Selection selectedData() { + return selectEditor().selectedData(); + } + + /** + Returns whether the editor has been modified. + (called by Burp) + + @return Whether the editor has been modified. + */ + @Override + public boolean isModified() { + return editors.stream().anyMatch(IMessageEditor::isModified); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ScalpelExecutor.java b/scalpel/src/main/java/lexfo/scalpel/ScalpelExecutor.java new file mode 100644 index 00000000..dcb133f5 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ScalpelExecutor.java @@ -0,0 +1,1294 @@ +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.handler.HttpRequestToBeSent; +import burp.api.montoya.http.handler.HttpResponseReceived; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Consumer; +import java.util.stream.Stream; +import jep.ClassEnquirer; +import jep.ClassList; +import jep.Interpreter; +import jep.JepConfig; +import jep.SubInterpreter; + +/** + * Responds to requested Python tasks from multiple threads through a task queue handled in a single sepearate thread. + * + *

    The executor is responsible for managing a single global Python interpreter + * for every script that's being executed. + * + *

    The executor itself is designed to be used concurrently by different threads. + * It provides a simple interface for submitting tasks to be executed by the script, + * and blocks each thread until the task has been completed, providing a thread-safe + * way to ensure that the script's state remains consistent. + * + *

    Tasks are submitted as function calls with optional arguments and keyword + * arguments. Each function call is executed in the script's global context, and + * the result of the function is returned to the JVM thread that submitted the + * task. + * + *

    The executor is capable of restarting the Python interpreter when the + * script file changes on disk. This ensures that any modifications made to the + * script are automatically loaded by the executor without requiring a manual + * restart of the extension. + * + */ +public class ScalpelExecutor { + + /** + * A custom ClassEnquirer for the Jep interpreter used by the script executor. + */ + private class CustomEnquirer implements ClassEnquirer { + + /** + * The base ClassEnquirer to use. + */ + private ClassList base; + + /** + * Constructs a new CustomEnquirer object. + */ + CustomEnquirer() { + this.base = ClassList.getInstance(); + } + + /** + * Gets the names of all the classes in a package. + * + * @param pkg the name of the package. + * @return an array of the names of the classes in the package. + */ + public String[] getClassNames(String pkg) { + return base.getClassNames(pkg); + } + + /** + * Gets the names of all the sub-packages of a package. + * + * @param p the name of the package. + * @return an array of the names of the sub-packages of the package. + */ + public String[] getSubPackages(String p) { + return base.getSubPackages(p); + } + + /** + * Determines whether a string represents a valid Java package. + * + * @param s the string to check. + * @return true if the string represents a valid Java package, false otherwise. + */ + public boolean isJavaPackage(String s) { + // https://github.com/ninia/jep/issues/347 + if (s.equals("lexfo") || s.equals("lexfo.scalpel")) { + return true; + } + return base.isJavaPackage(s); + } + } + + /** + * A class representing a task to be executed by the Scalpel script. + */ + private class Task { + + /** + * The name of the task. + */ + private String name; + + /** + * The arguments passed to the task. + */ + private Object[] args; + + /** + * Whether the task has been completed. (Used to break out of the awaitResult() loop in case of failure.) + */ + private Boolean finished = false; + + /** + * The keyword arguments passed to the task. + */ + private Map kwargs; + + /** + * An optional object containing the result of the task, if it has been completed. + */ + private Result result = Result.empty(); + + /** + * Constructs a new Task object. + * + * @param name the name of the task. + * @param args the arguments passed to the task. + * @param kwargs the keyword arguments passed to the task. + */ + public Task(String name, Object[] args, Map kwargs) { + this.name = name; + this.args = args; + this.kwargs = kwargs; + + ScalpelLogger.trace("Created task: " + name); + } + + /** + * Add the task to the queue and wait for it to be completed by the task thread. + * + * @return the result of the task. + */ + public synchronized Result await() { + // Log this before awaiting to debug potential deadlocks. + ScalpelLogger.trace("Awaiting task: " + name); + + // Acquire the lock on the Task object. + synchronized (this) { + // Ensure we return only when result has been set + // (apparently wait() might return even if notify hasn't been called for some weird software and hardware issues) + while ( + isEnabled && + (isRunnerAlive || isRunnerStarting) && + !isFinished() + ) { + // Wrap the wait in try/catch to handle InterruptedException. + try { + // Wait for the object to be notified. + this.wait(1000); + + if (!isFinished()) { + // Warn the user that a task is taking a long time. + ScalpelLogger.warn( + "Task " + name + " is still waiting..." + ); + } + } catch (InterruptedException e) { + // Log the error. + ScalpelLogger.error("Task " + name + " interrupted:"); + + // Log the stack trace. + ScalpelLogger.logStackTrace(e); + } + } + } + + ScalpelLogger.trace("Finished awaiting task: " + name); + // Return the awaited result. + return this.result; + } + + public Boolean isFinished() { + return finished; + } + + public synchronized void then(Consumer callback) { + Async.run(() -> this.await().ifSuccess(callback)); + } + + public synchronized void resolve(Object result) { + this.result = Result.success(result); + this.finished = true; + } + + public synchronized void reject() { + reject(Optional.empty()); + } + + public synchronized void reject(Throwable error) { + reject(Optional.of(error)); + } + + public synchronized void reject(Optional error) { + this.result = error.map(Result::error).orElse(Result.empty()); + this.finished = true; + } + } + + /** + * The MontoyaApi object to use for sending and receiving HTTP messages. + */ + private final MontoyaApi API; + + /** + * The path of the Scalpel script that will be passed to the framework. + */ + private Optional script = Optional.empty(); + + /** + * The path of the Scalpel framework that will be used to execute the script. + */ + private Optional framework = Optional.empty(); + + /** + * The task runner thread. + */ + private Thread runner; + + /** + * The Python task queue. + */ + private final Queue tasks = new LinkedBlockingQueue<>(); + + /** + * The timestamp of the last recorded modification to the script file. + */ + private long lastScriptModificationTimestamp = -1; + + /** + * The timestamp of the last recorded modification to the framework file. + */ + private long lastFrameworkModificationTimestamp = -1; + + private long lastConfigModificationTimestamp = -1; + + /** + * Flag indicating whether the task runner loop is running. + */ + private Boolean isRunnerAlive = false; + + private Boolean isRunnerStarting = true; + + private final Config config; + + private Optional editorProvider = Optional.empty(); + + private Boolean isEnabled; + + private final OutputStream pythonStdout = new OutputStream() { + @Override + public void write(int b) { + ConfigTab.pushCharToOutput(b, true); + } + }; + + private final OutputStream pythonStderr = new OutputStream() { + @Override + public void write(int b) { + ConfigTab.pushCharToOutput(b, false); + } + }; + + /** + * Constructs a new ScalpelExecutor object. + * + * @param API the MontoyaApi object to use for sending and receiving HTTP messages. + * @param config the Config object to use for getting the configuration values. + */ + public ScalpelExecutor(MontoyaApi API, Config config) { + // Store Montoya API object + this.API = API; + + // Keep a reference to the config + this.config = config; + + this.isEnabled = config.isEnabled(); + + // Create a File wrapper from the script path. + this.script = + Optional + .ofNullable(config.getUserScriptPath()) + .map(Path::toFile) + .filter(File::exists); + + this.framework = + Optional + .ofNullable(config.getFrameworkPath()) + .map(Path::toFile) + .filter(File::exists); + + this.lastConfigModificationTimestamp = config.getLastModified(); + this.framework.ifPresent(f -> + this.lastFrameworkModificationTimestamp = f.lastModified() + ); + this.script.ifPresent(s -> + this.lastScriptModificationTimestamp = s.lastModified() + ); + + // Launch task thread. + this.script.ifPresent(s -> this.runner = this.launchTaskRunner()); + } + + public boolean isEnabled() { + return this.isEnabled; + } + + public boolean isRunning() { + return this.isRunnerAlive; + } + + public boolean isStarting() { + return this.isRunnerStarting; + } + + public void enable() { + this.isEnabled = true; + } + + public void disable() { + this.isEnabled = false; + } + + /** + * Adds a new task to the queue of tasks to be executed by the script. + * + * @param name the name of the python function to be called. + * @param args the arguments to pass to the python function. + * @param kwargs the keyword arguments to pass to the python function. + * @param rejectOnReload reject the task when the runner is reloading. + * @return a Task object representing the added task. + */ + private Task addTask( + String name, + Object[] args, + Map kwargs, + boolean rejectOnReload + ) { + // Create task object. + final Task task = new Task(name, args, kwargs); + + synchronized (tasks) { + // Ensure the runner is alive. + if (isEnabled && (isRunnerAlive || isRunnerStarting)) { + // Queue the task. + tasks.add(task); + + // Release the runner's lock. + tasks.notifyAll(); + } else if (rejectOnReload) { + // The runner is dead, reject this task to avoid blocking Burp when awaiting. + task.reject(); + } + } + + // Return the queued or rejected task. + return task; + } + + /** + * Adds a new task to the queue of tasks to be executed by the script. + * + * @param name the name of the python function to be called. + * @param args the arguments to pass to the python function. + * @param kwargs the keyword arguments to pass to the python function. + * @return a Task object representing the added task. + */ + private Task addTask( + String name, + Object[] args, + Map kwargs + ) { + return addTask(name, args, kwargs, true); + } + + /** + * Awaits the result of a task. + * + * @param the type of the result of the task. + * @param name the name of the python function to be called. + * @param args the arguments to pass to the python function. + * @param kwargs the keyword arguments to pass to the python function. + * @return an Optional object containing the result of the task, or empty if the task was rejected or failed. + */ + @SuppressWarnings({ "unchecked" }) + private final Result awaitTask( + final String name, + final Object[] args, + final Map kwargs, + final Class expectedClass + ) { + // Queue a new task and await it's result. + final Result result = addTask(name, args, kwargs) + .await(); + + if (result.hasValue()) { + try { + final Object rawResult = result.getValue(); + final T castResult = (T) rawResult; + + ScalpelLogger.trace( + "Successfully cast " + + UnObfuscator.getClassName(rawResult) + + " to " + + UnObfuscator.getClassName(castResult) + ); + // Ensure the result can be cast to the expected type. + return Result.success(castResult); + } catch (ClassCastException e) { + ScalpelLogger.error("Failed casting " + name + "'s result:"); + // Log the error stack trace. + ScalpelLogger.logStackTrace(e); + + return Result.error(e); + } + } + // Convert the Result object + return Result.error(result.getError()); + } + + /** + * Checks if the script file has been modified since the last check. + * + * @return true if the script file has been modified since the last check, false otherwise. + */ + private Boolean hasScriptChanged() { + return script + .map(File::lastModified) + .map(m -> lastScriptModificationTimestamp != m) + .orElse(false); + } + + private Boolean hasConfigChanged() { + return config.getLastModified() != lastConfigModificationTimestamp; + } + + private void resetChangeIndicators() { + this.framework = + Optional + .ofNullable(config.getFrameworkPath()) + .map(Path::toFile) + .filter(File::exists); + + framework.ifPresent(f -> + lastFrameworkModificationTimestamp = f.lastModified() + ); + + this.script = + Optional + .ofNullable(config.getUserScriptPath()) + .map(Path::toFile) + .filter(File::exists); + + // Update the last modification date record. + script + .map(File::lastModified) + .ifPresent(f -> lastScriptModificationTimestamp = f); + + // Update the last modification date record. + lastConfigModificationTimestamp = config.getLastModified(); + } + + /** + * Checks if either the framework or user script file has been modified since the last check. + * + * @return true if either the framework or user script file has been modified since the last check, false otherwise. + */ + private Boolean mustReload() { + return ( + hasFrameworkChanged() || hasScriptChanged() || hasConfigChanged() + ); + } + + /** + * Checks if the framework file has been modified since the last check. + * + * @return true if the framework file has been modified since the last check, false otherwise. + */ + private final Boolean hasFrameworkChanged() { + return framework + .map(File::lastModified) + .map(m -> m != lastFrameworkModificationTimestamp) + .orElse(false); + } + + public void setEditorsProvider(ScalpelEditorProvider provider) { + this.editorProvider = Optional.of(provider); + provider.resetEditorsAsync(); + } + + public synchronized void notifyEventLoop() { + synchronized (tasks) { + tasks.notifyAll(); + } + } + + private synchronized void rejectAllTasks() { + synchronized (tasks) { + while (true) { + // Use polling and not foreach + clear to avoid race conditions (tasks being cleared but not rejected) + final Task task = tasks.poll(); + if (task == null) { + break; + } + task.reject(); + } + } + } + + private void processTask(final SubInterpreter interp, final Task task) { + ScalpelLogger.trace("Processing task: " + task.name); + try { + // Invoke Python function and get the returned value. + final Object pythonResult = interp.invoke( + task.name, + task.args, + task.kwargs + ); + + ScalpelLogger.trace("Executed task: " + task.name); + + if (pythonResult != null) { + task.resolve(pythonResult); + } else { + task.reject(); + } + } catch (Throwable e) { + task.reject(e); + + if (!e.getMessage().contains("Unable to find object")) { + ScalpelLogger.error("Error in task loop:"); + ScalpelLogger.logStackTrace(e); + } + } + + ScalpelLogger.trace("Processed task"); + + // Log the result value. + ScalpelLogger.trace(task.result.toString()); + } + + private void _innerTaskLoop(final SubInterpreter interp) + throws InterruptedException { + while (true) { + // Relaunch interpreter when files have changed (hot reload). + if (mustReload()) { + ScalpelLogger.info( + "Config or Python files have changed, reloading interpreter..." + ); + break; + } + + synchronized (tasks) { + ScalpelLogger.trace("Runner waiting for notifications."); + + if (!isEnabled) { + tasks.wait(1000); + continue; + } + + // Extract the oldest pending task from the queue. + final Task task = tasks.poll(); + + // Ensure a task was polled or poll again. + if (task == null) { + // Release the lock and wait for new tasks. + tasks.wait(1000); + continue; + } + + if (task.isFinished()) { + // if for some reason a task is already finished, just remove it from the list. + continue; + } + + processTask(interp, task); + + synchronized (task) { + // Wake threads awaiting the task. + task.notifyAll(); + ScalpelLogger.trace("Notified " + task.name); + } + + // Sleep the thread while there isn't any new tasks + tasks.wait(1000); + } + } + } + + private void safeCloseInterpreter(SubInterpreter interp) { + // KILL all threads that have been created in the Python script. + String shutdownCode = + """ + import threading + import ctypes + + def force_stop_thread(thread: threading.Thread): + if not thread.is_alive(): + return False # Thread already stopped or never started + + exc = SystemExit + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(thread.ident), ctypes.py_object(exc)) + if res == 0: + return False # Thread id not found + elif res > 1: + ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None) + return False # Throwable raise failure + return True # Thread was forced to exit + + for thread in threading.enumerate(): + if thread is not threading.current_thread(): # Avoid stopping the current thread + success = force_stop_thread(thread) + print(f"Stopping {thread.name} {'succeeded' if success else 'failed'}") + """; + + interp.exec(shutdownCode); + } + + // WARN: Declaring this method as synchronized cause deadlocks. + private void taskLoop() { + ScalpelLogger.debug("Starting task loop."); + + isRunnerStarting = true; + + SubInterpreter interp; + try { + interp = initInterpreter(); + } catch (Throwable e) { + interp = null; + ConfigTab.clearOutputs( + "Failed to load " + + script.map(File::getName).orElse("the selected script.") + ); + + // Log the error. + String trace = ScalpelLogger.exceptionToErrorMsg( + e, + "Failed to initialize interpreter" + ); + + // Check if Python itself is broken + if (trace.contains("No module named 'binascii'")) { + // This may happen if you messed with pyenv and installed Scalpel in a different Python version that the one in use at this time. + trace += + "\n/!\\ SOMETHING IS WRONG WITH YOUR PYTHON SETUP /!\\\n" + + "You may have mixed different Python installations when installling and using Scalpel" + + ", you may try re-installing Scalpel in your current Python environment"; + } + + ScalpelLogger.error(trace); + ConfigTab.putStringToOutput(trace, false); + } + + if (interp != null) { + isRunnerAlive = true; + isRunnerStarting = false; + + final String msg = + "Sucessfully loaded " + + script.map(File::getName).orElse("the selected script"); + + ConfigTab.clearOutputs(msg); + ScalpelLogger.info(msg); + + try { + _innerTaskLoop(interp); + } catch (Throwable e) { + // The task loop has crashed, log the stack trace. + ScalpelLogger.logStackTrace(e); + } + // Log the error. + ScalpelLogger.trace("Task loop has crashed"); + } else { + isRunnerAlive = false; + isRunnerStarting = false; + // The script couldn't be loaded, wait for it to change + this.resetChangeIndicators(); + rejectAllTasks(); + while (!mustReload()) { + rejectAllTasks(); + IO.sleep(100); + } + } + + isRunnerAlive = false; + isRunnerStarting = true; + + this.resetChangeIndicators(); + + if (interp != null) { + safeCloseInterpreter(interp); + } + + // Relaunch the task thread + this.runner = launchTaskRunner(); + } + + /** + * Launches the task runner thread. + * + * @return the launched thread. + */ + private Thread launchTaskRunner() { + // Instantiate the task runner thread. + final Thread thread = new Thread(this::taskLoop, "ScalpelRunnerLoop"); + + // Start the task runner thread. + thread.start(); + + // Force editor tabs recreation + // WARN: .resetEditors() depends on the runner loop, do not call it inside of it + this.editorProvider.ifPresent(ScalpelEditorProvider::resetEditors); + + // Return the running thread. + return thread; + } + + private Optional getDefaultIncludePath() { + final Path defaultVenv = Workspace + .getDefaultWorkspace() + .resolve(Workspace.VENV_DIR); + + try { + return Optional.of(Venv.getSitePackagesPath(defaultVenv)); + } catch (IOException e) { + ScalpelLogger.warn( + "Could not find a default include path for JEP (with venv " + + defaultVenv + + ")" + ); + ScalpelLogger.error( + "Could not find a default include path for JEP" + ); + ScalpelLogger.logStackTrace(e); + } + return Optional.empty(); + } + + /** + * Initializes the interpreter. + * + * @return the initialized interpreter. + */ + @SuppressWarnings({ "unchecked" }) + private SubInterpreter initInterpreter() { + try { + return framework + .map(file -> { + // Add a default include path so JEP can be loaded + final Optional defaultIncludePath = getDefaultIncludePath() + .map(Path::toString); + + final JepConfig jepConfig = new JepConfig() + .setClassEnquirer(new CustomEnquirer()) + .addIncludePaths( + defaultIncludePath.orElse(""), + RessourcesUnpacker.PYTHON_PATH.toString() + ) + .redirectStdout(pythonStdout) + .redirectStdErr(pythonStderr); + + // Instantiate a Python interpreter. + final SubInterpreter interp = new SubInterpreter(jepConfig); + + final HashMap burpEnv = new HashMap<>(10); + + // Make the Montoya API object accessible in Python + burpEnv.put("API", API); + + // Set the framework's filename to corresponding Python variable + // This isn't set by JEP, we have to do it ourselves. + interp.set("__file__", file.getAbsolutePath()); + + // Set the path to the user script that will define the actual callbacks. + burpEnv.put( + "user_script", + script + .orElseThrow(() -> + new RuntimeException( + "The selected script could not be created or was deleted." + ) + ) + .getAbsolutePath() + ); + + burpEnv.put("framework", file.getAbsolutePath()); + + // Pass the selected venv path so it can be activated by the framework. + burpEnv.put( + "venv", + config.getSelectedWorkspacePath() + + File.separator + + Workspace.VENV_DIR + ); + + interp.set("__scalpel__", burpEnv); + + // Run the framework (wraps the user script) + interp.runScript(file.getAbsolutePath()); + + // Check if get_callables can be called + final List> res = (List>) interp.invoke( + Constants.GET_CB_NAME + ); + + if (res == null) { + throw new RuntimeException( + "Failed to call get_callables" + ); + } + + // Don't run the event loop when no hooks are implemented + final Boolean hasValidHooks = res + .stream() + .map(m -> (String) m.get("name")) + .anyMatch(c -> + Constants.VALID_HOOK_PREFIXES + .stream() + .anyMatch(c::startsWith) + ); + + if (!hasValidHooks) { + throw new RuntimeException( + "No hooks were found.\n" + + "In your Python script, you should implement at least one of the following functions:\n" + + "request, response, req_edit_in, req_edit_out" + + "\n" + + "See the documentation for more information. " + + "(Link in \"Help\" tab)\n" + ); + } + + // Return the initialized interpreter. + return interp; + }) + .orElseThrow(() -> + new RuntimeException( + "The Python entrypoint at ~/.scalpel/extracted/_framework.py has been removed, please restart BurpSuite" + ) + ); + } catch (Throwable e) { + ScalpelLogger.error("Failed to instantiate interpreter:"); + ScalpelLogger.logStackTrace(e); + throw e; + } + } + + /** + * Evaluates the given script and returns the output. + * + * @param scriptContent the script to evaluate. + * @return the output of the script. + */ + public String[] evalAndCaptureOutput(String scriptContent) { + try (Interpreter interp = initInterpreter()) { + // Running Python instructions on the fly. + // https://github.com/t2y/jep-samples/blob/master/src/HelloWorld.java + interp.exec( + """ + from io import StringIO + import sys + temp_out = StringIO() + temp_err = StringIO() + sys.stdout = temp_out + sys.stderr = temp_err + """ + ); + Optional exceptionMessage = Optional.empty(); + try { + interp.exec(scriptContent); + } catch (Throwable e) { + final String stackTrace = Arrays + .stream(e.getStackTrace()) + .map(StackTraceElement::toString) + .reduce((a, b) -> a + "\n" + b) + .orElse("No stack trace."); + + final String msgAndTrace = e.getMessage() + "\n" + stackTrace; + + exceptionMessage = Optional.of(msgAndTrace); + } + interp.exec("captured_out = temp_out.getvalue()"); + interp.exec("captured_err = temp_err.getvalue()"); + + final String capturedOut = (String) interp.getValue("captured_out"); + final String capturedErr = (String) interp.getValue( + "captured_err" + ) + + exceptionMessage.map(msg -> "\n\n" + msg).orElse(""); + ScalpelLogger.all( + String.format( + "Executed:\n%s\nOutput:\n%s\nErr:%s\n", + scriptContent, + capturedOut, + capturedErr, + null + ) + ); + return new String[] { capturedOut, capturedErr }; + } + } + + /** + * Returns the name of the corresponding Python callback for the given message intercepted by Proxy. + * + * @param the type of the message. + * @param msg the message to get the callback name for. + * @return the name of the corresponding Python callback. + */ + private static final String getMessageCbName( + T msg + ) { + if ( + msg instanceof HttpRequest || msg instanceof HttpRequestToBeSent + ) return Constants.FRAMEWORK_REQ_CB_NAME; + if ( + msg instanceof HttpResponse || msg instanceof HttpResponseReceived + ) return Constants.FRAMEWORK_RES_CB_NAME; + throw new RuntimeException("Passed wrong type to geMessageCbName"); + } + + /** + * Calls the corresponding Python callback for the given message intercepted by Proxy. + * + * @param the type of the message. + * @param msg the message to call the callback for. + * @return the result of the callback. + */ + @SuppressWarnings({ "unchecked" }) + public Result callIntercepterHook( + T msg, + HttpService service + ) { + // Call the corresponding Python callback and add a debug HTTP header. + return safeJepInvoke( + getMessageCbName(msg), + new Object[] { msg, service }, + Map.of(), + (Class) msg.getClass() + ); + } + + /** + * Returns the name of the corresponding Python callback for the given tab. + * + * @param tabName the name of the tab. + * @param isRequest whether the tab is a request tab. + * @param isInbound whether the callback is use to modify the request back or update the editor's content. + * @return the name of the corresponding Python callback. + */ + private static final String getEditorCallbackName( + Boolean isRequest, + Boolean isInbound + ) { + // Either req_ or res_ depending if it is a request or a response. + final String editPrefix = isRequest + ? Constants.FRAMEWORK_REQ_EDIT_PREFIX + : Constants.FRAMEWORK_RES_EDIT_PREFIX; + + // Either in_ or out_ depending on context. + final String directionPrefix = isInbound + ? Constants.IN_SUFFIX + : Constants.OUT_SUFFIX; + + // Concatenate the prefixes + return editPrefix + directionPrefix; + } + + /** + * Calls the given Python function with the given arguments and keyword arguments. + * + * @param the expected class of the returned value. + * @param name the name of the Python function to call. + * @param args the arguments to pass to the function. + * @param kwargs the keyword arguments to pass to the function. + * @param expectedClass the expected class of the returned value. + * @return the result of the function call. + */ + public synchronized Result safeJepInvoke( + String name, + Object[] args, + Map kwargs, + Class expectedClass + ) { + // Create a task and await the result. + return awaitTask(name, args, kwargs, expectedClass); + } + + /** + * Calls the given Python function with the given argument. + * + * @param the expected class of the returned value. + * @param name the name of the Python function to call. + * @param arg the argument to pass to the function. + * @param expectedClass the expected class of the returned value. + * @return the result of the function call. + */ + + public Result safeJepInvoke( + String name, + Object arg, + Class expectedClass + ) { + // Call base safeJepInvoke with a single argument and a logger as default kwarg. + return safeJepInvoke( + name, + new Object[] { arg }, + Map.of(), + expectedClass + ); + } + + /** + * Calls the given Python function without any argument. + * + * @param the expected class of the returned value. + * @param name the name of the Python function to call. + * @param arg the argument to pass to the function. + * @param expectedClass the expected class of the returned value. + * @return the result of the function call. + */ + + public Result safeJepInvoke( + String name, + Class expectedClass + ) { + return safeJepInvoke(name, new Object[] {}, Map.of(), expectedClass); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param the expected class of the returned value. + * @param params the parameters to pass to the callback. + * @param isRequest whether the tab is a request tab. + * @param isInbound whether the callback is use to modify the request back or update the editor's content. + * @param tabName the name of the tab. + * @param expectedClass the expected class of the returned value. + * @return the result of the callback. + */ + public Result callEditorHook( + Object[] params, + Boolean isRequest, + Boolean isInbound, + String tabName, + Class expectedClass + ) { + String suffix = tabName.isEmpty() ? tabName : "_" + tabName; + + // Call safeJepInvoke with the corresponding function name + return safeJepInvoke( + getEditorCallbackName(isRequest, isInbound), + params, + Map.of("callback_suffix", suffix), + expectedClass + ); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param the expected class of the returned value. + * @param param the parameter to pass to the callback. + * @param isRequest whether the tab is a request tab. + * @param isInbound whether the callback is use to modify the request back or update the editor's content. + * @param tabName the name of the tab. + * @param expectedClass the expected class of the returned value. + * @return the result of the callback. + */ + public Result callEditorHook( + Object param, + HttpService service, + Boolean isRequest, + Boolean isInbound, + String tabName, + Class expectedClass + ) { + // Call base method with a single parameter. + return callEditorHook( + new Object[] { param, service }, + isRequest, + isInbound, + tabName, + expectedClass + ); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param msg the message to pass to the callback. + * @param isInbound whether the callback is use to modify the request back or update the editor's content. + * @param tabName the name of the tab. + * @return the result of the callback. + */ + public Result callEditorHook( + HttpMessage msg, + HttpService service, + Boolean isInbound, + String tabName + ) { + return callEditorHook( + msg, + service, + msg instanceof HttpRequest, + isInbound, + tabName, + byte[].class + ) + .map(ByteArray::byteArray); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param msg the message to pass to the callback. + * @param byteArray the byte array to pass to the callback (editor content). + * @param isInbound whether the callback is use to modify the request back or update the editor's content. + * @param tabName the name of the tab. + * @return the result of the callback. + */ + public Result callEditorHook( + HttpMessage msg, + HttpService service, + ByteArray byteArray, + Boolean isInbound, + String tabName + ) { + return callEditorHook( + new Object[] { + msg, + service, + PythonUtils.toPythonBytes(byteArray.getBytes()), + }, + msg instanceof HttpRequest, + isInbound, + tabName, + byte[].class + ) + .map(ByteArray::byteArray); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param req the message to pass to the callback. + * @param byteArray the byte array to pass to the callback (editor content). + * @param tabName the name of the tab. + * @return the result of the callback. + */ + public Result callEditorHookInRequest( + HttpRequest req, + HttpService service, + String tabName + ) { + return callEditorHook( + new Object[] { req, service }, + req instanceof HttpRequest, + true, + tabName, + byte[].class + ) + .map(ByteArray::byteArray); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param res the message to pass to the callback. + * @param byteArray the byte array to pass to the callback (editor content). + * @param tabName the name of the tab. + * @return the result of the callback. + */ + public Result callEditorHookInResponse( + HttpResponse res, + HttpRequest req, + HttpService service, + String tabName + ) { + return callEditorHook( + new Object[] { res, req, service }, + false, + true, + tabName, + byte[].class + ) + .map(ByteArray::byteArray); + } + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param msg the message to pass to the callback. + * @param byteArray the byte array to pass to the callback (editor content). + * @param tabName the name of the tab. + * @return the result of the callback. + */ + public Result callEditorHookOutRequest( + HttpRequest req, + HttpService service, + ByteArray byteArray, + String tabName + ) { + return callEditorHook( + new Object[] { + req, + service, + PythonUtils.toPythonBytes(byteArray.getBytes()), + }, + true, + false, + tabName, + HttpRequest.class + ); + } + + // TODO: update docstrings + + /** + * Calls the corresponding Python callback for the given tab. + * + * @param msg the message to pass to the callback. + * @param byteArray the byte array to pass to the callback (editor content). + * @param tabName the name of the tab. + * @return the result of the callback. + */ + public Result callEditorHookOutResponse( + HttpResponse res, + HttpRequest req, + HttpService service, + ByteArray byteArray, + String tabName + ) { + return callEditorHook( + new Object[] { + res, + req, + service, + PythonUtils.toPythonBytes(byteArray.getBytes()), + }, + false, + false, + tabName, + HttpResponse.class + ); + } + + public record CallableData( + String name, + HashMap annotations + ) {} + + @SuppressWarnings({ "unchecked" }) + public List getCallables() throws RuntimeException { + // TODO: Memoize this + // Jep doesn't offer any way to list functions, so we have to implement it Python side. + // Python returns ~ [{"name": , "annotations": },...] + return this.safeJepInvoke(Constants.GET_CB_NAME, List.class) + .map(l -> (List>) l) + .map(List::stream) + .map(s -> + s.map(c -> + new CallableData( + (String) c.get("name"), + (HashMap) c.get("annotations") + ) + ) + ) + .map(Stream::toList) + .getValue(); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ScalpelHttpRequestHandler.java b/scalpel/src/main/java/lexfo/scalpel/ScalpelHttpRequestHandler.java new file mode 100644 index 00000000..d137cdd5 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ScalpelHttpRequestHandler.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023. PortSwigger Ltd. All rights reserved. + * + * This code may be used to extend the functionality of Burp Suite Community Edition + * and Burp Suite Professional, provided that this usage does not violate the + * license terms for those products. + */ + +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.handler.*; +import burp.api.montoya.http.message.requests.HttpRequest; +import java.util.Optional; +import lexfo.scalpel.components.ErrorPopup; + +/** + Handles HTTP requests and responses. +*/ +public class ScalpelHttpRequestHandler implements HttpHandler { + + private final MontoyaApi API; + private final ErrorPopup popup; + /** + The ScalpelExecutor object used to execute Python scripts. + */ + private final ScalpelExecutor executor; + + /** + Constructs a new ScalpelHttpRequestHandler object with the specified MontoyaApi object and ScalpelExecutor object. + @param API The MontoyaApi object to use. + @param editorProvider The ScalpelEditorProvider object to use. + @param executor The ScalpelExecutor object to use. + */ + public ScalpelHttpRequestHandler( + MontoyaApi API, + ScalpelEditorProvider editorProvider, + ScalpelExecutor executor + ) { + // Reference the executor to be able to call Python callbacks. + this.executor = executor; + this.API = API; + this.popup = new ErrorPopup(API); + } + + /** + Handles HTTP requests. + @param httpRequestToBeSent The HttpRequestToBeSent object containing information about the HTTP request. + @return A RequestToBeSentAction object containing information about how to handle the HTTP request. + */ + @Override + public RequestToBeSentAction handleHttpRequestToBeSent( + HttpRequestToBeSent httpRequestToBeSent + ) { + // Call the request() Python callback + final Result newReq = executor.callIntercepterHook( + httpRequestToBeSent, + httpRequestToBeSent.httpService() + ); + + newReq.ifError(e -> { + final String title = + "Error in request() hook (" + + httpRequestToBeSent.toolSource().toolType().toolName() + + "): " + + httpRequestToBeSent.url(); + ConfigTab.putStringToOutput( + ScalpelLogger.exceptionToErrorMsg(e, title), + false + ); + + if ( + Config.getInstance().getDisplayProxyErrorPopup().equals("True") + ) { + final String errorMsg = ScalpelLogger.exceptionToErrorMsg( + e, + title + ); + popup.addError(errorMsg); + } + }); + + // Return the modified request when requested, else return the original. + return RequestToBeSentAction.continueWith( + newReq.orElse(httpRequestToBeSent) + ); + } + + /** + Handles HTTP responses. + @param httpResponseReceived The HttpResponseReceived object containing information about the HTTP response. + @return A ResponseReceivedAction object containing information about how to handle the HTTP response. + */ + @Override + public ResponseReceivedAction handleHttpResponseReceived( + HttpResponseReceived httpResponseReceived + ) { + // Get the network info form the initiating request. + final HttpService service = Optional + .ofNullable(httpResponseReceived.initiatingRequest()) + .map(HttpRequest::httpService) + .orElse(null); + + // Call the request() Python callback + final Result newRes = executor.callIntercepterHook( + httpResponseReceived, + service + ); + + newRes.ifError(e -> { + final String title = + "Error in response() hook (" + + httpResponseReceived.toolSource().toolType().toolName() + + "): " + + httpResponseReceived.initiatingRequest().url(); + ConfigTab.putStringToOutput( + ScalpelLogger.exceptionToErrorMsg(e, title), + false + ); + + if ( + Config.getInstance().getDisplayProxyErrorPopup().equals("True") + ) { + final String errorMsg = ScalpelLogger.exceptionToErrorMsg( + e, + title + ); + popup.addError(errorMsg); + } + }); + + // Return the modified request when requested, else return the original. + return ResponseReceivedAction.continueWith( + newRes.orElse(httpResponseReceived) + ); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/ScalpelLogger.java b/scalpel/src/main/java/lexfo/scalpel/ScalpelLogger.java new file mode 100644 index 00000000..2297a03d --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/ScalpelLogger.java @@ -0,0 +1,257 @@ +package lexfo.scalpel; + +import burp.api.montoya.logging.Logging; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Provides methods for logging messages to the Burp Suite output and standard streams. + */ +public class ScalpelLogger { + + /** + * Log levels used to filtrate logs by weight + * Useful for debugging. + */ + public enum Level { + TRACE(1), + DEBUG(2), + INFO(3), + WARN(4), + ERROR(5), + FATAL(6), + ALL(7); + + public static final String[] names = Arrays + .stream(Level.values()) + .map(Enum::name) + .toArray(String[]::new); + + public static final Map nameToLevel = Arrays + .stream(Level.values()) + .collect( + Collectors.toUnmodifiableMap(Enum::name, Function.identity()) + ); + + private int value; + + private Level(int value) { + this.value = value; + } + + public int value() { + return value; + } + } + + private static Logging logger = null; + + /** + * Set the Burp logger instance to use. + * @param logger + */ + public static void setLogger(Logging logging) { + ScalpelLogger.logger = logging; + } + + /** + * Configured log level + */ + private static Level loggerLevel = Level.INFO; + + /** + * Logs the specified message to the Burp Suite output and standard output at the TRACE level. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void trace(String msg) { + log(Level.TRACE, msg); + } + + /** + * Logs the specified message to the Burp Suite output and standard output at the DEBUG level. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void debug(String msg) { + log(Level.DEBUG, msg); + } + + /** + * Logs the specified message to the Burp Suite output and standard output at the INFO level. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void info(String msg) { + log(Level.INFO, msg); + } + + /** + * Logs the specified message to the Burp Suite output and standard output at the WARN level. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void warn(String msg) { + log(Level.WARN, msg); + } + + /** + * Logs the specified message to the Burp Suite output and standard output at the FATAL level. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void fatal(String msg) { + log(Level.FATAL, msg); + } + + /** + * Logs the specified message to the Burp Suite output and standard output. + * + * @param logger The Logging object to use. + * @param level The log level. + * @param msg The message to log. + */ + public static void log(Level level, String msg) { + if (level.value >= Level.DEBUG.value) { + ConfigTab.appendToDebugInfo(msg); + } + + if (logger == null || loggerLevel.value() > level.value()) { + return; + } + + switch (level) { + case ERROR: + error(msg); + break; + case FATAL: + System.err.println(msg); + logger.logToError(msg); + logger.raiseCriticalEvent(msg); + ConfigTab.putStringToOutput(msg, false); + break; + default: + System.out.println(msg); + logger.logToOutput(msg); + } + } + + /** + * Logs the specified message to the Burp Suite output and standard output at the TRACE level. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void log(String msg) { + log(Level.TRACE, msg); + } + + /** + * Logs the specified message to the Burp Suite error output and standard error. + * + * @param logger The Logging object to use. + * @param msg The message to log. + */ + public static void error(String msg) { + System.err.println(msg); + if (logger != null) { + logger.logToError(msg); + logger.raiseErrorEvent(msg); + } + } + + private static String stackTraceToString(StackTraceElement[] elems) { + return Arrays + .stream(elems) + .map(StackTraceElement::toString) + .reduce("", (first, second) -> first + "\n" + second); + } + + public static String exceptionToErrorMsg(Throwable throwed, String title) { + return ( + "ERROR:\n" + + title + + "\n" + + throwed.toString() + + stackTraceToString(throwed.getStackTrace()) + ); + } + + /** + * Logs the specified throwable stack trace to the Burp Suite error output and standard error. + * + * @param logger The Logging object to use. + * @param throwed The throwable to log. + */ + public static void logStackTrace(Throwable throwed) { + error(exceptionToErrorMsg(throwed, "")); + } + + public static void logFatalStackTrace(Throwable throwed) { + final String msg = exceptionToErrorMsg( + throwed, + "A fatal error occured" + ); + + // Log in both Output and Error tabs, to avoid confusing the user when install fails + all(msg); + error(msg); + } + + /** + * Logs the specified throwable stack trace to the Burp Suite error output and standard error. + * + * @param title title to display before the stacktrace + * @param logger The Logging object to use. + * @param throwed The throwable to log. + */ + public static void logStackTrace(String title, Throwable throwed) { + error(exceptionToErrorMsg(throwed, title)); + } + + /** + * Logs the current thread stack trace to the Burp Suite error output and standard error. + * + * @param logger The Logging object to use. + */ + public static void logStackTrace() { + error(stackTraceToString((Thread.currentThread().getStackTrace()))); + } + + /** + * Logs the current thread stack trace to either the Burp Suite output and standard output or the Burp Suite error output and standard error. + * + * @param logger The Logging object to use. + * @param error Whether to log to the error output or not. + */ + public static void logStackTrace(Boolean error) { + final String msg = stackTraceToString( + Thread.currentThread().getStackTrace() + ); + + if (error) { + error(msg); + } else { + logger.logToOutput(msg); + } + } + + public static void all(String msg) { + log(Level.ALL, msg); + } + + public static void setLogLevel(Level level) { + loggerLevel = level; + } + + public static void setLogLevel(String level) { + loggerLevel = Level.nameToLevel.getOrDefault(level, loggerLevel); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Terminal.java b/scalpel/src/main/java/lexfo/scalpel/Terminal.java new file mode 100644 index 00000000..e8ca2ef0 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Terminal.java @@ -0,0 +1,231 @@ +// https://raw.githubusercontent.com/JetBrains/jediterm/7e42fc1261ffd0b593557afa71851f1d1df76804/JediTerm/src/main/java/com/jediterm/example/BasicTerminalShellExample.java +package lexfo.scalpel; + +import burp.api.montoya.ui.Theme; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jediterm.pty.PtyProcessTtyConnector; +import com.jediterm.terminal.TerminalColor; +import com.jediterm.terminal.TextStyle; +import com.jediterm.terminal.TtyConnector; +import com.jediterm.terminal.emulator.ColorPalette; +import com.jediterm.terminal.ui.JediTermWidget; +import com.jediterm.terminal.ui.UIUtil; +import com.jediterm.terminal.ui.settings.DefaultSettingsProvider; +import com.jediterm.terminal.ui.settings.SettingsProvider; +import com.pty4j.PtyProcess; +import com.pty4j.PtyProcessBuilder; +import java.awt.Dimension; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +public class Terminal { + + private static SettingsProvider createSettingsProvider(Theme theme) { + return new DefaultSettingsProvider() { + @Override + public TextStyle getDefaultStyle() { + return theme == Theme.DARK + ? new TextStyle(TerminalColor.WHITE, TerminalColor.BLACK) + : new TextStyle(TerminalColor.BLACK, TerminalColor.WHITE); + } + + @Override + public ColorPalette getTerminalColorPalette() { + return theme == Theme.DARK + ? Palette.DARK_PALETTE + : Palette.LIGHT_PALETTE; + } + }; + } + + private static JediTermWidget createTerminalWidget( + Theme theme, + String venvPath, + Optional cwd, + Optional cmd + ) { + final JediTermWidget widget = new JediTermWidget( + createSettingsProvider(theme) + ); + widget.setTtyConnector( + createTtyConnector(venvPath, Optional.empty(), cwd, cmd) + ); + widget.start(); + return widget; + } + + public static String escapeshellarg(String str) { + if (UIUtil.isWindows) { + // Handle cmd.exe + // Characters to be escaped: & | < > ^ " and space + final String specialChars = "&|<>^\" "; + + // Enclose in double quotes and escape necessary characters + return ( + "\"" + + Stream + .of(specialChars.split("")) + .reduce(str, (s, ch) -> s.replace(ch, "^" + ch)) + + "\"" + ); + } + // Handle posix-y shell + return "'" + str.replace("'", "'\\''") + "'"; + } + + private static String dumps(Object obj) throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(obj); + } + + /** + * Creates a TtyConnector that will run a shell in the virtualenv. + * + * @param venvPath The path to the virtualenv. + * @return The TtyConnector. + */ + public static TtyConnector createTtyConnector(String venvPath) { + return createTtyConnector( + venvPath, + Optional.empty(), + Optional.empty(), + Optional.empty() + ); + } + + /** + * Creates a TtyConnector that will run a shell in the virtualenv. + * + * @param workspacePath The path to the virtualenv. + * @return The TtyConnector. + */ + protected static TtyConnector createTtyConnector( + String workspacePath, + Optional ttyDimension, + Optional cwd, + Optional cmd + ) { + Map env = System.getenv(); + final String[] commandToRun; + + final String sep = File.separator; + final String binDir = UIUtil.isWindows ? "Scripts" : "bin"; + final String activatePath = + workspacePath + + sep + + Workspace.VENV_DIR + + sep + + binDir + + sep + + "activate"; + + ScalpelLogger.debug("Activating terminal with " + activatePath); + + if (UIUtil.isWindows) { + final String winCmd = cmd + .map(c -> activatePath + " & " + c) + .orElse(activatePath); + + commandToRun = new String[] { "cmd.exe", "/K", winCmd }; + } else { + // Override the default bash load order to ensure that the virtualenv activate script is correctly loaded + // and we don't lose any interactive functionality. + // Also reset the terminal to clear any previous state. + final String initFilePath = RessourcesUnpacker.BASH_INIT_FILE_PATH.toString(); + + // We have to use bash because it is present on all distros and offers command-line options that permits launching them in a venv easily + final LinkedList shellStarter = new LinkedList<>( + List.of( + Constants.DEFAULT_UNIX_SHELL, + "--init-file", + initFilePath + ) + ); + + // Expand the array to a correctly escaped command line + final String shell = shellStarter + .stream() + .reduce("", (acc, tok) -> acc + " " + escapeshellarg(tok)); + + // Relaunch a venv-activated shell after the command is over + cmd = cmd.map(c -> c + ";" + shell); + + if (cmd.isPresent()) { + // Add the command to execute (e.g: open ~/.scalpel/venv/default/default.py) + shellStarter.addLast("-c"); + shellStarter.addLast(cmd.get()); + } + commandToRun = shellStarter.toArray(new String[0]); + + // Tell the shell the terminal is xterm like. + env = new HashMap<>(env); + env.put("TERM", "xterm-256color"); + env.put("SCALPEL_VENV_ACTIVATE", activatePath); + } + + try { + // Start the process in the virtualenv directory. + ScalpelLogger.debug("Executing command: " + dumps(commandToRun)); + final PtyProcessBuilder builder = new PtyProcessBuilder() + .setCommand(commandToRun) + .setEnvironment(env) + .setDirectory(cwd.orElse(workspacePath)); + + ttyDimension.ifPresent(d -> + builder + .setInitialRows((int) d.getHeight()) + .setInitialColumns((int) d.getWidth()) + ); + + final PtyProcess processs = builder.start(); + + return new PtyProcessTtyConnector(processs, StandardCharsets.UTF_8); + } catch (Throwable e) { + throw new IllegalStateException(e); + } + } + + /** + * Creates a JediTermWidget that will run a shell in the virtualenv. + * + * @param theme The theme to use. (Dark or Light) + * @param venvPath The path to the virtualenv. + * @return The JediTermWidget. + */ + public static JediTermWidget createTerminal(Theme theme, String venvPath) { + return createTerminalWidget( + theme, + venvPath, + Optional.empty(), + Optional.empty() + ); + } + + /** + * Creates a JediTermWidget that will run a shell in the virtualenv. + * + * @param theme The theme to use. (Dark or Light) + * @param venvPath The path to the virtualenv. + * @param cmd The command to run + * @return The JediTermWidget. + */ + public static JediTermWidget createTerminal( + Theme theme, + String venvPath, + String cwd, + String cmd + ) { + return createTerminalWidget( + theme, + venvPath, + Optional.of(cwd), + Optional.of(cmd) + ); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/UIBuilder.java b/scalpel/src/main/java/lexfo/scalpel/UIBuilder.java new file mode 100644 index 00000000..254a5927 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/UIBuilder.java @@ -0,0 +1,100 @@ +package lexfo.scalpel; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.Theme; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import javax.swing.*; +import org.apache.commons.io.FileUtils; + +/** + Provides methods for constructing the Burp Suite UI. +*/ +public class UIBuilder { + + /** + Constructs the configuration Burp tab. + + @param executor The ScalpelExecutor object to use. + @param defaultScriptPath The default text content + @return The constructed tab. + */ + public static final Component constructConfigTab( + MontoyaApi API, + ScalpelExecutor executor, + Config config, + Theme theme + ) { + return new ConfigTab(API, executor, config, theme).uiComponent(); + } + + /** + Constructs the debug Python testing Burp tab. + @param executor The ScalpelExecutor object to use. + @param logger The Logging object to use. + @return The constructed tab. + */ + public static final Component constructScalpelInterpreterTab( + Config config, + ScalpelExecutor executor + ) { + // Split pane + JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT); + JSplitPane scriptingPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + JTextArea outputArea = new JTextArea(); + JEditorPane editorPane = new JEditorPane(); + JButton button = new JButton("Run script."); + + button.addActionListener((ActionEvent e) -> { + ScalpelLogger.trace("Clicked button"); + final String scriptContent = editorPane.getText(); + try { + final String[] scriptOutput = executor.evalAndCaptureOutput( + scriptContent + ); + + final String txt = String.format( + "stdout:\n------------------\n%s\n------------------\n\nstderr:\n------------------\n%s", + scriptOutput[0], + scriptOutput[1] + ); + + outputArea.setText(txt); + } catch (Throwable exception) { + outputArea.setText(exception.getMessage()); + outputArea.append("\n\n"); + + final String stackTrace = Arrays + .stream(exception.getStackTrace()) + .map(StackTraceElement::toString) + .reduce((a, b) -> a + "\n" + b) + .orElse("No stack trace."); + + outputArea.append(stackTrace); + } + ScalpelLogger.trace("Handled action."); + }); + + final File file = config.getFrameworkPath().toFile(); + editorPane.setText( + IO.ioWrap( + () -> FileUtils.readFileToString(file, StandardCharsets.UTF_8), + () -> "" + ) + ); + + outputArea.setEditable(false); + scriptingPane.setLeftComponent(new JScrollPane(editorPane)); + scriptingPane.setRightComponent(new JScrollPane(outputArea)); + scriptingPane.setResizeWeight(0.5); + + splitPane.setResizeWeight(1); + splitPane.setLeftComponent(scriptingPane); + splitPane.setRightComponent(button); + + return splitPane; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/UIUtils.java b/scalpel/src/main/java/lexfo/scalpel/UIUtils.java new file mode 100644 index 00000000..fc7a53e9 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/UIUtils.java @@ -0,0 +1,78 @@ +package lexfo.scalpel; + +import java.awt.Adjustable; +import java.util.concurrent.atomic.AtomicReference; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +public class UIUtils { + + /** + * Set up auto-scrolling for a script output text area. + *

    + * If the user scrolls up, auto-scroll is disabled. + * If the user scrolls to the bottom, auto-scroll is enabled. + * + * @param scrollPane The scroll pane containing the text area. + * @param textArea The text area to auto-scroll. + */ + public static void setupAutoScroll( + JScrollPane scrollPane, + JTextArea textArea + ) { + final JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar(); + + // Unique auto-scroll flag for this text area + final AtomicReference needAutoScroll = new AtomicReference<>( + true + ); // Use an AtomicRef for mutable state inside lambdas + + verticalScrollBar.addAdjustmentListener(e -> { + final Adjustable adjustable = e.getAdjustable(); + if (!e.getValueIsAdjusting()) { + needAutoScroll.set( + adjustable.getMaximum() - + adjustable.getValue() - + adjustable.getVisibleAmount() < + 50 + ); + } + }); + + textArea + .getDocument() + .addDocumentListener( + new DocumentListener() { + private void update() { + if (needAutoScroll.get()) { + // Scroll down + SwingUtilities.invokeLater(() -> + verticalScrollBar.setValue( + verticalScrollBar.getMaximum() + ) + ); + } + } + + @Override + public void insertUpdate(DocumentEvent e) { + update(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + update(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + update(); + } + } + ); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/UnObfuscator.java b/scalpel/src/main/java/lexfo/scalpel/UnObfuscator.java new file mode 100644 index 00000000..b2a36a6c --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/UnObfuscator.java @@ -0,0 +1,41 @@ +package lexfo.scalpel; + +import java.util.HashSet; + +public class UnObfuscator { + + /** + * Finds a Montoya interface in the specified class, its superclasses or + * interfaces, and return its name. Otherwise, returns the name of the class + * of the object. + */ + public static String getClassName(Object obj) { + if (obj == null) return "null"; + HashSet> visited = new HashSet>(); + Class c = obj.getClass(); + String itf = findMontoyaInterface(c, visited); + if (itf != null) return itf; + return c.getSimpleName(); + } + + public static String findMontoyaInterface( + Class c, + HashSet> visited + ) { + if (c == null || visited.contains(c)) return null; + visited.add(c); + + if ( + c.getName().startsWith("burp.api.montoya") + ) return c.getSimpleName(); + + // Check interfaces + for (Class i : c.getInterfaces()) { + String r = findMontoyaInterface(i, visited); + if (r != null) return r; + } + + // Check superclasses + return findMontoyaInterface(c.getSuperclass(), visited); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Venv.java b/scalpel/src/main/java/lexfo/scalpel/Venv.java new file mode 100644 index 00000000..6b01f33a --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Venv.java @@ -0,0 +1,308 @@ +package lexfo.scalpel; + +import com.jediterm.terminal.ui.UIUtil; +import java.io.BufferedReader; +/** + * The Venv class is used to manage Python virtual environments. + */ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang3.ArrayUtils; + +/** + * Manage Python virtual environments. + */ +public class Venv { + + /** + * Create a virtual environment. + * + * @param path The path to the virtual environment directory. + * @return The finished process of the "python3 -m venv" command. + */ + public static Process create(Path path) + throws IOException, InterruptedException { + // Create the directory for the virtual environment + Files.createDirectories(path); + + // Create the virtual environment using the "python3 -m venv" command + final ProcessBuilder processBuilder = new ProcessBuilder( + Constants.PYTHON_BIN, + "-m", + "venv", + path.toString() + ); + + processBuilder.redirectErrorStream(true); + + final Process process = processBuilder.start(); + + // Wait for the virtual environment creation to complete + process.waitFor(); + + return process; + } + + // https://github.com/ninia/jep/issues/495 + private static void clearPipCache(Path venv) + throws IOException, InterruptedException { + final ProcessBuilder cacheClearProcessBuilder = new ProcessBuilder( + getPipPath(venv).toString(), + "cache", + "remove", + "jep" + ); + final Process cacheClearProcess = cacheClearProcessBuilder.start(); + cacheClearProcess.waitFor(); + if (cacheClearProcess.exitValue() != 0) { + ScalpelLogger.error("Failed to clear the pip cache for jep"); + } + } + + public static Process installDefaults( + Path venv, + Map env, + Boolean installJep + ) throws IOException, InterruptedException { + // Install the default packages + + final String[] javaDeps = Constants.DEFAULT_VENV_DEPENDENCIES; + // Dependencies required for Java to initiate a Python interpreter (jep) + + // Dependencies required by the Scalpel Python library. + final String[] scriptDeps = Constants.PYTHON_DEPENDENCIES; + + final String[] pkgsToInstall; + if (installJep) { + clearPipCache(venv); + pkgsToInstall = ArrayUtils.addAll(javaDeps, scriptDeps); + } else { + pkgsToInstall = scriptDeps; + } + + return install_background(venv, env, pkgsToInstall); + } + + public static Process installDefaults(Path path) + throws IOException, InterruptedException { + // Install the default packages + return installDefaults(path, Map.of(), true); + } + + /** + * Create a virtual environment and install the default packages. + * + * @param venv The path to the virtual environment directory. + * @return The exit code of the "pip install ..." command. + */ + public static Process createAndInstallDefaults(Path venv) + throws IOException, InterruptedException { + // Create the virtual environment + final Process proc = create(venv); + if (proc.exitValue() != 0) { + return proc; + } + return installDefaults(venv); + } + + /** + * Delete a virtual environment. + * + * @param venv The path to the virtual environment directory. + */ + public static void delete(Path venv) { + try { + // Delete the virtual environment directory + Files.delete(venv); + // Return 0 (success) + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Install a package in a virtual environment in a new thread. + * + * @param path The path to the virtual environment directory. + * @param pkgs The name of the package to install. + * @return The exit code of the "pip install ..." command. + */ + public static Thread install_background(Path path, String... pkgs) { + Thread thread = new Thread(() -> IO.ioWrap(() -> install(path, pkgs))); + thread.start(); + return thread; + } + + /** + * Install a package in a virtual environment. + * + * @param path The path to the virtual environment directory. + * @param pkgs The name of the package to install. + * @return The exit code of the "pip install ..." command. + */ + public static Process install(Path path, String... pkgs) + throws IOException { + return install(path, Map.of(), pkgs); + } + + /** + * Install a package in a virtual environment. + * + * @param path The path to the virtual environment directory. + * @param env The environnement variables to pass + * @param pkgs The name of the package to install. + * @return The exit code of the "pip install ..." command. + */ + public static Process install_background( + Path path, + Map env, + String... pkgs + ) throws IOException { + // Install the package using the "pip install" command + + final LinkedList command = new LinkedList<>( + List.of(getPipPath(path).toString(), "install") + ); + command.addAll(Arrays.asList(pkgs)); + + final ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.directory(path.toFile()); + processBuilder.environment().putAll(env); + processBuilder.redirectErrorStream(true); + + final Process process = processBuilder.start(); + + ScalpelLogger.debug( + "Launched " + command.stream().collect(Collectors.joining(" ")) + ); + + return process; + } + + /** + * Install a package in a virtual environment. + * + * @param path The path to the virtual environment directory. + * @param env The environnement variables to pass + * @param pkgs The name of the package to install. + * @return The exit code of the "pip install ..." command. + */ + public static Process install( + Path path, + Map env, + String... pkgs + ) throws IOException { + final Process proc = install_background(path, env, pkgs); + + final BufferedReader stdout = proc.inputReader(); + while (proc.isAlive()) { + ScalpelLogger.all(stdout.readLine()); + } + + return proc; + } + + protected static final class PackageInfo { + + public String name; + public String version; + } + + public static Path getSitePackagesPath(Path venvPath) throws IOException { + if (UIUtil.isWindows) { + // Find the sites-package directory path as in: /Lib/site-packages + return Files + .walk(venvPath) + .filter(Files::isDirectory) + .filter(p -> p.getFileName().toString().equalsIgnoreCase("lib")) + .map(p -> p.resolve("site-packages")) + .filter(Files::exists) + .findFirst() + .orElseThrow(() -> + new RuntimeException( + "Failed to find venv site-packages.\n" + + "Make sure dependencies are correctly installed. (python3,pip,venv,jdk)" + ) + ); + } + // Find the sites-package directory path as in: /lib/python*/site-packages + return Files + .walk(venvPath.resolve("lib")) + .filter(Files::isDirectory) + .filter(p -> p.getFileName().toString().startsWith("python")) + .map(p -> p.resolve("site-packages")) + .filter(Files::exists) + .findFirst() + .orElseThrow(() -> + new RuntimeException( + "Failed to find venv site-packages.\n" + + "Make sure dependencies are correctly installed. (python3,pip,venv,jdk)" + ) + ); + } + + public static Path getExecutablePath(Path venvPath, String filename) + throws IOException { + final String binDir = Constants.VENV_BIN_DIR; + + return Files + .walk(venvPath) + .filter(Files::isDirectory) + .filter(p -> p.getFileName().toString().equalsIgnoreCase(binDir)) + .map(p -> p.resolve(filename)) + .filter(Files::exists) + .map(Path::toAbsolutePath) + .findFirst() + .orElseThrow(() -> + new RuntimeException( + "Failed to find " + + filename + + " in " + + venvPath + + " .\n" + + "Make sure dependencies are correctly installed. (python3,pip,venv,jdk)" + ) + ); + } + + public static Path getPipPath(Path venvPath) throws IOException { + return getExecutablePath(venvPath, Constants.PIP_BIN); + } + + /** + * Get the list of installed packages in a virtual environment. + * + * @param path The path to the virtual environment directory. + * @return The list of installed packages. + */ + public static PackageInfo[] getInstalledPackages(Path path) + throws IOException { + final ProcessBuilder processBuilder = new ProcessBuilder( + getPipPath(path).toString(), + "list", + "--format", + "json", + "--exclude", + "pip", + "--exclude", + "setuptools" + ); + + // Launch and parse the JSON output using Jackson + final Process process = processBuilder.start(); + + // Read the JSON output + final String jsonData = new String( + process.getInputStream().readAllBytes() + ); + + // Parse the JSON output + return IO.readJSON(jsonData, PackageInfo[].class); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/Workspace.java b/scalpel/src/main/java/lexfo/scalpel/Workspace.java new file mode 100644 index 00000000..535b7727 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/Workspace.java @@ -0,0 +1,327 @@ +package lexfo.scalpel; + +import com.jediterm.terminal.Terminal; +import com.jediterm.terminal.ui.UIUtil; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lexfo.scalpel.components.ErrorDialog; +import org.apache.commons.io.FileUtils; + +/* + * Note: The Scalpel data folder follows this architeture: + * + ~ + └── .scalpel + ├── extracted (ressources) + ├── global.json + ├── .json + └── venvs + └── + ├── default.py + └── .venv +*/ + +/** + * A workspace is a folder containing a venv and the associated scripts. + *
    + * We may still call that a "venv" in the front-end to avoid confusing the user. + */ +public class Workspace { + + public static final String VENV_DIR = ".venv"; + public static final String DEFAULT_VENV_NAME = "default"; + + private static RuntimeException createExceptionFromProcess( + Process proc, + String msg, + String defaultCmdLine + ) { + final Stream outStream = Stream.concat( + proc.inputReader().lines(), + proc.errorReader().lines() + ); + final String out = outStream.collect(Collectors.joining("\n")); + final String cmd = proc.info().commandLine().orElse(defaultCmdLine); + + return new RuntimeException(cmd + " failed:\n" + out + "\n" + msg); + } + + /** + * Copy the script to the selected workspace + * @param scriptPath The script to copy + * @return The new file path + */ + public static Path copyScriptToWorkspace( + final Path workspace, + final Path scriptPath + ) { + final File original = scriptPath.toFile(); + final String baseErrMsg = + "Could not copy " + scriptPath + " to " + workspace + "\n"; + + final Path destination = Optional + .ofNullable(original) + .filter(File::exists) + .map(File::getName) + .map(workspace::resolve) + .orElseThrow(() -> + new RuntimeException(baseErrMsg + "File not found") + ); + + if (Files.exists(destination)) { + throw new RuntimeException(baseErrMsg + "File already exists"); + } + + try { + return Files + .copy( + original.toPath(), + destination, + StandardCopyOption.REPLACE_EXISTING + ) + .toAbsolutePath(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void copyWorkspaceFiles(Path workspace) throws IOException { + final File source = RessourcesUnpacker.WORKSPACE_PATH.toFile(); + final File dest = workspace.toFile(); + FileUtils.copyDirectory(source, dest, true); + } + + private static void println(Terminal terminal, String line) { + terminal.writeUnwrappedString(line); + terminal.carriageReturn(); + terminal.newLine(); + } + + public static void createAndInitWorkspace( + Path workspace, + Optional javaHome, + Optional terminal + ) { + // Run python -m venv + // Command to display in logs, actual command is formated in Venv.create + final String cmd = + Constants.PYTHON_BIN + + " -m venv " + + lexfo.scalpel.Terminal.escapeshellarg( + getVenvDir(workspace).toString() + ); + + // Display the command in the terminal to avoid confusing the user + terminal.ifPresent(t -> { + t.reset(); + println(t, "$ " + cmd); + }); + + try { + final Path venvDir = getVenvDir(workspace); + final Process proc = Venv.create(venvDir); + if (proc.exitValue() != 0) { + throw createExceptionFromProcess( + proc, + "Ensure that pip3, python3.*-venv, python >= 3.8 and openjdk >= 17 are installed and in PATH.", + cmd + ); + } + copyWorkspaceFiles(workspace); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + + try { + // Add default script. + copyScriptToWorkspace( + workspace, + RessourcesUnpacker.DEFAULT_SCRIPT_PATH + ); + } catch (RuntimeException e) { + ScalpelLogger.error( + "Default script could not be copied to " + workspace + ); + } + + // Run pip install + try { + final Process proc; + final Supplier toThrow = () -> + new RuntimeException("JAVA_HOME was not provided."); + + if (javaHome.isPresent()) { + proc = + Venv.installDefaults( + workspace, + Map.of( + "JAVA_HOME", + javaHome.map(Path::toString).orElseThrow(toThrow) + ), + true + ); + } else { + proc = Venv.installDefaults(workspace); + } + + // Log pip output + final BufferedReader stdout = proc.inputReader(); + while (proc.isAlive()) { + Optional + .ofNullable(stdout.readLine()) + .ifPresent(l -> { + ScalpelLogger.all(l); + + // Display progess in embedded terminal + terminal.ifPresent(t -> println(t, l)); + }); + } + + if (proc.exitValue() != 0) { + final String linuxMsg = + "On Debian/Ubuntu systems:\n\t" + + "apt install build-essential python3-dev openjdk-17-jdk"; + + final String winMsg = + "On Windows:\n\t" + + "Make sure you have installed Microsoft Visual C++ >=14.0 :\n\t" + + "https://visualstudio.microsoft.com/visual-cpp-build-tools/"; + + final String msg = UIUtil.isWindows ? winMsg : linuxMsg; + + throw createExceptionFromProcess( + proc, + "Could not install dependencies\n" + + "Make sure that a compiler, python dev libraries and openjdk 17 are properly installed and in PATH\n\n" + + msg, + "pip install jep ..." + ); + } + } catch (Throwable e) { + // Display a popup explaining why the packages could not be installed + ErrorDialog.showErrorDialog( + null, + "Could not install depencency packages.\n" + + "Error: " + + e.getMessage() + ); + throw new RuntimeException(e); + } + } + + /** + * If a previous install failed because python dependencies were not installed, + * this will be false, in this case, we just try to resume the install. + */ + private static boolean isJepInstalled(Path workspace) throws IOException { + final Path venvPath = workspace.resolve(VENV_DIR); + final File dir = Venv.getSitePackagesPath(venvPath).toFile(); + + final File[] jepDirs = dir.listFiles((__, name) -> name.matches("jep")); + + return jepDirs.length != 0; + } + + /** + * Get the default workspace path. + * This is the workspace that will be used when the project is created. + * If the default workspace does not exist, it will be created. + * If the default workspace cannot be created, an exception will be thrown. + * + * @return The default workspace path. + */ + public static Path getOrCreateDefaultWorkspace(Path javaHome) + throws IOException { + final Path workspace = Path.of( + Workspace.getWorkspacesDir().getPath(), + DEFAULT_VENV_NAME + ); + + final File venvDir = workspace.resolve(VENV_DIR).toFile(); + + // Return if default workspace dir already exists. + if (!venvDir.exists()) { + venvDir.mkdirs(); + } else if (!venvDir.isDirectory()) { + throw new RuntimeException("Default venv path is not a directory"); + } else if (isJepInstalled(workspace)) { + return workspace; + } + + createAndInitWorkspace( + workspace.toAbsolutePath(), + Optional.of(javaHome), + Optional.empty() + ); + + // Copy all example scripts to the default workspace. + try ( + final DirectoryStream stream = Files.newDirectoryStream( + RessourcesUnpacker.SAMPLES_PATH, + "*.py" + ) + ) { + for (final Path entry : stream) { + final Path targetPath = workspace.resolve(entry.getFileName()); + Files.copy( + entry, + targetPath, + StandardCopyOption.REPLACE_EXISTING + ); + } + } catch (IOException e) { + throw new RuntimeException("Error copying example scripts", e); + } + + return workspace; + } + + public static Path getVenvDir(Path workspace) { + return workspace.resolve(VENV_DIR); + } + + public static Path getDefaultWorkspace() { + return Paths + .get(getWorkspacesDir().getAbsolutePath()) + .resolve(DEFAULT_VENV_NAME); + } + + /** + * Get the scalpel configuration directory. + * + * @return The scalpel configuration directory. (default: $HOME/.scalpel) + */ + public static File getScalpelDir() { + final File dir = RessourcesUnpacker.DATA_DIR_PATH.toFile(); + if (!dir.exists()) { + dir.mkdir(); + } + + return dir; + } + + /** + * Get the default venvs directory. + * + * @return The default venvs directory. (default: $HOME/.scalpel/venvs) + */ + public static File getWorkspacesDir() { + final File dir = new File(getScalpelDir(), "venvs"); + if (!dir.exists()) { + dir.mkdir(); + } + + return dir; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/components/ErrorDialog.java b/scalpel/src/main/java/lexfo/scalpel/components/ErrorDialog.java new file mode 100644 index 00000000..049abc43 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/components/ErrorDialog.java @@ -0,0 +1,84 @@ +package lexfo.scalpel.components; + +import java.awt.*; +import java.util.regex.*; +import javax.swing.*; +import javax.swing.event.HyperlinkEvent; + +public class ErrorDialog { + + public static void showErrorDialog(Frame parent, String errorText) { + // Create a JEditorPane for clickable links and selectable text + final JEditorPane messagePane = new JEditorPane("text/html", ""); + messagePane.setEditable(false); + messagePane.setBackground(null); + messagePane.setOpaque(false); + messagePane.setContentType("text/html"); + + // Sanitize and format the error message + final String sanitizedHTML = sanitizeHTML(errorText); + final String formattedMessage = linkifyURLs(sanitizedHTML); + + // Set HTML content to make links clickable and text selectable + final String message = + "" + + formattedMessage.replace("\n", "
    ") + + ""; + messagePane.setText(message); + + // Make links open in user's default browser + messagePane.addHyperlinkListener(hyperlinkEvent -> { + if ( + HyperlinkEvent.EventType.ACTIVATED.equals( + hyperlinkEvent.getEventType() + ) + ) { + try { + Desktop + .getDesktop() + .browse(hyperlinkEvent.getURL().toURI()); + } catch (Throwable ex) { + ex.printStackTrace(); + } + } + }); + + // Wrap in a scroll pane + final JScrollPane scrollPane = new JScrollPane(messagePane); + scrollPane.setBorder(null); + scrollPane.setPreferredSize(new Dimension(350, 150)); + + // Show in JOptionPane + JOptionPane.showMessageDialog( + parent, + scrollPane, + "Installation Error", + JOptionPane.ERROR_MESSAGE + ); + } + + private static String sanitizeHTML(String text) { + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + private static String linkifyURLs(String text) { + // Regex to identify URLs + Pattern pattern = Pattern.compile( + "(https?://[\\w_\\-./?=&#]+)", + Pattern.CASE_INSENSITIVE + ); + Matcher matcher = pattern.matcher(text); + StringBuffer sb = new StringBuffer(); + while (matcher.find()) { + // Replace URLs with HTML links + matcher.appendReplacement(sb, "$1"); + } + matcher.appendTail(sb); + return sb.toString(); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/components/ErrorPopup.java b/scalpel/src/main/java/lexfo/scalpel/components/ErrorPopup.java new file mode 100644 index 00000000..31a69474 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/components/ErrorPopup.java @@ -0,0 +1,76 @@ +package lexfo.scalpel.components; + +import burp.api.montoya.MontoyaApi; +import java.awt.*; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.swing.*; +import lexfo.scalpel.ConfigTab; +import lexfo.scalpel.UIUtils; + +public class ErrorPopup extends JFrame { + + private static final ConcurrentLinkedQueue errorMessages = new ConcurrentLinkedQueue<>(); + private JTextArea errorArea; + private JCheckBox suppressCheckBox; + + public ErrorPopup(MontoyaApi API) { + super("Scalpel Error Log"); + setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); + setSize((int) (400 * 1.8), (int) (300 * 1.5)); + setLocationRelativeTo(API.userInterface().swingUtils().suiteFrame()); + setLayout(new BorderLayout()); + + errorArea = new JTextArea(); + errorArea.setEditable(false); + + final JScrollPane scrollPane = new JScrollPane(errorArea); + UIUtils.setupAutoScroll(scrollPane, errorArea); + + add(scrollPane, BorderLayout.CENTER); + + suppressCheckBox = + new JCheckBox("Do not display this again for this project"); + suppressCheckBox.addActionListener(e -> { + boolean selected = suppressCheckBox.isSelected(); + ConfigTab + .getInstance() + .setSettings( + Map.of( + "displayProxyErrorPopup", + !selected ? "True" : "False" + ) + ); + }); + + add(suppressCheckBox, BorderLayout.SOUTH); + + // Setup the window listener for handling the close operation + addWindowListener( + new java.awt.event.WindowAdapter() { + @Override + public void windowClosing( + java.awt.event.WindowEvent windowEvent + ) { + errorMessages.clear(); + errorArea.setText(""); + setVisible(false); + } + } + ); + } + + public void displayErrors() { + StringBuilder sb = new StringBuilder(); + for (String msg : errorMessages) { + sb.append(msg).append("\n\n"); + } + errorArea.setText(sb.toString()); + setVisible(true); + } + + public void addError(String message) { + errorMessages.add(message); + displayErrors(); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/components/PlaceholderTextField.java b/scalpel/src/main/java/lexfo/scalpel/components/PlaceholderTextField.java new file mode 100644 index 00000000..ed024b8e --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/components/PlaceholderTextField.java @@ -0,0 +1,68 @@ +package lexfo.scalpel.components; + +import java.awt.*; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; +import javax.swing.*; + +public class PlaceholderTextField extends JTextField { + + private String placeholder; + + public PlaceholderTextField(String placeholder) { + this.placeholder = placeholder; + addFocusListener( + new FocusAdapter() { + @Override + public void focusGained(FocusEvent e) { + if (getText().isEmpty()) { + setText(""); + repaint(); + } + } + + @Override + public void focusLost(FocusEvent e) { + if (getText().isEmpty()) { + setText(""); + repaint(); + } + } + } + ); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (getText().isEmpty() && !hasFocus()) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setColor(Color.GRAY); + g2.setFont(getFont().deriveFont(Font.ITALIC)); + int padding = (getHeight() - getFont().getSize()) / 2; + g2.drawString( + placeholder, + getInsets().left, + getHeight() - padding - 1 + ); + g2.dispose(); + } + } + + public static void main(String[] args) { + JFrame frame = new JFrame("Placeholder JTextField Example"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLayout(new FlowLayout()); + + PlaceholderTextField textField = new PlaceholderTextField( + "Enter text here..." + ); + textField.setColumns(20); + + frame.add(textField); + frame.pack(); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/components/SettingsPanel.java b/scalpel/src/main/java/lexfo/scalpel/components/SettingsPanel.java new file mode 100644 index 00000000..404cd472 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/components/SettingsPanel.java @@ -0,0 +1,140 @@ +package lexfo.scalpel.components; + +import java.awt.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +public class SettingsPanel extends JPanel { + + private final GridBagConstraints gbc = new GridBagConstraints(); + private final Map settingsComponentsByKey = new HashMap<>(); + private final Map keyToLabel = new HashMap<>(); + private final List>> changeListeners = new ArrayList<>(); + + public SettingsPanel() { + setLayout(new GridBagLayout()); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(4, 4, 4, 4); + } + + public void addListener(Consumer> listener) { + changeListeners.add(listener); + } + + public void addCheckboxSetting( + String key, + String label, + boolean isSelected + ) { + JCheckBox checkBox = new JCheckBox(); + checkBox.setSelected(isSelected); + checkBox.addActionListener(e -> notifyChangeListeners()); + addSettingComponent(key, label, checkBox); + } + + public void addTextFieldSetting(String key, String label, String text) { + JTextField textField = new JTextField(text, 20); + + // Debounce timer + Timer timer = new Timer(300, e -> notifyChangeListeners()); // 300 ms delay + timer.setRepeats(false); // Ensure the timer only runs once per event + + textField + .getDocument() + .addDocumentListener( + new DocumentListener() { + public void insertUpdate(DocumentEvent e) { + timer.restart(); + } + + public void removeUpdate(DocumentEvent e) { + timer.restart(); + } + + public void changedUpdate(DocumentEvent e) { + timer.restart(); + } + } + ); + + addSettingComponent(key, label, textField); + } + + public void addDropdownSetting( + String key, + String label, + String[] options, + String selectedItem + ) { + JComboBox comboBox = new JComboBox<>(options); + comboBox.setSelectedItem(selectedItem); + comboBox.addActionListener(e -> notifyChangeListeners()); + addSettingComponent(key, label, comboBox); + } + + private void addSettingComponent( + String key, + String label, + JComponent component + ) { + settingsComponentsByKey.put(key, component); + keyToLabel.put(key, label); + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + panel.add(new JLabel(label)); + panel.add(component); + add(panel, gbc); + } + + public void addInformationText(String text) { + JLabel infoLabel = new JLabel(text); + JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + panel.add(infoLabel); + add(panel, gbc); + } + + private void notifyChangeListeners() { + Map settingsValues = getSettingsValues(); + for (Consumer> listener : changeListeners) { + listener.accept(settingsValues); + } + } + + public Map getSettingsValues() { + Map settingsValues = new HashMap<>(); + settingsComponentsByKey.forEach((key, component) -> { + String value = ""; + if (component instanceof JCheckBox) { + value = ((JCheckBox) component).isSelected() ? "True" : "False"; + } else if (component instanceof JTextField) { + value = ((JTextField) component).getText(); + } else if (component instanceof JComboBox) { + value = (String) ((JComboBox) component).getSelectedItem(); + } + settingsValues.put(key, value); + }); + return settingsValues; + } + + public void setSettingsValues(Map settingsValues) { + settingsValues.forEach((key, value) -> { + JComponent component = settingsComponentsByKey.get(key); + if (component instanceof JCheckBox) { + ((JCheckBox) component).setSelected( + Boolean.parseBoolean(value) + ); + } else if (component instanceof JTextField) { + ((JTextField) component).setText(value); + } else if (component instanceof JComboBox) { + ((JComboBox) component).setSelectedItem(value); + } + }); + notifyChangeListeners(); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/components/WorkingPopup.java b/scalpel/src/main/java/lexfo/scalpel/components/WorkingPopup.java new file mode 100644 index 00000000..ce166614 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/components/WorkingPopup.java @@ -0,0 +1,48 @@ +package lexfo.scalpel.components; + +import java.awt.*; +import java.util.function.Consumer; +import javax.swing.*; + +/** + Provides a blocking wait dialog GUI popup. +*/ +public class WorkingPopup { + + /** + Shows a blocking wait dialog. + + @param task The task to run while the dialog is shown. + */ + public static void showBlockingWaitDialog( + String message, + Consumer task + ) { + final JFrame parent = new JFrame(); + parent.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + + final JDialog dialog = new JDialog(parent, "Please wait...", true); + dialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + + final JLabel label = new JLabel(message); + label.setHorizontalAlignment(SwingConstants.CENTER); + label.setBorder(BorderFactory.createEmptyBorder(20, 0, 20, 0)); + dialog.add(label, BorderLayout.CENTER); + + dialog.setLocationRelativeTo(parent); + + final Thread taskThread = new Thread(() -> { + try { + task.accept(label); + } finally { + SwingUtilities.invokeLater(dialog::dispose); + } + }); + taskThread.start(); + + SwingUtilities.invokeLater(() -> { + dialog.pack(); + dialog.setVisible(true); + }); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/AbstractEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/AbstractEditor.java new file mode 100644 index 00000000..71b2123b --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/AbstractEditor.java @@ -0,0 +1,477 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.http.message.responses.HttpResponse; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import java.awt.*; +import java.util.Optional; +import java.util.UUID; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.JTextPane; +import javax.swing.SwingUtilities; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.Result; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import lexfo.scalpel.ScalpelLogger; + +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.html +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.html +/** + Base class for implementing Scalpel editors + It handles all the Python stuff and only leaves the content setter/getter, modification checker and selection parts abstract + That way, if you wish to implement you own editor, you only have to add logic specific to it (get/set, selected data, has content been modified by user ?) + */ +public abstract class AbstractEditor implements IMessageEditor { + + private final String name; + + /** + The HTTP request or response being edited. + */ + private HttpRequestResponse _requestResponse; + + /** + The Montoya API object. + */ + private final MontoyaApi API; + + /** + The editor creation context. + */ + private final EditorCreationContext ctx; + + /** + The editor type (REQUEST or RESPONSE). + */ + private final EditorType type; + + /** + The editor ID. (unused) + */ + private final String id; + + /** + The editor provider that instantiated this editor. (unused) + */ + private final ScalpelEditorTabbedPane provider; + + /** + The executor responsible for interacting with Python. + */ + private final ScalpelExecutor executor; + + private final JPanel rootPanel = new JPanel(); + private final JTabbedPane tabs = new JTabbedPane(); + private final JTextPane errorPane = new JTextPane(); + private Optional inError = Optional.empty(); + private Optional outError = Optional.empty(); + + /** + Constructs a new Scalpel editor. + + @param API The Montoya API object. + @param creationContext The EditorCreationContext object containing information about the editor. + @param type The editor type (REQUEST or RESPONSE). + @param provider The ScalpelProvidedEditor object that instantiated this editor. + @param executor The executor to use. + */ + public AbstractEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor + ) { + this.name = name; + + // Keep a reference to the Montoya API + this.API = API; + + // Associate the editor with an unique ID (obsolete) + this.id = UUID.randomUUID().toString(); + + // Keep a reference to the provider. + this.provider = provider; + + // Store the context (e.g.: Tool origin, HTTP message type,...) + this.ctx = creationContext; + + // Reference the executor to be able to call Python callbacks. + this.executor = executor; + + this.type = type; + errorPane.setEditable(false); + this.rootPanel.setLayout(new GridLayout()); + } + + private void updateRootPanel() { + rootPanel.removeAll(); + + // The only thing to display is the error + if (inError.isPresent()) { + final Throwable err = inError.get(); + final String msg = ScalpelLogger.exceptionToErrorMsg( + err, + "Error in req_edit_in... hook:" + ); + errorPane.setText(msg); + rootPanel.add(errorPane); + return; + } + + // Include the editor and the out error + if (outError.isPresent()) { + final Throwable err = outError.get(); + final String msg = ScalpelLogger.exceptionToErrorMsg( + err, + "Error in req_edit_out... hook:" + ); + errorPane.setText(msg); + tabs.addTab("Editor", getUiComponent()); + tabs.addTab("Error", errorPane); + tabs.setSelectedComponent(errorPane); + rootPanel.add(tabs); + return; + } + + // There is no error, display the editor only + rootPanel.add(getUiComponent()); + } + + /** + * Set the editor's content + * + * Note: This should update isModified() + * @param bytes The new content + */ + protected abstract void setEditorContent(ByteArray bytes); + + protected void setEditorError(Throwable error) { + this.inError = Optional.of(error); + updateRootPanel(); + } + + /** + * Get the editor's content + * @return The editor's content + */ + protected abstract ByteArray getEditorContent(); + + /** + Returns the underlying UI component. + + @return The underlying UI component. + */ + @Override + public Component uiComponent() { + updateRootPanel(); + return rootPanel; + } + + public abstract Component getUiComponent(); + + /** + Returns the selected data. + (called by Burp) + + @return The selected data. + */ + @Override + public abstract Selection selectedData(); + + /** + Returns whether the editor has been modified since the last time it was programatically set + (called by Burp) + + @return Whether the editor has been modified. + */ + @Override + public abstract boolean isModified(); + + /** + Returns the editor type (REQUEST or RESPONSE). + + @return The editor type (REQUEST or RESPONSE). + */ + public final EditorType getEditorType() { + return type; + } + + /** + Returns the editor's unique ID. (unused) + @return The editor's unique ID. + */ + public final String getId() { + return id; + } + + /** + Returns the editor's creation context. + @return The editor's creation context. + */ + public final EditorCreationContext getCtx() { + return ctx; + } + + /** + * Returns the HTTP message being edited. + * @return The HTTP message being edited. + */ + public final HttpMessage getMessage() { + // Ensure request response exists. + if (_requestResponse == null) { + return null; + } + + // Safely extract the message from the requestResponse. + return type == EditorType.REQUEST + ? _requestResponse.request() + : _requestResponse.response(); + } + + /** + * Creates a new HTTP message by passing the editor's contents through a Python callback. + * + * @return The new HTTP message. + */ + public final HttpMessage processOutboundMessage() { + this.outError = Optional.empty(); + try { + // Safely extract the message from the requestResponse. + final HttpMessage msg = getMessage(); + + // Ensure request exists and has to be processed again before calling Python + if (msg == null || !isModified()) { + return null; + } + + final Result result; + + // Call Python "outbound" message editor callback with editor's contents. + if (type == EditorType.REQUEST) { + result = + executor + .callEditorHookOutRequest( + _requestResponse.request(), + getHttpService(), + getEditorContent(), + caption() + ) + .flatMap(Result::success); + } else { + result = + executor + .callEditorHookOutResponse( + _requestResponse.response(), + _requestResponse.request(), + getHttpService(), + getEditorContent(), + caption() + ) + .flatMap(Result::success); + } + + if (!result.isSuccess()) { + outError = Optional.of(result.getError()); + return msg; + } + outError = Optional.empty(); + + // Nothing was returned, return the original msg untouched. + if (result.isEmpty()) { + return msg; + } + + // Return the Python-processed message. + return result.getValue(); + } catch (Throwable e) { + ScalpelLogger.logStackTrace(e); + } + return null; // This should probably not happen, returning null here breaks Burp GUI + } + + /** + * Creates a new HTTP request by passing the editor's contents through a Python callback. + * (called by Burp) + * + * @return The new HTTP request. + */ + @Override + public final HttpRequest getRequest() { + // Cast the generic HttpMessage interface back to it's concrete type. + return (HttpRequest) processOutboundMessage(); + } + + /** + * Creates a new HTTP response by passing the editor's contents through a Python callback. + * (called by Burp) + * + * @return The new HTTP response. + */ + @Override + public final HttpResponse getResponse() { + // Cast the generic HttpMessage interface back to it's concrete type. + return (HttpResponse) processOutboundMessage(); + } + + /** + Returns the stored HttpRequestResponse. + + @return The stored HttpRequestResponse. + */ + public HttpRequestResponse getRequestResponse() { + return _requestResponse; + } + + // Returns a bool and avoids making duplicate calls to isEnabledFor to know if callback succeeded + public final boolean setRequestResponseInternal( + HttpRequestResponse requestResponse + ) { + this._requestResponse = requestResponse; + return updateContent(requestResponse); + } + + /** + Sets the HttpRequestResponse to be edited. + (called by Burp) + + @param requestResponse The HttpRequestResponse to be edited. + */ + @Override + public final void setRequestResponse(HttpRequestResponse requestResponse) { + setRequestResponseInternal(requestResponse); + } + + /** + * Get the network informations associated with the editor + * + * Gets the HttpService from requestResponse and falls back to request if it is null + * + * @return An HttpService if found, else null + */ + public final HttpService getHttpService() { + final HttpRequestResponse reqRes = this._requestResponse; + + // Ensure editor is initialized + if (reqRes == null) return null; + + // Check if networking infos are available in the requestRespone + if (reqRes.httpService() != null) { + return reqRes.httpService(); + } + + // Fall back to the initiating request + final HttpRequest req = reqRes.request(); + if (req != null) { + return req.httpService(); + } + + return null; + } + + public final Result executeHook( + HttpRequestResponse reqRes + ) throws Throwable { + if (reqRes == null) { + return Result.empty(); + } + + if (type == EditorType.REQUEST && reqRes.request() != null) { + return executor.callEditorHookInRequest( + reqRes.request(), + getHttpService(), + caption() + ); + } else if (type == EditorType.RESPONSE && reqRes.response() != null) { + return executor.callEditorHookInResponse( + reqRes.response(), + reqRes.request(), + getHttpService(), + caption() + ); + } + return Result.empty(); + } + + /** + Initializes the editor with Python callbacks output of the inputted HTTP message. + @param msg The HTTP message to be edited. + + @return True when the Python callback returned bytes, false otherwise. + */ + public final boolean updateContent(HttpRequestResponse reqRes) { + Result result; + this.inError = Optional.empty(); + try { + result = executeHook(reqRes); + } catch (Throwable e) { + result = Result.error(e); + } + + // Update the editor's content with the returned bytes. + result.ifSuccess(bytes -> + SwingUtilities.invokeLater(() -> setEditorContent(bytes)) + ); + result.ifError(e -> inError = Optional.of(e)); + result.ifError(e -> SwingUtilities.invokeLater(() -> setEditorError(e)) + ); + + updateRootPanel(); + + // Enable the tab when there is something to display (content or stack trace) + return !result.isEmpty(); + } + + /** + Determines whether the editor should be enabled for the provided HttpRequestResponse. + Also initializes the editor with Python callbacks output of the inputted HTTP message. + (called by Burp) + + @param reqRes The HttpRequestResponse to be edited. + */ + @Override + public final boolean isEnabledFor(HttpRequestResponse reqRes) { + if (reqRes == null) { + return true; + } + + // Extract the message from the reqRes. + final HttpMessage msg = + (type == EditorType.REQUEST ? reqRes.request() : reqRes.response()); + + // Ensure message exists. + if (msg == null || msg.toByteArray().length() == 0) { + return false; + } + + try { + // Enable the tab when there is content or an exception is thrown + return !executeHook(reqRes).isEmpty(); + } catch (Throwable e) { + // Log the error trace. + ScalpelLogger.logStackTrace(e); + } + return false; + } + + /** + Returns the name of the tab. + (called by Burp) + + @return The name of the tab. + */ + @Override + public final String caption() { + return this.name; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/DisplayableWhiteSpaceCharset.java b/scalpel/src/main/java/lexfo/scalpel/editors/DisplayableWhiteSpaceCharset.java new file mode 100644 index 00000000..e64da50e --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/DisplayableWhiteSpaceCharset.java @@ -0,0 +1,117 @@ +package lexfo.scalpel.editors; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.*; + +public class DisplayableWhiteSpaceCharset extends Charset { + + private final Charset utf8; + + public DisplayableWhiteSpaceCharset() { + super("DisplayableWhiteSpaceCharset", null); + utf8 = StandardCharsets.UTF_8; + } + + public boolean contains(Charset cs) { + return cs.equals(this); + } + + public CharsetDecoder newDecoder() { + return new WhitspaceCharsetDecoder(this, utf8.newDecoder()); + } + + public CharsetEncoder newEncoder() { + return utf8.newEncoder(); + } +} + +class WhitspaceCharsetDecoder extends CharsetDecoder { + + private final CharsetDecoder originalDecoder; + + protected WhitspaceCharsetDecoder( + Charset cs, + CharsetDecoder originalDecoder + ) { + super( + cs, + originalDecoder.averageCharsPerByte(), + originalDecoder.maxCharsPerByte() + ); + this.originalDecoder = originalDecoder; + } + + @Override + protected CoderResult decodeLoop(ByteBuffer in, CharBuffer out) { + CoderResult result = originalDecoder.decode(in, out, true); + if (result.isUnderflow()) { + out.flip(); + CharBuffer newBuffer = CharBuffer.allocate(out.remaining()); + while (out.hasRemaining()) { + char c = out.get(); + if (c == '\t') { + newBuffer.put(Character.toChars(187)[0]); + } else if (c == '\r') { + newBuffer.put(Character.toChars(164)[0]); + } else if (c == '\n') { + newBuffer.put(Character.toChars(182)[0]); + } else if (c == Character.toChars(127)[0]) { + newBuffer.put(Character.toChars(176)[0]); + } else { + newBuffer.put(c); + } + } + out.clear(); + newBuffer.flip(); + out.put(newBuffer); + } + return result; + } +} + +class WhitspaceCharsetEncoder extends CharsetEncoder { + + private final CharsetEncoder originalEncoder; + + protected WhitspaceCharsetEncoder( + Charset cs, + CharsetEncoder originalEncoder + ) { + super( + cs, + originalEncoder.averageBytesPerChar(), + originalEncoder.maxBytesPerChar() + ); + this.originalEncoder = originalEncoder; + } + + @Override + protected CoderResult encodeLoop(CharBuffer in, ByteBuffer out) { + while (in.hasRemaining()) { + char c = in.get(); + char newChar; + if (c == '\t') { + newChar = Character.toChars(187)[0]; + } else if (c == '\r') { + newChar = Character.toChars(164)[0]; + } else if (c == '\n') { + newChar = Character.toChars(182)[0]; + } else if (c == Character.toChars(127)[0]) { + newChar = Character.toChars(176)[0]; + } else { + newChar = c; + } + + CoderResult result = originalEncoder.encode( + CharBuffer.wrap(new char[] { newChar }), + out, + true + ); + if (result.isOverflow()) { + return result; + } + } + return CoderResult.UNDERFLOW; + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/IMessageEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/IMessageEditor.java new file mode 100644 index 00000000..4a80b277 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/IMessageEditor.java @@ -0,0 +1,41 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.HttpService; +import burp.api.montoya.http.message.HttpMessage; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.Result; + +/** + * Interface declaring all the necessary methods to implement a Scalpel editor + * If you wish to implement your own type of editor, you should use the AbstractEditor class as a base. + */ +public interface IMessageEditor + extends + ExtensionProvidedHttpRequestEditor, + ExtensionProvidedHttpResponseEditor { + HttpRequestResponse getRequestResponse(); + + boolean setRequestResponseInternal(HttpRequestResponse requestResponse); + + HttpService getHttpService(); + + Result executeHook(HttpRequestResponse reqRes) + throws Throwable; + + boolean updateContent(HttpRequestResponse reqRes); + + EditorType getEditorType(); + + String getId(); + + EditorCreationContext getCtx(); + + HttpMessage getMessage(); + + HttpMessage processOutboundMessage(); +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelBinaryEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelBinaryEditor.java new file mode 100644 index 00000000..35bd6228 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelBinaryEditor.java @@ -0,0 +1,33 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import org.exbin.bined.CodeType; + +public class ScalpelBinaryEditor extends ScalpelGenericBinaryEditor { + + public ScalpelBinaryEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor + ) { + super( + name, + editable, + API, + creationContext, + type, + provider, + executor, + CodeType.BINARY + ); + this.editor.setMaxBytesPerRow(8); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelDecimalEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelDecimalEditor.java new file mode 100644 index 00000000..05e63689 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelDecimalEditor.java @@ -0,0 +1,33 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import org.exbin.bined.CodeType; + +public class ScalpelDecimalEditor extends ScalpelGenericBinaryEditor { + + public ScalpelDecimalEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor + ) { + super( + name, + editable, + API, + creationContext, + type, + provider, + executor, + CodeType.DECIMAL + ); + // this.editor.setMaxBytesPerRow(11); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelGenericBinaryEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelGenericBinaryEditor.java new file mode 100644 index 00000000..01bc9e79 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelGenericBinaryEditor.java @@ -0,0 +1,183 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import burp.api.montoya.ui.editor.extension.EditorMode; +import java.awt.*; +import java.io.IOException; +import java.util.Optional; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import lexfo.scalpel.ScalpelLogger; +import org.exbin.auxiliary.paged_data.BinaryData; +import org.exbin.auxiliary.paged_data.ByteArrayEditableData; +import org.exbin.bined.CodeType; +import org.exbin.bined.EditMode; +import org.exbin.bined.SelectionRange; +import org.exbin.bined.swing.basic.CodeArea; + +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.html +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.html +/** + Hexadecimal editor implementation for a Scalpel editor + Users can press their keyboard's INSER key to enter insertion mode + (which is impossible in Burp's native hex editor) +*/ +public class ScalpelGenericBinaryEditor extends AbstractEditor { + + protected final CodeArea editor; + + private ByteArray oldContent = null; + + /** + Constructs a new Scalpel editor. + + @param API The Montoya API object. + @param creationContext The EditorCreationContext object containing information about the editor. + @param type The editor type (REQUEST or RESPONSE). + @param provider The ScalpelProvidedEditor object that instantiated this editor. + @param executor The executor to use. + */ + public ScalpelGenericBinaryEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor, + CodeType mode + ) { + super(name, editable, API, creationContext, type, provider, executor); + try { + // Create the base BinEd editor component. + this.editor = new CodeArea(); + this.editor.setCodeType(mode); + this.editor.setFont(API.userInterface().currentEditorFont()); + + // Charset to display whitespaces as something else. + // editor.setCharset(new DisplayableWhiteSpaceCharset()); + + // Decide wherever the editor must be editable or read only depending on context. + final boolean isEditable = + editable && + creationContext.editorMode() != EditorMode.READ_ONLY; + + // EXPANDING means that editing the content and modifying the content in a way that increases it's total size are allowed. + // (as opposed to INPLACE or CAPPED where exceeding data is removed) + final EditMode editMode = isEditable + ? EditMode.EXPANDING + : EditMode.READ_ONLY; + + editor.setEditMode(editMode); + } catch (Throwable e) { + // Log the error. + ScalpelLogger.error("Couldn't instantiate new editor:"); + + // Log the stack trace. + ScalpelLogger.logStackTrace(e); + + // Throw the error again. + throw e; + } + } + + /** + * Convert from Burp format to BinEd format + * @param binaryData Bytes as Burp format + * @return Bytes as BinEd format + */ + private BinaryData byteArrayToBinaryData(ByteArray byteArray) { + return new ByteArrayEditableData(byteArray.getBytes()); + } + + /** + * Convert from BinEd format to Burp format + * @param binaryData Bytes as BinEd format + * @return Bytes as Burp format + */ + private ByteArray binaryDataToByteArray(BinaryData binaryData) { + // Load the data + final ByteArrayEditableData buffer = new ByteArrayEditableData(); + try { + buffer.loadFromStream(binaryData.getDataInputStream()); + } catch (IOException ex) { + throw new RuntimeException( + "Unexpected error happened while loading bytes from hex editor" + ); + } + + final byte[] bytes = buffer.getData(); + + // Convert bytes to Burp ByteArray + return ByteArray.byteArray(bytes); + } + + protected void setEditorContent(ByteArray bytes) { + // Convert from burp format to BinEd format + final BinaryData newContent = byteArrayToBinaryData(bytes); + editor.setContentData(newContent); + + // Keep the old content for isModified() + oldContent = bytes; + } + + protected ByteArray getEditorContent() { + try { + // Convert BinEd format to Burp format + return binaryDataToByteArray(editor.getContentData()); + } catch (RuntimeException ex) { + // We have to catch and handle this here because otherwise Burp explodes + ScalpelLogger.error("Couldn't convert bytes:"); + ScalpelLogger.logStackTrace(ex); + + return oldContent; + } + } + + /** + Returns the underlying UI component. + (called by Burp) + + @return The underlying UI component. + */ + @Override + public Component getUiComponent() { + return editor; + } + + /** + Returns the selected data. + (called by Burp) + + @return The selected data. + */ + @Override + public Selection selectedData() { + final SelectionRange selected = editor.getSelection(); + + // Convert BinEd selection range to Burp selection range + return Selection.selection( + (int) selected.getStart(), + (int) selected.getEnd() + ); + } + + /** + Returns whether the editor has been modified. + (called by Burp) + + @return Whether the editor has been modified. + */ + @Override + public boolean isModified() { + // Check if current content is the same as the provided data. + return Optional + .ofNullable(this.getEditorContent()) + .map(c -> !c.equals(oldContent)) + .orElse(false); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelHexEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelHexEditor.java new file mode 100644 index 00000000..8aa96237 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelHexEditor.java @@ -0,0 +1,32 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import org.exbin.bined.CodeType; + +public class ScalpelHexEditor extends ScalpelGenericBinaryEditor { + + public ScalpelHexEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor + ) { + super( + name, + editable, + API, + creationContext, + type, + provider, + executor, + CodeType.HEXADECIMAL + ); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelOctalEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelOctalEditor.java new file mode 100644 index 00000000..0f2cdf47 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelOctalEditor.java @@ -0,0 +1,33 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import org.exbin.bined.CodeType; + +public class ScalpelOctalEditor extends ScalpelGenericBinaryEditor { + + public ScalpelOctalEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor + ) { + super( + name, + editable, + API, + creationContext, + type, + provider, + executor, + CodeType.OCTAL + ); + // this.editor.setMaxBytesPerRow(10); + } +} diff --git a/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelRawEditor.java b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelRawEditor.java new file mode 100644 index 00000000..cb1b5467 --- /dev/null +++ b/scalpel/src/main/java/lexfo/scalpel/editors/ScalpelRawEditor.java @@ -0,0 +1,108 @@ +package lexfo.scalpel.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.editor.RawEditor; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import burp.api.montoya.ui.editor.extension.EditorMode; +import java.awt.*; +import lexfo.scalpel.EditorType; +import lexfo.scalpel.ScalpelEditorTabbedPane; +import lexfo.scalpel.ScalpelExecutor; +import lexfo.scalpel.ScalpelLogger; + +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpRequestEditor.html +// https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/ui/editor/extension/ExtensionProvidedHttpResponseEditor.html +/** + Provides an UI text editor component for editing HTTP requests or responses. + Calls Python scripts to initialize the editor and update the requests or responses. +*/ +public class ScalpelRawEditor extends AbstractEditor { + + private final RawEditor editor; + + /** + Constructs a new Scalpel editor. + + @param API The Montoya API object. + @param creationContext The EditorCreationContext object containing information about the editor. + @param type The editor type (REQUEST or RESPONSE). + @param provider The ScalpelProvidedEditor object that instantiated this editor. + @param executor The executor to use. + */ + + public ScalpelRawEditor( + String name, + Boolean editable, + MontoyaApi API, + EditorCreationContext creationContext, + EditorType type, + ScalpelEditorTabbedPane provider, + ScalpelExecutor executor + ) { + super(name, editable, API, creationContext, type, provider, executor); + try { + // Create a new editor UI component. + // For some reason, when called from an asynchronous method, + // this method might stop executing when calling createRawEditor(), without throwing anything + // This results in a Burp deadlock and is probably due to one of the many race conditions in Burp. + this.editor = API.userInterface().createRawEditor(); + + // Decide wherever the editor must be editable or read only depending on context. + editor.setEditable( + editable && creationContext.editorMode() != EditorMode.READ_ONLY + ); + } catch (Throwable e) { + // Log the error. + ScalpelLogger.error("Couldn't instantiate new editor:"); + + // Log the stack trace. + ScalpelLogger.logStackTrace(e); + + // Throw the error again. + throw e; + } + } + + protected void setEditorContent(ByteArray bytes) { + editor.setContents(bytes); + } + + protected ByteArray getEditorContent() { + return editor.getContents(); + } + + /** + Returns the underlying UI component. + (called by Burp) + + @return The underlying UI component. + */ + @Override + public Component getUiComponent() { + return editor.uiComponent(); + } + + /** + Returns the selected data. + (called by Burp) + + @return The selected data. + */ + @Override + public Selection selectedData() { + return editor.selection().orElse(null); + } + + /** + Returns whether the editor has been modified. + (called by Burp) + + @return Whether the editor has been modified. + */ + @Override + public boolean isModified() { + return editor.isModified(); + } +} diff --git a/scalpel/src/main/resources/python3-10/__init__.py b/scalpel/src/main/resources/python3-10/__init__.py new file mode 100644 index 00000000..75c96fb3 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/__init__.py @@ -0,0 +1,27 @@ +""" +# Python libraries bundled with Scalpel + +--- + +## [pyscalpel](python3-10/pyscalpel.html) +This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension. + +It provides many utilities to manipulate HTTP requests, responses and converting data. + +--- + +## [qs](python3-10/qs.html) +A small module to parse PHP-style query strings. + +Used by pyscalpel + +--- + +# [↠Go back to the user documentation](../) +""" + +import pyscalpel +import qs + + +__all__ = ["pyscalpel", "qs"] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/certs.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/certs.py new file mode 100644 index 00000000..08a7c816 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/certs.py @@ -0,0 +1,91 @@ +import contextlib +import datetime +import ipaddress +import os +import re +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Tuple, Optional, Union, Dict, List, NewType + +# from cryptography import x509 +# from cryptography.hazmat.primitives import hashes, serialization +# from cryptography.hazmat.primitives.asymmetric import rsa, dsa, ec +# from cryptography.hazmat.primitives.serialization import pkcs12 +# from cryptography.x509 import NameOID, ExtendedKeyUsageOID + +# import OpenSSL + +from _internal_mitmproxy.coretypes import serializable + +# Default expiry must not be too long: https://github.com/_internal_mitmproxy/_internal_mitmproxy/issues/815 +CA_EXPIRY = datetime.timedelta(days=10 * 365) +CERT_EXPIRY = datetime.timedelta(days=365) + +# Generated with "openssl dhparam". It's too slow to generate this on startup. +DEFAULT_DHPARAM = b""" +-----BEGIN DH PARAMETERS----- +MIICCAKCAgEAyT6LzpwVFS3gryIo29J5icvgxCnCebcdSe/NHMkD8dKJf8suFCg3 +O2+dguLakSVif/t6dhImxInJk230HmfC8q93hdcg/j8rLGJYDKu3ik6H//BAHKIv +j5O9yjU3rXCfmVJQic2Nne39sg3CreAepEts2TvYHhVv3TEAzEqCtOuTjgDv0ntJ +Gwpj+BJBRQGG9NvprX1YGJ7WOFBP/hWU7d6tgvE6Xa7T/u9QIKpYHMIkcN/l3ZFB +chZEqVlyrcngtSXCROTPcDOQ6Q8QzhaBJS+Z6rcsd7X+haiQqvoFcmaJ08Ks6LQC +ZIL2EtYJw8V8z7C0igVEBIADZBI6OTbuuhDwRw//zU1uq52Oc48CIZlGxTYG/Evq +o9EWAXUYVzWkDSTeBH1r4z/qLPE2cnhtMxbFxuvK53jGB0emy2y1Ei6IhKshJ5qX +IB/aE7SSHyQ3MDHHkCmQJCsOd4Mo26YX61NZ+n501XjqpCBQ2+DfZCBh8Va2wDyv +A2Ryg9SUz8j0AXViRNMJgJrr446yro/FuJZwnQcO3WQnXeqSBnURqKjmqkeFP+d8 +6mk2tqJaY507lRNqtGlLnj7f5RNoBFJDCLBNurVgfvq9TCVWKDIFD4vZRjCrnl6I +rD693XKIHUCWOjMh1if6omGXKHH40QuME2gNa50+YPn1iYDl88uDbbMCAQI= +-----END DH PARAMETERS----- +""" + + +class Cert(serializable.Serializable): ... + + +def _name_to_keyval(name) -> List[Tuple[str, str]]: + parts = [] + for attr in name: + # pyca cryptography <35.0.0 backwards compatiblity + if hasattr(name, "rfc4514_attribute_name"): # pragma: no cover + k = attr.rfc4514_attribute_name # type: ignore + else: # pragma: no cover + k = attr.rfc4514_string().partition("=")[0] + v = attr.value + parts.append((k, v)) + return parts + + +def create_ca( + organization: str, + cn: str, + key_size: int, +): ... + + +def dummy_cert( + privkey, + cacert, + commonname: Optional[str], + sans: List[str], + organization: Optional[str] = None, +) -> Cert: ... + + +@dataclass(frozen=True) +class CertStoreEntry: ... + + +TCustomCertId = str # manually provided certs (e.g. _internal_mitmproxy's --certs) +TGeneratedCertId = Tuple[Optional[str], Tuple[str, ...]] # (common_name, sans) +TCertId = Union[TCustomCertId, TGeneratedCertId] + +DHParams = NewType("DHParams", bytes) + + +class CertStore: ... + + +def load_pem_private_key( + data: bytes, password: Optional[bytes] +): ... diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/command.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/command.py new file mode 100644 index 00000000..3b09cbe1 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/command.py @@ -0,0 +1,323 @@ +""" + This module manages and invokes typed commands. +""" +import functools +import inspect +import sys +import textwrap +import types +import typing + +import _internal_mitmproxy.types +from _internal_mitmproxy import exceptions, command_lexer +from _internal_mitmproxy.command_lexer import unquote + + +def verify_arg_signature(f: typing.Callable, args: typing.Iterable[typing.Any], kwargs: dict) -> None: + sig = inspect.signature(f) + try: + sig.bind(*args, **kwargs) + except TypeError as v: + raise exceptions.CommandError("command argument mismatch: %s" % v.args[0]) + + +def typename(t: type) -> str: + """ + Translates a type to an explanatory string. + """ + if t == inspect._empty: # type: ignore + raise exceptions.CommandError("missing type annotation") + to = _internal_mitmproxy.types.CommandTypes.get(t, None) + if not to: + raise exceptions.CommandError("unsupported type: %s" % getattr(t, "__name__", t)) + return to.display + + +def _empty_as_none(x: typing.Any) -> typing.Any: + if x == inspect.Signature.empty: + return None + return x + + +class CommandParameter(typing.NamedTuple): + name: str + type: typing.Type + kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD + + def __str__(self): + if self.kind is inspect.Parameter.VAR_POSITIONAL: + return f"*{self.name}" + else: + return self.name + + +class Command: + name: str + manager: "CommandManager" + signature: inspect.Signature + help: typing.Optional[str] + + def __init__(self, manager: "CommandManager", name: str, func: typing.Callable) -> None: + self.name = name + self.manager = manager + self.func = func + self.signature = inspect.signature(self.func) + + if func.__doc__: + txt = func.__doc__.strip() + self.help = "\n".join(textwrap.wrap(txt)) + else: + self.help = None + + # This fails with a CommandException if types are invalid + for name, parameter in self.signature.parameters.items(): + t = parameter.annotation + if not _internal_mitmproxy.types.CommandTypes.get(parameter.annotation, None): + raise exceptions.CommandError(f"Argument {name} has an unknown type {t} in {func}.") + if self.return_type and not _internal_mitmproxy.types.CommandTypes.get(self.return_type, None): + raise exceptions.CommandError(f"Return type has an unknown type ({self.return_type}) in {func}.") + + @property + def return_type(self) -> typing.Optional[typing.Type]: + return _empty_as_none(self.signature.return_annotation) + + @property + def parameters(self) -> typing.List[CommandParameter]: + """Returns a list of CommandParameters.""" + ret = [] + for name, param in self.signature.parameters.items(): + ret.append(CommandParameter(name, param.annotation, param.kind)) + return ret + + def signature_help(self) -> str: + params = " ".join(str(param) for param in self.parameters) + if self.return_type: + ret = f" -> {typename(self.return_type)}" + else: + ret = "" + return f"{self.name} {params}{ret}" + + def prepare_args(self, args: typing.Sequence[str]) -> inspect.BoundArguments: + try: + bound_arguments = self.signature.bind(*args) + except TypeError: + expected = f'Expected: {str(self.signature.parameters)}' + received = f'Received: {str(args)}' + raise exceptions.CommandError(f"Command argument mismatch: \n {expected}\n {received}") + + for name, value in bound_arguments.arguments.items(): + param = self.signature.parameters[name] + convert_to = param.annotation + if param.kind == param.VAR_POSITIONAL: + bound_arguments.arguments[name] = tuple( + parsearg(self.manager, x, convert_to) + for x in value + ) + else: + bound_arguments.arguments[name] = parsearg(self.manager, value, convert_to) + + bound_arguments.apply_defaults() + + return bound_arguments + + def call(self, args: typing.Sequence[str]) -> typing.Any: + """ + Call the command with a list of arguments. At this point, all + arguments are strings. + """ + bound_args = self.prepare_args(args) + ret = self.func(*bound_args.args, **bound_args.kwargs) + if ret is None and self.return_type is None: + return + typ = _internal_mitmproxy.types.CommandTypes.get(self.return_type) + assert typ + if not typ.is_valid(self.manager, typ, ret): + raise exceptions.CommandError( + f"{self.name} returned unexpected data - expected {typ.display}" + ) + return ret + + +class ParseResult(typing.NamedTuple): + value: str + type: typing.Type + valid: bool + + +class CommandManager: + commands: typing.Dict[str, Command] + + def __init__(self, master): + self.master = master + self.commands = {} + + def collect_commands(self, addon): + for i in dir(addon): + if not i.startswith("__"): + o = getattr(addon, i) + try: + # hasattr is not enough, see https://github.com/_internal_mitmproxy/_internal_mitmproxy/issues/3794 + is_command = isinstance(getattr(o, "command_name", None), str) + except Exception: + pass # getattr may raise if o implements __getattr__. + else: + if is_command: + try: + self.add(o.command_name, o) + except exceptions.CommandError as e: + self.master.log.warn( + f"Could not load command {o.command_name}: {e}" + ) + + def add(self, path: str, func: typing.Callable): + self.commands[path] = Command(self, path, func) + + @functools.lru_cache(maxsize=128) + def parse_partial( + self, + cmdstr: str + ) -> typing.Tuple[typing.Sequence[ParseResult], typing.Sequence[CommandParameter]]: + """ + Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items. + """ + + parts: typing.List[str] = command_lexer.expr.parseString(cmdstr, parseAll=True) + + parsed: typing.List[ParseResult] = [] + next_params: typing.List[CommandParameter] = [ + CommandParameter("", _internal_mitmproxy.types.Cmd), + CommandParameter("", _internal_mitmproxy.types.CmdArgs), + ] + expected: typing.Optional[CommandParameter] = None + for part in parts: + if part.isspace(): + parsed.append( + ParseResult( + value=part, + type=_internal_mitmproxy.types.Space, + valid=True, + ) + ) + continue + + if expected and expected.kind is inspect.Parameter.VAR_POSITIONAL: + assert not next_params + elif next_params: + expected = next_params.pop(0) + else: + expected = CommandParameter("", _internal_mitmproxy.types.Unknown) + + arg_is_known_command = ( + expected.type == _internal_mitmproxy.types.Cmd and part in self.commands + ) + arg_is_unknown_command = ( + expected.type == _internal_mitmproxy.types.Cmd and part not in self.commands + ) + command_args_following = ( + next_params and next_params[0].type == _internal_mitmproxy.types.CmdArgs + ) + if arg_is_known_command and command_args_following: + next_params = self.commands[part].parameters + next_params[1:] + if arg_is_unknown_command and command_args_following: + next_params.pop(0) + + to = _internal_mitmproxy.types.CommandTypes.get(expected.type, None) + valid = False + if to: + try: + to.parse(self, expected.type, part) + except exceptions.TypeError: + valid = False + else: + valid = True + + parsed.append( + ParseResult( + value=part, + type=expected.type, + valid=valid, + ) + ) + + return parsed, next_params + + def call(self, command_name: str, *args: typing.Any) -> typing.Any: + """ + Call a command with native arguments. May raise CommandError. + """ + if command_name not in self.commands: + raise exceptions.CommandError("Unknown command: %s" % command_name) + return self.commands[command_name].func(*args) + + def call_strings(self, command_name: str, args: typing.Sequence[str]) -> typing.Any: + """ + Call a command using a list of string arguments. May raise CommandError. + """ + if command_name not in self.commands: + raise exceptions.CommandError("Unknown command: %s" % command_name) + + return self.commands[command_name].call(args) + + def execute(self, cmdstr: str) -> typing.Any: + """ + Execute a command string. May raise CommandError. + """ + parts, _ = self.parse_partial(cmdstr) + if not parts: + raise exceptions.CommandError(f"Invalid command: {cmdstr!r}") + command_name, *args = [ + unquote(part.value) + for part in parts + if part.type != _internal_mitmproxy.types.Space + ] + return self.call_strings(command_name, args) + + def dump(self, out=sys.stdout) -> None: + cmds = list(self.commands.values()) + cmds.sort(key=lambda x: x.signature_help()) + for c in cmds: + for hl in (c.help or "").splitlines(): + print("# " + hl, file=out) + print(c.signature_help(), file=out) + print(file=out) + + +def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: + """ + Convert a string to a argument to the appropriate type. + """ + t = _internal_mitmproxy.types.CommandTypes.get(argtype, None) + if not t: + raise exceptions.CommandError(f"Unsupported argument type: {argtype}") + try: + return t.parse(manager, argtype, spec) + except exceptions.TypeError as e: + raise exceptions.CommandError(str(e)) from e + + +def command(name: typing.Optional[str] = None): + def decorator(function): + @functools.wraps(function) + def wrapper(*args, **kwargs): + verify_arg_signature(function, args, kwargs) + return function(*args, **kwargs) + + wrapper.__dict__["command_name"] = name or function.__name__.replace("_", ".") + return wrapper + + return decorator + + +def argument(name, type): + """ + Set the type of a command argument at runtime. This is useful for more + specific types such as _internal_mitmproxy.types.Choice, which we cannot annotate + directly as mypy does not like that. + """ + + def decorator(f: types.FunctionType) -> types.FunctionType: + assert name in f.__annotations__ + f.__annotations__[name] = type + return f + + return decorator diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/command_lexer.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/command_lexer.py new file mode 100644 index 00000000..2ba691df --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/command_lexer.py @@ -0,0 +1,41 @@ +import re + +import pyparsing + +# TODO: There is a lot of work to be done here. +# The current implementation is written in a way that _any_ input is valid, +# which does not make sense once things get more complex. + +PartialQuotedString = pyparsing.Regex( + re.compile( + r''' + "[^"]*(?:"|$) # double-quoted string that ends with double quote or EOF + | + '[^']*(?:'|$) # single-quoted string that ends with double quote or EOF + ''', + re.VERBOSE + ) +) + +expr = pyparsing.ZeroOrMore( + PartialQuotedString + | pyparsing.Word(" \r\n\t") + | pyparsing.CharsNotIn("""'" \r\n\t""") +).leaveWhitespace() + + +def quote(val: str) -> str: + if val and all(char not in val for char in "'\" \r\n\t"): + return val + if '"' not in val: + return f'"{val}"' + if "'" not in val: + return f"'{val}'" + return '"' + val.replace('"', r"\x22") + '"' + + +def unquote(x: str) -> str: + if len(x) > 1 and x[0] in "'\"" and x[0] == x[-1]: + return x[1:-1] + else: + return x diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/connection.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/connection.py new file mode 100644 index 00000000..284c2d21 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/connection.py @@ -0,0 +1,386 @@ +import uuid +import warnings +from abc import ABCMeta +from enum import Flag +from typing import Optional, Sequence, Tuple + +from _internal_mitmproxy import certs +from _internal_mitmproxy.coretypes import serializable +from _internal_mitmproxy.net import server_spec +from _internal_mitmproxy.utils import human + + +class ConnectionState(Flag): + """The current state of the underlying socket.""" + CLOSED = 0 + CAN_READ = 1 + CAN_WRITE = 2 + OPEN = CAN_READ | CAN_WRITE + + +# practically speaking we may have IPv6 addresses with flowinfo and scope_id, +# but type checking isn't good enough to properly handle tuple unions. +# this version at least provides useful type checking messages. +Address = Tuple[str, int] + + +class Connection(serializable.Serializable, metaclass=ABCMeta): + """ + Base class for client and server connections. + + The connection object only exposes metadata about the connection, but not the underlying socket object. + This is intentional, all I/O should be handled by `_internal_mitmproxy.proxy.server` exclusively. + """ + # all connections have a unique id. While + # f.client_conn == f2.client_conn already holds true for live flows (where we have object identity), + # we also want these semantics for recorded flows. + id: str + """A unique UUID to identify the connection.""" + state: ConnectionState + """The current connection state.""" + peername: Optional[Address] + """The remote's `(ip, port)` tuple for this connection.""" + sockname: Optional[Address] + """Our local `(ip, port)` tuple for this connection.""" + error: Optional[str] = None + """ + A string describing a general error with connections to this address. + + The purpose of this property is to signal that new connections to the particular endpoint should not be attempted, + for example because it uses an untrusted TLS certificate. Regular (unexpected) disconnects do not set the error + property. This property is only reused per client connection. + """ + + tls: bool = False + """ + `True` if TLS should be established, `False` otherwise. + Note that this property only describes if a connection should eventually be protected using TLS. + To check if TLS has already been established, use `Connection.tls_established`. + """ + certificate_list: Sequence[certs.Cert] = () + """ + The TLS certificate list as sent by the peer. + The first certificate is the end-entity certificate. + + > [RFC 8446] Prior to TLS 1.3, "certificate_list" ordering required each + > certificate to certify the one immediately preceding it; however, + > some implementations allowed some flexibility. Servers sometimes + > send both a current and deprecated intermediate for transitional + > purposes, and others are simply configured incorrectly, but these + > cases can nonetheless be validated properly. For maximum + > compatibility, all implementations SHOULD be prepared to handle + > potentially extraneous certificates and arbitrary orderings from any + > TLS version, with the exception of the end-entity certificate which + > MUST be first. + """ + alpn: Optional[bytes] = None + """The application-layer protocol as negotiated using + [ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation).""" + alpn_offers: Sequence[bytes] = () + """The ALPN offers as sent in the ClientHello.""" + # we may want to add SSL_CIPHER_description here, but that's currently not exposed by cryptography + cipher: Optional[str] = None + """The active cipher name as returned by OpenSSL's `SSL_CIPHER_get_name`.""" + cipher_list: Sequence[str] = () + """Ciphers accepted by the proxy server on this connection.""" + tls_version: Optional[str] = None + """The active TLS version.""" + sni: Optional[str] = None + """ + The [Server Name Indication (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) sent in the ClientHello. + """ + + timestamp_start: Optional[float] + timestamp_end: Optional[float] = None + """*Timestamp:* Connection has been closed.""" + timestamp_tls_setup: Optional[float] = None + """*Timestamp:* TLS handshake has been completed successfully.""" + + @property + def connected(self) -> bool: + """*Read-only:* `True` if Connection.state is ConnectionState.OPEN, `False` otherwise.""" + return self.state is ConnectionState.OPEN + + @property + def tls_established(self) -> bool: + """*Read-only:* `True` if TLS has been established, `False` otherwise.""" + return self.timestamp_tls_setup is not None + + def __eq__(self, other): + if isinstance(other, Connection): + return self.id == other.id + return False + + def __hash__(self): + return hash(self.id) + + def __repr__(self): + attrs = repr({ + k: { + "cipher_list": lambda: f"<{len(v)} ciphers>", + "id": lambda: f"…{v[-6:]}" + }.get(k, lambda: v)() + for k, v in self.__dict__.items() + }) + return f"{type(self).__name__}({attrs})" + + @property + def alpn_proto_negotiated(self) -> Optional[bytes]: # pragma: no cover + """*Deprecated:* An outdated alias for Connection.alpn.""" + warnings.warn("Connection.alpn_proto_negotiated is deprecated, use Connection.alpn instead.", + DeprecationWarning) + return self.alpn + + +class Client(Connection): + """A connection between a client and _internal_mitmproxy.""" + peername: Address + """The client's address.""" + sockname: Address + """The local address we received this connection on.""" + + mitmcert: Optional[certs.Cert] = None + """ + The certificate used by _internal_mitmproxy to establish TLS with the client. + """ + + timestamp_start: float + """*Timestamp:* TCP SYN received""" + + def __init__(self, peername: Address, sockname: Address, timestamp_start: float): + self.id = str(uuid.uuid4()) + self.peername = peername + self.sockname = sockname + self.timestamp_start = timestamp_start + self.state = ConnectionState.OPEN + + def __str__(self): + if self.alpn: + tls_state = f", alpn={self.alpn.decode(errors='replace')}" + elif self.tls_established: + tls_state = ", tls" + else: + tls_state = "" + return f"Client({human.format_address(self.peername)}, state={self.state.name.lower()}{tls_state})" + + def get_state(self): + # Important: Retain full compatibility with old proxy core for now! + # This means we need to add all new fields to the old implementation. + return { + 'address': self.peername, + 'alpn': self.alpn, + 'cipher_name': self.cipher, + 'id': self.id, + 'mitmcert': self.mitmcert.get_state() if self.mitmcert is not None else None, + 'sni': self.sni, + 'timestamp_end': self.timestamp_end, + 'timestamp_start': self.timestamp_start, + 'timestamp_tls_setup': self.timestamp_tls_setup, + 'tls_established': self.tls_established, + 'tls_extensions': [], + 'tls_version': self.tls_version, + # only used in sans-io + 'state': self.state.value, + 'sockname': self.sockname, + 'error': self.error, + 'tls': self.tls, + 'certificate_list': [x.get_state() for x in self.certificate_list], + 'alpn_offers': self.alpn_offers, + 'cipher_list': self.cipher_list, + } + + @classmethod + def from_state(cls, state) -> "Client": + client = Client( + state["address"], + ("_internal_mitmproxy", 8080), + state["timestamp_start"] + ) + client.set_state(state) + return client + + def set_state(self, state): + self.peername = tuple(state["address"]) if state["address"] else None + self.alpn = state["alpn"] + self.cipher = state["cipher_name"] + self.id = state["id"] + self.sni = state["sni"] + self.timestamp_end = state["timestamp_end"] + self.timestamp_start = state["timestamp_start"] + self.timestamp_tls_setup = state["timestamp_tls_setup"] + self.tls_version = state["tls_version"] + # only used in sans-io + self.state = ConnectionState(state["state"]) + self.sockname = tuple(state["sockname"]) if state["sockname"] else None + self.error = state["error"] + self.tls = state["tls"] + self.certificate_list = [certs.Cert.from_state(x) for x in state["certificate_list"]] + self.mitmcert = certs.Cert.from_state(state["mitmcert"]) if state["mitmcert"] is not None else None + self.alpn_offers = state["alpn_offers"] + self.cipher_list = state["cipher_list"] + + @property + def address(self): # pragma: no cover + """*Deprecated:* An outdated alias for Client.peername.""" + warnings.warn("Client.address is deprecated, use Client.peername instead.", DeprecationWarning, stacklevel=2) + return self.peername + + @address.setter + def address(self, x): # pragma: no cover + warnings.warn("Client.address is deprecated, use Client.peername instead.", DeprecationWarning, stacklevel=2) + self.peername = x + + @property + def cipher_name(self) -> Optional[str]: # pragma: no cover + """*Deprecated:* An outdated alias for Connection.cipher.""" + warnings.warn("Client.cipher_name is deprecated, use Client.cipher instead.", DeprecationWarning, stacklevel=2) + return self.cipher + + @property + def clientcert(self) -> Optional[certs.Cert]: # pragma: no cover + """*Deprecated:* An outdated alias for Connection.certificate_list[0].""" + warnings.warn("Client.clientcert is deprecated, use Client.certificate_list instead.", DeprecationWarning, + stacklevel=2) + if self.certificate_list: + return self.certificate_list[0] + else: + return None + + @clientcert.setter + def clientcert(self, val): # pragma: no cover + warnings.warn("Client.clientcert is deprecated, use Client.certificate_list instead.", DeprecationWarning) + if val: + self.certificate_list = [val] + else: + self.certificate_list = [] + + +class Server(Connection): + """A connection between _internal_mitmproxy and an upstream server.""" + + peername: Optional[Address] = None + """The server's resolved `(ip, port)` tuple. Will be set during connection establishment.""" + sockname: Optional[Address] = None + address: Optional[Address] + """The server's `(host, port)` address tuple. The host can either be a domain or a plain IP address.""" + + timestamp_start: Optional[float] = None + """*Timestamp:* TCP SYN sent.""" + timestamp_tcp_setup: Optional[float] = None + """*Timestamp:* TCP ACK received.""" + + via: Optional[server_spec.ServerSpec] = None + """An optional proxy server specification via which the connection should be established.""" + + def __init__(self, address: Optional[Address]): + self.id = str(uuid.uuid4()) + self.address = address + self.state = ConnectionState.CLOSED + + def __str__(self): + if self.alpn: + tls_state = f", alpn={self.alpn.decode(errors='replace')}" + elif self.tls_established: + tls_state = ", tls" + else: + tls_state = "" + if self.sockname: + local_port = f", src_port={self.sockname[1]}" + else: + local_port = "" + return f"Server({human.format_address(self.address)}, state={self.state.name.lower()}{tls_state}{local_port})" + + def __setattr__(self, name, value): + if name in ("address", "via"): + connection_open = self.__dict__.get("state", ConnectionState.CLOSED) is ConnectionState.OPEN + # assigning the current value is okay, that may be an artifact of calling .set_state(). + attr_changed = self.__dict__.get(name) != value + if connection_open and attr_changed: + raise RuntimeError(f"Cannot change server.{name} on open connection.") + return super().__setattr__(name, value) + + def get_state(self): + return { + 'address': self.address, + 'alpn': self.alpn, + 'id': self.id, + 'ip_address': self.peername, + 'sni': self.sni, + 'source_address': self.sockname, + 'timestamp_end': self.timestamp_end, + 'timestamp_start': self.timestamp_start, + 'timestamp_tcp_setup': self.timestamp_tcp_setup, + 'timestamp_tls_setup': self.timestamp_tls_setup, + 'tls_established': self.tls_established, + 'tls_version': self.tls_version, + 'via': None, + # only used in sans-io + 'state': self.state.value, + 'error': self.error, + 'tls': self.tls, + 'certificate_list': [x.get_state() for x in self.certificate_list], + 'alpn_offers': self.alpn_offers, + 'cipher_name': self.cipher, + 'cipher_list': self.cipher_list, + 'via2': self.via, + } + + @classmethod + def from_state(cls, state) -> "Server": + server = Server(None) + server.set_state(state) + return server + + def set_state(self, state): + self.address = tuple(state["address"]) if state["address"] else None + self.alpn = state["alpn"] + self.id = state["id"] + self.peername = tuple(state["ip_address"]) if state["ip_address"] else None + self.sni = state["sni"] + self.sockname = tuple(state["source_address"]) if state["source_address"] else None + self.timestamp_end = state["timestamp_end"] + self.timestamp_start = state["timestamp_start"] + self.timestamp_tcp_setup = state["timestamp_tcp_setup"] + self.timestamp_tls_setup = state["timestamp_tls_setup"] + self.tls_version = state["tls_version"] + self.state = ConnectionState(state["state"]) + self.error = state["error"] + self.tls = state["tls"] + self.certificate_list = [certs.Cert.from_state(x) for x in state["certificate_list"]] + self.alpn_offers = state["alpn_offers"] + self.cipher = state["cipher_name"] + self.cipher_list = state["cipher_list"] + self.via = state["via2"] + + @property + def ip_address(self) -> Optional[Address]: # pragma: no cover + """*Deprecated:* An outdated alias for `Server.peername`.""" + warnings.warn("Server.ip_address is deprecated, use Server.peername instead.", DeprecationWarning, stacklevel=2) + return self.peername + + @property + def cert(self) -> Optional[certs.Cert]: # pragma: no cover + """*Deprecated:* An outdated alias for `Connection.certificate_list[0]`.""" + warnings.warn("Server.cert is deprecated, use Server.certificate_list instead.", DeprecationWarning, + stacklevel=2) + if self.certificate_list: + return self.certificate_list[0] + else: + return None + + @cert.setter + def cert(self, val): # pragma: no cover + warnings.warn("Server.cert is deprecated, use Server.certificate_list instead.", DeprecationWarning, + stacklevel=2) + if val: + self.certificate_list = [val] + else: + self.certificate_list = [] + + +__all__ = [ + "Connection", + "Client", + "Server", + "ConnectionState" +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/basethread.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/basethread.py new file mode 100644 index 00000000..a3c81d19 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/basethread.py @@ -0,0 +1,14 @@ +import time +import threading + + +class BaseThread(threading.Thread): + def __init__(self, name, *args, **kwargs): + super().__init__(name=name, *args, **kwargs) + self._thread_started = time.time() + + def _threadinfo(self): + return "%s - age: %is" % ( + self.name, + int(time.time() - self._thread_started) + ) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/bidi.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/bidi.py new file mode 100644 index 00000000..49340429 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/bidi.py @@ -0,0 +1,27 @@ +class BiDi: + + """ + A wee utility class for keeping bi-directional mappings, like field + constants in protocols. Names are attributes on the object, dict-like + access maps values to names: + + CONST = BiDi(a=1, b=2) + assert CONST.a == 1 + assert CONST.get_name(1) == "a" + """ + + def __init__(self, **kwargs): + self.names = kwargs + self.values = {} + for k, v in kwargs.items(): + self.values[v] = k + if len(self.names) != len(self.values): + raise ValueError("Duplicate values not allowed.") + + def __getattr__(self, k): + if k in self.names: + return self.names[k] + raise AttributeError("No such attribute: %s", k) + + def get_name(self, n, default=None): + return self.values.get(n, default) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/multidict.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/multidict.py new file mode 100644 index 00000000..9a62304f --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/multidict.py @@ -0,0 +1,229 @@ +from abc import ABCMeta +from abc import abstractmethod +from typing import Iterator +from typing import List +from typing import MutableMapping +from typing import Sequence +from typing import Tuple +from typing import TypeVar + +from _internal_mitmproxy.coretypes import serializable + +KT = TypeVar('KT') +VT = TypeVar('VT') + + +class _MultiDict(MutableMapping[KT, VT], metaclass=ABCMeta): + """ + A MultiDict is a dictionary-like data structure that supports multiple values per key. + """ + + fields: Tuple[Tuple[KT, VT], ...] + """The underlying raw datastructure.""" + + def __repr__(self): + fields = ( + repr(field) + for field in self.fields + ) + return "{cls}[{fields}]".format( + cls=type(self).__name__, + fields=", ".join(fields) + ) + + @staticmethod + @abstractmethod + def _reduce_values(values: Sequence[VT]) -> VT: + """ + If a user accesses multidict["foo"], this method + reduces all values for "foo" to a single value that is returned. + For example, HTTP headers are folded, whereas we will just take + the first cookie we found with that name. + """ + + @staticmethod + @abstractmethod + def _kconv(key: KT) -> KT: + """ + This method converts a key to its canonical representation. + For example, HTTP headers are case-insensitive, so this method returns key.lower(). + """ + + def __getitem__(self, key: KT) -> VT: + values = self.get_all(key) + if not values: + raise KeyError(key) + return self._reduce_values(values) + + def __setitem__(self, key: KT, value: VT) -> None: + self.set_all(key, [value]) + + def __delitem__(self, key: KT) -> None: + if key not in self: + raise KeyError(key) + key = self._kconv(key) + self.fields = tuple( + field for field in self.fields + if key != self._kconv(field[0]) + ) + + def __iter__(self) -> Iterator[KT]: + seen = set() + for key, _ in self.fields: + key_kconv = self._kconv(key) + if key_kconv not in seen: + seen.add(key_kconv) + yield key + + def __len__(self) -> int: + return len({self._kconv(key) for key, _ in self.fields}) + + def __eq__(self, other) -> bool: + if isinstance(other, MultiDict): + return self.fields == other.fields + return False + + def get_all(self, key: KT) -> List[VT]: + """ + Return the list of all values for a given key. + If that key is not in the MultiDict, the return value will be an empty list. + """ + key = self._kconv(key) + return [ + value + for k, value in self.fields + if self._kconv(k) == key + ] + + def set_all(self, key: KT, values: List[VT]) -> None: + """ + Remove the old values for a key and add new ones. + """ + key_kconv = self._kconv(key) + + new_fields: List[Tuple[KT, VT]] = [] + for field in self.fields: + if self._kconv(field[0]) == key_kconv: + if values: + new_fields.append( + (field[0], values.pop(0)) + ) + else: + new_fields.append(field) + while values: + new_fields.append( + (key, values.pop(0)) + ) + self.fields = tuple(new_fields) + + def add(self, key: KT, value: VT) -> None: + """ + Add an additional value for the given key at the bottom. + """ + self.insert(len(self.fields), key, value) + + def insert(self, index: int, key: KT, value: VT) -> None: + """ + Insert an additional value for the given key at the specified position. + """ + item = (key, value) + self.fields = self.fields[:index] + (item,) + self.fields[index:] + + def keys(self, multi: bool = False): + """ + Get all keys. + + If `multi` is True, one key per value will be returned. + If `multi` is False, duplicate keys will only be returned once. + """ + return ( + k + for k, _ in self.items(multi) + ) + + def values(self, multi: bool = False): + """ + Get all values. + + If `multi` is True, all values will be returned. + If `multi` is False, only the first value per key will be returned. + """ + return ( + v + for _, v in self.items(multi) + ) + + def items(self, multi: bool = False): + """ + Get all (key, value) tuples. + + If `multi` is True, all `(key, value)` pairs will be returned. + If False, only one tuple per key is returned. + """ + if multi: + return self.fields + else: + return super().items() + + +class MultiDict(_MultiDict[KT, VT], serializable.Serializable): + """A concrete MultiDict, storing its own data.""" + + def __init__(self, fields=()): + super().__init__() + self.fields = tuple( + tuple(i) for i in fields + ) + + @staticmethod + def _reduce_values(values): + return values[0] + + @staticmethod + def _kconv(key): + return key + + def get_state(self): + return self.fields + + def set_state(self, state): + self.fields = tuple(tuple(x) for x in state) + + @classmethod + def from_state(cls, state): + return cls(state) + + +class MultiDictView(_MultiDict[KT, VT]): + """ + The MultiDictView provides the MultiDict interface over calculated data. + The view itself contains no state - data is retrieved from the parent on + request, and stored back to the parent on change. + """ + + def __init__(self, getter, setter): + self._getter = getter + self._setter = setter + super().__init__() + + @staticmethod + def _kconv(key): + # All request-attributes are case-sensitive. + return key + + @staticmethod + def _reduce_values(values): + # We just return the first element if + # multiple elements exist with the same key. + return values[0] + + @property # type: ignore + def fields(self): + return self._getter() + + @fields.setter + def fields(self, value): + self._setter(value) + + def copy(self) -> "MultiDict[KT,VT]": + return MultiDict(self.fields) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/serializable.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/serializable.py new file mode 100644 index 00000000..f582293f --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/coretypes/serializable.py @@ -0,0 +1,39 @@ +import abc +import uuid +from typing import Type, TypeVar + +T = TypeVar('T', bound='Serializable') + + +class Serializable(metaclass=abc.ABCMeta): + """ + Abstract Base Class that defines an API to save an object's state and restore it later on. + """ + + @classmethod + @abc.abstractmethod + def from_state(cls: Type[T], state) -> T: + """ + Create a new object from the given state. + """ + raise NotImplementedError() + + @abc.abstractmethod + def get_state(self): + """ + Retrieve object state. + """ + raise NotImplementedError() + + @abc.abstractmethod + def set_state(self, state): + """ + Set object state to the given state. + """ + raise NotImplementedError() + + def copy(self: T) -> T: + state = self.get_state() + if isinstance(state, dict) and "id" in state: + state["id"] = str(uuid.uuid4()) + return self.from_state(state) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/exceptions.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/exceptions.py new file mode 100644 index 00000000..2a88e141 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/exceptions.py @@ -0,0 +1,56 @@ +""" + +Edit 2020-12 @mhils: + The advice below hasn't paid off in any form. We now just use builtin exceptions and specialize where necessary. + +--- + +We try to be very hygienic regarding the exceptions we throw: + +- Every exception that might be externally visible to users shall be a subclass + of _internal_mitmproxyException.p +- Every exception in the base net module shall be a subclass + of NetlibException, and will not be propagated directly to users. + +See also: http://lucumr.pocoo.org/2014/10/16/on-error-handling/ +""" + + +class _internal_mitmproxyException(Exception): + """ + Base class for all exceptions thrown by _internal_mitmproxy. + """ + + def __init__(self, message=None): + super().__init__(message) + + +class FlowReadException(_internal_mitmproxyException): + pass + + +class ControlException(_internal_mitmproxyException): + pass + + +class CommandError(Exception): + pass + + +class OptionsError(_internal_mitmproxyException): + pass + + +class AddonManagerError(_internal_mitmproxyException): + pass + + +class AddonHalt(_internal_mitmproxyException): + """ + Raised by addons to signal that no further handlers should handle this event. + """ + pass + + +class TypeError(_internal_mitmproxyException): + pass diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/flow.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/flow.py new file mode 100644 index 00000000..89627bed --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/flow.py @@ -0,0 +1,264 @@ +import asyncio +import time +import typing # noqa +import uuid + +from _internal_mitmproxy import connection +from _internal_mitmproxy import exceptions +from _internal_mitmproxy import stateobject +from _internal_mitmproxy import version + + +class Error(stateobject.StateObject): + """ + An Error. + + This is distinct from an protocol error response (say, a HTTP code 500), + which is represented by a normal `_internal_mitmproxy.http.Response` object. This class is + responsible for indicating errors that fall outside of normal protocol + communications, like interrupted connections, timeouts, or protocol errors. + """ + + msg: str + """Message describing the error.""" + + timestamp: float + """Unix timestamp of when this error happened.""" + + KILLED_MESSAGE: typing.ClassVar[str] = "Connection killed." + + def __init__(self, msg: str, timestamp: typing.Optional[float] = None) -> None: + """Create an error. If no timestamp is passed, the current time is used.""" + self.msg = msg + self.timestamp = timestamp or time.time() + + _stateobject_attributes = dict( + msg=str, + timestamp=float + ) + + def __str__(self): + return self.msg + + def __repr__(self): + return self.msg + + @classmethod + def from_state(cls, state): + # the default implementation assumes an empty constructor. Override + # accordingly. + f = cls(None) + f.set_state(state) + return f + + +class Flow(stateobject.StateObject): + """ + Base class for network flows. A flow is a collection of objects, + for example HTTP request/response pairs or a list of TCP messages. + + See also: + - _internal_mitmproxy.http.HTTPFlow + - _internal_mitmproxy.tcp.TCPFlow + """ + client_conn: connection.Client + """The client that connected to _internal_mitmproxy.""" + + server_conn: connection.Server + """ + The server _internal_mitmproxy connected to. + + Some flows may never cause _internal_mitmproxy to initiate a server connection, + for example because their response is replayed by _internal_mitmproxy itself. + To simplify implementation, those flows will still have a `server_conn` attribute + with a `timestamp_start` set to `None`. + """ + + error: typing.Optional[Error] = None + """A connection or protocol error affecting this flow.""" + + intercepted: bool + """ + If `True`, the flow is currently paused by _internal_mitmproxy. + We're waiting for a user action to forward the flow to its destination. + """ + + marked: str = "" + """ + If this attribute is a non-empty string the flow has been marked by the user. + + A string value will be used as the marker annotation. May either be a single character or a Unicode emoji name. + + For example `:grapes:` becomes `ðŸ‡` in views that support emoji rendering. + Consult the [Github API Emoji List](https://api.github.com/emojis) for a list of emoji that may be used. + Not all emoji, especially [emoji modifiers](https://en.wikipedia.org/wiki/Miscellaneous_Symbols_and_Pictographs#Emoji_modifiers) + will render consistently. + + The default marker for the view will be used if the Unicode emoji name can not be interpreted. + """ + + is_replay: typing.Optional[str] + """ + This attribute indicates if this flow has been replayed in either direction. + + - a value of `request` indicates that the request has been artifically replayed by _internal_mitmproxy to the server. + - a value of `response` indicates that the response to the client's request has been set by server replay. + """ + + live: bool + """ + If `True`, the flow belongs to a currently active connection. + If `False`, the flow may have been already completed or loaded from disk. + """ + + def __init__( + self, + type: str, + client_conn: connection.Client, + server_conn: connection.Server, + live: bool = False, + ) -> None: + self.type = type + self.id = str(uuid.uuid4()) + self.client_conn = client_conn + self.server_conn = server_conn + self.live = live + + self.intercepted: bool = False + self._resume_event: typing.Optional[asyncio.Event] = None + self._backup: typing.Optional[Flow] = None + self.marked: str = "" + self.is_replay: typing.Optional[str] = None + self.metadata: typing.Dict[str, typing.Any] = dict() + self.comment: str = "" + + _stateobject_attributes = dict( + id=str, + error=Error, + client_conn=connection.Client, + server_conn=connection.Server, + type=str, + intercepted=bool, + is_replay=str, + marked=str, + metadata=typing.Dict[str, typing.Any], + comment=str, + ) + + def get_state(self): + d = super().get_state() + d.update(version=version.FLOW_FORMAT_VERSION) + if self._backup and self._backup != d: + d.update(backup=self._backup) + return d + + def set_state(self, state): + state = state.copy() + state.pop("version") + if "backup" in state: + self._backup = state.pop("backup") + super().set_state(state) + + @classmethod + def from_state(cls, state): + f = cls(None, None) + f.set_state(state) + return f + + def copy(self): + """Make a copy of this flow.""" + f = super().copy() + f.live = False + return f + + def modified(self): + """ + `True` if this file has been modified by a user, `False` otherwise. + """ + if self._backup: + return self._backup != self.get_state() + else: + return False + + def backup(self, force=False): + """ + Save a backup of this flow, which can be restored by calling `Flow.revert()`. + """ + if not self._backup: + self._backup = self.get_state() + + def revert(self): + """ + Revert to the last backed up state. + """ + if self._backup: + self.set_state(self._backup) + self._backup = None + + @property + def killable(self): + """*Read-only:* `True` if this flow can be killed, `False` otherwise.""" + return ( + self.live and + not (self.error and self.error.msg == Error.KILLED_MESSAGE) + ) + + def kill(self): + """ + Kill this flow. The current request/response will not be forwarded to its destination. + """ + if not self.killable: + raise exceptions.ControlException("Flow is not killable.") + # TODO: The way we currently signal killing is not ideal. One major problem is that we cannot kill + # flows in transit (https://github.com/_internal_mitmproxy/_internal_mitmproxy/issues/4711), even though they are advertised + # as killable. An alternative approach would be to introduce a `KillInjected` event similar to + # `MessageInjected`, which should fix this issue. + self.error = Error(Error.KILLED_MESSAGE) + self.intercepted = False + self.live = False + + def intercept(self): + """ + Intercept this Flow. Processing will stop until resume is + called. + """ + if self.intercepted: + return + self.intercepted = True + if self._resume_event is not None: + self._resume_event.clear() + + async def wait_for_resume(self): + """ + Wait until this Flow is resumed. + """ + if not self.intercepted: + return + if self._resume_event is None: + self._resume_event = asyncio.Event() + await self._resume_event.wait() + + def resume(self): + """ + Continue with the flow – called after an intercept(). + """ + if not self.intercepted: + return + self.intercepted = False + if self._resume_event is not None: + self._resume_event.set() + + @property + def timestamp_start(self) -> float: + """ + *Read-only:* Start time of the flow. + Depending on the flow type, this property is an alias for + `_internal_mitmproxy.connection.Client.timestamp_start` or `_internal_mitmproxy.http.Request.timestamp_start`. + """ + return self.client_conn.timestamp_start + + +__all__ = [ + "Flow", + "Error", +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/flowfilter.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/flowfilter.py new file mode 100644 index 00000000..b6f25fd8 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/flowfilter.py @@ -0,0 +1,662 @@ +""" + The following operators are understood: + + ~q Request + ~s Response + + Headers: + + Patterns are matched against "name: value" strings. Field names are + all-lowercase. + + ~a Asset content-type in response. Asset content types are: + text/javascript + application/x-javascript + application/javascript + text/css + image/* + font/* + application/font-* + ~h rex Header line in either request or response + ~hq rex Header in request + ~hs rex Header in response + + ~b rex Expression in the body of either request or response + ~bq rex Expression in the body of request + ~bs rex Expression in the body of response + ~t rex Shortcut for content-type header. + + ~d rex Request domain + ~m rex Method + ~u rex URL + ~c CODE Response code. + rex Equivalent to ~u rex +""" + +import functools +import re +import sys +from typing import ClassVar, Sequence, Type, Protocol, Union +import pyparsing as pp + +from _internal_mitmproxy import flow, http, tcp + + +def only(*types): + def decorator(fn): + @functools.wraps(fn) + def filter_types(self, flow): + if isinstance(flow, types): + return fn(self, flow) + return False + + return filter_types + + return decorator + + +class _Token: + + def dump(self, indent=0, fp=sys.stdout): + print("{spacing}{name}{expr}".format( + spacing="\t" * indent, + name=self.__class__.__name__, + expr=getattr(self, "expr", "") + ), file=fp) + + +class _Action(_Token): + code: ClassVar[str] + help: ClassVar[str] + + @classmethod + def make(klass, s, loc, toks): + return klass(*toks[1:]) + + +class FErr(_Action): + code = "e" + help = "Match error" + + def __call__(self, f): + return True if f.error else False + + +class FMarked(_Action): + code = "marked" + help = "Match marked flows" + + def __call__(self, f): + return bool(f.marked) + + +class FHTTP(_Action): + code = "http" + help = "Match HTTP flows" + + @only(http.HTTPFlow) + def __call__(self, f): + return True + + +class FWebSocket(_Action): + code = "websocket" + help = "Match WebSocket flows" + + @only(http.HTTPFlow) + def __call__(self, f: http.HTTPFlow): + return f.websocket is not None + + +class FTCP(_Action): + code = "tcp" + help = "Match TCP flows" + + @only(tcp.TCPFlow) + def __call__(self, f): + return True + + +class FReq(_Action): + code = "q" + help = "Match request with no response" + + @only(http.HTTPFlow) + def __call__(self, f): + if not f.response: + return True + + +class FResp(_Action): + code = "s" + help = "Match response" + + @only(http.HTTPFlow) + def __call__(self, f): + return bool(f.response) + + +class FAll(_Action): + code = "all" + help = "Match all flows" + + def __call__(self, f: flow.Flow): + return True + + +class _Rex(_Action): + flags = 0 + is_binary = True + + def __init__(self, expr): + self.expr = expr + if self.is_binary: + expr = expr.encode() + try: + self.re = re.compile(expr, self.flags) + except Exception: + raise ValueError("Cannot compile expression.") + + +def _check_content_type(rex, message): + return any( + name.lower() == b"content-type" and + rex.search(value) + for name, value in message.headers.fields + ) + + +class FAsset(_Action): + code = "a" + help = "Match asset in response: CSS, JavaScript, images, fonts." + ASSET_TYPES = [re.compile(x) for x in [ + b"text/javascript", + b"application/x-javascript", + b"application/javascript", + b"text/css", + b"image/.*", + b"font/.*", + b"application/font-.*", + ]] + + @only(http.HTTPFlow) + def __call__(self, f): + if f.response: + for i in self.ASSET_TYPES: + if _check_content_type(i, f.response): + return True + return False + + +class FContentType(_Rex): + code = "t" + help = "Content-type header" + + @only(http.HTTPFlow) + def __call__(self, f): + if _check_content_type(self.re, f.request): + return True + elif f.response and _check_content_type(self.re, f.response): + return True + return False + + +class FContentTypeRequest(_Rex): + code = "tq" + help = "Request Content-Type header" + + @only(http.HTTPFlow) + def __call__(self, f): + return _check_content_type(self.re, f.request) + + +class FContentTypeResponse(_Rex): + code = "ts" + help = "Response Content-Type header" + + @only(http.HTTPFlow) + def __call__(self, f): + if f.response: + return _check_content_type(self.re, f.response) + return False + + +class FHead(_Rex): + code = "h" + help = "Header" + flags = re.MULTILINE + + @only(http.HTTPFlow) + def __call__(self, f): + if f.request and self.re.search(bytes(f.request.headers)): + return True + if f.response and self.re.search(bytes(f.response.headers)): + return True + return False + + +class FHeadRequest(_Rex): + code = "hq" + help = "Request header" + flags = re.MULTILINE + + @only(http.HTTPFlow) + def __call__(self, f): + if f.request and self.re.search(bytes(f.request.headers)): + return True + + +class FHeadResponse(_Rex): + code = "hs" + help = "Response header" + flags = re.MULTILINE + + @only(http.HTTPFlow) + def __call__(self, f): + if f.response and self.re.search(bytes(f.response.headers)): + return True + + +class FBod(_Rex): + code = "b" + help = "Body" + flags = re.DOTALL + + @only(http.HTTPFlow, tcp.TCPFlow) + def __call__(self, f): + if isinstance(f, http.HTTPFlow): + if f.request and f.request.raw_content: + if self.re.search(f.request.get_content(strict=False)): + return True + if f.response and f.response.raw_content: + if self.re.search(f.response.get_content(strict=False)): + return True + if f.websocket: + for msg in f.websocket.messages: + if self.re.search(msg.content): + return True + elif isinstance(f, tcp.TCPFlow): + for msg in f.messages: + if self.re.search(msg.content): + return True + return False + + +class FBodRequest(_Rex): + code = "bq" + help = "Request body" + flags = re.DOTALL + + @only(http.HTTPFlow, tcp.TCPFlow) + def __call__(self, f): + if isinstance(f, http.HTTPFlow): + if f.request and f.request.raw_content: + if self.re.search(f.request.get_content(strict=False)): + return True + if f.websocket: + for msg in f.websocket.messages: + if msg.from_client and self.re.search(msg.content): + return True + elif isinstance(f, tcp.TCPFlow): + for msg in f.messages: + if msg.from_client and self.re.search(msg.content): + return True + + +class FBodResponse(_Rex): + code = "bs" + help = "Response body" + flags = re.DOTALL + + @only(http.HTTPFlow, tcp.TCPFlow) + def __call__(self, f): + if isinstance(f, http.HTTPFlow): + if f.response and f.response.raw_content: + if self.re.search(f.response.get_content(strict=False)): + return True + if f.websocket: + for msg in f.websocket.messages: + if not msg.from_client and self.re.search(msg.content): + return True + elif isinstance(f, tcp.TCPFlow): + for msg in f.messages: + if not msg.from_client and self.re.search(msg.content): + return True + + +class FMethod(_Rex): + code = "m" + help = "Method" + flags = re.IGNORECASE + + @only(http.HTTPFlow) + def __call__(self, f): + return bool(self.re.search(f.request.data.method)) + + +class FDomain(_Rex): + code = "d" + help = "Domain" + flags = re.IGNORECASE + is_binary = False + + @only(http.HTTPFlow) + def __call__(self, f): + return bool( + self.re.search(f.request.host) or + self.re.search(f.request.pretty_host) + ) + + +class FUrl(_Rex): + code = "u" + help = "URL" + is_binary = False + + # FUrl is special, because it can be "naked". + + @classmethod + def make(klass, s, loc, toks): + if len(toks) > 1: + toks = toks[1:] + return klass(*toks) + + @only(http.HTTPFlow) + def __call__(self, f): + if not f or not f.request: + return False + return self.re.search(f.request.pretty_url) + + +class FSrc(_Rex): + code = "src" + help = "Match source address" + is_binary = False + + def __call__(self, f): + if not f.client_conn or not f.client_conn.peername: + return False + r = "{}:{}".format(f.client_conn.peername[0], f.client_conn.peername[1]) + return f.client_conn.peername and self.re.search(r) + + +class FDst(_Rex): + code = "dst" + help = "Match destination address" + is_binary = False + + def __call__(self, f): + if not f.server_conn or not f.server_conn.address: + return False + r = "{}:{}".format(f.server_conn.address[0], f.server_conn.address[1]) + return f.server_conn.address and self.re.search(r) + + +class FReplay(_Action): + code = "replay" + help = "Match replayed flows" + + def __call__(self, f): + return f.is_replay is not None + + +class FReplayClient(_Action): + code = "replayq" + help = "Match replayed client request" + + def __call__(self, f): + return f.is_replay == 'request' + + +class FReplayServer(_Action): + code = "replays" + help = "Match replayed server response" + + def __call__(self, f): + return f.is_replay == 'response' + + +class FMeta(_Rex): + code = "meta" + help = "Flow metadata" + flags = re.MULTILINE + is_binary = False + + def __call__(self, f): + m = "\n".join([f"{key}: {value}" for key, value in f.metadata.items()]) + return self.re.search(m) + + +class FMarker(_Rex): + code = "marker" + help = "Match marked flows with specified marker" + is_binary = False + + def __call__(self, f): + return self.re.search(f.marked) + + +class FComment(_Rex): + code = "comment" + help = "Flow comment" + flags = re.MULTILINE + is_binary = False + + def __call__(self, f): + return self.re.search(f.comment) + + +class _Int(_Action): + + def __init__(self, num): + self.num = int(num) + + +class FCode(_Int): + code = "c" + help = "HTTP response code" + + @only(http.HTTPFlow) + def __call__(self, f): + if f.response and f.response.status_code == self.num: + return True + + +class FAnd(_Token): + + def __init__(self, lst): + self.lst = lst + + def dump(self, indent=0, fp=sys.stdout): + super().dump(indent, fp) + for i in self.lst: + i.dump(indent + 1, fp) + + def __call__(self, f): + return all(i(f) for i in self.lst) + + +class FOr(_Token): + + def __init__(self, lst): + self.lst = lst + + def dump(self, indent=0, fp=sys.stdout): + super().dump(indent, fp) + for i in self.lst: + i.dump(indent + 1, fp) + + def __call__(self, f): + return any(i(f) for i in self.lst) + + +class FNot(_Token): + + def __init__(self, itm): + self.itm = itm[0] + + def dump(self, indent=0, fp=sys.stdout): + super().dump(indent, fp) + self.itm.dump(indent + 1, fp) + + def __call__(self, f): + return not self.itm(f) + + +filter_unary: Sequence[Type[_Action]] = [ + FAsset, + FErr, + FHTTP, + FMarked, + FReplay, + FReplayClient, + FReplayServer, + FReq, + FResp, + FTCP, + FWebSocket, + FAll, +] +filter_rex: Sequence[Type[_Rex]] = [ + FBod, + FBodRequest, + FBodResponse, + FContentType, + FContentTypeRequest, + FContentTypeResponse, + FDomain, + FDst, + FHead, + FHeadRequest, + FHeadResponse, + FMethod, + FSrc, + FUrl, + FMeta, + FMarker, + FComment, +] +filter_int = [ + FCode +] + + +def _make(): + # Order is important - multi-char expressions need to come before narrow + # ones. + parts = [] + for cls in filter_unary: + f = pp.Literal(f"~{cls.code}") + pp.WordEnd() + f.setParseAction(cls.make) + parts.append(f) + + # This is a bit of a hack to simulate Word(pyparsing_unicode.printables), + # which has a horrible performance with len(pyparsing.pyparsing_unicode.printables) == 1114060 + unicode_words = pp.CharsNotIn("()~'\"" + pp.ParserElement.DEFAULT_WHITE_CHARS) + unicode_words.skipWhitespace = True + regex = ( + unicode_words + | pp.QuotedString('"', escChar='\\') + | pp.QuotedString("'", escChar='\\') + ) + for cls in filter_rex: + f = pp.Literal(f"~{cls.code}") + pp.WordEnd() + regex.copy() + f.setParseAction(cls.make) + parts.append(f) + + for cls in filter_int: + f = pp.Literal(f"~{cls.code}") + pp.WordEnd() + pp.Word(pp.nums) + f.setParseAction(cls.make) + parts.append(f) + + # A naked rex is a URL rex: + f = regex.copy() + f.setParseAction(FUrl.make) + parts.append(f) + + atom = pp.MatchFirst(parts) + expr = pp.infixNotation( + atom, + [(pp.Literal("!").suppress(), + 1, + pp.opAssoc.RIGHT, + lambda x: FNot(*x)), + (pp.Literal("&").suppress(), + 2, + pp.opAssoc.LEFT, + lambda x: FAnd(*x)), + (pp.Literal("|").suppress(), + 2, + pp.opAssoc.LEFT, + lambda x: FOr(*x)), + ]) + expr = pp.OneOrMore(expr) + return expr.setParseAction(lambda x: FAnd(x) if len(x) != 1 else x) + + +bnf = _make() + + +class TFilter(Protocol): + pattern: str + + def __call__(self, f: flow.Flow) -> bool: + ... # pragma: no cover + + +def parse(s: str) -> TFilter: + """ + Parse a filter expression and return the compiled filter function. + If the filter syntax is invalid, `ValueError` is raised. + """ + if not s: + raise ValueError("Empty filter expression") + try: + flt = bnf.parseString(s, parseAll=True)[0] + flt.pattern = s + return flt + except (pp.ParseException, ValueError) as e: + raise ValueError(f"Invalid filter expression: {s!r}") from e + + +def match(flt: Union[str, TFilter], flow: flow.Flow) -> bool: + """ + Matches a flow against a compiled filter expression. + Returns True if matched, False if not. + + If flt is a string, it will be compiled as a filter expression. + If the expression is invalid, ValueError is raised. + """ + if isinstance(flt, str): + flt = parse(flt) + if flt: + return flt(flow) + return True + + +match_all: TFilter = parse("~all") +"""A filter function that matches all flows""" + + +help = [] +for a in filter_unary: + help.append( + (f"~{a.code}", a.help) + ) +for b in filter_rex: + help.append( + (f"~{b.code} regex", b.help) + ) +for c in filter_int: + help.append( + (f"~{c.code} int", c.help) + ) +help.sort() +help.extend( + [ + ("!", "unary not"), + ("&", "and"), + ("|", "or"), + ("(...)", "grouping"), + ] +) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/hooks.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/hooks.py new file mode 100644 index 00000000..96022897 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/hooks.py @@ -0,0 +1,85 @@ +import re +import warnings +from dataclasses import dataclass, is_dataclass, fields +from typing import ClassVar, Any, Dict, Type, Set, List, TYPE_CHECKING, Sequence + +import _internal_mitmproxy.flow + +if TYPE_CHECKING: + import _internal_mitmproxy.addonmanager + import _internal_mitmproxy.log + + +class Hook: + name: ClassVar[str] + + def args(self) -> List[Any]: + args = [] + for field in fields(self): + args.append(getattr(self, field.name)) + return args + + def __new__(cls, *args, **kwargs): + if cls is Hook: + raise TypeError("Hook may not be instantiated directly.") + if not is_dataclass(cls): + raise TypeError("Subclass is not a dataclass.") + return super().__new__(cls) + + def __init_subclass__(cls, **kwargs): + # initialize .name attribute. HttpRequestHook -> http_request + if cls.__dict__.get("name", None) is None: + name = cls.__name__.replace("Hook", "") + cls.name = re.sub('(?!^)([A-Z]+)', r'_\1', name).lower() + if cls.name in all_hooks: + other = all_hooks[cls.name] + warnings.warn(f"Two conflicting event classes for {cls.name}: {cls} and {other}", RuntimeWarning) + if cls.name == "": + return # don't register Hook class. + all_hooks[cls.name] = cls + + # define a custom hash and __eq__ function so that events are hashable and not comparable. + cls.__hash__ = object.__hash__ + cls.__eq__ = object.__eq__ + + +all_hooks: Dict[str, Type[Hook]] = {} + + +@dataclass +class ConfigureHook(Hook): + """ + Called when configuration changes. The updated argument is a + set-like object containing the keys of all changed options. This + event is called during startup with all options in the updated set. + """ + updated: Set[str] + + +@dataclass +class DoneHook(Hook): + """ + Called when the addon shuts down, either by being removed from + the _internal_mitmproxy instance, or when _internal_mitmproxy itself shuts down. On + shutdown, this event is called after the event loop is + terminated, guaranteeing that it will be the final event an addon + sees. Note that log handlers are shut down at this point, so + calls to log functions will produce no output. + """ + + +@dataclass +class RunningHook(Hook): + """ + Called when the proxy is completely up and running. At this point, + you can expect all addons to be loaded and all options to be set. + """ + + +@dataclass +class UpdateHook(Hook): + """ + Update is called when one or more flow objects have been modified, + usually from a different addon. + """ + flows: Sequence[_internal_mitmproxy.flow.Flow] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/http.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/http.py new file mode 100644 index 00000000..72a776b0 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/http.py @@ -0,0 +1,1301 @@ +import binascii +import os +import re +import time +import urllib.parse +import json +from dataclasses import dataclass +from dataclasses import fields +from email.utils import formatdate +from email.utils import mktime_tz +from email.utils import parsedate_tz +from typing import Callable +from typing import Dict +from typing import Iterable +from typing import Iterator +from typing import List +from typing import Mapping +from typing import Optional +from typing import Tuple +from typing import Union +from typing import cast +from typing import Any + +from _internal_mitmproxy import flow + +# from _internal_mitmproxy.websocket import WebSocketData +WebSocketData = ... +from _internal_mitmproxy.coretypes import multidict +from _internal_mitmproxy.coretypes import serializable +from _internal_mitmproxy.net import encoding +from _internal_mitmproxy.net.http import cookies +from _internal_mitmproxy.net.http import multipart +from _internal_mitmproxy.net.http import status_codes +from _internal_mitmproxy.net.http import url +from _internal_mitmproxy.net.http.headers import assemble_content_type +from _internal_mitmproxy.net.http.headers import parse_content_type +from _internal_mitmproxy.utils import human +from _internal_mitmproxy.utils import strutils +from _internal_mitmproxy.utils import typecheck +from _internal_mitmproxy.utils.strutils import always_bytes +from _internal_mitmproxy.utils.strutils import always_str + + +# While headers _should_ be ASCII, it's not uncommon for certain headers to be utf-8 encoded. +def _native(x: bytes) -> str: + return x.decode("utf-8", "surrogateescape") + + +def _always_bytes(x: Union[str, bytes]) -> bytes: + return strutils.always_bytes(x, "utf-8", "surrogateescape") + + +# This cannot be easily typed with mypy yet, so we just specify MultiDict without concrete types. +class Headers(multidict.MultiDict): # type: ignore + """ + Header class which allows both convenient access to individual headers as well as + direct access to the underlying raw data. Provides a full dictionary interface. + + Create headers with keyword arguments: + >>> h = Headers(host="example.com", content_type="application/xml") + + Headers mostly behave like a normal dict: + >>> h["Host"] + "example.com" + + Headers are case insensitive: + >>> h["host"] + "example.com" + + Headers can also be created from a list of raw (header_name, header_value) byte tuples: + >>> h = Headers([ + (b"Host",b"example.com"), + (b"Accept",b"text/html"), + (b"accept",b"application/xml") + ]) + + Multiple headers are folded into a single header as per RFC 7230: + >>> h["Accept"] + "text/html, application/xml" + + Setting a header removes all existing headers with the same name: + >>> h["Accept"] = "application/text" + >>> h["Accept"] + "application/text" + + `bytes(h)` returns an HTTP/1 header block: + >>> print(bytes(h)) + Host: example.com + Accept: application/text + + For full control, the raw header fields can be accessed: + >>> h.fields + + Caveats: + - For use with the "Set-Cookie" and "Cookie" headers, either use `Response.cookies` or see `Headers.get_all`. + """ + + def __init__(self, fields: Iterable[Tuple[bytes, bytes]] = (), **headers): + """ + *Args:* + - *fields:* (optional) list of ``(name, value)`` header byte tuples, + e.g. ``[(b"Host", b"example.com")]``. All names and values must be bytes. + - *\\*\\*headers:* Additional headers to set. Will overwrite existing values from `fields`. + For convenience, underscores in header names will be transformed to dashes - + this behaviour does not extend to other methods. + + If ``**headers`` contains multiple keys that have equal ``.lower()`` representations, + the behavior is undefined. + """ + super().__init__(fields) + + for key, value in self.fields: + if not isinstance(key, bytes) or not isinstance(value, bytes): + raise TypeError("Header fields must be bytes.") + + # content_type -> content-type + self.update( + { + _always_bytes(name).replace(b"_", b"-"): _always_bytes(value) + for name, value in headers.items() + } + ) + + fields: Tuple[Tuple[bytes, bytes], ...] + + @staticmethod + def _reduce_values(values) -> str: + # Headers can be folded + return ", ".join(values) + + @staticmethod + def _kconv(key) -> str: + # Headers are case-insensitive + return key.lower() + + def __bytes__(self) -> bytes: + if self.fields: + return b"\r\n".join(b": ".join(field) for field in self.fields) + b"\r\n" + else: + return b"" + + def __delitem__(self, key: Union[str, bytes]) -> None: + key = _always_bytes(key) + super().__delitem__(key) + + def __iter__(self) -> Iterator[str]: + for x in super().__iter__(): + yield _native(x) + + def get_all(self, name: Union[str, bytes]) -> List[str]: + """ + Like `Headers.get`, but does not fold multiple headers into a single one. + This is useful for Set-Cookie and Cookie headers, which do not support folding. + + *See also:* + - + - + - + """ + name = _always_bytes(name) + return [_native(x) for x in super().get_all(name)] + + def set_all(self, name: Union[str, bytes], values: List[Union[str, bytes]]): + """ + Explicitly set multiple headers for the given key. + See `Headers.get_all`. + """ + name = _always_bytes(name) + values = [_always_bytes(x) for x in values] + return super().set_all(name, values) + + def insert(self, index: int, key: Union[str, bytes], value: Union[str, bytes]): + key = _always_bytes(key) + value = _always_bytes(value) + super().insert(index, key, value) + + def items(self, multi=False): + if multi: + return ((_native(k), _native(v)) for k, v in self.fields) + else: + return super().items() + + +@dataclass +class MessageData(serializable.Serializable): + http_version: bytes + headers: Headers + content: Optional[bytes] + trailers: Optional[Headers] + timestamp_start: float + timestamp_end: Optional[float] + + # noinspection PyUnreachableCode + if __debug__: + + def __post_init__(self): + for field in fields(self): + val = getattr(self, field.name) + typecheck.check_option_type(field.name, val, field.type) + + def set_state(self, state): + for k, v in state.items(): + if k in ("headers", "trailers") and v is not None: + v = Headers.from_state(v) + setattr(self, k, v) + + def get_state(self): + state = vars(self).copy() + state["headers"] = state["headers"].get_state() + if state["trailers"] is not None: + state["trailers"] = state["trailers"].get_state() + return state + + @classmethod + def from_state(cls, state): + state["headers"] = Headers.from_state(state["headers"]) + if state["trailers"] is not None: + state["trailers"] = Headers.from_state(state["trailers"]) + return cls(**state) + + +@dataclass +class RequestData(MessageData): + host: str + port: int + method: bytes + scheme: bytes + authority: bytes + path: bytes + + +@dataclass +class ResponseData(MessageData): + status_code: int + reason: bytes + + +class Message(serializable.Serializable): + """Base class for `Request` and `Response`.""" + + @classmethod + def from_state(cls, state): + return cls(**state) + + def get_state(self): + return self.data.get_state() + + def set_state(self, state): + self.data.set_state(state) + + data: MessageData + stream: Union[Callable[[bytes], Union[Iterable[bytes], bytes]], bool] = False + """ + This attribute controls if the message body should be streamed. + + If `False`, _internal_mitmproxy will buffer the entire body before forwarding it to the destination. + This makes it possible to perform string replacements on the entire body. + If `True`, the message body will not be buffered on the proxy + but immediately forwarded instead. + Alternatively, a transformation function can be specified, which will be called for each chunk of data. + Please note that packet boundaries generally should not be relied upon. + + This attribute must be set in the `requestheaders` or `responseheaders` hook. + Setting it in `request` or `response` is already too late, _internal_mitmproxy has buffered the message body already. + """ + + @property + def http_version(self) -> str: + """ + HTTP version string, for example `HTTP/1.1`. + """ + return self.data.http_version.decode("utf-8", "surrogateescape") + + @http_version.setter + def http_version(self, http_version: Union[str, bytes]) -> None: + self.data.http_version = strutils.always_bytes( + http_version, "utf-8", "surrogateescape" + ) + + @property + def is_http10(self) -> bool: + return self.data.http_version == b"HTTP/1.0" + + @property + def is_http11(self) -> bool: + return self.data.http_version == b"HTTP/1.1" + + @property + def is_http2(self) -> bool: + return self.data.http_version == b"HTTP/2.0" + + @property + def headers(self) -> Headers: + """ + The HTTP headers. + """ + return self.data.headers + + @headers.setter + def headers(self, h: Headers) -> None: + self.data.headers = h + + @property + def trailers(self) -> Optional[Headers]: + """ + The [HTTP trailers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer). + """ + return self.data.trailers + + @trailers.setter + def trailers(self, h: Optional[Headers]) -> None: + self.data.trailers = h + + @property + def raw_content(self) -> Optional[bytes]: + """ + The raw (potentially compressed) HTTP message body. + + In contrast to `Message.content` and `Message.text`, accessing this property never raises. + + *See also:* `Message.content`, `Message.text` + """ + return self.data.content + + @raw_content.setter + def raw_content(self, content: Optional[bytes]) -> None: + self.data.content = content + + @property + def content(self) -> Optional[bytes]: + """ + The uncompressed HTTP message body as bytes. + + Accessing this attribute may raise a `ValueError` when the HTTP content-encoding is invalid. + + *See also:* `Message.raw_content`, `Message.text` + """ + return self.get_content() + + @content.setter + def content(self, value: Optional[bytes]) -> None: + self.set_content(value) + + @property + def text(self) -> Optional[str]: + """ + The uncompressed and decoded HTTP message body as text. + + Accessing this attribute may raise a `ValueError` when either content-encoding or charset is invalid. + + *See also:* `Message.raw_content`, `Message.content` + """ + return self.get_text() + + @text.setter + def text(self, value: Optional[str]) -> None: + self.set_text(value) + + def set_content(self, value: Optional[bytes]) -> None: + if value is None: + self.raw_content = None + return + if not isinstance(value, bytes): + raise TypeError( + f"Message content must be bytes, not {type(value).__name__}. " + "Please use .text if you want to assign a str." + ) + ce = self.headers.get("content-encoding") + try: + self.raw_content = encoding.encode(value, ce or "identity") + except ValueError: + # So we have an invalid content-encoding? + # Let's remove it! + del self.headers["content-encoding"] + self.raw_content = value + + if "transfer-encoding" in self.headers: + # https://httpwg.org/specs/rfc7230.html#header.content-length + # don't set content-length if a transfer-encoding is provided + pass + else: + self.headers["content-length"] = str(len(self.raw_content)) + + def get_content(self, strict: bool = True) -> Optional[bytes]: + """ + Similar to `Message.content`, but does not raise if `strict` is `False`. + Instead, the compressed message body is returned as-is. + """ + if self.raw_content is None: + return None + ce = self.headers.get("content-encoding") + if ce: + try: + content = encoding.decode(self.raw_content, ce) + # A client may illegally specify a byte -> str encoding here (e.g. utf8) + if isinstance(content, str): + raise ValueError(f"Invalid Content-Encoding: {ce}") + return content + except ValueError: + if strict: + raise + return self.raw_content + else: + return self.raw_content + + def _get_content_type_charset(self) -> Optional[str]: + ct = parse_content_type(self.headers.get("content-type", "")) + if ct: + return ct[2].get("charset") + return None + + def _guess_encoding(self, content: bytes = b"") -> str: + enc = self._get_content_type_charset() + if not enc: + if "json" in self.headers.get("content-type", ""): + enc = "utf8" + if not enc: + if "html" in self.headers.get("content-type", ""): + meta_charset = re.search( + rb"""]+charset=['"]?([^'">]+)""", content, re.IGNORECASE + ) + if meta_charset: + enc = meta_charset.group(1).decode("ascii", "ignore") + if not enc: + if "text/css" in self.headers.get("content-type", ""): + # @charset rule must be the very first thing. + css_charset = re.match( + rb"""@charset "([^"]+)";""", content, re.IGNORECASE + ) + if css_charset: + enc = css_charset.group(1).decode("ascii", "ignore") + if not enc: + enc = "latin-1" + # Use GB 18030 as the superset of GB2312 and GBK to fix common encoding problems on Chinese websites. + if enc.lower() in ("gb2312", "gbk"): + enc = "gb18030" + + return enc + + def set_text(self, text: Optional[str]) -> None: + if text is None: + self.content = None + return + enc = self._guess_encoding() + + try: + self.content = cast(bytes, encoding.encode(text, enc)) + except ValueError: + # Fall back to UTF-8 and update the content-type header. + ct = parse_content_type(self.headers.get("content-type", "")) or ( + "text", + "plain", + {}, + ) + ct[2]["charset"] = "utf-8" + self.headers["content-type"] = assemble_content_type(*ct) + enc = "utf8" + self.content = text.encode(enc, "surrogateescape") + + def get_text(self, strict: bool = True) -> Optional[str]: + """ + Similar to `Message.text`, but does not raise if `strict` is `False`. + Instead, the message body is returned as surrogate-escaped UTF-8. + """ + content = self.get_content(strict) + if content is None: + return None + enc = self._guess_encoding(content) + try: + return cast(str, encoding.decode(content, enc)) + except ValueError: + if strict: + raise + return content.decode("utf8", "surrogateescape") + + @property + def timestamp_start(self) -> float: + """ + *Timestamp:* Headers received. + """ + return self.data.timestamp_start + + @timestamp_start.setter + def timestamp_start(self, timestamp_start: float) -> None: + self.data.timestamp_start = timestamp_start + + @property + def timestamp_end(self) -> Optional[float]: + """ + *Timestamp:* Last byte received. + """ + return self.data.timestamp_end + + @timestamp_end.setter + def timestamp_end(self, timestamp_end: Optional[float]): + self.data.timestamp_end = timestamp_end + + def decode(self, strict: bool = True) -> None: + """ + Decodes body based on the current Content-Encoding header, then + removes the header. If there is no Content-Encoding header, no + action is taken. + + *Raises:* + - `ValueError`, when the content-encoding is invalid and strict is True. + """ + decoded = self.get_content(strict) + self.headers.pop("content-encoding", None) + self.content = decoded + + def encode(self, encoding: str) -> None: + """ + Encodes body with the given encoding, where e is "gzip", "deflate", "identity", "br", or "zstd". + Any existing content-encodings are overwritten, the content is not decoded beforehand. + + *Raises:* + - `ValueError`, when the specified content-encoding is invalid. + """ + self.headers["content-encoding"] = encoding + self.content = self.raw_content + if "content-encoding" not in self.headers: + raise ValueError("Invalid content encoding {}".format(repr(encoding))) + + def json(self, **kwargs: Any) -> Any: + """ + Returns the JSON encoded content of the response, if any. + `**kwargs` are optional arguments that will be + passed to `json.loads()`. + + Will raise if the content can not be decoded and then parsed as JSON. + + *Raises:* + - `json.decoder.JSONDecodeError` if content is not valid JSON. + - `TypeError` if the content is not available, for example because the response + has been streamed. + """ + content = self.get_content(strict=False) + if content is None: + raise TypeError("Message content is not available.") + else: + return json.loads(content, **kwargs) + + +class Request(Message): + """ + An HTTP request. + """ + + data: RequestData + + def __init__( + self, + host: str, + port: int, + method: bytes, + scheme: bytes, + authority: bytes, + path: bytes, + http_version: bytes, + headers: Union[Headers, Tuple[Tuple[bytes, bytes], ...]], + content: Optional[bytes], + trailers: Union[Headers, Tuple[Tuple[bytes, bytes], ...], None], + timestamp_start: float, + timestamp_end: Optional[float], + ): + # auto-convert invalid types to retain compatibility with older code. + if isinstance(host, bytes): + host = host.decode("idna", "strict") + if isinstance(method, str): + method = method.encode("ascii", "strict") + if isinstance(scheme, str): + scheme = scheme.encode("ascii", "strict") + if isinstance(authority, str): + authority = authority.encode("ascii", "strict") + if isinstance(path, str): + path = path.encode("ascii", "strict") + if isinstance(http_version, str): + http_version = http_version.encode("ascii", "strict") + + if isinstance(content, str): + raise ValueError(f"Content must be bytes, not {type(content).__name__}") + if not isinstance(headers, Headers): + headers = Headers(headers) + if trailers is not None and not isinstance(trailers, Headers): + trailers = Headers(trailers) + + self.data = RequestData( + host=host, + port=port, + method=method, + scheme=scheme, + authority=authority, + path=path, + http_version=http_version, + headers=headers, + content=content, + trailers=trailers, + timestamp_start=timestamp_start, + timestamp_end=timestamp_end, + ) + + def __repr__(self) -> str: + if self.host and self.port: + hostport = f"{self.host}:{self.port}" + else: + hostport = "" + path = self.path or "" + return f"Request({self.method} {hostport}{path})" + + @classmethod + def make( + cls, + method: str, + url: str, + content: Union[bytes, str] = "", + headers: Union[ + Headers, + Dict[Union[str, bytes], Union[str, bytes]], + Iterable[Tuple[bytes, bytes]], + ] = (), + ) -> "Request": + """ + Simplified API for creating request objects. + """ + # Headers can be list or dict, we differentiate here. + if isinstance(headers, Headers): + pass + elif isinstance(headers, dict): + headers = Headers( + ( + always_bytes(k, "utf-8", "surrogateescape"), + always_bytes(v, "utf-8", "surrogateescape"), + ) + for k, v in headers.items() + ) + elif isinstance(headers, Iterable): + headers = Headers(headers) # type: ignore + else: + raise TypeError( + "Expected headers to be an iterable or dict, but is {}.".format( + type(headers).__name__ + ) + ) + + req = cls( + "", + 0, + method.encode("utf-8", "surrogateescape"), + b"", + b"", + b"", + b"HTTP/1.1", + headers, + b"", + None, + time.time(), + time.time(), + ) + + req.url = url + # Assign this manually to update the content-length header. + if isinstance(content, bytes): + req.content = content + elif isinstance(content, str): + req.text = content + else: + raise TypeError( + f"Expected content to be str or bytes, but is {type(content).__name__}." + ) + + return req + + @property + def first_line_format(self) -> str: + """ + *Read-only:* HTTP request form as defined in [RFC 7230](https://tools.ietf.org/html/rfc7230#section-5.3). + + origin-form and asterisk-form are subsumed as "relative". + """ + if self.method == "CONNECT": + return "authority" + elif self.authority: + return "absolute" + else: + return "relative" + + @property + def method(self) -> str: + """ + HTTP request method, e.g. "GET". + """ + return self.data.method.decode("utf-8", "surrogateescape").upper() + + @method.setter + def method(self, val: Union[str, bytes]) -> None: + self.data.method = always_bytes(val, "utf-8", "surrogateescape") + + @property + def scheme(self) -> str: + """ + HTTP request scheme, which should be "http" or "https". + """ + return self.data.scheme.decode("utf-8", "surrogateescape") + + @scheme.setter + def scheme(self, val: Union[str, bytes]) -> None: + self.data.scheme = always_bytes(val, "utf-8", "surrogateescape") + + @property + def authority(self) -> str: + """ + HTTP request authority. + + For HTTP/1, this is the authority portion of the request target + (in either absolute-form or authority-form). + For origin-form and asterisk-form requests, this property is set to an empty string. + + For HTTP/2, this is the :authority pseudo header. + + *See also:* `Request.host`, `Request.host_header`, `Request.pretty_host` + """ + try: + return self.data.authority.decode("idna") + except UnicodeError: + return self.data.authority.decode("utf8", "surrogateescape") + + @authority.setter + def authority(self, val: Union[str, bytes]) -> None: + if isinstance(val, str): + try: + val = val.encode("idna", "strict") + except UnicodeError: + val = val.encode("utf8", "surrogateescape") # type: ignore + self.data.authority = val + + @property + def host(self) -> str: + """ + Target server for this request. This may be parsed from the raw request + (e.g. from a ``GET http://example.com/ HTTP/1.1`` request line) + or inferred from the proxy mode (e.g. an IP in transparent mode). + + Setting the host attribute also updates the host header and authority information, if present. + + *See also:* `Request.authority`, `Request.host_header`, `Request.pretty_host` + """ + return self.data.host + + @host.setter + def host(self, val: Union[str, bytes]) -> None: + self.data.host = always_str(val, "idna", "strict") + + # Update host header + if "Host" in self.data.headers: + self.data.headers["Host"] = val + # Update authority + if self.data.authority: + self.authority = url.hostport(self.scheme, self.host, self.port) + + @property + def host_header(self) -> Optional[str]: + """ + The request's host/authority header. + + This property maps to either ``request.headers["Host"]`` or + ``request.authority``, depending on whether it's HTTP/1.x or HTTP/2.0. + + *See also:* `Request.authority`,`Request.host`, `Request.pretty_host` + """ + if self.is_http2: + return self.authority or self.data.headers.get("Host", None) + else: + return self.data.headers.get("Host", None) + + @host_header.setter + def host_header(self, val: Union[None, str, bytes]) -> None: + if val is None: + if self.is_http2: + self.data.authority = b"" + self.headers.pop("Host", None) + else: + if self.is_http2: + self.authority = val # type: ignore + if not self.is_http2 or "Host" in self.headers: + # For h2, we only overwrite, but not create, as :authority is the h2 host header. + self.headers["Host"] = val + + @property + def port(self) -> int: + """ + Target port. + """ + return self.data.port + + @port.setter + def port(self, port: int) -> None: + self.data.port = port + + @property + def path(self) -> str: + """ + HTTP request path, e.g. "/index.html". + Usually starts with a slash, except for OPTIONS requests, which may just be "*". + """ + return self.data.path.decode("utf-8", "surrogateescape") + + @path.setter + def path(self, val: Union[str, bytes]) -> None: + self.data.path = always_bytes(val, "utf-8", "surrogateescape") + + @property + def url(self) -> str: + """ + The full URL string, constructed from `Request.scheme`, `Request.host`, `Request.port` and `Request.path`. + + Settings this property updates these attributes as well. + """ + if self.first_line_format == "authority": + return f"{self.host}:{self.port}" + return url.unparse(self.scheme, self.host, self.port, self.path) + + @url.setter + def url(self, val: Union[str, bytes]) -> None: + val = always_str(val, "utf-8", "surrogateescape") + self.scheme, self.host, self.port, self.path = url.parse(val) + + @property + def pretty_host(self) -> str: + """ + *Read-only:* Like `Request.host`, but using `Request.host_header` header as an additional (preferred) data source. + This is useful in transparent mode where `Request.host` is only an IP address. + + *Warning:* When working in adversarial environments, this may not reflect the actual destination + as the Host header could be spoofed. + """ + authority = self.host_header + if authority: + return url.parse_authority(authority, check=False)[0] + else: + return self.host + + @property + def pretty_url(self) -> str: + """ + *Read-only:* Like `Request.url`, but using `Request.pretty_host` instead of `Request.host`. + """ + if self.first_line_format == "authority": + return self.authority + + host_header = self.host_header + if not host_header: + return self.url + + pretty_host, pretty_port = url.parse_authority(host_header, check=False) + pretty_port = pretty_port or url.default_port(self.scheme) or 443 + + return url.unparse(self.scheme, pretty_host, pretty_port, self.path) + + def _get_query(self): + query = urllib.parse.urlparse(self.url).query + return tuple(url.decode(query)) + + def _set_query(self, query_data): + query = url.encode(query_data) + _, _, path, params, _, fragment = urllib.parse.urlparse(self.url) + self.path = urllib.parse.urlunparse(["", "", path, params, query, fragment]) + + @property + def query(self) -> multidict.MultiDictView[str, str]: + """ + The request query as a mutable mapping view on the request's path. + For the most part, this behaves like a dictionary. + Modifications to the MultiDictView update `Request.path`, and vice versa. + """ + return multidict.MultiDictView(self._get_query, self._set_query) + + @query.setter + def query(self, value): + self._set_query(value) + + def _get_cookies(self): + h = self.headers.get_all("Cookie") + return tuple(cookies.parse_cookie_headers(h)) + + def _set_cookies(self, value): + self.headers["cookie"] = cookies.format_cookie_header(value) + + @property + def cookies(self) -> multidict.MultiDictView[str, str]: + """ + The request cookies. + For the most part, this behaves like a dictionary. + Modifications to the MultiDictView update `Request.headers`, and vice versa. + """ + return multidict.MultiDictView(self._get_cookies, self._set_cookies) + + @cookies.setter + def cookies(self, value): + self._set_cookies(value) + + @property + def path_components(self) -> Tuple[str, ...]: + """ + The URL's path components as a tuple of strings. + Components are unquoted. + """ + path = urllib.parse.urlparse(self.url).path + # This needs to be a tuple so that it's immutable. + # Otherwise, this would fail silently: + # request.path_components.append("foo") + return tuple(url.unquote(i) for i in path.split("/") if i) + + @path_components.setter + def path_components(self, components: Iterable[str]): + components = map(lambda x: url.quote(x, safe=""), components) + path = "/" + "/".join(components) + _, _, _, params, query, fragment = urllib.parse.urlparse(self.url) + self.path = urllib.parse.urlunparse(["", "", path, params, query, fragment]) + + def anticache(self) -> None: + """ + Modifies this request to remove headers that might produce a cached response. + """ + delheaders = ( + "if-modified-since", + "if-none-match", + ) + for i in delheaders: + self.headers.pop(i, None) + + def anticomp(self) -> None: + """ + Modify the Accept-Encoding header to only accept uncompressed responses. + """ + self.headers["accept-encoding"] = "identity" + + def constrain_encoding(self) -> None: + """ + Limits the permissible Accept-Encoding values, based on what we can decode appropriately. + """ + accept_encoding = self.headers.get("accept-encoding") + if accept_encoding: + self.headers["accept-encoding"] = ", ".join( + e + for e in {"gzip", "identity", "deflate", "br", "zstd"} + if e in accept_encoding + ) + + def _get_urlencoded_form(self): + is_valid_content_type = ( + "application/x-www-form-urlencoded" + in self.headers.get("content-type", "").lower() + ) + if is_valid_content_type: + return tuple(url.decode(self.get_text(strict=False))) + return () + + def _set_urlencoded_form(self, form_data): + """ + Sets the body to the URL-encoded form data, and adds the appropriate content-type header. + This will overwrite the existing content if there is one. + """ + self.headers["content-type"] = "application/x-www-form-urlencoded" + self.content = url.encode(form_data, self.get_text(strict=False)).encode() + + @property + def urlencoded_form(self) -> multidict.MultiDictView[str, str]: + """ + The URL-encoded form data. + + If the content-type indicates non-form data or the form could not be parsed, this is set to + an empty `MultiDictView`. + + Modifications to the MultiDictView update `Request.content`, and vice versa. + """ + return multidict.MultiDictView( + self._get_urlencoded_form, self._set_urlencoded_form + ) + + @urlencoded_form.setter + def urlencoded_form(self, value): + self._set_urlencoded_form(value) + + def _get_multipart_form(self): + is_valid_content_type = ( + "multipart/form-data" in self.headers.get("content-type", "").lower() + ) + if is_valid_content_type: + try: + return multipart.decode(self.headers.get("content-type"), self.content) + except ValueError: + pass + return () + + def _set_multipart_form(self, value): + is_valid_content_type = ( + self.headers.get("content-type", "") + .lower() + .startswith("multipart/form-data") + ) + if not is_valid_content_type: + """ + Generate a random boundary here. + + See for specifications + on generating the boundary. + """ + boundary = "-" * 20 + binascii.hexlify(os.urandom(16)).decode() + self.headers["content-type"] = f"multipart/form-data; boundary={boundary}" + self.content = multipart.encode(self.headers, value) + + @property + def multipart_form(self) -> multidict.MultiDictView[bytes, bytes]: + """ + The multipart form data. + + If the content-type indicates non-form data or the form could not be parsed, this is set to + an empty `MultiDictView`. + + Modifications to the MultiDictView update `Request.content`, and vice versa. + """ + return multidict.MultiDictView( + self._get_multipart_form, self._set_multipart_form + ) + + @multipart_form.setter + def multipart_form(self, value): + self._set_multipart_form(value) + + +class Response(Message): + """ + An HTTP response. + """ + + data: ResponseData + + def __init__( + self, + http_version: bytes, + status_code: int, + reason: bytes, + headers: Union[Headers, Tuple[Tuple[bytes, bytes], ...]], + content: Optional[bytes], + trailers: Union[None, Headers, Tuple[Tuple[bytes, bytes], ...]], + timestamp_start: float, + timestamp_end: Optional[float], + ): + # auto-convert invalid types to retain compatibility with older code. + if isinstance(http_version, str): + http_version = http_version.encode("ascii", "strict") + if isinstance(reason, str): + reason = reason.encode("ascii", "strict") + + if isinstance(content, str): + raise ValueError( + "Content must be bytes, not {}".format(type(content).__name__) + ) + if not isinstance(headers, Headers): + headers = Headers(headers) + if trailers is not None and not isinstance(trailers, Headers): + trailers = Headers(trailers) + + self.data = ResponseData( + http_version=http_version, + status_code=status_code, + reason=reason, + headers=headers, + content=content, + trailers=trailers, + timestamp_start=timestamp_start, + timestamp_end=timestamp_end, + ) + + def __repr__(self) -> str: + if self.raw_content: + ct = self.headers.get("content-type", "unknown content type") + size = human.pretty_size(len(self.raw_content)) + details = f"{ct}, {size}" + else: + details = "no content" + return f"Response({self.status_code}, {details})" + + @classmethod + def make( + cls, + status_code: int = 200, + content: Union[bytes, str] = b"", + headers: Union[ + Headers, Mapping[str, Union[str, bytes]], Iterable[Tuple[bytes, bytes]] + ] = (), + ) -> "Response": + """ + Simplified API for creating response objects. + """ + if isinstance(headers, Headers): + headers = headers + elif isinstance(headers, dict): + headers = Headers( + ( + always_bytes(k, "utf-8", "surrogateescape"), # type: ignore + always_bytes(v, "utf-8", "surrogateescape"), + ) + for k, v in headers.items() + ) + elif isinstance(headers, Iterable): + headers = Headers(headers) # type: ignore + else: + raise TypeError( + "Expected headers to be an iterable or dict, but is {}.".format( + type(headers).__name__ + ) + ) + + resp = cls( + b"HTTP/1.1", + status_code, + status_codes.RESPONSES.get(status_code, "").encode(), + headers, + None, + None, + time.time(), + time.time(), + ) + + # Assign this manually to update the content-length header. + if isinstance(content, bytes): + resp.content = content + elif isinstance(content, str): + resp.text = content + else: + raise TypeError( + f"Expected content to be str or bytes, but is {type(content).__name__}." + ) + + return resp + + @property + def status_code(self) -> int: + """ + HTTP Status Code, e.g. ``200``. + """ + return self.data.status_code + + @status_code.setter + def status_code(self, status_code: int) -> None: + self.data.status_code = status_code + + @property + def reason(self) -> str: + """ + HTTP reason phrase, for example "Not Found". + + HTTP/2 responses do not contain a reason phrase, an empty string will be returned instead. + """ + # Encoding: http://stackoverflow.com/a/16674906/934719 + return self.data.reason.decode("ISO-8859-1") + + @reason.setter + def reason(self, reason: Union[str, bytes]) -> None: + self.data.reason = strutils.always_bytes(reason, "ISO-8859-1") + + def _get_cookies(self): + h = self.headers.get_all("set-cookie") + all_cookies = cookies.parse_set_cookie_headers(h) + return tuple((name, (value, attrs)) for name, value, attrs in all_cookies) + + def _set_cookies(self, value): + cookie_headers = [] + for k, v in value: + header = cookies.format_set_cookie_header([(k, v[0], v[1])]) + cookie_headers.append(header) + self.headers.set_all("set-cookie", cookie_headers) + + @property + def cookies( + self, + ) -> multidict.MultiDictView[ + str, Tuple[str, multidict.MultiDict[str, Optional[str]]] + ]: + """ + The response cookies. A possibly empty `MultiDictView`, where the keys are cookie + name strings, and values are `(cookie value, attributes)` tuples. Within + attributes, unary attributes (e.g. `HTTPOnly`) are indicated by a `None` value. + Modifications to the MultiDictView update `Response.headers`, and vice versa. + + *Warning:* Changes to `attributes` will not be picked up unless you also reassign + the `(cookie value, attributes)` tuple directly in the `MultiDictView`. + """ + return multidict.MultiDictView(self._get_cookies, self._set_cookies) + + @cookies.setter + def cookies(self, value): + self._set_cookies(value) + + def refresh(self, now=None): + """ + This fairly complex and heuristic function refreshes a server + response for replay. + + - It adjusts date, expires, and last-modified headers. + - It adjusts cookie expiration. + """ + if not now: + now = time.time() + delta = now - self.timestamp_start + refresh_headers = [ + "date", + "expires", + "last-modified", + ] + for i in refresh_headers: + if i in self.headers: + d = parsedate_tz(self.headers[i]) + if d: + new = mktime_tz(d) + delta + try: + self.headers[i] = formatdate(new, usegmt=True) + except OSError: # pragma: no cover + pass # value out of bounds on Windows only (which is why we exclude it from coverage). + c = [] + for set_cookie_header in self.headers.get_all("set-cookie"): + try: + refreshed = cookies.refresh_set_cookie_header(set_cookie_header, delta) + except ValueError: + refreshed = set_cookie_header + c.append(refreshed) + if c: + self.headers.set_all("set-cookie", c) + + +class HTTPFlow(flow.Flow): + """ + An HTTPFlow is a collection of objects representing a single HTTP + transaction. + """ + + request: Request + """The client's HTTP request.""" + response: Optional[Response] = None + """The server's HTTP response.""" + error: Optional[flow.Error] = None + """ + A connection or protocol error affecting this flow. + + Note that it's possible for a Flow to have both a response and an error + object. This might happen, for instance, when a response was received + from the server, but there was an error sending it back to the client. + """ + + websocket = None + """ + If this HTTP flow initiated a WebSocket connection, this attribute contains all associated WebSocket data. + """ + + def __init__(self, client_conn, server_conn, live=None, mode="regular"): + super().__init__("http", client_conn, server_conn, live) + self.mode = mode + + _stateobject_attributes = flow.Flow._stateobject_attributes.copy() + # mypy doesn't support update with kwargs + _stateobject_attributes.update( + dict(request=Request, response=Response, websocket=WebSocketData, mode=str) + ) + + def __repr__(self): + s = " float: + """*Read-only:* An alias for `Request.timestamp_start`.""" + return self.request.timestamp_start + + def copy(self): + f = super().copy() + if self.request: + f.request = self.request.copy() + if self.response: + f.response = self.response.copy() + return f + + +__all__ = [ + "HTTPFlow", + "Message", + "Request", + "Response", + "Headers", +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/__init__.py new file mode 100644 index 00000000..c440d433 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/__init__.py @@ -0,0 +1,6 @@ +from .io import FlowWriter, FlowReader, FilteredFlowWriter, read_flows_from_paths + + +__all__ = [ + "FlowWriter", "FlowReader", "FilteredFlowWriter", "read_flows_from_paths" +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/compat.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/compat.py new file mode 100644 index 00000000..71fdbee5 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/compat.py @@ -0,0 +1,422 @@ +""" +This module handles the import of _internal_mitmproxy flows generated by old versions. + +The flow file version is decoupled from the _internal_mitmproxy release cycle (since +v3.0.0dev) and versioning. Every change or migration gets a new flow file +version number, this prevents issues with developer builds and snapshots. +""" +import uuid +from typing import Any, Dict, Mapping, Union # noqa + +from _internal_mitmproxy import version +from _internal_mitmproxy.utils import strutils + + +def convert_011_012(data): + data[b"version"] = (0, 12) + return data + + +def convert_012_013(data): + data[b"version"] = (0, 13) + return data + + +def convert_013_014(data): + data[b"request"][b"first_line_format"] = data[b"request"].pop(b"form_in") + data[b"request"][b"http_version"] = b"HTTP/" + ".".join( + str(x) for x in data[b"request"].pop(b"httpversion")).encode() + data[b"response"][b"http_version"] = b"HTTP/" + ".".join( + str(x) for x in data[b"response"].pop(b"httpversion")).encode() + data[b"response"][b"status_code"] = data[b"response"].pop(b"code") + data[b"response"][b"body"] = data[b"response"].pop(b"content") + data[b"server_conn"].pop(b"state") + data[b"server_conn"][b"via"] = None + data[b"version"] = (0, 14) + return data + + +def convert_014_015(data): + data[b"version"] = (0, 15) + return data + + +def convert_015_016(data): + for m in (b"request", b"response"): + if b"body" in data[m]: + data[m][b"content"] = data[m].pop(b"body") + if b"msg" in data[b"response"]: + data[b"response"][b"reason"] = data[b"response"].pop(b"msg") + data[b"request"].pop(b"form_out", None) + data[b"version"] = (0, 16) + return data + + +def convert_016_017(data): + data[b"server_conn"][b"peer_address"] = None + data[b"version"] = (0, 17) + return data + + +def convert_017_018(data): + # convert_unicode needs to be called for every dual release and the first py3-only release + data = convert_unicode(data) + + data["server_conn"]["ip_address"] = data["server_conn"].pop("peer_address", None) + data["marked"] = False + data["version"] = (0, 18) + return data + + +def convert_018_019(data): + # convert_unicode needs to be called for every dual release and the first py3-only release + data = convert_unicode(data) + + data["request"].pop("stickyauth", None) + data["request"].pop("stickycookie", None) + data["client_conn"]["sni"] = None + data["client_conn"]["alpn_proto_negotiated"] = None + data["client_conn"]["cipher_name"] = None + data["client_conn"]["tls_version"] = None + data["server_conn"]["alpn_proto_negotiated"] = None + if data["server_conn"]["via"]: + data["server_conn"]["via"]["alpn_proto_negotiated"] = None + data["mode"] = "regular" + data["metadata"] = dict() + data["version"] = (0, 19) + return data + + +def convert_019_100(data): + # convert_unicode needs to be called for every dual release and the first py3-only release + data = convert_unicode(data) + + data["version"] = (1, 0, 0) + return data + + +def convert_100_200(data): + data["version"] = (2, 0, 0) + data["client_conn"]["address"] = data["client_conn"]["address"]["address"] + data["server_conn"]["address"] = data["server_conn"]["address"]["address"] + data["server_conn"]["source_address"] = data["server_conn"]["source_address"]["address"] + if data["server_conn"]["ip_address"]: + data["server_conn"]["ip_address"] = data["server_conn"]["ip_address"]["address"] + + if data["server_conn"]["via"]: + data["server_conn"]["via"]["address"] = data["server_conn"]["via"]["address"]["address"] + data["server_conn"]["via"]["source_address"] = data["server_conn"]["via"]["source_address"]["address"] + if data["server_conn"]["via"]["ip_address"]: + data["server_conn"]["via"]["ip_address"] = data["server_conn"]["via"]["ip_address"]["address"] + + return data + + +def convert_200_300(data): + data["version"] = (3, 0, 0) + data["client_conn"]["mitmcert"] = None + data["server_conn"]["tls_version"] = None + if data["server_conn"]["via"]: + data["server_conn"]["via"]["tls_version"] = None + return data + + +def convert_300_4(data): + data["version"] = 4 + # This is an empty migration to transition to the new versioning scheme. + return data + + +client_connections: Mapping[str, str] = {} +server_connections: Mapping[str, str] = {} + + +def convert_4_5(data): + data["version"] = 5 + client_conn_key = ( + data["client_conn"]["timestamp_start"], + *data["client_conn"]["address"] + ) + server_conn_key = ( + data["server_conn"]["timestamp_start"], + *data["server_conn"]["source_address"] + ) + data["client_conn"]["id"] = client_connections.setdefault(client_conn_key, str(uuid.uuid4())) + data["server_conn"]["id"] = server_connections.setdefault(server_conn_key, str(uuid.uuid4())) + + if data["server_conn"]["via"]: + server_conn_key = ( + data["server_conn"]["via"]["timestamp_start"], + *data["server_conn"]["via"]["source_address"] + ) + data["server_conn"]["via"]["id"] = server_connections.setdefault(server_conn_key, str(uuid.uuid4())) + + return data + + +def convert_5_6(data): + data["version"] = 6 + data["client_conn"]["tls_established"] = data["client_conn"].pop("ssl_established") + data["client_conn"]["timestamp_tls_setup"] = data["client_conn"].pop("timestamp_ssl_setup") + data["server_conn"]["tls_established"] = data["server_conn"].pop("ssl_established") + data["server_conn"]["timestamp_tls_setup"] = data["server_conn"].pop("timestamp_ssl_setup") + if data["server_conn"]["via"]: + data["server_conn"]["via"]["tls_established"] = data["server_conn"]["via"].pop("ssl_established") + data["server_conn"]["via"]["timestamp_tls_setup"] = data["server_conn"]["via"].pop("timestamp_ssl_setup") + return data + + +def convert_6_7(data): + data["version"] = 7 + data["client_conn"]["tls_extensions"] = None + return data + + +def convert_7_8(data): + data["version"] = 8 + if "request" in data and data["request"] is not None: + data["request"]["trailers"] = None + if "response" in data and data["response"] is not None: + data["response"]["trailers"] = None + return data + + +def convert_8_9(data): + data["version"] = 9 + is_request_replay = False + if "request" in data: + data["request"].pop("first_line_format") + data["request"]["authority"] = b"" + is_request_replay = data["request"].pop("is_replay", False) + is_response_replay = False + if "response" in data and data["response"] is not None: + is_response_replay = data["response"].pop("is_replay", False) + if is_request_replay: # pragma: no cover + data["is_replay"] = "request" + elif is_response_replay: # pragma: no cover + data["is_replay"] = "response" + else: + data["is_replay"] = None + return data + + +def convert_9_10(data): + data["version"] = 10 + + def conv_conn(conn): + conn["state"] = 0 + conn["error"] = None + conn["tls"] = conn["tls_established"] + alpn = conn["alpn_proto_negotiated"] + conn["alpn_offers"] = [alpn] if alpn else None + cipher = conn["cipher_name"] + conn["cipher_list"] = [cipher] if cipher else None + + def conv_cconn(conn): + conn["sockname"] = ("", 0) + cc = conn.pop("clientcert", None) + conn["certificate_list"] = [cc] if cc else [] + conv_conn(conn) + + def conv_sconn(conn): + crt = conn.pop("cert", None) + conn["certificate_list"] = [crt] if crt else [] + conn["cipher_name"] = None + conn["via2"] = None + conv_conn(conn) + + conv_cconn(data["client_conn"]) + conv_sconn(data["server_conn"]) + if data["server_conn"]["via"]: + conv_sconn(data["server_conn"]["via"]) + + return data + + +def convert_10_11(data): + data["version"] = 11 + + def conv_conn(conn): + conn["sni"] = strutils.always_str(conn["sni"], "ascii", "backslashreplace") + conn["alpn"] = conn.pop("alpn_proto_negotiated") + conn["alpn_offers"] = conn["alpn_offers"] or [] + conn["cipher_list"] = conn["cipher_list"] or [] + + conv_conn(data["client_conn"]) + conv_conn(data["server_conn"]) + if data["server_conn"]["via"]: + conv_conn(data["server_conn"]["via"]) + + return data + + +_websocket_handshakes = {} + + +def convert_11_12(data): + data["version"] = 12 + + if "websocket" in data["metadata"]: + _websocket_handshakes[data["id"]] = data + + if "websocket_handshake" in data["metadata"]: + ws_flow = data + try: + data = _websocket_handshakes.pop(data["metadata"]["websocket_handshake"]) + except KeyError: + # The handshake flow is missing, which should never really happen. We make up a dummy. + data = { + 'client_conn': data["client_conn"], + 'error': data["error"], + 'id': data["id"], + 'intercepted': data["intercepted"], + 'is_replay': data["is_replay"], + 'marked': data["marked"], + 'metadata': {}, + 'mode': 'transparent', + 'request': {'authority': b'', 'content': None, 'headers': [], 'host': b'unknown', + 'http_version': b'HTTP/1.1', 'method': b'GET', 'path': b'/', 'port': 80, 'scheme': b'http', + 'timestamp_end': 0, 'timestamp_start': 0, 'trailers': None, }, + 'response': None, + 'server_conn': data["server_conn"], + 'type': 'http', + 'version': 12 + } + data["metadata"]["duplicated"] = ( + "This WebSocket flow has been migrated from an old file format version " + "and may appear duplicated." + ) + data["websocket"] = { + "messages": ws_flow["messages"], + "closed_by_client": ws_flow["close_sender"] == "client", + "close_code": ws_flow["close_code"], + "close_reason": ws_flow["close_reason"], + "timestamp_end": data.get("server_conn", {}).get("timestamp_end", None), + } + + else: + data["websocket"] = None + + return data + + +def convert_12_13(data): + data["version"] = 13 + if data["marked"]: + data["marked"] = ":default:" + else: + data["marked"] = "" + return data + + +def convert_13_14(data): + data["version"] = 14 + data["comment"] = "" + # bugfix for https://github.com/_internal_mitmproxy/_internal_mitmproxy/issues/4576 + if data.get("response", None) and data["response"]["timestamp_start"] is None: + data["response"]["timestamp_start"] = data["request"]["timestamp_end"] + data["response"]["timestamp_end"] = data["request"]["timestamp_end"] + 1 + return data + + +def convert_14_15(data): + data["version"] = 15 + if data.get("websocket", None): + # Add "injected" attribute. + data["websocket"]["messages"] = [ + msg + [False] + for msg in data["websocket"]["messages"] + ] + return data + + +def _convert_dict_keys(o: Any) -> Any: + if isinstance(o, dict): + return {strutils.always_str(k): _convert_dict_keys(v) for k, v in o.items()} + else: + return o + + +def _convert_dict_vals(o: dict, values_to_convert: dict) -> dict: + for k, v in values_to_convert.items(): + if not o or k not in o: + continue # pragma: no cover + if v is True: + o[k] = strutils.always_str(o[k]) + else: + _convert_dict_vals(o[k], v) + return o + + +def convert_unicode(data: dict) -> dict: + """ + This method converts between Python 3 and Python 2 dumpfiles. + """ + data = _convert_dict_keys(data) + data = _convert_dict_vals( + data, { + "type": True, + "id": True, + "request": { + "first_line_format": True + }, + "error": { + "msg": True + } + } + ) + return data + + +converters = { + (0, 11): convert_011_012, + (0, 12): convert_012_013, + (0, 13): convert_013_014, + (0, 14): convert_014_015, + (0, 15): convert_015_016, + (0, 16): convert_016_017, + (0, 17): convert_017_018, + (0, 18): convert_018_019, + (0, 19): convert_019_100, + (1, 0): convert_100_200, + (2, 0): convert_200_300, + (3, 0): convert_300_4, + 4: convert_4_5, + 5: convert_5_6, + 6: convert_6_7, + 7: convert_7_8, + 8: convert_8_9, + 9: convert_9_10, + 10: convert_10_11, + 11: convert_11_12, + 12: convert_12_13, + 13: convert_13_14, + 14: convert_14_15, +} + + +def migrate_flow(flow_data: Dict[Union[bytes, str], Any]) -> Dict[Union[bytes, str], Any]: + while True: + flow_version = flow_data.get(b"version", flow_data.get("version")) + + # Historically, we used the _internal_mitmproxy minor version tuple as the flow format version. + if not isinstance(flow_version, int): + flow_version = tuple(flow_version)[:2] + + if flow_version == version.FLOW_FORMAT_VERSION: + break + elif flow_version in converters: + flow_data = converters[flow_version](flow_data) + else: + should_upgrade = ( + isinstance(flow_version, int) + and flow_version > version.FLOW_FORMAT_VERSION + ) + raise ValueError( + "{} cannot read files with flow format version {}{}.".format( + version._internal_mitmproxy, + flow_version, + ", please update _internal_mitmproxy" if should_upgrade else "" + ) + ) + return flow_data diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/io.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/io.py new file mode 100644 index 00000000..543b2125 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/io.py @@ -0,0 +1,84 @@ +import os +from typing import Any, Dict, IO, Iterable, Type, Union, cast + +from _internal_mitmproxy import exceptions +from _internal_mitmproxy import flow +from _internal_mitmproxy import flowfilter +from _internal_mitmproxy import http +from _internal_mitmproxy import tcp +from _internal_mitmproxy.io import compat +from _internal_mitmproxy.io import tnetstring + +FLOW_TYPES: Dict[str, Type[flow.Flow]] = dict( + http=http.HTTPFlow, + tcp=tcp.TCPFlow, +) + + +class FlowWriter: + def __init__(self, fo): + self.fo = fo + + def add(self, flow): + d = flow.get_state() + tnetstring.dump(d, self.fo) + + +class FlowReader: + def __init__(self, fo: IO[bytes]): + self.fo: IO[bytes] = fo + + def stream(self) -> Iterable[flow.Flow]: + """ + Yields Flow objects from the dump. + """ + try: + while True: + # FIXME: This cast hides a lack of dynamic type checking + loaded = cast( + Dict[Union[bytes, str], Any], + tnetstring.load(self.fo), + ) + try: + mdata = compat.migrate_flow(loaded) + except ValueError as e: + raise exceptions.FlowReadException(str(e)) + if mdata["type"] not in FLOW_TYPES: + raise exceptions.FlowReadException("Unknown flow type: {}".format(mdata["type"])) + yield FLOW_TYPES[mdata["type"]].from_state(mdata) + except (ValueError, TypeError, IndexError) as e: + if str(e) == "not a tnetstring: empty file": + return # Error is due to EOF + raise exceptions.FlowReadException("Invalid data format.") + + +class FilteredFlowWriter: + def __init__(self, fo, flt): + self.fo = fo + self.flt = flt + + def add(self, f: flow.Flow): + if self.flt and not flowfilter.match(self.flt, f): + return + d = f.get_state() + tnetstring.dump(d, self.fo) + + +def read_flows_from_paths(paths): + """ + Given a list of filepaths, read all flows and return a list of them. + From a performance perspective, streaming would be advisable - + however, if there's an error with one of the files, we want it to be raised immediately. + + Raises: + FlowReadException, if any error occurs. + """ + try: + flows = [] + for path in paths: + path = os.path.expanduser(path) + with open(path, "rb") as f: + flows.extend(FlowReader(f).stream()) + except OSError as e: + raise exceptions.FlowReadException(e.strerror) + return flows diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/tnetstring.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/tnetstring.py new file mode 100644 index 00000000..bfe06c0c --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/io/tnetstring.py @@ -0,0 +1,250 @@ +""" +tnetstring: data serialization using typed netstrings +====================================================== + +This is a custom Python 3 implementation of tnetstrings. +Compared to other implementations, the main difference +is that this implementation supports a custom unicode datatype. + +An ordinary tnetstring is a blob of data prefixed with its length and postfixed +with its type. Here are some examples: + + >>> tnetstring.dumps("hello world") + 11:hello world, + >>> tnetstring.dumps(12345) + 5:12345# + >>> tnetstring.dumps([12345, True, 0]) + 19:5:12345#4:true!1:0#] + +This module gives you the following functions: + + :dump: dump an object as a tnetstring to a file + :dumps: dump an object as a tnetstring to a string + :load: load a tnetstring-encoded object from a file + :loads: load a tnetstring-encoded object from a string + +Note that since parsing a tnetstring requires reading all the data into memory +at once, there's no efficiency gain from using the file-based versions of these +functions. They're only here so you can use load() to read precisely one +item from a file or socket without consuming any extra data. + +The tnetstrings specification explicitly states that strings are binary blobs +and forbids the use of unicode at the protocol level. +**This implementation decodes dictionary keys as surrogate-escaped ASCII**, +all other strings are returned as plain bytes. + +:Copyright: (c) 2012-2013 by Ryan Kelly . +:Copyright: (c) 2014 by Carlo Pires . +:Copyright: (c) 2016 by Maximilian Hils . + +:License: MIT +""" + +import collections +import typing + +TSerializable = typing.Union[None, str, bool, int, float, bytes, list, tuple, dict] + + +def dumps(value: TSerializable) -> bytes: + """ + This function dumps a python object as a tnetstring. + """ + # This uses a deque to collect output fragments in reverse order, + # then joins them together at the end. It's measurably faster + # than creating all the intermediate strings. + q: collections.deque = collections.deque() + _rdumpq(q, 0, value) + return b''.join(q) + + +def dump(value: TSerializable, file_handle: typing.IO[bytes]) -> None: + """ + This function dumps a python object as a tnetstring and + writes it to the given file. + """ + file_handle.write(dumps(value)) + + +def _rdumpq(q: collections.deque, size: int, value: TSerializable) -> int: + """ + Dump value as a tnetstring, to a deque instance, last chunks first. + + This function generates the tnetstring representation of the given value, + pushing chunks of the output onto the given deque instance. It pushes + the last chunk first, then recursively generates more chunks. + + When passed in the current size of the string in the queue, it will return + the new size of the string in the queue. + + Operating last-chunk-first makes it easy to calculate the size written + for recursive structures without having to build their representation as + a string. This is measurably faster than generating the intermediate + strings, especially on deeply nested structures. + """ + write = q.appendleft + if value is None: + write(b'0:~') + return size + 3 + elif value is True: + write(b'4:true!') + return size + 7 + elif value is False: + write(b'5:false!') + return size + 8 + elif isinstance(value, int): + data = str(value).encode() + ldata = len(data) + span = str(ldata).encode() + write(b'%s:%s#' % (span, data)) + return size + 2 + len(span) + ldata + elif isinstance(value, float): + # Use repr() for float rather than str(). + # It round-trips more accurately. + # Probably unnecessary in later python versions that + # use David Gay's ftoa routines. + data = repr(value).encode() + ldata = len(data) + span = str(ldata).encode() + write(b'%s:%s^' % (span, data)) + return size + 2 + len(span) + ldata + elif isinstance(value, bytes): + data = value + ldata = len(data) + span = str(ldata).encode() + write(b',') + write(data) + write(b':') + write(span) + return size + 2 + len(span) + ldata + elif isinstance(value, str): + data = value.encode("utf8") + ldata = len(data) + span = str(ldata).encode() + write(b';') + write(data) + write(b':') + write(span) + return size + 2 + len(span) + ldata + elif isinstance(value, (list, tuple)): + write(b']') + init_size = size = size + 1 + for item in reversed(value): + size = _rdumpq(q, size, item) + span = str(size - init_size).encode() + write(b':') + write(span) + return size + 1 + len(span) + elif isinstance(value, dict): + write(b'}') + init_size = size = size + 1 + for (k, v) in value.items(): + size = _rdumpq(q, size, v) + size = _rdumpq(q, size, k) + span = str(size - init_size).encode() + write(b':') + write(span) + return size + 1 + len(span) + else: + raise ValueError("unserializable object: {} ({})".format(value, type(value))) + + +def loads(string: bytes) -> TSerializable: + """ + This function parses a tnetstring into a python object. + """ + return pop(string)[0] + + +def load(file_handle: typing.IO[bytes]) -> TSerializable: + """load(file) -> object + + This function reads a tnetstring from a file and parses it into a + python object. The file must support the read() method, and this + function promises not to read more data than necessary. + """ + # Read the length prefix one char at a time. + # Note that the netstring spec explicitly forbids padding zeros. + c = file_handle.read(1) + if c == b"": # we want to detect this special case. + raise ValueError("not a tnetstring: empty file") + data_length = b"" + while c.isdigit(): + data_length += c + if len(data_length) > 12: + raise ValueError("not a tnetstring: absurdly large length prefix") + c = file_handle.read(1) + if c != b":": + raise ValueError("not a tnetstring: missing or invalid length prefix") + + data = file_handle.read(int(data_length)) + data_type = file_handle.read(1)[0] + + return parse(data_type, data) + + +def parse(data_type: int, data: bytes) -> TSerializable: + if data_type == ord(b','): + return data + if data_type == ord(b';'): + return data.decode("utf8") + if data_type == ord(b'#'): + try: + return int(data) + except ValueError: + raise ValueError(f"not a tnetstring: invalid integer literal: {data!r}") + if data_type == ord(b'^'): + try: + return float(data) + except ValueError: + raise ValueError(f"not a tnetstring: invalid float literal: {data!r}") + if data_type == ord(b'!'): + if data == b'true': + return True + elif data == b'false': + return False + else: + raise ValueError(f"not a tnetstring: invalid boolean literal: {data!r}") + if data_type == ord(b'~'): + if data: + raise ValueError(f"not a tnetstring: invalid null literal: {data!r}") + return None + if data_type == ord(b']'): + l = [] + while data: + item, data = pop(data) + l.append(item) # type: ignore + return l + if data_type == ord(b'}'): + d = {} + while data: + key, data = pop(data) + val, data = pop(data) + d[key] = val # type: ignore + return d + raise ValueError(f"unknown type tag: {data_type}") + + +def pop(data: bytes) -> typing.Tuple[TSerializable, bytes]: + """ + This function parses a tnetstring into a python object. + It returns a tuple giving the parsed object and a string + containing any unparsed data from the end of the string. + """ + # Parse out data length, type and remaining string. + try: + blength, data = data.split(b':', 1) + length = int(blength) + except ValueError: + raise ValueError(f"not a tnetstring: missing or invalid length prefix: {data!r}") + try: + data, data_type, remain = data[:length], data[length], data[length + 1:] + except IndexError: + # This fires if len(data) < dlen, meaning we don't need + # to further validate that data is the right length. + raise ValueError(f"not a tnetstring: invalid length prefix: {length}") + # Parse the data based on the type tag. + return parse(data_type, data), remain + + +__all__ = ["dump", "dumps", "load", "loads", "pop"] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/log.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/log.py new file mode 100644 index 00000000..50e12bed --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/log.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass + +from _internal_mitmproxy import hooks + + +class LogEntry: + def __init__(self, msg, level): + # it's important that we serialize to string here already so that we don't pick up changes + # happening after this log statement. + self.msg = str(msg) + self.level = level + + def __eq__(self, other): + if isinstance(other, LogEntry): + return self.__dict__ == other.__dict__ + return False + + def __repr__(self): + return f"LogEntry({self.msg}, {self.level})" + + +class Log: + """ + The central logger, exposed to scripts as _internal_mitmproxy.ctx.log. + """ + def __init__(self, master): + self.master = master + + def debug(self, txt): + """ + Log with level debug. + """ + self(txt, "debug") + + def info(self, txt): + """ + Log with level info. + """ + self(txt, "info") + + def alert(self, txt): + """ + Log with level alert. Alerts have the same urgency as info, but + signals to interactive tools that the user's attention should be + drawn to the output even if they're not currently looking at the + event log. + """ + self(txt, "alert") + + def warn(self, txt): + """ + Log with level warn. + """ + self(txt, "warn") + + def error(self, txt): + """ + Log with level error. + """ + self(txt, "error") + + def __call__(self, text, level="info"): + self.master.event_loop.call_soon_threadsafe( + self.master.addons.trigger, AddLogHook(LogEntry(text, level)), + ) + + +@dataclass +class AddLogHook(hooks.Hook): + """ + Called whenever a new log entry is created through the _internal_mitmproxy + context. Be careful not to log from this event, which will cause an + infinite loop! + """ + entry: LogEntry + + +LogTierOrder = [ + "error", + "warn", + "info", + "alert", + "debug", +] + + +def log_tier(level): + return dict(error=0, warn=1, info=2, alert=2, debug=3).get(level) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/check.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/check.py new file mode 100644 index 00000000..133521a4 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/check.py @@ -0,0 +1,43 @@ +import ipaddress +import re + +# Allow underscore in host name +# Note: This could be a DNS label, a hostname, a FQDN, or an IP +from typing import AnyStr + +_label_valid = re.compile(br"[A-Z\d\-_]{1,63}$", re.IGNORECASE) + + +def is_valid_host(host: AnyStr) -> bool: + """ + Checks if the passed bytes are a valid DNS hostname or an IPv4/IPv6 address. + """ + if isinstance(host, str): + try: + host_bytes = host.encode("idna") + except UnicodeError: + return False + else: + host_bytes = host + try: + host_bytes.decode("idna") + except ValueError: + return False + # RFC1035: 255 bytes or less. + if len(host_bytes) > 255: + return False + if host_bytes and host_bytes.endswith(b"."): + host_bytes = host_bytes[:-1] + # DNS hostname + if all(_label_valid.match(x) for x in host_bytes.split(b".")): + return True + # IPv4/IPv6 address + try: + ipaddress.ip_address(host_bytes.decode('idna')) + return True + except ValueError: + return False + + +def is_valid_port(port: int) -> bool: + return 0 <= port <= 65535 diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/encoding.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/encoding.py new file mode 100644 index 00000000..b4f92ebb --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/encoding.py @@ -0,0 +1,242 @@ +""" +Utility functions for decoding response bodies. +""" + +import codecs +import collections +from io import BytesIO + +# import gzip +# import zlib +# import brotli +# import zstandard as zstd + +gzip = ... +zlib = ... +brotli = ... +zstd = ... + +from typing import Union, Optional, AnyStr, overload # noqa + +# We have a shared single-element cache for encoding and decoding. +# This is quite useful in practice, e.g. +# flow.request.content = flow.request.content.replace(b"foo", b"bar") +# does not require an .encode() call if content does not contain b"foo" +CachedDecode = collections.namedtuple("CachedDecode", "encoded encoding errors decoded") +_cache = CachedDecode(None, None, None, None) + + +@overload +def decode(encoded: None, encoding: str, errors: str = "strict") -> None: ... + + +@overload +def decode(encoded: str, encoding: str, errors: str = "strict") -> str: ... + + +@overload +def decode( + encoded: bytes, encoding: str, errors: str = "strict" +) -> Union[str, bytes]: ... + + +def decode( + encoded: Union[None, str, bytes], encoding: str, errors: str = "strict" +) -> Union[None, str, bytes]: + """ + Decode the given input object + + Returns: + The decoded value + + Raises: + ValueError, if decoding fails. + """ + if encoded is None: + return None + encoding = encoding.lower() + + global _cache + cached = ( + isinstance(encoded, bytes) + and _cache.encoded == encoded + and _cache.encoding == encoding + and _cache.errors == errors + ) + if cached: + return _cache.decoded + try: + try: + decoded = custom_decode[encoding](encoded) + except KeyError: + decoded = codecs.decode(encoded, encoding, errors) # type: ignore + if encoding in ("gzip", "deflate", "deflateraw", "br", "zstd"): + _cache = CachedDecode(encoded, encoding, errors, decoded) + return decoded + except TypeError: + raise + except Exception as e: + raise ValueError( + "{} when decoding {} with {}: {}".format( + type(e).__name__, + repr(encoded)[:10], + repr(encoding), + repr(e), + ) + ) + + +@overload +def encode(decoded: None, encoding: str, errors: str = "strict") -> None: ... + + +@overload +def encode( + decoded: str, encoding: str, errors: str = "strict" +) -> Union[str, bytes]: ... + + +@overload +def encode(decoded: bytes, encoding: str, errors: str = "strict") -> bytes: ... + + +def encode( + decoded: Union[None, str, bytes], encoding, errors="strict" +) -> Union[None, str, bytes]: + """ + Encode the given input object + + Returns: + The encoded value + + Raises: + ValueError, if encoding fails. + """ + if decoded is None: + return None + encoding = encoding.lower() + + global _cache + cached = ( + isinstance(decoded, bytes) + and _cache.decoded == decoded + and _cache.encoding == encoding + and _cache.errors == errors + ) + if cached: + return _cache.encoded + try: + try: + encoded = custom_encode[encoding](decoded) + except KeyError: + encoded = codecs.encode(decoded, encoding, errors) # type: ignore + if encoding in ("gzip", "deflate", "deflateraw", "br", "zstd"): + _cache = CachedDecode(encoded, encoding, errors, decoded) + return encoded + except TypeError: + raise + except Exception as e: + raise ValueError( + "{} when encoding {} with {}: {}".format( + type(e).__name__, + repr(decoded)[:10], + repr(encoding), + repr(e), + ) + ) + + +def identity(content): + """ + Returns content unchanged. Identity is the default value of + Accept-Encoding headers. + """ + return content + + +def decode_gzip(content: bytes) -> bytes: + if not content: + return b"" + gfile = gzip.GzipFile(fileobj=BytesIO(content)) + return gfile.read() + + +def encode_gzip(content: bytes) -> bytes: + s = BytesIO() + gf = gzip.GzipFile(fileobj=s, mode="wb") + gf.write(content) + gf.close() + return s.getvalue() + + +def decode_brotli(content: bytes) -> bytes: + if not content: + return b"" + return brotli.decompress(content) + + +def encode_brotli(content: bytes) -> bytes: + return brotli.compress(content) + + +def decode_zstd(content: bytes) -> bytes: + if not content: + return b"" + zstd_ctx = zstd.ZstdDecompressor() + try: + return zstd_ctx.decompress(content) + except zstd.ZstdError: + # If the zstd stream is streamed without a size header, + # try decoding with a 10MiB output buffer + return zstd_ctx.decompress(content, max_output_size=10 * 2**20) + + +def encode_zstd(content: bytes) -> bytes: + zstd_ctx = zstd.ZstdCompressor() + return zstd_ctx.compress(content) + + +def decode_deflate(content: bytes) -> bytes: + """ + Returns decompressed data for DEFLATE. Some servers may respond with + compressed data without a zlib header or checksum. An undocumented + feature of zlib permits the lenient decompression of data missing both + values. + + http://bugs.python.org/issue5784 + """ + if not content: + return b"" + try: + return zlib.decompress(content) + except zlib.error: + return zlib.decompress(content, -15) + + +def encode_deflate(content: bytes) -> bytes: + """ + Returns compressed content, always including zlib header and checksum. + """ + return zlib.compress(content) + + +custom_decode = { + "none": identity, + "identity": identity, + "gzip": decode_gzip, + "deflate": decode_deflate, + "deflateraw": decode_deflate, + "br": decode_brotli, + "zstd": decode_zstd, +} +custom_encode = { + "none": identity, + "identity": identity, + "gzip": encode_gzip, + "deflate": encode_deflate, + "deflateraw": encode_deflate, + "br": encode_brotli, + "zstd": encode_zstd, +} + +__all__ = ["encode", "decode"] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/cookies.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/cookies.py new file mode 100644 index 00000000..9bb1e8d9 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/cookies.py @@ -0,0 +1,384 @@ +import email.utils +import re +import time +from typing import Tuple, List, Iterable + +from _internal_mitmproxy.coretypes import multidict + +""" +A flexible module for cookie parsing and manipulation. + +This module differs from usual standards-compliant cookie modules in a number +of ways. We try to be as permissive as possible, and to retain even mal-formed +information. Duplicate cookies are preserved in parsing, and can be set in +formatting. We do attempt to escape and quote values where needed, but will not +reject data that violate the specs. + +Parsing accepts the formats in RFC6265 and partially RFC2109 and RFC2965. We +also parse the comma-separated variant of Set-Cookie that allows multiple +cookies to be set in a single header. Serialization follows RFC6265. + + http://tools.ietf.org/html/rfc6265 + http://tools.ietf.org/html/rfc2109 + http://tools.ietf.org/html/rfc2965 +""" + +_cookie_params = {'expires', 'path', 'comment', 'max-age', 'secure', 'httponly', 'version'} + +ESCAPE = re.compile(r"([\"\\])") + + +class CookieAttrs(multidict.MultiDict): + @staticmethod + def _kconv(key): + return key.lower() + + @staticmethod + def _reduce_values(values): + # See the StickyCookieTest for a weird cookie that only makes sense + # if we take the last part. + return values[-1] + + +TSetCookie = Tuple[str, str, CookieAttrs] +TPairs = List[List[str]] # TODO: Should be List[Tuple[str,str]]? + + +def _read_until(s, start, term): + """ + Read until one of the characters in term is reached. + """ + if start == len(s): + return "", start + 1 + for i in range(start, len(s)): + if s[i] in term: + return s[start:i], i + return s[start:i + 1], i + 1 + + +def _read_quoted_string(s, start): + """ + start: offset to the first quote of the string to be read + + A sort of loose super-set of the various quoted string specifications. + + RFC6265 disallows backslashes or double quotes within quoted strings. + Prior RFCs use backslashes to escape. This leaves us free to apply + backslash escaping by default and be compatible with everything. + """ + escaping = False + ret = [] + # Skip the first quote + i = start # initialize in case the loop doesn't run. + for i in range(start + 1, len(s)): + if escaping: + ret.append(s[i]) + escaping = False + elif s[i] == '"': + break + elif s[i] == "\\": + escaping = True + else: + ret.append(s[i]) + return "".join(ret), i + 1 + + +def _read_key(s, start, delims=";="): + """ + Read a key - the LHS of a token/value pair in a cookie. + """ + return _read_until(s, start, delims) + + +def _read_value(s, start, delims): + """ + Reads a value - the RHS of a token/value pair in a cookie. + """ + if start >= len(s): + return "", start + elif s[start] == '"': + return _read_quoted_string(s, start) + else: + return _read_until(s, start, delims) + + +def _read_cookie_pairs(s, off=0): + """ + Read pairs of lhs=rhs values from Cookie headers. + + off: start offset + """ + pairs = [] + + while True: + lhs, off = _read_key(s, off) + lhs = lhs.lstrip() + + rhs = "" + if off < len(s) and s[off] == "=": + rhs, off = _read_value(s, off + 1, ";") + if rhs or lhs: + pairs.append([lhs, rhs]) + + off += 1 + + if not off < len(s): + break + + return pairs, off + + +def _read_set_cookie_pairs(s: str, off=0) -> Tuple[List[TPairs], int]: + """ + Read pairs of lhs=rhs values from SetCookie headers while handling multiple cookies. + + off: start offset + specials: attributes that are treated specially + """ + cookies: List[TPairs] = [] + pairs: TPairs = [] + + while True: + lhs, off = _read_key(s, off, ";=,") + lhs = lhs.lstrip() + + rhs = "" + if off < len(s) and s[off] == "=": + rhs, off = _read_value(s, off + 1, ";,") + + # Special handling of attributes + if lhs.lower() == "expires": + # 'expires' values can contain commas in them so they need to + # be handled separately. + + # We actually bank on the fact that the expires value WILL + # contain a comma. Things will fail, if they don't. + + # '3' is just a heuristic we use to determine whether we've + # only read a part of the expires value and we should read more. + if len(rhs) <= 3: + trail, off = _read_value(s, off + 1, ";,") + rhs = rhs + "," + trail + + # as long as there's a "=", we consider it a pair + pairs.append([lhs, rhs]) + + elif lhs: + pairs.append([lhs, rhs]) + + # comma marks the beginning of a new cookie + if off < len(s) and s[off] == ",": + cookies.append(pairs) + pairs = [] + + off += 1 + + if not off < len(s): + break + + if pairs or not cookies: + cookies.append(pairs) + + return cookies, off + + +def _has_special(s: str) -> bool: + for i in s: + if i in '",;\\': + return True + o = ord(i) + if o < 0x21 or o > 0x7e: + return True + return False + + +def _format_pairs(pairs, specials=(), sep="; "): + """ + specials: A lower-cased list of keys that will not be quoted. + """ + vals = [] + for k, v in pairs: + if k.lower() not in specials and _has_special(v): + v = ESCAPE.sub(r"\\\1", v) + v = '"%s"' % v + vals.append(f"{k}={v}") + return sep.join(vals) + + +def _format_set_cookie_pairs(lst): + return _format_pairs( + lst, + specials=("expires", "path") + ) + + +def parse_cookie_header(line): + """ + Parse a Cookie header value. + Returns a list of (lhs, rhs) tuples. + """ + pairs, off_ = _read_cookie_pairs(line) + return pairs + + +def parse_cookie_headers(cookie_headers): + cookie_list = [] + for header in cookie_headers: + cookie_list.extend(parse_cookie_header(header)) + return cookie_list + + +def format_cookie_header(lst): + """ + Formats a Cookie header value. + """ + return _format_pairs(lst) + + +def parse_set_cookie_header(line: str) -> List[TSetCookie]: + """ + Parse a Set-Cookie header value + + Returns: + A list of (name, value, attrs) tuples, where attrs is a + CookieAttrs dict of attributes. No attempt is made to parse attribute + values - they are treated purely as strings. + """ + cookie_pairs, off = _read_set_cookie_pairs(line) + cookies = [] + for pairs in cookie_pairs: + if pairs: + cookie, *attrs = pairs + cookies.append(( + cookie[0], + cookie[1], + CookieAttrs(attrs) + )) + return cookies + + +def parse_set_cookie_headers(headers: Iterable[str]) -> List[TSetCookie]: + rv = [] + for header in headers: + cookies = parse_set_cookie_header(header) + rv.extend(cookies) + return rv + + +def format_set_cookie_header(set_cookies: List[TSetCookie]) -> str: + """ + Formats a Set-Cookie header value. + """ + + rv = [] + + for name, value, attrs in set_cookies: + + pairs = [(name, value)] + pairs.extend( + attrs.fields if hasattr(attrs, "fields") else attrs + ) + + rv.append(_format_set_cookie_pairs(pairs)) + + return ", ".join(rv) + + +def refresh_set_cookie_header(c: str, delta: int) -> str: + """ + Args: + c: A Set-Cookie string + delta: Time delta in seconds + Returns: + A refreshed Set-Cookie string + Raises: + ValueError, if the cookie is invalid. + """ + cookies = parse_set_cookie_header(c) + for cookie in cookies: + name, value, attrs = cookie + if not name or not value: + raise ValueError("Invalid Cookie") + + if "expires" in attrs: + e = email.utils.parsedate_tz(attrs["expires"]) + if e: + f = email.utils.mktime_tz(e) + delta + attrs.set_all("expires", [email.utils.formatdate(f, usegmt=True)]) + else: + # This can happen when the expires tag is invalid. + # reddit.com sends a an expires tag like this: "Thu, 31 Dec + # 2037 23:59:59 GMT", which is valid RFC 1123, but not + # strictly correct according to the cookie spec. Browsers + # appear to parse this tolerantly - maybe we should too. + # For now, we just ignore this. + del attrs["expires"] + return format_set_cookie_header(cookies) + + +def get_expiration_ts(cookie_attrs): + """ + Determines the time when the cookie will be expired. + + Considering both 'expires' and 'max-age' parameters. + + Returns: timestamp of when the cookie will expire. + None, if no expiration time is set. + """ + if 'expires' in cookie_attrs: + e = email.utils.parsedate_tz(cookie_attrs["expires"]) + if e: + return email.utils.mktime_tz(e) + + elif 'max-age' in cookie_attrs: + try: + max_age = int(cookie_attrs['Max-Age']) + except ValueError: + pass + else: + now_ts = time.time() + return now_ts + max_age + + return None + + +def is_expired(cookie_attrs): + """ + Determines whether a cookie has expired. + + Returns: boolean + """ + + exp_ts = get_expiration_ts(cookie_attrs) + now_ts = time.time() + + # If no expiration information was provided with the cookie + if exp_ts is None: + return False + else: + return exp_ts <= now_ts + + +def group_cookies(pairs): + """ + Converts a list of pairs to a (name, value, attrs) for each cookie. + """ + + if not pairs: + return [] + + cookie_list = [] + + # First pair is always a new cookie + name, value = pairs[0] + attrs = [] + + for k, v in pairs[1:]: + if k.lower() in _cookie_params: + attrs.append((k, v)) + else: + cookie_list.append((name, value, CookieAttrs(attrs))) + name, value, attrs = k, v, [] + + cookie_list.append((name, value, CookieAttrs(attrs))) + return cookie_list diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/headers.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/headers.py new file mode 100644 index 00000000..66739962 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/headers.py @@ -0,0 +1,41 @@ +import collections +from typing import Dict, Optional, Tuple + + +def parse_content_type(c: str) -> Optional[Tuple[str, str, Dict[str, str]]]: + """ + A simple parser for content-type values. Returns a (type, subtype, + parameters) tuple, where type and subtype are strings, and parameters + is a dict. If the string could not be parsed, return None. + + E.g. the following string: + + text/html; charset=UTF-8 + + Returns: + + ("text", "html", {"charset": "UTF-8"}) + """ + parts = c.split(";", 1) + ts = parts[0].split("/", 1) + if len(ts) != 2: + return None + d = collections.OrderedDict() + if len(parts) == 2: + for i in parts[1].split(";"): + clause = i.split("=", 1) + if len(clause) == 2: + d[clause[0].strip()] = clause[1].strip() + return ts[0].lower(), ts[1].lower(), d + + +def assemble_content_type(type, subtype, parameters): + if not parameters: + return f"{type}/{subtype}" + params = "; ".join( + f"{k}={v}" + for k, v in parameters.items() + ) + return "{}/{}; {}".format( + type, subtype, params + ) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/__init__.py new file mode 100644 index 00000000..5de7a1ab --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/__init__.py @@ -0,0 +1,24 @@ +from .read import ( + read_request_head, + read_response_head, + connection_close, + expected_http_body_size, + validate_headers, +) +from .assemble import ( + assemble_request, assemble_request_head, + assemble_response, assemble_response_head, + assemble_body, +) + + +__all__ = [ + "read_request_head", + "read_response_head", + "connection_close", + "expected_http_body_size", + "validate_headers", + "assemble_request", "assemble_request_head", + "assemble_response", "assemble_response_head", + "assemble_body", +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/assemble.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/assemble.py new file mode 100644 index 00000000..1290df67 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/assemble.py @@ -0,0 +1,89 @@ +def assemble_request(request): + if request.data.content is None: + raise ValueError("Cannot assemble flow with missing content") + head = assemble_request_head(request) + body = b"".join(assemble_body(request.data.headers, [request.data.content], request.data.trailers)) + return head + body + + +def assemble_request_head(request): + first_line = _assemble_request_line(request.data) + headers = _assemble_request_headers(request.data) + return b"%s\r\n%s\r\n" % (first_line, headers) + + +def assemble_response(response): + if response.data.content is None: + raise ValueError("Cannot assemble flow with missing content") + head = assemble_response_head(response) + body = b"".join(assemble_body(response.data.headers, [response.data.content], response.data.trailers)) + return head + body + + +def assemble_response_head(response): + first_line = _assemble_response_line(response.data) + headers = _assemble_response_headers(response.data) + return b"%s\r\n%s\r\n" % (first_line, headers) + + +def assemble_body(headers, body_chunks, trailers): + if "chunked" in headers.get("transfer-encoding", "").lower(): + for chunk in body_chunks: + if chunk: + yield b"%x\r\n%s\r\n" % (len(chunk), chunk) + if trailers: + yield b"0\r\n%s\r\n" % trailers + else: + yield b"0\r\n\r\n" + else: + if trailers: + raise ValueError("Sending HTTP/1.1 trailer headers requires transfer-encoding: chunked") + for chunk in body_chunks: + yield chunk + + +def _assemble_request_line(request_data): + """ + Args: + request_data (_internal_mitmproxy.net.http.request.RequestData) + """ + if request_data.method.upper() == b"CONNECT": + return b"%s %s %s" % ( + request_data.method, + request_data.authority, + request_data.http_version + ) + elif request_data.authority: + return b"%s %s://%s%s %s" % ( + request_data.method, + request_data.scheme, + request_data.authority, + request_data.path, + request_data.http_version + ) + else: + return b"%s %s %s" % ( + request_data.method, + request_data.path, + request_data.http_version + ) + + +def _assemble_request_headers(request_data): + """ + Args: + request_data (_internal_mitmproxy.net.http.request.RequestData) + """ + return bytes(request_data.headers) + + +def _assemble_response_line(response_data): + return b"%s %d %s" % ( + response_data.http_version, + response_data.status_code, + response_data.reason, + ) + + +def _assemble_response_headers(response): + return bytes(response.headers) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/read.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/read.py new file mode 100644 index 00000000..17c52e58 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/http1/read.py @@ -0,0 +1,347 @@ +import re +import time +from typing import List, Tuple, Iterable, Optional + +from _internal_mitmproxy.http import Request, Headers, Response +from _internal_mitmproxy.net.http import url + + +def get_header_tokens(headers, key): + """ + Retrieve all tokens for a header key. A number of different headers + follow a pattern where each header line can containe comma-separated + tokens, and headers can be set multiple times. + """ + if key not in headers: + return [] + tokens = headers[key].split(",") + return [token.strip() for token in tokens] + + +def connection_close(http_version, headers): + """ + Checks the message to see if the client connection should be closed + according to RFC 2616 Section 8.1. + If we don't have a Connection header, HTTP 1.1 connections are assumed + to be persistent. + """ + if "connection" in headers: + tokens = get_header_tokens(headers, "connection") + if "close" in tokens: + return True + elif "keep-alive" in tokens: + return False + + return http_version not in ( + "HTTP/1.1", b"HTTP/1.1", + "HTTP/2.0", b"HTTP/2.0", + ) + + +# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2: Header fields are tokens. +# "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +_valid_header_name = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$") + + +def validate_headers( + headers: Headers +) -> None: + """ + Validate headers to avoid request smuggling attacks. Raises a ValueError if they are malformed. + """ + + te_found = False + cl_found = False + + for (name, value) in headers.fields: + if not _valid_header_name.match(name): + raise ValueError(f"Received an invalid header name: {name!r}. Invalid header names may introduce " + f"request smuggling vulnerabilities. Disable the validate_inbound_headers option " + f"to skip this security check.") + + name_lower = name.lower() + te_found = te_found or name_lower == b"transfer-encoding" + cl_found = cl_found or name_lower == b"content-length" + + if te_found and cl_found: + raise ValueError("Received both a Transfer-Encoding and a Content-Length header, " + "refusing as recommended in RFC 7230 Section 3.3.3. " + "See https://github.com/_internal_mitmproxy/_internal_mitmproxy/issues/4799 for details. " + "Disable the validate_inbound_headers option to skip this security check.") + + +def expected_http_body_size( + request: Request, + response: Optional[Response] = None +) -> Optional[int]: + """ + Returns: + The expected body length: + - a positive integer, if the size is known in advance + - None, if the size in unknown in advance (chunked encoding) + - -1, if all data should be read until end of stream. + + Raises: + ValueError, if the content length header is invalid + """ + # Determine response size according to http://tools.ietf.org/html/rfc7230#section-3.3, which is inlined below. + if not response: + headers = request.headers + else: + headers = response.headers + + # 1. Any response to a HEAD request and any response with a 1xx + # (Informational), 204 (No Content), or 304 (Not Modified) status + # code is always terminated by the first empty line after the + # header fields, regardless of the header fields present in the + # message, and thus cannot contain a message body. + if request.method.upper() == "HEAD": + return 0 + if 100 <= response.status_code <= 199: + return 0 + if response.status_code in (204, 304): + return 0 + + # 2. Any 2xx (Successful) response to a CONNECT request implies that + # the connection will become a tunnel immediately after the empty + # line that concludes the header fields. A client MUST ignore any + # Content-Length or Transfer-Encoding header fields received in + # such a message. + if 200 <= response.status_code <= 299 and request.method.upper() == "CONNECT": + return 0 + + # 3. If a Transfer-Encoding header field is present and the chunked + # transfer coding (Section 4.1) is the final encoding, the message + # body length is determined by reading and decoding the chunked + # data until the transfer coding indicates the data is complete. + # + # If a Transfer-Encoding header field is present in a response and + # the chunked transfer coding is not the final encoding, the + # message body length is determined by reading the connection until + # it is closed by the server. If a Transfer-Encoding header field + # is present in a request and the chunked transfer coding is not + # the final encoding, the message body length cannot be determined + # reliably; the server MUST respond with the 400 (Bad Request) + # status code and then close the connection. + # + # If a message is received with both a Transfer-Encoding and a + # Content-Length header field, the Transfer-Encoding overrides the + # Content-Length. Such a message might indicate an attempt to + # perform request smuggling (Section 9.5) or response splitting + # (Section 9.4) and ought to be handled as an error. A sender MUST + # remove the received Content-Length field prior to forwarding such + # a message downstream. + # + if "transfer-encoding" in headers: + # we should make sure that there isn't also a content-length header. + # this is already handled in validate_headers. + + te: str = headers["transfer-encoding"] + if not te.isascii(): + # guard against .lower() transforming non-ascii to ascii + raise ValueError(f"Invalid transfer encoding: {te!r}") + te = te.lower().strip("\t ") + te = re.sub(r"[\t ]*,[\t ]*", ",", te) + if te in ( + "chunked", + "compress,chunked", + "deflate,chunked", + "gzip,chunked", + ): + return None + elif te in ( + "compress", + "deflate", + "gzip", + "identity", + ): + if response: + return -1 + else: + raise ValueError(f"Invalid request transfer encoding, message body cannot be determined reliably.") + else: + raise ValueError(f"Unknown transfer encoding: {headers['transfer-encoding']!r}") + + # 4. If a message is received without Transfer-Encoding and with + # either multiple Content-Length header fields having differing + # field-values or a single Content-Length header field having an + # invalid value, then the message framing is invalid and the + # recipient MUST treat it as an unrecoverable error. If this is a + # request message, the server MUST respond with a 400 (Bad Request) + # status code and then close the connection. If this is a response + # message received by a proxy, the proxy MUST close the connection + # to the server, discard the received response, and send a 502 (Bad + # Gateway) response to the client. If this is a response message + # received by a user agent, the user agent MUST close the + # connection to the server and discard the received response. + # + # 5. If a valid Content-Length header field is present without + # Transfer-Encoding, its decimal value defines the expected message + # body length in octets. If the sender closes the connection or + # the recipient times out before the indicated number of octets are + # received, the recipient MUST consider the message to be + # incomplete and close the connection. + if "content-length" in headers: + sizes = headers.get_all("content-length") + different_content_length_headers = any(x != sizes[0] for x in sizes) + if different_content_length_headers: + raise ValueError(f"Conflicting Content-Length headers: {sizes!r}") + try: + size = int(sizes[0]) + except ValueError: + raise ValueError(f"Invalid Content-Length header: {sizes[0]!r}") + if size < 0: + raise ValueError(f"Negative Content-Length header: {sizes[0]!r}") + return size + + # 6. If this is a request message and none of the above are true, then + # the message body length is zero (no message body is present). + if not response: + return 0 + + # 7. Otherwise, this is a response message without a declared message + # body length, so the message body length is determined by the + # number of octets received prior to the server closing the + # connection. + return -1 + + +def raise_if_http_version_unknown(http_version: bytes) -> None: + if not re.match(br"^HTTP/\d\.\d$", http_version): + raise ValueError(f"Unknown HTTP version: {http_version!r}") + + +def _read_request_line(line: bytes) -> Tuple[str, int, bytes, bytes, bytes, bytes, bytes]: + try: + method, target, http_version = line.split() + port: Optional[int] + + if target == b"*" or target.startswith(b"/"): + scheme, authority, path = b"", b"", target + host, port = "", 0 + elif method == b"CONNECT": + scheme, authority, path = b"", target, b"" + host, port = url.parse_authority(authority, check=True) + if not port: + raise ValueError + else: + scheme, rest = target.split(b"://", maxsplit=1) + authority, _, path_ = rest.partition(b"/") + path = b"/" + path_ + host, port = url.parse_authority(authority, check=True) + port = port or url.default_port(scheme) + if not port: + raise ValueError + # TODO: we can probably get rid of this check? + url.parse(target) + + raise_if_http_version_unknown(http_version) + except ValueError as e: + raise ValueError(f"Bad HTTP request line: {line!r}") from e + + return host, port, method, scheme, authority, path, http_version + + +def _read_response_line(line: bytes) -> Tuple[bytes, int, bytes]: + try: + parts = line.split(None, 2) + if len(parts) == 2: # handle missing message gracefully + parts.append(b"") + + http_version, status_code_str, reason = parts + status_code = int(status_code_str) + raise_if_http_version_unknown(http_version) + except ValueError as e: + raise ValueError(f"Bad HTTP response line: {line!r}") from e + + return http_version, status_code, reason + + +def _read_headers(lines: Iterable[bytes]) -> Headers: + """ + Read a set of headers. + Stop once a blank line is reached. + + Returns: + A headers object + + Raises: + exceptions.HttpSyntaxException + """ + ret: List[Tuple[bytes, bytes]] = [] + for line in lines: + if line[0] in b" \t": + if not ret: + raise ValueError("Invalid headers") + # continued header + ret[-1] = (ret[-1][0], ret[-1][1] + b'\r\n ' + line.strip()) + else: + try: + name, value = line.split(b":", 1) + value = value.strip() + if not name: + raise ValueError() + ret.append((name, value)) + except ValueError: + raise ValueError(f"Invalid header line: {line!r}") + return Headers(ret) + + +def read_request_head(lines: List[bytes]) -> Request: + """ + Parse an HTTP request head (request line + headers) from an iterable of lines + + Args: + lines: The input lines + + Returns: + The HTTP request object (without body) + + Raises: + ValueError: The input is malformed. + """ + host, port, method, scheme, authority, path, http_version = _read_request_line(lines[0]) + headers = _read_headers(lines[1:]) + + return Request( + host=host, + port=port, + method=method, + scheme=scheme, + authority=authority, + path=path, + http_version=http_version, + headers=headers, + content=None, + trailers=None, + timestamp_start=time.time(), + timestamp_end=None + ) + + +def read_response_head(lines: List[bytes]) -> Response: + """ + Parse an HTTP response head (response line + headers) from an iterable of lines + + Args: + lines: The input lines + + Returns: + The HTTP response object (without body) + + Raises: + ValueError: The input is malformed. + """ + http_version, status_code, reason = _read_response_line(lines[0]) + headers = _read_headers(lines[1:]) + + return Response( + http_version=http_version, + status_code=status_code, + reason=reason, + headers=headers, + content=None, + trailers=None, + timestamp_start=time.time(), + timestamp_end=None, + ) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/multipart.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/multipart.py new file mode 100644 index 00000000..c9cf6fd9 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/multipart.py @@ -0,0 +1,67 @@ +import mimetypes +import re +from typing import Tuple, List, Optional +from urllib.parse import quote + +from _internal_mitmproxy.net.http import headers + + +def encode(head, l): + k = head.get("content-type") + if k: + k = headers.parse_content_type(k) + if k is not None: + try: + boundary = k[2]["boundary"].encode("ascii") + boundary = quote(boundary) + except (KeyError, UnicodeError): + return b"" + hdrs = [] + for key, value in l: + file_type = mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8" + + if key: + hdrs.append(b"--%b" % boundary.encode('utf-8')) + disposition = b'form-data; name="%b"' % key + hdrs.append(b"Content-Disposition: %b" % disposition) + hdrs.append(b"Content-Type: %b" % file_type.encode('utf-8')) + hdrs.append(b'') + hdrs.append(value) + hdrs.append(b'') + + if value is not None: + # If boundary is found in value then raise ValueError + if re.search(rb"^--%b$" % re.escape(boundary.encode('utf-8')), value): + raise ValueError(b"boundary found in encoded string") + + hdrs.append(b"--%b--\r\n" % boundary.encode('utf-8')) + temp = b"\r\n".join(hdrs) + return temp + + +def decode(content_type: Optional[str], content: bytes) -> List[Tuple[bytes, bytes]]: + """ + Takes a multipart boundary encoded string and returns list of (key, value) tuples. + """ + if content_type: + ct = headers.parse_content_type(content_type) + if not ct: + return [] + try: + boundary = ct[2]["boundary"].encode("ascii") + except (KeyError, UnicodeError): + return [] + + rx = re.compile(br'\bname="([^"]+)"') + r = [] + if content is not None: + for i in content.split(b"--" + boundary): + parts = i.splitlines() + if len(parts) > 1 and parts[0][0:2] != b"--": + match = rx.search(parts[1]) + if match: + key = match.group(1) + value = b"".join(parts[3 + parts[2:].index(b""):]) + r.append((key, value)) + return r + return [] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/status_codes.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/status_codes.py new file mode 100644 index 00000000..aac38ba0 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/status_codes.py @@ -0,0 +1,109 @@ +CONTINUE = 100 +SWITCHING = 101 +OK = 200 +CREATED = 201 +ACCEPTED = 202 +NON_AUTHORITATIVE_INFORMATION = 203 +NO_CONTENT = 204 +RESET_CONTENT = 205 +PARTIAL_CONTENT = 206 +MULTI_STATUS = 207 + +MULTIPLE_CHOICE = 300 +MOVED_PERMANENTLY = 301 +FOUND = 302 +SEE_OTHER = 303 +NOT_MODIFIED = 304 +USE_PROXY = 305 +TEMPORARY_REDIRECT = 307 + +BAD_REQUEST = 400 +UNAUTHORIZED = 401 +PAYMENT_REQUIRED = 402 +FORBIDDEN = 403 +NOT_FOUND = 404 +NOT_ALLOWED = 405 +NOT_ACCEPTABLE = 406 +PROXY_AUTH_REQUIRED = 407 +REQUEST_TIMEOUT = 408 +CONFLICT = 409 +GONE = 410 +LENGTH_REQUIRED = 411 +PRECONDITION_FAILED = 412 +PAYLOAD_TOO_LARGE = 413 +REQUEST_URI_TOO_LONG = 414 +UNSUPPORTED_MEDIA_TYPE = 415 +REQUESTED_RANGE_NOT_SATISFIABLE = 416 +EXPECTATION_FAILED = 417 +IM_A_TEAPOT = 418 +NO_RESPONSE = 444 +CLIENT_CLOSED_REQUEST = 499 + +INTERNAL_SERVER_ERROR = 500 +NOT_IMPLEMENTED = 501 +BAD_GATEWAY = 502 +SERVICE_UNAVAILABLE = 503 +GATEWAY_TIMEOUT = 504 +HTTP_VERSION_NOT_SUPPORTED = 505 +INSUFFICIENT_STORAGE_SPACE = 507 +NOT_EXTENDED = 510 + +RESPONSES = { + # 100 + CONTINUE: "Continue", + SWITCHING: "Switching Protocols", + + # 200 + OK: "OK", + CREATED: "Created", + ACCEPTED: "Accepted", + NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information", + NO_CONTENT: "No Content", + RESET_CONTENT: "Reset Content.", + PARTIAL_CONTENT: "Partial Content", + MULTI_STATUS: "Multi-Status", + + # 300 + MULTIPLE_CHOICE: "Multiple Choices", + MOVED_PERMANENTLY: "Moved Permanently", + FOUND: "Found", + SEE_OTHER: "See Other", + NOT_MODIFIED: "Not Modified", + USE_PROXY: "Use Proxy", + # 306 not defined?? + TEMPORARY_REDIRECT: "Temporary Redirect", + + # 400 + BAD_REQUEST: "Bad Request", + UNAUTHORIZED: "Unauthorized", + PAYMENT_REQUIRED: "Payment Required", + FORBIDDEN: "Forbidden", + NOT_FOUND: "Not Found", + NOT_ALLOWED: "Method Not Allowed", + NOT_ACCEPTABLE: "Not Acceptable", + PROXY_AUTH_REQUIRED: "Proxy Authentication Required", + REQUEST_TIMEOUT: "Request Time-out", + CONFLICT: "Conflict", + GONE: "Gone", + LENGTH_REQUIRED: "Length Required", + PRECONDITION_FAILED: "Precondition Failed", + PAYLOAD_TOO_LARGE: "Payload Too Large", + REQUEST_URI_TOO_LONG: "Request-URI Too Long", + UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type", + REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", + EXPECTATION_FAILED: "Expectation Failed", + IM_A_TEAPOT: "I'm a teapot", + NO_RESPONSE: "No Response", + CLIENT_CLOSED_REQUEST: "Client Closed Request", + + + # 500 + INTERNAL_SERVER_ERROR: "Internal Server Error", + NOT_IMPLEMENTED: "Not Implemented", + BAD_GATEWAY: "Bad Gateway", + SERVICE_UNAVAILABLE: "Service Unavailable", + GATEWAY_TIMEOUT: "Gateway Time-out", + HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", + INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", + NOT_EXTENDED: "Not Extended" +} diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/url.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/url.py new file mode 100644 index 00000000..14ee6652 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/url.py @@ -0,0 +1,190 @@ +import re +import urllib.parse +from typing import AnyStr, Optional +from typing import Sequence +from typing import Tuple + +from _internal_mitmproxy.net import check +# This regex extracts & splits the host header into host and port. +# Handles the edge case of IPv6 addresses containing colons. +# https://bugzilla.mozilla.org/show_bug.cgi?id=45891 +from _internal_mitmproxy.net.check import is_valid_host, is_valid_port +from _internal_mitmproxy.utils.strutils import always_str + +_authority_re = re.compile(r"^(?P[^:]+|\[.+\])(?::(?P\d+))?$") + + +def parse(url): + """ + URL-parsing function that checks that + - port is an integer 0-65535 + - host is a valid IDNA-encoded hostname with no null-bytes + - path is valid ASCII + + Args: + A URL (as bytes or as unicode) + + Returns: + A (scheme, host, port, path) tuple + + Raises: + ValueError, if the URL is not properly formatted. + """ + # FIXME: We shouldn't rely on urllib here. + + # Size of Ascii character after encoding is 1 byte which is same as its size + # But non-Ascii character's size after encoding will be more than its size + def ascii_check(l): + if len(l) == len(str(l).encode()): + return True + return False + + if isinstance(url, bytes): + url = url.decode() + if not ascii_check(url): + url = urllib.parse.urlsplit(url) + url = list(url) + url[3] = urllib.parse.quote(url[3]) + url = urllib.parse.urlunsplit(url) + + parsed = urllib.parse.urlparse(url) + if not parsed.hostname: + raise ValueError("No hostname given") + + else: + host = parsed.hostname.encode("idna") + if isinstance(parsed, urllib.parse.ParseResult): + parsed = parsed.encode("ascii") + + port = parsed.port + if not port: + port = 443 if parsed.scheme == b"https" else 80 + + full_path = urllib.parse.urlunparse( + (b"", b"", parsed.path, parsed.params, parsed.query, parsed.fragment) + ) + if not full_path.startswith(b"/"): + full_path = b"/" + full_path + + if not check.is_valid_host(host): + raise ValueError("Invalid Host") + + return parsed.scheme, host, port, full_path + + +def unparse(scheme: str, host: str, port: int, path: str = "") -> str: + """ + Returns a URL string, constructed from the specified components. + + Args: + All args must be str. + """ + if path == "*": + path = "" + authority = hostport(scheme, host, port) + return f"{scheme}://{authority}{path}" + + +def encode(s: Sequence[Tuple[str, str]], similar_to: str = None) -> str: + """ + Takes a list of (key, value) tuples and returns a urlencoded string. + If similar_to is passed, the output is formatted similar to the provided urlencoded string. + """ + + remove_trailing_equal = False + if similar_to: + remove_trailing_equal = any("=" not in param for param in similar_to.split("&")) + + encoded = urllib.parse.urlencode(s, False, errors="surrogateescape") + + if encoded and remove_trailing_equal: + encoded = encoded.replace("=&", "&") + if encoded[-1] == '=': + encoded = encoded[:-1] + + return encoded + + +def decode(s): + """ + Takes a urlencoded string and returns a list of surrogate-escaped (key, value) tuples. + """ + return urllib.parse.parse_qsl(s, keep_blank_values=True, errors='surrogateescape') + + +def quote(b: str, safe: str = "/") -> str: + """ + Returns: + An ascii-encodable str. + """ + return urllib.parse.quote(b, safe=safe, errors="surrogateescape") + + +def unquote(s: str) -> str: + """ + Args: + s: A surrogate-escaped str + Returns: + A surrogate-escaped str + """ + return urllib.parse.unquote(s, errors="surrogateescape") + + +def hostport(scheme: AnyStr, host: AnyStr, port: int) -> AnyStr: + """ + Returns the host component, with a port specification if needed. + """ + if default_port(scheme) == port: + return host + else: + if isinstance(host, bytes): + return b"%s:%d" % (host, port) + else: + return "%s:%d" % (host, port) + + +def default_port(scheme: AnyStr) -> Optional[int]: + return { + "http": 80, + b"http": 80, + "https": 443, + b"https": 443, + }.get(scheme, None) + + +def parse_authority(authority: AnyStr, check: bool) -> Tuple[str, Optional[int]]: + """Extract the host and port from host header/authority information + + Raises: + ValueError, if check is True and the authority information is malformed. + """ + try: + if isinstance(authority, bytes): + m = _authority_re.match(authority.decode("utf-8")) + if not m: + raise ValueError + host = m["host"].encode("utf-8").decode("idna") + else: + m = _authority_re.match(authority) + if not m: + raise ValueError + host = m.group("host") + + if host.startswith("[") and host.endswith("]"): + host = host[1:-1] + if not is_valid_host(host): + raise ValueError + + if m.group("port"): + port = int(m.group("port")) + if not is_valid_port(port): + raise ValueError + return host, port + else: + return host, None + + except ValueError: + if check: + raise + else: + return always_str(authority, "utf-8", "surrogateescape"), None diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/user_agents.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/user_agents.py new file mode 100644 index 00000000..d0ca2f21 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/http/user_agents.py @@ -0,0 +1,50 @@ +""" + A small collection of useful user-agent header strings. These should be + kept reasonably current to reflect common usage. +""" + +# pylint: line-too-long + +# A collection of (name, shortcut, string) tuples. + +UASTRINGS = [ + ("android", + "a", + "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Nexus 7 Build/JRO03D) AFL/01.04.02"), # noqa + ("blackberry", + "l", + "Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+"), # noqa + ("bingbot", + "b", + "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"), # noqa + ("chrome", + "c", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"), # noqa + ("firefox", + "f", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1"), # noqa + ("googlebot", + "g", + "Googlebot/2.1 (+http://www.googlebot.com/bot.html)"), # noqa + ("ie9", + "i", + "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US)"), # noqa + ("ipad", + "p", + "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3"), # noqa + ("iphone", + "h", + "Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac OS X) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5"), # noqa + ("safari", + "s", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10"), # noqa +] + + +def get_by_shortcut(s): + """ + Retrieve a user agent entry by shortcut. + """ + for i in UASTRINGS: + if s == i[1]: + return i diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/server_spec.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/server_spec.py new file mode 100644 index 00000000..27cd0c95 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/server_spec.py @@ -0,0 +1,80 @@ +""" +Server specs are used to describe an upstream proxy or server. +""" +import functools +import re +from typing import Tuple, Literal, NamedTuple + +from _internal_mitmproxy.net import check + + +class ServerSpec(NamedTuple): + scheme: Literal["http", "https"] + address: Tuple[str, int] + + +server_spec_re = re.compile( + r""" + ^ + (?:(?P\w+)://)? # scheme is optional + (?P[^:/]+|\[.+\]) # hostname can be DNS name, IPv4, or IPv6 address. + (?::(?P\d+))? # port is optional + /? # we allow a trailing backslash, but no path + $ + """, + re.VERBOSE +) + + +@functools.lru_cache +def parse(server_spec: str) -> ServerSpec: + """ + Parses a server mode specification, e.g.: + + - http://example.com/ + - example.org + - example.com:443 + + *Raises:* + - ValueError, if the server specification is invalid. + """ + m = server_spec_re.match(server_spec) + if not m: + raise ValueError(f"Invalid server specification: {server_spec}") + + if m.group("scheme"): + scheme = m.group("scheme") + else: + scheme = "https" if m.group("port") in ("443", None) else "http" + if scheme not in ("http", "https"): + raise ValueError(f"Invalid server scheme: {scheme}") + + host = m.group("host") + # IPv6 brackets + if host.startswith("[") and host.endswith("]"): + host = host[1:-1] + if not check.is_valid_host(host): + raise ValueError(f"Invalid hostname: {host}") + + if m.group("port"): + port = int(m.group("port")) + else: + port = { + "http": 80, + "https": 443 + }[scheme] + if not check.is_valid_port(port): + raise ValueError(f"Invalid port: {port}") + + return ServerSpec(scheme, (host, port)) # type: ignore + + +def parse_with_mode(mode: str) -> Tuple[str, ServerSpec]: + """ + Parse a proxy mode specification, which is usually just `(reverse|upstream):server-spec`. + + *Raises:* + - ValueError, if the specification is invalid. + """ + mode, server_spec = mode.split(":", maxsplit=1) + return mode, parse(server_spec) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/tls.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/tls.py new file mode 100644 index 00000000..4faa7dfb --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/net/tls.py @@ -0,0 +1,271 @@ +import ipaddress +import os +import threading +from enum import Enum +from functools import lru_cache +from pathlib import Path +from typing import Iterable, Callable, Optional, Tuple, List, Any, BinaryIO + +import certifi + +from OpenSSL.crypto import X509 +from cryptography.hazmat.primitives.asymmetric import rsa + +from OpenSSL import SSL, crypto +from _internal_mitmproxy import certs + + +# redeclared here for strict type checking +class Method(Enum): + TLS_SERVER_METHOD = SSL.TLS_SERVER_METHOD + TLS_CLIENT_METHOD = SSL.TLS_CLIENT_METHOD + + +try: + SSL._lib.TLS_server_method # type: ignore +except AttributeError as e: # pragma: no cover + raise RuntimeError("Your installation of the cryptography Python package is outdated.") from e + + +class Version(Enum): + UNBOUNDED = 0 + SSL3 = SSL.SSL3_VERSION + TLS1 = SSL.TLS1_VERSION + TLS1_1 = SSL.TLS1_1_VERSION + TLS1_2 = SSL.TLS1_2_VERSION + TLS1_3 = SSL.TLS1_3_VERSION + + +class Verify(Enum): + VERIFY_NONE = SSL.VERIFY_NONE + VERIFY_PEER = SSL.VERIFY_PEER + + +DEFAULT_MIN_VERSION = Version.TLS1_2 +DEFAULT_MAX_VERSION = Version.UNBOUNDED +DEFAULT_OPTIONS = ( + SSL.OP_CIPHER_SERVER_PREFERENCE + | SSL.OP_NO_COMPRESSION +) + + +class MasterSecretLogger: + def __init__(self, filename: Path): + self.filename = filename.expanduser() + self.f: Optional[BinaryIO] = None + self.lock = threading.Lock() + + # required for functools.wraps, which pyOpenSSL uses. + __name__ = "MasterSecretLogger" + + def __call__(self, connection: SSL.Connection, keymaterial: bytes) -> None: + with self.lock: + if self.f is None: + self.filename.parent.mkdir(parents=True, exist_ok=True) + self.f = self.filename.open("ab") + self.f.write(b"\n") + self.f.write(keymaterial + b"\n") + self.f.flush() + + def close(self): + with self.lock: + if self.f is not None: + self.f.close() + + +def make_master_secret_logger(filename: Optional[str]) -> Optional[MasterSecretLogger]: + if filename: + return MasterSecretLogger(Path(filename)) + return None + + +log_master_secret = make_master_secret_logger( + os.getenv("_internal_mitmproxy_SSLKEYLOGFILE") or os.getenv("SSLKEYLOGFILE") +) + + +def _create_ssl_context( + *, + method: Method, + min_version: Version, + max_version: Version, + cipher_list: Optional[Iterable[str]], +) -> SSL.Context: + context = SSL.Context(method.value) + + ok = SSL._lib.SSL_CTX_set_min_proto_version(context._context, min_version.value) # type: ignore + ok += SSL._lib.SSL_CTX_set_max_proto_version(context._context, max_version.value) # type: ignore + if ok != 2: + raise RuntimeError( + f"Error setting TLS versions ({min_version=}, {max_version=}). " + "The version you specified may be unavailable in your libssl." + ) + + # Options + context.set_options(DEFAULT_OPTIONS) + + # Cipher List + if cipher_list is not None: + try: + context.set_cipher_list(b":".join(x.encode() for x in cipher_list)) + except SSL.Error as e: + raise RuntimeError("SSL cipher specification error: {e}") from e + + # SSLKEYLOGFILE + if log_master_secret: + context.set_keylog_callback(log_master_secret) + + return context + + +@lru_cache(256) +def create_proxy_server_context( + *, + min_version: Version, + max_version: Version, + cipher_list: Optional[Tuple[str, ...]], + verify: Verify, + hostname: Optional[str], + ca_path: Optional[str], + ca_pemfile: Optional[str], + client_cert: Optional[str], + alpn_protos: Optional[Tuple[bytes, ...]], +) -> SSL.Context: + context: SSL.Context = _create_ssl_context( + method=Method.TLS_CLIENT_METHOD, + min_version=min_version, + max_version=max_version, + cipher_list=cipher_list, + ) + + if verify is not Verify.VERIFY_NONE and hostname is None: + raise ValueError("Cannot validate certificate hostname without SNI") + + context.set_verify(verify.value, None) + if hostname is not None: + assert isinstance(hostname, str) + # Manually enable hostname verification on the context object. + # https://wiki.openssl.org/index.php/Hostname_validation + param = SSL._lib.SSL_CTX_get0_param(context._context) # type: ignore + # Matching on the CN is disabled in both Chrome and Firefox, so we disable it, too. + # https://www.chromestatus.com/feature/4981025180483584 + SSL._lib.X509_VERIFY_PARAM_set_hostflags( # type: ignore + param, + SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS | SSL._lib.X509_CHECK_FLAG_NEVER_CHECK_SUBJECT # type: ignore + ) + try: + ip: bytes = ipaddress.ip_address(hostname).packed + except ValueError: + SSL._openssl_assert( # type: ignore + SSL._lib.X509_VERIFY_PARAM_set1_host(param, hostname.encode(), len(hostname.encode())) == 1 # type: ignore + ) + else: + SSL._openssl_assert( # type: ignore + SSL._lib.X509_VERIFY_PARAM_set1_ip(param, ip, len(ip)) == 1 # type: ignore + ) + + if ca_path is None and ca_pemfile is None: + ca_pemfile = certifi.where() + try: + context.load_verify_locations(ca_pemfile, ca_path) + except SSL.Error as e: + raise RuntimeError(f"Cannot load trusted certificates ({ca_pemfile=}, {ca_path=}).") from e + + # Client Certs + if client_cert: + try: + context.use_privatekey_file(client_cert) + context.use_certificate_chain_file(client_cert) + except SSL.Error as e: + raise RuntimeError(f"Cannot load TLS client certificate: {e}") from e + + if alpn_protos: + # advertise application layer protocols + context.set_alpn_protos(alpn_protos) + + return context + + +@lru_cache(256) +def create_client_proxy_context( + *, + min_version: Version, + max_version: Version, + cipher_list: Optional[Tuple[str, ...]], + cert: certs.Cert, + key: rsa.RSAPrivateKey, + chain_file: Optional[Path], + alpn_select_callback: Optional[Callable[[SSL.Connection, List[bytes]], Any]], + request_client_cert: bool, + extra_chain_certs: Tuple[certs.Cert, ...], + dhparams: certs.DHParams, +) -> SSL.Context: + context: SSL.Context = _create_ssl_context( + method=Method.TLS_SERVER_METHOD, + min_version=min_version, + max_version=max_version, + cipher_list=cipher_list, + ) + + context.use_certificate(cert.to_pyopenssl()) + context.use_privatekey(crypto.PKey.from_cryptography_key(key)) + if chain_file is not None: + try: + context.load_verify_locations(str(chain_file), None) + except SSL.Error as e: + raise RuntimeError(f"Cannot load certificate chain ({chain_file}).") from e + + if alpn_select_callback is not None: + assert callable(alpn_select_callback) + context.set_alpn_select_callback(alpn_select_callback) + + if request_client_cert: + # The request_client_cert argument requires some explanation. We're + # supposed to be able to do this with no negative effects - if the + # client has no cert to present, we're notified and proceed as usual. + # Unfortunately, Android seems to have a bug (tested on 4.2.2) - when + # an Android client is asked to present a certificate it does not + # have, it hangs up, which is frankly bogus. Some time down the track + # we may be able to make the proper behaviour the default again, but + # until then we're conservative. + context.set_verify(Verify.VERIFY_PEER.value, accept_all) + else: + context.set_verify(Verify.VERIFY_NONE.value, None) + + for i in extra_chain_certs: + context.add_extra_chain_cert(i.to_pyopenssl()) + + if dhparams: + SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) # type: ignore + + return context + + +def accept_all( + conn_: SSL.Connection, + x509: X509, + errno: int, + err_depth: int, + is_cert_verified: int, +) -> bool: + # Return true to prevent cert verification error + return True + + +def is_tls_record_magic(d): + """ + Returns: + True, if the passed bytes start with the TLS record magic bytes. + False, otherwise. + """ + d = d[:3] + + # TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2, and TLSv1.3 + # http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello + # https://tls13.ulfheim.net/ + return ( + len(d) == 3 and + d[0] == 0x16 and + d[1] == 0x03 and + 0x0 <= d[2] <= 0x03 + ) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/options.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/options.py new file mode 100644 index 00000000..49016272 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/options.py @@ -0,0 +1,156 @@ +from typing import Optional, Sequence + +from _internal_mitmproxy import optmanager + +CONF_DIR = "~/._internal_mitmproxy" +CONF_BASENAME = "_internal_mitmproxy" +LISTEN_PORT = 8080 +CONTENT_VIEW_LINES_CUTOFF = 512 +KEY_SIZE = 2048 + + +class Options(optmanager.OptManager): + + def __init__(self, **kwargs) -> None: + super().__init__() + self.add_option( + "server", bool, True, + "Start a proxy server. Enabled by default." + ) + self.add_option( + "showhost", bool, False, + "Use the Host header to construct URLs for display." + ) + + # Proxy options + self.add_option( + "add_upstream_certs_to_client_chain", bool, False, + """ + Add all certificates of the upstream server to the certificate chain + that will be served to the proxy client, as extras. + """ + ) + self.add_option( + "confdir", str, CONF_DIR, + "Location of the default _internal_mitmproxy configuration files." + ) + self.add_option( + "certs", Sequence[str], [], + """ + SSL certificates of the form "[domain=]path". The domain may include + a wildcard, and is equal to "*" if not specified. The file at path + is a certificate in PEM format. If a private key is included in the + PEM, it is used, else the default key in the conf dir is used. The + PEM file should contain the full certificate chain, with the leaf + certificate as the first entry. + """ + ) + self.add_option( + "cert_passphrase", Optional[str], None, + """ + Passphrase for decrypting the private key provided in the --cert option. + + Note that passing cert_passphrase on the command line makes your passphrase visible in your system's + process list. Specify it in config.yaml to avoid this. + """ + ) + self.add_option( + "ciphers_client", Optional[str], None, + "Set supported ciphers for client <-> _internal_mitmproxy connections using OpenSSL syntax." + ) + self.add_option( + "ciphers_server", Optional[str], None, + "Set supported ciphers for _internal_mitmproxy <-> server connections using OpenSSL syntax." + ) + self.add_option( + "client_certs", Optional[str], None, + "Client certificate file or directory." + ) + self.add_option( + "ignore_hosts", Sequence[str], [], + """ + Ignore host and forward all traffic without processing it. In + transparent mode, it is recommended to use an IP address (range), + not the hostname. In regular mode, only SSL traffic is ignored and + the hostname should be used. The supplied value is interpreted as a + regular expression and matched on the ip or the hostname. + """ + ) + self.add_option( + "allow_hosts", Sequence[str], [], + "Opposite of --ignore-hosts." + ) + self.add_option( + "listen_host", str, "", + "Address to bind proxy to." + ) + self.add_option( + "listen_port", int, LISTEN_PORT, + "Proxy service port." + ) + self.add_option( + "mode", str, "regular", + """ + Mode can be "regular", "transparent", "socks5", "reverse:SPEC", + or "upstream:SPEC". For reverse and upstream proxy modes, SPEC + is host specification in the form of "http[s]://host[:port]". + """ + ) + self.add_option( + "upstream_cert", bool, True, + "Connect to upstream server to look up certificate details." + ) + + self.add_option( + "http2", bool, True, + "Enable/disable HTTP/2 support. " + "HTTP/2 support is enabled by default.", + ) + self.add_option( + "websocket", bool, True, + "Enable/disable WebSocket support. " + "WebSocket support is enabled by default.", + ) + self.add_option( + "rawtcp", bool, True, + "Enable/disable raw TCP connections. " + "TCP connections are enabled by default. " + ) + self.add_option( + "ssl_insecure", bool, False, + "Do not verify upstream server SSL/TLS certificates." + ) + self.add_option( + "ssl_verify_upstream_trusted_confdir", Optional[str], None, + """ + Path to a directory of trusted CA certificates for upstream server + verification prepared using the c_rehash tool. + """ + ) + self.add_option( + "ssl_verify_upstream_trusted_ca", Optional[str], None, + "Path to a PEM formatted trusted CA certificate." + ) + self.add_option( + "tcp_hosts", Sequence[str], [], + """ + Generic TCP SSL proxy mode for all hosts that match the pattern. + Similar to --ignore-hosts, but SSL connections are intercepted. + The communication contents are printed to the log in verbose mode. + """ + ) + self.add_option( + "content_view_lines_cutoff", int, CONTENT_VIEW_LINES_CUTOFF, + """ + Flow content view lines limit. Limit is enabled by default to + speedup flows browsing. + """ + ) + self.add_option( + "key_size", int, KEY_SIZE, + """ + TLS key size for certificates and CA. + """ + ) + + self.update(**kwargs) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/optmanager.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/optmanager.py new file mode 100644 index 00000000..e43dc417 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/optmanager.py @@ -0,0 +1,600 @@ +import contextlib +import copy +from dataclasses import dataclass +import functools +import os +import pprint +import textwrap +import typing + +# +# ._saferef +import ruamel.yaml + +from _internal_mitmproxy import exceptions +from _internal_mitmproxy.utils import typecheck + +""" + The base implementation for Options. +""" + +unset = object() + + +class _Option: + __slots__ = ("name", "typespec", "value", "_default", "choices", "help") + + def __init__( + self, + name: str, + typespec: typing.Union[ + type, object + ], # object for Optional[x], which is not a type. + default: typing.Any, + help: str, + choices: typing.Optional[typing.Sequence[str]], + ) -> None: + typecheck.check_option_type(name, default, typespec) + self.name = name + self.typespec = typespec + self._default = default + self.value = unset + self.help = textwrap.dedent(help).strip().replace("\n", " ") + self.choices = choices + + def __repr__(self): + return f"{self.current()} [{self.typespec}]" + + @property + def default(self): + return copy.deepcopy(self._default) + + def current(self) -> typing.Any: + if self.value is unset: + v = self.default + else: + v = self.value + return copy.deepcopy(v) + + def set(self, value: typing.Any) -> None: + typecheck.check_option_type(self.name, value, self.typespec) + self.value = value + + def reset(self) -> None: + self.value = unset + + def has_changed(self) -> bool: + return self.current() != self.default + + def __eq__(self, other) -> bool: + for i in self.__slots__: + if getattr(self, i) != getattr(other, i): + return False + return True + + def __deepcopy__(self, _): + o = _Option(self.name, self.typespec, self.default, self.help, self.choices) + if self.has_changed(): + o.value = self.current() + return o + + +@dataclass +class _UnconvertedStrings: + val: typing.List[str] + + +class OptManager: + """ + OptManager is the base class from which Options objects are derived. + + .changed is a blinker Signal that triggers whenever options are + updated. If any handler in the chain raises an exceptions.OptionsError + exception, all changes are rolled back, the exception is suppressed, + and the .errored signal is notified. + + Optmanager always returns a deep copy of options to ensure that + mutation doesn't change the option state inadvertently. + """ + + def __init__(self): + self.deferred: typing.Dict[str, typing.Any] = {} + self.changed = blinker.Signal() + self.errored = blinker.Signal() + # Options must be the last attribute here - after that, we raise an + # error for attribute assignment to unknown options. + self._options: typing.Dict[str, typing.Any] = {} + + def add_option( + self, + name: str, + typespec: typing.Union[type, object], + default: typing.Any, + help: str, + choices: typing.Optional[typing.Sequence[str]] = None, + ) -> None: + self._options[name] = _Option(name, typespec, default, help, choices) + self.changed.send(self, updated={name}) + + @contextlib.contextmanager + def rollback(self, updated, reraise=False): + old = copy.deepcopy(self._options) + try: + yield + except exceptions.OptionsError as e: + # Notify error handlers + self.errored.send(self, exc=e) + # Rollback + self.__dict__["_options"] = old + self.changed.send(self, updated=updated) + if reraise: + raise e + + def subscribe(self, func, opts): + """ + Subscribe a callable to the .changed signal, but only for a + specified list of options. The callable should accept arguments + (options, updated), and may raise an OptionsError. + + The event will automatically be unsubscribed if the callable goes out of scope. + """ + for i in opts: + if i not in self._options: + raise exceptions.OptionsError("No such option: %s" % i) + + # We reuse blinker's safe reference functionality to cope with weakrefs + # to bound methods. + func = blinker._saferef.safe_ref(func) + + @functools.wraps(func) + def _call(options, updated): + if updated.intersection(set(opts)): + f = func() + if f: + f(options, updated) + else: + self.changed.disconnect(_call) + + # Our wrapper function goes out of scope immediately, so we have to set + # weakrefs to false. This means we need to keep our own weakref, and + # clean up the hook when it's gone. + self.changed.connect(_call, weak=False) + + def __eq__(self, other): + if isinstance(other, OptManager): + return self._options == other._options + return False + + def __deepcopy__(self, memodict=None): + o = OptManager() + o.__dict__["_options"] = copy.deepcopy(self._options, memodict) + return o + + __copy__ = __deepcopy__ + + def __getattr__(self, attr): + if attr in self._options: + return self._options[attr].current() + else: + raise AttributeError("No such option: %s" % attr) + + def __setattr__(self, attr, value): + # This is slightly tricky. We allow attributes to be set on the instance + # until we have an _options attribute. After that, assignment is sent to + # the update function, and will raise an error for unknown options. + opts = self.__dict__.get("_options") + if not opts: + super().__setattr__(attr, value) + else: + self.update(**{attr: value}) + + def keys(self): + return set(self._options.keys()) + + def items(self): + return self._options.items() + + def __contains__(self, k): + return k in self._options + + def reset(self): + """ + Restore defaults for all options. + """ + for o in self._options.values(): + o.reset() + self.changed.send(self, updated=set(self._options.keys())) + + def update_known(self, **kwargs): + """ + Update and set all known options from kwargs. Returns a dictionary + of unknown options. + """ + known, unknown = {}, {} + for k, v in kwargs.items(): + if k in self._options: + known[k] = v + else: + unknown[k] = v + updated = set(known.keys()) + if updated: + with self.rollback(updated, reraise=True): + for k, v in known.items(): + self._options[k].set(v) + self.changed.send(self, updated=updated) + return unknown + + def update_defer(self, **kwargs): + unknown = self.update_known(**kwargs) + self.deferred.update(unknown) + + def update(self, **kwargs): + u = self.update_known(**kwargs) + if u: + raise KeyError("Unknown options: %s" % ", ".join(u.keys())) + + def setter(self, attr): + """ + Generate a setter for a given attribute. This returns a callable + taking a single argument. + """ + if attr not in self._options: + raise KeyError("No such option: %s" % attr) + + def setter(x): + setattr(self, attr, x) + + return setter + + def toggler(self, attr): + """ + Generate a toggler for a boolean attribute. This returns a callable + that takes no arguments. + """ + if attr not in self._options: + raise KeyError("No such option: %s" % attr) + o = self._options[attr] + if o.typespec != bool: + raise ValueError("Toggler can only be used with boolean options") + + def toggle(): + setattr(self, attr, not getattr(self, attr)) + + return toggle + + def default(self, option: str) -> typing.Any: + return self._options[option].default + + def has_changed(self, option): + """ + Has the option changed from the default? + """ + return self._options[option].has_changed() + + def merge(self, opts): + """ + Merge a dict of options into this object. Options that have None + value are ignored. Lists and tuples are appended to the current + option value. + """ + toset = {} + for k, v in opts.items(): + if v is not None: + if isinstance(v, (list, tuple)): + toset[k] = getattr(self, k) + v + else: + toset[k] = v + self.update(**toset) + + def __repr__(self): + options = pprint.pformat(self._options, indent=4).strip(" {}") + if "\n" in options: + options = "\n " + options + "\n" + return "{mod}.{cls}({{{options}}})".format( + mod=type(self).__module__, cls=type(self).__name__, options=options + ) + + def set(self, *specs: str, defer: bool = False) -> None: + """ + Takes a list of set specification in standard form (option=value). + Options that are known are updated immediately. If defer is true, + options that are not known are deferred, and will be set once they + are added. + + May raise an `OptionsError` if a value is malformed or an option is unknown and defer is False. + """ + # First, group specs by option name. + unprocessed: typing.Dict[str, typing.List[str]] = {} + for spec in specs: + if "=" in spec: + name, value = spec.split("=", maxsplit=1) + unprocessed.setdefault(name, []).append(value) + else: + unprocessed.setdefault(spec, []) + + # Second, convert values to the correct type. + processed: typing.Dict[str, typing.Any] = {} + for name in list(unprocessed.keys()): + if name in self._options: + processed[name] = self._parse_setval( + self._options[name], unprocessed.pop(name) + ) + + # Third, stash away unrecognized options or complain about them. + if defer: + self.deferred.update( + {k: _UnconvertedStrings(v) for k, v in unprocessed.items()} + ) + elif unprocessed: + raise exceptions.OptionsError( + f"Unknown option(s): {', '.join(unprocessed)}" + ) + + # Finally, apply updated options. + self.update(**processed) + + def process_deferred(self) -> None: + """ + Processes options that were deferred in previous calls to set, and + have since been added. + """ + update: typing.Dict[str, typing.Any] = {} + for optname, value in self.deferred.items(): + if optname in self._options: + if isinstance(value, _UnconvertedStrings): + value = self._parse_setval(self._options[optname], value.val) + update[optname] = value + self.update(**update) + for k in update.keys(): + del self.deferred[k] + + def _parse_setval(self, o: _Option, values: typing.List[str]) -> typing.Any: + """ + Convert a string to a value appropriate for the option type. + """ + if o.typespec == typing.Sequence[str]: + return values + if len(values) > 1: + raise exceptions.OptionsError( + f"Received multiple values for {o.name}: {values}" + ) + + optstr: typing.Optional[str] + if values: + optstr = values[0] + else: + optstr = None + + if o.typespec in (str, typing.Optional[str]): + if o.typespec == str and optstr is None: + raise exceptions.OptionsError(f"Option is required: {o.name}") + return optstr + elif o.typespec in (int, typing.Optional[int]): + if optstr: + try: + return int(optstr) + except ValueError: + raise exceptions.OptionsError(f"Not an integer: {optstr}") + elif o.typespec == int: + raise exceptions.OptionsError(f"Option is required: {o.name}") + else: + return None + elif o.typespec == bool: + if optstr == "toggle": + return not o.current() + if not optstr or optstr == "true": + return True + elif optstr == "false": + return False + else: + raise exceptions.OptionsError( + 'Boolean must be "true", "false", or have the value omitted (a synonym for "true").' + ) + raise NotImplementedError(f"Unsupported option type: {o.typespec}") + + def make_parser(self, parser, optname, metavar=None, short=None): + """ + Auto-Create a command-line parser entry for a named option. If the + option does not exist, it is ignored. + """ + if optname not in self._options: + return + + o = self._options[optname] + + def mkf(l, s): + l = l.replace("_", "-") + f = ["--%s" % l] + if s: + f.append("-" + s) + return f + + flags = mkf(optname, short) + + if o.typespec == bool: + g = parser.add_mutually_exclusive_group(required=False) + onf = mkf(optname, None) + offf = mkf("no-" + optname, None) + # The short option for a bool goes to whatever is NOT the default + if short: + if o.default: + offf = mkf("no-" + optname, short) + else: + onf = mkf(optname, short) + g.add_argument( + *offf, + action="store_false", + dest=optname, + ) + g.add_argument(*onf, action="store_true", dest=optname, help=o.help) + parser.set_defaults(**{optname: None}) + elif o.typespec in (int, typing.Optional[int]): + parser.add_argument( + *flags, + action="store", + type=int, + dest=optname, + help=o.help, + metavar=metavar, + ) + elif o.typespec in (str, typing.Optional[str]): + parser.add_argument( + *flags, + action="store", + type=str, + dest=optname, + help=o.help, + metavar=metavar, + choices=o.choices, + ) + elif o.typespec == typing.Sequence[str]: + parser.add_argument( + *flags, + action="append", + type=str, + dest=optname, + help=o.help + " May be passed multiple times.", + metavar=metavar, + choices=o.choices, + ) + else: + raise ValueError("Unsupported option type: %s", o.typespec) + + +def dump_defaults(opts, out: typing.TextIO): + """ + Dumps an annotated file with all options. + """ + # Sort data + s = ruamel.yaml.comments.CommentedMap() + for k in sorted(opts.keys()): + o = opts._options[k] + s[k] = o.default + txt = o.help.strip() + + if o.choices: + txt += " Valid values are %s." % ", ".join(repr(c) for c in o.choices) + else: + t = typecheck.typespec_to_str(o.typespec) + txt += " Type %s." % t + + txt = "\n".join(textwrap.wrap(txt)) + s.yaml_set_comment_before_after_key(k, before="\n" + txt) + return ruamel.yaml.YAML().dump(s, out) + + +def dump_dicts(opts, keys: typing.List[str] = None): + """ + Dumps the options into a list of dict object. + + Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} } + """ + options_dict = {} + keys = keys if keys else opts.keys() + for k in sorted(keys): + o = opts._options[k] + t = typecheck.typespec_to_str(o.typespec) + option = { + "type": t, + "default": o.default, + "value": o.current(), + "help": o.help, + "choices": o.choices, + } + options_dict[k] = option + return options_dict + + +def parse(text): + if not text: + return {} + try: + yaml = ruamel.yaml.YAML(typ="unsafe", pure=True) + data = yaml.load(text) + except ruamel.yaml.error.YAMLError as v: + if hasattr(v, "problem_mark"): + snip = v.problem_mark.get_snippet() + raise exceptions.OptionsError( + "Config error at line %s:\n%s\n%s" + % (v.problem_mark.line + 1, snip, v.problem) + ) + else: + raise exceptions.OptionsError("Could not parse options.") + if isinstance(data, str): + raise exceptions.OptionsError("Config error - no keys found.") + elif data is None: + return {} + return data + + +def load(opts: OptManager, text: str) -> None: + """ + Load configuration from text, over-writing options already set in + this object. May raise OptionsError if the config file is invalid. + """ + data = parse(text) + opts.update_defer(**data) + + +def load_paths(opts: OptManager, *paths: str) -> None: + """ + Load paths in order. Each path takes precedence over the previous + path. Paths that don't exist are ignored, errors raise an + OptionsError. + """ + for p in paths: + p = os.path.expanduser(p) + if os.path.exists(p) and os.path.isfile(p): + with open(p, encoding="utf8") as f: + try: + txt = f.read() + except UnicodeDecodeError as e: + raise exceptions.OptionsError(f"Error reading {p}: {e}") + try: + load(opts, txt) + except exceptions.OptionsError as e: + raise exceptions.OptionsError(f"Error reading {p}: {e}") + + +def serialize( + opts: OptManager, file: typing.TextIO, text: str, defaults: bool = False +) -> None: + """ + Performs a round-trip serialization. If text is not None, it is + treated as a previous serialization that should be modified + in-place. + + - If "defaults" is False, only options with non-default values are + serialized. Default values in text are preserved. + - Unknown options in text are removed. + - Raises OptionsError if text is invalid. + """ + data = parse(text) + for k in opts.keys(): + if defaults or opts.has_changed(k): + data[k] = getattr(opts, k) + for k in list(data.keys()): + if k not in opts._options: + del data[k] + + ruamel.yaml.YAML().dump(data, file) + + +def save(opts: OptManager, path: str, defaults: bool = False) -> None: + """ + Save to path. If the destination file exists, modify it in-place. + + Raises OptionsError if the existing data is corrupt. + """ + path = os.path.expanduser(path) + if os.path.exists(path) and os.path.isfile(path): + with open(path, encoding="utf8") as f: + try: + data = f.read() + except UnicodeDecodeError as e: + raise exceptions.OptionsError(f"Error trying to modify {path}: {e}") + else: + data = "" + + with open(path, "wt", encoding="utf8") as f: + serialize(opts, f, data, defaults) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/__init__.py new file mode 100644 index 00000000..7e690789 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/__init__.py @@ -0,0 +1,43 @@ +import re +import socket +import sys +from typing import Callable, Optional, Tuple + + +def init_transparent_mode() -> None: + """ + Initialize transparent mode. + """ + + +original_addr: Optional[Callable[[socket.socket], Tuple[str, int]]] +""" +Get the original destination for the given socket. +This function will be None if transparent mode is not supported. +""" + +if re.match(r"linux(?:2)?", sys.platform): + from . import linux + + original_addr = linux.original_addr +elif sys.platform == "darwin" or sys.platform.startswith("freebsd"): + from . import osx + + original_addr = osx.original_addr +elif sys.platform.startswith("openbsd"): + from . import openbsd + + original_addr = openbsd.original_addr +elif sys.platform == "win32": + from . import windows + + resolver = windows.Resolver() + init_transparent_mode = resolver.setup # noqa + original_addr = resolver.original_addr +else: + original_addr = None + +__all__ = [ + "original_addr", + "init_transparent_mode" +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/linux.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/linux.py new file mode 100644 index 00000000..f446bb72 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/linux.py @@ -0,0 +1,34 @@ +import socket +import struct +import typing + +# Python's socket module does not have these constants +SO_ORIGINAL_DST = 80 +SOL_IPV6 = 41 + + +def original_addr(csock: socket.socket) -> typing.Tuple[str, int]: + # Get the original destination on Linux. + # In theory, this can be done using the following syscalls: + # sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) + # sock.getsockopt(SOL_IPV6, SO_ORIGINAL_DST, 28) + # + # In practice, it is a bit more complex: + # 1. We cannot rely on sock.family to decide which syscall to use because of IPv4-mapped + # IPv6 addresses. If sock.family is AF_INET6 while sock.getsockname() is ::ffff:127.0.0.1, + # we need to call the IPv4 version to get a result. + # 2. We can't just try the IPv4 syscall and then do IPv6 if that doesn't work, + # because doing the wrong syscall can apparently crash the whole Python runtime. + # As such, we use a heuristic to check which syscall to do. + is_ipv4 = "." in csock.getsockname()[0] # either 127.0.0.1 or ::ffff:127.0.0.1 + if is_ipv4: + # the struct returned here should only have 8 bytes, but invoking sock.getsockopt + # with buflen=8 doesn't work. + dst = csock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) + port, raw_ip = struct.unpack_from("!2xH4s", dst) + ip = socket.inet_ntop(socket.AF_INET, raw_ip) + else: + dst = csock.getsockopt(SOL_IPV6, SO_ORIGINAL_DST, 28) + port, raw_ip = struct.unpack_from("!2xH4x16s", dst) + ip = socket.inet_ntop(socket.AF_INET6, raw_ip) + return ip, port diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/openbsd.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/openbsd.py new file mode 100644 index 00000000..e8f5ff8e --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/openbsd.py @@ -0,0 +1,2 @@ +def original_addr(csock): + return csock.getsockname() diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/osx.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/osx.py new file mode 100644 index 00000000..98fed866 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/osx.py @@ -0,0 +1,36 @@ +import subprocess + +from . import pf + +""" + Doing this the "right" way by using DIOCNATLOOK on the pf device turns out + to be a pain. Apple has made a number of modifications to the data + structures returned, and compiling userspace tools to test and work with + this turns out to be a pain in the ass. Parsing pfctl output is short, + simple, and works. + + Note: Also Tested with FreeBSD 10 pkgng Python 2.7.x. + Should work almost exactly as on Mac OS X and except with some changes to + the output processing of pfctl (see pf.py). +""" + +STATECMD = ("sudo", "-n", "/sbin/pfctl", "-s", "state") + + +def original_addr(csock): + peer = csock.getpeername() + try: + stxt = subprocess.check_output(STATECMD, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + if "sudo: a password is required" in e.output.decode(errors="replace"): + insufficient_priv = True + else: + raise RuntimeError("Error getting pfctl state: " + repr(e)) + else: + insufficient_priv = "sudo: a password is required" in stxt.decode(errors="replace") + + if insufficient_priv: + raise RuntimeError( + "Insufficient privileges to access pfctl. " + "See https://_internal_mitmproxy.org/docs/latest/howto-transparent/#macos for details.") + return pf.lookup(peer[0], peer[1], stxt) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/pf.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/pf.py new file mode 100644 index 00000000..e2742024 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/pf.py @@ -0,0 +1,42 @@ +import re +import sys + + +def lookup(address, port, s): + """ + Parse the pfctl state output s, to look up the destination host + matching the client (address, port). + + Returns an (address, port) tuple, or None. + """ + # We may get an ipv4-mapped ipv6 address here, e.g. ::ffff:127.0.0.1. + # Those still appear as "127.0.0.1" in the table, so we need to strip the prefix. + address = re.sub(r"^::ffff:(?=\d+.\d+.\d+.\d+$)", "", address) + s = s.decode() + + # ALL tcp 192.168.1.13:57474 -> 23.205.82.58:443 ESTABLISHED:ESTABLISHED + specv4 = f"{address}:{port}" + + # ALL tcp 2a01:e35:8bae:50f0:9d9b:ef0d:2de3:b733[58505] -> 2606:4700:30::681f:4ad0[443] ESTABLISHED:ESTABLISHED + specv6 = f"{address}[{port}]" + + for i in s.split("\n"): + if "ESTABLISHED:ESTABLISHED" in i and specv4 in i: + s = i.split() + if len(s) > 4: + if sys.platform.startswith("freebsd"): + # strip parentheses for FreeBSD pfctl + s = s[3][1:-1].split(":") + else: + s = s[4].split(":") + + if len(s) == 2: + return s[0], int(s[1]) + elif "ESTABLISHED:ESTABLISHED" in i and specv6 in i: + s = i.split() + if len(s) > 4: + s = s[4].split("[") + port = s[1].split("]") + port = port[0] + return s[0], int(port) + raise RuntimeError("Could not resolve original destination.") diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/windows.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/windows.py new file mode 100644 index 00000000..8ccafa10 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/platform/windows.py @@ -0,0 +1,596 @@ +import collections +import collections.abc +import contextlib +import ctypes +import ctypes.wintypes +import io +import json +import os +import re +import socket +import socketserver +import threading +import time +import typing + +import pydivert +import pydivert.consts + +REDIRECT_API_HOST = "127.0.0.1" +REDIRECT_API_PORT = 8085 + + +########################## +# Resolver + +def read(rfile: io.BufferedReader) -> typing.Any: + x = rfile.readline().strip() + if not x: + return None + return json.loads(x) + + +def write(data, wfile: io.BufferedWriter) -> None: + wfile.write(json.dumps(data).encode() + b"\n") + wfile.flush() + + +class Resolver: + sock: socket.socket + lock: threading.RLock + + def __init__(self): + self.sock = None + self.lock = threading.RLock() + + def setup(self): + with self.lock: + TransparentProxy.setup() + self._connect() + + def _connect(self): + if self.sock: + self.sock.close() + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((REDIRECT_API_HOST, REDIRECT_API_PORT)) + + self.wfile = self.sock.makefile('wb') + self.rfile = self.sock.makefile('rb') + write(os.getpid(), self.wfile) + + def original_addr(self, csock: socket.socket): + ip, port = csock.getpeername()[:2] + ip = re.sub(r"^::ffff:(?=\d+.\d+.\d+.\d+$)", "", ip) + ip = ip.split("%", 1)[0] + with self.lock: + try: + write((ip, port), self.wfile) + addr = read(self.rfile) + if addr is None: + raise RuntimeError("Cannot resolve original destination.") + return tuple(addr) + except (EOFError, OSError, AttributeError): + self._connect() + return self.original_addr(csock) + + +class APIRequestHandler(socketserver.StreamRequestHandler): + """ + TransparentProxy API: Returns the pickled server address, port tuple + for each received pickled client address, port tuple. + """ + + def handle(self): + proxifier: TransparentProxy = self.server.proxifier + try: + pid: int = read(self.rfile) + if pid is None: + return + with proxifier.exempt(pid): + while True: + c = read(self.rfile) + if c is None: + return + try: + server = proxifier.client_server_map[tuple(c)] + except KeyError: + server = None + write(server, self.wfile) + except (EOFError, OSError): + pass + + +class APIServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + + def __init__(self, proxifier, *args, **kwargs): + super().__init__(*args, **kwargs) + self.proxifier = proxifier + self.daemon_threads = True + + +########################## +# Windows API + +# from Windows' error.h +ERROR_INSUFFICIENT_BUFFER = 0x7A + +IN6_ADDR = ctypes.c_ubyte * 16 +IN4_ADDR = ctypes.c_ubyte * 4 + + +# +# IPv6 +# + +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa366896(v=vs.85).aspx +class MIB_TCP6ROW_OWNER_PID(ctypes.Structure): + _fields_ = [ + ('ucLocalAddr', IN6_ADDR), + ('dwLocalScopeId', ctypes.wintypes.DWORD), + ('dwLocalPort', ctypes.wintypes.DWORD), + ('ucRemoteAddr', IN6_ADDR), + ('dwRemoteScopeId', ctypes.wintypes.DWORD), + ('dwRemotePort', ctypes.wintypes.DWORD), + ('dwState', ctypes.wintypes.DWORD), + ('dwOwningPid', ctypes.wintypes.DWORD), + ] + + +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa366905(v=vs.85).aspx +def MIB_TCP6TABLE_OWNER_PID(size): + class _MIB_TCP6TABLE_OWNER_PID(ctypes.Structure): + _fields_ = [ + ('dwNumEntries', ctypes.wintypes.DWORD), + ('table', MIB_TCP6ROW_OWNER_PID * size) + ] + + return _MIB_TCP6TABLE_OWNER_PID() + + +# +# IPv4 +# + +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa366913(v=vs.85).aspx +class MIB_TCPROW_OWNER_PID(ctypes.Structure): + _fields_ = [ + ('dwState', ctypes.wintypes.DWORD), + ('ucLocalAddr', IN4_ADDR), + ('dwLocalPort', ctypes.wintypes.DWORD), + ('ucRemoteAddr', IN4_ADDR), + ('dwRemotePort', ctypes.wintypes.DWORD), + ('dwOwningPid', ctypes.wintypes.DWORD), + ] + + +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa366921(v=vs.85).aspx +def MIB_TCPTABLE_OWNER_PID(size): + class _MIB_TCPTABLE_OWNER_PID(ctypes.Structure): + _fields_ = [ + ('dwNumEntries', ctypes.wintypes.DWORD), + ('table', MIB_TCPROW_OWNER_PID * size) + ] + + return _MIB_TCPTABLE_OWNER_PID() + + +TCP_TABLE_OWNER_PID_CONNECTIONS = 4 + + +class TcpConnectionTable(collections.abc.Mapping): + DEFAULT_TABLE_SIZE = 4096 + + def __init__(self): + self._tcp = MIB_TCPTABLE_OWNER_PID(self.DEFAULT_TABLE_SIZE) + self._tcp_size = ctypes.wintypes.DWORD(self.DEFAULT_TABLE_SIZE) + self._tcp6 = MIB_TCP6TABLE_OWNER_PID(self.DEFAULT_TABLE_SIZE) + self._tcp6_size = ctypes.wintypes.DWORD(self.DEFAULT_TABLE_SIZE) + self._map = {} + + def __getitem__(self, item): + return self._map[item] + + def __iter__(self): + return self._map.__iter__() + + def __len__(self): + return self._map.__len__() + + def refresh(self): + self._map = {} + self._refresh_ipv4() + self._refresh_ipv6() + + def _refresh_ipv4(self): + ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( + ctypes.byref(self._tcp), + ctypes.byref(self._tcp_size), + False, + socket.AF_INET, + TCP_TABLE_OWNER_PID_CONNECTIONS, + 0 + ) + if ret == 0: + for row in self._tcp.table[:self._tcp.dwNumEntries]: + local_ip = socket.inet_ntop(socket.AF_INET, bytes(row.ucLocalAddr)) + local_port = socket.htons(row.dwLocalPort) + self._map[(local_ip, local_port)] = row.dwOwningPid + elif ret == ERROR_INSUFFICIENT_BUFFER: + self._tcp = MIB_TCPTABLE_OWNER_PID(self._tcp_size.value) + # no need to update size, that's already done. + self._refresh_ipv4() + else: + raise RuntimeError("[IPv4] Unknown GetExtendedTcpTable return code: %s" % ret) + + def _refresh_ipv6(self): + ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( + ctypes.byref(self._tcp6), + ctypes.byref(self._tcp6_size), + False, + socket.AF_INET6, + TCP_TABLE_OWNER_PID_CONNECTIONS, + 0 + ) + if ret == 0: + for row in self._tcp6.table[:self._tcp6.dwNumEntries]: + local_ip = socket.inet_ntop(socket.AF_INET6, bytes(row.ucLocalAddr)) + local_port = socket.htons(row.dwLocalPort) + self._map[(local_ip, local_port)] = row.dwOwningPid + elif ret == ERROR_INSUFFICIENT_BUFFER: + self._tcp6 = MIB_TCP6TABLE_OWNER_PID(self._tcp6_size.value) + # no need to update size, that's already done. + self._refresh_ipv6() + else: + raise RuntimeError("[IPv6] Unknown GetExtendedTcpTable return code: %s" % ret) + + +def get_local_ip() -> typing.Optional[str]: + # Auto-Detect local IP. This is required as re-injecting to 127.0.0.1 does not work. + # https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + s.connect(("8.8.8.8", 80)) + return s.getsockname()[0] + except OSError: + return None + finally: + s.close() + + +def get_local_ip6(reachable: str) -> typing.Optional[str]: + # The same goes for IPv6, with the added difficulty that .connect() fails if + # the target network is not reachable. + s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + try: + s.connect((reachable, 80)) + return s.getsockname()[0] + except OSError: + return None + finally: + s.close() + + +class Redirect(threading.Thread): + daemon = True + windivert: pydivert.WinDivert + + def __init__( + self, + handle: typing.Callable[[pydivert.Packet], None], + filter: str, + layer: pydivert.Layer = pydivert.Layer.NETWORK, + flags: pydivert.Flag = 0 + ) -> None: + self.handle = handle + self.windivert = pydivert.WinDivert(filter, layer, flags=flags) + super().__init__() + + def start(self): + self.windivert.open() + super().start() + + def run(self): + while True: + try: + packet = self.windivert.recv() + except OSError as e: + if e.winerror == 995: + return + else: + raise + else: + self.handle(packet) + + def shutdown(self): + self.windivert.close() + + def recv(self) -> typing.Optional[pydivert.Packet]: + """ + Convenience function that receives a packet from the passed handler and handles error codes. + If the process has been shut down, None is returned. + """ + try: + return self.windivert.recv() + except OSError as e: + if e.winerror == 995: # type: ignore + return None + else: + raise + + +class RedirectLocal(Redirect): + trusted_pids: typing.Set[int] + + def __init__( + self, + redirect_request: typing.Callable[[pydivert.Packet], None], + filter: str + ) -> None: + self.tcp_connections = TcpConnectionTable() + self.trusted_pids = set() + self.redirect_request = redirect_request + super().__init__(self.handle, filter) + + def handle(self, packet): + client = (packet.src_addr, packet.src_port) + + if client not in self.tcp_connections: + self.tcp_connections.refresh() + + # If this fails, we most likely have a connection from an external client. + # In this, case we always want to proxy the request. + pid = self.tcp_connections.get(client, None) + + if pid not in self.trusted_pids: + self.redirect_request(packet) + else: + # It's not really clear why we need to recalculate the checksum here, + # but this was identified as necessary in https://github.com/_internal_mitmproxy/_internal_mitmproxy/pull/3174. + self.windivert.send(packet, recalculate_checksum=True) + + +TConnection = typing.Tuple[str, int] + + +class ClientServerMap: + """A thread-safe LRU dict.""" + connection_cache_size: typing.ClassVar[int] = 65536 + + def __init__(self): + self._lock = threading.Lock() + self._map = collections.OrderedDict() + + def __getitem__(self, item: TConnection) -> TConnection: + with self._lock: + return self._map[item] + + def __setitem__(self, key: TConnection, value: TConnection) -> None: + with self._lock: + self._map[key] = value + self._map.move_to_end(key) + while len(self._map) > self.connection_cache_size: + self._map.popitem(False) + + +class TransparentProxy: + """ + Transparent Windows Proxy for _internal_mitmproxy based on WinDivert/PyDivert. This module can be used to + redirect both traffic that is forwarded by the host and traffic originating from the host itself. + + Requires elevated (admin) privileges. Can be started separately by manually running the file. + + How it works: + + (1) First, we intercept all packages that match our filter. + We both consider traffic that is forwarded by the OS (WinDivert's NETWORK_FORWARD layer) as well + as traffic sent from the local machine (WinDivert's NETWORK layer). In the case of traffic from + the local machine, we need to exempt packets sent from the proxy to not create a redirect loop. + To accomplish this, we use Windows' GetExtendedTcpTable syscall and determine the source + application's PID. + + For each intercepted package, we + 1. Store the source -> destination mapping (address and port) + 2. Remove the package from the network (by not reinjecting it). + 3. Re-inject the package into the local network stack, but with the destination address + changed to the proxy. + + (2) Next, the proxy receives the forwarded packet, but does not know the real destination yet + (which we overwrote with the proxy's address). On Linux, we would now call + getsockopt(SO_ORIGINAL_DST). We now access the redirect module's API (see APIRequestHandler), + submit the source information and get the actual destination back (which we stored in 1.1). + + (3) The proxy now establishes the upstream connection as usual. + + (4) Finally, the proxy sends the response back to the client. To make it work, we need to change + the packet's source address back to the original destination (using the mapping from 1.1), + to which the client believes it is talking to. + + Limitations: + + - We assume that ephemeral TCP ports are not re-used for multiple connections at the same time. + The proxy will fail if an application connects to example.com and example.org from + 192.168.0.42:4242 simultaneously. This could be mitigated by introducing unique "meta-addresses" + which _internal_mitmproxy sees, but this would remove the correct client info from _internal_mitmproxy. + """ + local: typing.Optional[RedirectLocal] = None + # really weird linting error here. + forward: typing.Optional[Redirect] = None # noqa + response: Redirect + icmp: Redirect + + proxy_port: int + filter: str + + client_server_map: ClientServerMap + + def __init__( + self, + local: bool = True, + forward: bool = True, + proxy_port: int = 8080, + filter: typing.Optional[str] = "tcp.DstPort == 80 or tcp.DstPort == 443", + ) -> None: + self.proxy_port = proxy_port + self.filter = ( + filter + or + f"tcp.DstPort != {proxy_port} and tcp.DstPort != {REDIRECT_API_PORT} and tcp.DstPort < 49152" + ) + + self.ipv4_address = get_local_ip() + self.ipv6_address = get_local_ip6("2001:4860:4860::8888") + # print(f"IPv4: {self.ipv4_address}, IPv6: {self.ipv6_address}") + self.client_server_map = ClientServerMap() + + self.api = APIServer(self, (REDIRECT_API_HOST, REDIRECT_API_PORT), APIRequestHandler) + self.api_thread = threading.Thread(target=self.api.serve_forever) + self.api_thread.daemon = True + + if forward: + self.forward = Redirect( + self.redirect_request, + self.filter, + pydivert.Layer.NETWORK_FORWARD + ) + if local: + self.local = RedirectLocal( + self.redirect_request, + self.filter + ) + + # The proxy server responds to the client. To the client, + # this response should look like it has been sent by the real target + self.response = Redirect( + self.redirect_response, + f"outbound and tcp.SrcPort == {proxy_port}", + ) + + # Block all ICMP requests (which are sent on Windows by default). + # If we don't do this, our proxy machine may send an ICMP redirect to the client, + # which instructs the client to directly connect to the real gateway + # if they are on the same network. + self.icmp = Redirect( + lambda _: None, + "icmp", + flags=pydivert.Flag.DROP + ) + + @classmethod + def setup(cls): + # TODO: Make sure that server can be killed cleanly. That's a bit difficult as we don't have access to + # controller.should_exit when this is called. + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_unavailable = s.connect_ex((REDIRECT_API_HOST, REDIRECT_API_PORT)) + if server_unavailable: + proxifier = TransparentProxy() + proxifier.start() + + def start(self): + self.api_thread.start() + self.icmp.start() + self.response.start() + if self.forward: + self.forward.start() + if self.local: + self.local.start() + + def shutdown(self): + if self.local: + self.local.shutdown() + if self.forward: + self.forward.shutdown() + self.response.shutdown() + self.icmp.shutdown() + self.api.shutdown() + + def redirect_request(self, packet: pydivert.Packet): + # print(" * Redirect client -> server to proxy") + # print(f"{packet.src_addr}:{packet.src_port} -> {packet.dst_addr}:{packet.dst_port}") + client = (packet.src_addr, packet.src_port) + + self.client_server_map[client] = (packet.dst_addr, packet.dst_port) + + # We do need to inject to an external IP here, 127.0.0.1 does not work. + if packet.address_family == socket.AF_INET: + assert self.ipv4_address + packet.dst_addr = self.ipv4_address + elif packet.address_family == socket.AF_INET6: + if not self.ipv6_address: + self.ipv6_address = get_local_ip6(packet.src_addr) + assert self.ipv6_address + packet.dst_addr = self.ipv6_address + else: + raise RuntimeError("Unknown address family") + packet.dst_port = self.proxy_port + packet.direction = pydivert.consts.Direction.INBOUND + + # We need a handle on the NETWORK layer. the local handle is not guaranteed to exist, + # so we use the response handle. + self.response.windivert.send(packet) + + def redirect_response(self, packet: pydivert.Packet): + """ + If the proxy responds to the client, let the client believe the target server sent the + packets. + """ + # print(" * Adjust proxy -> client") + client = (packet.dst_addr, packet.dst_port) + try: + packet.src_addr, packet.src_port = self.client_server_map[client] + except KeyError: + print(f"Warning: Previously unseen connection from proxy to {client}") + else: + packet.recalculate_checksums() + + self.response.windivert.send(packet, recalculate_checksum=False) + + @contextlib.contextmanager + def exempt(self, pid: int): + if self.local: + self.local.trusted_pids.add(pid) + try: + yield + finally: + if self.local: + self.local.trusted_pids.remove(pid) + + +if __name__ == "__main__": + import click + + @click.group() + def cli(): + pass + + @cli.command() + @click.option("--local/--no-local", default=True, + help="Redirect the host's own traffic.") + @click.option("--forward/--no-forward", default=True, + help="Redirect traffic that's forwarded by the host.") + @click.option("--filter", type=str, metavar="WINDIVERT_FILTER", + help="Custom WinDivert interception rule.") + @click.option("-p", "--proxy-port", type=int, metavar="8080", default=8080, + help="The port _internal_mitmproxy is listening on.") + def redirect(**options): + """Redirect flows to _internal_mitmproxy.""" + proxy = TransparentProxy(**options) + proxy.start() + print(f" * Redirection active.") + print(f" Filter: {proxy.filter}") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print(" * Shutting down...") + proxy.shutdown() + print(" * Shut down.") + + @cli.command() + def connections(): + """List all TCP connections and the associated PIDs.""" + connections = TcpConnectionTable() + connections.refresh() + for (ip, port), pid in connections.items(): + print(f"{ip}:{port} -> {pid}") + + cli() diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/py.typed b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/script/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/script/__init__.py new file mode 100644 index 00000000..e75f282a --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/script/__init__.py @@ -0,0 +1,5 @@ +from .concurrent import concurrent + +__all__ = [ + "concurrent", +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/script/concurrent.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/script/concurrent.py new file mode 100644 index 00000000..77d06900 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/script/concurrent.py @@ -0,0 +1,31 @@ +""" +This module provides a @concurrent decorator primitive to +offload computations from _internal_mitmproxy's main master thread. +""" + +import asyncio +import inspect +from _internal_mitmproxy import hooks + + +def concurrent(fn): + if fn.__name__ not in set(hooks.all_hooks.keys()) - {"load", "configure"}: + raise NotImplementedError( + "Concurrent decorator not supported for '%s' method." % fn.__name__ + ) + + async def _concurrent(*args): + def run(): + if inspect.iscoroutinefunction(fn): + # Run the async function in a new event loop + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(fn(*args)) + finally: + loop.close() + else: + fn(*args) + + await asyncio.get_running_loop().run_in_executor(None, run) + + return _concurrent diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/stateobject.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/stateobject.py new file mode 100644 index 00000000..c5bc0061 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/stateobject.py @@ -0,0 +1,99 @@ +import json +import typing + +from _internal_mitmproxy.coretypes import serializable +from _internal_mitmproxy.utils import typecheck + + +class StateObject(serializable.Serializable): + """ + An object with serializable state. + + State attributes can either be serializable types(str, tuple, bool, ...) + or StateObject instances themselves. + """ + + _stateobject_attributes: typing.ClassVar[typing.MutableMapping[str, typing.Any]] + """ + An attribute-name -> class-or-type dict containing all attributes that + should be serialized. If the attribute is a class, it must implement the + Serializable protocol. + """ + + def get_state(self): + """ + Retrieve object state. + """ + state = {} + for attr, cls in self._stateobject_attributes.items(): + val = getattr(self, attr) + state[attr] = get_state(cls, val) + return state + + def set_state(self, state): + """ + Load object state from data returned by a get_state call. + """ + state = state.copy() + for attr, cls in self._stateobject_attributes.items(): + val = state.pop(attr) + if val is None: + setattr(self, attr, val) + else: + curr = getattr(self, attr, None) + if hasattr(curr, "set_state"): + curr.set_state(val) + else: + setattr(self, attr, make_object(cls, val)) + if state: + raise RuntimeWarning(f"Unexpected State in __setstate__: {state}") + + +def _process(typeinfo: typecheck.Type, val: typing.Any, make: bool) -> typing.Any: + if val is None: + return None + elif make and hasattr(typeinfo, "from_state"): + return typeinfo.from_state(val) + elif not make and hasattr(val, "get_state"): + return val.get_state() + + typename = str(typeinfo) + + if typename.startswith("typing.List"): + T = typecheck.sequence_type(typeinfo) + return [_process(T, x, make) for x in val] + elif typename.startswith("typing.Tuple"): + Ts = typecheck.tuple_types(typeinfo) + if len(Ts) != len(val): + raise ValueError(f"Invalid data. Expected {Ts}, got {val}.") + return tuple( + _process(T, x, make) for T, x in zip(Ts, val) + ) + elif typename.startswith("typing.Dict"): + k_cls, v_cls = typecheck.mapping_types(typeinfo) + return { + _process(k_cls, k, make): _process(v_cls, v, make) + for k, v in val.items() + } + elif typename.startswith("typing.Any"): + # This requires a bit of explanation. We can't import our IO layer here, + # because it causes a circular import. Rather than restructuring the + # code for this, we use JSON serialization, which has similar primitive + # type restrictions as tnetstring, to check for conformance. + try: + json.dumps(val) + except TypeError: + raise ValueError(f"Data not serializable: {val}") + return val + else: + return typeinfo(val) + + +def make_object(typeinfo: typecheck.Type, val: typing.Any) -> typing.Any: + """Create an object based on the state given in val.""" + return _process(typeinfo, val, True) + + +def get_state(typeinfo: typecheck.Type, val: typing.Any) -> typing.Any: + """Get the state of the object given as val.""" + return _process(typeinfo, val, False) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/tcp.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/tcp.py new file mode 100644 index 00000000..2fb054df --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/tcp.py @@ -0,0 +1,64 @@ +import time +from typing import List + +from _internal_mitmproxy import flow +from _internal_mitmproxy.coretypes import serializable + + +class TCPMessage(serializable.Serializable): + """ + An individual TCP "message". + Note that TCP is *stream-based* and not *message-based*. + For practical purposes the stream is chunked into messages here, + but you should not rely on message boundaries. + """ + + def __init__(self, from_client, content, timestamp=None): + self.from_client = from_client + self.content = content + self.timestamp = timestamp or time.time() + + @classmethod + def from_state(cls, state): + return cls(*state) + + def get_state(self): + return self.from_client, self.content, self.timestamp + + def set_state(self, state): + self.from_client, self.content, self.timestamp = state + + def __repr__(self): + return "{direction} {content}".format( + direction="->" if self.from_client else "<-", + content=repr(self.content) + ) + + +class TCPFlow(flow.Flow): + """ + A TCPFlow is a simplified representation of a TCP session. + """ + + messages: List[TCPMessage] + """ + The messages transmitted over this connection. + + The latest message can be accessed as `flow.messages[-1]` in event hooks. + """ + + def __init__(self, client_conn, server_conn, live=None): + super().__init__("tcp", client_conn, server_conn, live) + self.messages = [] + + _stateobject_attributes = flow.Flow._stateobject_attributes.copy() + _stateobject_attributes["messages"] = List[TCPMessage] + + def __repr__(self): + return "".format(len(self.messages)) + + +__all__ = [ + "TCPFlow", + "TCPMessage", +] diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/taddons.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/taddons.py new file mode 100644 index 00000000..f15f0a56 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/taddons.py @@ -0,0 +1,124 @@ +import asyncio +import sys + +import _internal_mitmproxy.master +import _internal_mitmproxy.options +from _internal_mitmproxy import addonmanager, hooks, log +from _internal_mitmproxy import command +from _internal_mitmproxy import eventsequence +from _internal_mitmproxy.addons import script, core + + +class TestAddons(addonmanager.AddonManager): + def __init__(self, master): + super().__init__(master) + + def trigger(self, event: hooks.Hook): + if isinstance(event, log.AddLogHook): + self.master.logs.append(event.entry) + super().trigger(event) + + +class RecordingMaster(_internal_mitmproxy.master.Master): + def __init__(self, *args, **kwargs): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + super().__init__(*args, **kwargs, event_loop=loop) + self.addons = TestAddons(self) + self.logs = [] + + def dump_log(self, outf=sys.stdout): + for i in self.logs: + print(f"{i.level}: {i.msg}", file=outf) + + def has_log(self, txt, level=None): + for i in self.logs: + if level and i.level != level: + continue + if txt.lower() in i.msg.lower(): + return True + return False + + async def await_log(self, txt, level=None, timeout=1): + # start with a sleep(0), which lets all other coroutines advance. + # often this is enough to not sleep at all. + await asyncio.sleep(0) + for i in range(int(timeout / 0.01)): + if self.has_log(txt, level): + return True + else: + await asyncio.sleep(0.01) + raise AssertionError(f"Did not find log entry {txt!r} in {self.logs}.") + + def clear(self): + self.logs = [] + + +class context: + """ + A context for testing addons, which sets up the _internal_mitmproxy.ctx module so + handlers can run as they would within _internal_mitmproxy. The context also + provides a number of helper methods for common testing scenarios. + """ + + def __init__(self, *addons, options=None, loadcore=True): + options = options or _internal_mitmproxy.options.Options() + self.master = RecordingMaster( + options + ) + self.options = self.master.options + + if loadcore: + self.master.addons.add(core.Core()) + + for a in addons: + self.master.addons.add(a) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + return False + + async def cycle(self, addon, f): + """ + Cycles the flow through the events for the flow. Stops if the flow + is intercepted. + """ + for evt in eventsequence.iterate(f): + await self.master.addons.invoke_addon( + addon, + evt + ) + if f.intercepted: + return + + def configure(self, addon, **kwargs): + """ + A helper for testing configure methods. Modifies the registered + Options object with the given keyword arguments, then calls the + configure method on the addon with the updated value. + """ + if addon not in self.master.addons: + self.master.addons.register(addon) + with self.options.rollback(kwargs.keys(), reraise=True): + if kwargs: + self.options.update(**kwargs) + else: + self.master.addons.invoke_addon_sync(addon, hooks.ConfigureHook(set())) + + def script(self, path): + """ + Loads a script from path, and returns the enclosed addon. + """ + sc = script.Script(path, False) + return sc.addons[0] if sc.addons else None + + def command(self, func, *args): + """ + Invoke a command function with a list of string arguments within a command context, mimicking the actual command environment. + """ + cmd = command.Command(self.master.commands, "test.command", func) + return cmd.call(args) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/tflow.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/tflow.py new file mode 100644 index 00000000..6a574335 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/tflow.py @@ -0,0 +1,218 @@ +import uuid +from typing import Optional, Union + +from _internal_mitmproxy import connection +from _internal_mitmproxy import flow +from _internal_mitmproxy import http +from _internal_mitmproxy import tcp +from _internal_mitmproxy import websocket +from _internal_mitmproxy.test.tutils import treq, tresp +from wsproto.frame_protocol import Opcode + + +def ttcpflow(client_conn=True, server_conn=True, messages=True, err=None) -> tcp.TCPFlow: + if client_conn is True: + client_conn = tclient_conn() + if server_conn is True: + server_conn = tserver_conn() + if messages is True: + messages = [ + tcp.TCPMessage(True, b"hello", 946681204.2), + tcp.TCPMessage(False, b"it's me", 946681204.5), + ] + if err is True: + err = terr() + + f = tcp.TCPFlow(client_conn, server_conn) + f.messages = messages + f.error = err + f.live = True + return f + + +def twebsocketflow(messages=True, err=None, close_code=None, close_reason='') -> http.HTTPFlow: + flow = http.HTTPFlow(tclient_conn(), tserver_conn()) + flow.request = http.Request( + "example.com", + 80, + b"GET", + b"http", + b"example.com", + b"/ws", + b"HTTP/1.1", + headers=http.Headers( + connection="upgrade", + upgrade="websocket", + sec_websocket_version="13", + sec_websocket_key="1234", + ), + content=b'', + trailers=None, + timestamp_start=946681200, + timestamp_end=946681201, + + ) + flow.response = http.Response( + b"HTTP/1.1", + 101, + reason=b"Switching Protocols", + headers=http.Headers( + connection='upgrade', + upgrade='websocket', + sec_websocket_accept=b'', + ), + content=b'', + trailers=None, + timestamp_start=946681202, + timestamp_end=946681203, + ) + + flow.websocket = twebsocket() + + flow.websocket.close_reason = close_reason + + if close_code is not None: + flow.websocket.close_code = close_code + else: + if err is True: + # ABNORMAL_CLOSURE + flow.websocket.close_code = 1006 + else: + # NORMAL_CLOSURE + flow.websocket.close_code = 1000 + + flow.live = True + return flow + + +def tflow( + *, + client_conn: Optional[connection.Client] = None, + server_conn: Optional[connection.Server] = None, + req: Optional[http.Request] = None, + resp: Union[bool, http.Response] = False, + err: Union[bool, flow.Error] = False, + ws: Union[bool, websocket.WebSocketData] = False, + live: bool = True, +) -> http.HTTPFlow: + """Create a flow for testing.""" + if client_conn is None: + client_conn = tclient_conn() + if server_conn is None: + server_conn = tserver_conn() + if req is None: + req = treq() + + if resp is True: + resp = tresp() + if err is True: + err = terr() + if ws is True: + ws = twebsocket() + + assert resp is False or isinstance(resp, http.Response) + assert err is False or isinstance(err, flow.Error) + assert ws is False or isinstance(ws, websocket.WebSocketData) + + f = http.HTTPFlow(client_conn, server_conn) + f.request = req + f.response = resp or None + f.error = err or None + f.websocket = ws or None + f.live = live + return f + + +class DummyFlow(flow.Flow): + """A flow that is neither HTTP nor TCP.""" + + def __init__(self, client_conn, server_conn, live=None): + super().__init__("dummy", client_conn, server_conn, live) + + +def tdummyflow(client_conn=True, server_conn=True, err=None) -> DummyFlow: + if client_conn is True: + client_conn = tclient_conn() + if server_conn is True: + server_conn = tserver_conn() + if err is True: + err = terr() + + f = DummyFlow(client_conn, server_conn) + f.error = err + f.live = True + return f + + +def tclient_conn() -> connection.Client: + c = connection.Client.from_state(dict( + id=str(uuid.uuid4()), + address=("127.0.0.1", 22), + mitmcert=None, + tls_established=True, + timestamp_start=946681200, + timestamp_tls_setup=946681201, + timestamp_end=946681206, + sni="address", + cipher_name="cipher", + alpn=b"http/1.1", + tls_version="TLSv1.2", + tls_extensions=[(0x00, bytes.fromhex("000e00000b6578616d"))], + state=0, + sockname=("", 0), + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_list=[], + )) + return c + + +def tserver_conn() -> connection.Server: + c = connection.Server.from_state(dict( + id=str(uuid.uuid4()), + address=("address", 22), + source_address=("address", 22), + ip_address=("192.168.0.1", 22), + timestamp_start=946681202, + timestamp_tcp_setup=946681203, + timestamp_tls_setup=946681204, + timestamp_end=946681205, + tls_established=True, + sni="address", + alpn=None, + tls_version="TLSv1.2", + via=None, + state=0, + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_name=None, + cipher_list=[], + via2=None, + )) + return c + + +def terr(content: str = "error") -> flow.Error: + err = flow.Error(content, 946681207) + return err + + +def twebsocket(messages: bool = True) -> websocket.WebSocketData: + ws = websocket.WebSocketData() + + if messages: + ws.messages = [ + websocket.WebSocketMessage(Opcode.BINARY, True, b"hello binary", 946681203), + websocket.WebSocketMessage(Opcode.TEXT, True, b"hello text", 946681204), + websocket.WebSocketMessage(Opcode.TEXT, False, b"it's me", 946681205), + ] + ws.close_reason = "Close Reason" + ws.close_code = 1000 + ws.closed_by_client = False + ws.timestamp_end = 946681205 + + return ws diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/tutils.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/tutils.py new file mode 100644 index 00000000..6f061249 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/test/tutils.py @@ -0,0 +1,43 @@ +from _internal_mitmproxy import http + + +def treq(**kwargs) -> http.Request: + """ + Returns: + _internal_mitmproxy.net.http.Request + """ + default = dict( + host="address", + port=22, + method=b"GET", + scheme=b"http", + authority=b"", + path=b"/path", + http_version=b"HTTP/1.1", + headers=http.Headers(((b"header", b"qvalue"), (b"content-length", b"7"))), + content=b"content", + trailers=None, + timestamp_start=946681200, + timestamp_end=946681201, + ) + default.update(kwargs) + return http.Request(**default) # type: ignore + + +def tresp(**kwargs) -> http.Response: + """ + Returns: + _internal_mitmproxy.net.http.Response + """ + default = dict( + http_version=b"HTTP/1.1", + status_code=200, + reason=b"OK", + headers=http.Headers(((b"header-response", b"svalue"), (b"content-length", b"7"))), + content=b"message", + trailers=None, + timestamp_start=946681202, + timestamp_end=946681203, + ) + default.update(kwargs) + return http.Response(**default) # type: ignore diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/types.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/types.py new file mode 100644 index 00000000..30f44d57 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/types.py @@ -0,0 +1,502 @@ +import codecs +import os +import glob +import re +import typing + +from _internal_mitmproxy import exceptions +from _internal_mitmproxy import flow +from _internal_mitmproxy.utils import emoji, strutils + +if typing.TYPE_CHECKING: # pragma: no cover + from _internal_mitmproxy.command import CommandManager + + +class Path(str): + pass + + +class Cmd(str): + pass + + +class CmdArgs(str): + pass + + +class Unknown(str): + pass + + +class Space(str): + pass + + +class CutSpec(typing.Sequence[str]): + pass + + +class Data(typing.Sequence[typing.Sequence[typing.Union[str, bytes]]]): + pass + + +class Marker(str): + pass + + +class Choice: + def __init__(self, options_command): + self.options_command = options_command + + def __instancecheck__(self, instance): # pragma: no cover + # return false here so that arguments are piped through parsearg, + # which does extended validation. + return False + + +class _BaseType: + typ: typing.Type = object + display: str = "" + + def completion(self, manager: "CommandManager", t: typing.Any, s: str) -> typing.Sequence[str]: + """ + Returns a list of completion strings for a given prefix. The strings + returned don't necessarily need to be suffixes of the prefix, since + completers will do prefix filtering themselves.. + """ + raise NotImplementedError + + def parse(self, manager: "CommandManager", typ: typing.Any, s: str) -> typing.Any: + """ + Parse a string, given the specific type instance (to allow rich type annotations like Choice) and a string. + + Raises exceptions.TypeError if the value is invalid. + """ + raise NotImplementedError + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + """ + Check if data is valid for this type. + """ + raise NotImplementedError + + +class _BoolType(_BaseType): + typ = bool + display = "bool" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return ["false", "true"] + + def parse(self, manager: "CommandManager", t: type, s: str) -> bool: + if s == "true": + return True + elif s == "false": + return False + else: + raise exceptions.TypeError( + "Booleans are 'true' or 'false', got %s" % s + ) + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return val in [True, False] + + +class _StrType(_BaseType): + typ = str + display = "str" + + # https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals + escape_sequences = re.compile(r""" + \\ ( + [\\'"abfnrtv] # Standard C escape sequence + | [0-7]{1,3} # Character with octal value + | x.. # Character with hex value + | N{[^}]+} # Character name in the Unicode database + | u.... # Character with 16-bit hex value + | U........ # Character with 32-bit hex value + ) + """, re.VERBOSE) + + @staticmethod + def _unescape(match: re.Match) -> str: + return codecs.decode(match.group(0), "unicode-escape") # type: ignore + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [] + + def parse(self, manager: "CommandManager", t: type, s: str) -> str: + try: + return self.escape_sequences.sub(self._unescape, s) + except ValueError as e: + raise exceptions.TypeError(f"Invalid str: {e}") from e + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return isinstance(val, str) + + +class _BytesType(_BaseType): + typ = bytes + display = "bytes" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [] + + def parse(self, manager: "CommandManager", t: type, s: str) -> bytes: + try: + return strutils.escaped_str_to_bytes(s) + except ValueError as e: + raise exceptions.TypeError(str(e)) + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return isinstance(val, bytes) + + +class _UnknownType(_BaseType): + typ = Unknown + display = "unknown" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [] + + def parse(self, manager: "CommandManager", t: type, s: str) -> str: + return s + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return False + + +class _IntType(_BaseType): + typ = int + display = "int" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [] + + def parse(self, manager: "CommandManager", t: type, s: str) -> int: + try: + return int(s) + except ValueError as e: + raise exceptions.TypeError(str(e)) from e + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return isinstance(val, int) + + +class _PathType(_BaseType): + typ = Path + display = "path" + + def completion(self, manager: "CommandManager", t: type, start: str) -> typing.Sequence[str]: + if not start: + start = "./" + path = os.path.expanduser(start) + ret = [] + if os.path.isdir(path): + files = glob.glob(os.path.join(path, "*")) + prefix = start + else: + files = glob.glob(path + "*") + prefix = os.path.dirname(start) + prefix = prefix or "./" + for f in files: + display = os.path.join(prefix, os.path.normpath(os.path.basename(f))) + if os.path.isdir(f): + display += "/" + ret.append(display) + if not ret: + ret = [start] + ret.sort() + return ret + + def parse(self, manager: "CommandManager", t: type, s: str) -> str: + return os.path.expanduser(s) + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return isinstance(val, str) + + +class _CmdType(_BaseType): + typ = Cmd + display = "cmd" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return list(manager.commands.keys()) + + def parse(self, manager: "CommandManager", t: type, s: str) -> str: + if s not in manager.commands: + raise exceptions.TypeError("Unknown command: %s" % s) + return s + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return val in manager.commands + + +class _ArgType(_BaseType): + typ = CmdArgs + display = "arg" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [] + + def parse(self, manager: "CommandManager", t: type, s: str) -> str: + return s + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return isinstance(val, str) + + +class _StrSeqType(_BaseType): + typ = typing.Sequence[str] + display = "str[]" + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [] + + def parse(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return [x.strip() for x in s.split(",")] + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + if isinstance(val, str) or isinstance(val, bytes): + return False + try: + for v in val: + if not isinstance(v, str): + return False + except TypeError: + return False + return True + + +class _CutSpecType(_BaseType): + typ = CutSpec + display = "cut[]" + valid_prefixes = [ + "request.method", + "request.scheme", + "request.host", + "request.http_version", + "request.port", + "request.path", + "request.url", + "request.text", + "request.content", + "request.raw_content", + "request.timestamp_start", + "request.timestamp_end", + "request.header[", + + "response.status_code", + "response.reason", + "response.text", + "response.content", + "response.timestamp_start", + "response.timestamp_end", + "response.raw_content", + "response.header[", + + "client_conn.peername.port", + "client_conn.peername.host", + "client_conn.tls_version", + "client_conn.sni", + "client_conn.tls_established", + + "server_conn.address.port", + "server_conn.address.host", + "server_conn.ip_address.host", + "server_conn.tls_version", + "server_conn.sni", + "server_conn.tls_established", + ] + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + spec = s.split(",") + opts = [] + for pref in self.valid_prefixes: + spec[-1] = pref + opts.append(",".join(spec)) + return opts + + def parse(self, manager: "CommandManager", t: type, s: str) -> CutSpec: + parts: typing.Any = s.split(",") + return parts + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + if not isinstance(val, str): + return False + parts = [x.strip() for x in val.split(",")] + for p in parts: + for pref in self.valid_prefixes: + if p.startswith(pref): + break + else: + return False + return True + + +class _BaseFlowType(_BaseType): + viewmarkers = [ + "@all", + "@focus", + "@shown", + "@hidden", + "@marked", + "@unmarked", + ] + valid_prefixes = viewmarkers + [ + "~q", + "~s", + "~a", + "~hq", + "~hs", + "~b", + "~bq", + "~bs", + "~t", + "~d", + "~m", + "~u", + "~c", + ] + + def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + return self.valid_prefixes + + +class _FlowType(_BaseFlowType): + typ = flow.Flow + display = "flow" + + def parse(self, manager: "CommandManager", t: type, s: str) -> flow.Flow: + try: + flows = manager.call_strings("view.flows.resolve", [s]) + except exceptions.CommandError as e: + raise exceptions.TypeError(str(e)) from e + if len(flows) != 1: + raise exceptions.TypeError( + "Command requires one flow, specification matched %s." % len(flows) + ) + return flows[0] + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + return isinstance(val, flow.Flow) + + +class _FlowsType(_BaseFlowType): + typ = typing.Sequence[flow.Flow] + display = "flow[]" + + def parse(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[flow.Flow]: + try: + return manager.call_strings("view.flows.resolve", [s]) + except exceptions.CommandError as e: + raise exceptions.TypeError(str(e)) from e + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + try: + for v in val: + if not isinstance(v, flow.Flow): + return False + except TypeError: + return False + return True + + +class _DataType(_BaseType): + typ = Data + display = "data[][]" + + def completion( + self, manager: "CommandManager", t: type, s: str + ) -> typing.Sequence[str]: # pragma: no cover + raise exceptions.TypeError("data cannot be passed as argument") + + def parse( + self, manager: "CommandManager", t: type, s: str + ) -> typing.Any: # pragma: no cover + raise exceptions.TypeError("data cannot be passed as argument") + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + # FIXME: validate that all rows have equal length, and all columns have equal types + try: + for row in val: + for cell in row: + if not (isinstance(cell, str) or isinstance(cell, bytes)): + return False + except TypeError: + return False + return True + + +class _ChoiceType(_BaseType): + typ = Choice + display = "choice" + + def completion(self, manager: "CommandManager", t: Choice, s: str) -> typing.Sequence[str]: + return manager.execute(t.options_command) + + def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: + opts = manager.execute(t.options_command) + if s not in opts: + raise exceptions.TypeError("Invalid choice.") + return s + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + try: + opts = manager.execute(typ.options_command) + except exceptions.CommandError: + return False + return val in opts + + +ALL_MARKERS = ['true', 'false'] + list(emoji.emoji) + + +class _MarkerType(_BaseType): + typ = Marker + display = "marker" + + def completion(self, manager: "CommandManager", t: Choice, s: str) -> typing.Sequence[str]: + return ALL_MARKERS + + def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: + if s not in ALL_MARKERS: + raise exceptions.TypeError("Invalid choice.") + if s == 'true': + return ":default:" + elif s == 'false': + return "" + return s + + def is_valid(self, manager: "CommandManager", typ: typing.Any, val: str) -> bool: + return val in ALL_MARKERS + + +class TypeManager: + def __init__(self, *types): + self.typemap = {} + for t in types: + self.typemap[t.typ] = t() + + def get(self, t: typing.Optional[typing.Type], default=None) -> typing.Optional[_BaseType]: + if type(t) in self.typemap: + return self.typemap[type(t)] + return self.typemap.get(t, default) + + +CommandTypes = TypeManager( + _ArgType, + _BoolType, + _ChoiceType, + _CmdType, + _CutSpecType, + _DataType, + _FlowType, + _FlowsType, + _IntType, + _MarkerType, + _PathType, + _StrType, + _StrSeqType, + _BytesType, +) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/__init__.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/arg_check.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/arg_check.py new file mode 100644 index 00000000..e6cb61cc --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/arg_check.py @@ -0,0 +1,163 @@ +import sys +import re + +DEPRECATED = """ +--confdir +-Z +--body-size-limit +--stream +--palette +--palette-transparent +--follow +--order +--no-mouse +--reverse +--http2-priority +--no-http2-priority +--no-websocket +--websocket +--upstream-bind-address +--ciphers-client +--ciphers-server +--client-certs +--no-upstream-cert +--add-upstream-certs-to-client-chain +--upstream-trusted-confdir +--upstream-trusted-ca +--ssl-version-client +--ssl-version-server +--no-onboarding +--onboarding-host +--onboarding-port +--server-replay-use-header +--no-pop +--replay-ignore-content +--replay-ignore-payload-param +--replay-ignore-param +--replay-ignore-host +--replace-from-file +""" + +REPLACED = """ +-t +-u +--wfile +-a +--afile +-z +-b +--bind-address +--port +-I +--ignore +--tcp +--cert +--insecure +-c +--replace +--replacements +-i +-f +--filter +--socks +""" + +REPLACEMENTS = { + "--stream": "stream_large_bodies", + "--palette": "console_palette", + "--palette-transparent": "console_palette_transparent:", + "--follow": "console_focus_follow", + "--order": "view_order", + "--no-mouse": "console_mouse", + "--reverse": "view_order_reversed", + "--no-websocket": "websocket", + "--no-upstream-cert": "upstream_cert", + "--upstream-trusted-confdir": "ssl_verify_upstream_trusted_confdir", + "--upstream-trusted-ca": "ssl_verify_upstream_trusted_ca", + "--no-onboarding": "onboarding", + "--no-pop": "server_replay_nopop", + "--replay-ignore-content": "server_replay_ignore_content", + "--replay-ignore-payload-param": "server_replay_ignore_payload_params", + "--replay-ignore-param": "server_replay_ignore_params", + "--replay-ignore-host": "server_replay_ignore_host", + "--replace-from-file": "replacements (use @ to specify path)", + "-t": "--stickycookie", + "-u": "--stickyauth", + "--wfile": "--save-stream-file", + "-a": "-w Prefix path with + to append.", + "--afile": "-w Prefix path with + to append.", + "-z": "--anticomp", + "-b": "--listen-host", + "--bind-address": "--listen-host", + "--port": "--listen-port", + "-I": "--ignore-hosts", + "--ignore": "--ignore-hosts", + "--tcp": "--tcp-hosts", + "--cert": "--certs", + "--insecure": "--ssl-insecure", + "-c": "-C", + "--replace": ["--modify-body", "--modify-headers"], + "--replacements": ["--modify-body", "--modify-headers"], + "-i": "--intercept", + "-f": "--view-filter", + "--filter": "--view-filter", + "--socks": "--mode socks5" +} + + +def check(): + args = sys.argv[1:] + print() + if "-U" in args: + print("-U is deprecated, please use --mode upstream:SPEC instead") + + if "-T" in args: + print("-T is deprecated, please use --mode transparent instead") + + for option in ("-e", "--eventlog", "--norefresh"): + if option in args: + print(f"{option} has been removed.") + + for option in ("--nonanonymous", "--singleuser", "--htpasswd"): + if option in args: + print( + '{} is deprecated.\n' + 'Please use `--proxyauth SPEC` instead.\n' + 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' + '"@path" to use an Apache htpasswd file, or\n' + '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' + 'for LDAP authentication.'.format(option)) + + for option in REPLACED.splitlines(): + if option in args: + if isinstance(REPLACEMENTS.get(option), list): + new_options = REPLACEMENTS.get(option) + else: + new_options = [REPLACEMENTS.get(option)] + print( + "{} is deprecated.\n" + "Please use `{}` instead.".format( + option, + "` or `".join(new_options) + ) + ) + + for option in DEPRECATED.splitlines(): + if option in args: + print( + "{} is deprecated.\n" + "Please use `--set {}=value` instead.\n" + "To show all options and their default values use --options".format( + option, + REPLACEMENTS.get(option, None) or option.lstrip("-").replace("-", "_") + ) + ) + + # Check for underscores in the options. Options always follow '--'. + for argument in args: + underscoreParam = re.search(r'[-]{2}((.*?_)(.*?(\s|$)))+', argument) + if underscoreParam is not None: + print("{} uses underscores, please use hyphens {}".format( + argument, + argument.replace('_', '-')) + ) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/asyncio_utils.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/asyncio_utils.py new file mode 100644 index 00000000..b8c48ccd --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/asyncio_utils.py @@ -0,0 +1,67 @@ +import asyncio +import sys +import time +from collections.abc import Coroutine +from typing import Optional + +from _internal_mitmproxy.utils import human + + +def cancel_task(task: asyncio.Task, message: str) -> None: + """Like task.cancel(), but optionally with a message if the Python version supports it.""" + if sys.version_info >= (3, 9): + task.cancel(message) # type: ignore + else: # pragma: no cover + task.cancel() + + +def create_task( + coro: Coroutine, *, + name: str, + client: Optional[tuple] = None, + ignore_closed_loop: bool = True, +) -> Optional[asyncio.Task]: + """ + Like asyncio.create_task, but also store some debug info on the task object. + + If ignore_closed_loop is True, the task will be silently discarded if the event loop is closed. + This is currently useful during shutdown where no new tasks can be spawned. + Ideally we stop closing the event loop during shutdown and then remove this parameter. + """ + try: + t = asyncio.create_task(coro, name=name) + except RuntimeError: + if ignore_closed_loop: + coro.close() + return None + else: + raise + set_task_debug_info(t, name=name, client=client) + return t + + +def set_task_debug_info( + task: asyncio.Task, + *, + name: str, + client: Optional[tuple] = None, +) -> None: + """Set debug info for an externally-spawned task.""" + task.created = time.time() # type: ignore + task.set_name(name) + if client: + task.client = client # type: ignore + + +def task_repr(task: asyncio.Task) -> str: + """Get a task representation with debug info.""" + name = task.get_name() + a: float = getattr(task, "created", 0) + if a: + age = f" (age: {time.time() - a:.0f}s)" + else: + age = "" + client = getattr(task, "client", "") + if client: + client = f"{human.format_address(client)}: " + return f"{client}{name}{age}" diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/bits.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/bits.py new file mode 100644 index 00000000..2c89a999 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/bits.py @@ -0,0 +1,13 @@ +def setbit(byte, offset, value): + """ + Set a bit in a byte to 1 if value is truthy, 0 if not. + """ + if value: + return byte | (1 << offset) + else: + return byte & ~(1 << offset) + + +def getbit(byte, offset): + mask = 1 << offset + return bool(byte & mask) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/data.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/data.py new file mode 100644 index 00000000..5a175fce --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/data.py @@ -0,0 +1,36 @@ +import os.path +import importlib +import inspect + + +class Data: + + def __init__(self, name): + self.name = name + m = importlib.import_module(name) + dirname = os.path.dirname(inspect.getsourcefile(m)) + self.dirname = os.path.abspath(dirname) + + def push(self, subpath): + """ + Change the data object to a path relative to the module. + """ + dirname = os.path.normpath(os.path.join(self.dirname, subpath)) + ret = Data(self.name) + ret.dirname = dirname + return ret + + def path(self, path): + """ + Returns a path to the package data housed at 'path' under this + module.Path can be a path to a file, or to a directory. + + This function will raise ValueError if the path does not exist. + """ + fullpath = os.path.normpath(os.path.join(self.dirname, path)) + if not os.path.exists(fullpath): + raise ValueError("dataPath: %s does not exist." % fullpath) + return fullpath + + +pkg_data = Data(__name__).push("..") diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/debug.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/debug.py new file mode 100644 index 00000000..10796ffc --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/debug.py @@ -0,0 +1,127 @@ +import asyncio +import gc +import linecache +import os +import platform +import signal +import sys +import threading +import traceback +from contextlib import redirect_stdout + +from OpenSSL import SSL +from _internal_mitmproxy import version +from _internal_mitmproxy.utils import asyncio_utils + + +def dump_system_info(): + _internal_mitmproxy_version = version.get_dev_version() + + data = [ + f"_internal_mitmproxy: {_internal_mitmproxy_version}", + f"Python: {platform.python_version()}", + "OpenSSL: {}".format(SSL.SSLeay_version(SSL.SSLEAY_VERSION).decode()), + f"Platform: {platform.platform()}", + ] + return "\n".join(data) + + +def dump_info(signal=None, frame=None, file=sys.stdout, testing=False): # pragma: no cover + with redirect_stdout(file): + print("****************************************************") + print("Summary") + print("=======") + + try: + import psutil + except: + print("(psutil not installed, skipping some debug info)") + else: + p = psutil.Process() + print("num threads: ", p.num_threads()) + if hasattr(p, "num_fds"): + print("num fds: ", p.num_fds()) + print("memory: ", p.memory_info()) + + print() + print("Files") + print("=====") + for i in p.open_files(): + print(i) + + print() + print("Connections") + print("===========") + for i in p.connections(): + print(i) + + print() + print("Threads") + print("=======") + bthreads = [] + for i in threading.enumerate(): + if hasattr(i, "_threadinfo"): + bthreads.append(i) + else: + print(i.name) + bthreads.sort(key=lambda x: x._thread_started) + for i in bthreads: + print(i._threadinfo()) + + print() + print("Memory") + print("=======") + gc.collect() + d = {} + for i in gc.get_objects(): + t = str(type(i)) + if "_internal_mitmproxy" in t: + d[t] = d.setdefault(t, 0) + 1 + itms = list(d.items()) + itms.sort(key=lambda x: x[1]) + for i in itms[-20:]: + print(i[1], i[0]) + + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + print() + print("Tasks") + print("=======") + for task in asyncio.all_tasks(): + f = task.get_stack(limit=1)[0] + line = linecache.getline(f.f_code.co_filename, f.f_lineno, f.f_globals).strip() + line = f"{line} # at {os.path.basename(f.f_code.co_filename)}:{f.f_lineno}" + print(f"{asyncio_utils.task_repr(task)}\n" + f" {line}") + + print("****************************************************") + + if not testing and not os.getenv("_internal_mitmproxy_DEBUG_STAY_ALIVE"): # pragma: no cover + sys.exit(1) + + +def dump_stacks(signal=None, frame=None, file=sys.stdout, testing=False): + id2name = {th.ident: th.name for th in threading.enumerate()} + code = [] + for threadId, stack in sys._current_frames().items(): + code.append( + "\n# Thread: %s(%d)" % ( + id2name.get(threadId, ""), threadId + ) + ) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + print("\n".join(code), file=file) + if not testing and not os.getenv("_internal_mitmproxy_DEBUG_STAY_ALIVE"): # pragma: no cover + sys.exit(1) + + +def register_info_dumpers(): + if os.name != "nt": # pragma: windows no cover + signal.signal(signal.SIGUSR1, dump_info) + signal.signal(signal.SIGUSR2, dump_stacks) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/emoji.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/emoji.py new file mode 100644 index 00000000..9ae3ff6d --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/emoji.py @@ -0,0 +1,1887 @@ +#!/usr/bin/env python3 +""" +All of the emoji and characters that can be used as flow markers. +""" + +# auto-generated. run this file to refresh. + +emoji = { + ":+1:": "ðŸ‘", + ":-1:": "👎", + ":100:": "💯", + ":1234:": "🔢", + ":1st_place_medal:": "🥇", + ":2nd_place_medal:": "🥈", + ":3rd_place_medal:": "🥉", + ":8ball:": "🎱", + ":a:": "🅰", + ":ab:": "🆎", + ":abacus:": "🧮", + ":abc:": "🔤", + ":abcd:": "🔡", + ":accept:": "🉑", + ":adhesive_bandage:": "🩹", + ":adult:": "🧑", + ":aerial_tramway:": "🚡", + ":afghanistan:": "🇦â€ðŸ‡«", + ":airplane:": "✈", + ":aland_islands:": "🇦â€ðŸ‡½", + ":alarm_clock:": "â°", + ":albania:": "🇦â€ðŸ‡±", + ":alembic:": "âš—", + ":algeria:": "🇩â€ðŸ‡¿", + ":alien:": "👽", + ":ambulance:": "🚑", + ":american_samoa:": "🇦â€ðŸ‡¸", + ":amphora:": "ðŸº", + ":anchor:": "âš“", + ":andorra:": "🇦â€ðŸ‡©", + ":angel:": "👼", + ":anger:": "💢", + ":angola:": "🇦â€ðŸ‡´", + ":angry:": "😠", + ":anguilla:": "🇦â€ðŸ‡®", + ":anguished:": "😧", + ":ant:": "ðŸœ", + ":antarctica:": "🇦â€ðŸ‡¶", + ":antigua_barbuda:": "🇦â€ðŸ‡¬", + ":apple:": "ðŸŽ", + ":aquarius:": "â™’", + ":argentina:": "🇦â€ðŸ‡·", + ":aries:": "♈", + ":armenia:": "🇦â€ðŸ‡²", + ":arrow_backward:": "â—€", + ":arrow_double_down:": "â¬", + ":arrow_double_up:": "â«", + ":arrow_down:": "⬇", + ":arrow_down_small:": "🔽", + ":arrow_forward:": "â–¶", + ":arrow_heading_down:": "⤵", + ":arrow_heading_up:": "⤴", + ":arrow_left:": "⬅", + ":arrow_lower_left:": "↙", + ":arrow_lower_right:": "↘", + ":arrow_right:": "âž¡", + ":arrow_right_hook:": "↪", + ":arrow_up:": "⬆", + ":arrow_up_down:": "↕", + ":arrow_up_small:": "🔼", + ":arrow_upper_left:": "↖", + ":arrow_upper_right:": "↗", + ":arrows_clockwise:": "🔃", + ":arrows_counterclockwise:": "🔄", + ":art:": "🎨", + ":articulated_lorry:": "🚛", + ":artificial_satellite:": "🛰", + ":artist:": "🧑â€ðŸŽ¨", + ":aruba:": "🇦â€ðŸ‡¼", + ":ascension_island:": "🇦â€ðŸ‡¨", + ":asterisk:": "*â€âƒ£", + ":astonished:": "😲", + ":astronaut:": "🧑â€ðŸš€", + ":athletic_shoe:": "👟", + ":atm:": "ðŸ§", + ":atom_symbol:": "âš›", + ":australia:": "🇦â€ðŸ‡º", + ":austria:": "🇦â€ðŸ‡¹", + ":auto_rickshaw:": "🛺", + ":avocado:": "🥑", + ":axe:": "🪓", + ":azerbaijan:": "🇦â€ðŸ‡¿", + ":b:": "🅱", + ":baby:": "👶", + ":baby_bottle:": "ðŸ¼", + ":baby_chick:": "ðŸ¤", + ":baby_symbol:": "🚼", + ":back:": "🔙", + ":bacon:": "🥓", + ":badger:": "🦡", + ":badminton:": "ðŸ¸", + ":bagel:": "🥯", + ":baggage_claim:": "🛄", + ":baguette_bread:": "🥖", + ":bahamas:": "🇧â€ðŸ‡¸", + ":bahrain:": "🇧â€ðŸ‡­", + ":balance_scale:": "âš–", + ":bald_man:": "👨â€ðŸ¦²", + ":bald_woman:": "👩â€ðŸ¦²", + ":ballet_shoes:": "🩰", + ":balloon:": "🎈", + ":ballot_box:": "🗳", + ":ballot_box_with_check:": "☑", + ":bamboo:": "ðŸŽ", + ":banana:": "ðŸŒ", + ":bangbang:": "‼", + ":bangladesh:": "🇧â€ðŸ‡©", + ":banjo:": "🪕", + ":bank:": "ðŸ¦", + ":bar_chart:": "📊", + ":barbados:": "🇧â€ðŸ‡§", + ":barber:": "💈", + ":baseball:": "âš¾", + ":basket:": "🧺", + ":basketball:": "ðŸ€", + ":basketball_man:": "⛹â€â™‚", + ":basketball_woman:": "⛹â€â™€", + ":bat:": "🦇", + ":bath:": "🛀", + ":bathtub:": "ðŸ›", + ":battery:": "🔋", + ":beach_umbrella:": "ðŸ–", + ":bear:": "ðŸ»", + ":bearded_person:": "🧔", + ":bed:": "ðŸ›", + ":bee:": "ðŸ", + ":beer:": "ðŸº", + ":beers:": "ðŸ»", + ":beetle:": "ðŸž", + ":beginner:": "🔰", + ":belarus:": "🇧â€ðŸ‡¾", + ":belgium:": "🇧â€ðŸ‡ª", + ":belize:": "🇧â€ðŸ‡¿", + ":bell:": "🔔", + ":bellhop_bell:": "🛎", + ":benin:": "🇧â€ðŸ‡¯", + ":bento:": "ðŸ±", + ":bermuda:": "🇧â€ðŸ‡²", + ":beverage_box:": "🧃", + ":bhutan:": "🇧â€ðŸ‡¹", + ":bicyclist:": "🚴", + ":bike:": "🚲", + ":biking_man:": "🚴â€â™‚", + ":biking_woman:": "🚴â€â™€", + ":bikini:": "👙", + ":billed_cap:": "🧢", + ":biohazard:": "☣", + ":bird:": "ðŸ¦", + ":birthday:": "🎂", + ":black_circle:": "âš«", + ":black_flag:": "ðŸ´", + ":black_heart:": "🖤", + ":black_joker:": "ðŸƒ", + ":black_large_square:": "⬛", + ":black_medium_small_square:": "â—¾", + ":black_medium_square:": "â—¼", + ":black_nib:": "✒", + ":black_small_square:": "â–ª", + ":black_square_button:": "🔲", + ":blond_haired_man:": "👱â€â™‚", + ":blond_haired_person:": "👱", + ":blond_haired_woman:": "👱â€â™€", + ":blonde_woman:": "👱â€â™€", + ":blossom:": "🌼", + ":blowfish:": "ðŸ¡", + ":blue_book:": "📘", + ":blue_car:": "🚙", + ":blue_heart:": "💙", + ":blue_square:": "🟦", + ":blush:": "😊", + ":boar:": "ðŸ—", + ":boat:": "⛵", + ":bolivia:": "🇧â€ðŸ‡´", + ":bomb:": "💣", + ":bone:": "🦴", + ":book:": "📖", + ":bookmark:": "🔖", + ":bookmark_tabs:": "📑", + ":books:": "📚", + ":boom:": "💥", + ":boot:": "👢", + ":bosnia_herzegovina:": "🇧â€ðŸ‡¦", + ":botswana:": "🇧â€ðŸ‡¼", + ":bouncing_ball_man:": "⛹â€â™‚", + ":bouncing_ball_person:": "⛹", + ":bouncing_ball_woman:": "⛹â€â™€", + ":bouquet:": "ðŸ’", + ":bouvet_island:": "🇧â€ðŸ‡»", + ":bow:": "🙇", + ":bow_and_arrow:": "ðŸ¹", + ":bowing_man:": "🙇â€â™‚", + ":bowing_woman:": "🙇â€â™€", + ":bowl_with_spoon:": "🥣", + ":bowling:": "🎳", + ":boxing_glove:": "🥊", + ":boy:": "👦", + ":brain:": "🧠", + ":brazil:": "🇧â€ðŸ‡·", + ":bread:": "ðŸž", + ":breast_feeding:": "🤱", + ":bricks:": "🧱", + ":bride_with_veil:": "👰", + ":bridge_at_night:": "🌉", + ":briefcase:": "💼", + ":british_indian_ocean_territory:": "🇮â€ðŸ‡´", + ":british_virgin_islands:": "🇻â€ðŸ‡¬", + ":broccoli:": "🥦", + ":broken_heart:": "💔", + ":broom:": "🧹", + ":brown_circle:": "🟤", + ":brown_heart:": "🤎", + ":brown_square:": "🟫", + ":brunei:": "🇧â€ðŸ‡³", + ":bug:": "ðŸ›", + ":building_construction:": "ðŸ—", + ":bulb:": "💡", + ":bulgaria:": "🇧â€ðŸ‡¬", + ":bullettrain_front:": "🚅", + ":bullettrain_side:": "🚄", + ":burkina_faso:": "🇧â€ðŸ‡«", + ":burrito:": "🌯", + ":burundi:": "🇧â€ðŸ‡®", + ":bus:": "🚌", + ":business_suit_levitating:": "🕴", + ":busstop:": "ðŸš", + ":bust_in_silhouette:": "👤", + ":busts_in_silhouette:": "👥", + ":butter:": "🧈", + ":butterfly:": "🦋", + ":cactus:": "🌵", + ":cake:": "ðŸ°", + ":calendar:": "📆", + ":call_me_hand:": "🤙", + ":calling:": "📲", + ":cambodia:": "🇰â€ðŸ‡­", + ":camel:": "ðŸ«", + ":camera:": "📷", + ":camera_flash:": "📸", + ":cameroon:": "🇨â€ðŸ‡²", + ":camping:": "ðŸ•", + ":canada:": "🇨â€ðŸ‡¦", + ":canary_islands:": "🇮â€ðŸ‡¨", + ":cancer:": "♋", + ":candle:": "🕯", + ":candy:": "ðŸ¬", + ":canned_food:": "🥫", + ":canoe:": "🛶", + ":cape_verde:": "🇨â€ðŸ‡»", + ":capital_abcd:": "🔠", + ":capricorn:": "♑", + ":car:": "🚗", + ":card_file_box:": "🗃", + ":card_index:": "📇", + ":card_index_dividers:": "🗂", + ":caribbean_netherlands:": "🇧â€ðŸ‡¶", + ":carousel_horse:": "🎠", + ":carrot:": "🥕", + ":cartwheeling:": "🤸", + ":cat:": "ðŸ±", + ":cat2:": "ðŸˆ", + ":cayman_islands:": "🇰â€ðŸ‡¾", + ":cd:": "💿", + ":central_african_republic:": "🇨â€ðŸ‡«", + ":ceuta_melilla:": "🇪â€ðŸ‡¦", + ":chad:": "🇹â€ðŸ‡©", + ":chains:": "⛓", + ":chair:": "🪑", + ":champagne:": "ðŸ¾", + ":chart:": "💹", + ":chart_with_downwards_trend:": "📉", + ":chart_with_upwards_trend:": "📈", + ":checkered_flag:": "ðŸ", + ":cheese:": "🧀", + ":cherries:": "ðŸ’", + ":cherry_blossom:": "🌸", + ":chess_pawn:": "♟", + ":chestnut:": "🌰", + ":chicken:": "ðŸ”", + ":child:": "🧒", + ":children_crossing:": "🚸", + ":chile:": "🇨â€ðŸ‡±", + ":chipmunk:": "ðŸ¿", + ":chocolate_bar:": "ðŸ«", + ":chopsticks:": "🥢", + ":christmas_island:": "🇨â€ðŸ‡½", + ":christmas_tree:": "🎄", + ":church:": "⛪", + ":cinema:": "🎦", + ":circus_tent:": "🎪", + ":city_sunrise:": "🌇", + ":city_sunset:": "🌆", + ":cityscape:": "ðŸ™", + ":cl:": "🆑", + ":clamp:": "🗜", + ":clap:": "ðŸ‘", + ":clapper:": "🎬", + ":classical_building:": "ðŸ›", + ":climbing:": "🧗", + ":climbing_man:": "🧗â€â™‚", + ":climbing_woman:": "🧗â€â™€", + ":clinking_glasses:": "🥂", + ":clipboard:": "📋", + ":clipperton_island:": "🇨â€ðŸ‡µ", + ":clock1:": "ðŸ•", + ":clock10:": "🕙", + ":clock1030:": "🕥", + ":clock11:": "🕚", + ":clock1130:": "🕦", + ":clock12:": "🕛", + ":clock1230:": "🕧", + ":clock130:": "🕜", + ":clock2:": "🕑", + ":clock230:": "ðŸ•", + ":clock3:": "🕒", + ":clock330:": "🕞", + ":clock4:": "🕓", + ":clock430:": "🕟", + ":clock5:": "🕔", + ":clock530:": "🕠", + ":clock6:": "🕕", + ":clock630:": "🕡", + ":clock7:": "🕖", + ":clock730:": "🕢", + ":clock8:": "🕗", + ":clock830:": "🕣", + ":clock9:": "🕘", + ":clock930:": "🕤", + ":closed_book:": "📕", + ":closed_lock_with_key:": "ðŸ”", + ":closed_umbrella:": "🌂", + ":cloud:": "â˜", + ":cloud_with_lightning:": "🌩", + ":cloud_with_lightning_and_rain:": "⛈", + ":cloud_with_rain:": "🌧", + ":cloud_with_snow:": "🌨", + ":clown_face:": "🤡", + ":clubs:": "♣", + ":cn:": "🇨â€ðŸ‡³", + ":coat:": "🧥", + ":cocktail:": "ðŸ¸", + ":coconut:": "🥥", + ":cocos_islands:": "🇨â€ðŸ‡¨", + ":coffee:": "☕", + ":coffin:": "âš°", + ":cold_face:": "🥶", + ":cold_sweat:": "😰", + ":collision:": "💥", + ":colombia:": "🇨â€ðŸ‡´", + ":comet:": "☄", + ":comoros:": "🇰â€ðŸ‡²", + ":compass:": "🧭", + ":computer:": "💻", + ":computer_mouse:": "🖱", + ":confetti_ball:": "🎊", + ":confounded:": "😖", + ":confused:": "😕", + ":congo_brazzaville:": "🇨â€ðŸ‡¬", + ":congo_kinshasa:": "🇨â€ðŸ‡©", + ":congratulations:": "㊗", + ":construction:": "🚧", + ":construction_worker:": "👷", + ":construction_worker_man:": "👷â€â™‚", + ":construction_worker_woman:": "👷â€â™€", + ":control_knobs:": "🎛", + ":convenience_store:": "ðŸª", + ":cook:": "🧑â€ðŸ³", + ":cook_islands:": "🇨â€ðŸ‡°", + ":cookie:": "ðŸª", + ":cool:": "🆒", + ":cop:": "👮", + ":copyright:": "©", + ":corn:": "🌽", + ":costa_rica:": "🇨â€ðŸ‡·", + ":cote_divoire:": "🇨â€ðŸ‡®", + ":couch_and_lamp:": "🛋", + ":couple:": "👫", + ":couple_with_heart:": "💑", + ":couple_with_heart_man_man:": "👨â€â¤â€ðŸ‘¨", + ":couple_with_heart_woman_man:": "👩â€â¤â€ðŸ‘¨", + ":couple_with_heart_woman_woman:": "👩â€â¤â€ðŸ‘©", + ":couplekiss:": "ðŸ’", + ":couplekiss_man_man:": "👨â€â¤â€ðŸ’‹â€ðŸ‘¨", + ":couplekiss_man_woman:": "👩â€â¤â€ðŸ’‹â€ðŸ‘¨", + ":couplekiss_woman_woman:": "👩â€â¤â€ðŸ’‹â€ðŸ‘©", + ":cow:": "ðŸ®", + ":cow2:": "ðŸ„", + ":cowboy_hat_face:": "🤠", + ":crab:": "🦀", + ":crayon:": "ðŸ–", + ":credit_card:": "💳", + ":crescent_moon:": "🌙", + ":cricket:": "🦗", + ":cricket_game:": "ðŸ", + ":croatia:": "🇭â€ðŸ‡·", + ":crocodile:": "ðŸŠ", + ":croissant:": "ðŸ¥", + ":crossed_fingers:": "🤞", + ":crossed_flags:": "🎌", + ":crossed_swords:": "âš”", + ":crown:": "👑", + ":cry:": "😢", + ":crying_cat_face:": "😿", + ":crystal_ball:": "🔮", + ":cuba:": "🇨â€ðŸ‡º", + ":cucumber:": "🥒", + ":cup_with_straw:": "🥤", + ":cupcake:": "ðŸ§", + ":cupid:": "💘", + ":curacao:": "🇨â€ðŸ‡¼", + ":curling_stone:": "🥌", + ":curly_haired_man:": "👨â€ðŸ¦±", + ":curly_haired_woman:": "👩â€ðŸ¦±", + ":curly_loop:": "âž°", + ":currency_exchange:": "💱", + ":curry:": "ðŸ›", + ":cursing_face:": "🤬", + ":custard:": "ðŸ®", + ":customs:": "🛃", + ":cut_of_meat:": "🥩", + ":cyclone:": "🌀", + ":cyprus:": "🇨â€ðŸ‡¾", + ":czech_republic:": "🇨â€ðŸ‡¿", + ":dagger:": "🗡", + ":dancer:": "💃", + ":dancers:": "👯", + ":dancing_men:": "👯â€â™‚", + ":dancing_women:": "👯â€â™€", + ":dango:": "ðŸ¡", + ":dark_sunglasses:": "🕶", + ":dart:": "🎯", + ":dash:": "💨", + ":date:": "📅", + ":de:": "🇩â€ðŸ‡ª", + ":deaf_man:": "ðŸ§â€â™‚", + ":deaf_person:": "ðŸ§", + ":deaf_woman:": "ðŸ§â€â™€", + ":deciduous_tree:": "🌳", + ":deer:": "🦌", + ":denmark:": "🇩â€ðŸ‡°", + ":department_store:": "ðŸ¬", + ":derelict_house:": "ðŸš", + ":desert:": "ðŸœ", + ":desert_island:": "ðŸ", + ":desktop_computer:": "🖥", + ":detective:": "🕵", + ":diamond_shape_with_a_dot_inside:": "💠", + ":diamonds:": "♦", + ":diego_garcia:": "🇩â€ðŸ‡¬", + ":disappointed:": "😞", + ":disappointed_relieved:": "😥", + ":diving_mask:": "🤿", + ":diya_lamp:": "🪔", + ":dizzy:": "💫", + ":dizzy_face:": "😵", + ":djibouti:": "🇩â€ðŸ‡¯", + ":dna:": "🧬", + ":do_not_litter:": "🚯", + ":dog:": "ðŸ¶", + ":dog2:": "ðŸ•", + ":dollar:": "💵", + ":dolls:": "🎎", + ":dolphin:": "ðŸ¬", + ":dominica:": "🇩â€ðŸ‡²", + ":dominican_republic:": "🇩â€ðŸ‡´", + ":door:": "🚪", + ":doughnut:": "ðŸ©", + ":dove:": "🕊", + ":dragon:": "ðŸ‰", + ":dragon_face:": "ðŸ²", + ":dress:": "👗", + ":dromedary_camel:": "ðŸª", + ":drooling_face:": "🤤", + ":drop_of_blood:": "🩸", + ":droplet:": "💧", + ":drum:": "ðŸ¥", + ":duck:": "🦆", + ":dumpling:": "🥟", + ":dvd:": "📀", + ":e-mail:": "📧", + ":eagle:": "🦅", + ":ear:": "👂", + ":ear_of_rice:": "🌾", + ":ear_with_hearing_aid:": "🦻", + ":earth_africa:": "ðŸŒ", + ":earth_americas:": "🌎", + ":earth_asia:": "ðŸŒ", + ":ecuador:": "🇪â€ðŸ‡¨", + ":egg:": "🥚", + ":eggplant:": "ðŸ†", + ":egypt:": "🇪â€ðŸ‡¬", + ":eight:": "8â€âƒ£", + ":eight_pointed_black_star:": "✴", + ":eight_spoked_asterisk:": "✳", + ":eject_button:": "â", + ":el_salvador:": "🇸â€ðŸ‡»", + ":electric_plug:": "🔌", + ":elephant:": "ðŸ˜", + ":elf:": "ðŸ§", + ":elf_man:": "ðŸ§â€â™‚", + ":elf_woman:": "ðŸ§â€â™€", + ":email:": "✉", + ":end:": "🔚", + ":england:": "ðŸ´â€ó §â€ó ¢â€ó ¥â€ó ®â€ó §â€ó ¿", + ":envelope:": "✉", + ":envelope_with_arrow:": "📩", + ":equatorial_guinea:": "🇬â€ðŸ‡¶", + ":eritrea:": "🇪â€ðŸ‡·", + ":es:": "🇪â€ðŸ‡¸", + ":estonia:": "🇪â€ðŸ‡ª", + ":ethiopia:": "🇪â€ðŸ‡¹", + ":eu:": "🇪â€ðŸ‡º", + ":euro:": "💶", + ":european_castle:": "ðŸ°", + ":european_post_office:": "ðŸ¤", + ":european_union:": "🇪â€ðŸ‡º", + ":evergreen_tree:": "🌲", + ":exclamation:": "â—", + ":exploding_head:": "🤯", + ":expressionless:": "😑", + ":eye:": "ðŸ‘", + ":eye_speech_bubble:": "ðŸ‘â€ðŸ—¨", + ":eyeglasses:": "👓", + ":eyes:": "👀", + ":face_with_head_bandage:": "🤕", + ":face_with_thermometer:": "🤒", + ":facepalm:": "🤦", + ":facepunch:": "👊", + ":factory:": "ðŸ­", + ":factory_worker:": "🧑â€ðŸ­", + ":fairy:": "🧚", + ":fairy_man:": "🧚â€â™‚", + ":fairy_woman:": "🧚â€â™€", + ":falafel:": "🧆", + ":falkland_islands:": "🇫â€ðŸ‡°", + ":fallen_leaf:": "ðŸ‚", + ":family:": "👪", + ":family_man_boy:": "👨â€ðŸ‘¦", + ":family_man_boy_boy:": "👨â€ðŸ‘¦â€ðŸ‘¦", + ":family_man_girl:": "👨â€ðŸ‘§", + ":family_man_girl_boy:": "👨â€ðŸ‘§â€ðŸ‘¦", + ":family_man_girl_girl:": "👨â€ðŸ‘§â€ðŸ‘§", + ":family_man_man_boy:": "👨â€ðŸ‘¨â€ðŸ‘¦", + ":family_man_man_boy_boy:": "👨â€ðŸ‘¨â€ðŸ‘¦â€ðŸ‘¦", + ":family_man_man_girl:": "👨â€ðŸ‘¨â€ðŸ‘§", + ":family_man_man_girl_boy:": "👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘¦", + ":family_man_man_girl_girl:": "👨â€ðŸ‘¨â€ðŸ‘§â€ðŸ‘§", + ":family_man_woman_boy:": "👨â€ðŸ‘©â€ðŸ‘¦", + ":family_man_woman_boy_boy:": "👨â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦", + ":family_man_woman_girl:": "👨â€ðŸ‘©â€ðŸ‘§", + ":family_man_woman_girl_boy:": "👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦", + ":family_man_woman_girl_girl:": "👨â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§", + ":family_woman_boy:": "👩â€ðŸ‘¦", + ":family_woman_boy_boy:": "👩â€ðŸ‘¦â€ðŸ‘¦", + ":family_woman_girl:": "👩â€ðŸ‘§", + ":family_woman_girl_boy:": "👩â€ðŸ‘§â€ðŸ‘¦", + ":family_woman_girl_girl:": "👩â€ðŸ‘§â€ðŸ‘§", + ":family_woman_woman_boy:": "👩â€ðŸ‘©â€ðŸ‘¦", + ":family_woman_woman_boy_boy:": "👩â€ðŸ‘©â€ðŸ‘¦â€ðŸ‘¦", + ":family_woman_woman_girl:": "👩â€ðŸ‘©â€ðŸ‘§", + ":family_woman_woman_girl_boy:": "👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘¦", + ":family_woman_woman_girl_girl:": "👩â€ðŸ‘©â€ðŸ‘§â€ðŸ‘§", + ":farmer:": "🧑â€ðŸŒ¾", + ":faroe_islands:": "🇫â€ðŸ‡´", + ":fast_forward:": "â©", + ":fax:": "📠", + ":fearful:": "😨", + ":feet:": "ðŸ¾", + ":female_detective:": "🕵â€â™€", + ":female_sign:": "♀", + ":ferris_wheel:": "🎡", + ":ferry:": "â›´", + ":field_hockey:": "ðŸ‘", + ":fiji:": "🇫â€ðŸ‡¯", + ":file_cabinet:": "🗄", + ":file_folder:": "ðŸ“", + ":film_projector:": "📽", + ":film_strip:": "🎞", + ":finland:": "🇫â€ðŸ‡®", + ":fire:": "🔥", + ":fire_engine:": "🚒", + ":fire_extinguisher:": "🧯", + ":firecracker:": "🧨", + ":firefighter:": "🧑â€ðŸš’", + ":fireworks:": "🎆", + ":first_quarter_moon:": "🌓", + ":first_quarter_moon_with_face:": "🌛", + ":fish:": "ðŸŸ", + ":fish_cake:": "ðŸ¥", + ":fishing_pole_and_fish:": "🎣", + ":fist:": "✊", + ":fist_left:": "🤛", + ":fist_oncoming:": "👊", + ":fist_raised:": "✊", + ":fist_right:": "🤜", + ":five:": "5â€âƒ£", + ":flags:": "ðŸŽ", + ":flamingo:": "🦩", + ":flashlight:": "🔦", + ":flat_shoe:": "🥿", + ":fleur_de_lis:": "âšœ", + ":flight_arrival:": "🛬", + ":flight_departure:": "🛫", + ":flipper:": "ðŸ¬", + ":floppy_disk:": "💾", + ":flower_playing_cards:": "🎴", + ":flushed:": "😳", + ":flying_disc:": "ðŸ¥", + ":flying_saucer:": "🛸", + ":fog:": "🌫", + ":foggy:": "ðŸŒ", + ":foot:": "🦶", + ":football:": "ðŸˆ", + ":footprints:": "👣", + ":fork_and_knife:": "ðŸ´", + ":fortune_cookie:": "🥠", + ":fountain:": "⛲", + ":fountain_pen:": "🖋", + ":four:": "4â€âƒ£", + ":four_leaf_clover:": "ðŸ€", + ":fox_face:": "🦊", + ":fr:": "🇫â€ðŸ‡·", + ":framed_picture:": "🖼", + ":free:": "🆓", + ":french_guiana:": "🇬â€ðŸ‡«", + ":french_polynesia:": "🇵â€ðŸ‡«", + ":french_southern_territories:": "🇹â€ðŸ‡«", + ":fried_egg:": "ðŸ³", + ":fried_shrimp:": "ðŸ¤", + ":fries:": "ðŸŸ", + ":frog:": "ðŸ¸", + ":frowning:": "😦", + ":frowning_face:": "☹", + ":frowning_man:": "ðŸ™â€â™‚", + ":frowning_person:": "ðŸ™", + ":frowning_woman:": "ðŸ™â€â™€", + ":fu:": "🖕", + ":fuelpump:": "⛽", + ":full_moon:": "🌕", + ":full_moon_with_face:": "ðŸŒ", + ":funeral_urn:": "âš±", + ":gabon:": "🇬â€ðŸ‡¦", + ":gambia:": "🇬â€ðŸ‡²", + ":game_die:": "🎲", + ":garlic:": "🧄", + ":gb:": "🇬â€ðŸ‡§", + ":gear:": "âš™", + ":gem:": "💎", + ":gemini:": "♊", + ":genie:": "🧞", + ":genie_man:": "🧞â€â™‚", + ":genie_woman:": "🧞â€â™€", + ":georgia:": "🇬â€ðŸ‡ª", + ":ghana:": "🇬â€ðŸ‡­", + ":ghost:": "👻", + ":gibraltar:": "🇬â€ðŸ‡®", + ":gift:": "ðŸŽ", + ":gift_heart:": "ðŸ’", + ":giraffe:": "🦒", + ":girl:": "👧", + ":globe_with_meridians:": "ðŸŒ", + ":gloves:": "🧤", + ":goal_net:": "🥅", + ":goat:": "ðŸ", + ":goggles:": "🥽", + ":golf:": "⛳", + ":golfing:": "ðŸŒ", + ":golfing_man:": "ðŸŒâ€â™‚", + ":golfing_woman:": "ðŸŒâ€â™€", + ":gorilla:": "ðŸ¦", + ":grapes:": "ðŸ‡", + ":greece:": "🇬â€ðŸ‡·", + ":green_apple:": "ðŸ", + ":green_book:": "📗", + ":green_circle:": "🟢", + ":green_heart:": "💚", + ":green_salad:": "🥗", + ":green_square:": "🟩", + ":greenland:": "🇬â€ðŸ‡±", + ":grenada:": "🇬â€ðŸ‡©", + ":grey_exclamation:": "â•", + ":grey_question:": "â”", + ":grimacing:": "😬", + ":grin:": "ðŸ˜", + ":grinning:": "😀", + ":guadeloupe:": "🇬â€ðŸ‡µ", + ":guam:": "🇬â€ðŸ‡º", + ":guard:": "💂", + ":guardsman:": "💂â€â™‚", + ":guardswoman:": "💂â€â™€", + ":guatemala:": "🇬â€ðŸ‡¹", + ":guernsey:": "🇬â€ðŸ‡¬", + ":guide_dog:": "🦮", + ":guinea:": "🇬â€ðŸ‡³", + ":guinea_bissau:": "🇬â€ðŸ‡¼", + ":guitar:": "🎸", + ":gun:": "🔫", + ":guyana:": "🇬â€ðŸ‡¾", + ":haircut:": "💇", + ":haircut_man:": "💇â€â™‚", + ":haircut_woman:": "💇â€â™€", + ":haiti:": "🇭â€ðŸ‡¹", + ":hamburger:": "ðŸ”", + ":hammer:": "🔨", + ":hammer_and_pick:": "âš’", + ":hammer_and_wrench:": "🛠", + ":hamster:": "ðŸ¹", + ":hand:": "✋", + ":hand_over_mouth:": "🤭", + ":handbag:": "👜", + ":handball_person:": "🤾", + ":handshake:": "ðŸ¤", + ":hankey:": "💩", + ":hash:": "#â€âƒ£", + ":hatched_chick:": "ðŸ¥", + ":hatching_chick:": "ðŸ£", + ":headphones:": "🎧", + ":health_worker:": "🧑â€âš•", + ":hear_no_evil:": "🙉", + ":heard_mcdonald_islands:": "🇭â€ðŸ‡²", + ":heart:": "â¤", + ":heart_decoration:": "💟", + ":heart_eyes:": "ðŸ˜", + ":heart_eyes_cat:": "😻", + ":heartbeat:": "💓", + ":heartpulse:": "💗", + ":hearts:": "♥", + ":heavy_check_mark:": "✔", + ":heavy_division_sign:": "âž—", + ":heavy_dollar_sign:": "💲", + ":heavy_exclamation_mark:": "â—", + ":heavy_heart_exclamation:": "â£", + ":heavy_minus_sign:": "âž–", + ":heavy_multiplication_x:": "✖", + ":heavy_plus_sign:": "âž•", + ":hedgehog:": "🦔", + ":helicopter:": "ðŸš", + ":herb:": "🌿", + ":hibiscus:": "🌺", + ":high_brightness:": "🔆", + ":high_heel:": "👠", + ":hiking_boot:": "🥾", + ":hindu_temple:": "🛕", + ":hippopotamus:": "🦛", + ":hocho:": "🔪", + ":hole:": "🕳", + ":honduras:": "🇭â€ðŸ‡³", + ":honey_pot:": "ðŸ¯", + ":honeybee:": "ðŸ", + ":hong_kong:": "🇭â€ðŸ‡°", + ":horse:": "ðŸ´", + ":horse_racing:": "ðŸ‡", + ":hospital:": "ðŸ¥", + ":hot_face:": "🥵", + ":hot_pepper:": "🌶", + ":hotdog:": "🌭", + ":hotel:": "ðŸ¨", + ":hotsprings:": "♨", + ":hourglass:": "⌛", + ":hourglass_flowing_sand:": "â³", + ":house:": "ðŸ ", + ":house_with_garden:": "ðŸ¡", + ":houses:": "ðŸ˜", + ":hugs:": "🤗", + ":hungary:": "🇭â€ðŸ‡º", + ":hushed:": "😯", + ":ice_cream:": "ðŸ¨", + ":ice_cube:": "🧊", + ":ice_hockey:": "ðŸ’", + ":ice_skate:": "⛸", + ":icecream:": "ðŸ¦", + ":iceland:": "🇮â€ðŸ‡¸", + ":id:": "🆔", + ":ideograph_advantage:": "ðŸ‰", + ":imp:": "👿", + ":inbox_tray:": "📥", + ":incoming_envelope:": "📨", + ":india:": "🇮â€ðŸ‡³", + ":indonesia:": "🇮â€ðŸ‡©", + ":infinity:": "♾", + ":information_desk_person:": "ðŸ’", + ":information_source:": "ℹ", + ":innocent:": "😇", + ":interrobang:": "â‰", + ":iphone:": "📱", + ":iran:": "🇮â€ðŸ‡·", + ":iraq:": "🇮â€ðŸ‡¶", + ":ireland:": "🇮â€ðŸ‡ª", + ":isle_of_man:": "🇮â€ðŸ‡²", + ":israel:": "🇮â€ðŸ‡±", + ":it:": "🇮â€ðŸ‡¹", + ":izakaya_lantern:": "ðŸ®", + ":jack_o_lantern:": "🎃", + ":jamaica:": "🇯â€ðŸ‡²", + ":japan:": "🗾", + ":japanese_castle:": "ðŸ¯", + ":japanese_goblin:": "👺", + ":japanese_ogre:": "👹", + ":jeans:": "👖", + ":jersey:": "🇯â€ðŸ‡ª", + ":jigsaw:": "🧩", + ":jordan:": "🇯â€ðŸ‡´", + ":joy:": "😂", + ":joy_cat:": "😹", + ":joystick:": "🕹", + ":jp:": "🇯â€ðŸ‡µ", + ":judge:": "🧑â€âš–", + ":juggling_person:": "🤹", + ":kaaba:": "🕋", + ":kangaroo:": "🦘", + ":kazakhstan:": "🇰â€ðŸ‡¿", + ":kenya:": "🇰â€ðŸ‡ª", + ":key:": "🔑", + ":keyboard:": "⌨", + ":keycap_ten:": "🔟", + ":kick_scooter:": "🛴", + ":kimono:": "👘", + ":kiribati:": "🇰â€ðŸ‡®", + ":kiss:": "💋", + ":kissing:": "😗", + ":kissing_cat:": "😽", + ":kissing_closed_eyes:": "😚", + ":kissing_heart:": "😘", + ":kissing_smiling_eyes:": "😙", + ":kite:": "ðŸª", + ":kiwi_fruit:": "ðŸ¥", + ":kneeling_man:": "🧎â€â™‚", + ":kneeling_person:": "🧎", + ":kneeling_woman:": "🧎â€â™€", + ":knife:": "🔪", + ":koala:": "ðŸ¨", + ":koko:": "ðŸˆ", + ":kosovo:": "🇽â€ðŸ‡°", + ":kr:": "🇰â€ðŸ‡·", + ":kuwait:": "🇰â€ðŸ‡¼", + ":kyrgyzstan:": "🇰â€ðŸ‡¬", + ":lab_coat:": "🥼", + ":label:": "ðŸ·", + ":lacrosse:": "ðŸ¥", + ":lantern:": "ðŸ®", + ":laos:": "🇱â€ðŸ‡¦", + ":large_blue_circle:": "🔵", + ":large_blue_diamond:": "🔷", + ":large_orange_diamond:": "🔶", + ":last_quarter_moon:": "🌗", + ":last_quarter_moon_with_face:": "🌜", + ":latin_cross:": "âœ", + ":latvia:": "🇱â€ðŸ‡»", + ":laughing:": "😆", + ":leafy_green:": "🥬", + ":leaves:": "ðŸƒ", + ":lebanon:": "🇱â€ðŸ‡§", + ":ledger:": "📒", + ":left_luggage:": "🛅", + ":left_right_arrow:": "↔", + ":left_speech_bubble:": "🗨", + ":leftwards_arrow_with_hook:": "↩", + ":leg:": "🦵", + ":lemon:": "ðŸ‹", + ":leo:": "♌", + ":leopard:": "ðŸ†", + ":lesotho:": "🇱â€ðŸ‡¸", + ":level_slider:": "🎚", + ":liberia:": "🇱â€ðŸ‡·", + ":libra:": "♎", + ":libya:": "🇱â€ðŸ‡¾", + ":liechtenstein:": "🇱â€ðŸ‡®", + ":light_rail:": "🚈", + ":link:": "🔗", + ":lion:": "ðŸ¦", + ":lips:": "👄", + ":lipstick:": "💄", + ":lithuania:": "🇱â€ðŸ‡¹", + ":lizard:": "🦎", + ":llama:": "🦙", + ":lobster:": "🦞", + ":lock:": "🔒", + ":lock_with_ink_pen:": "ðŸ”", + ":lollipop:": "ðŸ­", + ":loop:": "âž¿", + ":lotion_bottle:": "🧴", + ":lotus_position:": "🧘", + ":lotus_position_man:": "🧘â€â™‚", + ":lotus_position_woman:": "🧘â€â™€", + ":loud_sound:": "🔊", + ":loudspeaker:": "📢", + ":love_hotel:": "ðŸ©", + ":love_letter:": "💌", + ":love_you_gesture:": "🤟", + ":low_brightness:": "🔅", + ":luggage:": "🧳", + ":luxembourg:": "🇱â€ðŸ‡º", + ":lying_face:": "🤥", + ":m:": "â“‚", + ":macau:": "🇲â€ðŸ‡´", + ":macedonia:": "🇲â€ðŸ‡°", + ":madagascar:": "🇲â€ðŸ‡¬", + ":mag:": "ðŸ”", + ":mag_right:": "🔎", + ":mage:": "🧙", + ":mage_man:": "🧙â€â™‚", + ":mage_woman:": "🧙â€â™€", + ":magnet:": "🧲", + ":mahjong:": "🀄", + ":mailbox:": "📫", + ":mailbox_closed:": "📪", + ":mailbox_with_mail:": "📬", + ":mailbox_with_no_mail:": "📭", + ":malawi:": "🇲â€ðŸ‡¼", + ":malaysia:": "🇲â€ðŸ‡¾", + ":maldives:": "🇲â€ðŸ‡»", + ":male_detective:": "🕵â€â™‚", + ":male_sign:": "♂", + ":mali:": "🇲â€ðŸ‡±", + ":malta:": "🇲â€ðŸ‡¹", + ":man:": "👨", + ":man_artist:": "👨â€ðŸŽ¨", + ":man_astronaut:": "👨â€ðŸš€", + ":man_cartwheeling:": "🤸â€â™‚", + ":man_cook:": "👨â€ðŸ³", + ":man_dancing:": "🕺", + ":man_facepalming:": "🤦â€â™‚", + ":man_factory_worker:": "👨â€ðŸ­", + ":man_farmer:": "👨â€ðŸŒ¾", + ":man_firefighter:": "👨â€ðŸš’", + ":man_health_worker:": "👨â€âš•", + ":man_in_manual_wheelchair:": "👨â€ðŸ¦½", + ":man_in_motorized_wheelchair:": "👨â€ðŸ¦¼", + ":man_in_tuxedo:": "🤵", + ":man_judge:": "👨â€âš–", + ":man_juggling:": "🤹â€â™‚", + ":man_mechanic:": "👨â€ðŸ”§", + ":man_office_worker:": "👨â€ðŸ’¼", + ":man_pilot:": "👨â€âœˆ", + ":man_playing_handball:": "🤾â€â™‚", + ":man_playing_water_polo:": "🤽â€â™‚", + ":man_scientist:": "👨â€ðŸ”¬", + ":man_shrugging:": "🤷â€â™‚", + ":man_singer:": "👨â€ðŸŽ¤", + ":man_student:": "👨â€ðŸŽ“", + ":man_teacher:": "👨â€ðŸ«", + ":man_technologist:": "👨â€ðŸ’»", + ":man_with_gua_pi_mao:": "👲", + ":man_with_probing_cane:": "👨â€ðŸ¦¯", + ":man_with_turban:": "👳â€â™‚", + ":mandarin:": "ðŸŠ", + ":mango:": "🥭", + ":mans_shoe:": "👞", + ":mantelpiece_clock:": "🕰", + ":manual_wheelchair:": "🦽", + ":maple_leaf:": "ðŸ", + ":marshall_islands:": "🇲â€ðŸ‡­", + ":martial_arts_uniform:": "🥋", + ":martinique:": "🇲â€ðŸ‡¶", + ":mask:": "😷", + ":massage:": "💆", + ":massage_man:": "💆â€â™‚", + ":massage_woman:": "💆â€â™€", + ":mate:": "🧉", + ":mauritania:": "🇲â€ðŸ‡·", + ":mauritius:": "🇲â€ðŸ‡º", + ":mayotte:": "🇾â€ðŸ‡¹", + ":meat_on_bone:": "ðŸ–", + ":mechanic:": "🧑â€ðŸ”§", + ":mechanical_arm:": "🦾", + ":mechanical_leg:": "🦿", + ":medal_military:": "🎖", + ":medal_sports:": "ðŸ…", + ":medical_symbol:": "âš•", + ":mega:": "📣", + ":melon:": "ðŸˆ", + ":memo:": "ðŸ“", + ":men_wrestling:": "🤼â€â™‚", + ":menorah:": "🕎", + ":mens:": "🚹", + ":mermaid:": "🧜â€â™€", + ":merman:": "🧜â€â™‚", + ":merperson:": "🧜", + ":metal:": "🤘", + ":metro:": "🚇", + ":mexico:": "🇲â€ðŸ‡½", + ":microbe:": "🦠", + ":micronesia:": "🇫â€ðŸ‡²", + ":microphone:": "🎤", + ":microscope:": "🔬", + ":middle_finger:": "🖕", + ":milk_glass:": "🥛", + ":milky_way:": "🌌", + ":minibus:": "ðŸš", + ":minidisc:": "💽", + ":mobile_phone_off:": "📴", + ":moldova:": "🇲â€ðŸ‡©", + ":monaco:": "🇲â€ðŸ‡¨", + ":money_mouth_face:": "🤑", + ":money_with_wings:": "💸", + ":moneybag:": "💰", + ":mongolia:": "🇲â€ðŸ‡³", + ":monkey:": "ðŸ’", + ":monkey_face:": "ðŸµ", + ":monocle_face:": "ðŸ§", + ":monorail:": "ðŸš", + ":montenegro:": "🇲â€ðŸ‡ª", + ":montserrat:": "🇲â€ðŸ‡¸", + ":moon:": "🌔", + ":moon_cake:": "🥮", + ":morocco:": "🇲â€ðŸ‡¦", + ":mortar_board:": "🎓", + ":mosque:": "🕌", + ":mosquito:": "🦟", + ":motor_boat:": "🛥", + ":motor_scooter:": "🛵", + ":motorcycle:": "ðŸ", + ":motorized_wheelchair:": "🦼", + ":motorway:": "🛣", + ":mount_fuji:": "🗻", + ":mountain:": "â›°", + ":mountain_bicyclist:": "🚵", + ":mountain_biking_man:": "🚵â€â™‚", + ":mountain_biking_woman:": "🚵â€â™€", + ":mountain_cableway:": "🚠", + ":mountain_railway:": "🚞", + ":mountain_snow:": "ðŸ”", + ":mouse:": "ðŸ­", + ":mouse2:": "ðŸ", + ":movie_camera:": "🎥", + ":moyai:": "🗿", + ":mozambique:": "🇲â€ðŸ‡¿", + ":mrs_claus:": "🤶", + ":muscle:": "💪", + ":mushroom:": "ðŸ„", + ":musical_keyboard:": "🎹", + ":musical_note:": "🎵", + ":musical_score:": "🎼", + ":mute:": "🔇", + ":myanmar:": "🇲â€ðŸ‡²", + ":nail_care:": "💅", + ":name_badge:": "📛", + ":namibia:": "🇳â€ðŸ‡¦", + ":national_park:": "ðŸž", + ":nauru:": "🇳â€ðŸ‡·", + ":nauseated_face:": "🤢", + ":nazar_amulet:": "🧿", + ":necktie:": "👔", + ":negative_squared_cross_mark:": "âŽ", + ":nepal:": "🇳â€ðŸ‡µ", + ":nerd_face:": "🤓", + ":netherlands:": "🇳â€ðŸ‡±", + ":neutral_face:": "ðŸ˜", + ":new:": "🆕", + ":new_caledonia:": "🇳â€ðŸ‡¨", + ":new_moon:": "🌑", + ":new_moon_with_face:": "🌚", + ":new_zealand:": "🇳â€ðŸ‡¿", + ":newspaper:": "📰", + ":newspaper_roll:": "🗞", + ":next_track_button:": "â­", + ":ng:": "🆖", + ":ng_man:": "🙅â€â™‚", + ":ng_woman:": "🙅â€â™€", + ":nicaragua:": "🇳â€ðŸ‡®", + ":niger:": "🇳â€ðŸ‡ª", + ":nigeria:": "🇳â€ðŸ‡¬", + ":night_with_stars:": "🌃", + ":nine:": "9â€âƒ£", + ":niue:": "🇳â€ðŸ‡º", + ":no_bell:": "🔕", + ":no_bicycles:": "🚳", + ":no_entry:": "â›”", + ":no_entry_sign:": "🚫", + ":no_good:": "🙅", + ":no_good_man:": "🙅â€â™‚", + ":no_good_woman:": "🙅â€â™€", + ":no_mobile_phones:": "📵", + ":no_mouth:": "😶", + ":no_pedestrians:": "🚷", + ":no_smoking:": "🚭", + ":non-potable_water:": "🚱", + ":norfolk_island:": "🇳â€ðŸ‡«", + ":north_korea:": "🇰â€ðŸ‡µ", + ":northern_mariana_islands:": "🇲â€ðŸ‡µ", + ":norway:": "🇳â€ðŸ‡´", + ":nose:": "👃", + ":notebook:": "📓", + ":notebook_with_decorative_cover:": "📔", + ":notes:": "🎶", + ":nut_and_bolt:": "🔩", + ":o:": "â­•", + ":o2:": "🅾", + ":ocean:": "🌊", + ":octopus:": "ðŸ™", + ":oden:": "ðŸ¢", + ":office:": "ðŸ¢", + ":office_worker:": "🧑â€ðŸ’¼", + ":oil_drum:": "🛢", + ":ok:": "🆗", + ":ok_hand:": "👌", + ":ok_man:": "🙆â€â™‚", + ":ok_person:": "🙆", + ":ok_woman:": "🙆â€â™€", + ":old_key:": "ðŸ—", + ":older_adult:": "🧓", + ":older_man:": "👴", + ":older_woman:": "👵", + ":om:": "🕉", + ":oman:": "🇴â€ðŸ‡²", + ":on:": "🔛", + ":oncoming_automobile:": "🚘", + ":oncoming_bus:": "ðŸš", + ":oncoming_police_car:": "🚔", + ":oncoming_taxi:": "🚖", + ":one:": "1â€âƒ£", + ":one_piece_swimsuit:": "🩱", + ":onion:": "🧅", + ":open_book:": "📖", + ":open_file_folder:": "📂", + ":open_hands:": "ðŸ‘", + ":open_mouth:": "😮", + ":open_umbrella:": "☂", + ":ophiuchus:": "⛎", + ":orange:": "ðŸŠ", + ":orange_book:": "📙", + ":orange_circle:": "🟠", + ":orange_heart:": "🧡", + ":orange_square:": "🟧", + ":orangutan:": "🦧", + ":orthodox_cross:": "☦", + ":otter:": "🦦", + ":outbox_tray:": "📤", + ":owl:": "🦉", + ":ox:": "ðŸ‚", + ":oyster:": "🦪", + ":package:": "📦", + ":page_facing_up:": "📄", + ":page_with_curl:": "📃", + ":pager:": "📟", + ":paintbrush:": "🖌", + ":pakistan:": "🇵â€ðŸ‡°", + ":palau:": "🇵â€ðŸ‡¼", + ":palestinian_territories:": "🇵â€ðŸ‡¸", + ":palm_tree:": "🌴", + ":palms_up_together:": "🤲", + ":panama:": "🇵â€ðŸ‡¦", + ":pancakes:": "🥞", + ":panda_face:": "ðŸ¼", + ":paperclip:": "📎", + ":paperclips:": "🖇", + ":papua_new_guinea:": "🇵â€ðŸ‡¬", + ":parachute:": "🪂", + ":paraguay:": "🇵â€ðŸ‡¾", + ":parasol_on_ground:": "â›±", + ":parking:": "🅿", + ":parrot:": "🦜", + ":part_alternation_mark:": "〽", + ":partly_sunny:": "â›…", + ":partying_face:": "🥳", + ":passenger_ship:": "🛳", + ":passport_control:": "🛂", + ":pause_button:": "â¸", + ":paw_prints:": "ðŸ¾", + ":peace_symbol:": "☮", + ":peach:": "ðŸ‘", + ":peacock:": "🦚", + ":peanuts:": "🥜", + ":pear:": "ðŸ", + ":pen:": "🖊", + ":pencil:": "ðŸ“", + ":pencil2:": "âœ", + ":penguin:": "ðŸ§", + ":pensive:": "😔", + ":people_holding_hands:": "🧑â€ðŸ¤â€ðŸ§‘", + ":performing_arts:": "🎭", + ":persevere:": "😣", + ":person_bald:": "🧑â€ðŸ¦²", + ":person_curly_hair:": "🧑â€ðŸ¦±", + ":person_fencing:": "🤺", + ":person_in_manual_wheelchair:": "🧑â€ðŸ¦½", + ":person_in_motorized_wheelchair:": "🧑â€ðŸ¦¼", + ":person_red_hair:": "🧑â€ðŸ¦°", + ":person_white_hair:": "🧑â€ðŸ¦³", + ":person_with_probing_cane:": "🧑â€ðŸ¦¯", + ":person_with_turban:": "👳", + ":peru:": "🇵â€ðŸ‡ª", + ":petri_dish:": "🧫", + ":philippines:": "🇵â€ðŸ‡­", + ":phone:": "☎", + ":pick:": "â›", + ":pie:": "🥧", + ":pig:": "ðŸ·", + ":pig2:": "ðŸ–", + ":pig_nose:": "ðŸ½", + ":pill:": "💊", + ":pilot:": "🧑â€âœˆ", + ":pinching_hand:": "ðŸ¤", + ":pineapple:": "ðŸ", + ":ping_pong:": "ðŸ“", + ":pirate_flag:": "ðŸ´â€â˜ ", + ":pisces:": "♓", + ":pitcairn_islands:": "🇵â€ðŸ‡³", + ":pizza:": "ðŸ•", + ":place_of_worship:": "ðŸ›", + ":plate_with_cutlery:": "ðŸ½", + ":play_or_pause_button:": "â¯", + ":pleading_face:": "🥺", + ":point_down:": "👇", + ":point_left:": "👈", + ":point_right:": "👉", + ":point_up:": "â˜", + ":point_up_2:": "👆", + ":poland:": "🇵â€ðŸ‡±", + ":police_car:": "🚓", + ":police_officer:": "👮", + ":policeman:": "👮â€â™‚", + ":policewoman:": "👮â€â™€", + ":poodle:": "ðŸ©", + ":poop:": "💩", + ":popcorn:": "ðŸ¿", + ":portugal:": "🇵â€ðŸ‡¹", + ":post_office:": "ðŸ£", + ":postal_horn:": "📯", + ":postbox:": "📮", + ":potable_water:": "🚰", + ":potato:": "🥔", + ":pouch:": "ðŸ‘", + ":poultry_leg:": "ðŸ—", + ":pound:": "💷", + ":pout:": "😡", + ":pouting_cat:": "😾", + ":pouting_face:": "🙎", + ":pouting_man:": "🙎â€â™‚", + ":pouting_woman:": "🙎â€â™€", + ":pray:": "ðŸ™", + ":prayer_beads:": "📿", + ":pregnant_woman:": "🤰", + ":pretzel:": "🥨", + ":previous_track_button:": "â®", + ":prince:": "🤴", + ":princess:": "👸", + ":printer:": "🖨", + ":probing_cane:": "🦯", + ":puerto_rico:": "🇵â€ðŸ‡·", + ":punch:": "👊", + ":purple_circle:": "🟣", + ":purple_heart:": "💜", + ":purple_square:": "🟪", + ":purse:": "👛", + ":pushpin:": "📌", + ":put_litter_in_its_place:": "🚮", + ":qatar:": "🇶â€ðŸ‡¦", + ":question:": "â“", + ":rabbit:": "ðŸ°", + ":rabbit2:": "ðŸ‡", + ":raccoon:": "ðŸ¦", + ":racehorse:": "ðŸŽ", + ":racing_car:": "ðŸŽ", + ":radio:": "📻", + ":radio_button:": "🔘", + ":radioactive:": "☢", + ":rage:": "😡", + ":railway_car:": "🚃", + ":railway_track:": "🛤", + ":rainbow:": "🌈", + ":rainbow_flag:": "ðŸ³â€ðŸŒˆ", + ":raised_back_of_hand:": "🤚", + ":raised_eyebrow:": "🤨", + ":raised_hand:": "✋", + ":raised_hand_with_fingers_splayed:": "ðŸ–", + ":raised_hands:": "🙌", + ":raising_hand:": "🙋", + ":raising_hand_man:": "🙋â€â™‚", + ":raising_hand_woman:": "🙋â€â™€", + ":ram:": "ðŸ", + ":ramen:": "ðŸœ", + ":rat:": "ðŸ€", + ":razor:": "🪒", + ":receipt:": "🧾", + ":record_button:": "âº", + ":recycle:": "â™»", + ":red_car:": "🚗", + ":red_circle:": "🔴", + ":red_envelope:": "🧧", + ":red_haired_man:": "👨â€ðŸ¦°", + ":red_haired_woman:": "👩â€ðŸ¦°", + ":red_square:": "🟥", + ":registered:": "®", + ":relaxed:": "☺", + ":relieved:": "😌", + ":reminder_ribbon:": "🎗", + ":repeat:": "ðŸ”", + ":repeat_one:": "🔂", + ":rescue_worker_helmet:": "⛑", + ":restroom:": "🚻", + ":reunion:": "🇷â€ðŸ‡ª", + ":revolving_hearts:": "💞", + ":rewind:": "âª", + ":rhinoceros:": "ðŸ¦", + ":ribbon:": "🎀", + ":rice:": "ðŸš", + ":rice_ball:": "ðŸ™", + ":rice_cracker:": "ðŸ˜", + ":rice_scene:": "🎑", + ":right_anger_bubble:": "🗯", + ":ring:": "ðŸ’", + ":ringed_planet:": "ðŸª", + ":robot:": "🤖", + ":rocket:": "🚀", + ":rofl:": "🤣", + ":roll_eyes:": "🙄", + ":roll_of_paper:": "🧻", + ":roller_coaster:": "🎢", + ":romania:": "🇷â€ðŸ‡´", + ":rooster:": "ðŸ“", + ":rose:": "🌹", + ":rosette:": "ðŸµ", + ":rotating_light:": "🚨", + ":round_pushpin:": "ðŸ“", + ":rowboat:": "🚣", + ":rowing_man:": "🚣â€â™‚", + ":rowing_woman:": "🚣â€â™€", + ":ru:": "🇷â€ðŸ‡º", + ":rugby_football:": "ðŸ‰", + ":runner:": "ðŸƒ", + ":running:": "ðŸƒ", + ":running_man:": "ðŸƒâ€â™‚", + ":running_shirt_with_sash:": "🎽", + ":running_woman:": "ðŸƒâ€â™€", + ":rwanda:": "🇷â€ðŸ‡¼", + ":sa:": "🈂", + ":safety_pin:": "🧷", + ":safety_vest:": "🦺", + ":sagittarius:": "â™", + ":sailboat:": "⛵", + ":sake:": "ðŸ¶", + ":salt:": "🧂", + ":samoa:": "🇼â€ðŸ‡¸", + ":san_marino:": "🇸â€ðŸ‡²", + ":sandal:": "👡", + ":sandwich:": "🥪", + ":santa:": "🎅", + ":sao_tome_principe:": "🇸â€ðŸ‡¹", + ":sari:": "🥻", + ":sassy_man:": "ðŸ’â€â™‚", + ":sassy_woman:": "ðŸ’â€â™€", + ":satellite:": "📡", + ":satisfied:": "😆", + ":saudi_arabia:": "🇸â€ðŸ‡¦", + ":sauna_man:": "🧖â€â™‚", + ":sauna_person:": "🧖", + ":sauna_woman:": "🧖â€â™€", + ":sauropod:": "🦕", + ":saxophone:": "🎷", + ":scarf:": "🧣", + ":school:": "ðŸ«", + ":school_satchel:": "🎒", + ":scientist:": "🧑â€ðŸ”¬", + ":scissors:": "✂", + ":scorpion:": "🦂", + ":scorpius:": "â™", + ":scotland:": "ðŸ´â€ó §â€ó ¢â€ó ³â€ó £â€ó ´â€ó ¿", + ":scream:": "😱", + ":scream_cat:": "🙀", + ":scroll:": "📜", + ":seat:": "💺", + ":secret:": "㊙", + ":see_no_evil:": "🙈", + ":seedling:": "🌱", + ":selfie:": "🤳", + ":senegal:": "🇸â€ðŸ‡³", + ":serbia:": "🇷â€ðŸ‡¸", + ":service_dog:": "ðŸ•â€ðŸ¦º", + ":seven:": "7â€âƒ£", + ":seychelles:": "🇸â€ðŸ‡¨", + ":shallow_pan_of_food:": "🥘", + ":shamrock:": "☘", + ":shark:": "🦈", + ":shaved_ice:": "ðŸ§", + ":sheep:": "ðŸ‘", + ":shell:": "ðŸš", + ":shield:": "🛡", + ":shinto_shrine:": "⛩", + ":ship:": "🚢", + ":shirt:": "👕", + ":shit:": "💩", + ":shoe:": "👞", + ":shopping:": "ðŸ›", + ":shopping_cart:": "🛒", + ":shorts:": "🩳", + ":shower:": "🚿", + ":shrimp:": "ðŸ¦", + ":shrug:": "🤷", + ":shushing_face:": "🤫", + ":sierra_leone:": "🇸â€ðŸ‡±", + ":signal_strength:": "📶", + ":singapore:": "🇸â€ðŸ‡¬", + ":singer:": "🧑â€ðŸŽ¤", + ":sint_maarten:": "🇸â€ðŸ‡½", + ":six:": "6â€âƒ£", + ":six_pointed_star:": "🔯", + ":skateboard:": "🛹", + ":ski:": "🎿", + ":skier:": "â›·", + ":skull:": "💀", + ":skull_and_crossbones:": "☠", + ":skunk:": "🦨", + ":sled:": "🛷", + ":sleeping:": "😴", + ":sleeping_bed:": "🛌", + ":sleepy:": "😪", + ":slightly_frowning_face:": "ðŸ™", + ":slightly_smiling_face:": "🙂", + ":slot_machine:": "🎰", + ":sloth:": "🦥", + ":slovakia:": "🇸â€ðŸ‡°", + ":slovenia:": "🇸â€ðŸ‡®", + ":small_airplane:": "🛩", + ":small_blue_diamond:": "🔹", + ":small_orange_diamond:": "🔸", + ":small_red_triangle:": "🔺", + ":small_red_triangle_down:": "🔻", + ":smile:": "😄", + ":smile_cat:": "😸", + ":smiley:": "😃", + ":smiley_cat:": "😺", + ":smiling_face_with_three_hearts:": "🥰", + ":smiling_imp:": "😈", + ":smirk:": "ðŸ˜", + ":smirk_cat:": "😼", + ":smoking:": "🚬", + ":snail:": "ðŸŒ", + ":snake:": "ðŸ", + ":sneezing_face:": "🤧", + ":snowboarder:": "ðŸ‚", + ":snowflake:": "â„", + ":snowman:": "⛄", + ":snowman_with_snow:": "☃", + ":soap:": "🧼", + ":sob:": "😭", + ":soccer:": "âš½", + ":socks:": "🧦", + ":softball:": "🥎", + ":solomon_islands:": "🇸â€ðŸ‡§", + ":somalia:": "🇸â€ðŸ‡´", + ":soon:": "🔜", + ":sos:": "🆘", + ":sound:": "🔉", + ":south_africa:": "🇿â€ðŸ‡¦", + ":south_georgia_south_sandwich_islands:": "🇬â€ðŸ‡¸", + ":south_sudan:": "🇸â€ðŸ‡¸", + ":space_invader:": "👾", + ":spades:": "â™ ", + ":spaghetti:": "ðŸ", + ":sparkle:": "â‡", + ":sparkler:": "🎇", + ":sparkles:": "✨", + ":sparkling_heart:": "💖", + ":speak_no_evil:": "🙊", + ":speaker:": "🔈", + ":speaking_head:": "🗣", + ":speech_balloon:": "💬", + ":speedboat:": "🚤", + ":spider:": "🕷", + ":spider_web:": "🕸", + ":spiral_calendar:": "🗓", + ":spiral_notepad:": "🗒", + ":sponge:": "🧽", + ":spoon:": "🥄", + ":squid:": "🦑", + ":sri_lanka:": "🇱â€ðŸ‡°", + ":st_barthelemy:": "🇧â€ðŸ‡±", + ":st_helena:": "🇸â€ðŸ‡­", + ":st_kitts_nevis:": "🇰â€ðŸ‡³", + ":st_lucia:": "🇱â€ðŸ‡¨", + ":st_martin:": "🇲â€ðŸ‡«", + ":st_pierre_miquelon:": "🇵â€ðŸ‡²", + ":st_vincent_grenadines:": "🇻â€ðŸ‡¨", + ":stadium:": "ðŸŸ", + ":standing_man:": "ðŸ§â€â™‚", + ":standing_person:": "ðŸ§", + ":standing_woman:": "ðŸ§â€â™€", + ":star:": "â­", + ":star2:": "🌟", + ":star_and_crescent:": "☪", + ":star_of_david:": "✡", + ":star_struck:": "🤩", + ":stars:": "🌠", + ":station:": "🚉", + ":statue_of_liberty:": "🗽", + ":steam_locomotive:": "🚂", + ":stethoscope:": "🩺", + ":stew:": "ðŸ²", + ":stop_button:": "â¹", + ":stop_sign:": "🛑", + ":stopwatch:": "â±", + ":straight_ruler:": "ðŸ“", + ":strawberry:": "ðŸ“", + ":stuck_out_tongue:": "😛", + ":stuck_out_tongue_closed_eyes:": "ðŸ˜", + ":stuck_out_tongue_winking_eye:": "😜", + ":student:": "🧑â€ðŸŽ“", + ":studio_microphone:": "🎙", + ":stuffed_flatbread:": "🥙", + ":sudan:": "🇸â€ðŸ‡©", + ":sun_behind_large_cloud:": "🌥", + ":sun_behind_rain_cloud:": "🌦", + ":sun_behind_small_cloud:": "🌤", + ":sun_with_face:": "🌞", + ":sunflower:": "🌻", + ":sunglasses:": "😎", + ":sunny:": "☀", + ":sunrise:": "🌅", + ":sunrise_over_mountains:": "🌄", + ":superhero:": "🦸", + ":superhero_man:": "🦸â€â™‚", + ":superhero_woman:": "🦸â€â™€", + ":supervillain:": "🦹", + ":supervillain_man:": "🦹â€â™‚", + ":supervillain_woman:": "🦹â€â™€", + ":surfer:": "ðŸ„", + ":surfing_man:": "ðŸ„â€â™‚", + ":surfing_woman:": "ðŸ„â€â™€", + ":suriname:": "🇸â€ðŸ‡·", + ":sushi:": "ðŸ£", + ":suspension_railway:": "🚟", + ":svalbard_jan_mayen:": "🇸â€ðŸ‡¯", + ":swan:": "🦢", + ":swaziland:": "🇸â€ðŸ‡¿", + ":sweat:": "😓", + ":sweat_drops:": "💦", + ":sweat_smile:": "😅", + ":sweden:": "🇸â€ðŸ‡ª", + ":sweet_potato:": "ðŸ ", + ":swim_brief:": "🩲", + ":swimmer:": "ðŸŠ", + ":swimming_man:": "ðŸŠâ€â™‚", + ":swimming_woman:": "ðŸŠâ€â™€", + ":switzerland:": "🇨â€ðŸ‡­", + ":symbols:": "🔣", + ":synagogue:": "ðŸ•", + ":syria:": "🇸â€ðŸ‡¾", + ":syringe:": "💉", + ":t-rex:": "🦖", + ":taco:": "🌮", + ":tada:": "🎉", + ":taiwan:": "🇹â€ðŸ‡¼", + ":tajikistan:": "🇹â€ðŸ‡¯", + ":takeout_box:": "🥡", + ":tanabata_tree:": "🎋", + ":tangerine:": "ðŸŠ", + ":tanzania:": "🇹â€ðŸ‡¿", + ":taurus:": "♉", + ":taxi:": "🚕", + ":tea:": "ðŸµ", + ":teacher:": "🧑â€ðŸ«", + ":technologist:": "🧑â€ðŸ’»", + ":teddy_bear:": "🧸", + ":telephone:": "☎", + ":telephone_receiver:": "📞", + ":telescope:": "🔭", + ":tennis:": "🎾", + ":tent:": "⛺", + ":test_tube:": "🧪", + ":thailand:": "🇹â€ðŸ‡­", + ":thermometer:": "🌡", + ":thinking:": "🤔", + ":thought_balloon:": "💭", + ":thread:": "🧵", + ":three:": "3â€âƒ£", + ":thumbsdown:": "👎", + ":thumbsup:": "ðŸ‘", + ":ticket:": "🎫", + ":tickets:": "🎟", + ":tiger:": "ðŸ¯", + ":tiger2:": "ðŸ…", + ":timer_clock:": "â²", + ":timor_leste:": "🇹â€ðŸ‡±", + ":tipping_hand_man:": "ðŸ’â€â™‚", + ":tipping_hand_person:": "ðŸ’", + ":tipping_hand_woman:": "ðŸ’â€â™€", + ":tired_face:": "😫", + ":tm:": "â„¢", + ":togo:": "🇹â€ðŸ‡¬", + ":toilet:": "🚽", + ":tokelau:": "🇹â€ðŸ‡°", + ":tokyo_tower:": "🗼", + ":tomato:": "ðŸ…", + ":tonga:": "🇹â€ðŸ‡´", + ":tongue:": "👅", + ":toolbox:": "🧰", + ":tooth:": "🦷", + ":top:": "ðŸ”", + ":tophat:": "🎩", + ":tornado:": "🌪", + ":tr:": "🇹â€ðŸ‡·", + ":trackball:": "🖲", + ":tractor:": "🚜", + ":traffic_light:": "🚥", + ":train:": "🚋", + ":train2:": "🚆", + ":tram:": "🚊", + ":triangular_flag_on_post:": "🚩", + ":triangular_ruler:": "ðŸ“", + ":trident:": "🔱", + ":trinidad_tobago:": "🇹â€ðŸ‡¹", + ":tristan_da_cunha:": "🇹â€ðŸ‡¦", + ":triumph:": "😤", + ":trolleybus:": "🚎", + ":trophy:": "ðŸ†", + ":tropical_drink:": "ðŸ¹", + ":tropical_fish:": "ðŸ ", + ":truck:": "🚚", + ":trumpet:": "🎺", + ":tshirt:": "👕", + ":tulip:": "🌷", + ":tumbler_glass:": "🥃", + ":tunisia:": "🇹â€ðŸ‡³", + ":turkey:": "🦃", + ":turkmenistan:": "🇹â€ðŸ‡²", + ":turks_caicos_islands:": "🇹â€ðŸ‡¨", + ":turtle:": "ðŸ¢", + ":tuvalu:": "🇹â€ðŸ‡»", + ":tv:": "📺", + ":twisted_rightwards_arrows:": "🔀", + ":two:": "2â€âƒ£", + ":two_hearts:": "💕", + ":two_men_holding_hands:": "👬", + ":two_women_holding_hands:": "👭", + ":u5272:": "🈹", + ":u5408:": "🈴", + ":u55b6:": "🈺", + ":u6307:": "🈯", + ":u6708:": "🈷", + ":u6709:": "🈶", + ":u6e80:": "🈵", + ":u7121:": "🈚", + ":u7533:": "🈸", + ":u7981:": "🈲", + ":u7a7a:": "🈳", + ":uganda:": "🇺â€ðŸ‡¬", + ":uk:": "🇬â€ðŸ‡§", + ":ukraine:": "🇺â€ðŸ‡¦", + ":umbrella:": "☔", + ":unamused:": "😒", + ":underage:": "🔞", + ":unicorn:": "🦄", + ":united_arab_emirates:": "🇦â€ðŸ‡ª", + ":united_nations:": "🇺â€ðŸ‡³", + ":unlock:": "🔓", + ":up:": "🆙", + ":upside_down_face:": "🙃", + ":uruguay:": "🇺â€ðŸ‡¾", + ":us:": "🇺â€ðŸ‡¸", + ":us_outlying_islands:": "🇺â€ðŸ‡²", + ":us_virgin_islands:": "🇻â€ðŸ‡®", + ":uzbekistan:": "🇺â€ðŸ‡¿", + ":v:": "✌", + ":vampire:": "🧛", + ":vampire_man:": "🧛â€â™‚", + ":vampire_woman:": "🧛â€â™€", + ":vanuatu:": "🇻â€ðŸ‡º", + ":vatican_city:": "🇻â€ðŸ‡¦", + ":venezuela:": "🇻â€ðŸ‡ª", + ":vertical_traffic_light:": "🚦", + ":vhs:": "📼", + ":vibration_mode:": "📳", + ":video_camera:": "📹", + ":video_game:": "🎮", + ":vietnam:": "🇻â€ðŸ‡³", + ":violin:": "🎻", + ":virgo:": "â™", + ":volcano:": "🌋", + ":volleyball:": "ðŸ", + ":vomiting_face:": "🤮", + ":vs:": "🆚", + ":vulcan_salute:": "🖖", + ":waffle:": "🧇", + ":wales:": "ðŸ´â€ó §â€ó ¢â€ó ·â€ó ¬â€ó ³â€ó ¿", + ":walking:": "🚶", + ":walking_man:": "🚶â€â™‚", + ":walking_woman:": "🚶â€â™€", + ":wallis_futuna:": "🇼â€ðŸ‡«", + ":waning_crescent_moon:": "🌘", + ":waning_gibbous_moon:": "🌖", + ":warning:": "âš ", + ":wastebasket:": "🗑", + ":watch:": "⌚", + ":water_buffalo:": "ðŸƒ", + ":water_polo:": "🤽", + ":watermelon:": "ðŸ‰", + ":wave:": "👋", + ":wavy_dash:": "〰", + ":waxing_crescent_moon:": "🌒", + ":waxing_gibbous_moon:": "🌔", + ":wc:": "🚾", + ":weary:": "😩", + ":wedding:": "💒", + ":weight_lifting:": "ðŸ‹", + ":weight_lifting_man:": "ðŸ‹â€â™‚", + ":weight_lifting_woman:": "ðŸ‹â€â™€", + ":western_sahara:": "🇪â€ðŸ‡­", + ":whale:": "ðŸ³", + ":whale2:": "ðŸ‹", + ":wheel_of_dharma:": "☸", + ":wheelchair:": "♿", + ":white_check_mark:": "✅", + ":white_circle:": "⚪", + ":white_flag:": "ðŸ³", + ":white_flower:": "💮", + ":white_haired_man:": "👨â€ðŸ¦³", + ":white_haired_woman:": "👩â€ðŸ¦³", + ":white_heart:": "ðŸ¤", + ":white_large_square:": "⬜", + ":white_medium_small_square:": "â—½", + ":white_medium_square:": "â—»", + ":white_small_square:": "â–«", + ":white_square_button:": "🔳", + ":wilted_flower:": "🥀", + ":wind_chime:": "ðŸŽ", + ":wind_face:": "🌬", + ":wine_glass:": "ðŸ·", + ":wink:": "😉", + ":wolf:": "ðŸº", + ":woman:": "👩", + ":woman_artist:": "👩â€ðŸŽ¨", + ":woman_astronaut:": "👩â€ðŸš€", + ":woman_cartwheeling:": "🤸â€â™€", + ":woman_cook:": "👩â€ðŸ³", + ":woman_dancing:": "💃", + ":woman_facepalming:": "🤦â€â™€", + ":woman_factory_worker:": "👩â€ðŸ­", + ":woman_farmer:": "👩â€ðŸŒ¾", + ":woman_firefighter:": "👩â€ðŸš’", + ":woman_health_worker:": "👩â€âš•", + ":woman_in_manual_wheelchair:": "👩â€ðŸ¦½", + ":woman_in_motorized_wheelchair:": "👩â€ðŸ¦¼", + ":woman_judge:": "👩â€âš–", + ":woman_juggling:": "🤹â€â™€", + ":woman_mechanic:": "👩â€ðŸ”§", + ":woman_office_worker:": "👩â€ðŸ’¼", + ":woman_pilot:": "👩â€âœˆ", + ":woman_playing_handball:": "🤾â€â™€", + ":woman_playing_water_polo:": "🤽â€â™€", + ":woman_scientist:": "👩â€ðŸ”¬", + ":woman_shrugging:": "🤷â€â™€", + ":woman_singer:": "👩â€ðŸŽ¤", + ":woman_student:": "👩â€ðŸŽ“", + ":woman_teacher:": "👩â€ðŸ«", + ":woman_technologist:": "👩â€ðŸ’»", + ":woman_with_headscarf:": "🧕", + ":woman_with_probing_cane:": "👩â€ðŸ¦¯", + ":woman_with_turban:": "👳â€â™€", + ":womans_clothes:": "👚", + ":womans_hat:": "👒", + ":women_wrestling:": "🤼â€â™€", + ":womens:": "🚺", + ":woozy_face:": "🥴", + ":world_map:": "🗺", + ":worried:": "😟", + ":wrench:": "🔧", + ":wrestling:": "🤼", + ":writing_hand:": "âœ", + ":x:": "âŒ", + ":yarn:": "🧶", + ":yawning_face:": "🥱", + ":yellow_circle:": "🟡", + ":yellow_heart:": "💛", + ":yellow_square:": "🟨", + ":yemen:": "🇾â€ðŸ‡ª", + ":yen:": "💴", + ":yin_yang:": "☯", + ":yo_yo:": "🪀", + ":yum:": "😋", + ":zambia:": "🇿â€ðŸ‡²", + ":zany_face:": "🤪", + ":zap:": "âš¡", + ":zebra:": "🦓", + ":zero:": "0â€âƒ£", + ":zimbabwe:": "🇿â€ðŸ‡¼", + ":zipper_mouth_face:": "ðŸ¤", + ":zombie:": "🧟", + ":zombie_man:": "🧟â€â™‚", + ":zombie_woman:": "🧟â€â™€", + ":zzz:": "💤", + ":default:": "â—", + "a": "a", + "b": "b", + "c": "c", + "d": "d", + "e": "e", + "f": "f", + "g": "g", + "h": "h", + "i": "i", + "j": "j", + "k": "k", + "l": "l", + "m": "m", + "n": "n", + "o": "o", + "p": "p", + "q": "q", + "r": "r", + "s": "s", + "t": "t", + "u": "u", + "v": "v", + "w": "w", + "x": "x", + "y": "y", + "z": "z", + "A": "A", + "B": "B", + "C": "C", + "D": "D", + "E": "E", + "F": "F", + "G": "G", + "H": "H", + "I": "I", + "J": "J", + "K": "K", + "L": "L", + "M": "M", + "N": "N", + "O": "O", + "P": "P", + "Q": "Q", + "R": "R", + "S": "S", + "T": "T", + "U": "U", + "V": "V", + "W": "W", + "X": "X", + "Y": "Y", + "Z": "Z", + "0": "0", + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9", +} + + +if __name__ == "__main__": # pragma: no cover + import requests + import io + import re + import string + from pathlib import Path + from _internal_mitmproxy.tools.console.common import SYMBOL_MARK + + CHAR_MARKERS = list(string.ascii_letters) + list(string.digits) + EMOJI_SRC = ' ":{name}:": "{emoji_val}",' + CHAR_SRC = ' "{name}": "{emoji_val}",' + + out = io.StringIO() + + r = requests.get("https://api.github.com/emojis") + for name, url in r.json().items(): + codepoints = url.rpartition("/")[2].partition(".png")[0].split("-") + try: + emoji_val = "\u200d".join(chr(int(c, 16)) for c in codepoints) + except ValueError: + continue # some GitHub-specific emojis, e.g. "atom". + print(EMOJI_SRC.format(name=name, emoji_val=emoji_val), file=out) + + # add the default marker + print(EMOJI_SRC.format(name="default", emoji_val=SYMBOL_MARK), file=out) + + for c in CHAR_MARKERS: + print(CHAR_SRC.format(name=c, emoji_val=c), file=out) + + Path(__file__).write_text( + re.sub(r"(?<={\n)[\s\S]*(?=}\n)", lambda x: out.getvalue(), Path(__file__).read_text("utf8"), 1), + "utf8" + ) \ No newline at end of file diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/human.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/human.py new file mode 100644 index 00000000..1c9654b5 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/human.py @@ -0,0 +1,101 @@ +import datetime +import functools +import ipaddress +import time +import typing + + +SIZE_UNITS = { + "b": 1024 ** 0, + "k": 1024 ** 1, + "m": 1024 ** 2, + "g": 1024 ** 3, + "t": 1024 ** 4, +} + + +def pretty_size(size: int) -> str: + """Convert a number of bytes into a human-readable string. + + len(return value) <= 5 always holds true. + """ + s: float = size # type cast for mypy + if s < 1024: + return f"{s}b" + for suffix in ["k", "m", "g", "t"]: + s /= 1024 + if s < 99.95: + return f"{s:.1f}{suffix}" + if s < 1024 or suffix == "t": + return f"{s:.0f}{suffix}" + raise AssertionError + + +@functools.lru_cache() +def parse_size(s: typing.Optional[str]) -> typing.Optional[int]: + """ + Parse a size with an optional k/m/... suffix. + Invalid values raise a ValueError. For added convenience, passing `None` returns `None`. + """ + if s is None: + return None + try: + return int(s) + except ValueError: + pass + for i in SIZE_UNITS.keys(): + if s.endswith(i): + try: + return int(s[:-1]) * SIZE_UNITS[i] + except ValueError: + break + raise ValueError("Invalid size specification.") + + +def pretty_duration(secs: typing.Optional[float]) -> str: + formatters = [ + (100, "{:.0f}s"), + (10, "{:2.1f}s"), + (1, "{:1.2f}s"), + ] + if secs is None: + return "" + + for limit, formatter in formatters: + if secs >= limit: + return formatter.format(secs) + # less than 1 sec + return "{:.0f}ms".format(secs * 1000) + + +def format_timestamp(s): + s = time.localtime(s) + d = datetime.datetime.fromtimestamp(time.mktime(s)) + return d.strftime("%Y-%m-%d %H:%M:%S") + + +def format_timestamp_with_milli(s): + d = datetime.datetime.fromtimestamp(s) + return d.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + + +@functools.lru_cache() +def format_address(address: typing.Optional[tuple]) -> str: + """ + This function accepts IPv4/IPv6 tuples and + returns the formatted address string with port number + """ + if address is None: + return "" + try: + host = ipaddress.ip_address(address[0]) + if host.is_unspecified: + return "*:{}".format(address[1]) + if isinstance(host, ipaddress.IPv4Address): + return "{}:{}".format(str(host), address[1]) + # If IPv6 is mapped to IPv4 + elif host.ipv4_mapped: + return "{}:{}".format(str(host.ipv4_mapped), address[1]) + return "[{}]:{}".format(str(host), address[1]) + except ValueError: + return "{}:{}".format(address[0], address[1]) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/sliding_window.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/sliding_window.py new file mode 100644 index 00000000..cb31756d --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/sliding_window.py @@ -0,0 +1,30 @@ +import itertools +from typing import TypeVar, Iterable, Iterator, Tuple, Optional, List + +T = TypeVar('T') + + +def window(iterator: Iterable[T], behind: int = 0, ahead: int = 0) -> Iterator[Tuple[Optional[T], ...]]: + """ + Sliding window for an iterator. + + Example: + >>> for prev, i, nxt in window(range(10), 1, 1): + >>> print(prev, i, nxt) + + None 0 1 + 0 1 2 + 1 2 3 + 2 3 None + """ + # TODO: move into utils + iters: List[Iterator[Optional[T]]] = list(itertools.tee(iterator, behind + 1 + ahead)) + for i in range(behind): + iters[i] = itertools.chain((behind - i) * [None], iters[i]) + for i in range(ahead): + iters[-1 - i] = itertools.islice( + itertools.chain(iters[-1 - i], (ahead - i) * [None]), + (ahead - i), + None + ) + return zip(*iters) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/spec.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/spec.py new file mode 100644 index 00000000..3a022a7a --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/spec.py @@ -0,0 +1,22 @@ +import typing +from _internal_mitmproxy import flowfilter + + +def parse_spec(option: str) -> typing.Tuple[flowfilter.TFilter, str, str]: + """ + Parse strings in the following format: + + [/flow-filter]/subject/replacement + + """ + sep, rem = option[0], option[1:] + parts = rem.split(sep, 2) + if len(parts) == 2: + subject, replacement = parts + return flowfilter.match_all, subject, replacement + elif len(parts) == 3: + patt, subject, replacement = parts + flow_filter = flowfilter.parse(patt) + return flow_filter, subject, replacement + else: + raise ValueError("Invalid number of parameters (2 or 3 are expected)") diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/strutils.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/strutils.py new file mode 100644 index 00000000..0622b737 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/strutils.py @@ -0,0 +1,251 @@ +import codecs +import io +import re +from typing import Iterable, Union, overload + + +# https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading + +@overload +def always_bytes(str_or_bytes: None, *encode_args) -> None: + ... + + +@overload +def always_bytes(str_or_bytes: Union[str, bytes], *encode_args) -> bytes: + ... + + +def always_bytes(str_or_bytes: Union[None, str, bytes], *encode_args) -> Union[None, bytes]: + if str_or_bytes is None or isinstance(str_or_bytes, bytes): + return str_or_bytes + elif isinstance(str_or_bytes, str): + return str_or_bytes.encode(*encode_args) + else: + raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__)) + + +@overload +def always_str(str_or_bytes: None, *encode_args) -> None: + ... + + +@overload +def always_str(str_or_bytes: Union[str, bytes], *encode_args) -> str: + ... + + +def always_str(str_or_bytes: Union[None, str, bytes], *decode_args) -> Union[None, str]: + """ + Returns, + str_or_bytes unmodified, if + """ + if str_or_bytes is None or isinstance(str_or_bytes, str): + return str_or_bytes + elif isinstance(str_or_bytes, bytes): + return str_or_bytes.decode(*decode_args) + else: + raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__)) + + +# Translate control characters to "safe" characters. This implementation +# initially replaced them with the matching control pictures +# (http://unicode.org/charts/PDF/U2400.pdf), but that turned out to render badly +# with monospace fonts. We are back to "." therefore. +_control_char_trans = { + x: ord(".") # x + 0x2400 for unicode control group pictures + for x in range(32) +} +_control_char_trans[127] = ord(".") # 0x2421 +_control_char_trans_newline = _control_char_trans.copy() +for x in ("\r", "\n", "\t"): + del _control_char_trans_newline[ord(x)] + +_control_char_trans = str.maketrans(_control_char_trans) +_control_char_trans_newline = str.maketrans(_control_char_trans_newline) + + +def escape_control_characters(text: str, keep_spacing=True) -> str: + """ + Replace all unicode C1 control characters from the given text with a single "." + + Args: + keep_spacing: If True, tabs and newlines will not be replaced. + """ + if not isinstance(text, str): + raise ValueError("text type must be unicode but is {}".format(type(text).__name__)) + + trans = _control_char_trans_newline if keep_spacing else _control_char_trans + return text.translate(trans) + + +def bytes_to_escaped_str(data: bytes, keep_spacing: bool = False, escape_single_quotes: bool = False) -> str: + """ + Take bytes and return a safe string that can be displayed to the user. + + Single quotes are always escaped, double quotes are never escaped: + "'" + bytes_to_escaped_str(...) + "'" + gives a valid Python string. + + Args: + keep_spacing: If True, tabs and newlines will not be escaped. + """ + + if not isinstance(data, bytes): + raise ValueError(f"data must be bytes, but is {data.__class__.__name__}") + # We always insert a double-quote here so that we get a single-quoted string back + # https://stackoverflow.com/questions/29019340/why-does-python-use-different-quotes-for-representing-strings-depending-on-their + ret = repr(b'"' + data).lstrip("b")[2:-1] + if not escape_single_quotes: + ret = re.sub(r"(? bytes: + """ + Take an escaped string and return the unescaped bytes equivalent. + + Raises: + ValueError, if the escape sequence is invalid. + """ + if not isinstance(data, str): + raise ValueError(f"data must be str, but is {data.__class__.__name__}") + + # This one is difficult - we use an undocumented Python API here + # as per http://stackoverflow.com/a/23151714/934719 + return codecs.escape_decode(data)[0] # type: ignore + + +def is_mostly_bin(s: bytes) -> bool: + if not s or len(s) == 0: + return False + + return sum( + i < 9 or 13 < i < 32 or 126 < i + for i in s[:100] + ) / len(s[:100]) > 0.3 + + +def is_xml(s: bytes) -> bool: + for char in s: + if char in (9, 10, 32): # is space? + continue + return char == 60 # is a "<"? + return False + + +def clean_hanging_newline(t): + """ + Many editors will silently add a newline to the final line of a + document (I'm looking at you, Vim). This function fixes this common + problem at the risk of removing a hanging newline in the rare cases + where the user actually intends it. + """ + if t and t[-1] == "\n": + return t[:-1] + return t + + +def hexdump(s): + """ + Returns: + A generator of (offset, hex, str) tuples + """ + for i in range(0, len(s), 16): + offset = f"{i:0=10x}" + part = s[i:i + 16] + x = " ".join(f"{i:0=2x}" for i in part) + x = x.ljust(47) # 16*2 + 15 + part_repr = always_str(escape_control_characters( + part.decode("ascii", "replace").replace("\ufffd", "."), + False + )) + yield (offset, x, part_repr) + + +def _move_to_private_code_plane(matchobj): + return chr(ord(matchobj.group(0)) + 0xE000) + + +def _restore_from_private_code_plane(matchobj): + return chr(ord(matchobj.group(0)) - 0xE000) + + +NO_ESCAPE = r"(?>> split_special_areas( + >>> "test /* don't modify me */ foo", + >>> [r"/\\*[\\s\\S]*?\\*/"]) # (regex matching comments) + ["test ", "/* don't modify me */", " foo"] + + "".join(split_special_areas(x, ...)) == x always holds true. + """ + return re.split( + "({})".format("|".join(area_delimiter)), + data, + flags=re.MULTILINE + ) + + +def escape_special_areas( + data: str, + area_delimiter: Iterable[str], + control_characters, +): + """ + Escape all control characters present in special areas with UTF8 symbols + in the private use plane (U+E000 t+ ord(char)). + This is useful so that one can then use regex replacements on the resulting string without + interfering with special areas. + + control_characters must be 0 < ord(x) < 256. + + Example: + + >>> print(x) + if (true) { console.log('{}'); } + >>> x = escape_special_areas(x, "{", ["'" + SINGLELINE_CONTENT + "'"]) + >>> print(x) + if (true) { console.log('�}'); } + >>> x = re.sub(r"\\s*{\\s*", " {\n ", x) + >>> x = unescape_special_areas(x) + >>> print(x) + if (true) { + console.log('{}'); } + """ + buf = io.StringIO() + parts = split_special_areas(data, area_delimiter) + rex = re.compile(fr"[{control_characters}]") + for i, x in enumerate(parts): + if i % 2: + x = rex.sub(_move_to_private_code_plane, x) + buf.write(x) + return buf.getvalue() + + +def unescape_special_areas(data: str): + """ + Invert escape_special_areas. + + x == unescape_special_areas(escape_special_areas(x)) always holds true. + """ + return re.sub(r"[\ue000-\ue0ff]", _restore_from_private_code_plane, data) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/typecheck.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/typecheck.py new file mode 100644 index 00000000..501fcb07 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/typecheck.py @@ -0,0 +1,90 @@ +import typing + +Type = typing.Union[ + typing.Any # anything more elaborate really fails with mypy at the moment. +] + + +def sequence_type(typeinfo: typing.Type[typing.List]) -> Type: + """Return the type of a sequence, e.g. typing.List""" + return typeinfo.__args__[0] # type: ignore + + +def tuple_types(typeinfo: typing.Type[typing.Tuple]) -> typing.Sequence[Type]: + """Return the types of a typing.Tuple""" + return typeinfo.__args__ # type: ignore + + +def union_types(typeinfo: typing.Type[typing.Tuple]) -> typing.Sequence[Type]: + """return the types of a typing.Union""" + return typeinfo.__args__ # type: ignore + + +def mapping_types(typeinfo: typing.Type[typing.Mapping]) -> typing.Tuple[Type, Type]: + """return the types of a mapping, e.g. typing.Dict""" + return typeinfo.__args__ # type: ignore + + +def check_option_type(name: str, value: typing.Any, typeinfo: Type) -> None: + """ + Check if the provided value is an instance of typeinfo and raises a + TypeError otherwise. This function supports only those types required for + options. + """ + e = TypeError("Expected {} for {}, but got {}.".format( + typeinfo, + name, + type(value) + )) + + typename = str(typeinfo) + + if typename.startswith("typing.Union") or typename.startswith("typing.Optional"): + for T in union_types(typeinfo): + try: + check_option_type(name, value, T) + except TypeError: + pass + else: + return + raise e + elif typename.startswith("typing.Tuple"): + types = tuple_types(typeinfo) + if not isinstance(value, (tuple, list)): + raise e + if len(types) != len(value): + raise e + for i, (x, T) in enumerate(zip(value, types)): + check_option_type(f"{name}[{i}]", x, T) + return + elif typename.startswith("typing.Sequence"): + T = sequence_type(typeinfo) + if not isinstance(value, (tuple, list)): + raise e + for v in value: + check_option_type(name, v, T) + elif typename.startswith("typing.IO"): + if hasattr(value, "read"): + return + else: + raise e + elif typename.startswith("typing.Any"): + return + elif not isinstance(value, typeinfo): + if typeinfo is float and isinstance(value, int): + return + raise e + + +def typespec_to_str(typespec: typing.Any) -> str: + if typespec in (str, int, float, bool): + t = typespec.__name__ + elif typespec == typing.Optional[str]: + t = 'optional str' + elif typespec == typing.Sequence[str]: + t = 'sequence of str' + elif typespec == typing.Optional[int]: + t = 'optional int' + else: + raise NotImplementedError + return t diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/vt_codes.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/vt_codes.py new file mode 100644 index 00000000..591b72e3 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/utils/vt_codes.py @@ -0,0 +1,51 @@ +""" +This module provides a method to detect if a given file object supports virtual terminal escape codes. +""" +import os +import sys +from typing import IO + +if os.name == "nt": + from ctypes import byref, windll # type: ignore + from ctypes.wintypes import BOOL, DWORD, HANDLE, LPDWORD + + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + + # https://docs.microsoft.com/de-de/windows/console/getstdhandle + GetStdHandle = windll.kernel32.GetStdHandle + GetStdHandle.argtypes = [DWORD] + GetStdHandle.restype = HANDLE + + # https://docs.microsoft.com/de-de/windows/console/getconsolemode + GetConsoleMode = windll.kernel32.GetConsoleMode + GetConsoleMode.argtypes = [HANDLE, LPDWORD] + GetConsoleMode.restype = BOOL + + # https://docs.microsoft.com/de-de/windows/console/setconsolemode + SetConsoleMode = windll.kernel32.SetConsoleMode + SetConsoleMode.argtypes = [HANDLE, DWORD] + SetConsoleMode.restype = BOOL + + def ensure_supported(f: IO[str]) -> bool: + if not f.isatty(): + return False + if f == sys.stdout: + h = STD_OUTPUT_HANDLE + elif f == sys.stderr: + h = STD_ERROR_HANDLE + else: + return False + + handle = GetStdHandle(h) + console_mode = DWORD() + ok = GetConsoleMode(handle, byref(console_mode)) + if not ok: + return False + + ok = SetConsoleMode(handle, console_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING) + return ok +else: + def ensure_supported(f: IO[str]) -> bool: + return f.isatty() diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/version.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/version.py new file mode 100644 index 00000000..9f20020c --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/version.py @@ -0,0 +1,53 @@ +import os +import subprocess +import sys + +VERSION = "8.0.0" +_internal_mitmproxy = "_internal_mitmproxy " + VERSION + +# Serialization format version. This is displayed nowhere, it just needs to be incremented by one +# for each change in the file format. +FLOW_FORMAT_VERSION = 15 + + +def get_dev_version() -> str: + """ + Return a detailed version string, sourced either from VERSION or obtained dynamically using git. + """ + + _internal_mitmproxy_version = VERSION + + here = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + try: + # Check that we're in the _internal_mitmproxy repository: https://github.com/_internal_mitmproxy/_internal_mitmproxy/issues/3987 + # cb0e3287090786fad566feb67ac07b8ef361b2c3 is the first _internal_mitmproxy commit. + subprocess.run( + ['git', 'cat-file', '-e', 'cb0e3287090786fad566feb67ac07b8ef361b2c3'], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + cwd=here, + check=True) + git_describe = subprocess.check_output( + ['git', 'describe', '--tags', '--long'], + stderr=subprocess.STDOUT, + cwd=here, + ) + last_tag, tag_dist_str, commit = git_describe.decode().strip().rsplit("-", 2) + commit = commit.lstrip("g")[:7] + tag_dist = int(tag_dist_str) + except Exception: + pass + else: + # Add commit info for non-tagged releases + if tag_dist > 0: + _internal_mitmproxy_version += f" (+{tag_dist}, commit {commit})" + + # PyInstaller build indicator, if using precompiled binary + if getattr(sys, 'frozen', False): + _internal_mitmproxy_version += " binary" + + return _internal_mitmproxy_version + + +if __name__ == "__main__": # pragma: no cover + print(VERSION) diff --git a/scalpel/src/main/resources/python3-10/_internal_mitmproxy/websocket.py b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/websocket.py new file mode 100644 index 00000000..cb0f45af --- /dev/null +++ b/scalpel/src/main/resources/python3-10/_internal_mitmproxy/websocket.py @@ -0,0 +1,166 @@ +""" +_internal_mitmproxy used to have its own WebSocketFlow type until _internal_mitmproxy 6, but now WebSocket connections now are represented +as HTTP flows as well. They can be distinguished from regular HTTP requests by having the +`_internal_mitmproxy.http.HTTPFlow.websocket` attribute set. + +This module only defines the classes for individual `WebSocketMessage`s and the `WebSocketData` container. +""" +import time +import warnings +from typing import List, Tuple, Union +from typing import Optional + +from _internal_mitmproxy import stateobject +from _internal_mitmproxy.coretypes import serializable +from wsproto.frame_protocol import Opcode + +WebSocketMessageState = Tuple[int, bool, bytes, float, bool, bool] + + +class WebSocketMessage(serializable.Serializable): + """ + A single WebSocket message sent from one peer to the other. + + Fragmented WebSocket messages are reassembled by _internal_mitmproxy and then + represented as a single instance of this class. + + The [WebSocket RFC](https://tools.ietf.org/html/rfc6455) specifies both + text and binary messages. To avoid a whole class of nasty type confusion bugs, + _internal_mitmproxy stores all message contents as `bytes`. If you need a `str`, you can access the `text` property + on text messages: + + >>> if message.is_text: + >>> text = message.text + """ + + from_client: bool + """True if this messages was sent by the client.""" + type: Opcode + """ + The message type, as per RFC 6455's [opcode](https://tools.ietf.org/html/rfc6455#section-5.2). + + _internal_mitmproxy currently only exposes messages assembled from `TEXT` and `BINARY` frames. + """ + content: bytes + """A byte-string representing the content of this message.""" + timestamp: float + """Timestamp of when this message was received or created.""" + dropped: bool + """True if the message has not been forwarded by _internal_mitmproxy, False otherwise.""" + injected: bool + """True if the message was injected and did not originate from a client/server, False otherwise""" + + def __init__( + self, + type: Union[int, Opcode], + from_client: bool, + content: bytes, + timestamp: Optional[float] = None, + dropped: bool = False, + injected: bool = False, + ) -> None: + self.from_client = from_client + self.type = Opcode(type) + self.content = content + self.timestamp: float = timestamp or time.time() + self.dropped = dropped + self.injected = injected + + @classmethod + def from_state(cls, state: WebSocketMessageState): + return cls(*state) + + def get_state(self) -> WebSocketMessageState: + return int(self.type), self.from_client, self.content, self.timestamp, self.dropped, self.injected + + def set_state(self, state: WebSocketMessageState) -> None: + typ, self.from_client, self.content, self.timestamp, self.dropped, self.injected = state + self.type = Opcode(typ) + + def __repr__(self): + if self.type == Opcode.TEXT: + return repr(self.content.decode(errors="replace")) + else: + return repr(self.content) + + @property + def is_text(self) -> bool: + """ + `True` if this message is assembled from WebSocket `TEXT` frames, + `False` if it is assembled from `BINARY` frames. + """ + return self.type == Opcode.TEXT + + def drop(self): + """Drop this message, i.e. don't forward it to the other peer.""" + self.dropped = True + + def kill(self): # pragma: no cover + """A deprecated alias for `.drop()`.""" + warnings.warn("WebSocketMessage.kill() is deprecated, use .drop() instead.", DeprecationWarning, stacklevel=2) + self.drop() + + @property + def text(self) -> str: + """ + The message content as text. + + This attribute is only available if `WebSocketMessage.is_text` is `True`. + + *See also:* `WebSocketMessage.content` + """ + if self.type != Opcode.TEXT: + raise AttributeError(f"{self.type.name.title()} WebSocket frames do not have a 'text' attribute.") + + return self.content.decode() + + @text.setter + def text(self, value: str) -> None: + if self.type != Opcode.TEXT: + raise AttributeError(f"{self.type.name.title()} WebSocket frames do not have a 'text' attribute.") + + self.content = value.encode() + + +class WebSocketData(stateobject.StateObject): + """ + A data container for everything related to a single WebSocket connection. + This is typically accessed as `_internal_mitmproxy.http.HTTPFlow.websocket`. + """ + + messages: List[WebSocketMessage] + """All `WebSocketMessage`s transferred over this connection.""" + + closed_by_client: Optional[bool] = None + """ + `True` if the client closed the connection, + `False` if the server closed the connection, + `None` if the connection is active. + """ + close_code: Optional[int] = None + """[Close Code](https://tools.ietf.org/html/rfc6455#section-7.1.5)""" + close_reason: Optional[str] = None + """[Close Reason](https://tools.ietf.org/html/rfc6455#section-7.1.6)""" + + timestamp_end: Optional[float] = None + """*Timestamp:* WebSocket connection closed.""" + + _stateobject_attributes = dict( + messages=List[WebSocketMessage], + closed_by_client=bool, + close_code=int, + close_reason=str, + timestamp_end=float, + ) + + def __init__(self): + self.messages = [] + + def __repr__(self): + return f"" + + @classmethod + def from_state(cls, state): + d = WebSocketData() + d.set_state(state) + return d diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/__init__.py b/scalpel/src/main/resources/python3-10/pyscalpel/__init__.py new file mode 100644 index 00000000..2942ffe6 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/__init__.py @@ -0,0 +1,46 @@ +""" +This is a module providing tools to handle Burp HTTP traffic through the use of the Scalpel extension. + +It provides many utilities to manipulate HTTP requests, responses and converting data. +""" + +from pyscalpel.http import Request, Response, Flow +from pyscalpel.edit import editor +from pyscalpel.burp_utils import ctx as _context +from pyscalpel.java.scalpel_types import Context +from pyscalpel.logger import Logger, logger +from pyscalpel.events import MatchEvent +from . import http +from . import java +from . import encoding +from . import utils +from . import burp_utils +from . import venv +from . import edit + +ctx: Context = _context +"""The Scalpel Python execution context + +Contains the Burp Java API object, the venv directory, the user script path, +the path to the file loading the user script and a logging object +""" + + +__all__ = [ + "http", + "java", + "encoding", + "utils", + "burp_utils", + "venv", + "edit", + "Request", + "Response", + "Flow", + "ctx", + "Context", + "MatchEvent", + "editor", + "logger", + "Logger", +] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/_framework.py b/scalpel/src/main/resources/python3-10/pyscalpel/_framework.py new file mode 100644 index 00000000..1361a801 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/_framework.py @@ -0,0 +1,508 @@ +import traceback +from sys import _getframe +import inspect +from typing import Callable, TypeVar, cast, Any, TypedDict +import sys +from functools import wraps +from os.path import dirname +import os +import glob + + +# Define a debug logger to be able to debug cases where the logger is not initialized +# or the path isn't well set +# or the _framework is invoked by pdoc or other tools +# +# Output will be printed to the terminal +class DebugLogger: + """ + Debug logger to use if for some reason the logger is not initialized + or pyscalpel.logger cannot be imported. + """ + + def all(self, msg: str): + print(msg) + + def trace(self, msg: str): + print(msg) + + def debug(self, msg: str): + print(msg) + + def info(self, msg: str): + print(msg) + + def warn(self, msg: str): + print(msg) + + def fatal(self, msg: str): + print(msg) + + def error(self, msg: str): + print(msg, file=sys.stderr) + + +logger = DebugLogger() + + +# Copies of functions from pyscalpel.venv +# We duplicate them here because importing pyscalpel.venv triggers pyscalpel/__init__.py which imports mitmproxy +# Hence, mitmproxy would be imported **BEFORE** the venv is activated. +# In some distributions (kali), this results in a crash because the system's mitmproxy version is buggy. (PyO3 errors) +_old_prefix = sys.prefix +_old_exec_prefix = sys.exec_prefix + + +def deactivate() -> None: + """Deactivates the current virtual environment.""" + if "_OLD_VIRTUAL_PATH" in os.environ: + os.environ["PATH"] = os.environ["_OLD_VIRTUAL_PATH"] + del os.environ["_OLD_VIRTUAL_PATH"] + if "_OLD_VIRTUAL_PYTHONHOME" in os.environ: + os.environ["PYTHONHOME"] = os.environ["_OLD_VIRTUAL_PYTHONHOME"] + del os.environ["_OLD_VIRTUAL_PYTHONHOME"] + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + sys.prefix = _old_prefix + sys.exec_prefix = _old_exec_prefix + + +def activate(pkg_path: str | None) -> None: + """Activates the virtual environment at the given path.""" + deactivate() + + if pkg_path is None: + return + + virtual_env = os.path.abspath(pkg_path) + os.environ["_OLD_VIRTUAL_PATH"] = os.environ.get("PATH", "") + os.environ["VIRTUAL_ENV"] = virtual_env + + old_pythonhome = os.environ.pop("PYTHONHOME", None) + if old_pythonhome: + os.environ["_OLD_VIRTUAL_PYTHONHOME"] = old_pythonhome + + if os.name == "nt": + site_packages_paths = os.path.join(virtual_env, "Lib", "site-packages") + else: + site_packages_paths = glob.glob( + os.path.join(virtual_env, "lib", "python*", "site-packages") + ) + + if not site_packages_paths: + raise RuntimeError( + f"No 'site-packages' directory found in virtual environment at {virtual_env}" + ) + + site_packages = site_packages_paths[0] + sys.path.insert(0, site_packages) + sys.prefix = virtual_env + sys.exec_prefix = virtual_env + + +VENV = None + + +try: + VENV = __scalpel__["venv"] # type: ignore pylint: disable=undefined-variable + + activate(VENV) + + from pyscalpel.java.scalpel_types import Context + from pyscalpel.logger import logger as _logger + + ctx: Context = cast(Context, __scalpel__) # type: ignore pylint: disable=undefined-variable + + logger = _logger + + logger.all("Python: Loading _framework.py ...") + + import pyscalpel.venv + + # Update forwarded values + pyscalpel.venv._old_exec_prefix = _old_exec_prefix + pyscalpel.venv._old_prefix = _old_prefix + + # import debugpy + + # debugpy.listen(("localhost", 5678)) + + # Import the globals module to set the ctx + import pyscalpel._globals + import pyscalpel + + # Set the logger in the globals module + pyscalpel._globals.ctx = ctx # pylint: disable=protected-access + pyscalpel.ctx = ctx + + # Get the user script path from the JEP initialized variable + user_script: str = ctx["user_script"] + + # Get utils to dynamically import the user script in a convenient way + import importlib.util + + # specify the absolute path of the script you want to import + path = user_script + + sys.path.append(dirname(path)) + + # create a module spec based on the script path + spec = importlib.util.spec_from_file_location("scalpel_user_module", path) + + # Assert that the provided path can be loaded + assert spec is not None + assert spec.loader is not None + + # create a module based on the spec + user_module = importlib.util.module_from_spec(spec) + + # load the user_module into memory + spec.loader.exec_module(user_module) + + from pyscalpel.burp_utils import IHttpRequest, IHttpResponse + from pyscalpel.java.burp.http_service import IHttpService + from pyscalpel.http import Request, Response, Flow + from pyscalpel.events import MatchEvent + from pyscalpel.utils import removeprefix + + # Declare convenient types for the callbacks + CallbackReturn = TypeVar("CallbackReturn", Request, Response, bytes) | None + + CallbackType = Callable[..., CallbackReturn] + + # Get all the callable objects from the user module + callable_objs: dict[str, Callable] = { + name: obj for name, obj in inspect.getmembers(user_module) if callable(obj) + } + + match_callback: Callable[[Flow, MatchEvent], bool] = callable_objs.get("match") or ( + lambda _, __: True + ) + + class CallableData(TypedDict): + name: str + annotations: dict[str, Any] + + def _get_callables() -> list[CallableData]: + logger.trace("Python: _get_callables() called") + # Also return the annotations because they contain the editor mode (hex,raw) + # Annotations are a dict so they will be converted to HashMap + # https://github.com/ninia/jep/wiki/How-Jep-Works#objects:~:text=Dict%20%2D%3E%20java.util.HashMap + return [ + {"name": name, "annotations": getattr(hook, "__annotations__", {})} + for name, hook in callable_objs.items() + ] + + def call_match_callback(*args) -> bool: + """Calls the match callback with the correct parameters. + + Returns: + bool: The match callback result + """ + params_len = len(inspect.signature(match_callback).parameters) + filtered_args = args[:params_len] + return match_callback(*filtered_args) + + def fun_name(frame=1): + """Returns the name of the caller function + + Args: + frame (int, optional): The frame to get the name from. Defaults to 1. + """ + return _getframe(frame).f_code.co_name + + def _try_wrap(callback: CallbackType) -> CallbackType: + """Wraps a callback in a try catch block and add some debug logs. + + Args: + callback (CallbackType): The callback to wrap + + Returns: + CallbackType: The wrapped callback + """ + logger.trace("Python: _try_wrap() called") + + # Define the wrapper function + @wraps(callback) + def _wrapped_cb(*args, **kwargs): + try: + logger.trace(f"Python: _wrapped_cb() for {callback.__name__} called") + return callback(*args, **kwargs) + except Exception as ex: # pylint: disable=broad-except + msg = f"Python: {callback.__name__}() error:\n{ex}\n{traceback.format_exc()}" + logger.error(msg) + print(msg, file=sys.stderr) + raise ex + + # Replace the callback with the wrapped one + return _wrapped_cb + + def _try_if_present( + callback: Callable[..., CallbackReturn] + ) -> Callable[..., CallbackReturn]: + """Decorator to return a None lambda when the callback is not present in the user script. + + Args: + callback (Callable[..., CallbackReturn]): The callback to wrap + + Returns: + Callable[..., CallbackReturn]: The wrapped callback + """ + logger.trace(f"Python: _try_if_present({callback.__name__}) called") + + # Remove the leading underscore from the callback name + name = removeprefix(callback.__name__, "_") + + # Get the user callback from the user script's callable objects + user_cb = callable_objs.get(name) + + # Ensure the user callback is present + if user_cb is not None: + logger.trace(f"Python: {name}() is present") + + # Wrap the user callback in a try catch block and return it + # @_try_wrap + @wraps(user_cb) + def new_cb(*args, **kwargs) -> CallbackReturn: + return callback(*args, **kwargs, callback=user_cb) + + # Return the wrapped callback + return new_cb + + logger.trace(f"Python: {name}() is not present") + + # Ignore the callback. + return lambda *_, **__: None + + _HookReturnTp = TypeVar("_HookReturnTp") + + def _hook_type_check( + hook_name: str, expected_type: type[_HookReturnTp], received: Any + ) -> _HookReturnTp: + if received is not None and not isinstance(received, expected_type): + raise TypeError( + f"{hook_name}() returned type {type(received)} instead of {expected_type}" + ) + return received + + @_try_if_present + def _request( + req: IHttpRequest, service: IHttpService, callback: CallbackType = ... + ) -> IHttpRequest | None: + """Wrapper for the request callback + + Args: + req (IHttpRequest): The request object + callback (CallbackType, optional): The user callback. + + Returns: + IHttpRequest | None: The modified request object or None for an unmodified request + """ + py_req = Request.from_burp(req, service) + + flow = Flow( + scheme=py_req.scheme, host=py_req.host, port=py_req.port, request=py_req + ) + if not call_match_callback(flow, "request"): + return None + + # Call the user callback + processed_req = _hook_type_check(callback.__name__, Request, callback(py_req)) + + # Convert the request to a Burp request + return processed_req and processed_req.to_burp() + + @_try_if_present + def _response( + res: IHttpResponse, service: IHttpService, callback: CallbackType = ... + ) -> IHttpResponse | None: + """Wrapper for the response callback + + Args: + res (IHttpResponse): The response object + callback (CallbackType, optional): The user callback. + + Returns: + IHttpResponse | None: The modified response object or None for an unmodified response + """ + py_res = Response.from_burp(res, service) + + flow = Flow( + scheme=py_res.scheme, + host=py_res.host, + port=py_res.port, + request=py_res.request, + response=py_res, + ) + if not call_match_callback(flow, "response"): + return None + + result_res = _hook_type_check(callback.__name__, Response, callback(py_res)) + + return result_res.to_burp() if result_res is not None else None + + def _req_edit_in( + req: IHttpRequest, service: IHttpService, callback_suffix: str = ... + ) -> bytes | None: + """Wrapper for the request edit callback + + Args: + req (IHttpRequest): The request object + service (IHttpService): The service object, contains target IP and port + callback_suffix (str): The editor's name + Returns: + bytes | None: The bytes to display in the editor or None for a disabled editor + """ + logger.trace(f"Python: _req_edit_in -> {callback_suffix}") + callback = callable_objs.get("req_edit_in" + callback_suffix) + if callback is None: + return None + + py_req = Request.from_burp(req, service) + + flow = Flow( + scheme=py_req.scheme, host=py_req.host, port=py_req.port, request=py_req + ) + if not call_match_callback(flow, "req_edit_in"): + return None + + logger.trace(f"Python: calling {callback.__name__}") + + # Call the user callback, ensure the type is correct and return the bytes to display in the editor + return _hook_type_check(callback.__name__, bytes, callback(py_req)) + + def _req_edit_out( + req: IHttpRequest, + service: IHttpService, + text: list[int], + callback_suffix: str = ..., + ) -> IHttpRequest | None: + """Wrapper for the request edit callback + + Args: + req (IHttpRequest): The request object + service (IHttpService): The network service (contains target IP and port) + text (list[int]): The editor content + callback (CallbackType, optional): The user callback. + + Returns: + bytes | None: The bytes to construct the new request from + or None for an unmodified request + """ + logger.trace(f"Python: _req_edit_out -> {callback_suffix}") + callback = callable_objs.get("req_edit_out" + callback_suffix) + if callback is None: + return None + + py_req = Request.from_burp(req, service) + + flow = Flow( + scheme=py_req.scheme, + host=py_req.host, + port=py_req.port, + request=py_req, + text=bytes(text), + ) + if not call_match_callback(flow, "req_edit_out"): + return None + + logger.trace(f"Python: calling {callback.__name__}") + # Call the user callback and return the bytes to construct the new request from + result = _hook_type_check( + callback.__name__, Request, callback(py_req, bytes(text)) + ) + return result and result.to_burp() + + # @_try_wrap + def _res_edit_in( + res: IHttpResponse, + request: IHttpRequest, + service: IHttpService, + callback_suffix: str = ..., + ) -> bytes | None: + """Wrapper for the response edit callback + + Args: + res (IHttpResponse): The response object + request (IHttpRequest): The initiating request + service (IHttpService): The network service (contains target IP and port) + callback (CallbackType, optional): The user callback. + + Returns: + bytes | None: The bytes to display in the editor or None for a disabled editor + """ + logger.trace(f"Python: _res_edit_in -> {callback_suffix}") + callback = callable_objs.get("res_edit_in" + callback_suffix) + if callback is None: + return None + + py_res = Response.from_burp(res, service=service, request=request) + + flow = Flow( + scheme=py_res.scheme, + host=py_res.host, + port=py_res.port, + request=py_res.request, + response=py_res, + ) + if not call_match_callback(flow, "res_edit_in"): + return None + + logger.trace(f"Python: calling {callback.__name__}") + # Call the user callback and return the bytes to display in the editor + return _hook_type_check(callback.__name__, bytes, callback(py_res)) + + # @_try_wrap + def _res_edit_out( + res: IHttpResponse, + req: IHttpRequest, + service: IHttpService, + text: list[int], + callback_suffix: str = ..., + ) -> IHttpResponse | None: + """Wrapper for the response edit callback + + Args: + res (IHttpResponse): The response object + req (IHttpRequest): The initiating request + service (IHttpService): The network service (contains target IP and port) + text (list[int]): The editor content + callback (CallbackType, optional): The user callback. + + Returns: + bytes | None: The bytes to construct the new response from + or None for an unmodified response + """ + logger.trace(f"Python: _res_edit_out -> {callback_suffix}") + callback = callable_objs.get("res_edit_out" + callback_suffix) + if callback is None: + return None + + py_res = Response.from_burp(res, service=service, request=req) + + flow = Flow( + scheme=py_res.scheme, + host=py_res.host, + port=py_res.port, + request=py_res.request, + response=py_res, + text=bytes(text), + ) + if not call_match_callback(flow, "res_edit_out"): + return None + + logger.trace(f"Python: calling {callback.__name__}") + # Call the user callback and return the bytes to construct the new response from + result = _hook_type_check( + callback.__name__, Response, callback(py_res, bytes(text)) + ) + return result and result.to_burp() + + logger.all("Python: Loaded _framework.py") + +except Exception as global_ex: # pylint: disable=broad-except + # Global generic exception handler to ensure the error is logged and visible to the user. + logger.fatal(f"Couldn't load script:\n{global_ex}\n{traceback.format_exc()}") + logger.all("Python: Failed loading _framework.py") + raise global_ex diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/_globals.py b/scalpel/src/main/resources/python3-10/pyscalpel/_globals.py new file mode 100644 index 00000000..a4f1e0d4 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/_globals.py @@ -0,0 +1,3 @@ +from pyscalpel.java.scalpel_types import Context + +ctx: Context = Context() # type: ignore diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/burp.py b/scalpel/src/main/resources/python3-10/pyscalpel/burp.py new file mode 100644 index 00000000..c8bf520c --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/burp.py @@ -0,0 +1,29 @@ +from pyscalpel import Request, ctx +from threading import Thread + + +def send_to_repeater( + req: Request, title: str | None = None +) -> None: # pragma: no cover + """Sends an HTTP request to the Burp Repeater tool. + + The request will be displayed in the user interface using a default tab index, but will not be sent until the user initiates this action. + + Args: + req (Request): The full HTTP request. + + title (str | None, optional): An optional caption which will appear on the Repeater tab containing the request. + If this value is None then a default tab index will be displayed. + """ + # Convert request to Burp format. + breq = req.to_burp() + + # Directly access the Montoya API Java object to send the request to repeater + repeater = ctx["API"].repeater() + + # waiting for sendToRepeater while intercepting a request causes a Burp deadlock + if title is None: + Thread(target=lambda: repeater.sendToRepeater(breq)).start() + else: + # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/repeater/Repeater.html#sendToRepeater(burp.api.montoya.http.message.requests.HttpRequest) + Thread(target=lambda: repeater.sendToRepeater(breq, title)).start() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/burp_utils.py b/scalpel/src/main/resources/python3-10/pyscalpel/burp_utils.py new file mode 100644 index 00000000..ef495a22 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/burp_utils.py @@ -0,0 +1,81 @@ +from typing import TypeVar, cast +from functools import singledispatch +from collections.abc import Iterable + +import pyscalpel._globals +from pyscalpel.java.burp.http_request import IHttpRequest, HttpRequest +from pyscalpel.java.burp.http_response import IHttpResponse, HttpResponse +from pyscalpel.java.burp.byte_array import IByteArray, ByteArray +from pyscalpel.java.burp.http_parameter import IHttpParameter, HttpParameter +from pyscalpel.java.bytes import JavaBytes +from pyscalpel.java.scalpel_types.utils import PythonUtils +from pyscalpel.encoding import always_bytes, urldecode, urlencode_all + + +ctx = pyscalpel._globals.ctx + + +HttpRequestOrResponse = TypeVar("HttpRequestOrResponse", IHttpRequest, IHttpResponse) + +ByteArraySerialisable = TypeVar("ByteArraySerialisable", IHttpRequest, IHttpResponse) + +ByteArrayConvertible = TypeVar( + "ByteArrayConvertible", bytes, JavaBytes, list[int], str, bytearray +) + + +@singledispatch +def new_response(obj: ByteArrayConvertible) -> IHttpResponse: # pragma: no cover + """Create a new HttpResponse from the given bytes""" + return HttpResponse.httpResponse(byte_array(obj)) + + +@new_response.register +def _new_response(obj: IByteArray) -> IHttpResponse: # pragma: no cover + return HttpResponse.httpResponse(obj) + + +@singledispatch +def new_request(obj: ByteArrayConvertible) -> IHttpRequest: # pragma: no cover + """Create a new HttpRequest from the given bytes""" + return HttpRequest.httpRequest(byte_array(obj)) + + +@new_request.register +def _new_request(obj: IByteArray) -> IHttpRequest: # pragma: no cover + return HttpRequest.httpRequest(obj) + + +@singledispatch +def byte_array( + _bytes: bytes | JavaBytes | list[int] | bytearray, +) -> IByteArray: # pragma: no cover + """Create a new :class:`IByteArray` from the given bytes-like obbject""" + # Handle buggy bytes casting + # This is needed because Python will _sometimes_ try + # to interpret bytes as a an integer when passing to ByteArray.byteArray() and crash like this: + # + # TypeError: Error converting parameter 1: 'bytes' object cannot be interpreted as an integer + # + # Restarting Burp fixes the issue when it happens, so to avoid unstable behaviour + # we explcitely convert the bytes to a PyJArray of Java byte + cast_value = cast(JavaBytes, PythonUtils.toJavaBytes(bytes(_bytes))) + return ByteArray.byteArray(cast_value) + + +@byte_array.register +def _byte_array_str(string: str) -> IByteArray: # pragma: no cover + return ByteArray.byteArray(string) + + +def get_bytes(array: IByteArray) -> bytes: # pragma: no cover + return to_bytes(array.getBytes()) + + +def to_bytes(obj: ByteArraySerialisable | JavaBytes) -> bytes: # pragma: no cover + # Handle java signed bytes + if isinstance(obj, Iterable): + # Convert java signed bytes to python unsigned bytes + return bytes([b & 0xFF for b in cast(JavaBytes, obj)]) + + return get_bytes(cast(ByteArraySerialisable, obj).toByteArray()) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/edit.py b/scalpel/src/main/resources/python3-10/pyscalpel/edit.py new file mode 100644 index 00000000..6682264b --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/edit.py @@ -0,0 +1,38 @@ +""" + Scalpel allows choosing between normal and binary editors, + to do so, the user can apply the `editor` decorator to the `req_edit_in` / `res_edit_int` hook: +""" +from typing import Callable, Literal, get_args + +EditorMode = Literal["raw", "hex", "octal", "binary", "decimal"] +EDITOR_MODES: set[EditorMode] = set(get_args(EditorMode)) + + +def editor(mode: EditorMode): + """Decorator to specify the editor type for a given hook + + This can be applied to a req_edit_in / res_edit_in hook declaration to specify the editor that should be displayed in Burp + + Example: + ```py + @editor("hex") + def req_edit_in(req: Request) -> bytes | None: + return bytes(req) + ``` + This displays the request in an hex editor. + + Currently, the only modes supported are `"raw"`, `"hex"`, `"octal"`, `"binary"` and `"decimal"`. + + + Args: + mode (EDITOR_MODE): The editor mode (raw, hex,...) + """ + + if mode not in EDITOR_MODES: + raise ValueError(f"Argument must be one of {EDITOR_MODES}") + + def decorator(hook: Callable): + hook.__annotations__["scalpel_editor_mode"] = mode + return hook + + return decorator diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/encoding.py b/scalpel/src/main/resources/python3-10/pyscalpel/encoding.py new file mode 100644 index 00000000..46ee5080 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/encoding.py @@ -0,0 +1,47 @@ +""" + Utilities for encoding data. +""" + +from urllib.parse import unquote_to_bytes as urllibdecode +from _internal_mitmproxy.utils import strutils + + +# str/bytes conversion helpers from mitmproxy/http.py: +# https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/http.py#:~:text=def-,_native,-(x%3A +def always_bytes(data: str | bytes | int, encoding="latin-1") -> bytes: + """Convert data to bytes + + Args: + data (str | bytes | int): The data to convert + + Returns: + bytes: The converted bytes + """ + if isinstance(data, int): + data = str(data) + return strutils.always_bytes(data, encoding, "surrogateescape") + + +def always_str(data: str | bytes | int, encoding="latin-1") -> str: + """Convert data to string + + Args: + data (str | bytes | int): The data to convert + + Returns: + str: The converted string + """ + if isinstance(data, int): + return str(data) + return strutils.always_str(data, encoding, "surrogateescape") + + + +def urlencode_all(data: bytes | str, encoding="latin-1") -> bytes: + """URL Encode all bytes in the given bytes object""" + return "".join(f"%{b:02X}" for b in always_bytes(data, encoding)).encode(encoding) + + +def urldecode(data: bytes | str, encoding="latin-1") -> bytes: + """URL Decode all bytes in the given bytes object""" + return urllibdecode(always_bytes(data, encoding)) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/events.py b/scalpel/src/main/resources/python3-10/pyscalpel/events.py new file mode 100644 index 00000000..ba291446 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/events.py @@ -0,0 +1,15 @@ +"""Events that can be passed to the match() hook""" + +from typing import Literal, get_args + +MatchEvent = Literal[ + "request", + "response", + "req_edit_in", + "req_edit_out", + "res_edit_in", + "res_edit_out", +] + + +MATCH_EVENTS: set[MatchEvent] = set(get_args(MatchEvent)) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/__init__.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/__init__.py new file mode 100644 index 00000000..4d51acaf --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/__init__.py @@ -0,0 +1,19 @@ +""" + This module contains objects representing HTTP objects passed to the user's hooks +""" + +from .request import Request, Headers +from .response import Response +from .flow import Flow +from .utils import match_patterns, host_is +from . import body + +__all__ = [ + "body", # <- pdoc shows a warning for this declaration but won't display it when absent + "Request", + "Response", + "Headers", + "Flow", + "host_is", + "match_patterns", +] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/body/__init__.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/__init__.py new file mode 100644 index 00000000..aa5358b8 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/__init__.py @@ -0,0 +1,33 @@ +""" + Pentesters often have to manipulate form data in precise and extensive ways + + This module contains implementations for the most common forms (multipart,urlencoded, JSON) + + Users may be implement their own form by creating a Serializer, + assigning the .serializer attribute in `Request` and using the "form" property + + Forms are designed to be convertible from one to another. + + For example, JSON forms may be converted to URL encoded forms + by using the php query string syntax: + + ```{"key1": {"key2" : {"key3" : "nested_value"}}} -> key1[key2][key3]=nested_value``` + + And vice-versa. +""" + +from .form import * + + +__all__ = [ + "Form", + "JSON_VALUE_TYPES", + "JSONForm", + "MultiPartForm", + "MultiPartFormField", + "URLEncodedForm", + "FormSerializer", + "json_unescape", + "json_unescape_bytes", + "json_escape_bytes", +] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/body/abstract.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/abstract.py new file mode 100644 index 00000000..83b7c3b2 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/abstract.py @@ -0,0 +1,116 @@ +from __future__ import annotations + +from typing import Protocol, TypeVar +from abc import ABC, abstractmethod, ABCMeta + +from collections.abc import MutableMapping + +from pyscalpel.http.headers import Headers + + +class ObjectWithHeadersField(Protocol): + headers: Headers + + +class ObjectWithHeadersProperty(Protocol): + @property + def headers(self) -> Headers: ... + + @headers.setter + def headers(self, value): ... + + +# Multipart needs the Content-Type header for the boundary parameter +# So Serializer needs an object that references the header +# This is used as a Forward declaration +ObjectWithHeaders = ObjectWithHeadersField | ObjectWithHeadersProperty + +KT = TypeVar("KT") +VT = TypeVar("VT") + + +class Form(MutableMapping[KT, VT], metaclass=ABCMeta): + pass + + +Scalars = str | bytes | int | bool | float + +TupleExportedForm = tuple[ + tuple[bytes, bytes | None], + ..., +] + + +ExportedForm = TupleExportedForm + + +# Abstract base class +class FormSerializer(ABC): + @abstractmethod + def serialize(self, deserialized_body: Form, req: ObjectWithHeaders) -> bytes: + """Serialize a parsed form to raw bytes + + Args: + deserialized_body (Form): The parsed form + req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type) + + Returns: + bytes: Form's raw bytes representation + """ + + @abstractmethod + def deserialize(self, body: bytes, req: ObjectWithHeaders) -> Form | None: + """Parses the form from its raw bytes representation + + Args: + body (bytes): The form as bytes + req (ObjectWithHeaders): The originating request (used for multipart to get an up to date boundary from content-type) + + Returns: + Form | None: The parsed form + """ + + @abstractmethod + def get_empty_form(self, req: ObjectWithHeaders) -> Form: + """Get an empty parsed form object + + Args: + req (ObjectWithHeaders): The originating request (used to get a boundary for multipart forms) + + Returns: + Form: The empty form + """ + + @abstractmethod + def deserialized_type(self) -> type[Form]: + """Gets the form concrete type + + Returns: + type[Form]: The form concrete type + """ + + @abstractmethod + def import_form(self, exported: ExportedForm, req: ObjectWithHeaders) -> Form: + """Imports a form exported by a serializer + Used to convert a form from a Content-Type to another + Information may be lost in the process + + Args: + exported (ExportedForm): The exported form + req: (ObjectWithHeaders): Used to get multipart boundary + + Returns: + Form: The form converted to this serializer's format + """ + + @abstractmethod + def export_form(self, source: Form) -> TupleExportedForm: + """Formats a form so it can be imported by another serializer + Information may be lost in the process + + Args: + form (Form): The form to export + + Returns: + ExportedForm: The exported form + """ diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/body/form.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/form.py new file mode 100644 index 00000000..924852bd --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/form.py @@ -0,0 +1,27 @@ +from __future__ import annotations + + +from typing import Literal, get_args + +from pyscalpel.http.body.abstract import * +from pyscalpel.http.body.json_form import * +from pyscalpel.http.body.multipart import * +from pyscalpel.http.body.urlencoded import * + + +# In Python 3.11 it should be possible to do +# IMPLEMENTED_CONTENT_TYPES_TP = Type[*IMPLEMENTED_CONTENT_TYPES] +ImplementedContentType = Literal[ + "application/x-www-form-urlencoded", "application/json", "multipart/form-data" +] + +IMPLEMENTED_CONTENT_TYPES: set[ImplementedContentType] = set( + get_args(ImplementedContentType) +) + + +CONTENT_TYPE_TO_SERIALIZER: dict[ImplementedContentType, FormSerializer] = { + "application/x-www-form-urlencoded": URLEncodedFormSerializer(), + "application/json": JSONFormSerializer(), + "multipart/form-data": MultiPartFormSerializer(), +} diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/body/json_form.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/json_form.py new file mode 100644 index 00000000..3ff65d74 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/json_form.py @@ -0,0 +1,189 @@ +from __future__ import annotations + + +from collections.abc import Mapping + +import re +import string +import json +from typing import Any + +import qs + +from pyscalpel.http.body.abstract import ( + FormSerializer, + TupleExportedForm, + ExportedForm, +) +from pyscalpel.encoding import always_bytes +from pyscalpel.http.body.urlencoded import URLEncodedFormSerializer +from pyscalpel.utils import removesuffix + +JSON_KEY_TYPES = str | int | float +JSON_VALUE_TYPES = ( + str + | int + | float + | bool + | None + | list["JSON_VALUE_TYPES"] + | dict[JSON_KEY_TYPES, "JSON_VALUE_TYPES"] +) + + +# TODO: JSON keys are actually only strings, so we should wrap +# getter and setter to always convert the keys to string. +class JSONForm(dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]): + """Form representing a JSON object {} + + Implemented by a plain dict + + Args: + dict (_type_): A dict containing JSON-compatible types. + """ + + pass + + +def json_escape_bytes(data: bytes) -> str: + printable = string.printable.encode("utf-8") + + return "".join(chr(ch) if ch in printable else f"\\u{ch:04x}" for ch in data) + + +def json_unescape(escaped: str) -> str: + def decode_match(match): + return chr(int(match.group(1), 16)) + + return re.sub(r"\\u([0-9a-fA-F]{4})", decode_match, escaped) + + +def json_unescape_bytes(escaped: str) -> bytes: + return json_unescape(escaped).encode("latin-1") + + +class PrintableJsonEncoder(json.JSONEncoder): + def default(self, o: Any): + if isinstance(o, bytes): + return json_escape_bytes(o) + return super().default(o) + + +def json_convert(value) -> JSON_VALUE_TYPES: + return json.loads(json.dumps(value, cls=PrintableJsonEncoder)) + + +def transform_tuple_to_dict(tup): + """Transforms duplicates keys to list + + E.g: + (("key_duplicate", 1),("key_duplicate", 2),("key_duplicate", 3), + ("key_duplicate", 4),("key_uniq": "val") , + ("key_duplicate", 5),("key_duplicate", 6)) + -> + {"key_duplicate": [1,2,3,4,5], "key_uniq": "val"} + + + Args: + tup (_type_): _description_ + + Returns: + _type_: _description_ + """ + result_dict = {} + for pair in tup: + key, value = pair + converted_key: bytes | str + match key: + case bytes(): + converted_key = removesuffix(key, b"[]") + case str(): + converted_key = removesuffix(key, "[]") + case _: + converted_key = key + + if converted_key in result_dict: + if isinstance(result_dict[converted_key], list): + result_dict[converted_key].append(value) + else: + result_dict[converted_key] = [result_dict[converted_key], value] + else: + result_dict[converted_key] = value + return result_dict + + +def json_encode_exported_form( + exported: TupleExportedForm, +) -> tuple[tuple[str, str], ...]: + """Unicode escape (\\uXXXX) non printable bytes + + Args: + exported (TupleExportedForm): The exported form tuple + + Returns: + tuple[tuple[str, str], ...]: exported with every values as escaped strings + """ + return tuple( + ( + json_escape_bytes(key), + json_escape_bytes(val or b""), + ) + for key, val in exported + ) + + +def encode_JSON_form( + d: dict[JSON_KEY_TYPES, JSON_VALUE_TYPES] +) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: + new_dict = {} + for k, v in d.items(): + new_key = json_unescape(k) if isinstance(k, str) else k + if isinstance(v, dict): + new_value = encode_JSON_form(v) + elif isinstance(v, str): + new_value = json_unescape(v) + else: + new_value = v + new_dict[new_key] = new_value + return new_dict + + +class JSONFormSerializer(FormSerializer): + def serialize( + self, deserialized_body: Mapping[JSON_KEY_TYPES, JSON_VALUE_TYPES], req=... + ) -> bytes: + return json.dumps(deserialized_body).encode("utf-8") + + def deserialize(self, body: bytes, req=...) -> JSONForm | None: + try: + parsed = json.loads(body) + except json.JSONDecodeError: + return None + + return JSONForm(parsed) if isinstance(parsed, dict) else None + + def get_empty_form(self, req=...) -> JSONForm: + return JSONForm() + + def deserialized_type(self) -> type[JSONForm]: + return JSONForm + + def export_form(self, source: JSONForm) -> TupleExportedForm: + unescaped_source = encode_JSON_form(source) + # Transform the dict to a php style query string + serialized_to_qs = qs.build_qs(unescaped_source) + + # Parse the query string + qs_parser = URLEncodedFormSerializer() + parsed_qs = qs_parser.deserialize(always_bytes(serialized_to_qs)) + + # Export the parsed query string to the tuple format + tupled_form = qs_parser.export_form(parsed_qs) + return tupled_form + + def import_form(self, exported: ExportedForm, req=...) -> JSONForm: + # Parse array keys like "key1[key2][key3]" and place value to the correct path + # e.g: ("key1[key2][key3]", "nested_value") -> {"key1": {"key2" : {"key3" : "nested_value"}}} + dict_form = qs.qs_parse_pairs(list(json_encode_exported_form(exported))) + json_form = JSONForm(dict_form.items()) + return json_form diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/body/multipart.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/multipart.py new file mode 100644 index 00000000..de2206cc --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/multipart.py @@ -0,0 +1,679 @@ +from __future__ import annotations + +import os +from typing import Literal, Sequence, Any, Iterator +from requests.structures import CaseInsensitiveDict +from io import TextIOWrapper, BufferedReader, IOBase +import mimetypes + +from collections.abc import Mapping, MutableMapping + +from pyscalpel.encoding import always_bytes, always_str + +from typing import cast, Any, Iterable, TypeVar +from requests_toolbelt.multipart.decoder import ( + BodyPart, + MultipartDecoder, + ImproperBodyPartContentException, +) +from urllib.parse import quote as urllibquote +from pyscalpel.http.mime import ( + unparse_header_value, + parse_header, + extract_boundary, + find_header_param, + update_header_param, + split_mime_header_value, +) +from pyscalpel.http.body.abstract import ( + ExportedForm, + Form, + TupleExportedForm, +) + +from .abstract import FormSerializer, ObjectWithHeaders, Scalars + +# Define constants to avoid typos. +CONTENT_TYPE_KEY = "Content-Type" +CONTENT_DISPOSITION_KEY = "Content-Disposition" +DEFAULT_CONTENT_TYPE = "application/octet-stream" + + +def get_mime(filename: str | None) -> str: + """Guess the MIME type from a filename based on the extension + + Returns the default content-type when passing None + + Args: + filename (str | None): The filename + + Returns: + str: The MIME type (e.g: application/json) + """ + if filename is None: + return DEFAULT_CONTENT_TYPE + + # Guess the type from the file extension. + mime_type, _ = mimetypes.guess_type(filename) + + if mime_type is not None: + return mime_type + else: + # Extension is unknown. + return DEFAULT_CONTENT_TYPE + + +AnyStr = TypeVar("AnyStr", str, bytes) + + +def escape_parameter(param: str | bytes, extended=False) -> str: + if not extended: + if isinstance(param, bytes): + # https://datatracker.ietf.org/doc/html/rfc8187#section-3.2 + return param.replace(b'"', b"%22").decode("utf-8") + + return param.replace('"', "%22") + + # https://datatracker.ietf.org/doc/html/rfc8187#section-3.2.1 + attr_chars: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # ALPHA + attr_chars += "abcdefghijklmnopqrstuvwxyz" # alpha + attr_chars += "0123456789" # DIGIT + attr_chars += "!#$&+-.^_`|~" # special characters + return urllibquote(param, safe=attr_chars) + + +class MultiPartFormField: + """ + This class represents a field in a multipart/form-data request. + + It provides functionalities to create form fields from various inputs like raw body parts, + files and manual construction with name, filename, body, and content type. + + It also offers properties and methods to interact with the form field's headers and content. + + Raises: + StopIteration: Raised when the specified Content-Disposition header is not found or could not be parsed. + + Returns: + MultiPartFormField: An instance of the class representing a form field in a multipart/form-data request. + """ + + headers: CaseInsensitiveDict[str] + content: bytes + encoding: str + + def __init__( + self, + headers: CaseInsensitiveDict[str], + content: bytes = b"", + encoding: str = "utf-8", + ): + self.headers = headers + self.content = content + self.encoding = encoding + + @classmethod + def from_body_part(cls, body_part: BodyPart): + headers = cls._fix_headers(cast(Mapping[bytes, bytes], body_part.headers)) + return cls(headers, body_part.content, body_part.encoding) + + @classmethod + def make( + cls, + name: str, + filename: str | None = None, + body: bytes = b"", + content_type: str | None = None, + encoding: str = "utf-8", + ) -> MultiPartFormField: + # Ensure the form won't break if someone includes quotes + escaped_name: str = escape_parameter(name) + + # rfc7578 4.2. specifies that urlencoding shouldn't be applied to filename + # But most browsers still replaces the " character by %22 to avoid breaking the parameters quotation. + escaped_filename: str | None = filename and escape_parameter(filename) + + if content_type is None: + content_type = get_mime(filename) + + urlencoded_content_type = urllibquote(content_type) + + disposition = f'form-data; name="{escaped_name}"' + if filename is not None: + # When the param is a file, add a filename MIME param and a content-type header + disposition += f'; filename="{escaped_filename}"' + headers = CaseInsensitiveDict( + { + CONTENT_DISPOSITION_KEY: disposition, + CONTENT_TYPE_KEY: urlencoded_content_type, + } + ) + else: + headers = CaseInsensitiveDict( + { + CONTENT_DISPOSITION_KEY: disposition, + } + ) + + return cls(headers, body, encoding) + + # TODO: Rewrite request_toolbelt multipart parser to get rid of encoding. + @staticmethod + def from_file( + name: str, + file: TextIOWrapper | BufferedReader | str | IOBase, + filename: str | None = None, + content_type: str | None = None, + encoding: str | None = None, + ): + if isinstance(file, str): + file = open(file, mode="rb") + + if filename is None: + match file: + case TextIOWrapper() | BufferedReader(): + filename = os.path.basename(file.name) + case _: + filename = name + + # Guess the MIME content-type from the file extension + if content_type is None: + content_type = ( + mimetypes.guess_type(filename)[0] or "application/octet-stream" + ) + + # Read the whole file into memory + content: bytes + match file: + case TextIOWrapper(): + content = file.read().encode(file.encoding) + # Override file.encoding if provided. + encoding = encoding or file.encoding + case BufferedReader() | IOBase(): + content = file.read() + + instance = MultiPartFormField.make( + name, + filename=filename, + body=content, + content_type=content_type, + encoding=encoding or "utf-8", + ) + + file.close() + + return instance + + @staticmethod + def __serialize_content( + content: bytes, headers: Mapping[str | bytes, str | bytes] + ) -> bytes: + # Prepend content with headers + merged_content: bytes = b"" + header_lines = ( + always_bytes(key) + b": " + always_bytes(value) + for key, value in headers.items() + ) + merged_content += b"\r\n".join(header_lines) + merged_content += b"\r\n\r\n" + merged_content += content + return merged_content + + def __bytes__(self) -> bytes: + return self.__serialize_content( + self.content, + cast(Mapping[bytes | str, bytes | str], self.headers), + ) + + def __eq__(self, other) -> bool: + match other: + case MultiPartFormField() | bytes(): + return bytes(other) == bytes(self) + case str(): + return other.encode("latin-1") == bytes(self) + return False + + def __hash__(self) -> int: + return hash(bytes(self)) + + @staticmethod + def _fix_headers(headers: Mapping[bytes, bytes]) -> CaseInsensitiveDict[str]: + # Fix the headers key by converting them to strings + # https://github.com/requests/toolbelt/pull/353 + + fixed_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict() + for key, value in headers.items(): + fixed_headers[always_str(key)] = always_str(value.decode()) + return fixed_headers + + # Unused for now + # @staticmethod + # def _unfix_headers(headers: Mapping[str, str]) -> CaseInsensitiveDict[bytes]: + # # Unfix the headers key by converting them to bytes + + # unfixed_headers: CaseInsensitiveDict[bytes] = CaseInsensitiveDict() + # for key, value in headers.items(): + # unfixed_headers[always_bytes(key)] = always_bytes(value) # type: ignore requests_toolbelt uses wrong types but it still works fine. + # return unfixed_headers + + @property + def text(self) -> str: + return self.content.decode(self.encoding) + + @property + def content_type(self) -> str | None: + return self.headers.get(CONTENT_TYPE_KEY) + + @content_type.setter + def content_type(self, content_type: str | None) -> None: + headers = self.headers + if content_type is None: + del headers[CONTENT_TYPE_KEY] + else: + headers[CONTENT_TYPE_KEY] = content_type + + def _parse_disposition(self) -> list[tuple[str, str]]: + header_key = CONTENT_DISPOSITION_KEY + header_value = self.headers[header_key] + return parse_header(header_key, header_value) + + def _unparse_disposition(self, parsed_header: list[tuple[str, str]]): + unparsed = unparse_header_value(parsed_header) + self.headers[CONTENT_DISPOSITION_KEY] = unparsed + + def get_disposition_param(self, key: str) -> tuple[str, str | None] | None: + """Get a param from the Content-Disposition header + + Args: + key (str): the param name + + Raises: + StopIteration: Raised when the param was not found. + + Returns: + tuple[str, str | None] | None: Returns the param as (key, value) + """ + # Parse the Content-Disposition header + parsed_disposition = self._parse_disposition() + return find_header_param(parsed_disposition, key) + + def set_disposition_param(self, key: str, value: str): + """Set a Content-Type header parameter + + Args: + key (str): The parameter name + value (str): The parameter value + """ + parsed = self._parse_disposition() + updated = update_header_param(parsed, key, value) + self._unparse_disposition(cast(list[tuple[str, str]], updated)) + + @property + def name(self) -> str: + """Get the Content-Disposition header name parameter + + Returns: + str: The Content-Disposition header name parameter value + """ + # Assume name is always present + return cast(tuple[str, str], self.get_disposition_param("name"))[1] + + @name.setter + def name(self, value: str): + self.set_disposition_param("name", value) + + @property + def filename(self) -> str | None: + """Get the Content-Disposition header filename parameter + + Returns: + str | None: The Content-Disposition header filename parameter value + """ + param = self.get_disposition_param("filename") + return param and param[1] + + @filename.setter + def filename(self, value: str): + self.set_disposition_param("filename", value) + + +class MultiPartForm(Mapping[str, MultiPartFormField]): + """ + This class represents a multipart/form-data request. + + It contains a collection of MultiPartFormField objects, providing methods + to add, get, and delete form fields. + + The class also enables the conversion of the entire form + into bytes for transmission. + + - Args: + - fields (Sequence[MultiPartFormField]): A sequence of MultiPartFormField objects that make up the form. + - content_type (str): The content type of the form. + - encoding (str): The encoding of the form. + + - Raises: + - TypeError: Raised when an incorrect type is passed to MultiPartForm.set. + - KeyError: Raised when trying to access a field that does not exist in the form. + + - Returns: + - MultiPartForm: An instance of the class representing a multipart/form-data request. + + - Yields: + - Iterator[MultiPartFormField]: Yields each field in the form. + """ + + fields: list[MultiPartFormField] + content_type: str + encoding: str + + def __init__( + self, + fields: Sequence[MultiPartFormField], + content_type: str, + encoding: str = "utf-8", + ): + self.content_type = content_type + self.encoding = encoding + super().__init__() + self.fields = list(fields) + + @classmethod + def from_bytes( + cls, content: bytes, content_type: str, encoding: str = "utf-8" + ) -> MultiPartForm: + """Create a MultiPartForm by parsing a raw multipart form + + - Args: + - content (bytes): The multipart form as raw bytes + - content_type (str): The Content-Type header with the corresponding boundary param (required). + - encoding (str, optional): The encoding to use (not required). Defaults to "utf-8". + + - Returns: + - MultiPartForm: The parsed multipart form + """ + decoder = MultipartDecoder(content, content_type, encoding=encoding) + parts: tuple[BodyPart] = decoder.parts + fields: tuple[MultiPartFormField, ...] = tuple( + MultiPartFormField.from_body_part(body_part) for body_part in parts + ) + return cls(fields, content_type, encoding) + + @property + def boundary(self) -> bytes: + """Get the form multipart boundary + + Returns: + bytes: The multipart boundary + """ + return extract_boundary(self.content_type, self.encoding) + + def __bytes__(self) -> bytes: + boundary = self.boundary + serialized = b"" + encoding = self.encoding + for field in self.fields: + serialized += b"--" + boundary + b"\r\n" + + # Format the headers + for key, val in field.headers.items(): + serialized += ( + key.encode(encoding) + b": " + val.encode(encoding) + b"\r\n" + ) + serialized += b"\r\n" + field.content + b"\r\n" + + # Format the final boundary + serialized += b"--" + boundary + b"--\r\n\r\n" + return serialized + + # Override + def get_all(self, key: str) -> list[MultiPartFormField]: + """ + Return the list of all values for a given key. + If that key is not in the MultiDict, the return value will be an empty list. + """ + return [field for field in self.fields if key == field.name] + + def get( + self, key: str, default: MultiPartFormField | None = None + ) -> MultiPartFormField | None: + values = self.get_all(key) + if not values: + return default + + return values[0] + + def del_all(self, key: str): + # Mutate object to avoid invalidating user references to fields + for field in self.fields: + if key == field.name: + self.fields.remove(field) + + def __delitem__(self, key: str): + self.del_all(key) + + def set( + self, + key: str, + value: ( + TextIOWrapper + | BufferedReader + | IOBase + | MultiPartFormField + | bytes + | str + | int + | float + | None + ), + ) -> None: + new_field: MultiPartFormField + match value: + case MultiPartFormField(): + new_field = value + case int() | float(): + return self.set(key, str(value)) + case bytes() | str(): + new_field = MultiPartFormField.make(key) + new_field.content = always_bytes(value) + case IOBase(): + new_field = MultiPartFormField.from_file(key, value) + case None: + self.del_all(key) + return + case _: + raise TypeError("Wrong type was passed to MultiPartForm.set") + + for i, field in enumerate(self.fields): + if field.name == key: + self.fields[i] = new_field + return + + self.append(new_field) + + def setdefault( + self, key: str, default: MultiPartFormField | None = None + ) -> MultiPartFormField: + found = self.get(key) + if found is None: + default = default or MultiPartFormField.make(key) + self[key] = default + return default + + return found + + def __setitem__( + self, + key: str, + value: ( + TextIOWrapper + | BufferedReader + | MultiPartFormField + | IOBase + | bytes + | str + | int + | float + | None + ), + ) -> None: + self.set(key, value) + + def __getitem__(self, key: str) -> MultiPartFormField: + values = self.get_all(key) + if not values: + raise KeyError(key) + return values[0] + + def __len__(self) -> int: + return len(self.fields) + + def __eq__(self, other) -> bool: + if isinstance(other, MultiPartForm): + return self.fields == other.fields + return False + + def __iter__(self) -> Iterator[MultiPartFormField]: + seen = set() + for field in self.fields: + if field not in seen: + seen.add(field) + yield field + + def insert(self, index: int, value: MultiPartFormField) -> None: + """ + Insert an additional value for the given key at the specified position. + """ + self.fields.insert(index, value) + + def append(self, value: MultiPartFormField) -> None: + self.fields.append(value) + + def __repr__(self): # pragma: no cover + fields = (repr(field) for field in self.fields) + return f"{type(self).__name__}[{', '.join(fields)}]" + + def items(self) -> tuple[tuple[str, MultiPartFormField], ...]: + fields = self.fields + items = ((i.name, i) for i in fields) + return tuple(items) + + def keys(self) -> tuple[str, ...]: + return tuple(field.name for field in self.fields) + + def values(self) -> tuple[MultiPartFormField, ...]: + return tuple(self.fields) + + +def scalar_to_bytes(scalar: Scalars | None) -> bytes: + """Convert "scalar" types (str,bytes,int,float,bool) to bytes for query string conversion. + + Args: + scalar (Scalars | None): value to convert + + Returns: + bytes: The converted bytes + """ + match scalar: + case bool(): + return b"1" if scalar else b"0" + case str() | bytes(): + return always_bytes(scalar) + case int() | float(): + return always_bytes(str(scalar)) + case _: + return b"" + + +def scalar_to_str(scalar: Scalars | None) -> str: + """Convert "scalar" types (str,bytes,int,float,bool) to str for query string conversion. + + Args: + scalar (Scalars | None): value to convert + + Returns: + str: The converted str + """ + match scalar: + case bool(): + return "1" if scalar else "0" + case str() | bytes(): + return always_str(scalar) + case int() | float(): + return str(scalar) + case _: + return "" + + +class MultiPartFormSerializer(FormSerializer): + """ + This class is responsible for serializing and deserializing instances of the MultiPartForm class to and from bytes. + It extends the FormSerializer and provides the functionality to handle the form data in the context of a HTTP request. + The class also handles the import and export of form data. + + Methods: + serialize(deserialized_body, req): Converts the given MultiPartForm instance into bytes. + deserialize(body, req): Converts a byte representation of a form back into a MultiPartForm instance. + get_empty_form(req): Returns an empty MultiPartForm instance with the appropriate content type. + deserialized_type(): Returns the type of the object this serializer handles, i.e., MultiPartForm. + import_form(exported, req): Imports form data from a provided sequence or dictionary and creates a MultiPartForm instance. + export_form(source): Exports form data from a MultiPartForm instance into a tuple of byte-string pairs. + """ + + def serialize( + self, deserialized_body: MultiPartForm, req: ObjectWithHeaders + ) -> bytes: + content_type: str | None = req.headers.get(CONTENT_TYPE_KEY) + + if content_type: + deserialized_body.content_type = content_type + + return bytes(deserialized_body) + + def deserialize(self, body: bytes, req: ObjectWithHeaders) -> MultiPartForm | None: + content_type: str | None = req.headers.get(CONTENT_TYPE_KEY) + + assert content_type + + if not body: + return None + + try: + return MultiPartForm.from_bytes(body, content_type) + except ImproperBodyPartContentException: + return None + + def get_empty_form(self, req: ObjectWithHeaders) -> Any: + content_type: str | None = req.headers.get(CONTENT_TYPE_KEY) + + assert content_type + + return MultiPartForm(tuple(), content_type) + + def deserialized_type(self) -> type[MultiPartForm]: + return MultiPartForm + + def import_form( + self, exported: ExportedForm, req: ObjectWithHeaders + ) -> MultiPartForm: + content_type = req.headers.get("Content-Type") + assert content_type + + sequence: Iterable[tuple[Scalars, Scalars | None]] + match exported: + case dict(): + sequence = exported.items() + case tuple(): + sequence = exported + + fields = tuple( + MultiPartFormField.make(scalar_to_str(name), body=scalar_to_bytes(body)) + for name, body in sequence + ) + return MultiPartForm(fields, content_type) + + def export_form(self, source: MultiPartForm) -> tuple[tuple[bytes, bytes]]: + # Only retain name and content + return tuple( + (always_bytes(key), field.content) for key, field in source.items() + ) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/body/urlencoded.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/urlencoded.py new file mode 100644 index 00000000..d72c620f --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/body/urlencoded.py @@ -0,0 +1,111 @@ +from __future__ import annotations +from urllib.parse import quote_from_bytes as quote, parse_qsl +from typing import Any, Iterable + +import sys +import qs +from pyscalpel.encoding import always_bytes, always_str +from pyscalpel.http.body.abstract import ( + ExportedForm, + TupleExportedForm, +) + +from _internal_mitmproxy.coretypes import multidict + +from .abstract import FormSerializer, ExportedForm + + +class URLEncodedFormView(multidict.MultiDictView[str, str]): + def __init__(self, origin: multidict.MultiDictView[str, str]) -> None: + super().__init__(origin._getter, origin._setter) + + def __setitem__(self, key: int | str | bytes, value: int | str | bytes) -> None: + super().__setitem__(always_str(key), always_str(value)) + + +class URLEncodedForm(multidict.MultiDict[bytes, bytes]): + def __init__(self, fields: Iterable[tuple[str | bytes, str | bytes]]) -> None: + fields_converted_to_bytes: Iterable[tuple[bytes, bytes]] = ( + ( + always_bytes(key), + always_bytes(val), + ) + for (key, val) in fields + ) + super().__init__(fields_converted_to_bytes) + + def __setitem__(self, key: int | str | bytes, value: int | str | bytes) -> None: + super().__setitem__(always_bytes(key), always_bytes(value)) + + def __getitem__(self, key: int | bytes | str) -> bytes: + return super().__getitem__(always_bytes(key)) + + +def convert_for_urlencode(val: str | float | bool | bytes | int) -> str | bytes: + match val: + case bytes(): + return val + case bool(): + return "1" if val else "0" + case _: + return str(val) + + +class URLEncodedFormSerializer(FormSerializer): + def serialize( + self, deserialized_body: multidict.MultiDict[bytes, bytes], req=... + ) -> bytes: + return b"&".join( + b"=".join(quote(kv, safe="[]").encode() for kv in field) + for field in deserialized_body.fields + ) + + def deserialize(self, body: bytes, req=...) -> URLEncodedForm: + try: + # XXX: urllib is broken when passing bytes to parse_qsl because it tries to decode it + # but doesn't pass the specified encoding and instead the internal one is used (i.e: ascii, which can't decode some bytes) + # This should be enough: + # fields = urllib.parse.parse_qsl(body) + + # But because urllib is broken we need all of this: + # (I may be wrong) + decoded = body.decode("latin-1") + parsed = parse_qsl(decoded, keep_blank_values=True) + fields = ( + ( + key.encode("latin-1"), + val.encode("latin-1"), + ) + for key, val in parsed + ) + return URLEncodedForm(fields) + except UnicodeEncodeError as exc: # pragma: no cover + print("Query string crashed urrlib parser:", body, file=sys.stderr) + raise exc + + def get_empty_form(self, req=...) -> Any: + return URLEncodedForm(tuple()) + + def deserialized_type(self) -> type[URLEncodedForm]: + return URLEncodedForm + + def import_form(self, exported: ExportedForm, req=...) -> URLEncodedForm: + match exported: + case tuple(): + fields = list() + for key, val in exported: + # Skip null values + if val is None: + continue + + # Convert key,val as str + fields.append( + (convert_for_urlencode(key), convert_for_urlencode(val)) + ) + return URLEncodedForm(fields) + case dict(): + mapped_qs = qs.build_qs(exported) + return self.deserialize(mapped_qs.encode()) + + def export_form(self, source: URLEncodedForm) -> TupleExportedForm: + return source.fields diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/flow.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/flow.py new file mode 100644 index 00000000..19ba46e3 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/flow.py @@ -0,0 +1,48 @@ +from __future__ import annotations +from typing import Literal +from pyscalpel.http.request import Request +from pyscalpel.http.response import Response +from pyscalpel.http.utils import host_is + + +# For some reasons, @dataclass constructors stopped working on windows on newer Python versions +class Flow: + """Contains request and response and some utilities for match()""" + + def __init__( + self, + scheme: Literal["http", "https"] = "http", + host: str = "", + port: int = 0, + request: Request | None = None, + response: Response | None = None, + text: bytes | None = None, + ): + self.scheme = scheme + self.host = host + self.port = port + self.request = request + self.response = response + self.text = text + + def host_is(self, *patterns: str) -> bool: + """Matches a wildcard pattern against the target host + + Returns: + bool: True if at least one pattern matched + """ + return host_is(self.host, *patterns) + + def path_is(self, *patterns: str) -> bool: + """Matches a wildcard pattern against the request path + + Includes query string `?` and fragment `#` + + Returns: + bool: True if at least one pattern matched + """ + req = self.request + if req is None: + return False + + return req.path_is(*patterns) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/headers.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/headers.py new file mode 100644 index 00000000..d7527f03 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/headers.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import ( + Iterable, +) +from _internal_mitmproxy.http import ( + Headers as MITMProxyHeaders, +) + + +from pyscalpel.java.burp.http_header import IHttpHeader, HttpHeader +from pyscalpel.encoding import always_bytes, always_str + + +class Headers(MITMProxyHeaders): + """A wrapper around the MITMProxy Headers. + + This class provides additional methods for converting headers between Burp suite and MITMProxy formats. + """ + + def __init__(self, fields: Iterable[tuple[bytes, bytes]] | None = None, **headers): + """ + :param fields: The headers to construct the from. + :param headers: The headers to construct the from. + """ + # Cannot safely use [] as default param because the reference will be shared with all __init__ calls + fields = fields or [] + + # Construct the base/inherited MITMProxy headers. + super().__init__(fields, **headers) + + @classmethod + def from_mitmproxy(cls, headers: MITMProxyHeaders) -> Headers: + """ + Creates a `Headers` from a `mitmproxy.http.Headers`. + + :param headers: The `mitmproxy.http.Headers` to convert. + :type headers: :class Headers ` + :return: A `Headers` with the same headers as the `mitmproxy.http.Headers`. + """ + + # Construct from the raw MITMProxy headers data. + return cls(headers.fields) + + @classmethod + def from_burp(cls, headers: list[IHttpHeader]) -> Headers: # pragma: no cover + """Construct an instance of the Headers class from a Burp suite HttpHeader array. + :param headers: The Burp suite HttpHeader array to convert. + :return: A Headers with the same headers as the Burp suite HttpHeader array. + """ + + # print(f"burp: {headers}") + # Convert the list of Burp IHttpHeaders to a list of tuples: (key, value) + return cls( + ( + ( + always_bytes(header.name()), + always_bytes(header.value()), + ) + for header in headers + ) + ) + + def to_burp(self) -> list[IHttpHeader]: # pragma: no cover + """Convert the headers to a Burp suite HttpHeader array. + :return: A Burp suite HttpHeader array. + """ + + # Convert the list of tuples: (key, value) to a list of Burp IHttpHeaders + return [ + HttpHeader.httpHeader(always_str(header[0]), always_str(header[1])) + for header in self.fields + ] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/mime.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/mime.py new file mode 100644 index 00000000..f82cd3cf --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/mime.py @@ -0,0 +1,140 @@ +import re +from urllib.parse import quote as urllibquote +from requests_toolbelt.multipart.encoder import MultipartEncoder, encode_with +from typing import Sequence + + +def split_mime_header_value(header_value: str) -> tuple[str, str]: + """Takes the MIME header value as a string: 'text/html; param1="val1"; param2="val2" + Outputs a pair: ("text/html", 'param1="val1"; param2="val2"') + Args: + header_value (str): The header value + + Returns: + tuple[str, str]: A pair like ("text/html", 'param1="val1"; param2="val2"') + """ + parts = header_value.split(";", 1) # Split on the first semicolon + if len(parts) == 2: + # If there are two parts, return them as is + main_value, params = parts[0].strip(), parts[1].strip() + else: + # If there's no semicolon, return the whole string as the main value and an empty string for params + main_value, params = header_value.strip(), "" + return main_value, params + + +def parse_mime_header_params(header_params: str) -> list[tuple[str, str]]: + """Takes the mime parameters as a string: 'key1="val1";key2="val2";...' + Parses the value and outputs a list of key/value pairs [("key1", "val1"), ("key2", "val2"), ...] + + Args: + header_params (str): The header parameters as a string 'key1="val1";key2="val2";...' + + Returns: + list[tuple[str, str]]: List of key/value pairs [("key1", "val1"), ("key2", "val2"), ...] + """ + params = list() + if header_params is not None: + inside_quotes = False + start = 0 + for i, char in enumerate(header_params): + if char == '"': + inside_quotes = not inside_quotes + elif char == ";" and not inside_quotes: + pair = header_params[start:i] + split_pair = pair.split("=", 1) + if len(split_pair) == 2: # Check if there is a key-value pair + key, value = split_pair[0].strip(), split_pair[1].strip() + value = value.strip('"') + params.append((key, value)) + start = i + 1 + pair = header_params[start:] + split_pair = pair.split("=", 1) + if len(split_pair) == 2: # Check if there is a key-value pair + key, value = split_pair[0].strip(), split_pair[1].strip() + value = value.strip('"') + params.append((key, value)) + return params + + +def unparse_header_value(parsed_header: list[tuple[str, str]]) -> str: + """Creates a header value from a list like: [(header key, header value), (parameter1 key, parameter1 value), ...] + + Args: + parsed_header (list[tuple[str, str]]): List containers header key, header value and parameters as tuples + + Returns: + _type_: A header value (doesn't include the key) + """ + # email library doesn't allow to set multiple MIME parameters so we have to do it ourselves. + assert len(parsed_header) >= 2 + header_value: str = parsed_header[0][1] + for param_key, param_value in parsed_header[1:]: + quoted_value = urllibquote(param_value) + header_value += f'; {param_key}="{quoted_value}"' + + return header_value + + +def parse_header(key: str, value: str) -> list[tuple[str, str]]: + + header_value, header_params = split_mime_header_value(value) + parsed_header = parse_mime_header_params(header_params=header_params) + parsed_header.insert( + 0, + ( + key, + header_value, + ), + ) + return parsed_header + + +# Taken from requests_toolbelt +def _split_on_find(content, bound): + point = content.find(bound) + return content[:point], content[point + len(bound) :] + + +# Taken from requests_toolbelt +def extract_boundary(content_type: str, encoding: str) -> bytes: + ct_info = tuple(x.strip() for x in content_type.split(";")) + mimetype = ct_info[0] + if mimetype.split("/")[0].lower() != "multipart": + raise RuntimeError(f"Unexpected mimetype in content-type: '{mimetype}'") + for item in ct_info[1:]: + attr, value = _split_on_find(item, "=") + if attr.lower() == "boundary": + return encode_with(value.strip('"'), encoding) + raise RuntimeError("Missing boundary in content-type header") + + +def find_header_param( + params: Sequence[tuple[str, str | None]], key: str +) -> tuple[str, str | None] | None: + try: + return next(param for param in params if param[0] == key) + except StopIteration: + return None + + +def update_header_param( + params: Sequence[tuple[str, str | None]], key: str, value: str | None +) -> list[tuple[str, str | None]]: + """Copy the provided params and update or add the matching value""" + new_params: list[tuple[str, str | None]] = list() + found: bool = False + for pkey, pvalue in params: + if not found and key == pkey: + pvalue = value + found = True + new_params.append((pkey, pvalue)) + + if not found: + new_params.append((key, value)) + + return new_params + + +def get_header_value_without_params(header_value: str) -> str: + return header_value.split(";", maxsplit=1)[0].strip() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/request.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/request.py new file mode 100644 index 00000000..749c6337 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/request.py @@ -0,0 +1,940 @@ +"""Pythonic wrappers for the Burp Request Java object """ + +from __future__ import annotations + +import urllib.parse +import re + +from typing import ( + Iterable, + Literal, + cast, + Sequence, + Any, + MutableMapping, + Mapping, + TYPE_CHECKING, +) +from copy import deepcopy +from pyscalpel.java.burp import ( + IHttpRequest, + HttpRequest, + IHttpService, + HttpService, + IByteArray, +) +from pyscalpel.burp_utils import get_bytes +from pyscalpel.java.scalpel_types.utils import PythonUtils +from pyscalpel.encoding import always_bytes, always_str +from pyscalpel.http.headers import Headers +from pyscalpel.http.mime import get_header_value_without_params +from pyscalpel.http.utils import host_is, match_patterns +from pyscalpel.http.body import ( + FormSerializer, + JSONFormSerializer, + URLEncodedFormSerializer, + MultiPartFormSerializer, + MultiPartForm, + URLEncodedFormView, + URLEncodedForm, + JSON_KEY_TYPES, + JSON_VALUE_TYPES, + CONTENT_TYPE_TO_SERIALIZER, + JSONForm, + IMPLEMENTED_CONTENT_TYPES, + ImplementedContentType, +) + +from _internal_mitmproxy.coretypes import multidict +from _internal_mitmproxy.net.http.url import ( + parse as url_parse, + unparse as url_unparse, + encode as url_encode, + decode as url_decode, +) +from _internal_mitmproxy.net.http import cookies + +if TYPE_CHECKING: # pragma: no cover + from pyscalpel.http.response import Response + + +class FormNotParsedException(Exception): + """Exception raised when a form deserialization failed + + Args: + Exception (Exception): The base exception + """ + + +class Request: + """A "Burp oriented" HTTP request class + + + This class allows to manipulate Burp requests in a Pythonic way. + """ + + _Port = int + _QueryParam = tuple[str, str] + _ParsedQuery = tuple[_QueryParam, ...] + _HttpVersion = str + _HeaderKey = str + _HeaderValue = str + _Header = tuple[_HeaderKey, _HeaderValue] + _Host = str + _Method = str + _Scheme = Literal["http", "https"] + _Authority = str + _Content = bytes + _Path = str + + host: _Host + port: _Port + method: _Method + scheme: _Scheme + authority: _Authority + + # Path also includes URI parameters (;), query (?) and fragment (#) + # Simply because it is more conveninent to manipulate that way in a pentensting context + # It also mimics the way mitmproxy works. + path: _Path + + http_version: _HttpVersion + _headers: Headers + _serializer: FormSerializer | None = None + _deserialized_content: Any = None + _content: _Content | None = None + _old_deserialized_content: Any = None + _is_form_initialized: bool = False + update_content_length: bool = True + + def __init__( + self, + method: str, + scheme: Literal["http", "https"], + host: str, + port: int, + path: str, + http_version: str, + headers: ( + Headers | tuple[tuple[bytes, bytes], ...] | Iterable[tuple[bytes, bytes]] + ), + authority: str, + content: bytes | None, + ): + self.scheme = scheme + self.host = host + self.port = port + self.path = path + self.method = method + self.authority = authority + self.http_version = http_version + self.headers = headers if isinstance(headers, Headers) else Headers(headers) + self._content = content + + # Initialize the serializer (json,urlencoded,multipart) + self.update_serializer_from_content_type( + self.headers.get("Content-Type"), fail_silently=True + ) + + # Initialize old deserialized content to avoid modifying content if it has not been modified + # (see test_content_do_not_modify_json() in scalpel/src/main/resources/python/pyscalpel/tests/test_request.py) + self._old_deserialized_content = deepcopy(self._deserialized_content) + + def _del_header(self, header: str) -> bool: + if header in self._headers.keys(): + del self._headers[header] + return True + + return False + + def _update_content_length(self) -> None: + if self.update_content_length: + if self._content is None: + self._del_header("Content-Length") + else: + length = len(cast(bytes, self._content)) + self._headers["Content-Length"] = str(length) + + @staticmethod + def _parse_qs(query_string: str) -> _ParsedQuery: + return tuple(urllib.parse.parse_qsl(query_string)) + + @staticmethod + def _parse_url( + url: str, + ) -> tuple[_Scheme, _Host, _Port, _Path]: + scheme, host, port, path = url_parse(url) + + # This method is only used to create HTTP requests from URLs + # so we can ensure the scheme is valid for this usage + if scheme not in (b"http", b"https"): + scheme = b"http" + + return cast( + tuple[Literal["http", "https"], str, int, str], + (scheme.decode("ascii"), host.decode("idna"), port, path.decode("ascii")), + ) + + @staticmethod + def _unparse_url(scheme: _Scheme, host: _Host, port: _Port, path: _Path) -> str: + return url_unparse(scheme, host, port, path) + + @classmethod + def make( + cls, + method: str, + url: str, + content: bytes | str = "", + headers: ( + Headers + | dict[str | bytes, str | bytes] + | dict[str, str] + | dict[bytes, bytes] + | Iterable[tuple[bytes, bytes]] + ) = (), + ) -> Request: + """Create a request from an URL + + Args: + method (str): The request method (GET,POST,PUT,PATCH, DELETE,TRACE,...) + url (str): The request URL + content (bytes | str, optional): The request content. Defaults to "". + headers (Headers, optional): The request headers. Defaults to (). + + Returns: + Request: The HTTP request + """ + scalpel_headers: Headers + match headers: + case Headers(): + scalpel_headers = headers + case dict(): + casted_headers = cast(dict[str | bytes, str | bytes], headers) + scalpel_headers = Headers( + ( + (always_bytes(key), always_bytes(val)) + for key, val in casted_headers.items() + ) + ) + case _: + scalpel_headers = Headers(headers) + + scheme, host, port, path = Request._parse_url(url) + http_version = "HTTP/1.1" + + # Inferr missing Host header from URL + host_header = scalpel_headers.get("Host") + if host_header is None: + match (scheme, port): + case ("http", 80) | ("https", 443): + host_header = host + case _: + host_header = f"{host}:{port}" + + scalpel_headers["Host"] = host_header + + authority: str = host_header + encoded_content = always_bytes(content) + + assert isinstance(host, str) + + return cls( + method=method, + scheme=scheme, + host=host, + port=port, + path=path, + http_version=http_version, + headers=scalpel_headers, + authority=authority, + content=encoded_content, + ) + + @classmethod + def from_burp( + cls, request: IHttpRequest, service: IHttpService | None = None + ) -> Request: # pragma: no cover (uses Java API) + """Construct an instance of the Request class from a Burp suite HttpRequest. + :param request: The Burp suite HttpRequest to convert. + :return: A Request with the same data as the Burp suite HttpRequest. + """ + service = service or request.httpService() + body = get_bytes(request.body()) + + # Burp will give you lowercased and pseudo headers when using HTTP/2. + # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=are%20converted%20to-,lowercase,-. + # https://blog.yaakov.online/http-2-header-casing/ + headers: Headers = Headers.from_burp(request.headers()) + + # Burp gives a 0 length byte array body even when it doesn't exist, instead of null. + # Empty but existing bodies without a Content-Length header are lost in the process. + if not body and not headers.get("Content-Length"): + body = None + + # request.url() gives a relative url for some reason + # So we have to parse and unparse to get the full path + # (path + parameters + query + fragment) + _, _, path, parameters, query, fragment = urllib.parse.urlparse(request.url()) + + # Concatenate the path components + # Empty parameters,query and fragment are lost in the process + # e.g.: http://example.com;?# becomes http://example.com + # To use such an URL, the user must set the path directly + # To fix this we would need to write our own URL parser, which is a bit overkill for now. + path = urllib.parse.urlunparse(("", "", path, parameters, query, fragment)) + + host = "" + port = 0 + scheme = "http" + if service: + host = service.host() + port = service.port() + scheme = "https" if service.secure() else "http" + + return cls( + method=request.method(), + scheme=scheme, + host=host, + port=port, + path=path, + http_version=request.httpVersion() or "HTTP/1.1", + headers=headers, + authority=headers.get(":authority") or headers.get("Host") or "", + content=body, + ) + + def __bytes__(self) -> bytes: + """Convert the request to bytes + :return: The request as bytes. + """ + # Reserialize the request to bytes. + first_line = ( + b" ".join( + always_bytes(s) for s in (self.method, self.path, self.http_version) + ) + + b"\r\n" + ) + + # Strip HTTP/2 pseudo headers. + # https://portswigger.net/burp/documentation/desktop/http2/http2-basics-for-burp-users#:~:text=HTTP/2%20specification.-,Pseudo%2Dheaders,-In%20HTTP/2 + mapped_headers = tuple( + field for field in self.headers.fields if not field[0].startswith(b":") + ) + + if self.headers.get(b"Host") is None and self.http_version == "HTTP/2": + # Host header is not present in HTTP/2, but is required by Burp message editor. + # So we have to add it back from the :authority pseudo-header. + # https://portswigger.net/burp/documentation/desktop/http2/http2-normalization-in-the-message-editor#sending-requests-without-any-normalization:~:text=pseudo%2Dheaders%20and-,derives,-the%20%3Aauthority%20from + mapped_headers = ( + (b"Host", always_bytes(self.headers[":authority"])), + ) + tuple(mapped_headers) + + # Construct the request's headers part. + headers_lines = b"".join( + b"%s: %s\r\n" % (key, val) for key, val in mapped_headers + ) + + # Set a default value for the request's body. (None -> b"") + body = self.content or b"" + + # Construct the whole request and return it. + return first_line + headers_lines + b"\r\n" + body + + def to_burp(self) -> IHttpRequest: # pragma: no cover + """Convert the request to a Burp suite :class:`IHttpRequest`. + :return: The request as a Burp suite :class:`IHttpRequest`. + """ + # Convert the request to a Burp ByteArray. + request_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self)) + + if self.port == 0: + # No networking information is available, so we build a plain network-less request. + return HttpRequest.httpRequest(request_byte_array) + + # Build the Burp HTTP networking service. + service: IHttpService = HttpService.httpService( + self.host, self.port, self.scheme == "https" + ) + + # Instantiate and return a new Burp HTTP request. + return HttpRequest.httpRequest(service, request_byte_array) + + @classmethod + def from_raw( + cls, + data: bytes | str, + real_host: str = "", + port: int = 0, + scheme: Literal["http"] | Literal["https"] | str = "http", + ) -> Request: # pragma: no cover + """Construct an instance of the Request class from raw bytes. + :param data: The raw bytes to convert. + :param real_host: The real host to connect to. + :param port: The port of the request. + :param scheme: The scheme of the request. + :return: A :class:`Request` with the same data as the raw bytes. + """ + # Convert the raw bytes to a Burp ByteArray. + # We use the Burp API to trivialize the parsing of the request from raw bytes. + str_or_byte_array: IByteArray | str = ( + data if isinstance(data, str) else PythonUtils.toByteArray(data) + ) + + # Handle the case where the networking informations are not provided. + if port == 0: + # Instantiate and return a new Burp HTTP request without networking informations. + burp_request: IHttpRequest = HttpRequest.httpRequest(str_or_byte_array) + else: + # Build the Burp HTTP networking service. + service: IHttpService = HttpService.httpService( + real_host, port, scheme == "https" + ) + + # Instantiate a new Burp HTTP request with networking informations. + burp_request: IHttpRequest = HttpRequest.httpRequest( + service, str_or_byte_array + ) + + # Construct the request from the Burp. + return cls.from_burp(burp_request) + + @property + def url(self) -> str: + """ + The full URL string, constructed from `Request.scheme`, + `Request.host`, `Request.port` and `Request.path`. + + Setting this property updates these attributes as well. + """ + return Request._unparse_url(self.scheme, self.host, self.port, self.path) + + @url.setter + def url(self, val: str | bytes) -> None: + (self.scheme, self.host, self.port, self.path) = Request._parse_url( + always_str(val) + ) + + def _get_query(self) -> _ParsedQuery: + query = urllib.parse.urlparse(self.url).query + return tuple(url_decode(query)) + + def _set_query(self, query_data: Sequence[_QueryParam]): + query = url_encode(query_data) + _, _, path, params, _, fragment = urllib.parse.urlparse(self.url) + self.path = urllib.parse.urlunparse(["", "", path, params, query, fragment]) + + @property + def query(self) -> URLEncodedFormView: + """The query string parameters as a dict-like object + + Returns: + QueryParamsView: The query string parameters + """ + return URLEncodedFormView( + multidict.MultiDictView(self._get_query, self._set_query) + ) + + @query.setter + def query(self, value: Sequence[tuple[str, str]]): + self._set_query(value) + + def _has_deserialized_content_changed(self) -> bool: + return self._deserialized_content != self._old_deserialized_content + + def _serialize_content(self): + if self._serializer is None: + return + + if self._deserialized_content is None: + self._content = None + return + + self._update_serialized_content( + self._serializer.serialize(self._deserialized_content, req=self) + ) + + def _update_serialized_content(self, serialized: bytes): + if self._serializer is None: + self._content = serialized + return + + # Update the parsed form + self._deserialized_content = self._serializer.deserialize(serialized, self) + self._old_deserialized_content = deepcopy(self._deserialized_content) + + # Set the raw content directly + self._content = serialized + + def _deserialize_content(self): + if self._serializer is None: + return + + if self._content: + self._deserialized_content = self._serializer.deserialize( + self._content, req=self + ) + + def _update_deserialized_content(self, deserialized: Any): + if self._serializer is None: + return + + if deserialized is None: + self._deserialized_content = None + self._old_deserialized_content = None + return + + self._deserialized_content = deserialized + self._content = self._serializer.serialize(deserialized, self) + self._update_content_length() + + @property + def content(self) -> bytes | None: + """The request content / body as raw bytes + + Returns: + bytes | None: The content if it exists + """ + if self._serializer and self._has_deserialized_content_changed(): + self._update_deserialized_content(self._deserialized_content) + self._old_deserialized_content = deepcopy(self._deserialized_content) + + self._update_content_length() + + return self._content + + @content.setter + def content(self, value: bytes | str | None): + match value: + case None: + self._content = None + self._deserialized_content = None + return + case str(): + value = value.encode("latin-1") + + self._update_content_length() + + self._update_serialized_content(value) + + @property + def body(self) -> bytes | None: + """Alias for content() + + Returns: + bytes | None: The request body / content + """ + return self.content + + @body.setter + def body(self, value: bytes | str | None): + self.content = value + + def update_serializer_from_content_type( + self, + content_type: ImplementedContentType | str | None = None, + fail_silently: bool = False, + ): + """Update the form parsing based on the given Content-Type + + Args: + content_type (ImplementedContentTypesTp | str | None, optional): The form content-type. Defaults to None. + fail_silently (bool, optional): Determine if an excpetion is raised when the content-type is unknown. Defaults to False. + + Raises: + FormNotParsedException: Raised when the content-type is unknown. + """ + # Strip the boundary param so we can use our content-type to serializer map + _content_type: str = get_header_value_without_params( + content_type or self.headers.get("Content-Type") or "" + ) + + serializer = None + if _content_type in IMPLEMENTED_CONTENT_TYPES: + serializer = CONTENT_TYPE_TO_SERIALIZER.get(_content_type) + + if serializer is None: + if fail_silently: + serializer = self._serializer + else: + raise FormNotParsedException( + f"Unimplemented form content-type: {_content_type}" + ) + self._set_serializer(serializer) + + @property + def content_type(self) -> str | None: + """The Content-Type header value. + + Returns: + str | None: <=> self.headers.get("Content-Type") + """ + return self.headers.get("Content-Type") + + @content_type.setter + def content_type(self, value: str) -> str | None: + self.headers["Content-Type"] = value + + def create_defaultform( + self, + content_type: ImplementedContentType | str | None = None, + update_header: bool = True, + ) -> MutableMapping[Any, Any]: + """Creates the form if it doesn't exist, else returns the existing one + + Args: + content_type (IMPLEMENTED_CONTENT_TYPES_TP | None, optional): The form content-type. Defaults to None. + update_header (bool, optional): Whether to update the header. Defaults to True. + + Raises: + FormNotParsedException: Thrown when provided content-type has no implemented form-serializer + FormNotParsedException: Thrown when the raw content could not be parsed. + + Returns: + MutableMapping[Any, Any]: The mapped form. + """ + if not self._is_form_initialized or content_type: + self.update_serializer_from_content_type(content_type) + + # Set content-type if it does not exist + if (content_type and update_header) or not self.headers.get_all( + "Content-Type" + ): + self.headers["Content-Type"] = content_type + + serializer = self._serializer + if serializer is None: + # This should probably never trigger here as it should already be raised by update_serializer_from_content_type + raise FormNotParsedException( + f"Form of content-type {self.content_type} not implemented." + ) + + # Create default form. + if not self.content: + self._deserialized_content = serializer.get_empty_form(self) + elif self._deserialized_content is None: + self._deserialize_content() + + if self._deserialized_content is None: + raise FormNotParsedException( + f"Could not parse content to {serializer.deserialized_type()}\nContent:{self._content}" + ) + + if not isinstance(self._deserialized_content, serializer.deserialized_type()): + self._deserialized_content = serializer.get_empty_form(self) + + self._is_form_initialized = True + return self._deserialized_content + + @property + def form(self) -> MutableMapping[Any, Any]: + """Mapping from content parsed accordingly to Content-Type + + Raises: + FormNotParsedException: The content could not be parsed accordingly to Content-Type + + Returns: + MutableMapping[Any, Any]: The mapped request form + """ + if not self._is_form_initialized: + self.update_serializer_from_content_type() + + self.create_defaultform() + if self._deserialized_content is None: + raise FormNotParsedException() + + self._is_form_initialized = True + return self._deserialized_content + + @form.setter + def form(self, form: MutableMapping[Any, Any]): + if not self._is_form_initialized: + self.update_serializer_from_content_type() + self._is_form_initialized = True + + self._deserialized_content = form + + # Update raw _content + self._serialize_content() + + def _set_serializer(self, serializer: FormSerializer | None): + # Update the serializer + old_serializer = self._serializer + self._serializer = serializer + + if serializer is None: + self._deserialized_content = None + return + + if type(serializer) == type(old_serializer): + return + + if old_serializer is None: + self._deserialize_content() + return + + old_form = self._deserialized_content + + if old_form is None: + self._deserialize_content() + return + + # Convert the form to an intermediate format for easier conversion + exported_form = old_serializer.export_form(old_form) + + # Parse the intermediate data to the new serializer format + imported_form = serializer.import_form(exported_form, self) + self._deserialized_content = imported_form + + def _update_serializer_and_get_form( + self, serializer: FormSerializer + ) -> MutableMapping[Any, Any] | None: + # Set the serializer and update the content + self._set_serializer(serializer) + + # Return the new form + return self._deserialized_content + + def _update_serializer_and_set_form( + self, serializer: FormSerializer, form: MutableMapping[Any, Any] + ) -> None: + # NOOP when the serializer is the same + self._set_serializer(serializer) + + self._update_deserialized_content(form) + + @property + def urlencoded_form(self) -> URLEncodedForm: + """The urlencoded form data + + Converts the content to the urlencoded form format if needed. + Modification to this object will update Request.content and vice versa + + Returns: + QueryParams: The urlencoded form data + """ + self._is_form_initialized = True + return cast( + URLEncodedForm, + self._update_serializer_and_get_form(URLEncodedFormSerializer()), + ) + + @urlencoded_form.setter + def urlencoded_form(self, form: URLEncodedForm): + self._is_form_initialized = True + self._update_serializer_and_set_form(URLEncodedFormSerializer(), form) + + @property + def json_form(self) -> dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: + """The JSON form data + + Converts the content to the JSON form format if needed. + Modification to this object will update Request.content and vice versa + + Returns: + dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]: The JSON form data + """ + self._is_form_initialized = True + if self._update_serializer_and_get_form(JSONFormSerializer()) is None: + serializer = cast(JSONFormSerializer, self._serializer) + self._deserialized_content = serializer.get_empty_form(self) + + return self._deserialized_content + + @json_form.setter + def json_form(self, form: dict[JSON_KEY_TYPES, JSON_VALUE_TYPES]): + self._is_form_initialized = True + self._update_serializer_and_set_form(JSONFormSerializer(), JSONForm(form)) + + def _ensure_multipart_content_type(self) -> str: + content_types_headers = self.headers.get_all("Content-Type") + pattern = re.compile( + r"^multipart/form-data;\s*boundary=([^;\s]+)", re.IGNORECASE + ) + + # Find a valid multipart content-type header with a valid boundary + matched_content_type: str | None = None + for content_type in content_types_headers: + if pattern.match(content_type): + matched_content_type = content_type + break + + # If no boundary was found, overwrite the Content-Type header + # If an user wants to avoid this behaviour,they should manually create a MultiPartForm(), convert it to bytes + # and pass it as raw_form() + if matched_content_type is None: + # TODO: Randomly generate this? The boundary could be used to fingerprint Scalpel + new_content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + self.headers["Content-Type"] = new_content_type + return new_content_type + + return matched_content_type + + @property + def multipart_form(self) -> MultiPartForm: + """The multipart form data + + Converts the content to the multipart form format if needed. + Modification to this object will update Request.content and vice versa + + Returns: + MultiPartForm + """ + self._is_form_initialized = True + + # Keep boundary even if content-type has changed + if isinstance(self._deserialized_content, MultiPartForm): + return self._deserialized_content + + # We do not have an existing form, so we have to ensure we have a content-type header with a boundary + self._ensure_multipart_content_type() + + # Serialize the current form and try to parse it with the new serializer + form = self._update_serializer_and_get_form(MultiPartFormSerializer()) + serializer = cast(MultiPartFormSerializer, self._serializer) + + # Set a default value + if not form: + self._deserialized_content = serializer.get_empty_form(self) + + # get_empty_form() fails when the request doesn't have a valid Content-Type multipart/form-data with a boundary + if self._deserialized_content is None: + raise FormNotParsedException( + f"Could not parse content to {serializer.deserialized_type()}" + ) + + return self._deserialized_content + + @multipart_form.setter + def multipart_form(self, form: MultiPartForm): + self._is_form_initialized = True + if not isinstance(self._deserialized_content, MultiPartForm): + # Generate a multipart header because we don't have any boundary to format the multipart. + self._ensure_multipart_content_type() + + return self._update_serializer_and_set_form( + MultiPartFormSerializer(), cast(MutableMapping, form) + ) + + @property + def cookies(self) -> multidict.MultiDictView[str, str]: + """ + The request cookies. + For the most part, this behaves like a dictionary. + Modifications to the MultiDictView update `Request.headers`, and vice versa. + """ + return multidict.MultiDictView(self._get_cookies, self._set_cookies) + + def _get_cookies(self) -> tuple[tuple[str, str], ...]: + header = self.headers.get_all("Cookie") + return tuple(cookies.parse_cookie_headers(header)) + + def _set_cookies(self, value: tuple[tuple[str, str], ...]): + self.headers["cookie"] = cookies.format_cookie_header(value) + + @cookies.setter + def cookies(self, value: tuple[tuple[str, str], ...] | Mapping[str, str]): + if hasattr(value, "items") and callable(getattr(value, "items")): + value = tuple(cast(Mapping[str, str], value).items()) + self._set_cookies(cast(tuple[tuple[str, str], ...], value)) + + @property + def host_header(self) -> str | None: + """Host header value + + Returns: + str | None: The host header value + """ + return self.headers.get("Host") + + @host_header.setter + def host_header(self, value: str | None): + self.headers["Host"] = value + + def text(self, encoding="utf-8") -> str: + """The decoded content + + Args: + encoding (str, optional): encoding to use. Defaults to "utf-8". + + Returns: + str: The decoded content + """ + if self.content is None: + return "" + + return self.content.decode(encoding) + + @property + def headers(self) -> Headers: + """The request HTTP headers + + Returns: + Headers: a case insensitive dict containing the HTTP headers + """ + self._update_content_length() + return self._headers + + @headers.setter + def headers(self, value: Headers): + self._headers = value + self._update_content_length() + + @property + def content_length(self) -> int: + """Returns the Content-Length header value + Returns 0 if the header is absent + + Args: + value (int | str): The Content-Length value + + Raises: + RuntimeError: Throws RuntimeError when the value is invalid + """ + content_length: str | None = self.headers.get("Content-Length") + if content_length is None: + return 0 + + trimmed = content_length.strip() + if not trimmed.isdigit(): + raise ValueError("Content-Length does not contain only digits") + + return int(trimmed) + + @content_length.setter + def content_length(self, value: int | str): + if self.update_content_length: + # It is useless to manually set content-length because the value will be erased. + raise RuntimeError( + "Cannot set content_length when self.update_content_length is True" + ) + + if isinstance(value, int): + value = str(value) + + self._headers["Content-Length"] = value + + @property + def pretty_host(self) -> str: + """Returns the most approriate host + Returns self.host when it exists, else it returns self.host_header + + Returns: + str: The request target host + """ + return self.host or self.headers.get("Host") or "" + + def host_is(self, *patterns: str) -> bool: + """Perform wildcard matching (fnmatch) on the target host. + + Args: + pattern (str): The pattern to use + + Returns: + bool: Whether the pattern matches + """ + return host_is(self.pretty_host, *patterns) + + def path_is(self, *patterns: str) -> bool: + return match_patterns(self.path, *patterns) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/response.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/response.py new file mode 100644 index 00000000..ae22dff9 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/response.py @@ -0,0 +1,213 @@ +from __future__ import annotations + +import time +from typing import Literal, cast +from _internal_mitmproxy.http import ( + Response as MITMProxyResponse, +) + +from pyscalpel.java.burp.http_response import IHttpResponse, HttpResponse +from pyscalpel.burp_utils import get_bytes +from pyscalpel.java.burp.byte_array import IByteArray +from pyscalpel.java.scalpel_types.utils import PythonUtils +from pyscalpel.encoding import always_bytes +from pyscalpel.http.headers import Headers +from pyscalpel.http.utils import host_is +from pyscalpel.java.burp.http_service import IHttpService +from pyscalpel.java.burp.http_request import IHttpRequest +from pyscalpel.http.request import Request + + +class Response(MITMProxyResponse): + """A "Burp oriented" HTTP response class + + + This class allows to manipulate Burp responses in a Pythonic way. + + Fields: + scheme: http or https + host: The initiating request target host + port: The initiating request target port + request: The initiating request. + """ + + scheme: Literal["http", "https"] = "http" + host: str = "" + port: int = 0 + request: Request | None = None + + def __init__( + self, + http_version: bytes, + status_code: int, + reason: bytes, + headers: Headers | tuple[tuple[bytes, bytes], ...], + content: bytes | None, + trailers: Headers | tuple[tuple[bytes, bytes], ...] | None, + scheme: Literal["http", "https"] = "http", + host: str = "", + port: int = 0, + ): + # Construct the base/inherited MITMProxy response. + super().__init__( + http_version, + status_code, + reason, + headers, + content, + trailers, + timestamp_start=time.time(), + timestamp_end=time.time(), + ) + self.scheme = scheme + self.host = host + self.port = port + + @classmethod + # https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response + # link to mitmproxy documentation + def from_mitmproxy(cls, response: MITMProxyResponse) -> Response: + """Construct an instance of the Response class from a [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response). + :param response: The [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response) to convert. + :return: A :class:`Response` with the same data as the [mitmproxy.http.HTTPResponse](https://docs.mitmproxy.org/stable/api/mitmproxy/http.html#Response). + """ + return cls( + always_bytes(response.http_version), + response.status_code, + always_bytes(response.reason), + Headers.from_mitmproxy(response.headers), + response.content, + Headers.from_mitmproxy(response.trailers) if response.trailers else None, + ) + + @classmethod + def from_burp( + cls, + response: IHttpResponse, + service: IHttpService | None = None, + request: IHttpRequest | None = None, + ) -> Response: + """Construct an instance of the Response class from a Burp suite :class:`IHttpResponse`.""" + body = get_bytes(cast(IByteArray, response.body())) if response.body() else b"" + scalpel_response = cls( + always_bytes(response.httpVersion() or "HTTP/1.1"), + response.statusCode(), + always_bytes(response.reasonPhrase() or b""), + Headers.from_burp(response.headers()), + body, + None, + ) + + burp_request: IHttpRequest | None = request + if burp_request is None: + try: + # Some responses can have a "initiatingRequest" field. + # https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/handler/HttpResponseReceived.html#initiatingRequest():~:text=HttpRequest-,initiatingRequest(),-Returns%3A + burp_request = response.initiatingRequest() # type: ignore + except AttributeError: + pass + + if burp_request: + scalpel_response.request = Request.from_burp(burp_request, service) + + if not service and burp_request: + # The only way to check if the Java method exist without writing Java is catching the error. + service = burp_request.httpService() + + if service: + scalpel_response.scheme = "https" if service.secure() else "http" + scalpel_response.host = service.host() + scalpel_response.port = service.port() + + return scalpel_response + + def __bytes__(self) -> bytes: + """Convert the response to raw bytes.""" + # Reserialize the response to bytes. + + # Format the first line of the response. (e.g. "HTTP/1.1 200 OK\r\n") + first_line = ( + b" ".join( + always_bytes(s) + for s in (self.http_version, str(self.status_code), self.reason) + ) + + b"\r\n" + ) + + # Format the response's headers part. + headers_lines = b"".join( + b"%s: %s\r\n" % (key, val) for key, val in self.headers.fields + ) + + # Set a default value for the response's body. (None -> b"") + body = self.content or b"" + + # Build the whole response and return it. + return first_line + headers_lines + b"\r\n" + body + + def to_burp(self) -> IHttpResponse: # pragma: no cover (uses Java API) + """Convert the response to a Burp suite :class:`IHttpResponse`.""" + response_byte_array: IByteArray = PythonUtils.toByteArray(bytes(self)) + + return HttpResponse.httpResponse(response_byte_array) + + @classmethod + def from_raw( + cls, data: bytes | str + ) -> Response: # pragma: no cover (uses Java API) + """Construct an instance of the Response class from raw bytes. + :param data: The raw bytes to convert. + :return: A :class:`Response` parsed from the raw bytes. + """ + # Use the Burp API to trivialize the parsing of the response from raw bytes. + # Convert the raw bytes to a Burp ByteArray. + # Plain strings are OK too. + str_or_byte_array: IByteArray | str = ( + data if isinstance(data, str) else PythonUtils.toByteArray(data) + ) + + # Instantiate a new Burp HTTP response. + burp_response: IHttpResponse = HttpResponse.httpResponse(str_or_byte_array) + + return cls.from_burp(burp_response) + + @classmethod + def make( + cls, + status_code: int = 200, + content: bytes | str = b"", + headers: Headers | tuple[tuple[bytes, bytes], ...] = (), + host: str = "", + port: int = 0, + scheme: Literal["http", "https"] = "http", + ) -> "Response": + # Use the base/inherited make method to construct a MITMProxy response. + mitmproxy_res = MITMProxyResponse.make(status_code, content, headers) + + res = cls.from_mitmproxy(mitmproxy_res) + res.host = host + res.scheme = scheme + res.port = port + + return res + + def host_is(self, *patterns: str) -> bool: + """Matches the host against the provided patterns + + Returns: + bool: Whether at least one pattern matched + """ + return host_is(self.host, *patterns) + + @property + def body(self) -> bytes | None: + """Alias for content() + + Returns: + bytes | None: The request body / content + """ + return self.content + + @body.setter + def body(self, val: bytes | None): + self.content = val diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/http/utils.py b/scalpel/src/main/resources/python3-10/pyscalpel/http/utils.py new file mode 100644 index 00000000..ad58e1e9 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/http/utils.py @@ -0,0 +1,30 @@ +from fnmatch import fnmatch + + +def match_patterns(to_match: str, *patterns: str) -> bool: + """Matches a string using unix-like wildcard matching against multiple patterns + + Args: + to_match (str): The string to match against + patterns (str): The patterns to use + + Returns: + bool: The match result (True if at least one pattern matches, else False) + """ + for pattern in patterns: + if fnmatch(to_match, pattern): + return True + return False + + +def host_is(host: str, *patterns: str) -> bool: + """Matches a host using unix-like wildcard matching against multiple patterns + + Args: + host (str): The host to match against + patterns (str): The patterns to use + + Returns: + bool: The match result (True if at least one pattern matches, else False) + """ + return match_patterns(host, *patterns) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/internal/status_code.py b/scalpel/src/main/resources/python3-10/pyscalpel/internal/status_code.py new file mode 100644 index 00000000..5e8e2d9f --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/internal/status_code.py @@ -0,0 +1,111 @@ +# https://github.com/mitmproxy/mitmproxy/blob/main/mitmproxy/net/http/status_codes.py + +CONTINUE = 100 +SWITCHING = 101 +PROCESSING = 102 +EARLY_HINTS = 103 + +OK = 200 +CREATED = 201 +ACCEPTED = 202 +NON_AUTHORITATIVE_INFORMATION = 203 +NO_CONTENT = 204 +RESET_CONTENT = 205 +PARTIAL_CONTENT = 206 +MULTI_STATUS = 207 + +MULTIPLE_CHOICE = 300 +MOVED_PERMANENTLY = 301 +FOUND = 302 +SEE_OTHER = 303 +NOT_MODIFIED = 304 +USE_PROXY = 305 +TEMPORARY_REDIRECT = 307 + +BAD_REQUEST = 400 +UNAUTHORIZED = 401 +PAYMENT_REQUIRED = 402 +FORBIDDEN = 403 +NOT_FOUND = 404 +NOT_ALLOWED = 405 +NOT_ACCEPTABLE = 406 +PROXY_AUTH_REQUIRED = 407 +REQUEST_TIMEOUT = 408 +CONFLICT = 409 +GONE = 410 +LENGTH_REQUIRED = 411 +PRECONDITION_FAILED = 412 +PAYLOAD_TOO_LARGE = 413 +REQUEST_URI_TOO_LONG = 414 +UNSUPPORTED_MEDIA_TYPE = 415 +REQUESTED_RANGE_NOT_SATISFIABLE = 416 +EXPECTATION_FAILED = 417 +IM_A_TEAPOT = 418 +NO_RESPONSE = 444 +CLIENT_CLOSED_REQUEST = 499 + +INTERNAL_SERVER_ERROR = 500 +NOT_IMPLEMENTED = 501 +BAD_GATEWAY = 502 +SERVICE_UNAVAILABLE = 503 +GATEWAY_TIMEOUT = 504 +HTTP_VERSION_NOT_SUPPORTED = 505 +INSUFFICIENT_STORAGE_SPACE = 507 +NOT_EXTENDED = 510 + +RESPONSES = { + # 100 + CONTINUE: "Continue", + SWITCHING: "Switching Protocols", + PROCESSING: "Processing", + EARLY_HINTS: "Early Hints", + # 200 + OK: "OK", + CREATED: "Created", + ACCEPTED: "Accepted", + NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information", + NO_CONTENT: "No Content", + RESET_CONTENT: "Reset Content.", + PARTIAL_CONTENT: "Partial Content", + MULTI_STATUS: "Multi-Status", + # 300 + MULTIPLE_CHOICE: "Multiple Choices", + MOVED_PERMANENTLY: "Moved Permanently", + FOUND: "Found", + SEE_OTHER: "See Other", + NOT_MODIFIED: "Not Modified", + USE_PROXY: "Use Proxy", + # 306 not defined?? + TEMPORARY_REDIRECT: "Temporary Redirect", + # 400 + BAD_REQUEST: "Bad Request", + UNAUTHORIZED: "Unauthorized", + PAYMENT_REQUIRED: "Payment Required", + FORBIDDEN: "Forbidden", + NOT_FOUND: "Not Found", + NOT_ALLOWED: "Method Not Allowed", + NOT_ACCEPTABLE: "Not Acceptable", + PROXY_AUTH_REQUIRED: "Proxy Authentication Required", + REQUEST_TIMEOUT: "Request Time-out", + CONFLICT: "Conflict", + GONE: "Gone", + LENGTH_REQUIRED: "Length Required", + PRECONDITION_FAILED: "Precondition Failed", + PAYLOAD_TOO_LARGE: "Payload Too Large", + REQUEST_URI_TOO_LONG: "Request-URI Too Long", + UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type", + REQUESTED_RANGE_NOT_SATISFIABLE: "Requested Range not satisfiable", + EXPECTATION_FAILED: "Expectation Failed", + IM_A_TEAPOT: "I'm a teapot", + NO_RESPONSE: "No Response", + CLIENT_CLOSED_REQUEST: "Client Closed Request", + # 500 + INTERNAL_SERVER_ERROR: "Internal Server Error", + NOT_IMPLEMENTED: "Not Implemented", + BAD_GATEWAY: "Bad Gateway", + SERVICE_UNAVAILABLE: "Service Unavailable", + GATEWAY_TIMEOUT: "Gateway Time-out", + HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", + INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", + NOT_EXTENDED: "Not Extended", +} diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/__init__.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/__init__.py new file mode 100644 index 00000000..813119c1 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/__init__.py @@ -0,0 +1,19 @@ +""" + This module declares type definitions used for Java objects. + + If you are a normal user, you should probably never have to manipulate these objects yourself. +""" +from .bytes import JavaBytes +from .import_java import import_java +from .object import JavaClass, JavaObject +from . import burp +from . import scalpel_types + +__all__ = [ + "burp", + "scalpel_types", + "import_java", + "JavaObject", + "JavaBytes", + "JavaClass", +] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/__init__.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/__init__.py new file mode 100644 index 00000000..ccdabe1c --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/__init__.py @@ -0,0 +1,34 @@ +""" + This module exposes Java objects from Burp's extensions API + + If you are a normal user, you should probably never have to manipulate these objects yourself. +""" +from .byte_array import IByteArray, ByteArray +from .http_header import IHttpHeader, HttpHeader +from .http_message import IHttpMessage +from .http_request import IHttpRequest, HttpRequest +from .http_response import IHttpResponse, HttpResponse +from .http_parameter import IHttpParameter, HttpParameter +from .http_service import IHttpService, HttpService +from .http_request_response import IHttpRequestResponse +from .http import IHttp +from .logging import Logging + +__all__ = [ + "IHttp", + "IHttpRequest", + "HttpRequest", + "IHttpResponse", + "HttpResponse", + "IHttpRequestResponse", + "IHttpHeader", + "HttpHeader", + "IHttpMessage", + "IHttpParameter", + "HttpParameter", + "IHttpService", + "HttpService", + "IByteArray", + "ByteArray", + "Logging", +] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/byte_array.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/byte_array.py new file mode 100644 index 00000000..2973c88d --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/byte_array.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +# pylint: disable=invalid-name + +from abc import abstractmethod, ABCMeta +from typing import overload, Protocol +from pyscalpel.java.object import JavaObject +from pyscalpel.java.bytes import JavaBytes +from pyscalpel.java.import_java import import_java + + +class IByteArray(JavaObject, Protocol): # pragma: no cover + __metaclass__ = ABCMeta + + """ generated source for interface ByteArray """ + + # + # * Access the byte stored at the provided index. + # * + # * @param index Index of the byte to be retrieved. + # * + # * @return The byte at the index. + # + @abstractmethod + def getByte(self, index: int) -> int: + """generated source for method getByte""" + + # + # * Sets the byte at the provided index to the provided byte. + # * + # * @param index Index of the byte to be set. + # * @param value The byte to be set. + # + @abstractmethod + @overload + def setByte(self, index: int, value: int) -> None: + """generated source for method setByte""" + + # + # * Sets the byte at the provided index to the provided narrowed integer value. + # * + # * @param index Index of the byte to be set. + # * @param value The integer value to be set after a narrowing primitive conversion to a byte. + # + @abstractmethod + @overload + def setByte(self, index: int, value: int) -> None: + """generated source for method setByte_0""" + + # + # * Sets bytes starting at the specified index to the provided bytes. + # * + # * @param index The index of the first byte to set. + # * @param data The byte[] or sequence of bytes to be set. + # + @abstractmethod + @overload + def setBytes(self, index: int, *data: int) -> None: + """generated source for method setBytes""" + + # + # * Sets bytes starting at the specified index to the provided integers after narrowing primitive conversion to bytes. + # * + # * @param index The index of the first byte to set. + # * @param data The int[] or the sequence of integers to be set after a narrowing primitive conversion to bytes. + # + + @abstractmethod + @overload + def setBytes(self, index: int, byteArray: IByteArray) -> None: + """generated source for method setBytes_1""" + + # + # * Number of bytes stored in the {@code ByteArray}. + # * + # * @return Length of the {@code ByteArray}. + # + @abstractmethod + def length(self) -> int: + """generated source for method length""" + + # + # * Copy of all bytes + # * + # * @return Copy of all bytes. + # + @abstractmethod + def getBytes(self) -> JavaBytes: + """generated source for method getBytes""" + + # + # * New ByteArray with all bytes between the start index (inclusive) and the end index (exclusive). + # * + # * @param startIndexInclusive The inclusive start index of retrieved range. + # * @param endIndexExclusive The exclusive end index of retrieved range. + # * + # * @return ByteArray containing all bytes in the specified range. + # + @abstractmethod + @overload + def subArray( + self, startIndexInclusive: int, endIndexExclusive: int + ) -> "IByteArray": + """generated source for method subArray""" + + # + # * New ByteArray with all bytes in the specified range. + # * + # * @param range The {@link Range} of bytes to be returned. + # * + # * @return ByteArray containing all bytes in the specified range. + # + @abstractmethod + @overload + def subArray(self, _range) -> IByteArray: + """generated source for method subArray_0""" + + # + # * Create a copy of the {@code ByteArray} + # * + # * @return New {@code ByteArray} with a copy of the wrapped bytes. + # + @abstractmethod + def copy(self) -> IByteArray: + """generated source for method copy""" + + # + # * Create a copy of the {@code ByteArray} in temporary file.
    + # * This method is used to save the {@code ByteArray} object to a temporary file, + # * so that it is no longer held in memory. Extensions can use this method to convert + # * {@code ByteArray} objects into a form suitable for long-term usage. + # * + # * @return A new {@code ByteArray} instance stored in temporary file. + # + @abstractmethod + def copyToTempFile(self) -> IByteArray: + """generated source for method copyToTempFile""" + + # + # * Searches the data in the ByteArray for the first occurrence of a specified term. + # * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + # * + # * @param searchTerm The value to be searched for. + # * + # * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + # + @abstractmethod + @overload + def indexOf(self, searchTerm: IByteArray) -> int: + """generated source for method indexOf""" + + # + # * Searches the data in the ByteArray for the first occurrence of a specified term. + # * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + # * + # * @param searchTerm The value to be searched for. + # * + # * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + # + @abstractmethod + @overload + def indexOf(self, searchTerm: str) -> int: + """generated source for method indexOf_0""" + + # + # * Searches the data in the ByteArray for the first occurrence of a specified term. + # * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * + # * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + # + @abstractmethod + @overload + def indexOf(self, searchTerm: IByteArray, caseSensitive: bool) -> int: + """generated source for method indexOf_1""" + + # + # * Searches the data in the ByteArray for the first occurrence of a specified term. + # * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * + # * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + # + @abstractmethod + @overload + def indexOf(self, searchTerm: str, caseSensitive: bool) -> int: + """generated source for method indexOf_2""" + + # + # * Searches the data in the ByteArray for the first occurrence of a specified term. + # * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * @param startIndexInclusive The inclusive start index for the search. + # * @param endIndexExclusive The exclusive end index for the search. + # * + # * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + # + @abstractmethod + @overload + def indexOf( + self, + searchTerm: IByteArray, + caseSensitive: bool, + startIndexInclusive: int, + endIndexExclusive: int, + ) -> int: + """generated source for method indexOf_3""" + + # + # * Searches the data in the ByteArray for the first occurrence of a specified term. + # * It works on byte-based data in a way that is similar to the way the native Java method {@link String#indexOf(String)} works on String-based data. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * @param startIndexInclusive The inclusive start index for the search. + # * @param endIndexExclusive The exclusive end index for the search. + # * + # * @return The offset of the first occurrence of the pattern within the specified bounds, or -1 if no match is found. + # + @abstractmethod + @overload + def indexOf( + self, + searchTerm: str, + caseSensitive: bool, + startIndexInclusive: int, + endIndexExclusive: int, + ) -> int: + """generated source for method indexOf_4""" + + # + # * Searches the data in the ByteArray and counts all matches for a specified term. + # * + # * @param searchTerm The value to be searched for. + # * + # * @return The count of all matches of the pattern + # + @abstractmethod + @overload + def countMatches(self, searchTerm: IByteArray) -> int: + """generated source for method countMatches""" + + # + # * Searches the data in the ByteArray and counts all matches for a specified term. + # * + # * @param searchTerm The value to be searched for. + # * + # * @return The count of all matches of the pattern + # + @abstractmethod + @overload + def countMatches(self, searchTerm: str) -> int: + """generated source for method countMatches_0""" + + # + # * Searches the data in the ByteArray and counts all matches for a specified term. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * + # * @return The count of all matches of the pattern + # + @abstractmethod + @overload + def countMatches(self, searchTerm: IByteArray, caseSensitive: bool) -> int: + """generated source for method countMatches_1""" + + # + # * Searches the data in the ByteArray and counts all matches for a specified term. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * + # * @return The count of all matches of the pattern + # + @abstractmethod + @overload + def countMatches(self, searchTerm: str, caseSensitive: bool) -> int: + """generated source for method countMatches_2""" + + # + # * Searches the data in the ByteArray and counts all matches for a specified term. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * @param startIndexInclusive The inclusive start index for the search. + # * @param endIndexExclusive The exclusive end index for the search. + # * + # * @return The count of all matches of the pattern within the specified bounds + # + @abstractmethod + @overload + def countMatches( + self, + searchTerm: IByteArray, + caseSensitive: bool, + startIndexInclusive: int, + endIndexExclusive: int, + ) -> int: + """generated source for method countMatches_3""" + + # + # * Searches the data in the ByteArray and counts all matches for a specified term. + # * + # * @param searchTerm The value to be searched for. + # * @param caseSensitive Flags whether the search is case-sensitive. + # * @param startIndexInclusive The inclusive start index for the search. + # * @param endIndexExclusive The exclusive end index for the search. + # * + # * @return The count of all matches of the pattern within the specified bounds + # + @abstractmethod + @overload + def countMatches( + self, + searchTerm: str, + caseSensitive: bool, + startIndexInclusive: int, + endIndexExclusive: int, + ) -> int: + """generated source for method countMatches_4""" + + # + # * Convert the bytes of the ByteArray into String form using the encoding specified by Burp Suite. + # * + # * @return The converted data in String form. + # + @abstractmethod + def __str__(self) -> str: + """generated source for method toString""" + + # + # * Create a copy of the {@code ByteArray} appended with the provided bytes. + # * + # * @param data The byte[] or sequence of bytes to append. + # + @abstractmethod + def withAppended(self, *data: int) -> IByteArray: + """generated source for method withAppended""" + + # + # * Create a copy of the {@code ByteArray} appended with the provided integers after narrowing primitive conversion to bytes. + # * + # * @param data The int[] or sequence of integers to append after narrowing primitive conversion to bytes. + # + + # + @abstractmethod + def byteArrayOfLength(self, length: int) -> IByteArray: + """generated source for method byteArrayOfLength""" + + # + # * Create a new {@code ByteArray} with the provided byte data.
    + # * + # * @param data byte[] to wrap, or sequence of bytes to wrap. + # * + # * @return New {@code ByteArray} wrapping the provided byte array. + # + # @abstractmethod + @abstractmethod + def byteArray(self, data: bytes | JavaBytes | list[int] | str) -> IByteArray: + """generated source for method byteArray""" + + # + # * Create a new {@code ByteArray} with the provided integers after a narrowing primitive conversion to bytes.
    + # * + # * @param data bytes. + # * + # * @return New {@code ByteArray} wrapping the provided data after a narrowing primitive conversion to bytes. + # + + +ByteArray: IByteArray = import_java("burp.api.montoya.core", "ByteArray", IByteArray) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http.py new file mode 100644 index 00000000..26278d76 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +# pylint: disable=invalid-name +# Stubs for https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/Http.html +from abc import abstractmethod, ABCMeta +from typing import Protocol +from pyscalpel.java.object import JavaObject +from pyscalpel.java.burp import IHttpRequest, IHttpRequestResponse + + +class IHttp(JavaObject, Protocol, metaclass=ABCMeta): # pragma: no cover + """generated source for interface Http""" + + __metaclass__ = ABCMeta + + @abstractmethod + def sendRequest(self, request: IHttpRequest) -> IHttpRequestResponse: + ... + + +# This class can only be instantiated with the API diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_header.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_header.py new file mode 100644 index 00000000..4219d827 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_header.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +# pylint: disable=invalid-name +# +# * Burp HTTP header able to retrieve to hold details about an HTTP header. +# + + +from abc import abstractmethod, ABCMeta +from typing import overload, Protocol +from pyscalpel.java.object import JavaObject +from pyscalpel.java.import_java import import_java + + +class IHttpHeader(JavaObject, Protocol, metaclass=ABCMeta): # pragma: no cover + """generated source for interface HttpHeader""" + + __metaclass__ = ABCMeta + # + # * @return The name of the header. + # + + @abstractmethod + def name(self) -> str: + """generated source for method name""" + + # + # * @return The value of the header. + # + @abstractmethod + def value(self) -> str: + """generated source for method value""" + + # + # * @return The {@code String} representation of the header. + # + @abstractmethod + def __str__(self): + """generated source for method toString""" + + # + # * Create a new instance of {@code HttpHeader} from name and value. + # * + # * @param name The name of the header. + # * @param value The value of the header. + # * + # * @return A new {@code HttpHeader} instance. + # + @abstractmethod + @overload + def httpHeader(self, name: str, value: str) -> IHttpHeader: + """generated source for method httpHeader""" + + # + # * Create a new instance of HttpHeader from a {@code String} header representation. + # * It will be parsed according to the HTTP/1.1 specification for headers. + # * + # * @param header The {@code String} header representation. + # * + # * @return A new {@code HttpHeader} instance. + # + @abstractmethod + @overload + def httpHeader(self, header: str) -> IHttpHeader: + """generated source for method httpHeader_0""" + + +HttpHeader: IHttpHeader = import_java( + "burp.api.montoya.http.message", "HttpHeader", IHttpHeader +) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_message.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_message.py new file mode 100644 index 00000000..6858aaa0 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_message.py @@ -0,0 +1,78 @@ +# pylint: disable=invalid-name + +from abc import abstractmethod +from typing import Protocol +from pyscalpel.java.burp.byte_array import IByteArray +from pyscalpel.java.burp.http_header import IHttpHeader +from pyscalpel.java.object import JavaObject + +# +# * Burp message retrieve common information shared by {@link HttpRequest} and {@link HttpResponse}. +# + + +class IHttpMessage(JavaObject, Protocol): # pragma: no cover + """generated source for interface HttpMessage""" + + # + # * HTTP headers contained in the message. + # * + # * @return A list of HTTP headers. + # + @abstractmethod + def headers(self) -> IHttpHeader: + """generated source for method headers""" + + # + # * Offset within the message where the message body begins. + # * + # * @return The message body offset. + # + @abstractmethod + def bodyOffset(self) -> int: + """generated source for method bodyOffset""" + + # + # * Body of a message as a byte array. + # * + # * @return The body of a message as a byte array. + # + @abstractmethod + def body(self) -> IByteArray: + """generated source for method body""" + + # + # * Body of a message as a {@code String}. + # * + # * @return The body of a message as a {@code String}. + # + @abstractmethod + def bodyToString(self) -> str: + """generated source for method bodyToString""" + + # + # * Markers for the message. + # * + # * @return A list of markers. + # + @abstractmethod + def markers(self) -> JavaObject: + """generated source for method markers""" + + # + # * Message as a byte array. + # * + # * @return The message as a byte array. + # + @abstractmethod + def toByteArray(self) -> IByteArray: + """generated source for method toByteArray""" + + # + # * Message as a {@code String}. + # * + # * @return The message as a {@code String}. + # + @abstractmethod + def __str__(self) -> str: + """generated source for method toString""" diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_parameter.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_parameter.py new file mode 100644 index 00000000..530a59a5 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_parameter.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +# pylint: disable=invalid-name +# +# * Burp HTTP parameter able to retrieve to hold details about an HTTP request parameter. +# + + +from abc import ABCMeta, abstractmethod +from pyscalpel.java.object import JavaObject +from pyscalpel.java.import_java import import_java + + +class IHttpParameter(JavaObject): # pragma: no cover + """generated source for interface HttpParameter""" + + __metaclass__ = ABCMeta + # + # * @return The parameter type. + # + + @abstractmethod + def type_(self) -> JavaObject: + """generated source for method type_""" + + # + # * @return The parameter name. + # + @abstractmethod + def name(self) -> str: + """generated source for method name""" + + # + # * @return The parameter value. + # + @abstractmethod + def value(self) -> str: + """generated source for method value""" + + # + # * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#URL} type. + # * + # * @param name The parameter name. + # * @param value The parameter value. + # * + # * @return A new {@code HttpParameter} instance. + # + @abstractmethod + def urlParameter(self, name: str, value: str) -> IHttpParameter: + """generated source for method urlParameter""" + + # + # * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#BODY} type. + # * + # * @param name The parameter name. + # * @param value The parameter value. + # * + # * @return A new {@code HttpParameter} instance. + # + @abstractmethod + def bodyParameter(self, name: str, value: str) -> IHttpParameter: + """generated source for method bodyParameter""" + + # + # * Create a new Instance of {@code HttpParameter} with {@link HttpParameterType#COOKIE} type. + # * + # * @param name The parameter name. + # * @param value The parameter value. + # * + # * @return A new {@code HttpParameter} instance. + # + @abstractmethod + def cookieParameter(self, name: str, value: str) -> IHttpParameter: + """generated source for method cookieParameter""" + + # + # * Create a new Instance of {@code HttpParameter} with the specified type. + # * + # * @param name The parameter name. + # * @param value The parameter value. + # * @param type The header type. + # * + # * @return A new {@code HttpParameter} instance. + # + @abstractmethod + def parameter(self, name: str, value: str, type_: JavaObject) -> IHttpParameter: + """generated source for method parameter""" + + +# from burp.api.montoya.http.message.params import ( # pylint: disable=import-error # type: ignore +# HttpParameter as _BurpHttpParameter, +# ) + +# HttpParameter: IHttpParameter = _BurpHttpParameter + +HttpParameter: IHttpParameter = import_java( + "burp.api.montoya.http.message.params", "HttpParameter", IHttpParameter +) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_request.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_request.py new file mode 100644 index 00000000..fd027c05 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_request.py @@ -0,0 +1,489 @@ +from __future__ import annotations + +# pylint: disable=invalid-name + +from abc import abstractmethod +from typing import Iterable, Protocol, overload +from pyscalpel.java.burp.byte_array import IByteArray +from pyscalpel.java.burp.http_header import IHttpHeader +from pyscalpel.java.burp.http_message import IHttpMessage +from pyscalpel.java.burp.http_parameter import IHttpParameter +from pyscalpel.java.burp.http_service import IHttpService +from pyscalpel.java.object import JavaObject +from pyscalpel.java.import_java import import_java + +# * Burp HTTP request able to retrieve and modify details of an HTTP request. +# + + +class IHttpRequest(IHttpMessage, Protocol): # pragma: no cover + """generated source for interface HttpRequest""" + + # * HTTP service for the request. + # * + # * @return An {@link HttpService} object containing details of the HTTP service. + # + + @abstractmethod + def httpService(self) -> IHttpService: + """generated source for method httpService""" + + # + # * URL for the request. + # * If the request is malformed, then a {@link MalformedRequestException} is thrown. + # * + # * @return The URL in the request. + # * @throws MalformedRequestException if request is malformed. + # + + @abstractmethod + def url(self) -> str: + """generated source for method url""" + + # + # * HTTP method for the request. + # * If the request is malformed, then a {@link MalformedRequestException} is thrown. + # * + # * @return The HTTP method used in the request. + # * @throws MalformedRequestException if request is malformed. + # + + @abstractmethod + def method(self) -> str: + """generated source for method method""" + + # + # * Path and File for the request. + # * If the request is malformed, then a {@link MalformedRequestException} is thrown. + # * + # * @return the path and file in the request + # * @throws MalformedRequestException if request is malformed. + # + + @abstractmethod + def path(self) -> str: + """generated source for method path""" + + # + # * HTTP Version text parsed from the request line for HTTP 1 messages. + # * HTTP 2 messages will return "HTTP/2" + # * + # * @return Version string + # + + @abstractmethod + def httpVersion(self) -> str | None: + """generated source for method httpVersion""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def headers(self) -> list[IHttpHeader]: + """generated source for method headers""" + + # + # * @return The detected content type of the request. + # + + @abstractmethod + def contentType(self) -> JavaObject: + """generated source for method contentType""" + + # + # * @return The parameters contained in the request. + # + + @abstractmethod + def parameters(self) -> list[IHttpParameter]: + """generated source for method parameters""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def body(self) -> IByteArray: + """generated source for method body""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def bodyToString(self) -> str: + """generated source for method bodyToString""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def bodyOffset(self) -> int: + """generated source for method bodyOffset""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def markers(self): + """generated source for method markers""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def toByteArray(self) -> IByteArray: + """generated source for method toByteArray""" + + # + # * {@inheritDoc} + # + + @abstractmethod + def __str__(self) -> str: + """generated source for method toString""" + + # + # * Create a copy of the {@code HttpRequest} in temporary file.
    + # * This method is used to save the {@code HttpRequest} object to a temporary file, + # * so that it is no longer held in memory. Extensions can use this method to convert + # * {@code HttpRequest} objects into a form suitable for long-term usage. + # * + # * @return A new {@code ByteArray} instance stored in temporary file. + # + + @abstractmethod + def copyToTempFile(self) -> IHttpRequest: + """generated source for method copyToTempFile""" + + # + # * Create a copy of the {@code HttpRequest} with the new service. + # * + # * @param service An {@link HttpService} reference to add. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withService(self, service: IHttpService) -> IHttpRequest: + """generated source for method withService""" + + # + # * Create a copy of the {@code HttpRequest} with the new path. + # * + # * @param path The path to use. + # * + # * @return A new {@code HttpRequest} instance with updated path. + # + + @abstractmethod + def withPath(self, path: str) -> IHttpRequest: + """generated source for method withPath""" + + # + # * Create a copy of the {@code HttpRequest} with the new method. + # * + # * @param method the method to use + # * + # * @return a new {@code HttpRequest} instance with updated method. + # + + @abstractmethod + def withMethod(self, method: str) -> IHttpRequest: + """generated source for method withMethod""" + + # + # * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + # * + # * @param parameters HTTP parameters to add. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withAddedParameters(self, parameters: Iterable[IHttpParameter]) -> IHttpRequest: + """generated source for method withAddedParameters""" + + # + # * Create a copy of the {@code HttpRequest} with the added HTTP parameters. + # * + # * @param parameters HTTP parameters to add. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withAddedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest: + """generated source for method withAddedParameters_0""" + + # + # * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + # * + # * @param parameters HTTP parameters to remove. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withRemovedParameters( + self, parameters: Iterable[IHttpParameter] + ) -> IHttpRequest: + """generated source for method withRemovedParameters""" + + # + # * Create a copy of the {@code HttpRequest} with the removed HTTP parameters. + # * + # * @param parameters HTTP parameters to remove. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withRemovedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest: + """generated source for method withRemovedParameters_0""" + + # + # * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
    + # * If a parameter does not exist in the request, a new one will be added. + # * + # * @param parameters HTTP parameters to update. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withUpdatedParameters(self, parameters: list[IHttpParameter]) -> IHttpRequest: + """generated source for method withUpdatedParameters""" + + # + # * Create a copy of the {@code HttpRequest} with the updated HTTP parameters.
    + # * If a parameter does not exist in the request, a new one will be added. + # * + # * @param parameters HTTP parameters to update. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withUpdatedParameters_0(self, *parameters: IHttpParameter) -> IHttpRequest: + """generated source for method withUpdatedParameters_0""" + + # + # * Create a copy of the {@code HttpRequest} with the transformation applied. + # * + # * @param transformation Transformation to apply. + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withTransformationApplied(self, transformation) -> IHttpRequest: + """generated source for method withTransformationApplied""" + + # + # * Create a copy of the {@code HttpRequest} with the updated body.
    + # * Updates Content-Length header. + # * + # * @param body the new body for the request + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withBody(self, body) -> IHttpRequest: + """generated source for method withBody""" + + # + # * Create a copy of the {@code HttpRequest} with the updated body.
    + # * Updates Content-Length header. + # * + # * @param body the new body for the request + # * + # * @return A new {@code HttpRequest} instance. + # + + @abstractmethod + def withBody_0(self, body: IByteArray) -> IHttpRequest: + """generated source for method withBody_0""" + + # + # * Create a copy of the {@code HttpRequest} with the added header. + # * + # * @param name The name of the header. + # * @param value The value of the header. + # * + # * @return The updated HTTP request with the added header. + # + + @abstractmethod + def withAddedHeader(self, name: str, value: str) -> IHttpRequest: + """generated source for method withAddedHeader""" + + # + # * Create a copy of the {@code HttpRequest} with the added header. + # * + # * @param header The {@link HttpHeader} to add to the HTTP request. + # * + # * @return The updated HTTP request with the added header. + # + + @abstractmethod + def withAddedHeader_0(self, header: IHttpHeader) -> IHttpRequest: + """generated source for method withAddedHeader_0""" + + # + # * Create a copy of the {@code HttpRequest} with the updated header. + # * + # * @param name The name of the header to update the value of. + # * @param value The new value of the specified HTTP header. + # * + # * @return The updated request containing the updated header. + # + + @abstractmethod + def withUpdatedHeader(self, name: str, value: str) -> IHttpRequest: + """generated source for method withUpdatedHeader""" + + # + # * Create a copy of the {@code HttpRequest} with the updated header. + # * + # * @param header The {@link HttpHeader} to update containing the new value. + # * + # * @return The updated request containing the updated header. + # + + @abstractmethod + def withUpdatedHeader_0(self, header: IHttpHeader) -> IHttpRequest: + """generated source for method withUpdatedHeader_0""" + + # + # * Removes an existing HTTP header from the current request. + # * + # * @param name The name of the HTTP header to remove from the request. + # * + # * @return The updated request containing the removed header. + # + + @abstractmethod + def withRemovedHeader(self, name: str) -> IHttpRequest: + """generated source for method withRemovedHeader""" + + # + # * Removes an existing HTTP header from the current request. + # * + # * @param header The {@link HttpHeader} to remove from the request. + # * + # * @return The updated request containing the removed header. + # + + @abstractmethod + def withRemovedHeader_0(self, header: IHttpHeader) -> IHttpRequest: + """generated source for method withRemovedHeader_0""" + + # + # * Create a copy of the {@code HttpRequest} with the added markers. + # * + # * @param markers Request markers to add. + # * + # * @return A new {@code MarkedHttpRequestResponse} instance. + # + + @abstractmethod + def withMarkers(self, markers) -> IHttpRequest: + """generated source for method withMarkers""" + + # + # * Create a copy of the {@code HttpRequest} with the added markers. + # * + # * @param markers Request markers to add. + # * + # * @return A new {@code MarkedHttpRequestResponse} instance. + # + + @abstractmethod + def withMarkers_0(self, *markers) -> IHttpRequest: + """generated source for method withMarkers_0""" + + # + # * Create a copy of the {@code HttpRequest} with added default headers. + # * + # * @return a new (@code HttpRequest) with added default headers + # + + @abstractmethod + def withDefaultHeaders(self) -> IHttpRequest: + """generated source for method withDefaultHeaders""" + + # + # * Create a new empty instance of {@link HttpRequest}.
    + # * + # * @². + # + + @abstractmethod + @overload + def httpRequest(self, request: IByteArray | str) -> IHttpRequest: + """generated source for method httpRequest""" + + @abstractmethod + @overload + def httpRequest(self, service: IHttpService, req: IByteArray | str) -> IHttpRequest: + """generated source for method httpRequest""" + + # + # * Create a new instance of {@link HttpRequest}.
    + # * + # * + # * @². + # + # + + @abstractmethod + def httpRequestFromUrl(self, url: str) -> IHttpRequest: + """generated source for method httpRequestFromUrl""" + + # + # * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.
    + # * + # * @param service An HTTP service for the request. + # * @param headers A list of HTTP 2 headers. + # * @param body A body of the HTTP 2 request. + # * + # * @². + # + + @abstractmethod + def http2Request( + self, service: IHttpService, headers: Iterable[IHttpHeader], body: IByteArray + ) -> IHttpRequest: + """generated source for method http2Request""" + + # + # * Create a new instance of {@link HttpRequest} containing HTTP 2 headers and body.
    + # * + # * @param service An HTTP service for the request. + # * @param headers A list of HTTP 2 headers. + # * @param body A body of the HTTP 2 request. + # * + # * @². + # + + +# if "pdoc" in modules: +# _BurpHttpRequest = cast(IHttpRequest, None) +# else: +# try: +# from burp.api.montoya.http.message.requests import ( # pylint: disable=import-error # type: ignore +# HttpRequest as _BurpHttpRequest, +# ) +# except ImportError as exc: +# raise ImportError("Could not import Java class HttpRequest") from exc + +# HttpRequest = _BurpHttpRequest +HttpRequest: IHttpRequest = import_java( + "burp.api.montoya.http.message.requests", "HttpRequest", IHttpRequest +) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_request_response.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_request_response.py new file mode 100644 index 00000000..e7026c6a --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_request_response.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +# pylint: disable=invalid-name +# Stubs for https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/http/message/HttpRequestResponse.html +from abc import abstractmethod, ABCMeta +from typing import Protocol +from pyscalpel.java.object import JavaObject +from pyscalpel.java.burp import IHttpRequest, IHttpResponse + + +class IHttpRequestResponse(JavaObject, Protocol, metaclass=ABCMeta): # pragma: no cover + """generated source for interface HttpRequestResponse""" + + __metaclass__ = ABCMeta + + @abstractmethod + def request(self) -> IHttpRequest | None: + ... + + @abstractmethod + def response(self) -> IHttpResponse | None: + ... diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_response.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_response.py new file mode 100644 index 00000000..b3eb9d62 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_response.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +# pylint: disable=invalid-name + +from abc import abstractmethod +from typing import overload, Protocol +from pyscalpel.java.burp.http_message import IHttpMessage +from pyscalpel.java.burp.byte_array import IByteArray +from pyscalpel.java.burp.http_header import IHttpHeader +from pyscalpel.java.object import JavaObject +from pyscalpel.java.import_java import import_java + +# * Burp HTTP response able to retrieve and modify details about an HTTP response. +# + + +class IHttpResponse(IHttpMessage, Protocol): # pragma: no cover + """generated source for interface HttpResponse""" + + # + # * Obtain the HTTP status code contained in the response. + # * + # * @return HTTP status code. + # + @abstractmethod + def statusCode(self) -> int: + """generated source for method statusCode""" + + # + # * Obtain the HTTP reason phrase contained in the response for HTTP 1 messages. + # * HTTP 2 messages will return a mapped phrase based on the status code. + # * + # * @return HTTP Reason phrase. + # + @abstractmethod + def reasonPhrase(self) -> str | None: + """generated source for method reasonPhrase""" + + # + # * Return the HTTP Version text parsed from the response line for HTTP 1 messages. + # * HTTP 2 messages will return "HTTP/2" + # * + # * @return Version string + # + @abstractmethod + def httpVersion(self) -> str | None: + """generated source for method httpVersion""" + + # + # * {@inheritDoc} + # + @abstractmethod + def headers(self) -> list[IHttpHeader]: + """generated source for method headers""" + + # + # * {@inheritDoc} + # + @abstractmethod + def body(self) -> IByteArray | None: + """generated source for method body""" + + # + # * {@inheritDoc} + # + @abstractmethod + def bodyToString(self) -> str: + """generated source for method bodyToString""" + + # + # * {@inheritDoc} + # + @abstractmethod + def bodyOffset(self) -> int: + """generated source for method bodyOffset""" + + # + # * {@inheritDoc} + # + @abstractmethod + def markers(self) -> JavaObject: + """generated source for method markers""" + + # + # * Obtain details of the HTTP cookies set in the response. + # * + # * @return A list of {@link Cookie} objects representing the cookies set in the response, if any. + # + @abstractmethod + def cookies(self) -> JavaObject: + """generated source for method cookies""" + + # + # * Obtain the MIME type of the response, as stated in the HTTP headers. + # * + # * @return The stated MIME type. + # + @abstractmethod + def statedMimeType(self) -> JavaObject: + """generated source for method statedMimeType""" + + # + # * Obtain the MIME type of the response, as inferred from the contents of the HTTP message body. + # * + # * @return The inferred MIME type. + # + @abstractmethod + def inferredMimeType(self) -> JavaObject: + """generated source for method inferredMimeType""" + + # + # * Retrieve the number of types given keywords appear in the response. + # * + # * @param keywords Keywords to count. + # * + # * @return List of keyword counts in the order they were provided. + # + @abstractmethod + def keywordCounts(self, *keywords) -> int: + """generated source for method keywordCounts""" + + # + # * Retrieve the values of response attributes. + # * + # * @param types Response attributes to retrieve values for. + # * + # * @return List of {@link Attribute} objects. + # + @abstractmethod + def attributes(self, *types) -> JavaObject: + """generated source for method attributes""" + + # + # * {@inheritDoc} + # + @abstractmethod + def toByteArray(self) -> IByteArray: + """generated source for method toByteArray""" + + # + # * {@inheritDoc} + # + @abstractmethod + def __str__(self) -> str: + """generated source for method toString""" + + # + # * Create a copy of the {@code HttpResponse} in temporary file.
    + # * This method is used to save the {@code HttpResponse} object to a temporary file, + # * so that it is no longer held in memory. Extensions can use this method to convert + # * {@code HttpResponse} objects into a form suitable for long-term usage. + # * + # * @return A new {@code HttpResponse} instance stored in temporary file. + # + @abstractmethod + def copyToTempFile(self) -> IHttpResponse: + """generated source for method copyToTempFile""" + + # + # * Create a copy of the {@code HttpResponse} with the provided status code. + # * + # * @param statusCode the new status code for response + # * + # * @return A new {@code HttpResponse} instance. + # + @abstractmethod + def withStatusCode(self, statusCode: int) -> IHttpResponse: + """generated source for method withStatusCode""" + + # + # * Create a copy of the {@code HttpResponse} with the new reason phrase. + # * + # * @param reasonPhrase the new reason phrase for response + # * + # * @return A new {@code HttpResponse} instance. + # + @abstractmethod + def withReasonPhrase(self, reasonPhrase: str) -> IHttpResponse: + """generated source for method withReasonPhrase""" + + # + # * Create a copy of the {@code HttpResponse} with the new http version. + # * + # * @param httpVersion the new http version for response + # * + # * @return A new {@code HttpResponse} instance. + # + @abstractmethod + def withHttpVersion(self, httpVersion: str) -> IHttpResponse: + """generated source for method withHttpVersion""" + + # + # * Create a copy of the {@code HttpResponse} with the updated body.
    + # * Updates Content-Length header. + # * + # * @param body the new body for the response + # * + # * @return A new {@code HttpResponse} instance. + # + @abstractmethod + def withBody(self, body: IByteArray | str) -> IHttpResponse: + """generated source for method withBody""" + + # + # * Create a copy of the {@code HttpResponse} with the added header. + # * + # * @param header The {@link HttpHeader} to add to the response. + # * + # * @return The updated response containing the added header. + # + # @abstractmethod + # def withAddedHeader(self, header) -> 'IHttpResponse': + # """ generated source for method withAddedHeader """ + + # # + # # * Create a copy of the {@code HttpResponse} with the added header. + # # * + # # * @param name The name of the header. + # # * @param value The value of the header. + # # * + # # * @return The updated response containing the added header. + # # + @abstractmethod + def withAddedHeader(self, name: str, value: str) -> IHttpResponse: + """generated source for method withAddedHeader_0""" + + # + # * Create a copy of the {@code HttpResponse} with the updated header. + # * + # * @param header The {@link HttpHeader} to update containing the new value. + # * + # * @return The updated response containing the updated header. + # + # @abstractmethod + # def withUpdatedHeader(self, header) -> 'IHttpResponse': + # """ generated source for method withUpdatedHeader """ + + # # + # # * Create a copy of the {@code HttpResponse} with the updated header. + # # * + # # * @param name The name of the header to update the value of. + # # * @param value The new value of the specified HTTP header. + # # * + # # * @return The updated response containing the updated header. + # # + @abstractmethod + def withUpdatedHeader(self, name: str, value: str) -> IHttpResponse: + """generated source for method withUpdatedHeader_0""" + + # + # * Create a copy of the {@code HttpResponse} with the removed header. + # * + # * @param header The {@link HttpHeader} to remove from the response. + # * + # * @return The updated response containing the removed header. + # + # @abstractmethod + # def withRemovedHeader(self, header) -> 'IHttpResponse': + # """ generated source for method withRemovedHeader """ + + # # + # # * Create a copy of the {@code HttpResponse} with the removed header. + # # * + # # * @param name The name of the HTTP header to remove from the response. + # # * + # # * @return The updated response containing the removed header. + # # + @abstractmethod + def withRemovedHeader(self, name: str) -> IHttpResponse: + """generated source for method withRemovedHeader_0""" + + # + # * Create a copy of the {@code HttpResponse} with the added markers. + # * + # * @param markers Request markers to add. + # * + # * @return A new {@code MarkedHttpRequestResponse} instance. + # + @abstractmethod + @overload + def withMarkers(self, markers: JavaObject) -> IHttpResponse: + """generated source for method withMarkers""" + + # + # * Create a copy of the {@code HttpResponse} with the added markers. + # * + # * @param markers Request markers to add. + # * + # * @return A new {@code MarkedHttpRequestResponse} instance. + # + @abstractmethod + @overload + def withMarkers(self, *markers: JavaObject) -> IHttpResponse: + """generated source for method withMarkers_0""" + + # + # * Create a new empty instance of {@link HttpResponse}.
    + # * + # * @return A new {@link HttpResponse} instance. + # + @abstractmethod + def httpResponse(self, response: IByteArray | str) -> IHttpResponse: + """generated source for method httpResponse""" + + +HttpResponse: IHttpResponse = import_java( + "burp.api.montoya.http.message.responses", "HttpResponse", IHttpResponse +) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_service.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_service.py new file mode 100644 index 00000000..8cbe555f --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/http_service.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +# pylint: disable=invalid-name + +from abc import ABCMeta, abstractmethod +from typing import overload +from pyscalpel.java.object import JavaObject +from pyscalpel.java.import_java import import_java + + +class IHttpService(JavaObject, metaclass=ABCMeta): # pragma: no cover + @abstractmethod + def host(self) -> str: + """The hostname or IP address for the service.""" + + @abstractmethod + @overload + def httpService(self, baseUrl: str) -> IHttpService: + """Create a new instance of {@code HttpService} from a base URL.""" + + @abstractmethod + @overload + def httpService(self, baseUrl: str, secure: bool) -> IHttpService: + """Create a new instance of {@code HttpService} from a base URL and a protocol.""" + + @abstractmethod + @overload + def httpService(self, host: str, port: int, secure: bool) -> IHttpService: + """Create a new instance of {@code HttpService} from a host, a port and a protocol.""" + + @abstractmethod + def httpService(self, *args, **kwargs) -> IHttpService: + """Create a new instance of {@code HttpService} from a host, a port and a protocol.""" + + @abstractmethod + def port(self) -> int: + """The port number for the service.""" + + @abstractmethod + def secure(self) -> bool: + """True if a secure protocol is used for the connection, false otherwise.""" + + @abstractmethod + def __str__(self) -> str: + """The {@code String} representation of the service.""" + + +HttpService: IHttpService = import_java( + "burp.api.montoya.http", "HttpService", IHttpService +) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/logging.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/logging.py new file mode 100644 index 00000000..d5a60d81 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/burp/logging.py @@ -0,0 +1,95 @@ +# pylint: disable=invalid-name + +from abc import abstractmethod +from pyscalpel.java.object import JavaObject + +# +# * Provides access to the functionality related to logging and events. +# + + +class Logging(JavaObject): # pragma: no cover + """generated source for interface Logging""" + + # + # * Obtain the current extension's standard output + # * stream. Extensions should write all output to this stream, allowing the + # * Burp user to configure how that output is handled from within the UI. + # * + # * @return The extension's standard output stream. + # + @abstractmethod + def output(self) -> JavaObject: + """generated source for method output""" + + # + # * Obtain the current extension's standard error + # * stream. Extensions should write all error messages to this stream, + # * allowing the Burp user to configure how that output is handled from + # * within the UI. + # * + # * @return The extension's standard error stream. + # + @abstractmethod + def error(self) -> JavaObject: + """generated source for method error""" + + # + # * This method prints a line of output to the current extension's standard + # * output stream. + # * + # * @param message The message to print. + # + @abstractmethod + def logToOutput(self, message: str) -> None: + """generated source for method logToOutput""" + + # + # * This method prints a line of output to the current extension's standard + # * error stream. + # * + # * @param message The message to print. + # + @abstractmethod + def error(self, message: str) -> None: + """generated source for method error""" + + # + # * This method can be used to display a debug event in the Burp Suite + # * event log. + # * + # * @param message The debug message to display. + # + @abstractmethod + def raiseDebugEvent(self, message: str) -> None: + """generated source for method raiseDebugEvent""" + + # + # * This method can be used to display an informational event in the Burp + # * Suite event log. + # * + # * @param message The informational message to display. + # + @abstractmethod + def raiseInfoEvent(self, message: str) -> None: + """generated source for method raiseInfoEvent""" + + # + # * This method can be used to display an error event in the Burp Suite + # * event log. + # * + # * @param message The error message to display. + # + @abstractmethod + def raiseErrorEvent(self, message: str) -> None: + """generated source for method raiseErrorEvent""" + + # + # * This method can be used to display a critical event in the Burp Suite + # * event log. + # * + # * @param message The critical message to display. + # + @abstractmethod + def raiseCriticalEvent(self, message: str) -> None: + """generated source for method raiseCriticalEvent""" diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/bytes.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/bytes.py new file mode 100644 index 00000000..0155f00a --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/bytes.py @@ -0,0 +1,5 @@ +from abc import ABCMeta + + +class JavaBytes(list[int]): + __metaclass__ = ABCMeta diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/import_java.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/import_java.py new file mode 100644 index 00000000..ed7ccf6c --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/import_java.py @@ -0,0 +1,34 @@ +import os + +from typing import cast, Type, TypeVar +from functools import lru_cache +from sys import modules + +from pyscalpel.java.object import JavaObject + + +@lru_cache +def _is_pdoc() -> bool: # pragma: no cover + return "pdoc" in modules + + +ExpectedObject = TypeVar("ExpectedObject") + + +def import_java( + module: str, name: str, expected_type: Type[ExpectedObject] = JavaObject +) -> ExpectedObject: + """Import a Java class using Python's import mechanism. + + :param module: The module to import from. (e.g. "java.lang") + :param name: The name of the class to import. (e.g. "String") + :param expected_type: The expected type of the class. (e.g. JavaObject) + :return: The imported class. + """ + if _is_pdoc() or os.environ.get("_DO_NOT_IMPORT_JAVA") is not None: + return None # type: ignore + try: # pragma: no cover + module = __import__(module, fromlist=[name]) + return getattr(module, name) + except ImportError as exc: # pragma: no cover + raise ImportError(f"Could not import Java class {name}") from exc diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/object.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/object.py new file mode 100644 index 00000000..ac5cf867 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/object.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +# pylint: disable=invalid-name + +from abc import abstractmethod, ABCMeta +from typing import overload, Protocol + + +class JavaObject(Protocol, metaclass=ABCMeta): + """generated source for class Object""" + + @abstractmethod + def __init__(self): + """generated source for method __init__""" + + @abstractmethod + def getClass(self) -> JavaClass: + """generated source for method getClass""" + + @abstractmethod + def hashCode(self) -> int: + """generated source for method hashCode""" + + @abstractmethod + def equals(self, obj) -> bool: + """generated source for method equals""" + + @abstractmethod + def clone(self) -> JavaObject: + """generated source for method clone""" + + @abstractmethod + def __str__(self) -> str: + """generated source for method toString""" + + @abstractmethod + def notify(self) -> None: + """generated source for method notify""" + + @abstractmethod + def notifyAll(self) -> None: + """generated source for method notifyAll""" + + @abstractmethod + @overload + def wait(self) -> None: + """generated source for method wait""" + + @abstractmethod + @overload + def wait(self, arg0: int) -> None: + """generated source for method wait_0""" + + @abstractmethod + @overload + def wait(self, timeoutMillis: int, nanos: int) -> None: + """generated source for method wait_1""" + + @abstractmethod + def finalize(self) -> None: + """generated source for method finalize""" + + +class JavaClass(JavaObject, metaclass=ABCMeta): + pass diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/__init__.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/__init__.py new file mode 100644 index 00000000..421d1a23 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/__init__.py @@ -0,0 +1,4 @@ +from .context import Context +from .utils import IPythonUtils, PythonUtils + +__all__ = ["Context", "IPythonUtils", "PythonUtils"] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/context.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/context.py new file mode 100644 index 00000000..51e20bf2 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/context.py @@ -0,0 +1,28 @@ +from typing import TypedDict +from typing import Any + + +class Context(TypedDict): + """Scalpel Python execution context""" + + API: Any + """ + The Burp [Montoya API] + (https://portswigger.github.io/burp-extensions-montoya-api/javadoc/burp/api/montoya/MontoyaApi.html) + root object. + + Allows you to interact with Burp by directly manipulating the Java object. + + """ + + directory: str + """The framework directory""" + + user_script: str + """The loaded script path""" + + framework: str + """The framework (loader script) path""" + + venv: str + """The venv the script was loaded in""" diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/utils.py b/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/utils.py new file mode 100644 index 00000000..7e64f510 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/java/scalpel_types/utils.py @@ -0,0 +1,41 @@ +# pylint: disable=invalid-name + +from typing import TypeVar +from abc import ABCMeta, abstractmethod +from pyscalpel.java.object import JavaObject +from pyscalpel.java.bytes import JavaBytes +from pyscalpel.java.burp.http_request import IHttpRequest +from pyscalpel.java.burp.http_response import IHttpResponse +from pyscalpel.java.burp.byte_array import IByteArray +from pyscalpel.java.import_java import import_java + +RequestOrResponse = TypeVar("RequestOrResponse", bound=IHttpRequest | IHttpResponse) + + +class IPythonUtils(JavaObject): # pragma: no cover + __metaclass__ = ABCMeta + + @abstractmethod + def toPythonBytes(self, java_bytes: JavaBytes) -> list[int]: + pass + + @abstractmethod + def toJavaBytes(self, python_bytes: bytes | list[int] | bytearray) -> JavaBytes: + pass + + @abstractmethod + def toByteArray(self, python_bytes: bytes | list[int] | bytearray) -> IByteArray: + pass + + @abstractmethod + def getClassName(self, msg: JavaObject) -> str: + pass + + @abstractmethod + def updateHeader( + self, msg: RequestOrResponse, name: str, value: str + ) -> RequestOrResponse: + pass + + +PythonUtils: IPythonUtils = import_java("lexfo.scalpel", "PythonUtils", IPythonUtils) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/logger.py b/scalpel/src/main/resources/python3-10/pyscalpel/logger.py new file mode 100644 index 00000000..3a30a879 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/logger.py @@ -0,0 +1,72 @@ +import sys +from pyscalpel.java import import_java + + +# Define a default logger to use if for some reason the logger is not initialized +# (e.g. running the script from pdoc) +class Logger: # pragma: no cover + """Provides methods for logging messages to the Burp Suite output and standard streams.""" + + def all(self, msg: str): + """Prints the message to the standard output + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}") + + def trace(self, msg: str): + """Prints the message to the standard output + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}") + + def debug(self, msg: str): + """Prints the message to the standard output + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}") + + def info(self, msg: str): + """Prints the message to the standard output + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}") + + def warn(self, msg: str): + """Prints the message to the standard output + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}") + + def fatal(self, msg: str): + """Prints the message to the standard output + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}") + + def error(self, msg: str): + """Prints the message to the standard error + + Args: + msg (str): The message to print + """ + print(f"(default): {msg}", file=sys.stderr) + + +try: + logger: Logger = import_java("lexfo.scalpel", "ScalpelLogger", Logger) +except ImportError as ex: # pragma: no cover + logger: Logger = Logger() + logger.error("(default): Couldn't import logger") + logger.error(str(ex)) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_abstract.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_abstract.py new file mode 100644 index 00000000..edf4725b --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_abstract.py @@ -0,0 +1,44 @@ +import unittest +from pyscalpel.http.body.abstract import * + + +class TestFormSerializer(unittest.TestCase): + def test_instantiation(self): + class BadFormSerializer(FormSerializer): + """FormSerializer subclass that does not implement all abstract methods""" + + pass + + with self.assertRaises(TypeError): + BadFormSerializer() + + def test_good_implementation(self): + class GoodFormSerializer(FormSerializer): + """FormSerializer subclass that does implement all abstract methods""" + + def serialize( + self, deserialized_body: Form, req: ObjectWithHeaders + ) -> bytes: ... + + def deserialize( + self, body: bytes, req: ObjectWithHeaders + ) -> Form | None: ... + + def get_empty_form(self, req: ObjectWithHeaders) -> Form: ... + + def deserialized_type(self) -> type: ... + + def import_form( + self, exported: ExportedForm, req: ObjectWithHeaders + ) -> Form: ... + + def export_form(self, source: Form) -> TupleExportedForm: ... + + try: + GoodFormSerializer() + except TypeError: + self.fail("GoodFormSerializer raised TypeError unexpectedly!") + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_encoding.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_encoding.py new file mode 100644 index 00000000..9bf046dc --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_encoding.py @@ -0,0 +1,72 @@ +import unittest +from pyscalpel.encoding import * + + +class TestUtilsModule(unittest.TestCase): + def test_always_bytes(self): + # Test with str input + data_str = "test" + result_str = always_bytes(data_str) + expected_str = b"test" + self.assertEqual(result_str, expected_str) + + # Test with bytes input + data_bytes = b"test" + result_bytes = always_bytes(data_bytes) + expected_bytes = b"test" + self.assertEqual(result_bytes, expected_bytes) + + # Test with int input + data_int = 123 + result_int = always_bytes(data_int) + expected_int = b"123" + self.assertEqual(result_int, expected_int) + + def test_always_str(self): + # Test with str input + data_str = "test" + result_str = always_str(data_str) + expected_str = "test" + self.assertEqual(result_str, expected_str) + + # Test with bytes input + data_bytes = b"test" + result_bytes = always_str(data_bytes) + expected_bytes = "test" + self.assertEqual(result_bytes, expected_bytes) + + # Test with int input + data_int = 123 + result_int = always_str(data_int) + expected_int = "123" + self.assertEqual(result_int, expected_int) + + def test_urlencode_all(self): + # Test with bytes input + data_bytes = b"test" + result_bytes = urlencode_all(data_bytes) + expected_bytes = b"%74%65%73%74" + self.assertEqual(result_bytes, expected_bytes) + + # Test with str input + data_str = "äöü" + result_str = urlencode_all(data_str, "utf-8") + expected_str = b"%C3%A4%C3%B6%C3%BC" + self.assertEqual(result_str, expected_str) + + def test_urldecode(self): + # Test with bytes input + data_bytes = b"%74%65%73%74" + result_bytes = urldecode(data_bytes) + expected_bytes = b"test" + self.assertEqual(result_bytes, expected_bytes) + + # Test with str input + data_str = "%C3%A4%C3%B6%C3%BC" + result_str = urldecode(data_str) + expected_str = b"\xc3\xa4\xc3\xb6\xc3\xbc" + self.assertEqual(result_str, expected_str) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_flow.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_flow.py new file mode 100644 index 00000000..58e4d024 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_flow.py @@ -0,0 +1,86 @@ +import unittest + +from unittest.mock import MagicMock + +from pyscalpel.http.flow import * + + +class FlowTestCase(unittest.TestCase): + def test_construct_default(self): + flow = Flow() + + self.assertEqual("http", flow.scheme) + self.assertEqual("", flow.host) + self.assertEqual(0, flow.port) + self.assertEqual(None, flow.request) + self.assertEqual(None, flow.response) + self.assertEqual(None, flow.text) + + def test_construct(self): + request = Request.make("GET", "https://localhost") + response = Response.make(200) + flow = Flow( + scheme="https", + host="localhost", + port=443, + request=request, + response=response, + text=b"Hello world!", + ) + + self.assertEqual("https", flow.scheme) + self.assertEqual("localhost", flow.host) + self.assertEqual(443, flow.port) + self.assertEqual(request, flow.request) + self.assertEqual(response, flow.response) + self.assertEqual(b"Hello world!", flow.text) + + def test_host_is_with_matching_pattern(self): + flow = Flow(host="example.com") + self.assertTrue(flow.host_is("example.com")) + + def test_host_is_with_non_matching_pattern(self): + flow = Flow(host="example.com") + self.assertFalse(flow.host_is("notexample.com")) + + def test_host_is_with_wildcard_match(self): + flow = Flow(host="sub.example.com") + self.assertTrue(flow.host_is("*.example.com")) + + def test_host_is_with_non_matching_wildcard(self): + flow = Flow(host="example.com") + self.assertFalse(flow.host_is("*.notexample.com")) + + def test_path_is_with_matching_pattern(self): + request_mock = MagicMock() + request_mock.path_is.return_value = True + flow = Flow(request=request_mock) + self.assertTrue(flow.path_is("/test/path")) + + def test_path_is_with_non_matching_pattern(self): + request_mock = MagicMock() + request_mock.path_is.return_value = False + flow = Flow(request=request_mock) + self.assertFalse(flow.path_is("/another/path")) + + def test_path_is_with_wildcard_match(self): + request_mock = MagicMock() + # Assume the mocked path_is method can handle wildcards correctly + request_mock.path_is.side_effect = lambda pattern: pattern == "/test/*" + flow = Flow(request=request_mock) + self.assertTrue(flow.path_is("/test/*")) + + def test_path_is_with_non_matching_wildcard(self): + request_mock = MagicMock() + # Assume the mocked path_is method correctly handles non-matching patterns + request_mock.path_is.return_value = False + flow = Flow(request=request_mock) + self.assertFalse(flow.path_is("/another/*")) + + def test_path_is_with_no_request(self): + flow = Flow() # No request is set + self.assertFalse(flow.path_is("/test/path")) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_form.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_form.py new file mode 100644 index 00000000..4b1e6a81 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_form.py @@ -0,0 +1,1013 @@ +import unittest +import tempfile +from io import BytesIO +from collections import namedtuple +from pyscalpel.http.body.form import ( + CaseInsensitiveDict, + MultiPartFormField, + MultiPartForm, + Mapping, + cast, + URLEncodedForm, + JSON_KEY_TYPES, + JSON_VALUE_TYPES, + JSONForm, + JSONFormSerializer, + URLEncodedFormSerializer, + MultiPartFormSerializer, + multidict, + os, + Form, + ObjectWithHeaders, + convert_for_urlencode, +) + + +class MultiPartFormTestCase(unittest.TestCase): + def setUp(self): + headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="file"; filename="example.txt"', + "Content-Type": "text/plain", + } + ) + content = b"This is the content of the file." + encoding = "utf-8" + self.form_field = MultiPartFormField(headers, content, encoding) + self.form = MultiPartForm([self.form_field], "multipart/form-data") + + def test_mapping_interface(self): + self.assertIsInstance(self.form, Mapping) + + def test_get_all(self): + key = "file" + expected_values = [self.form_field] + values = self.form.get_all(key) + self.assertEqual(values, expected_values) + + def test_get_all_empty_key(self): + key = "nonexistent" + expected_values = [] + values = self.form.get_all(key) + self.assertEqual(values, expected_values) + + def test_get(self): + key = "file" + expected_value = self.form_field + value = self.form.get(key) + self.assertEqual(value, expected_value) + + def test_get_default(self): + key = "nonexistent" + default = MultiPartFormField.make("kjdsqkjdhdsqsq") + expected_value = default + value = self.form.get(key, default) + self.assertEqual(value, expected_value) + + def test_del_all(self): + key = "file" + self.form.del_all(key) + values = self.form.get_all(key) + self.assertEqual(values, []) + + def test_del_all_empty_key(self): + key = "nonexistent" + self.form.del_all(key) # No exception should be raised + + def test_delitem(self): + key = "file" + del self.form[key] + values = self.form.get_all(key) + self.assertEqual(values, []) + + # def test_delitem_key_error(self): + # key = "nonexistent" + # with self.assertRaises(KeyError): + # del self.form[key] + + def test_set(self): + key = "new_file" + value = MultiPartFormField.make(key) + self.form[key] = value + self.assertEqual(self.form.get(key), value) + + def test_set_bytes_value(self): + key = "new_file" + value = b"example content" + self.form[key] = value + form_field = cast(MultiPartFormField, self.form.get(key)) + self.assertEqual(form_field.name, key) + self.assertEqual(form_field.content, value) + + def test_set_io_value(self): + key = "new_file" + value = BytesIO(b"example content") + self.form[key] = value + form_field = cast(MultiPartFormField, self.form.get(key)) + self.assertIsInstance(form_field, MultiPartFormField) + self.assertEqual(form_field.name, key) + self.assertEqual(form_field.content, b"example content") + + def test_set_none_value(self): + key = "file" + self.form[key] = None + values = self.form.get_all(key) + self.assertEqual(values, []) + + def test_set_default(self): + key = "nonexistent" + default = MultiPartFormField.make(key) + value = self.form.setdefault(key, default) + self.assertEqual(value, default) + self.assertEqual(self.form.get(key), default) + + def test_set_default_existing(self): + key = "file" + default = MultiPartFormField.make(key) + value = self.form.setdefault(key, default) + self.assertEqual(value, self.form_field) + self.assertEqual(self.form.get(key), self.form_field) + + def test_len(self): + form = MultiPartForm(tuple(), "multipart/form-data; --Boundary") + for i in range(10): + form[str(i)] = str(i) + self.assertEqual(i + 1, len(form)) + + def test_iter(self): + form = MultiPartForm(tuple(), "multipart/form-data; --Boundary") + + for i in range(10): + form[str(i)] = str(i) + + expected_fields = form.fields + + # Type checker is broken and does not infer from __iter__ when converting using list() + # This doesn't raise any error -> fields = [field for field in form] + fields = cast(list[MultiPartFormField], list(form)) + self.assertListEqual(fields, expected_fields) + + def test_eq_same_fields(self): + form2 = MultiPartForm([self.form_field], "multipart/form-data") + self.assertEqual(self.form, form2) + + def test_eq_different_fields(self): + form2 = MultiPartForm([], "multipart/form-data") + self.assertNotEqual(self.form, form2) + + def test_items(self): + expected_items = [("file", self.form_field)] + items = list(self.form.items()) + self.assertEqual(items, expected_items) + + def test_values(self): + expected_values = [self.form_field] + values = list(self.form.values()) + self.assertEqual(values, expected_values) + + def test_keys(self): + expected_keys = ["file"] + keys = list(self.form.keys()) + self.assertEqual(keys, expected_keys) + + # def test_repr(self): + # expected_repr = "MultiPartForm[]" + # form_repr = repr(self.form) + # self.assertEqual(form_repr, expected_repr) + + +class URLEncodedFormSerializerTestCase(unittest.TestCase): + def test_serialize(self): + serializer = URLEncodedFormSerializer() + deserialized_body = multidict.MultiDict(((b"name", b"John"), (b"age", b"30"))) + expected = b"name=John&age=30" + result = serializer.serialize(deserialized_body) + self.assertEqual(result, expected) + + def test_deserialize(self): + serializer = URLEncodedFormSerializer() + body = b"name=John&age=30" + expected = URLEncodedForm([(b"name", b"John"), (b"age", b"30")]) + result = serializer.deserialize(body) + self.assertEqual(result, expected) + + def test_deserialize_empty_body(self): + serializer = URLEncodedFormSerializer() + body = b"" + expected = URLEncodedForm([]) + result = serializer.deserialize(body) + self.assertEqual(result, expected) + + def test_get_empty_form(self): + serializer = URLEncodedFormSerializer() + expected = URLEncodedForm([]) + result = serializer.get_empty_form() + self.assertEqual(result, expected) + + def test_deserialized_type(self): + serializer = URLEncodedFormSerializer() + expected = URLEncodedForm + result = serializer.deserialized_type() + self.assertEqual(result, expected) + + def test_import_form(self): + exported_form = ( + ("key1", "value1"), + ("key2", "value2"), + ("key3", "value3"), + ) + expected_fields = [ + (b"key1", b"value1"), + (b"key2", b"value2"), + (b"key3", b"value3"), + ] + serializer = URLEncodedFormSerializer() + imported_form = serializer.import_form(exported_form) # type: ignore + self.assertIsInstance(imported_form, URLEncodedForm) + items = list(imported_form.items()) + self.assertEqual(items, expected_fields) + + def test_export_form(self): + form = URLEncodedForm([(b"key1", b"value1"), (b"key2", b"value2")]) + serializer = URLEncodedFormSerializer() + exported_form = serializer.export_form(form) + expected_exported_form = ((b"key1", b"value1"), (b"key2", b"value2")) + self.assertEqual(exported_form, expected_exported_form) + + def test_serialize_does_urlencode(self): + form = URLEncodedForm( + ((b"secret", b"MySecretKey"), (b"encrypted", b"+ZSV6BfZwcr7c6m3fZTHyg==")) + ) + serializer = URLEncodedFormSerializer() + serialized = serializer.serialize(form) + expected = b"secret=MySecretKey&encrypted=%2BZSV6BfZwcr7c6m3fZTHyg%3D%3D" + self.assertEqual(expected, serialized) + + def test_setitem_with_string_key(self): + form = URLEncodedForm([(b"key1", b"value1")]) + form["key2"] = "value2" + self.assertEqual(form[b"key2"], b"value2") + + def test_getitem_with_string_key(self): + form = URLEncodedForm([(b"key1", b"value1"), (b"key2", b"value2")]) + result = form["key1"] + self.assertEqual(result, b"value1") + + def test_set_and_get_with_string_key(self): + form = URLEncodedForm([]) + form["key1"] = "value1" + result = form["key1"] + self.assertEqual(result, b"value1") + + def test_import_form_with_dict(self): + # The dictionary to be imported + exported_dict = { + "key1": "value1", + "key2": "value2", + } + + serializer = URLEncodedFormSerializer() + imported_form = serializer.import_form(exported_dict) + expected_fields = [(b"key1", b"value1"), (b"key2", b"value2")] + + items = list(imported_form.items()) + + self.assertEqual(items, expected_fields) + + def test_import_form_skips_none_values(self): + exported_form = ( + ("key1", "value1"), + ("key2", None), # This value should be skipped + ("key3", "value3"), + ) + + serializer = URLEncodedFormSerializer() + imported_form = serializer.import_form(exported_form) # type: ignore + + expected_fields = [ + (b"key1", b"value1"), + (b"key3", b"value3"), + ] + + items = list(imported_form.items()) + + self.assertEqual(items, expected_fields) + + def test_convert_for_urlencode_with_boolean(self): + val = True + result = convert_for_urlencode(val) + self.assertEqual(result, "1") + + val = False + result = convert_for_urlencode(val) + self.assertEqual(result, "0") + + +class JSONFormSerializerTestCase(unittest.TestCase): + def test_serialize(self): + serializer = JSONFormSerializer() + deserialized_body: dict[JSON_KEY_TYPES, JSON_VALUE_TYPES] = { + "name": "John", + "age": 30, + } + expected = b'{"name": "John", "age": 30}' + result = serializer.serialize(deserialized_body) + self.assertEqual(result, expected) + + def test_deserialize(self): + serializer = JSONFormSerializer() + body = b'{"name": "John", "age": 30}' + expected = {"name": "John", "age": 30} + result = serializer.deserialize(body) + self.assertEqual(result, expected) + + def test_deserialize_empty_body(self): + serializer = JSONFormSerializer() + body = b"" + expected = None + result = serializer.deserialize(body) + self.assertEqual(result, expected) + + def test_get_empty_form(self): + serializer = JSONFormSerializer() + expected = JSONForm({}) + result = serializer.get_empty_form() + self.assertEqual(result, expected) + + def test_deserialized_type(self): + serializer = JSONFormSerializer() + expected = JSONForm + result = serializer.deserialized_type() + self.assertEqual(result, expected) + + +class MultiPartFormFieldTestCase(unittest.TestCase): + def test_init(self): + headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="file"; filename="example.txt"', + "Content-Type": "text/plain", + } + ) + content = b"This is the content of the file." + encoding = "utf-8" + expected_headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="file"; filename="example.txt"', + "Content-Type": "text/plain", + } + ) + expected_content = b"This is the content of the file." + expected_encoding = "utf-8" + result = MultiPartFormField(headers, content, encoding) + self.assertEqual(result.headers, expected_headers) + self.assertEqual(result.content, expected_content) + self.assertEqual(result.encoding, expected_encoding) + + def test_file_upload(self): + with tempfile.NamedTemporaryFile(delete=False) as temp_file: + temp_file.write(b"This is the content of the file.") + + filename = temp_file.name + content_type = "text/plain" + + form_field = MultiPartFormField.from_file( + "file", filename, content_type=content_type + ) + + self.assertEqual(form_field.name, "file") + self.assertEqual(form_field.filename, os.path.basename(filename)) + self.assertEqual(form_field.content_type, content_type) + self.assertEqual(form_field.content, b"This is the content of the file.") + + os.remove(filename) + + +class FormConversionsTestCase(unittest.TestCase): + def test_json_to_urlencode(self): + json_serializer = JSONFormSerializer() + urlencode_serializer = URLEncodedFormSerializer() + + json_form = { + "key1": [1, 2, 3, 4, 5.0], + "key2": "2", + "level0": {"level1": {"level2": "nested"}}, + } + + form = JSONForm(json_form.items()) + + self.assertDictEqual(json_form, form, "JSON constructor is broken") + + exported = json_serializer.export_form(form) + expected_exported = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + self.assertTupleEqual( + expected_exported, exported, "JSON tuple export is broken" + ) + + imported = urlencode_serializer.import_form( + exported, cast(ObjectWithHeaders, None) + ) + + expected_fields = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + self.assertTupleEqual( + expected_fields, imported.fields, "URLEncode import is broken" + ) + + serialized = urlencode_serializer.serialize(imported) + + expected_serialized = b"key1[]=1&key1[]=2&key1[]=3&key1[]=4&key1[]=5.0&key2=2&level0[level1][level2]=nested" + + self.assertEqual( + expected_serialized, serialized, "Urlencode serialize is broken" + ) + + def test_urlencode_to_json_tuple(self): + json_serializer = JSONFormSerializer() + urlencode_serializer = URLEncodedFormSerializer() + + form = URLEncodedForm( + [ + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ] + ) + + exported = urlencode_serializer.export_form(form) + + expected_exported = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + self.assertTupleEqual( + exported, + expected_exported, + "Failed to export URL-encoded form to tuple", + ) + + imported = json_serializer.import_form(exported) + + expected_imported = { + "key1": ["1", "2", "3", "4", "5.0"], + "key2": "2", + "level0": {"level1": {"level2": "nested"}}, + } + + self.assertDictEqual( + imported, + expected_imported, + "Failed to convert URL-encoded form to JSON", + ) + + def test_urlencode_to_json_dict(self): + json_serializer = JSONFormSerializer() + urlencode_serializer = URLEncodedFormSerializer() + + tupled_form = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + form = URLEncodedForm(list(tupled_form)) + + exported = urlencode_serializer.export_form(form) + + expected_exported = tupled_form + self.assertTupleEqual( + expected_exported, + exported, + "Failed to export URL-encoded form", + ) + + imported = json_serializer.import_form(exported) + + expected_imported = { + "key1": ["1", "2", "3", "4", "5.0"], + "key2": "2", + "level0": {"level1": {"level2": "nested"}}, + } + + self.assertDictEqual( + expected_imported, + imported, + "Failed to import nested values to JSON", + ) + + def test_urlencode_to_multipart(self): + urlencode_serializer = URLEncodedFormSerializer() + multipart_serializer = MultiPartFormSerializer() + + form = URLEncodedForm( + [ + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ] + ) + + exported = urlencode_serializer.export_form(form) + + expected_exported = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + self.assertTupleEqual(exported, expected_exported) + + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + + multipart_data = multipart_serializer.import_form(exported, req) + multipart_bytes = bytes(multipart_data) + expected_multipart_bytes = b"""--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +1\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +3\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +4\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +5.0\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key2"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="level0[level1][level2]"\r +\r +nested\r +--f0f056705fd4c99a5f41f9fa87c334d5--\r +\r +""" + + self.assertEqual(multipart_bytes, expected_multipart_bytes) + + def test_multipart_to_urlencoded(self): + urlencode_serializer = URLEncodedFormSerializer() + multipart_serializer = MultiPartFormSerializer() + + multipart_data_bytes = b"""--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1"\r +\r +1\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1"\r +\r +3\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1"\r +\r +4\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1"\r +\r +5.0\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key2"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="level0[level1][level2]"\r +\r +nested\r +--f0f056705fd4c99a5f41f9fa87c334d5--\r +\r +""" + + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + + multipart_form = multipart_serializer.deserialize(multipart_data_bytes, req=req) + + self.assertIsNotNone(multipart_form) + assert multipart_form + + exported = multipart_serializer.export_form(multipart_form) + expected_exported = ( + (b"key1", b"1"), + (b"key1", b"2"), + (b"key1", b"3"), + (b"key1", b"4"), + (b"key1", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + self.assertTupleEqual(expected_exported, exported) + + imported = urlencode_serializer.import_form(exported, req=req) + + expected_imported = URLEncodedForm(expected_exported) + + self.assertEqual(expected_imported, imported) + + exported_urlencoded = urlencode_serializer.export_form(imported) + expected_exported_urlencoded = expected_exported + + self.assertTupleEqual(expected_exported_urlencoded, exported_urlencoded) + + def test_multipart_to_urlencode(self): + # Init multipart form + multipart_bytes = b"""--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +1\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +3\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +4\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +5.0\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key2"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="level0[level1][level2]"\r +\r +nested\r +--f0f056705fd4c99a5f41f9fa87c334d5--\r +\r +""" + + multipart_form = MultiPartForm.from_bytes( + multipart_bytes, + "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5", + ) + + # Export form to tuple + exported = MultiPartFormSerializer().export_form(multipart_form) + expected_exported = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + # Ensure export works as expected + self.assertTupleEqual( + expected_exported, exported, "Could not export MultiPartForm" + ) + + # Convert form to URLEncoded + imported = URLEncodedFormSerializer().import_form(exported) + expected_imported = URLEncodedForm(expected_exported) + + self.assertTupleEqual(expected_imported.fields, imported.fields) + + def test_multipart_to_json(self): + json_serializer = JSONFormSerializer() + multipart_serializer = MultiPartFormSerializer() + + # Example multipart form data + multipart_data_bytes = b"""--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +1\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +3\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +4\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +5.0\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key2"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="level0[level1][level2]"\r +\r +nested\r +--f0f056705fd4c99a5f41f9fa87c334d5--\r +\r +""" + + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + + multipart_form = multipart_serializer.deserialize(multipart_data_bytes, req=req) + + self.assertIsNotNone(multipart_form) + assert multipart_form + + exported = multipart_serializer.export_form(multipart_form) + expected_exported = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + self.assertTupleEqual(expected_exported, exported) + + imported = json_serializer.import_form(exported) + + expected_imported = { + "key1": ["1", "2", "3", "4", "5.0"], + "key2": "2", + "level0": {"level1": {"level2": "nested"}}, + } + + self.assertDictEqual( + imported, + expected_imported, + "Failed to convert multipart form data to JSON", + ) + + exported = json_serializer.export_form(imported) + + # Should not change + # expected_exported = ( + # (b"key1[]", b"1"), + # (b"key1[]", b"2"), + # (b"key1[]", b"3"), + # (b"key1[]", b"4"), + # (b"key1[]", b"5.0"), + # (b"key2", b"2"), + # (b"level0[level1][level2]", b"nested"), + # ) + + self.assertTupleEqual( + exported, + expected_exported, + "Failed to export JSON form to tuple", + ) + + def test_json_to_multipart(self): + json_serializer = JSONFormSerializer() + multipart_serializer = MultiPartFormSerializer() + + json_form = { + "key1": [1, 2, 3, 4, 5.0], + "key2": "2", + "level0": {"level1": {"level2": "nested"}}, + } + + form = JSONForm(json_form.items()) + + self.assertDictEqual(json_form, form, "JSON constructor is broken") + + exported = json_serializer.export_form(form) + expected_exported = ( + (b"key1[]", b"1"), + (b"key1[]", b"2"), + (b"key1[]", b"3"), + (b"key1[]", b"4"), + (b"key1[]", b"5.0"), + (b"key2", b"2"), + (b"level0[level1][level2]", b"nested"), + ) + + self.assertTupleEqual( + expected_exported, exported, "JSON tuple export is broken" + ) + + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + + multipart_data = multipart_serializer.import_form(exported, req) + multipart_bytes = bytes(multipart_data) + expected_multipart_bytes = b"""--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +1\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +3\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +4\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key1[]"\r +\r +5.0\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="key2"\r +\r +2\r +--f0f056705fd4c99a5f41f9fa87c334d5\r +Content-Disposition: form-data; name="level0[level1][level2]"\r +\r +nested\r +--f0f056705fd4c99a5f41f9fa87c334d5--\r +\r +""" + + self.assertEqual(multipart_bytes, expected_multipart_bytes) + + def test_basename(self): + content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = MultiPartForm(tuple(), content_type) + form["file"] = "hello" + + expected = "../../../../../../../../../../../../../../../etc/passwd" + form["file"].filename = expected + self.assertEqual(expected, form["file"].filename) + + current_file_path = __file__ # Get the path of the currently running script + with open(current_file_path, encoding="utf-8") as file: + form["file2"] = file + + expected = os.path.basename(current_file_path) # Extract the file name + self.assertEqual(expected, form["file2"].filename) + + def test_name(self): + # Test the name property and setter + content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = MultiPartForm(tuple(), content_type) + form["file"] = "hello" + # Test the name property + self.assertEqual("file", form["file"].name) + # Test the name setter + form["file"].name = "new_name" + + with self.assertRaises(KeyError): + # form["file"] has been renamed to form["name"] + print(form["file"]) + + self.assertEqual("new_name", second=form["new_name"].name) + + def test_insertion(self): + content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = MultiPartForm(tuple(), content_type) + form["file1"] = "hello" + form["file3"] = "hello2" + + # Insert in the middle + field = MultiPartFormField.make("file2", "hello3") + form.insert(1, field) + for i, field in enumerate(form): + self.assertEqual(f"file{i + 1}", field.name) + + def test_getitem_exception(self): + content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = MultiPartForm(tuple(), content_type) + with self.assertRaises(KeyError): + form["nonexistent"] + + def test_set_wrong_type(self): + content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = MultiPartForm(tuple(), content_type) + with self.assertRaises(TypeError): + form.set("file", self) # type:ignore + + def test_set_existing_name(self): + content_type = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = MultiPartForm(tuple(), content_type) + form["file"] = "hello" + form.set("file", "hello2") + self.assertEqual(b"hello2", form["file"].content) + + def test_serializer_import_dict(self): + serializer = MultiPartFormSerializer() + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + form = serializer.import_form({"key": "value"}, req) + self.assertIsInstance(form, MultiPartForm) + self.assertEqual(form["key"].content, b"value") + + def test_serializer_deserialize_garbage(self): + serializer = MultiPartFormSerializer() + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + form = serializer.deserialize(b"invalid", req) + self.assertIsNone(form) + + def test_serializer_deserialize_empty(self): + serializer = MultiPartFormSerializer() + FakeRequestTp = namedtuple("FakeRequest", ["headers"]) + req = FakeRequestTp( + headers={ + "Content-Type": "multipart/form-data; boundary=f0f056705fd4c99a5f41f9fa87c334d5" + } + ) + form = serializer.deserialize(b"", req) + self.assertIsNone(form) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_headers.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_headers.py new file mode 100644 index 00000000..b37ca8c3 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_headers.py @@ -0,0 +1,86 @@ +import unittest + +from pyscalpel.http.headers import * + + +class HeadersTest(unittest.TestCase): + def setUp(self): + self.headers = Headers( + ((b"Host", b"example.com"), (b"Content-Type", b"application/xml")) + ) + + def test_get_header_case_insensitive(self): + self.assertEqual(self.headers["Host"], "example.com") + self.assertEqual(self.headers["host"], "example.com") + + def test_create_headers_from_raw_data(self): + headers = Headers( + [ + (b"Host", b"example.com"), + (b"Accept", b"text/html"), + (b"accept", b"application/xml"), + ] + ) + self.assertEqual(headers["Host"], "example.com") + self.assertEqual(headers["Accept"], "text/html, application/xml") + + def test_set_header_removes_existing_headers(self): + self.headers["Accept"] = "application/text" + self.assertEqual(self.headers["Accept"], "application/text") + + def test_bytes_representation(self): + expected_bytes = b"Host: example.com\r\nContent-Type: application/xml\r\n" + self.assertEqual(bytes(self.headers), expected_bytes) + + def test_get_all(self): + self.headers.set_all("Accept", ["text/html", "application/xml"]) + self.assertEqual( + self.headers.get_all("Accept"), ["text/html", "application/xml"] + ) + + def test_insert(self): + self.headers.insert(1, "User-Agent", "Mozilla/5.0") + self.assertEqual(self.headers.fields[1], (b"User-Agent", b"Mozilla/5.0")) + + # Verify that the inserted header is accessible by name + self.assertEqual(self.headers["User-Agent"], "Mozilla/5.0") + + # Verify that the inserted header is included in the bytes representation + expected_bytes = b"Host: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: application/xml\r\n" + self.assertEqual(bytes(self.headers), expected_bytes) + + # Verify that inserting a header at an index greater than the length of fields appends the header + self.headers.insert(5, "X-Custom", "Custom-Value") + self.assertEqual(self.headers.fields[-1], (b"X-Custom", b"Custom-Value")) + + # Verify that the appended header is included in the bytes representation + expected_bytes = b"Host: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: application/xml\r\nX-Custom: Custom-Value\r\n" + self.assertEqual(bytes(self.headers), expected_bytes) + + def test_items(self): + self.headers["User-Agent"] = "Mozilla/5.0" + self.headers["Accept-Language"] = "en-US,en;q=0.9" + self.headers["Cache-Control"] = "no-cache" + + items = list(self.headers.items()) + expected_items = [ + ("Host", "example.com"), + ("Content-Type", "application/xml"), + ("User-Agent", "Mozilla/5.0"), + ("Accept-Language", "en-US,en;q=0.9"), + ("Cache-Control", "no-cache"), + ] + self.assertEqual(items, expected_items) + + def test_from_mitmproxy(self): + mitmproxy_headers = Headers.from_mitmproxy(self.headers) + self.assertEqual(mitmproxy_headers["Host"], "example.com") + + def test_encoding(self): + head = Headers(((b"Abc", "ééé".encode("latin-1")),)) + dec = bytes(head).decode("latin-1") + self.assertEqual("Abc: ééé\r\n", dec) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_hook.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_hook.py new file mode 100644 index 00000000..11f379e3 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_hook.py @@ -0,0 +1,18 @@ +import unittest +from pyscalpel import editor + + +class TestHook(unittest.TestCase): + def test_annotation(self): + def hello(): + pass + + editor("binary")(hello) + self.assertEqual(hello.__annotations__["scalpel_editor_mode"], "binary") + + with self.assertRaises(ValueError): + editor("INVALID")(hello) # type: ignore + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_json.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_json.py new file mode 100644 index 00000000..ee9d1319 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_json.py @@ -0,0 +1,102 @@ +""" Most JSON code is covered in test_form.py, this covers the rest """ + +import unittest +import pyscalpel.http.body.json_form as json_form + + +class TestJson(unittest.TestCase): + def test_json_convert(self): + data = { + "a": 1, + "b": "c", + "d": [1, 2, 3], + "e": {"f": "g"}, + "h": b"i\x00\x01\x02\x03", + } + expected = { + "a": 1, + "b": "c", + "d": [1, 2, 3], + "e": {"f": "g"}, + "h": "i\\u0000\\u0001\\u0002\\u0003", + } + self.assertEqual(json_form.json_convert(data), expected) + + # def transform_tuple_to_dict(tup): + # """Transforms duplicates keys to list + + # E.g: + # (("key_duplicate", 1),("key_duplicate", 2),("key_duplicate", 3), + # ("key_duplicate", 4),("key_uniq": "val") , + # ("key_duplicate", 5),("key_duplicate", 6)) + # -> + # {"key_duplicate": [1,2,3,4,5], "key_uniq": "val"} + + # Args: + # tup (_type_): _description_ + + # Returns: + # _type_: _description_ + # """ + # result_dict = {} + # for pair in tup: + # key, value = pair + # converted_key: bytes | str + # match key: + # case bytes(): + # converted_key = key.removesuffix(b"[]") + # case str(): + # converted_key = key.removesuffix("[]") + # case _: + # converted_key = key + + # if converted_key in result_dict: + # if isinstance(result_dict[converted_key], list): + # result_dict[converted_key].append(value) + # else: + # result_dict[converted_key] = [result_dict[converted_key], value] + # else: + # result_dict[converted_key] = value + # return result_dict + + def test_transform_tuple_to_dict(self): + data = ( + ("key_duplicate", 1), + ("key_duplicate", 2), + ("key_duplicate", 3), + ("key_duplicate", 4), + ("key_uniq", "val"), + ("key_duplicate", 5), + ("key_duplicate", 6), + ) + expected = { + "key_duplicate": [1, 2, 3, 4, 5, 6], + "key_uniq": "val", + } + self.assertEqual(json_form.transform_tuple_to_dict(data), expected) + + # match key: + # case bytes(): + # converted_key = key.removesuffix(b"[]") + # case str(): + # converted_key = key.removesuffix("[]") + # case _: + # converted_key = key + # Also cover bytes and _ cases + data = ( + (b"key_duplicate[]", 1), + (b"key_duplicate[]", 2), + (b"key_duplicate[]", 3), + (b"key_duplicate[]", 4), + (b"key_uniq", "val"), + (b"key_duplicate[]", 5), + (b"key_duplicate[]", 6), + (1, 2), + ) + + expected = { + b"key_duplicate": [1, 2, 3, 4, 5, 6], + b"key_uniq": "val", + 1: 2, + } + self.assertEqual(json_form.transform_tuple_to_dict(data), expected) diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_mime.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_mime.py new file mode 100644 index 00000000..025c656d --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_mime.py @@ -0,0 +1,234 @@ +from pyscalpel.http.mime import * +from pyscalpel.http.body.multipart import get_mime +import unittest + + +class ParseMimeHeaderTestCase(unittest.TestCase): + def test_empty_string(self): + header_str = "" + result = parse_mime_header_params(header_str) + self.assertEqual(result, []) + + def test_single_pair_no_quotes(self): + header_str = "key=value" + result = parse_mime_header_params(header_str) + expected = [("key", "value")] + self.assertEqual(result, expected) + + def test_single_pair_with_quotes(self): + header_str = 'key="value"' + result = parse_mime_header_params(header_str) + expected = [("key", "value")] + self.assertEqual(result, expected) + + def test_multiple_pairs(self): + header_str = "key1=value1; key2=value2; key3=value3" + result = parse_mime_header_params(header_str) + expected = [("key1", "value1"), ("key2", "value2"), ("key3", "value3")] + self.assertEqual(result, expected) + + def test_mixed_quotes(self): + header_str = 'key1="value1"; key2=value2; key3="value3"' + result = parse_mime_header_params(header_str) + expected = [("key1", "value1"), ("key2", "value2"), ("key3", "value3")] + self.assertEqual(result, expected) + + def test_value_with_semicolon_and_quotes(self): + header_str = 'key="value;with;semicolons"; key2=value2' + result = parse_mime_header_params(header_str) + expected = [("key", "value;with;semicolons"), ("key2", "value2")] + self.assertEqual(result, expected) + + +class HeaderParsingTestCase(unittest.TestCase): + def test_unparse_header_value(self): + parsed_header = [ + ("Content-Type", "text/html"), + ("charset", "utf-8"), + ("boundary", "abcdef"), + ] + expected = 'text/html; charset="utf-8"; boundary="abcdef"' + result = unparse_header_value(parsed_header) + self.assertEqual(result, expected) + + def test_unparse_header_value_single_parameter(self): + parsed_header = [ + ("Content-Type", "text/html"), + ("charset", "utf-8"), + ] + expected = 'text/html; charset="utf-8"' + result = unparse_header_value(parsed_header) + self.assertEqual(result, expected) + + def test_parse_header(self): + key = "Content-Type" + value = 'text/html; charset="utf-8"; boundary="abcdef"' + expected = [ + ("Content-Type", "text/html"), + ("charset", "utf-8"), + ("boundary", "abcdef"), + ] + result = parse_header(key, value) + self.assertEqual(result, expected) + + def test_parse_header_single_parameter(self): + key = "Content-Type" + value = 'text/html; charset="utf-8"' + expected = [ + ("Content-Type", "text/html"), + ("charset", "utf-8"), + ] + result = parse_header(key, value) + self.assertEqual(result, expected) + + +class BoundaryExtractionTestCase(unittest.TestCase): + def test_extract_boundary(self): + content_type = 'multipart/form-data; boundary="abcdefg12345"' + encoding = "utf-8" + expected = b"abcdefg12345" + result = extract_boundary(content_type, encoding) + self.assertEqual(result, expected) + + def test_extract_boundary_no_quotes(self): + content_type = "multipart/form-data; boundary=abcdefg12345" + encoding = "utf-8" + expected = b"abcdefg12345" + result = extract_boundary(content_type, encoding) + self.assertEqual(result, expected) + + def test_extract_boundary_multiple_parameters(self): + content_type = 'multipart/form-data; charset=utf-8; boundary="abcdefg12345"' + encoding = "utf-8" + expected = b"abcdefg12345" + result = extract_boundary(content_type, encoding) + self.assertEqual(result, expected) + + def test_extract_boundary_missing_boundary(self): + content_type = "multipart/form-data; charset=utf-8" + encoding = "utf-8" + with self.assertRaisesRegex(RuntimeError, r"Missing boundary"): + extract_boundary(content_type, encoding) + + def test_extract_boundary_unexpected_mimetype(self): + content_type = 'application/json; boundary="abcdefg12345"' + encoding = "utf-8" + with self.assertRaisesRegex(RuntimeError, r"Unexpected mimetype"): + extract_boundary(content_type, encoding) + + +class TestHeaderParams(unittest.TestCase): + def setUp(self): + self.params: Sequence[tuple[str, str | None]] = [ + ("Content-Disposition", "form-data"), + ("name", "file"), + ("filename", "index.html"), + ] + + def test_find_header_param(self): + # Testing existing key + self.assertEqual(find_header_param(self.params, "name"), ("name", "file")) + + self.assertEqual( + find_header_param(self.params, "filename"), ("filename", "index.html") + ) + + # Testing non-existing key + self.assertEqual(find_header_param(self.params, "non-existing-key"), None) + + def test_update_header_param(self): + # Testing update of existing key + updated_params = update_header_param(self.params, "name", "updated_file") + self.assertIn(("name", "updated_file"), updated_params) + + # Testing addition of new key + updated_params = update_header_param(self.params, "new-key", "new-value") + self.assertIn(("new-key", "new-value"), updated_params) + + # Testing update with None value + updated_params = update_header_param(self.params, "name", None) + self.assertIn(("name", None), updated_params) + + +class TestParseMIMEHeaderValue(unittest.TestCase): + def test_null_string(self): + self.assertListEqual(parse_mime_header_params(None), []) + + def test_empty_string(self): + self.assertListEqual(parse_mime_header_params(""), []) + + def test_single_parameter(self): + self.assertListEqual( + parse_mime_header_params("key1=value1"), [("key1", "value1")] + ) + + def test_multiple_parameters(self): + self.assertListEqual( + parse_mime_header_params("key1=value1; key2=value2"), + [("key1", "value1"), ("key2", "value2")], + ) + + def test_quoted_value(self): + self.assertListEqual( + parse_mime_header_params('key1="value1 with spaces"; key2=value2'), + [("key1", "value1 with spaces"), ("key2", "value2")], + ) + + def test_extra_spaces(self): + self.assertListEqual( + parse_mime_header_params(' key1 = "value1 with spaces" ; key2 = value2 '), + [("key1", "value1 with spaces"), ("key2", "value2")], + ) + + def test_value_with_equal_sign(self): + self.assertListEqual( + parse_mime_header_params('key1="value1=value1"; key2=value2'), + [("key1", "value1=value1"), ("key2", "value2")], + ) + + def test_value_with_semi_colon(self): + self.assertListEqual( + parse_mime_header_params('key1="value1;value2"; key3=value3'), + [("key1", "value1;value2"), ("key3", "value3")], + ) + + def test_value_with_space_at_end(self): + self.assertListEqual( + parse_mime_header_params('key1="value1;value2"; key3=value3 '), + [("key1", "value1;value2"), ("key3", "value3")], + ) + self.assertListEqual( + parse_mime_header_params('key1="value1;value2"; key3=value3 ; '), + [("key1", "value1;value2"), ("key3", "value3")], + ) + + def test_parse_header(self): + header_key = "Content-Disposition" + header_value = 'text/html; filename="file"' + expected = [(header_key, "text/html"), ("filename", "file")] + parsed = parse_header(header_key, header_value) + self.assertListEqual(expected, parsed) + + +# Test utils +class TestMimeUtils(unittest.TestCase): + def test_get_mime(self): + self.assertEqual(get_mime("file.txt"), "text/plain") + self.assertEqual(get_mime("file.html"), "text/html") + self.assertEqual(get_mime("file.jpg"), "image/jpeg") + self.assertEqual(get_mime("file.png"), "image/png") + self.assertEqual(get_mime("file.gif"), "image/gif") + self.assertEqual(get_mime("file.pdf"), "application/pdf") + self.assertEqual(get_mime("file.zip"), "application/zip") + self.assertEqual(get_mime("file.tar"), "application/x-tar") + + # json + self.assertEqual(get_mime("file.json"), "application/json") + self.assertEqual(get_mime("file.js"), "text/javascript") + + # default + self.assertEqual(get_mime("file"), "application/octet-stream") + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_multipart.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_multipart.py new file mode 100644 index 00000000..cda9b5be --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_multipart.py @@ -0,0 +1,119 @@ +""" +Most of multipart.py is covered in test_form.py, this covers the rest, mostly the utility functions +""" + +import unittest +from pyscalpel.http.body.multipart import * + + +class TestMultipartUtils(unittest.TestCase): + def test_escape_parameter(self): + self.assertEqual(escape_parameter(b'abc"def'), "abc%22def") + self.assertEqual(escape_parameter('abc"def'), "abc%22def") + self.assertEqual(escape_parameter('abc"def', True), "abc%22def") + self.assertEqual(escape_parameter('abc"def', False), "abc%22def") + self.assertEqual(escape_parameter(b'abc"def', True), "abc%22def") + self.assertEqual(escape_parameter(b'abc"def', False), "abc%22def") + self.assertEqual(escape_parameter('abc"def', True), "abc%22def") + self.assertEqual(escape_parameter('abc"def', False), "abc%22def") + self.assertEqual(escape_parameter('abc"def', True), "abc%22def") + + def test_scalar_to_bytes(self): + self.assertEqual(scalar_to_bytes("abc"), b"abc") + self.assertEqual(scalar_to_bytes(b"abc"), b"abc") + self.assertEqual(scalar_to_bytes(123), b"123") + self.assertEqual(scalar_to_bytes(123.0), b"123.0") + self.assertEqual(scalar_to_bytes(True), b"1") + self.assertEqual(scalar_to_bytes(False), b"0") + # Test non scalar + self.assertEqual(scalar_to_bytes(None), b"") + self.assertEqual(scalar_to_bytes([1, 2, 3]), b"") + self.assertEqual(scalar_to_bytes({"a": 1}), b"") + self.assertEqual(scalar_to_bytes(object()), b"") + + def test_scalar_to_str(self): + self.assertEqual(scalar_to_str("abc"), "abc") + self.assertEqual(scalar_to_str(b"abc"), "abc") + self.assertEqual(scalar_to_str(123), "123") + self.assertEqual(scalar_to_str(123.0), "123.0") + self.assertEqual(scalar_to_str(True), "1") + self.assertEqual(scalar_to_str(False), "0") + # Test non scalar + self.assertEqual(scalar_to_str(None), "") + self.assertEqual(scalar_to_str([1, 2, 3]), "") + self.assertEqual(scalar_to_str({"a": 1}), "") + self.assertEqual(scalar_to_str(object()), "") + + +class TestMultiPartFormField(unittest.TestCase): + + def test_text_method_with_empty_content(self): + field = MultiPartFormField(CaseInsensitiveDict(), b"", "utf-8") + self.assertEqual(field.text, "") + + def test_text_method_with_utf8_content(self): + content = "Hello, World!".encode("utf-8") + field = MultiPartFormField(CaseInsensitiveDict(), content, "utf-8") + self.assertEqual(field.text, "Hello, World!") + + def test_text_method_with_iso88591_content(self): + content = "Héllo, Wörld!".encode("iso-8859-1") + field = MultiPartFormField(CaseInsensitiveDict(), content, "iso-8859-1") + self.assertEqual(field.text, "Héllo, Wörld!") + + def test_text_method_with_changed_encoding(self): + content = "Hello, World!".encode("iso-8859-1") + field = MultiPartFormField(CaseInsensitiveDict(), content, "utf-8") + field.encoding = "iso-8859-1" # Change the encoding + self.assertEqual(field.text, "Hello, World!") + + def test_bytes_method_with_name_only(self): + headers = CaseInsensitiveDict({"Content-Disposition": 'form-data; name="test"'}) + field = MultiPartFormField(headers) + expected_bytes = b'Content-Disposition: form-data; name="test"\r\n\r\n' + self.assertEqual(bytes(field), expected_bytes) + + def test_bytes_method_with_content_and_type(self): + headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="test"', + "Content-Type": "text/plain", + } + ) + content = b"Hello, World!" + field = MultiPartFormField(headers, content) + expected_bytes = b'Content-Disposition: form-data; name="test"\r\nContent-Type: text/plain\r\n\r\nHello, World!' + self.assertEqual(bytes(field), expected_bytes) + + def test_bytes_method_with_filename_and_content_type(self): + headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="file"; filename="test.txt"', + "Content-Type": "text/plain", + } + ) + content = b"File content" + field = MultiPartFormField(headers, content) + expected_bytes = b'Content-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nFile content' + self.assertEqual(bytes(field), expected_bytes) + + def test_bytes_method_includes_correct_headers_and_content(self): + headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="test"', + "Content-Type": "text/plain", + } + ) + content = b"Test content" + field = MultiPartFormField(headers, content) + expected_start = b'Content-Disposition: form-data; name="test"\r\nContent-Type: text/plain\r\n\r\n' + self.assertTrue(bytes(field).startswith(expected_start)) + self.assertIn(b"Test content", bytes(field)) + + +if __name__ == "__main__": + unittest.main() + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_request.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_request.py new file mode 100644 index 00000000..10136342 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_request.py @@ -0,0 +1,1297 @@ +from __future__ import annotations + + +from typing import ( + cast, +) +from pyscalpel.http.headers import Headers + +from pyscalpel.http.body import ( + JSONFormSerializer, + URLEncodedFormSerializer, + MultiPartFormSerializer, + MultiPartForm, + MultiPartFormField, + URLEncodedForm, + JSONForm, + json_escape_bytes, + json_unescape_bytes, + escape_parameter, +) + +from pyscalpel.http.request import * +import unittest + + +class RequestGenericTestCase(unittest.TestCase): + def test_init(self): + method = "GET" + scheme = "https" + host = "example.com" + port = 443 + path = "/path" + http_version = "HTTP/1.1" + headers = Headers([(b"Content-Type", b"application/json")]) + content = b'{"key": "value"}' + + request = Request( + method=method, + scheme=scheme, + host=host, + port=port, + path=path, + http_version=http_version, + headers=headers, + content=content, + authority="", + ) + + self.assertEqual(request.method, method) + self.assertEqual(request.scheme, scheme) + self.assertEqual(request.host, host) + self.assertEqual(request.port, port) + self.assertEqual(request.path, path) + self.assertEqual(request.http_version, http_version) + self.assertEqual(request.headers, headers) + self.assertEqual(request.content, content) + + def test_make(self): + method = "POST" + url = "http://example.com/path" + content = '{"key": "value"}' + headers = { + "Content-Type": "application/json", + "X-Custom-Header": "custom", + } + + request = Request.make(method, url, content, headers) + + self.assertEqual(request.method, method) + self.assertEqual(request.url, url) + self.assertEqual(request.content, content.encode()) + self.assertEqual(request.headers.get("Content-Type"), "application/json") + self.assertEqual(request.headers.get("X-Custom-Header"), "custom") + + def create_request(self) -> Request: + method = "POST" + scheme = "https" + host = "example.com" + port = 443 + path = "/path" + http_version = "HTTP/1.1" + headers = Headers( + [ + (b"Content-Type", b"application/json; charset=utf-8"), + (b"X-Custom-Header", b"custom"), + ] + ) + content = b'{"key": "value"}' + + return Request( + method=method, + scheme=scheme, + host=host, + port=port, + path=path, + http_version=http_version, + headers=headers, + content=content, + authority="", + ) + + def test_set_url(self): + request = Request.make("GET", "http://example.com/path") + + request.url = "https://example.com/new-path?param=value" + + self.assertEqual(request.scheme, "https") + self.assertEqual(request.host, "example.com") + self.assertEqual(request.port, 443) + self.assertEqual(request.path, "/new-path?param=value") + self.assertEqual(request.url, "https://example.com/new-path?param=value") + + def test_query_params(self): + request = Request.make( + "GET", "http://example.com/path?param1=value1¶m2=value2" + ) + + self.assertEqual( + request.query.get_all("param1"), + ["value1"], + "Failed to get query parameter 'param1'", + ) + self.assertEqual( + request.query.get_all("param2"), + ["value2"], + "Failed to get query parameter 'param2'", + ) + + request.query.set_all("param1", ["new_value1", "new_value2"]) + self.assertEqual( + request.query.get_all("param1"), + ["new_value1", "new_value2"], + "Failed to set query parameter 'param1'", + ) + + request.query.add("param3", "value3") + self.assertEqual( + request.query.get_all("param3"), + ["value3"], + "Failed to add query parameter 'param3'", + ) + + # TODO: Handle remove via None, del ,remove_all + del request.query["param2"] + self.assertEqual( + request.query.get_all("param2"), + [], + "Failed to remove query parameter 'param2'", + ) + + query_params = request.query.items() + self.assertEqual( + list(query_params), + [("param1", "new_value1"), ("param3", "value3")], + "Failed to get query parameters as items()", + ) + + def test_body_content(self): + request = Request.make( + "POST", "http://example.com/path", content=b"request body" + ) + + self.assertEqual(request.content, b"request body", "Failed to get request body") + + request.content = b"new content" + self.assertEqual(request.content, b"new content", "Failed to set request body") + + def test_headers(self): + request = Request.make( + "GET", + "http://example.com/path", + headers={"Content-Type": "application/json"}, + ) + + self.assertEqual( + request.headers.get("Content-Type"), + "application/json", + "Failed to get header 'Content-Type'", + ) + + request.headers["Content-Type"] = "text/html" + self.assertEqual( + request.headers.get("Content-Type"), + "text/html", + "Failed to set header 'Content-Type'", + ) + + del request.headers["Content-Type"] + self.assertIsNone( + request.headers.get("Content-Type"), + "Failed to delete header 'Content-Type'", + ) + + def test_http_version(self): + request = Request.make("GET", "http://example.com/path") + + self.assertEqual(request.http_version, "HTTP/1.1", "Failed to get HTTP version") + + request.http_version = "HTTP/2.0" + self.assertEqual(request.http_version, "HTTP/2.0", "Failed to set HTTP version") + + def test_update_serializer_from_content_type(self): + request = self.create_request() + + # Test existing content-type + request.update_serializer_from_content_type() + self.assertIsInstance(request._serializer, JSONFormSerializer) + + # Test custom content-type + request.headers["Content-Type"] = "application/x-www-form-urlencoded" + request.update_serializer_from_content_type() + self.assertIsInstance(request._serializer, URLEncodedFormSerializer) + + # Test unimplemented content-type + request.headers["Content-Type"] = "application/xml" + with self.assertRaises(FormNotParsedException): + request.update_serializer_from_content_type() + + # Test fail_silently=True + request.update_serializer_from_content_type(fail_silently=True) + self.assertIsInstance(request._serializer, URLEncodedFormSerializer) + + def test_create_defaultform(self): + request = self.create_request() + + # Test with existing form + request.form = {"key": "value"} + form = request.create_defaultform() + self.assertEqual(form, {"key": "value"}) + self.assertIsInstance(request._serializer, JSONFormSerializer) + + # Test without existing form + request.content = None + form = request.create_defaultform() + self.assertEqual(form, {}) + self.assertIsInstance(request._serializer, JSONFormSerializer) + + # Test unimplemented content-type + with self.assertRaises(FormNotParsedException): + request.update_serializer_from_content_type("application/xml") # type: ignore + + # Test fail_silently=True + request.create_defaultform(update_header=True) + self.assertIsInstance(request._serializer, JSONFormSerializer) + + def test_urlencoded_form(self): + request = self.create_request() + + # Test getter + request._deserialized_content = URLEncodedForm([(b"key1", b"value1")]) + request.headers["Content-Type"] = "application/x-www-form-urlencoded" + request._serializer = URLEncodedFormSerializer() + form = request.urlencoded_form + self.assertEqual(form, URLEncodedForm([(b"key1", b"value1")])) + self.assertIsInstance(request._serializer, URLEncodedFormSerializer) + + # Test setter + request.urlencoded_form = URLEncodedForm([(b"key2", b"value2")]) + + # WARNING: Previous form has been invalidated + # self.assertEqual(form, QueryParams([(b"key2", b"value2")])) + + form = request.form + self.assertEqual(form, URLEncodedForm([(b"key2", b"value2")])) + + self.assertIsInstance(request._serializer, URLEncodedFormSerializer) + + def test_json_form(self): + request = self.create_request() + + # Test getter + request._deserialized_content = {"key1": "value1"} + form = request.json_form + self.assertEqual(form, {"key1": "value1"}) + self.assertIsInstance(request._serializer, JSONFormSerializer) + + # Test setter + request.json_form = {"key2": "value2"} + + # WARNING: Previous form has been invalidated + # self.assertEqual(form, {"key2": "value2"}) + + form = request.form + self.assertEqual(form, {"key2": "value2"}) + + self.assertIsInstance(request._serializer, JSONFormSerializer) + + def test_multipart_form(self): + request = self.create_request() + + # Test getter + request.headers["Content-Type"] = ( + "multipart/form-data; boundary=----WebKitFormBoundaryy6klzjxzTk68s1dI" + ) + form = request.multipart_form + self.assertIsInstance(form, MultiPartForm) + self.assertIsInstance(request._serializer, MultiPartFormSerializer) + + # Test setter + request.multipart_form = MultiPartForm( + (MultiPartFormField.make("key", body=b"val"),), + content_type=request.headers["Content-Type"], + ) + self.assertIsInstance(form, MultiPartForm) + self.assertIsInstance(request._serializer, MultiPartFormSerializer) + + def test_multipart_complex(self): + # Real use-case test + request = Request.make("POST", "http://localhost:3000/upload") + + request.multipart_form["query"] = "inserer" + self.assertEqual(request.multipart_form["query"].content, b"inserer") + + request.multipart_form["formulaireQuestionReponses[0][idQuestion]"] = 2081 + self.assertEqual( + request.multipart_form["formulaireQuestionReponses[0][idQuestion]"].content, + b"2081", + ) + + request.multipart_form["formulaireQuestionReponses[0][idReponse]"] = 1027 + self.assertEqual( + request.multipart_form["formulaireQuestionReponses[0][idReponse]"].content, + b"1027", + ) + + request.multipart_form["idQuestionnaire"] = 89 + self.assertEqual( + request.multipart_form["idQuestionnaire"].content, + b"89", + ) + + request.multipart_form["emptyParam"] = "" + self.assertEqual( + request.multipart_form["emptyParam"].content, + b"", + ) + + from base64 import b64decode + + zip_data = b64decode( + """UEsDBBQAAAAIAFpPvlYQIK6pcAAAACMBAAAHABwAbG9sLnBocFVUCQADu6x1ZNBwd2R1eAsAAQTo +AwAABOgDAACzsS/IKOAqSCwqTo0vLinSUM9OrTSMBhJGsSDSGEyaxEbH2mbkq+GWS63ELZmckZ+M +Uy+QNAUpykstLsGvBkiaxdqmpKYWEDIrMSc/L52gYabxhiCH5+Tkq+soqOSXlhSUlmhacxUUZeaV +xBdpIEQAUEsBAh4DFAAAAAgAWk++VhAgrqlwAAAAIwEAAAcAGAAAAAAAAQAAALSBAAAAAGxvbC5w +aHBVVAUAA7usdWR1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBNAAAAsQAAAAAA""" + ) + + request.multipart_form["image"] = MultiPartFormField.make( + "image", "shell.jpg", zip_data + ) + + self.assertEqual(request.multipart_form["image"].name, "image") + self.assertEqual(request.multipart_form["image"].filename, "shell.jpg") + self.assertEqual(request.multipart_form["image"].content_type, "image/jpeg") + self.assertEqual(request.multipart_form["image"].content, zip_data) + # print("\n" + bytes(request.multipart_form).decode("latin-1")) + + def test_multipart_to_json(self): + request = Request.make("POST", "http://localhost:3000/upload") + request.multipart_form["query"] = "inserer" + request.multipart_form["formulaireQuestionReponses[0][idQuestion]"] = 2081 + request.multipart_form["formulaireQuestionReponses[0][idReponse]"] = 1027 + request.multipart_form["idQuestionnaire"] = 89 + request.multipart_form["answer"] = "Hello\nWorld\n!" + + from base64 import b64decode + + zip_data = b64decode( + """UEsDBBQAAAAIAFpPvlYQIK6pcAAAACMBAAAHABwAbG9sLnBocFVUCQADu6x1ZNBwd2R1eAsAAQTo + AwAABOgDAACzsS/IKOAqSCwqTo0vLinSUM9OrTSMBhJGsSDSGEyaxEbH2mbkq+GWS63ELZmckZ+M + Uy+QNAUpykstLsGvBkiaxdqmpKYWEDIrMSc/L52gYabxhiCH5+Tkq+soqOSXlhSUlmhacxUUZeaV + xBdpIEQAUEsBAh4DFAAAAAgAWk++VhAgrqlwAAAAIwEAAAcAGAAAAAAAAQAAALSBAAAAAGxvbC5w + aHBVVAUAA7usdWR1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBNAAAAsQAAAAAA""" + ) + + request.multipart_form["image"] = MultiPartFormField.make( + "image", "shell.jpg", zip_data + ) + + # Convert form to JSON + json_form = request.json_form + # print("JSON_FORM:", json_form) + self.assertEqual(json_form["query"], "inserer") + self.assertEqual( + json_form["formulaireQuestionReponses"]["0"]["idQuestion"], "2081" # type: ignore + ) + self.assertEqual( + json_form["formulaireQuestionReponses"]["0"]["idReponse"], "1027" # type: ignore + ) + self.assertEqual(json_form["idQuestionnaire"], "89") + self.assertEqual(json_form["answer"], "Hello\nWorld\n!") + + # Assert the form is converted to JSON correctly + expected_json_form = { + "query": "inserer", + "formulaireQuestionReponses": { + # - PHP arrays are actually maps, so this maps to a dict because it is the same data structure + # -> Even if the array keys are only int, it can map non contiguously (like having values for indexes 1,2 and 5 but not 3 and 4) + # -> Keys map to string because PHP would map it thay way. + # + # We can imagine alternate conversion mode where digit only keys would be converted to int + # and contigous integer arrays starting from 0 would be mapped to list + # but it could be inconsistent on many edge cases and harder to implement + "0": {"idQuestion": "2081", "idReponse": "1027"} + }, + "idQuestionnaire": "89", + "image": json_escape_bytes(zip_data), + "answer": "Hello\nWorld\n!", + } + + # Assert that the binary data isn't destroyed + self.assertEqual(zip_data, json_unescape_bytes(expected_json_form["image"])) + + self.assertEqual(json_form, expected_json_form) + + def test_json_to_multipart(self): + req = Request.make("POST", "http://localhost:3000/upload") + + # Create JSON form + req.json_form = { + "query": "inserer", + "formulaireQuestionReponses": { + "0": {"idQuestion": "2081", "idReponse": "1027"} + }, + "idQuestionnaire": "89", + "answer": "Hello\nWorld\n!", + } + + from base64 import b64decode + + zip_data = b64decode( + """UEsDBBQAAAAIAFpPvlYQIK6pcAAAACMBAAAHABwAbG9sLnBocFVUCQADu6x1ZNBwd2R1eAsAAQTo + AwAABOgDAACzsS/IKOAqSCwqTo0vLinSUM9OrTSMBhJGsSDSGEyaxEbH2mbkq+GWS63ELZmckZ+M + Uy+QNAUpykstLsGvBkiaxdqmpKYWEDIrMSc/L52gYabxhiCH5+Tkq+soqOSXlhSUlmhacxUUZeaV + xBdpIEQAUEsBAh4DFAAAAAgAWk++VhAgrqlwAAAAIwEAAAcAGAAAAAAAAQAAALSBAAAAAGxvbC5w + aHBVVAUAA7usdWR1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBNAAAAsQAAAAAA""" + ) + + req.json_form["image"] = json_escape_bytes(zip_data) + + # Convert JSON form to multipart form + self.assertIsInstance(req.multipart_form, MultiPartForm) + + # Check values + self.assertEqual(req.multipart_form["query"].content, b"inserer") + self.assertEqual( + req.multipart_form["formulaireQuestionReponses[0][idQuestion]"].content, + b"2081", + ) + self.assertEqual( + req.multipart_form["formulaireQuestionReponses[0][idReponse]"].content, + b"1027", + ) + self.assertEqual(req.multipart_form["idQuestionnaire"].content, b"89") + self.assertEqual(req.multipart_form["answer"].content, b"Hello\nWorld\n!") + self.assertEqual(req.multipart_form["image"].content, zip_data) + + def test_urlencoded_to_multipart(self): + request = self.create_request() + + # Set urlencoded form + request.headers["Content-Type"] = "application/x-www-form-urlencoded" + request.urlencoded_form = URLEncodedForm( + [(b"key1", b"value1"), (b"key2", b"value2")] + ) + + # Check urlencoded form + self.assertEqual( + request.urlencoded_form, + URLEncodedForm([(b"key1", b"value1"), (b"key2", b"value2")]), + ) + + # Transform urlencoded form to multipart form + request.headers["Content-Type"] = ( + "multipart/form-data; boundary=4N_4RB17R4RY_57R1NG" + ) + request.update_serializer_from_content_type() + + multipart_form = request.multipart_form + self.assertIsInstance(multipart_form, MultiPartForm) + self.assertIsInstance(request._serializer, MultiPartFormSerializer) + + # Check multipart form + self.assertEqual(len(multipart_form.fields), 2) + self.assertEqual(multipart_form.fields[0].name, "key1") + self.assertEqual(multipart_form.fields[0].content, b"value1") + self.assertEqual(multipart_form.fields[1].name, "key2") + self.assertEqual(multipart_form.fields[1].content, b"value2") + + # Check byte serialization + expected_bytes = b"--4N_4RB17R4RY_57R1NG\r\n" + expected_bytes += b'Content-Disposition: form-data; name="key1"' + expected_bytes += b"\r\n\r\nvalue1\r\n" + expected_bytes += b"--4N_4RB17R4RY_57R1NG\r\n" + expected_bytes += b'Content-Disposition: form-data; name="key2"' + expected_bytes += b"\r\n\r\nvalue2\r\n" + expected_bytes += b"--4N_4RB17R4RY_57R1NG--\r\n\r\n" + multipart_bytes = bytes(multipart_form) + self.assertEqual(expected_bytes, multipart_bytes) + + def test_urlencoded_to_json(self): + request = Request.make("GET", "http://localhost") + + # Initialize JSON form data + request.urlencoded_form = URLEncodedForm( + [(b"key1", b"value1"), (b"key2", b"value2")] + ) + + # Check initial form data + form = request.urlencoded_form + self.assertEqual( + form, URLEncodedForm([(b"key1", b"value1"), (b"key2", b"value2")]) + ) + self.assertIsInstance(request._serializer, URLEncodedFormSerializer) + + # Convert form to JSON + json_form = request.json_form + + # Validate the JSON form + self.assertEqual(json_form, {"key1": "value1", "key2": "value2"}) + self.assertIsInstance(request._serializer, JSONFormSerializer) + + def test_json_to_urlencoded(self): + request = Request.make("GET", "http://localhost") + + # Initialize JSON form data + request.json_form = {"key1": "value1", "key2": "value2"} + + # Check initial JSON form data + json_form = request.json_form + self.assertEqual(json_form, {"key1": "value1", "key2": "value2"}) + self.assertIsInstance(request._serializer, JSONFormSerializer) + + # Convert JSON to URL-encoded form + urlencoded_form = request.urlencoded_form + + # Validate the URL-encoded form + self.assertEqual( + urlencoded_form, + URLEncodedForm([(b"key1", b"value1"), (b"key2", b"value2")]), + ) + self.assertIsInstance(request._serializer, URLEncodedFormSerializer) + + def test_all_use_cases(self): + req = Request.make( + "GET", + "http://localhost:3000/echo?filename=28.jpg&username=wiener&password=peter", + headers=Headers( + ( + (b"X-Duplicate", b"A"), + (b"X-Duplicate", b"B"), + ) + ), + ) + + # Ensure initial data is correct. + self.assertEqual("GET", req.method) + self.assertEqual( + "/echo?filename=28.jpg&username=wiener&password=peter", req.path + ) + self.assertEqual("localhost", req.host) + self.assertEqual(3000, req.port) + self.assertEqual("localhost:3000", req.headers["Host"]) + self.assertEqual("A, B", req.headers["X-Duplicate"]) + self.assertListEqual(["A", "B"], req.headers.get_all("X-Duplicate")) + + # Input parameters + + # Raw edit query string + new_qs = "saucisse=poulet&chocolat=blanc#" + req.path = req.path.split("?")[0] + "?" + new_qs + self.assertEqual("/echo?saucisse=poulet&chocolat=blanc#", req.path) + + # Dict edit query string + # Implemented by mitmproxy + req.query["saucisse"] = "test123" + self.assertDictEqual( + {"saucisse": "test123", "chocolat": "blanc"}, dict(req.query) + ) + expected = "/echo?saucisse=test123&chocolat=blanc" + self.assertEqual(expected, req.path) + + req.query["saucisse"] = "123" + req.query["number"] = 123 + req.query[4] = 123 + + expected = "/echo?saucisse=123&chocolat=blanc&number=123&4=123" + self.assertEqual(expected, req.path) + + ### SERIALISATION POST ### + + # application/x-www-form-urlencoded + req.create_defaultform("application/x-www-form-urlencoded") + req.urlencoded_form[b"abc"] = b"def" + req.urlencoded_form["mark"] = "jkl" + + req.urlencoded_form[b"mark2"] = "jkl" + req.urlencoded_form["marl3"] = "kkk" + # req.urlencoded_form["123"] = "kkk" + req.urlencoded_form["VACHE"] = "leet" + + req.urlencoded_form[999] = 1337 + + expected = b"abc=def&mark=jkl&mark2=jkl&marl3=kkk&VACHE=leet&999=1337" + self.assertEqual(expected, req.content) + self.assertEqual(len(expected), req.content_length, "Wrong content-lenght") + + req.content = None + self.assertIsNone(req.content) + + # application/json + req.json_form[1] = "test" + + self.assertIsInstance(req._serializer, JSONFormSerializer) + self.assertIsInstance(req.form, JSONForm) + self.assertEqual( + "test", req.form.get(1), f"Broken JSON form: {req._deserialized_content}" + ) + + req.json_form[1.5] = "test" + + self.assertEqual("test", req.form[1.5]) + + req.json_form["test"] = 1 + req.json_form["test2"] = True + req.json_form["test3"] = None + req.json_form["test4"] = [1, 2, 3] + req.json_form["test5"] = {"a": 1, "b": 2, "c": 3} + + # TODO: JSON keys should probably be converted to strings + expected = { + 1: "test", + 1.5: "test", + "test": 1, + "test2": True, + "test3": None, + "test4": [1, 2, 3], + "test5": {"a": 1, "b": 2, "c": 3}, + } + self.assertDictEqual(expected, req.form) + self.assertDictEqual(expected, req.json_form) + expected = b'{"1": "test", "1.5": "test", "test": 1, "test2": true, "test3": null, "test4": [1, 2, 3], "test5": {"a": 1, "b": 2, "c": 3}}' + self.assertEqual(expected, req.content) + self.assertEqual(len(expected), req.content_length, "Wrong content-lenght") + + # multipart/form-data + req.content = None + req.create_defaultform("multipart/form-data ; boundary=---Boundary") + + # Ensure the Content-Type has been updated (required for multipart) + self.assertEqual( + "multipart/form-data ; boundary=---Boundary", req.headers["Content-Type"] + ) + + req.multipart_form["a"] = b"test default" + req.multipart_form["file2"] = b"test html" + req.multipart_form["file2"].filename = "index.html" + req.multipart_form["file2"].content_type = "text/html" + + # Sample data to be written + data = {"name": "John", "age": 30, "city": "New York"} + + import tempfile + import json + from os.path import basename + from urllib.parse import quote + + # Create a temporary file and write some JSON to it + with tempfile.NamedTemporaryFile( + mode="w+", delete=False, suffix=".json" + ) as temp: + json.dump(data, temp) + + first_name = temp.name + req.multipart_form["data.json"] = open(temp.name, "rb") + + # Create an empty temporary file with a malicious name + with tempfile.NamedTemporaryFile( + mode="w+", delete=False, suffix="sp0;0f' ed\"\\" + ) as temp: + second_name = temp.name + req.multipart_form["spoofed"] = open(temp.name, "r", encoding="utf-8") + + quote = lambda param: escape_parameter(param, extended=False) + + expected = '-----Boundary\r\nContent-Disposition: form-data; name="a"\r\n\r\n' + expected += "test default\r\n" + expected += "-----Boundary\r\n" + expected += ( + 'Content-Disposition: form-data; name="file2"; filename="index.html"\r\n' + ) + expected += "Content-Type: text/html\r\n\r\ntest html\r\n" + expected += "-----Boundary\r\n" + expected += f'Content-Disposition: form-data; name="data.json"; filename="{basename(first_name)}"\r\n' + expected += "Content-Type: application/json\r\n\r\n\r\n" + expected += f'-----Boundary\r\nContent-Disposition: form-data; name="spoofed"; filename="{quote(basename(second_name))}"\r\n' + expected += "Content-Type: application/octet-stream\r\n\r\n\r\n" + expected += "-----Boundary--\r\n\r\n" + expected = expected.encode("latin-1") + + self.assertEqual(expected, req.content) + self.assertEqual(len(expected), req.content_length, "Wrong content-lenght") + + ########## Déduire la sérialisation ########## + ############### URLENCODED #################### + req.content = None + req.update_serializer_from_content_type("application/x-www-form-urlencoded") + req.create_defaultform() + + req.form["a"] = "b" + req.form["c"] = "d" + + self.assertEqual(b"a=b&c=d", req.content) + ########## MULTIPART ########## + req.content = None + + req.update_serializer_from_content_type( + "multipart/form-data; boundary=--------------------2763ba3527064667e1c4f57ca596c055" + ) + req.create_defaultform() + + req.form[b"a"] = b"b" + req.form[b"c"] = b"d" + req.form[b"e"] = b"f" + + expected = b'-----Boundary\r\nContent-Disposition: form-data; name="a"' + expected += ( + b'\r\n\r\nb\r\n-----Boundary\r\nContent-Disposition: form-data; name="c"' + ) + expected += ( + b'\r\n\r\nd\r\n-----Boundary\r\nContent-Disposition: form-data; name="e"' + ) + expected += b"\r\n\r\nf\r\n-----Boundary--\r\n\r\n" + self.assertEqual(expected, req.content) + self.assertEqual(len(expected), req.content_length, "Wrong content-lenght") + + # Set and remove the content-type + expected = "text/html" + req.multipart_form["a"].content_type = expected + self.assertEqual( + expected, + req.multipart_form["a"].headers["Content-Type"], + "Failed to set multipart field content-type", + ) + + req.multipart_form["a"].content_type = None + self.assertIsNone(req.multipart_form["a"].headers.get("Content-Type")) + + # ("############# JSON ##############") + + req.content = None + req.update_serializer_from_content_type("application/json") + + req.create_defaultform() + req.form["a"] = "chocolat" + req.form["b"] = 3 + req.form["c"] = True + req.form["d"] = None + req.form["e"] = [1, 2, 3] + req.form["f"] = {"a": 1, "b": 2, "c": 3} + + expected = b'{"a": "chocolat", "b": 3, "c": true, "d": null, "e": [1, 2, 3], "f": {"a": 1, "b": 2, "c": 3}}' + self.assertEqual(expected, req.content) + req.content = None + + # Test that the seriailizer has been deducted from content-type + req.create_defaultform("application/json") + req.form["obj"] = {"sub1": [1, 2, 3], "sub2": 4} + self.assertEqual(b'{"obj": {"sub1": [1, 2, 3], "sub2": 4}}', req.content) + + def test_cookies(self): + req = Request.make("GET", "http://localhost") + token = "f37088cde673e4741fcd30882f5ccfaf" + req.cookies["session"] = token + self.assertEqual(token, req.cookies["session"]) + self.assertEqual(f"session={token}", req.headers["Cookie"]) + expected = {"session": token} + self.assertDictEqual(expected, dict(req.cookies)) + + req.cookies["tracking"] = "1" + self.assertEqual("1", req.cookies["tracking"]) + self.assertEqual(f"session={token}; tracking=1", req.headers["Cookie"]) + expected = {"session": token, "tracking": "1"} + self.assertDictEqual(expected, dict(req.cookies)) + + expected = {"hello": "world", "ambionics": "lexfo"} + req.cookies = expected + self.assertDictEqual(expected, dict(req.cookies)) + + def test_host_header(self): + req = Request.make("GET", "http://localhost") + self.assertEqual("localhost", req.host_header) + self.assertEqual(req.headers["Host"], req.host_header) + + req.host_header = "lexfo.fr" + self.assertEqual("lexfo.fr", req.host_header) + + def test_check_param(self): + # Easily check if a param exists + req = Request.make("GET", "http://localhost?hello=world") + self.assertTrue(req.query.get("hello")) + self.assertFalse(req.query.get("absent")) + + def test_content_length(self): + req = Request.make("GET", "http://localhost?hello=world") + self.assertEqual(0, req.content_length) + + req.content = b"lol" + self.assertEqual(3, req.content_length) + + with self.assertRaises(RuntimeError): + req.content_length = 1337 + + req.update_content_length = False + + req.content_length = 1337 + self.assertEqual(1337, req.content_length) + + req.content = b"Hello World" + self.assertEqual(1337, req.content_length) + + req.update_content_length = True + self.assertEqual(11, req.content_length) + + def test_host_is(self): + req = Request.make("GET", "http://mail.int.google.com") + + self.assertTrue(req.host_is("mail.int.google.com")) + self.assertTrue(req.host_is("*.google.com")) + self.assertTrue(req.host_is("*google.com")) + self.assertTrue(req.host_is("mail.*")) + self.assertTrue(req.host_is("*.com")) + self.assertTrue(req.host_is("*.*gle.*")) + self.assertTrue(req.host_is("mail.*.*.com")) + self.assertTrue(req.host_is("mail.*.com")) + + self.assertFalse(req.host_is("google")) + self.assertFalse(req.host_is("mail.int.google.fr")) + self.assertFalse(req.host_is("*.gle.*")) + self.assertFalse(req.host_is(".com")) + + def test_urlencoded_bug(self): + req = Request.make( + "POST", + "http://localhost:3000/encrypt", + b"secret=MySecretKey&content=", + {"Content-Type": "application/x-www-form-urlencoded"}, + ) + + form = req.urlencoded_form + self.assertIsNotNone(form) + self.assertIsInstance(form, URLEncodedForm) + expected = ((b"secret", b"MySecretKey"), (b"content", b"")) + self.assertTupleEqual(expected, form.fields) + + def test_form_bug(self): + req = Request.make( + "POST", + "http://localhost:3000/encrypt", + b"secret=MySecretKey&encrypted=BNTYvqs5E%2BE%2Bgx0J%2B6yCG%2FUDUChX3yf61ks%2FZeUei7k%3D", + {"Content-Type": "application/x-www-form-urlencoded"}, + ) + + form = cast(URLEncodedForm, req.form) + self.assertIsNotNone(form) + content_type = req.headers.get("Content-Type") + self.assertIsNotNone(content_type) + self.assertEqual("application/x-www-form-urlencoded", content_type) + self.assertIsInstance(form, URLEncodedForm) + expected = ( + (b"secret", b"MySecretKey"), + (b"encrypted", b"BNTYvqs5E+E+gx0J+6yCG/UDUChX3yf61ks/ZeUei7k="), + ) + self.assertTupleEqual(expected, form.fields) + + def test_content_do_not_modify_json(self): + """ + If the user doesn't use the .json form, do not unserialize/reserialize json because it will modify whitespaces + which might break stuff, especially in GraphQL APIS + """ + expected = b'{"hello":"world"}' + req = Request.make( + "POST", + "http://localhost:3000/graphql", + expected, + {"Content-Type": "application/json"}, + ) + self.assertEqual(expected, req.content) + + def test_content_do_not_modify_urlencoded(self): + """ + If the user doesn't use the forms feature, do not unserialize/reserialize, because it might break stuff + when the app doesnt urlencode as expected. + + Proper url encoding would be hello+world or hello%20world, but some apps may only accept the first, the second, or no encoding at all + a generic urlencoder can not handle this cases, so the least is to not break the body on request where the content is not modified using .form features + """ + expected = b"abc=hello world&efg=apan+yan&hij=quoi%20coubeh" + req = Request.make( + "POST", + "http://localhost:3000/signup", + expected, + {"Content-Type": "x-www-form-urlencoded"}, + ) + self.assertEqual(expected, req.content) + + def test_path_is(self): + req = Request.make("GET", "http://example.com/abc/def") + + self.assertTrue(req.path_is("*")) + self.assertTrue(req.path_is("/abc/def")) + self.assertTrue(req.path_is("/abc/*")) + self.assertTrue(req.path_is("*/def")) + self.assertTrue(req.path_is("*/def*")) + self.assertTrue(req.path_is("/abc/def*")) + + +class TestRequestBytesMethod(unittest.TestCase): + def test_bytes_method_basic_get_request(self): + request = Request( + method="GET", + scheme="http", + host="example.com", + port=80, + path="/", + http_version="HTTP/1.1", + headers=[(b"Host", b"example.com")], + authority="example.com", + content=None, + ) + expected_bytes = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + self.assertEqual(bytes(request), expected_bytes) + + def test_bytes_method_post_request_with_body(self): + request = Request( + method="POST", + scheme="http", + host="example.com", + port=80, + path="/submit", + http_version="HTTP/1.1", + headers=[ + (b"Host", b"example.com"), + (b"Content-Type", b"application/x-www-form-urlencoded"), + ], + authority="example.com", + content=b"key=value", + ) + # Correctly include the Content-Length header in the expected bytes string. + expected_bytes = b"POST /submit HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 9\r\n\r\nkey=value" + self.assertEqual(bytes(request), expected_bytes) + + def test_bytes_method_with_custom_headers(self): + headers = Headers( + [(b"Host", b"example.com"), (b"X-Custom-Header", b"CustomValue")] + ) + request = Request( + method="GET", + scheme="http", + host="example.com", + port=80, + path="/custom", + http_version="HTTP/1.1", + headers=headers, + authority="example.com", + content=None, + ) + expected_bytes = b"GET /custom HTTP/1.1\r\nHost: example.com\r\nX-Custom-Header: CustomValue\r\n\r\n" + self.assertEqual(bytes(request), expected_bytes) + + def test_bytes_method_http2_to_http1_conversion(self): + # Simulate an HTTP/2 request with an :authority pseudo-header and without a Host header + headers = Headers( + [ + (b":authority", b"example.com"), + (b"Content-Type", b"application/json"), + ] + ) + request = Request( + method="GET", + scheme="https", + host="example.com", + port=443, + path="/", + http_version="HTTP/2", + headers=headers, + authority="example.com", + content=None, + ) + # Expected bytes should include a Host header derived from the :authority pseudo-header, + # and the HTTP/2 pseudo-header should be removed. + expected_bytes = b"GET / HTTP/2\r\nHost: example.com\r\nContent-Type: application/json\r\n\r\n" + self.assertEqual(bytes(request), expected_bytes) + + +class TestRequestContentType(unittest.TestCase): + def setUp(self): + self.request = Request( + method="GET", + scheme="http", + host="example.com", + port=80, + path="/", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=None, + ) + + def test_content_type_set_and_get(self): + # Test setting content_type + self.request.content_type = "application/json" + self.assertIn("Content-Type", self.request.headers) + self.assertEqual(self.request.headers["Content-Type"], "application/json") + + # Test getting content_type + content_type = self.request.content_type + self.assertEqual(content_type, "application/json") + + def test_content_type_get_when_not_set(self): + # Ensure content_type returns None when not set + content_type = self.request.content_type + self.assertIsNone(content_type) + + +class TestRequestQuerySetter(unittest.TestCase): + def setUp(self): + self.base_url = "http://example.com/path/to/resource" + self.request = Request( + method="GET", + scheme="http", + host="example.com", + port=80, + path="/path/to/resource", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=None, + ) + + def test_update_query_string(self): + # Set the query string when none exists + self.request.query = [("param1", "value1"), ("param2", "value2")] + self.assertEqual( + self.request.url, self.base_url + "?param1=value1¶m2=value2" + ) + + def test_replace_existing_query_string(self): + # Initialize with an existing query string + initial_query = "initial1=val1&initial2=val2" + self.request.url = self.base_url + "?" + initial_query + # Replace the existing query string + self.request.query = [("newparam1", "newvalue1"), ("newparam2", "newvalue2")] + self.assertEqual( + self.request.url, self.base_url + "?newparam1=newvalue1&newparam2=newvalue2" + ) + + def test_empty_query_string(self): + # Set an empty query string + self.request.query = [] + self.assertEqual(self.request.url, self.base_url) + + +class TestRequestBodyProperty(unittest.TestCase): + def setUp(self): + self.request = Request( + method="POST", + scheme="http", + host="example.com", + port=80, + path="/submit", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=b"initial body content", + ) + + def test_body_property_with_bytes(self): + # Update and retrieve the body with bytes + new_body = b"new body content" + self.request.body = new_body + self.assertEqual(self.request.body, new_body) # Use the getter here + self.assertEqual(self.request.headers["Content-Length"], str(len(new_body))) + + def test_body_property_with_str(self): + # Update and retrieve the body with a string + new_body_str = "new string content" + self.request.body = new_body_str + retrieved_body = self.request.body # Use the getter here + self.assertEqual(retrieved_body, new_body_str.encode()) + self.assertEqual( + self.request.headers["Content-Length"], str(len(new_body_str.encode())) + ) + + def test_body_property_with_none(self): + # Set the body to None and check + self.request.body = None + self.assertIsNone(self.request.body) # Use the getter here + self.assertFalse("Content-Length" in self.request.headers) + + +class TestRequestContentLengthProperty(unittest.TestCase): + def setUp(self): + self.request = Request( + method="GET", + scheme="http", + host="example.com", + port=80, + path="/", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=None, + ) + + def test_content_length_absent(self): + # Verify content_length returns 0 when the Content-Length header is absent + self.assertEqual(self.request.content_length, 0) + + def test_content_length_invalid(self): + # Set the Content-Length header to a non-digit value + self.request.update_content_length = False + self.request.content_length = "invalid" + + extracted = self.request.headers.get("Content-Length") + self.assertIsNotNone(extracted) + self.assertEqual(extracted, "invalid") + + # Verify that accessing content_length raises ValueError + with self.assertRaises(ValueError) as context: + _ = self.request.content_length + self.assertIn( + "Content-Length does not contain only digits", str(context.exception) + ) + + +class TestRequestTextMethod(unittest.TestCase): + def setUp(self): + self.request = Request( + method="POST", + scheme="http", + host="example.com", + port=80, + path="/data", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=b"", + ) + + def test_text_with_no_content(self): + # Test that text returns an empty string when content is None + self.request.content = None + self.assertEqual(self.request.text(), "") + + def test_text_with_default_utf8_encoding(self): + # Test decoding with default utf-8 encoding + self.request.content = b"\xc3\xa9" + self.assertEqual(self.request.text(), "é") + + def test_text_with_specified_encoding(self): + # Test decoding with a specified encoding + self.request.content = b"\xe9" + self.assertEqual(self.request.text(encoding="iso-8859-1"), "é") + + +class TestRequestCookiesSetter(unittest.TestCase): + def setUp(self): + self.request = Request( + method="GET", + scheme="http", + host="example.com", + port=80, + path="/", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=None, + ) + + def test_set_single_cookie(self): + # Test setting a single cookie + self.request.cookies = (("sessionid", "123456"),) + self.assertEqual(self.request.headers["Cookie"], "sessionid=123456") + + def test_set_multiple_cookies(self): + # Test setting multiple cookies + self.request.cookies = (("sessionid", "123456"), ("userid", "abcde")) + self.assertIn("sessionid=123456; userid=abcde", self.request.headers["Cookie"]) + + def test_set_no_cookies_removes_cookie_header(self): + # Test setting no cookies removes the Cookie header if it exists + # Set a cookie first to ensure the header exists + self.request.cookies = (("sessionid", "123456"),) + self.assertTrue(self.request.headers.get("cookie")) + # Now set no cookies and expect the Cookie header to be removed + self.request.cookies = () + self.assertFalse(self.request.headers.get("cookie")) + + +class TestRequestCreateDefaultFormErrors(unittest.TestCase): + def setUp(self): + self.request = Request( + method="POST", + scheme="http", + host="example.com", + port=80, + path="/submit", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=None, + ) + + def test_create_form_with_unsupported_content_type(self): + # Set an unsupported content type directly + self.request.headers["Content-Type"] = "application/unsupported" + with self.assertRaises(FormNotParsedException) as context: + self.request.create_defaultform("application/unsupported") + self.assertIn( + "implemented", + str(context.exception), + ) + + def test_create_form_with_unparsable_content_for_supported_content_type(self): + # Set a supported content type but provide unparsable content + self.request.headers["Content-Type"] = "application/json" + self.request.content = b"unparsable json content" + with self.assertRaises(FormNotParsedException) as context: + self.request.create_defaultform("application/json") + self.assertTrue("Could not parse content" in str(context.exception)) + + +from requests.structures import CaseInsensitiveDict + + +class TestRequestMultiPartFormSetter(unittest.TestCase): + def setUp(self): + self.request = Request( + method="POST", + scheme="http", + host="example.com", + port=80, + path="/upload", + http_version="HTTP/1.1", + headers=Headers(), + authority="example.com", + content=None, + ) + + def test_multipart_form_sets_content_type_header_with_field(self): + # Create a MultiPartFormField + field_headers = CaseInsensitiveDict( + { + "Content-Disposition": 'form-data; name="field1"', + "Content-Type": "text/plain", + } + ) + field_content = b"field content" + field = MultiPartFormField(headers=field_headers, content=field_content) + + # Assuming MultiPartForm accepts a list of MultiPartFormField objects + multipart_form = MultiPartForm( + fields=[field], content_type="multipart/form-data" + ) + + # Set the multipart form, triggering the setter + self.request.multipart_form = multipart_form + + # Verify that _ensure_multipart_content_type() was called by checking the Content-Type header + content_type_header = self.request.headers.get("Content-Type") + self.assertTrue( + content_type_header.startswith("multipart/form-data; boundary="), + "Content-Type header was not set correctly for multipart/form-data.", + ) + + +class TestRequestEncoding(unittest.TestCase): + def test_utf16(self): + req_bytes = """GET / HTTP/1.1\r +Host: localhost:3000\r +X-Test: ééé\r +Content-Length: 0\r +\r +""" + + req = Request.make( + "GET", + "http://localhost:3000", + headers={"Host": "localhost:3000", "X-Test": "ééé"}, + ) + new_bytes = bytes(req).decode("latin-1") + self.assertEqual(new_bytes, req_bytes) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_response.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_response.py new file mode 100644 index 00000000..72b7cc50 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_response.py @@ -0,0 +1,237 @@ +import unittest +from unittest.mock import MagicMock, patch +from pyscalpel.java.burp import IHttpHeader +from pyscalpel.http.response import * + + +class ResponseTestCase(unittest.TestCase): + def test_from_mitmproxy(self): + mitmproxy_response = MITMProxyResponse.make( + 200, + b"Hello World!", + Headers([(b"Content-Type", b"text/html")]), + ) + response = Response.from_mitmproxy(mitmproxy_response) + + self.assertEqual("HTTP/1.1", response.http_version) + self.assertEqual(200, response.status_code) + self.assertEqual("OK", response.reason) + + # TODO: Add an update_content_length flag like in Request. + # (requires dropping mitmproxy and writting from scratch) + del response.headers["Content-Length"] + self.assertEqual(Headers([(b"Content-Type", b"text/html")]), response.headers) + self.assertEqual(b"Hello World!", response.content) + self.assertIsNone(response.trailers) + + def test_make(self): + response = Response.make( + status_code=200, + content=b"Hello World!", + headers=Headers([(b"Content-Type", b"text/html")]), + host="localhost", + port=8080, + scheme="http", + ) + + # TODO: Add an update_content_length flag like in Request. + # (requires dropping mitmproxy and writting from scratch) + del response.headers["Content-Length"] + + self.assertEqual("HTTP/1.1", response.http_version) + self.assertEqual(200, response.status_code) + self.assertEqual("OK", response.reason) + self.assertEqual(Headers([(b"Content-Type", b"text/html")]), response.headers) + self.assertEqual(b"Hello World!", response.content) + self.assertIsNone(response.trailers) + self.assertEqual("http", response.scheme) + self.assertEqual("localhost", response.host) + self.assertEqual(8080, response.port) + + def test_host_is(self): + response = Response.make(200) + response.host = "example.com" + + self.assertTrue(response.host_is("example.com")) + self.assertFalse(response.host_is("google.com")) + + +class TestResponseMockedBasic(unittest.TestCase): + def setUp(self): + # Create a complete mock for IHttpResponse + self.mock_response = MagicMock(spec=IHttpResponse) + self.mock_response.httpVersion.return_value = "HTTP/1.1" + self.mock_response.statusCode.return_value = 200 + self.mock_response.reasonPhrase.return_value = "OK" + + # Mocking headers to return a list of IHttpHeader mocks + mock_header1 = MagicMock(spec=IHttpHeader) + mock_header1.name.return_value = "Content-Type" + mock_header1.value.return_value = "text/html" + + mock_header2 = MagicMock(spec=IHttpHeader) + mock_header2.name.return_value = "Server" + mock_header2.value.return_value = "Apache" + + self.mock_response.headers.return_value = [mock_header1, mock_header2] + + # Mocking body to return an IByteArray mock + mock_body = MagicMock(spec=IByteArray) + mock_body.getBytes.return_value = b"" + self.mock_response.body.return_value = mock_body + + @patch("pyscalpel.http.response.Headers.from_burp") + def test_from_burp(self, mock_headers_from_burp): + # Setup mock Headers.from_burp + mock_headers_from_burp.return_value = Headers([]) + + # Call the method under test + response = Response.from_burp(self.mock_response) + + # Asserts + self.assertEqual(response.http_version, "HTTP/1.1") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.reason, "OK") + self.assertEqual(response.content, b"") + mock_headers_from_burp.assert_called_once() + + +class TestResponseFromBurp(unittest.TestCase): + def setUp(self): + self.mock_response = MagicMock(spec=IHttpResponse) + self.mock_response.httpVersion.return_value = "HTTP/1.1" + self.mock_response.statusCode.return_value = 200 + self.mock_response.reasonPhrase.return_value = "OK" + self.mock_response.headers.return_value = [] + self.mock_response.body.return_value = MagicMock( + spec=IByteArray, getBytes=lambda: b"response body" + ) + + self.mock_service = MagicMock(spec=IHttpService) + self.mock_service.secure.return_value = False + self.mock_service.host.return_value = "example.com" + self.mock_service.port.return_value = 80 + + self.mock_request = MagicMock(spec=IHttpRequest) + self.mock_request.httpService.return_value = self.mock_service + + @patch("pyscalpel.http.response.Headers.from_burp") + @patch("pyscalpel.http.response.Request.from_burp") + def test_from_burp_with_direct_request_and_service( + self, mock_request_from_burp, mock_headers_from_burp + ): + mock_request_from_burp.return_value = MagicMock() + mock_headers_from_burp.return_value = MagicMock() + + response = Response.from_burp( + self.mock_response, self.mock_service, self.mock_request + ) + + mock_request_from_burp.assert_called_once_with( + self.mock_request, self.mock_service + ) + self.assertEqual(response.scheme, "http") + self.assertEqual(response.host, "example.com") + self.assertEqual(response.port, 80) + + @patch("pyscalpel.http.response.Headers.from_burp") + @patch("pyscalpel.http.response.Request.from_burp") + def test_from_burp_with_request_obtained_from_response( + self, mock_request_from_burp, mock_headers_from_burp + ): + self.mock_response.initiatingRequest = MagicMock(return_value=self.mock_request) + mock_request_from_burp.return_value = MagicMock() + mock_headers_from_burp.return_value = MagicMock() + + response = Response.from_burp(self.mock_response) + + mock_request_from_burp.assert_called_once_with(self.mock_request, None) + self.mock_request.httpService.assert_called_once() + + @patch("pyscalpel.http.response.Headers.from_burp") + @patch("pyscalpel.http.response.Request.from_burp") + def test_from_burp_with_secure_service( + self, mock_request_from_burp, mock_headers_from_burp + ): + self.mock_service.secure.return_value = True + mock_request_from_burp.return_value = MagicMock() + mock_headers_from_burp.return_value = MagicMock() + + response = Response.from_burp(self.mock_response, self.mock_service) + + self.assertEqual(response.scheme, "https") + + +class TestResponseBytesConversion(unittest.TestCase): + def test_response_to_bytes(self): + # Example 1: Basic response with Content-Length header + content1 = b"Hello World!" + response1 = Response.make( + status_code=200, + content=content1, + headers=((b"Content-Type", b"text/html"),), + host="www.example.com", + port=80, + scheme="http", + ) + # NOTE: The Content-Length header is automatically added by the MITMProxy Response object + # MITMProxy sets headers to lowercase by default, so the Content-Length header is lowercase + # If an existing Content-Length header with mixed case is already present, it will be not be overwritten + # This is only the case for the Response object and not the Request, so it should probably not matter anyway. + expected_bytes1 = ( + b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\ncontent-length: " + + str(len(content1)).encode() + + b"\r\n\r\nHello World!" + ) + self.assertEqual(bytes(response1), expected_bytes1) + + # Example 2: More complex response with multiple headers, different scheme, and Content-Length header + content2 = b"Not Found" + response2 = Response.make( + status_code=404, + content=content2, + headers=((b"Content-Type", b"text/plain"), (b"Server", b"TestServer")), + host="www.another-example.com", + port=443, + scheme="https", + ) + expected_bytes2 = ( + b"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nServer: TestServer\r\ncontent-length: " + + str(len(content2)).encode() + + b"\r\n\r\nNot Found" + ) + self.assertEqual(bytes(response2), expected_bytes2) + + +class TestResponseBodyProperty(unittest.TestCase): + def test_body_property_and_bytes_output(self): + # Initial response setup without unused parameters + response = Response.make( + status_code=200, + content=b"Original content", + headers=((b"Content-Type", b"text/plain"),), + ) + + # Update the body content + new_content = b"Updated content" + response.body = new_content + + # Check if the body property reflects the update + self.assertEqual(response.body, new_content) + + # Build expected bytes output considering the update + expected_bytes = ( + b"HTTP/1.1 200 OK\r\n" + b"Content-Type: text/plain\r\n" + b"content-length: " + str(len(new_content)).encode() + b"\r\n" + b"\r\n" + b"Updated content" + ) + + # Assuming the __bytes__ method correctly calculates and includes the Content-Length header + # and other details based on the current state of the Response object + self.assertEqual(bytes(response), expected_bytes) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_utils.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_utils.py new file mode 100644 index 00000000..1217b079 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_utils.py @@ -0,0 +1,72 @@ +import unittest +from unittest.mock import patch, MagicMock +from pyscalpel.utils import ( + removeprefix, + current_function_name, + get_tab_name, +) # Adjust import paths as necessary + + +class TestPyscalpelUtil(unittest.TestCase): + # Tests for removeprefix + def test_removeprefix_with_str(self): + self.assertEqual(removeprefix("TestString", "Test"), "String") + + def test_removeprefix_with_bytes(self): + self.assertEqual(removeprefix(b"TestBytes", b"Test"), b"Bytes") + + def test_removeprefix_no_match_str(self): + self.assertEqual(removeprefix("TestString", "XYZ"), "TestString") + + def test_removeprefix_no_match_bytes(self): + self.assertEqual(removeprefix(b"TestBytes", b"XYZ"), b"TestBytes") + + # Tests for current_function_name + @patch("pyscalpel.utils.inspect.currentframe") + def test_current_function_name(self, mock_currentframe): + mock_currentframe.return_value = None + self.assertEqual("", second=current_function_name()) + + frame = MagicMock() + mock_currentframe.return_value = frame + + frame.f_back = None + self.assertEqual("", second=current_function_name()) + + frame.f_back = MagicMock() + frame.f_back.f_code.co_name = "test_function" + + self.assertEqual(current_function_name(), "test_function") + + @patch("pyscalpel.utils.inspect.currentframe") + def test_get_tab_name_from_editor_callback(self, mock_currentframe): + # Create a mock for the frame's code object with the desired function name + mock_code = MagicMock() + mock_code.co_name = "req_edit_in_test_editor" + + # Create a mock for the frame and link the mock code object + mock_frame = MagicMock() + mock_frame.f_code = mock_code + + # Setup the frame chain to reflect the expected call stack + mock_frame.f_back = MagicMock() + mock_frame.f_back.f_code = mock_code + mock_frame.f_back.f_back = None # End of the chain + + mock_currentframe.return_value = mock_frame + + self.assertEqual(get_tab_name(), "test_editor") + + @patch("pyscalpel.utils.inspect.currentframe") + def test_get_tab_name_not_from_editor_callback_raises(self, mock_currentframe): + frame = MagicMock() + frame.f_code.co_name = "nope" + frame.f_back = None + mock_currentframe.return_value = frame + + with self.assertRaises(RuntimeError): + print(get_tab_name()) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_venv.py b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_venv.py new file mode 100644 index 00000000..8e54b1f6 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/tests/test_venv.py @@ -0,0 +1,52 @@ +import os + +# Optional because it slows downs tests by a lot and isn't much useful +if os.getenv("_VENV_TESTS"): + import shutil + from unittest.mock import patch + import unittest + + from pyscalpel.venv import * + + class TestEnvironmentManager(unittest.TestCase): + def setUp(self): + self.venv_path = os.path.join( + os.path.expanduser("~"), ".scalpel", "test_venv" + ) + if os.path.exists(self.venv_path): + shutil.rmtree(self.venv_path) + create(self.venv_path) + + @patch("subprocess.call", return_value=0) + def test_install(self, mock_subprocess_call): + activate(self.venv_path) + result = install("requests") + pip_path = os.path.join(sys.prefix, "bin", "pip") + mock_subprocess_call.assert_called_once_with( + [pip_path, "install", "--require-virtualenv", "requests"] + ) + self.assertEqual(result, 0) + + @patch("subprocess.call", return_value=0) + def test_uninstall(self, mock_subprocess_call): + activate(self.venv_path) + result = uninstall("requests") + pip_path = os.path.join(sys.prefix, "bin", "pip") + mock_subprocess_call.assert_called_once_with( + [pip_path, "uninstall", "--require-virtualenv", "-y", "requests"] + ) + self.assertEqual(result, 0) + + def test_activate_deactivate(self): + activate(self.venv_path) + self.assertEqual(sys.prefix, os.path.abspath(self.venv_path)) + deactivate() + self.assertEqual(sys.prefix, _old_prefix) + self.assertEqual(sys.exec_prefix, _old_exec_prefix) + + def tearDown(self): + if os.path.exists(self.venv_path): + shutil.rmtree(self.venv_path) + + if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/utils.py b/scalpel/src/main/resources/python3-10/pyscalpel/utils.py new file mode 100644 index 00000000..35e57e63 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/utils.py @@ -0,0 +1,74 @@ +import inspect +from typing import TypeVar, Union +from pyscalpel.burp_utils import ( + urldecode, + urlencode_all, +) + + +T = TypeVar("T", str, bytes) + + +def removeprefix(s: T, prefix: Union[str, bytes]) -> T: + if isinstance(s, str) and isinstance(prefix, str): + if s.startswith(prefix): + return s[len(prefix) :] # type: ignore + elif isinstance(s, bytes) and isinstance(prefix, bytes): + if s.startswith(prefix): + return s[len(prefix) :] # type: ignore + return s + + +def removesuffix(s: T, suffix: Union[str, bytes]) -> T: + if isinstance(s, str) and isinstance(suffix, str): + if s.endswith(suffix): + return s[: -len(suffix)] + elif isinstance(s, bytes) and isinstance(suffix, bytes): + if s.endswith(suffix): + return s[: -len(suffix)] + return s + + +def current_function_name() -> str: + """Get current function name + + Returns: + str: The function name + """ + frame = inspect.currentframe() + if frame is None: + return "" + + caller_frame = frame.f_back + if caller_frame is None: + return "" + + return caller_frame.f_code.co_name + + +def get_tab_name() -> str: + """Get current editor tab name + + Returns: + str: The tab name + """ + frame = inspect.currentframe() + prefixes = ("req_edit_in", "req_edit_out") + + # Go to previous frame till the editor name is found + while frame is not None: + frame_name = frame.f_code.co_name + for prefix in prefixes: + if frame_name.startswith(prefix): + return removeprefix(removeprefix(frame_name, prefix), "_") + + frame = frame.f_back + + raise RuntimeError("get_tab_name() wasn't called from an editor callback.") + + +__all__ = [ + "urldecode", + "urlencode_all", + "current_function_name", +] diff --git a/scalpel/src/main/resources/python3-10/pyscalpel/venv.py b/scalpel/src/main/resources/python3-10/pyscalpel/venv.py new file mode 100644 index 00000000..53a28c02 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/pyscalpel/venv.py @@ -0,0 +1,114 @@ +""" +This module provides reimplementations of Python virtual environnements scripts + +This is designed to be used internally, +but in the case where the user desires to dynamically switch venvs using this, +they should ensure the selected venv has the dependencies required by Scalpel. +""" + +import os +import sys +import glob +import subprocess + +_old_prefix = sys.prefix +_old_exec_prefix = sys.exec_prefix + +# Python's virtualenv's activate/deactivate ported from the bash script to Python code. +# https://docs.python.org/3/library/venv.html#:~:text=each%20provided%20path.-,How%20venvs%20work%C2%B6,-When%20a%20Python + +# pragma: no cover + + +def deactivate() -> None: # pragma: no cover + """Deactivates the current virtual environment.""" + if "_OLD_VIRTUAL_PATH" in os.environ: + os.environ["PATH"] = os.environ["_OLD_VIRTUAL_PATH"] + del os.environ["_OLD_VIRTUAL_PATH"] + if "_OLD_VIRTUAL_PYTHONHOME" in os.environ: + os.environ["PYTHONHOME"] = os.environ["_OLD_VIRTUAL_PYTHONHOME"] + del os.environ["_OLD_VIRTUAL_PYTHONHOME"] + if "VIRTUAL_ENV" in os.environ: + del os.environ["VIRTUAL_ENV"] + + sys.prefix = _old_prefix + sys.exec_prefix = _old_exec_prefix + + +def activate(path: str | None) -> None: # pragma: no cover + """Activates the virtual environment at the given path.""" + deactivate() + + if path is None: + return + + virtual_env = os.path.abspath(path) + os.environ["_OLD_VIRTUAL_PATH"] = os.environ.get("PATH", "") + os.environ["VIRTUAL_ENV"] = virtual_env + + old_pythonhome = os.environ.pop("PYTHONHOME", None) + if old_pythonhome: + os.environ["_OLD_VIRTUAL_PYTHONHOME"] = old_pythonhome + + if os.name == "nt": + site_packages_paths = os.path.join(virtual_env, "Lib", "site-packages") + else: + site_packages_paths = glob.glob( + os.path.join(virtual_env, "lib", "python*", "site-packages") + ) + + if not site_packages_paths: + raise RuntimeError( + f"No 'site-packages' directory found in virtual environment at {virtual_env}" + ) + + site_packages = site_packages_paths[0] + sys.path.insert(0, site_packages) + sys.prefix = virtual_env + sys.exec_prefix = virtual_env + + +def install(*packages: str) -> int: # pragma: no cover + """Install a Python package in the current venv. + + Returns: + int: The pip install command exit code. + """ + pip = os.path.join(sys.prefix, "bin", "pip") + return subprocess.call([pip, "install", "--require-virtualenv", "--", *packages]) + + +def uninstall(*packages: str) -> int: # pragma: no cover + """Uninstall a Python package from the current venv. + + Returns: + int: The pip uninstall command exit code. + """ + pip = os.path.join(sys.prefix, "bin", "pip") + return subprocess.call( + [pip, "uninstall", "--require-virtualenv", "-y", "--", *packages] + ) + + +def create(path: str) -> int: # pragma: no cover + """Creates a Python venv on the given path + + Returns: + int: The `python3 -m venv` command exit code. + """ + return subprocess.call(["python3", "-m", "venv", "--", path]) + + +def create_default() -> str: # pragma: no cover + """Creates a default venv in the user's home directory + Only creates it if the directory doesn't already exist + + Returns: + str: The venv directory path. + """ + scalpel_venv = os.path.join(os.path.expanduser("~"), ".scalpel", "venv_default") + # Don't recreate the venv if it alreay exists + if not os.path.exists(scalpel_venv): + os.makedirs(scalpel_venv, exist_ok=True) + create(scalpel_venv) + return scalpel_venv diff --git a/scalpel/src/main/resources/python3-10/qs/.gitignore b/scalpel/src/main/resources/python3-10/qs/.gitignore new file mode 100644 index 00000000..db4561ea --- /dev/null +++ b/scalpel/src/main/resources/python3-10/qs/.gitignore @@ -0,0 +1,54 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# 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 +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/scalpel/src/main/resources/python3-10/qs/LICENSE b/scalpel/src/main/resources/python3-10/qs/LICENSE new file mode 100644 index 00000000..697fd18f --- /dev/null +++ b/scalpel/src/main/resources/python3-10/qs/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Mark Henderson + +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. + diff --git a/scalpel/src/main/resources/python3-10/qs/README.md b/scalpel/src/main/resources/python3-10/qs/README.md new file mode 100644 index 00000000..0fb0f660 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/qs/README.md @@ -0,0 +1,5 @@ +# qs.py + +This is a simple package to give Python PHP style querystring parsing. This allows for field names to have named indexes allowing for dictionaires to be passed + +https://github.com/emehrkay/qs diff --git a/scalpel/src/main/resources/python3-10/qs/__init__.py b/scalpel/src/main/resources/python3-10/qs/__init__.py new file mode 100644 index 00000000..4e78001b --- /dev/null +++ b/scalpel/src/main/resources/python3-10/qs/__init__.py @@ -0,0 +1,11 @@ +from .qs import * + +__all__ = [ + "list_to_dict", + "is_valid_php_query_name", + "merge_dict_in_list", + "merge", + "qs_parse", + "build_qs", + "qs_parse_pairs", +] diff --git a/scalpel/src/main/resources/python3-10/qs/qs.py b/scalpel/src/main/resources/python3-10/qs/qs.py new file mode 100644 index 00000000..79c64454 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/qs/qs.py @@ -0,0 +1,322 @@ +import re +from urllib.parse import quote_plus, unquote_plus +from typing import Mapping, Sequence, Any, cast +from copy import deepcopy + +# NOTE: In this code "name" corresponds to an urlencoded key and "value" to an urlencoded value +# "key" refers to the keys in a php style query name, and "field" refers to the base key in a php style query name +# Example: "[][]=&=" + + +def list_to_dict(lst: list[Any]) -> dict[int, Any]: + """Maps a list to an equivalent dictionary + + e.g: ["a","b","c"] -> {0:"a",1:"b",2:"c"} + + Used to convert lists to PHP-style arrays + + Args: + lst (list[Any]): The list to transform + + Returns: + dict[int, Any]: The "PHP-style array" dict + """ + + return {i: value for i, value in enumerate(lst)} + + +def is_valid_php_query_name(name: str) -> bool: + """ + Check if a given name follows PHP query string syntax. + This implementation assumes that names will be structured like: + field + field[key] + field[key1][key2] + field[] + """ + pattern = r""" + ^ # Asserts the start of the line, it means to start matching from the beginning of the string. + [^\[\]&]+ # Matches one or more characters that are not `[`, `]`, or `&`. It describes the base key. + ( # Opens a group. This group is used to match any subsequent keys within brackets. + \[ # Matches a literal `[`, which is the start of a key. + [^\[\]&]* # Matches zero or more characters that are not `[`, `]`, or `&`, which is the content of a key. + \] # Matches a literal `]`, which is the end of a key. + )* # Closes the group and asserts that the group can appear zero or more times, for nested keys. + $ # Asserts the end of the line, meaning the string should end with the preceding group. + """ + return bool(re.match(pattern, name, re.VERBOSE)) + + +def _get_name_value(tokens: dict, name: str, value: str, urlencoded: bool) -> None: + """ + Parses the query string, and store the key/value pairs in the `tokens` dict. + If the name doesn't follow PHP query string syntax, it treats it as a single key. + + Args: + tokens (dict): The dictionary to store the parsed key/value pairs. + name (str): The key from the query string. + value (str): The value from the query string. + urlencoded (bool): If True, decode the name and value. + """ + if urlencoded: + name = unquote_plus(name) + value = unquote_plus(value) + + # If name doesn't follow PHP query string syntax, treat it as a single key + if not is_valid_php_query_name(name): + tokens[name] = value + return + + pattern = r""" + ( # Group start + [^\[\]&]+ # One or more of any character except square brackets and the ampersand + | # Or + \[\] # Match empty square brackets + | # Or + \[ # Match an opening square bracket + [^\[\]&]* # Zero or more of any character except square brackets and the ampersand + \] # Match a closing square bracket + ) # Group end + """ + matches = re.findall(pattern, name, re.VERBOSE) + + new_value: str | list | dict = value + for i, match in enumerate(reversed(matches)): + match match: + case "[]": + if i == 0: + new_value = [new_value] + else: + new_value += new_value # type: ignore + + # Regex pattern matches a string enclosed by square brackets. The string + # may contain any character except square brackets and ampersand. + case _ if re.match(r"\[[^\[\]&]*\]", match): + # Here we're using another regular expression to remove the square + # brackets from the match, thereby extracting the name. + name = re.sub(r"[\[\]]", "", match) + new_value = {name: new_value} + + case _: # Plain field (no square brackets) + if match not in tokens: + match new_value: + case list() | tuple(): + tokens[match] = [] + case dict(): + tokens[match] = {} + + match new_value: + case _ if i == 0: + tokens[match] = new_value + case dict(): + if isinstance(tokens[match], str): + tokens[match] = [tokens[match]] + tokens[match] = merge(new_value, tokens[match]) + case list() | tuple(): + tokens[match] = tokens[match] + list(new_value) + case _: + if not isinstance(tokens[match], list): + # The key is duplicated, so we transform the first value into a list so we can append the new one + tokens[match] = [tokens[match]] + tokens[match].append(new_value) + + +def merge_dict_in_list(source: dict, destination: list) -> list | dict: + """ + Merge a dictionary into a list. + + Only the values of integer keys from the dictionary are merged into the list. + + If the dictionary contains only integer keys, returns a merged list. + If the dictionary contains other keys as well, returns a merged dict. + + Args: + source (dict): The dictionary to merge. + destination (list): The list to merge. + + Returns: + list | dict: Merged data. + """ + # Retain only integer keys: + int_keys = sorted([key for key in source.keys() if isinstance(key, int)]) + array_values = [source[key] for key in int_keys] + merged_array = array_values + destination + + if len(int_keys) == len(source.keys()): + return merged_array + + return merge(source, list_to_dict(merged_array)) + + +def merge(source: dict | list, destination: dict | list, shallow: bool = True): + """ + Merge the `source` and `destination`. + Performs a shallow or deep merge based on the `shallow` flag. + Args: + source (Any): The source data to merge. + destination (Any): The destination data to merge into. + shallow (bool): If True, perform a shallow merge. Defaults to True. + Returns: + Any: Merged data. + """ + if not shallow: + source = deepcopy(source) + destination = deepcopy(destination) + + match (source, destination): + case (list(), list()): + return source + destination + case (dict(), list()): + return merge_dict_in_list(source, destination) + + items = cast(Mapping, source).items() + for key, value in items: + if isinstance(value, dict) and isinstance(destination, dict): + # get node or create one + node = destination.setdefault(key, {}) + node = merge(value, node) + destination[key] = node + else: + if ( + isinstance(value, list) or isinstance(value, tuple) + ) and key in destination: + value = merge(destination[key], list(value)) + + if isinstance(key, str) and isinstance(destination, list): + destination = list_to_dict( + destination + ) # << WRITE TEST THAT WILL REACH THIS LINE + + cast(dict, destination)[key] = value + return destination + + +def qs_parse( + qs: str, keep_blank_values: bool = True, strict_parsing: bool = False +) -> dict: + """ + Parses a query string using PHP's nesting syntax, and returns a dict. + + Args: + qs (str): The query string to parse. + keep_blank_values (bool): If True, includes keys with blank values. Defaults to True. + strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False. + + Returns: + dict: A dictionary representing the parsed query string. + """ + + tokens = {} + pairs = [ + pair for query_segment in qs.split("&") for pair in query_segment.split(";") + ] + + for name_val in pairs: + if not name_val and not strict_parsing: + continue + nv = name_val.split("=") + + if len(nv) != 2: + if strict_parsing: + raise ValueError(f"Bad query field: {name_val}") + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append("") + else: + continue + + if len(nv[1]) or keep_blank_values: + _get_name_value(tokens, nv[0], nv[1], urlencoded=True) + + return tokens + + +def build_qs(query: Mapping) -> str: + """ + Build a query string from a dictionary or list of 2-tuples. + Coerces data types before serialization. + Args: + query (Mapping): The query data to build the string from. + Returns: + str: A query string. + """ + + def dict_generator(indict, pre=None): + pre = pre[:] if pre else [] + if isinstance(indict, dict): + for key, value in indict.items(): + if isinstance(value, dict): + for d in dict_generator(value, pre + [key]): + yield d + else: + yield pre + [key, value] + else: + yield indict + + paths = [i for i in dict_generator(query)] + qs = [] + + for path in paths: + names = path[:-1] + value = path[-1] + s: list[str] = [] + for i, n in enumerate(names): + n = f"[{n}]" if i > 0 else str(n) + s.append(n) + + match value: + case list() | tuple(): + for v in value: + multi = s[:] + if not s[-1].endswith("[]"): + multi.append("[]") + multi.append("=") + # URLEncode value + multi.append(quote_plus(str(v))) + qs.append("".join(multi)) + case _: + s.append("=") + # URLEncode value + s.append(quote_plus(str(value))) + qs.append("".join(s)) + + return "&".join(qs) + + +def qs_parse_pairs( + pairs: Sequence[tuple[str, str] | tuple[str]], + keep_blank_values: bool = True, + strict_parsing: bool = False, +) -> dict: + """ + Parses a list of key/value pairs and returns a dict. + + Args: + pairs (list[tuple[str, str]]): The list of key/value pairs. + keep_blank_values (bool): If True, includes keys with blank values. Defaults to True. + strict_parsing (bool): If True, raises ValueError on any errors. Defaults to False. + + Returns: + dict: A dictionary representing the parsed pairs. + """ + + tokens = {} + + for name_val in pairs: + if not name_val and not strict_parsing: + continue + nv = name_val + + if len(nv) != 2: + if strict_parsing: + raise ValueError(f"Bad query field: {name_val}") + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv = (nv[0], "") + else: + continue + + if len(nv[1]) or keep_blank_values: + _get_name_value(tokens, nv[0], nv[1], False) + + return tokens diff --git a/scalpel/src/main/resources/python3-10/qs/tests.py b/scalpel/src/main/resources/python3-10/qs/tests.py new file mode 100644 index 00000000..38e19e48 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/qs/tests.py @@ -0,0 +1,501 @@ +import unittest +from typing import Sequence, cast +from qs import * + + +class TestBase(unittest.TestCase): + def test_merge(self): + source = {"a": 1, "b": {"c": 2}} + destination = {"a": 3, "b": {"d": 4}} + expected = {"a": 1, "b": {"c": 2, "d": 4}} + self.assertEqual(merge(source, destination), expected) + + def test_merge_array(self): + source = {0: "nest", "key6": "deep"} + destination = ["along"] + expected = {0: "nest", "key6": "deep", 1: "along"} + self.assertEqual(merge(source, destination), expected) + + def test_qs_parse_no_strict_no_blanks(self): + qs = "a=1&b=2&c=3" + expected = {"a": "1", "b": "2", "c": "3"} + self.assertEqual(qs_parse(qs), expected) + + def test_qs_parse_with_strict(self): + qs = "a=1&b=2&c" + with self.assertRaises(ValueError): + qs_parse(qs, strict_parsing=True) + + def test_qs_parse_keep_blanks(self): + qs = "a=1&b=2&c" + expected = {"a": "1", "b": "2", "c": ""} + self.assertEqual(qs_parse(qs, keep_blank_values=True), expected) + + def test_simple_duplicates_wrong(self): + qs = "a=1&a=2&a=3&a=4" + + # Mimic PHP parse_str + expected = {"a": "4"} + self.assertEqual(qs_parse(qs), expected) + + def test_simple_duplicates_rigth(self): + qs = "a[]=1&a[]=2&a[]=3&a[]=4" + + # Mimic PHP parse_str + expected = {"a": ["1", "2", "3", "4"]} + self.assertEqual(qs_parse(qs), expected) + + def test_qs_parse_complex(self): + qs = "key1[key2][key3][key4][]=ho&key1[key2][key3][key4][]=hey&key1[key2][key3][key4][]=choco&key1[key2][key3][key4][key5][]=nest" + qs += "&key1[key2][key3][key4][key5][key6]=deep&key1[key2][key3][key4][key5][]=along&key1[key2][key3][key4][key5][key5_1]=hello" + expected = { + "key1": { + "key2": { + "key3": { + "key4": { + 0: "ho", + 1: "hey", + 2: "choco", + "key5": { + 0: "nest", + "key6": "deep", + 1: "along", + "key5_1": "hello", + }, + } + } + } + } + } + old = self.maxDiff + self.maxDiff = None + output = qs_parse(qs) + self.assertEqual(output, expected) + self.maxDiff = old + + def test_build_qs(self): + query = {"a": 1, "b": 2, "c": 3} + expected = "a=1&b=2&c=3" + self.assertEqual(build_qs(query), expected) + + def test_build_qs_nested_dict(self): + query = {"a": 1, "b": {"c": 2, "d": 3}} + expected = "a=1&b[c]=2&b[d]=3" + self.assertEqual(build_qs(query), expected) + + def test_build_qs_with_list(self): + query = {"a": 1, "b": [2, 3]} + expected = "a=1&b[]=2&b[]=3" + self.assertEqual(build_qs(query), expected) + + def test_deep_merge_non_shallow(self): + # Initial source and destination objects with nested structures + source = {"a": {"nested": [4, 5, 6]}, "b": [4, 5]} + destination = {"a": {"nested": [1, 2, 3]}, "c": [7, 8]} + + # Perform a deep merge + merged_result = merge(source, destination, shallow=False) + + # Modify the original source and destination to check if changes reflect in the merged result + source["a"]["nested"].append(99) + destination["a"]["nested"].append(100) + source["b"].append(101) + destination["c"].append(102) + + # Expected merged result should remain unchanged by modifications to source or destination + expected = {"a": {"nested": [1, 2, 3, 4, 5, 6]}, "b": [4, 5], "c": [7, 8]} + + self.assertEqual( + merged_result, + expected, + "Deep merge failed to isolate merged result from changes in source or destination", + ) + + +class TestQSParsePairs(unittest.TestCase): + def test_single_pair(self): + pairs = [("a", "1")] + expected = {"a": "1"} + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_multiple_pairs(self): + pairs = [("a", "1"), ("b", "2"), ("c", "3")] + expected = {"a": "1", "b": "2", "c": "3"} + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_duplicate_keys(self): + pairs = [("a[]", "1"), ("a[]", "2"), ("a[]", "3")] + expected = {"a": ["1", "2", "3"]} + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_blank_values(self): + pairs = [("a", ""), ("b", ""), ("c", "3")] + expected = {"a": "", "b": "", "c": "3"} + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_blank_values_ignore(self): + pairs = [("a", ""), ("b", ""), ("c", "3")] + expected = {"c": "3"} + self.assertEqual(qs_parse_pairs(pairs, keep_blank_values=False), expected) + + # def test_strict_parsing(self): + # pairs = [("a", "1"), ("b", "2"), ("c", "")] + # with self.assertRaises(ValueError): + # qs_parse_pairs(pairs, strict_parsing=True) + + def test_complex_parsing(self): + pairs = [ + ("key1[key2][key3][key4][]", "ho"), + ("key1[key2][key3][key4][]", "hey"), + ("key1[key2][key3][key4][]", "choco"), + ("key1[key2][key3][key4][key5][]", "nest"), + ("key1[key2][key3][key4][key5][key6]", "deep"), + ("key1[key2][key3][key4][key5][]", "along"), + ("key1[key2][key3][key4][key5][key5_1]", "hello"), + ] + expected = { + "key1": { + "key2": { + "key3": { + "key4": { + 0: "ho", + 1: "hey", + 2: "choco", + "key5": { + 0: "nest", + "key6": "deep", + 1: "along", + "key5_1": "hello", + }, + } + } + } + } + } + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_complex_parsing_with_file(self): + from base64 import b64decode + + zip_data = b64decode( + """UEsDBBQAAAAIAFpPvlYQIK6pcAAAACMBAAAHABwAbG9sLnBocFVUCQADu6x1ZNBwd2R1eAsAAQTo + AwAABOgDAACzsS/IKOAqSCwqTo0vLinSUM9OrTSMBhJGsSDSGEyaxEbH2mbkq+GWS63ELZmckZ+M + Uy+QNAUpykstLsGvBkiaxdqmpKYWEDIrMSc/L52gYabxhiCH5+Tkq+soqOSXlhSUlmhacxUUZeaV + xBdpIEQAUEsBAh4DFAAAAAgAWk++VhAgrqlwAAAAIwEAAAcAGAAAAAAAAQAAALSBAAAAAGxvbC5w + aHBVVAUAA7usdWR1eAsAAQToAwAABOgDAABQSwUGAAAAAAEAAQBNAAAAsQAAAAAA""" + ).decode("latin-1") + + pairs = [ + ("key1[key2][key3][key4][]", "ho"), + ("key1[key2][key3][key4][]", "hey"), + ("key1[key2][key3][key4][]", "choco"), + ("key1[key2][key3][key4][key5][]", "nest"), + ("key1[key2][key3][key4][key5][key6]", "deep"), + ("key1[key2][key3][key4][key5][]", "along"), + ("key1[key2][key3][key4][key5][key5_1]", "hello"), + ("key1[key2][key3][key4][key5][key5_file]", zip_data), + ] + expected = { + "key1": { + "key2": { + "key3": { + "key4": { + 0: "ho", + 1: "hey", + 2: "choco", + "key5": { + 0: "nest", + "key6": "deep", + 1: "along", + "key5_1": "hello", + "key5_file": zip_data, + }, + } + } + } + } + } + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_complex_parsing_weird_keys(self): + pairs = [ + ("key_with_underscore[key*with*stars][]", "ho"), + ("key-with-dash[key123][]", "hey"), + ("key[key-with-dash][]", "choco"), + ("2key[3key][4key][5key][]", "nest"), + ("2key[3key][4key][5key][6key]", "deep"), + ("key1[key2][key3][key4][key5][]", "along"), + ("key1[key2][key3][key4][key5][key5_1]", "hello"), + ] + expected = { + "key_with_underscore": { + "key*with*stars": ["ho"], + }, + "key-with-dash": { + "key123": ["hey"], + }, + "key": { + "key-with-dash": ["choco"], + }, + "2key": { + "3key": { + "4key": { + "5key": { + 0: "nest", + "6key": "deep", + }, + }, + }, + }, + "key1": { + "key2": { + "key3": { + "key4": { + "key5": { + 0: "along", + "key5_1": "hello", + }, + }, + }, + }, + }, + } + self.maxDiff = None + self.assertEqual(qs_parse_pairs(pairs), expected) + + def test_is_valid_query(self): + # Valid query names + self.assertTrue(is_valid_php_query_name("field")) + self.assertTrue(is_valid_php_query_name("field[key]")) + self.assertTrue(is_valid_php_query_name("field[key1][key2]")) + self.assertTrue(is_valid_php_query_name("field[]")) + self.assertTrue(is_valid_php_query_name("_field")) # Starts with underscore + self.assertTrue( + is_valid_php_query_name("field[key_with_underscore]") + ) # Key with underscore + self.assertTrue( + is_valid_php_query_name("field[key-with-dash]") + ) # Key with dash + self.assertTrue( + is_valid_php_query_name("field[key*with*stars]") + ) # Key with stars + self.assertTrue( + is_valid_php_query_name("field123[key456][key789]") + ) # Keys and field with digits + self.assertTrue( + is_valid_php_query_name("key1[key2][key3][key4][]") + ) # More complex and nested field + self.assertTrue(is_valid_php_query_name("key1[key2][key3][key4][key5][]")) + self.assertTrue(is_valid_php_query_name("key1[key2][key3][key4][key5][key6]")) + self.assertTrue(is_valid_php_query_name("key1[key2][key3][key4][key5][key5_1]")) + self.assertTrue( + is_valid_php_query_name("field[ ]") + ) # Brackets can contain spaces + self.assertTrue( + is_valid_php_query_name("2field[key]") + ) # Name can start with a number + + # Invalid query names + self.assertFalse( + is_valid_php_query_name("a[x]b[y]c[z]") + ) # Can't have anything between brackets + + self.assertFalse( + is_valid_php_query_name("[key]") + ) # Name must not start with a bracket + + self.assertFalse( + is_valid_php_query_name("field[key][") + ) # Empty brackets at the end + self.assertFalse(is_valid_php_query_name("field[")) # Incomplete bracket + self.assertFalse(is_valid_php_query_name("")) # Empty string + self.assertFalse( + is_valid_php_query_name("field[key&]") + ) # Special character & in the key + self.assertFalse( + is_valid_php_query_name("field[key&key]") + ) # Special character & in the key + + def test_handling_duplicated_keys_with_mixed_syntax(self): + pairs = [ + ("key", "lol"), + ("key[subkey][]", "initialValue"), + ("key[subkey][]", "newValue"), + ] + + expected = {"key": {0: "lol", "subkey": ["initialValue", "newValue"]}} + + result = qs_parse_pairs(pairs, keep_blank_values=True, strict_parsing=False) + + self.assertDictEqual( + result, + expected, + "Failed to properly handle duplicated keys by converting to list and appending new value", + ) + + +class TestSimple(unittest.TestCase): + + def test_empty_query_element_strict_parsing_false(self): + # Test for an empty element between valid elements + qs = "a=1&&b=2" # Note the double ampersand + expected = {"a": "1", "b": "2"} + result = qs_parse(qs, keep_blank_values=True, strict_parsing=False) + self.assertEqual( + result, expected, "Failed to skip an empty query string element" + ) + + def test_malformed_query_strict_parsing_true_raises_error(self): + # Test for a malformed element with strict parsing enabled + qs = "a=1&malformed&b=2" + with self.assertRaises( + ValueError, + msg="Did not raise ValueError for a malformed query string element with strict parsing", + ): + qs_parse(qs, strict_parsing=True) + + def test_malformed_query_keep_blank_values_true(self): + # Test for a malformed element with keep_blank_values=True + qs = "a=1&malformed&b=2" + expected = {"a": "1", "malformed": "", "b": "2"} + result = qs_parse(qs, keep_blank_values=True, strict_parsing=False) + self.assertEqual( + result, + expected, + "Failed to append an empty string for a malformed query string element with keep_blank_values=True", + ) + + def test_malformed_query_keep_blank_values_false(self): + # Test for a malformed element with keep_blank_values=False + qs = "a=1&malformed&b=2" + expected = {"a": "1", "b": "2"} + result = qs_parse(qs, keep_blank_values=False, strict_parsing=False) + self.assertEqual( + result, + expected, + "Failed to skip a malformed query string element with keep_blank_values=False", + ) + + def test_empty_pair_strict_parsing_false(self): + # Test with an empty tuple in the pairs list + pairs = [("a", "1"), (), ("b", "2")] + expected = {"a": "1", "b": "2"} + result = qs_parse_pairs(pairs, keep_blank_values=True, strict_parsing=False) + self.assertEqual(result, expected, "Failed to skip an empty pair") + + def test_malformed_pair_strict_parsing_true_raises_error(self): + # Test a malformed pair with strict parsing + pairs = [("a", "1"), ("malformed",), ("b", "2")] + with self.assertRaises( + ValueError, + msg="Did not raise ValueError for a malformed pair with strict parsing", + ): + qs_parse_pairs(pairs, strict_parsing=True) + + def test_malformed_pair_keep_blank_values_true(self): + # Test a malformed pair with keep_blank_values=True + pairs = [("a", "1"), ("malformed",), ("b", "2")] + expected = {"a": "1", "malformed": "", "b": "2"} + result = qs_parse_pairs(pairs, keep_blank_values=True, strict_parsing=False) + self.assertEqual( + result, + expected, + "Failed to append an empty string for a malformed pair with keep_blank_values=True", + ) + + def test_malformed_pair_keep_blank_values_false(self): + # Test a malformed pair with keep_blank_values=False + pairs = [("a", "1"), ("malformed",), ("b", "2")] + expected = {"a": "1", "b": "2"} + result = qs_parse_pairs(pairs, keep_blank_values=False, strict_parsing=False) + self.assertEqual( + result, + expected, + "Failed to skip a malformed pair with keep_blank_values=False", + ) + + def test_invalid_php_query_name_treated_as_single_key(self): + # Names that don't follow PHP query string syntax + invalid_names = [ + "[invalid]", # Starts with a bracket + "invalid[]name", # Square brackets in the middle of the name + "invalid&name", # Contains an ampersand + "invalid[name", # Missing closing bracket + "name]invalid", # Starts with closing bracket + ] + + for name in invalid_names: + with self.subTest(name=name): + # Ensuring the name is indeed considered invalid + self.assertFalse( + is_valid_php_query_name(name), + f"{name} unexpectedly passed as a valid PHP query name", + ) + + # The actual test case + pairs = [(name, "value")] + expected = {name: "value"} + result = qs_parse_pairs(pairs) + self.assertEqual(result, expected, f"Failed for name: {name}") + + +class TestMergeDictInList(unittest.TestCase): + def test_merge_dict_with_only_integer_keys_into_list(self): + # A dictionary with only integer keys + source = {0: "apple", 1: "banana", 2: "cherry"} + # A list to merge into + destination = ["date", "elderberry", "fig"] + # Expected result: the values from the source are prepended to the destination list + expected = ["apple", "banana", "cherry", "date", "elderberry", "fig"] + + result = merge_dict_in_list(source, destination) + + self.assertEqual( + result, + expected, + "Failed to merge a dictionary with only integer keys into a list correctly", + ) + + def test_merge_dict_with_mixed_keys_into_list(self): + # Dictionary with a mix of integer and non-integer keys + source = {0: "apple", 1: "banana", "extra": "grape"} + # A list to merge into + destination = ["date", "elderberry", "fig"] + # Expected result: a dictionary since source contains non-integer keys as well + expected = { + 0: "apple", + 1: "banana", + 2: "date", + 3: "elderberry", + 4: "fig", + "extra": "grape", + } + + result = merge_dict_in_list(source, destination) + + self.assertTrue( + isinstance(result, dict), + "Expected a dictionary when source contains non-integer keys", + ) + self.assertEqual( + result, + expected, + "Failed to merge a dictionary with mixed keys into a list correctly", + ) + + def test_merge_transform_list_dest_to_dict(self): + dest = {"abc": ["def", "ghi"]} + source = {"abc": {"hello": "apple"}} + + expected = {"abc": {"hello": "apple", 0: "def", 1: "ghi"}} + result = merge(source, dest) + self.assertIsInstance(result, dict) + self.assertDictEqual( + cast(dict, result), + expected, + "Failed to merge a dictionary with a list correctly", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/scalpel/src/main/resources/python3-10/samples/base64-body.py b/scalpel/src/main/resources/python3-10/samples/base64-body.py new file mode 100644 index 00000000..6bf63f5e --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/base64-body.py @@ -0,0 +1,84 @@ +""" + Base64 Encoding and Decoding + + This script is designed to handle incoming and outgoing HTTP requests/responses + that may contain base64-encoded data in their contents. + + It first tries to decode the contents using base64. If the content is not base64-encoded + (i.e., if a binascii.Error exception is thrown), it leaves the content as it is. + + When the request/response is sent back out, it encodes the content using base64. +""" + +from base64 import b64decode, b64encode +import binascii +from pyscalpel import Request, Response + + +def req_edit_in(req: Request) -> bytes: + """ + Tries to decode the content of the incoming HTTP request using base64. + + Args: + req: The incoming HTTP request. + + Returns: + The base64-decoded content of the HTTP request, or the original content if it wasn't base64-encoded. + """ + if req.content: + try: + req.content = b64decode(req.content, validate=True) + except binascii.Error: + pass + + return req.content or b"" + + +def req_edit_out(req: Request, text: bytes) -> Request: + """ + Encodes the content of the outgoing HTTP request using base64. + + Args: + req: The outgoing HTTP request. + text: The content to be base64-encoded. + + Returns: + The HTTP request with the base64-encoded content. + """ + if req.content: + req.content = b64encode(text) + return req + + +def res_edit_in(res: Response) -> bytes: + """ + Tries to decode the content of the incoming HTTP response using base64. + + Args: + res: The incoming HTTP response. + + Returns: + The base64-decoded content of the HTTP response, or the original content if it wasn't base64-encoded. + """ + if res.content: + try: + res.content = b64decode(res.content, validate=True) + except binascii.Error: + pass + return bytes(res) + + +def res_edit_out(res: Response, text: bytes) -> Response: + """ + Encodes the content of the outgoing HTTP response using base64. + + Args: + res: The outgoing HTTP response. + text: The content to be base64-encoded. + + Returns: + The HTTP response with the base64-encoded content. + """ + if res.content: + res.content = b64encode(text) + return res diff --git a/scalpel/src/main/resources/python3-10/samples/collaborator-java.py b/scalpel/src/main/resources/python3-10/samples/collaborator-java.py new file mode 100644 index 00000000..32aa5359 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/collaborator-java.py @@ -0,0 +1,31 @@ +""" + Host Header Spoofing for SSRF Detection + + This script demonstrates the ability to spoof the Host header in HTTP requests. + The purpose is to detect out-of-band interactions and Server-Side Request Forgery (SSRF) vulnerabilities. + + The Host header is set to a payload generated by Burp Suite's Collaborator tool. + This payload is a unique domain name that Burp Suite listens for DNS lookups and HTTP requests on. + + It showcases the capability to use Burp Suite's API object in Python seamlessly, via Java Embedded Python (JEP), + which allows Python scripts to interact with Java objects as if they were Python objects. +""" + +from pyscalpel import Request, ctx + +# Directly access the Montoya API Java object to generate a payload +PAYLOAD = str(ctx["API"].collaborator().defaultPayloadGenerator().generatePayload()) + + +def request(req: Request) -> Request | None: + """ + Modifies the Host header in the HTTP request to the generated payload. + + Args: + req: The incoming HTTP request. + + Returns: + The modified HTTP request with the spoofed Host header. + """ + req.host_header = PAYLOAD + return req diff --git a/scalpel/src/main/resources/python3-10/samples/crypto-stateful.py b/scalpel/src/main/resources/python3-10/samples/crypto-stateful.py new file mode 100644 index 00000000..da93751a --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/crypto-stateful.py @@ -0,0 +1,172 @@ +""" + AES Encryption and Decryption with Session + + Requires pycryptodome: + $ pip install pycrytodome + + This script demonstrates the use of AES encryption and decryption in HTTP requests and responses, with the preservation of session state. + The session state is maintained across calls and is not reset, demonstrating the capability of preserving global state. + It uses the pycryptodome library to perform AES encryption and decryption, and SHA256 for key derivation. + The script acts on paths starting with "/encrypt-session" and where the request method is not POST, or where a session is already established. +""" + +from pyscalpel import Request, Response, Flow +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Util.Padding import pad, unpad +from base64 import b64encode, b64decode + + +session: bytes = b"" # Global variable for session preservation + + +def match(flow: Flow) -> bool: + """ + Matches if the request path starts with '/encrypt-session' and the session is set or the request method is not 'POST'. + + Args: + flow: The flow object representing the HTTP transaction. + + Returns: + True if the request path and request method match the conditions, otherwise False. + """ + return flow.path_is("/encrypt-session*") and bool( + session or flow.request.method != "POST" + ) + + +def get_cipher(secret: bytes, iv=bytes(16)): + """ + Constructs an AES cipher object using a derived AES key from the provided secret and an initialization vector. + + Args: + secret: The secret to derive the AES key from. + iv: The initialization vector. + + Returns: + The AES cipher object. + """ + hasher = SHA256.new() + hasher.update(secret) + derived_aes_key = hasher.digest()[:32] + cipher = AES.new(derived_aes_key, AES.MODE_CBC, iv) + return cipher + + +def decrypt(secret: bytes, data: bytes) -> bytes: + """ + Decrypts the base64-encoded, AES-encrypted data using the provided secret. + + Args: + secret: The secret to use for decryption. + data: The base64-encoded, AES-encrypted data. + + Returns: + The decrypted data. + """ + data = b64decode(data) + cipher = get_cipher(secret) + decrypted = cipher.decrypt(data) + return unpad(decrypted, AES.block_size) + + +def encrypt(secret: bytes, data: bytes) -> bytes: + """ + Encrypts the data using the provided secret and AES encryption, and then base64-encodes it. + + Args: + secret: The secret to use for encryption. + data: The data to encrypt. + + Returns: + The base64-encoded, AES-encrypted data. + """ + cipher = get_cipher(secret) + padded_data = pad(data, AES.block_size) + encrypted = cipher.encrypt(padded_data) + return b64encode(encrypted) + + +def response(res: Response) -> Response | None: + """ + If the request method is 'GET', sets the global session variable to the response's content. + + Args: + res: The incoming HTTP response. + + Returns: + None. + """ + if res.request.method == "GET": + global session + session = res.content or b"" + return + + +def req_edit_in_encrypted(req: Request) -> bytes: + """ + Decrypts the 'encrypted' field in the incoming HTTP request's form using the session. + + Args: + req: The incoming HTTP request. + + Returns: + The decrypted content of the 'encrypted' field. + """ + secret = session + encrypted = req.form[b"encrypted"] + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + +def req_edit_out_encrypted(req: Request, text: bytes) -> Request: + """ + Encrypts the provided text using the session, and sets it as the 'encrypted' field in the outgoing HTTP request's form. + + Args: + req: The outgoing HTTP request. + text: The text to encrypt. + + Returns: + The modified HTTP request with the encrypted text. + """ + secret = session + req.form[b"encrypted"] = encrypt(secret, text) + return req + + +def res_edit_in_encrypted(res: Response) -> bytes: + """ + Decrypts the incoming HTTP response's content using the session. + + Args: + res: The incoming HTTP response. + + Returns: + The decrypted content. + """ + secret = session + encrypted = res.content + + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + +def res_edit_out_encrypted(res: Response, text: bytes) -> Response: + """ + Encrypts the provided text using the session, and sets it as the outgoing HTTP response's content. + + Args: + res: The outgoing HTTP response. + text: The text to encrypt. + + Returns: + The modified HTTP response with the encrypted text. + """ + secret = session + res.content = encrypt(secret, text) + return res diff --git a/scalpel/src/main/resources/python3-10/samples/crypto.py b/scalpel/src/main/resources/python3-10/samples/crypto.py new file mode 100644 index 00000000..a50fcb1e --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/crypto.py @@ -0,0 +1,151 @@ +""" + AES Encryption and Decryption + + Requires pycryptodome: + $ pip install pycrytodome + + This script demonstrates the use of AES encryption and decryption in HTTP requests and responses. + It uses the pycryptodome library to perform AES encryption and decryption, and SHA256 for key derivation. + The script acts on paths matching "/encrypt" and where the request form has a "secret" field. + +""" + +from pyscalpel import Request, Response, Flow +from Crypto.Cipher import AES +from Crypto.Hash import SHA256 +from Crypto.Util.Padding import pad, unpad +from base64 import b64encode, b64decode + + +def match(flow: Flow) -> bool: + """ + Matches if the request path is '/encrypt' and if 'secret' is in the form of the request. + + Args: + flow: The flow object representing the HTTP transaction. + + Returns: + True if the request path matches and 'secret' is present, otherwise False. + """ + return flow.path_is("/encrypt") and flow.request.form.get(b"secret") is not None + + +def get_cipher(secret: bytes, iv=bytes(16)): + """ + Constructs an AES cipher object using a derived AES key from the provided secret and an initialization vector. + + Args: + secret: The secret to derive the AES key from. + iv: The initialization vector. + + Returns: + The AES cipher object. + """ + hasher = SHA256.new() + hasher.update(secret) + derived_aes_key = hasher.digest()[:32] + cipher = AES.new(derived_aes_key, AES.MODE_CBC, iv) + return cipher + + +def decrypt(secret: bytes, data: bytes) -> bytes: + """ + Decrypts the base64-encoded, AES-encrypted data using the provided secret. + + Args: + secret: The secret to use for decryption. + data: The base64-encoded, AES-encrypted data. + + Returns: + The decrypted data. + """ + data = b64decode(data) + cipher = get_cipher(secret) + decrypted = cipher.decrypt(data) + return unpad(decrypted, AES.block_size) + + +def encrypt(secret: bytes, data: bytes) -> bytes: + """ + Encrypts the data using the provided secret and AES encryption, and then base64-encodes it. + + Args: + secret: The secret to use for encryption. + data: The data to encrypt. + + Returns: + The base64-encoded, AES-encrypted data. + """ + cipher = get_cipher(secret) + padded_data = pad(data, AES.block_size) + encrypted = cipher.encrypt(padded_data) + return b64encode(encrypted) + + +def req_edit_in_encrypted(req: Request) -> bytes | None: + """ + Decrypts the 'encrypted' field in the incoming HTTP request's form using the 'secret' field. + + Args: + req: The incoming HTTP request. + + Returns: + The decrypted content of the 'encrypted' field. + """ + secret = req.form[b"secret"] + encrypted = req.form[b"encrypted"] + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + +def req_edit_out_encrypted(req: Request, text: bytes) -> Request: + """ + Encrypts the provided text using the 'secret' field in the outgoing HTTP request's form, and sets it as the 'encrypted' field. + + Args: + req: The outgoing HTTP request. + text: The text to encrypt. + + Returns: + The modified HTTP request with the encrypted text. + """ + secret = req.form[b"secret"] + req.form[b"encrypted"] = encrypt(secret, text) + return req + + +def res_edit_in_encrypted(res: Response) -> bytes | None: + """ + Decrypts the incoming HTTP response's content using the 'secret' field in the associated request's form. + + Args: + res: The incoming HTTP response. + + Returns: + The decrypted content. + """ + secret = res.request.form[b"secret"] + encrypted = res.content + + if not encrypted: + return b"" + + return decrypt(secret, encrypted) + + +def res_edit_out_encrypted(res: Response, text: bytes) -> Response: + """ + Encrypts the provided text using the 'secret' field in the associated request's form, and sets it as the outgoing HTTP response's content. + + Args: + res: The outgoing HTTP response. + text: The text to encrypt. + + Returns: + The modified HTTP response with the encrypted text. + """ + secret = res.request.form[b"secret"] + res.content = encrypt(secret, text) + return res diff --git a/scalpel/src/main/resources/python3-10/samples/default.py b/scalpel/src/main/resources/python3-10/samples/default.py new file mode 100644 index 00000000..1205d1d9 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/default.py @@ -0,0 +1,96 @@ +""" + Default script + + This script demonstrates basic usage of Scalpel by adding debug headers to HTTP requests and responses. + It shows how to intercept and modify HTTP traffic using custom headers, and how to create editors using req_edit_* / res_edit_* hooks. +""" + +from pyscalpel import Request, Response + + +def request(req: Request) -> Request | None: + """ + Adds a debug header to every outgoing HTTP request. + + Args: + req: The outgoing HTTP request. + + Returns: + The modified HTTP request with the debug header. + """ + req.headers["X-Python-Intercept-Request"] = "request" + return req + + +def response(res: Response) -> Response | None: + """ + Adds a debug header to every incoming HTTP response. + + Args: + res: The incoming HTTP response. + + Returns: + The modified HTTP response with the debug header. + """ + res.headers["X-Python-Intercept-Response"] = "response" + return res + + +def req_edit_in(req: Request) -> bytes | None: + """ + Converts a request to the text to display in the editor and adds a debug header. + + Args: + req: The incoming HTTP request. + + Returns: + The modified request as bytes with the debug header. + """ + req.headers["X-Python-In-Request-Editor"] = "req_edit_in" + return bytes(req) + + +def req_edit_out(_: Request, text: bytes) -> Request | None: + """ + Converts the modified editor text back to a request and adds a debug header. + + Args: + _: The original HTTP request (unused). + text: The request content as bytes. + + Returns: + The modified HTTP request with the debug header. + """ + req = Request.from_raw(text) + req.headers["X-Python-Out-Request-Editor"] = "req_edit_out" + return req + + +def res_edit_in(res: Response) -> bytes | None: + """ + Converts a response to the text to display in the editor and adds a debug header. + + Args: + res: The incoming HTTP response. + + Returns: + The modified response as bytes with the debug header. + """ + res.headers["X-Python-In-Response-Editor"] = "res_edit_in" + return bytes(res) + + +def res_edit_out(_: Response, text: bytes) -> Response | None: + """ + Converts the modified editor text back to a response and adds a debug header. + + Args: + _: The original HTTP response (unused). + text: The response content as bytes. + + Returns: + The modified HTTP response with the debug header. + """ + res = Response.from_raw(text) + res.headers["X-Python-Out-Response-Editor"] = "res_edit_out" + return res diff --git a/scalpel/src/main/resources/python3-10/samples/get-to-post.py b/scalpel/src/main/resources/python3-10/samples/get-to-post.py new file mode 100644 index 00000000..cb55549b --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/get-to-post.py @@ -0,0 +1,41 @@ +""" + Request Method Modification Script + + This script demonstrates modifying an incoming HTTP GET request to a POST request. + It changes the content type to 'application/x-www-form-urlencoded' and transfers the + query parameters to the URL-encoded form body. + + This is similar to the "Change request method" feature in Burp Suite. + + This is useful for testing purposes or scenarios where the server needs to handle the request differently. +""" + +from typing import Optional +from pyscalpel import Request +from pyscalpel.http.body import URLEncodedForm + + +def request(req: Request) -> Optional[Request]: + """ + Modifies an incoming GET request to a POST request and changes its content type. + + If the request method is GET, this function prints the request method and path, + then changes the request method to POST, sets the content type to 'application/x-www-form-urlencoded', + transfers the query parameters to the URL-encoded form body, and clears the query parameters. + + Args: + req: The incoming HTTP request. + + Returns: + The modified HTTP request if the method was GET, otherwise None. + """ + print(req.method) + if req.method == "GET": + print(f"GET request to {req.path}") + print("Changing request method") + params = req.query + req.method = "POST" + req.content_type = "application/x-www-form-urlencoded" + req.urlencoded_form = URLEncodedForm(params.items()) + req.query.clear() + return req diff --git a/scalpel/src/main/resources/python3-10/samples/gzip-multipart.py b/scalpel/src/main/resources/python3-10/samples/gzip-multipart.py new file mode 100644 index 00000000..4360da24 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/gzip-multipart.py @@ -0,0 +1,112 @@ +""" + GZIP Decompression and Re-Encoding + + This script interacts with an API that gzip compresses its request and response contents. + + The target this script was made for encodes utf-16le documents and compress them with GZIP before transmitting it through it's HTTP API. + + This script decompresses the gzip encoded content, decodes the utf-16le encoded text, and + re-encodes it in latin-1 to get rid of additional zero bytes that would be invisible + in plain text and would interfere with editing the plaintext. +""" + +import gzip +from pyscalpel import Request, Response + + +def req_edit_in_fs(req: Request) -> bytes | None: + """ + Decompresses the gzip content and re-encodes from utf-16le to latin-1. + + Args: + req: The incoming HTTP request. + + Returns: + The decompressed and re-encoded content of the HTTP request. + """ + gz = req.multipart_form["fs"].content + content = gzip.decompress(gz).decode("utf-16le").encode("latin-1") + return content + + +def req_edit_out_fs(req: Request, text: bytes) -> Request | None: + """ + Encodes the content from latin-1 to utf-16le and compresses it with gzip. + + Args: + req: The outgoing HTTP request. + text: The content to be re-encoded and compressed. + + Returns: + The HTTP request with the re-encoded and compressed content. + """ + data = text.decode("latin-1").encode("utf-16le") + content = gzip.compress(data, mtime=0) + req.multipart_form["fs"].content = content + return req + + +def req_edit_in_filetosend(req: Request) -> bytes | None: + """ + Decompresses the gzip content. + + Args: + req: The incoming HTTP request. + + Returns: + The decompressed content of the HTTP request. + """ + gz = req.multipart_form["filetosend"].content + content = gzip.decompress(gz) + return content + + +def req_edit_out_filetosend(req: Request, text: bytes) -> Request | None: + """ + Compresses the content with gzip. + + Args: + req: The outgoing HTTP request. + text: The content to be compressed. + + Returns: + The HTTP request with the compressed content. + """ + data = text + content = gzip.compress(data, mtime=0) + req.multipart_form["filetosend"].content = content + return req + + +def res_edit_in(res: Response) -> bytes | None: + """ + Decompresses the gzip content, decodes from utf-16le to utf-8. + + Args: + res: The incoming HTTP response. + + Returns: + The decompressed and re-encoded content of the HTTP response. + """ + gz = res.content + if not gz: + return + + content = gzip.decompress(gz) + content.decode("utf-16le").encode("utf-8") + return content + + +def res_edit_out(res: Response, text: bytes) -> Response | None: + """ + Replaces the content of the HTTP response with the input text. + + Args: + res: The outgoing HTTP response. + text: The text to be set as the new content of the response. + + Returns: + The HTTP response with the new content. + """ + res.content = text + return res diff --git a/scalpel/src/main/resources/python3-10/samples/match-host.py b/scalpel/src/main/resources/python3-10/samples/match-host.py new file mode 100644 index 00000000..bbd5fb99 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/match-host.py @@ -0,0 +1,115 @@ +""" + Host Matching + + This example demonstrates the matching feature. + + It can be used to understand how to create matching rules to filter unwanted host from being processed. + Here, we create a match hook which will only intercept requests to *.localhost and 127.0.0.1 +""" + +from pyscalpel import Request, Response, Flow + + +def match(flow: Flow) -> bool: + """ + Matches the host of the HTTP request with the specified patterns. + Will be called before calling other hooks to decide whether to ignore the event or not. + + Args: + flow: The flow object representing the HTTP transaction. + + Returns: + True if the host of the HTTP request matches either of the specified patterns, otherwise False. + """ + return flow.host_is("*localhost", "127.0.0.1") + + +#### All of the hooks below will only be called if match() returns True #### + + +def request(req: Request) -> Request | None: + """ + Intercepts and modifies the incoming HTTP request by adding a custom header. + + Args: + req: The incoming HTTP request. + + Returns: + The modified HTTP request. + """ + req.headers["X-Python-Intercept-Request"] = "request" + return req + + +def response(res: Response) -> Response | None: + """ + Intercepts and modifies the incoming HTTP response by adding a custom header. + + Args: + res: The incoming HTTP response. + + Returns: + The modified HTTP response. + """ + res.headers["X-Python-Intercept-Response"] = "response" + return res + + +def req_edit_in(req: Request) -> bytes | None: + """ + Modifies the incoming HTTP request before it is sent to the editor by adding a custom header. + + Args: + req: The incoming HTTP request. + + Returns: + The modified HTTP request as bytes. + """ + req.headers["X-Python-In-Request-Editor"] = "req_edit_in" + return bytes(req) + + +def req_edit_out(_: Request, text: bytes) -> Request | None: + """ + Modifies the outgoing HTTP request after it has been edited by adding a custom header. + + Args: + _: The original HTTP request (ignored). + text: The edited HTTP request as bytes. + + Returns: + The modified HTTP request. + """ + req = Request.from_raw(text) + req.headers["X-Python-Out-Request-Editor"] = "req_edit_out" + return req + + +def res_edit_in(res: Response) -> bytes | None: + """ + Modifies the incoming HTTP response before it is sent to the editor by adding a custom header. + + Args: + res: The incoming HTTP response. + + Returns: + The modified HTTP response as bytes. + """ + res.headers["X-Python-In-Response-Editor"] = "res_edit_in" + return bytes(res) + + +def res_edit_out(_: Response, text: bytes) -> Response | None: + """ + Modifies the outgoing HTTP response after it has been edited by adding a custom header. + + Args: + _: The original HTTP response (ignored). + text: The edited HTTP response as bytes. + + Returns: + The modified HTTP response. + """ + res = Response.from_raw(text) + res.headers["X-Python-Out-Response-Editor"] = "res_edit_out" + return res diff --git a/scalpel/src/main/resources/python3-10/samples/multiple_tabs.py b/scalpel/src/main/resources/python3-10/samples/multiple_tabs.py new file mode 100644 index 00000000..0bc5c304 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/multiple_tabs.py @@ -0,0 +1,100 @@ +""" + Parameter Manipulation + + This example provides utility functions to manipulate HTTP request query parameters. + It decodes and encodes parameter values, and gets and sets values for parameters with names derived from a "tab name". + This is useful in HTTP request manipulation in applications such as proxies, web scrapers, and web services. +""" + +from pyscalpel import Request +from pyscalpel.utils import ( + urldecode, + urlencode_all, + get_tab_name, +) + + +def get_and_decode_param(req: Request, param: str) -> bytes | None: + """ + Retrieves the value of the specified query parameter from the given request, and URL-decodes it. + + Args: + req: The request from which to retrieve the query parameter. + param: The name of the query parameter to retrieve and decode. + + Returns: + The URL-decoded value of the query parameter, or None if the parameter is not found. + """ + found = req.query.get(param) + if found is not None: + return urldecode(found) + + +def set_and_encode_param(req: Request, param: str, param_value: bytes) -> Request: + """ + URL-encodes the given value and sets it as the value of the specified query parameter in the given request. + + Args: + req: The request in which to set the query parameter. + param: The name of the query parameter to set. + param_value: The value to URL-encode and set for the query parameter. + + Returns: + The updated request with the encoded query parameter set. + """ + req.query[param] = urlencode_all(param_value) + return req + + +def req_edit_in_filename(req: Request) -> bytes | None: + """ + Retrieves the filename from the request's query parameters. + + Args: + req: The request from which to retrieve the filename. + + Returns: + The URL-decoded filename, or None if the parameter is not found. + """ + return get_and_decode_param(req, get_tab_name()) + + +def req_edit_out_filename(req: Request, text: bytes) -> Request | None: + """ + Sets the filename in the request's query parameters. + + Args: + req: The request in which to set the filename. + text: The filename to URL-encode and set. + + Returns: + The updated request with the encoded filename set. + """ + return set_and_encode_param(req, get_tab_name(), text) + + +def req_edit_in_directory(req: Request) -> bytes | None: + """ + Retrieves the directory from the request's query parameters. + + Args: + req: The request from which to retrieve the directory. + + Returns: + The URL-decoded directory, or None if the parameter is not found. + """ + return get_and_decode_param(req, get_tab_name()) + + +def req_edit_out_directory(req: Request, text: bytes) -> Request | None: + """ + Sets the directory in the request's query parameters. + + Args: + req: The request in which to set the directory. + text: The directory to URL-encode and set. + + Returns: + The updated request with the encoded directory set. + """ + return set_and_encode_param(req, get_tab_name(), text) diff --git a/scalpel/src/main/resources/python3-10/samples/path-traversal.py b/scalpel/src/main/resources/python3-10/samples/path-traversal.py new file mode 100644 index 00000000..48f918af --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/path-traversal.py @@ -0,0 +1,50 @@ +""" + Path Traversal Exploitation + + This is a proof-of-concept script that demonstrates the exploitation of path traversal vulnerabilities. + This script specifically targets systems that attempt to resolve the path traversal issue by stripping sequences non-recursively. + It exploits this vulnerability by prepending a long sequence of traversal strings to the file path, hoping to bypass the system's security checks. + It can be used for the following Portswigger lab: https://portswigger.net/web-security/file-path-traversal/lab-sequences-stripped-non-recursively +""" + +from pyscalpel import Request +from pyscalpel.utils import urldecode, urlencode_all, removeprefix + +# The query parameter to target for path traversal exploitation. +PARAM_NAME = "filename" + +# The traversal string sequence used to exploit the vulnerability. +PREFIX = b"....//" * 500 + + +def req_edit_in(req: Request) -> bytes | None: + """ + Decodes and strips the traversal string sequence from the file path in the incoming HTTP request. + + Args: + req: The incoming HTTP request. + + Returns: + The decoded file path stripped of the traversal string sequence, or None if the query parameter is not found. + """ + param = req.query[PARAM_NAME] + if param is not None: + text_bytes = param.encode() + return removeprefix(urldecode(text_bytes), PREFIX) + + +def req_edit_out(req: Request, text: bytes) -> Request | None: + """ + Prepends the traversal string sequence to the file path and encodes it, then sets it as the value of the target query parameter in the outgoing HTTP request. + + Args: + req: The original HTTP request. + text: The file path to modify. + + Returns: + The modified HTTP request with the encoded file path. + """ + encoded = urlencode_all(PREFIX + text) + str_encoded = str(encoded, "ascii") + req.query[PARAM_NAME] = str_encoded + return req diff --git a/scalpel/src/main/resources/python3-10/samples/send-to-repeater.py b/scalpel/src/main/resources/python3-10/samples/send-to-repeater.py new file mode 100644 index 00000000..440749d8 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/send-to-repeater.py @@ -0,0 +1,33 @@ +""" + Script for Automated Sending to Burp Repeater + + This script sends HTTP requests containing a specific parameter to the Repeater tool in Burp Suite. + The parameter it checks for is 'cmd', but this can be modified as needed. + The script also keeps track of previously seen 'cmd' values to avoid resending the same request. +""" + +from pyscalpel import Request +from pyscalpel.burp import send_to_repeater + +# A set is used to store the values of the 'cmd' parameter that have already been encountered. +# The use of a set ensures that each value is stored only once, avoiding duplicate requests being sent to Repeater. +seen = set() + + +def request(req: Request) -> None: + """ + If the 'cmd' parameter is present in the request and its value hasn't been seen before, the request is sent to Repeater. + + Args: + req: The incoming HTTP request. + + Returns: + None + """ + cmd = req.query.get("cmd") + if cmd is not None and cmd not in seen: + # If the 'cmd' parameter is present and its value is new, add the value to the 'seen' set. + seen.add(cmd) + + # Send the request to Repeater with a caption indicating the value of the 'cmd' parameter. + send_to_repeater(req, f"cmd={cmd}") diff --git a/scalpel/src/main/resources/python3-10/samples/urlencoded-param.py b/scalpel/src/main/resources/python3-10/samples/urlencoded-param.py new file mode 100644 index 00000000..049497a2 --- /dev/null +++ b/scalpel/src/main/resources/python3-10/samples/urlencoded-param.py @@ -0,0 +1,70 @@ +""" + URL Decoding and Encoding for Parameters + + This script interacts with HTTP requests that contain urlencoded parameters. + It is useful when you need to view or modify urlencoded parameters in a more readable format. + The script provides functions to decode and encode the 'filename' and 'directory' query parameters. +""" + +from pyscalpel import Request +from pyscalpel.utils import urldecode, urlencode_all + + +def req_edit_in_filename(req: Request) -> bytes | None: + """ + URL decodes the 'filename' parameter from the request. + + Args: + req: The incoming HTTP request. + + Returns: + The decoded 'filename' parameter. + """ + param = req.query.get("filename") + if param is not None: + return urldecode(param) + + +def req_edit_out_filename(req: Request, text: bytes) -> Request | None: + """ + URL encodes the 'filename' parameter for the outgoing request. + + Args: + req: The outgoing HTTP request. + text: The value to be URL encoded and set as the 'filename' parameter. + + Returns: + The HTTP request with the newly URL encoded 'filename' parameter. + """ + req.query["filename"] = urlencode_all(text) + return req + + +def req_edit_in_directory(req: Request) -> bytes | None: + """ + URL decodes the 'directory' parameter from the request. + + Args: + req: The incoming HTTP request. + + Returns: + The decoded 'directory' parameter. + """ + param = req.query.get("directory") + if param is not None: + return urldecode(param) + + +def req_edit_out_directory(req: Request, text: bytes) -> Request | None: + """ + URL encodes the 'directory' parameter for the outgoing request. + + Args: + req: The outgoing HTTP request. + text: The value to be URL encoded and set as the 'directory' parameter. + + Returns: + The HTTP request with the newly URL encoded 'directory' parameter. + """ + req.query["directory"] = urlencode_all(text) + return req diff --git a/scalpel/src/main/resources/shell/init-venv.sh b/scalpel/src/main/resources/shell/init-venv.sh new file mode 100644 index 00000000..ad4f4c87 --- /dev/null +++ b/scalpel/src/main/resources/shell/init-venv.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +load_src() { + source /etc/bashrc.bashrc + source ~/.bashrc + source /etc/profile + source ~/.bash_profile + source ~/.profile + source "$1" +} + +load_src "$SCALPEL_VENV_ACTIVATE" 2>/dev/null diff --git a/scalpel/src/main/resources/templates/default.py b/scalpel/src/main/resources/templates/default.py new file mode 100644 index 00000000..fc37a363 --- /dev/null +++ b/scalpel/src/main/resources/templates/default.py @@ -0,0 +1,30 @@ +from typing import Optional +from pyscalpel import Request, Response, Flow + + +# def match(flow: Flow) -> bool: +# ... + + +# def request(req: Request) -> Optional[Request]: +# ... + + +# def response(res: Response) -> Optional[Response]: +# ... + + +# def req_edit_in(req: Request) -> Optional[bytes]: +# ... + + +# def req_edit_out(req: Request, text: bytes) -> Optional[Request]: +# ... + + +# def res_edit_in(res: Response) -> Optional[bytes]: +# ... + + +# def res_edit_out(res: Response, text: bytes) -> Optional[Response]: +# ... diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..f7cd19da --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'scalpel' diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/test/package-lock.json b/test/package-lock.json new file mode 100644 index 00000000..60a7e0bd --- /dev/null +++ b/test/package-lock.json @@ -0,0 +1,639 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "body-parser": "^1.20.2", + "express": "^4.18.2", + "raw-body": "^2.5.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 00000000..6b1ad1e5 --- /dev/null +++ b/test/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "body-parser": "^1.20.2", + "express": "^4.18.2", + "raw-body": "^2.5.2" + } +} diff --git a/test/server.js b/test/server.js new file mode 100644 index 00000000..7ac034d6 --- /dev/null +++ b/test/server.js @@ -0,0 +1,177 @@ +const express = require("express"); + +const { urlencoded } = express; + +const app = express(); + +const bodyParser = require("body-parser"); + +const getRawBody = require("raw-body"); + +app.use(urlencoded({ extended: true })); + +app.use(async (req, _res, next) => { + req.rawBody = getRawBody(req); + + next(); +}); + +// Known issue: duplicate headers are not supported +jsonifyRequest = (req) => ({ + url: req.url, + url_decoded: decodeURIComponent(req.url), + headers: (() => { + let key; + return Object.assign( + {}, + ...req.rawHeaders + .map((val, i) => (i & 1 ? { [key]: val } : (key = val) && null)) + .filter((e) => e), + ); + })(), + body: req.body.entries && req.body.toString(), +}); + +app.all("/base64", (req, res) => { + console.log("Received /base64"); + // Remove date header to ensure identical requests returns the exact same response + res.setHeader("Date", "[REDACTED]"); + const decoded = new Buffer(req.body.toString("utf-8"), "base64").toString( + "utf-8", + ); + + res.send( + Buffer.from(`Received in base64:\n-----\n${decoded}\n-----`).toString( + "base64", + ), + ); +}); + +app.all("/json", (req, res) => { + console.log("Received"); + // Remove date header to ensure identical requests returns the exact same response + // res.setHeader("Date", "[REDACTED]"); + const date = Date.now(); + console.log("Date: " + date); + res.setHeader("Date", date); + res.send(jsonifyRequest(req)); +}); + +app.all("/echo", async (req, res) => { + console.log("Received"); + // Remove date header to ensure identical requests returns the exact same response + res.setHeader("Date", "[REDACTED]"); + res.write("HEADERS:\n"); + rawHeader = req.rawHeaders; + for (let i = 0; i < rawHeader.length; i += 2) { + res.write(`${rawHeader[i]}: ${rawHeader[i + 1]}\n`); + } + res.write("\nBODY:\n"); + res.write(await req.rawBody); + res.end(); +}); + +// Display a multipart form to upload multiple files +app.get("/upload", (req, res) => { + res.send(` +

    + + + +
    + `); +}); + +// Handle a multipart form +app.post("/upload", (req, res) => { + console.log("Received"); + // Remove date header to ensure identical requests returns the exact same response + res.setHeader("Date", "[REDACTED]"); + res.send(req.body); +}); + +const crypto = require("crypto"); +const exp = require("constants"); + +const derive = (secret) => { + const hasher = crypto.createHash("sha256"); + hasher.update(secret); + const derived_aes_key = hasher.digest().slice(0, 32); + return derived_aes_key; +}; + +const get_cipher_decrypt = (secret, iv = Buffer.alloc(16, 0)) => { + const derived_aes_key = derive(secret); + const cipher = crypto.createDecipheriv("aes-256-cbc", derived_aes_key, iv); + return cipher; +}; + +const get_cipher_encrypt = (secret, iv = Buffer.alloc(16, 0)) => { + const derived_aes_key = derive(secret); + const cipher = crypto.createCipheriv("aes-256-cbc", derived_aes_key, iv); + return cipher; +}; + +const decrypt = (secret, data) => { + const decipher = get_cipher_decrypt(secret); + let decrypted = decipher.update(data, "base64", "utf8"); + decrypted += decipher.final("utf8"); + return decrypted; +}; + +const encrypt = (secret, data) => { + const cipher = get_cipher_encrypt(secret); + let encrypted = cipher.update(data, "utf8", "base64"); + encrypted += cipher.final("base64"); + return encrypted; +}; + +app.post("/encrypt", (req, res) => { + console.log(req.body); + console.log("Received"); + res.setHeader("Date", "[REDACTED]"); + + const secret = req.body["secret"]; + const data = req.body["encrypted"]; + + if (data === undefined) { + res.send("No content"); + return; + } + + const decrypted = decrypt(secret, data); + console.log({ decrypted }); + const resContent = `You have sent "${decrypted}" using secret "${secret}"`; + const encrypted = encrypt(secret, resContent); + + res.send(encrypted); +}); + +const session = "r4nd0mh3xs7r1ng"; + +app.get("/encrypt-session", (req, res) => { + res.send(session); +}); + +app.post("/encrypt-session", (req, res) => { + console.log(req.body); + console.log("Received"); + res.setHeader("Date", "[REDACTED]"); + + const secret = session; + const data = req.body["encrypted"]; + + if (data === undefined) { + res.send("No content"); + return; + } + + const decrypted = decrypt(secret, data); + console.log({ decrypted }); + const resContent = `You have sent "${decrypted}" using secret "${secret}"`; + const encrypted = encrypt(secret, resContent); + + res.send(encrypted); +}); + +app.listen(3000, ["localhost", "nol-thinkpad"]); diff --git a/transpile_tools/3.10_to_3.8.py b/transpile_tools/3.10_to_3.8.py new file mode 100644 index 00000000..1ed2c63c --- /dev/null +++ b/transpile_tools/3.10_to_3.8.py @@ -0,0 +1,87 @@ +import argparse +import os +import re +import sys +from pathlib import Path +from shutil import copy2 +from transpiler import transform_to_legacy + + +def process_directory( + input_dir: str, output_dir: str, exclude_patterns: list[str], verbose: bool +) -> None: + input_path = Path(input_dir) + output_path = Path(output_dir) + compiled_excludes = [re.compile(pattern) for pattern in exclude_patterns] + + for current_dir, _, files in os.walk(input_path): + for file in files: + current_file_path = Path(current_dir) / file + excluded = any( + exclude.search(str(current_file_path)) for exclude in compiled_excludes + ) + if not excluded and file.endswith(".py"): + process_file(current_file_path, input_path, output_path, verbose) + else: + copy_non_python_file( + current_file_path, input_path, output_path, verbose + ) + + +def process_file( + file_path: Path, input_base_path: Path, output_base_path: Path, verbose: bool +) -> None: + relative_path = file_path.relative_to(input_base_path) + output_file_path = output_base_path / relative_path + + output_file_path.parent.mkdir(parents=True, exist_ok=True) + + with open(file_path, "r") as file: + code = file.read() + + new_code = transform_to_legacy(code) + + with open(output_file_path, "w") as file: + file.write(new_code) + + if verbose: + print(f"Processed {file_path} -> {output_file_path}") + + +def copy_non_python_file( + file_path: Path, input_base_path: Path, output_base_path: Path, verbose: bool +) -> None: + relative_path = file_path.relative_to(input_base_path) + output_file_path = output_base_path / relative_path + + output_file_path.parent.mkdir(parents=True, exist_ok=True) + + copy2(file_path, output_file_path) + + if verbose: + print(f"Copied {file_path} -> {output_file_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Process a directory of files.") + parser.add_argument("input_directory", type=str, help="Input directory path") + parser.add_argument("output_directory", type=str, help="Output directory path") + parser.add_argument( + "--exclude", + action="append", + default=[], + help="Regex pattern to exclude files (can be used multiple times)", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Increase output verbosity" + ) + + args = parser.parse_args() + + process_directory( + args.input_directory, args.output_directory, args.exclude, args.verbose + ) + + +if __name__ == "__main__": + main() diff --git a/transpile_tools/transpiler.py b/transpile_tools/transpiler.py new file mode 100644 index 00000000..73ded6e7 --- /dev/null +++ b/transpile_tools/transpiler.py @@ -0,0 +1,338 @@ +"""Quick and dirty transpiler for porting Python 3.10 _basic_ match statements and type hints to Python 3.8""" + +import ast + + +class MatchToIfElseTransformer(ast.NodeTransformer): + def _case_to_if(self, case: ast.match_case, subject: ast.expr) -> ast.If: + """Transform a match case to an if statement + + Args: + case (ast.match_case): The match case to transform + subject (ast.expr): The subject of the match statement + + Returns: + ast.If: The transformed if statement + """ + test = self._pattern_to_expr(case.pattern, subject) + if case.guard is not None: + # Handle case like 'case x if x > 0:' + # Simply add the condition to the if statement + test = ast.BoolOp( + op=ast.And(), + values=[ + test, + self.visit(case.guard), + ], + ) + + # Ensure recursive transformation within the body of each case + body = [self.visit(stmt) for stmt in case.body] + return ast.If(test=test, body=body, orelse=[]) + + def visit_Match(self, node: ast.Match) -> ast.AST: + """Transform a Python 3.10 match statement to a series of if-else statements + + Args: + node (ast.Match): The match statement to transform + + Returns: + ast.AST: The transformed if-else statements + """ + if_stmts = [] + + # Transform each case to an if statement + for case in node.cases: + if_stmts.append(self._case_to_if(case, node.subject)) + + # Link the if-else statements together + for i in range(len(if_stmts) - 1, 0, -1): + if_stmts[i - 1].orelse = [if_stmts[i]] + + return if_stmts[0] if if_stmts else node + + def _pattern_to_expr(self, pattern: ast.AST, subject: ast.expr) -> ast.expr: + match pattern: + # Transform case "abc" to subject == value + case ast.MatchValue(): + return ast.Compare( + left=subject, + ops=[ast.Eq()], + comparators=[pattern.value], + ) + + # Transform to isinstance(subject, cls) and subject.attr == value and ... + case ast.MatchClass( + cls=cls, kwd_attrs=kwd_attrs, kwd_patterns=kwd_patterns + ): + # Check if the subject is an instance of the class + comparisons = [ + ast.Call( + func=ast.Name(id="isinstance", ctx=ast.Load()), + args=[subject, ast.Name(id=cls.id, ctx=ast.Load())], + keywords=[], + ) + ] + + # Check the attributes of the subject + for attr, pattern in zip(kwd_attrs, kwd_patterns): + left = ast.Call( + func=ast.Name(id="getattr", ctx=ast.Load()), + args=[subject, ast.Constant(value=attr)], + keywords=[], + ) + comparator = self._pattern_to_expr(pattern, subject) + comparison = ast.Compare( + left=left, + ops=[ast.Eq()], + comparators=[comparator], + ) + comparisons.append(comparison) + + return ast.BoolOp( + op=ast.And(), + values=comparisons, + ) + + # Handle case _ if : ... + case ast.MatchAs(): + if pattern.pattern is None: + return ast.Constant(value=True) + + return self._pattern_to_expr(pattern.pattern, subject) + + # Transform to or or ... + case ast.MatchOr(): + return ast.BoolOp( + op=ast.Or(), + values=[ + self._pattern_to_expr(p, subject) for p in pattern.patterns + ], + ) + + # Handle list/tuple patterns + # Transform to subject[0] == value1 and subject[1] == value2 and ... + case ast.MatchSequence(): + checks = [] + for i, elt in enumerate(pattern.patterns): + element_check = self._pattern_to_expr( + elt, + ast.Subscript( + value=subject, + slice=ast.Index(value=ast.Constant(value=i)), + ctx=ast.Load(), + ), + ) + checks.append(element_check) + + # Combine checks with logical AND + return ast.BoolOp(op=ast.And(), values=checks) + + case ast.MatchSingleton(): + return ast.Compare( + left=subject, + ops=[ast.Is()], + comparators=[pattern], + ) + + # Default case, transform to subject == pattern + case _: + return ast.Compare( + left=subject, + ops=[ast.Eq()], + comparators=[pattern], + ) + + +class TypeHintTransformer(ast.NodeTransformer): + """Transform Python 3.10 type hints to Python 3.8 compatible type hints + + Args: + ast (_type_): The abstract syntax tree to transform + """ + + def __init__(self): + self.need_imports = set() + + def visit_BinOp(self, node: ast.BinOp) -> ast.AST: + """Transform binary operations in type hints + E.g. type1 | type2 -> Union[type1, type2] + + Args: + node (ast.BinOp): The binary operation to transform + + Returns: + ast.AST: The transformed binary operation + """ + if isinstance(node.op, ast.BitOr): + # Import Union if not already imported + self.need_imports.add("Union") + left = self.visit(node.left) + right = self.visit(node.right) + return ast.Subscript( + value=ast.Name(id="Union", ctx=ast.Load()), + slice=ast.Index(value=ast.Tuple(elts=[left, right], ctx=ast.Load())), + ctx=ast.Load(), + ) + + return self.generic_visit(node) + + def visit_Subscript(self, node: ast.Subscript) -> ast.AST: + """Transform subscript slices in type hints + E.g. list[int] -> List[int] + + Args: + node (ast.Subscript): The subscript slice to transform + + Returns: + ast.AST: The transformed subscript slice + """ + # Transform the subscript slice if it's a type hint using '[]' + # Eg. list[int] -> List[int] + node.value = self.visit(node.value) + node.slice = self.visit(node.slice) + + # Handle unsunported suscript type hints like Mapping[K, V] + if isinstance(node.value, ast.Name) and node.value.id in { + "Mapping", + "MutableMapping", + }: + # Transform Mapping[K, V] to plain Mapping + # E.g. Mapping[K, V] -> Mapping + return ast.Name(id=node.value.id, ctx=node.ctx) + + if isinstance(node.value, ast.Name) and node.value.id in { + "list", + "tuple", + "dict", + "set", + "Sequence", + }: + self.need_imports.add(node.value.id.capitalize()) + + # Ensure the slice is properly transformed, including nested type hints + transformed_slice = self.transform_slice(node.slice) + return ast.Subscript( + value=ast.Name(id=node.value.id.capitalize(), ctx=ast.Load()), + slice=transformed_slice, + ctx=node.ctx, + ) + + return node + + def transform_slice(self, slice: ast.AST) -> ast.AST: + """Recursively transform slices for nested type hints + E.g. list[list[int]] -> List[List[int]] + + Args: + slice (ast.AST): The slice to transform + + Returns: + ast.AST: The transformed slices + """ + if isinstance(slice, ast.Index): + return ast.Index(value=self.visit(slice.value)) + + return slice + + def visit_Tuple(self, node: ast.Tuple) -> ast.AST: + """Transform tuples in type hints + E.g. (int, str) -> Tuple[int, str] + + + Args: + node (ast.Tuple): The tuple to transform + + Returns: + ast.AST: The transformed tuple + """ + elts = [self.visit(elt) for elt in node.elts] + return ast.Tuple(elts=elts, ctx=ast.Load()) + + def visit_Module(self, node: ast.Module) -> ast.AST: + """Ensure all necessary imports are added to the module + + Args: + node (ast.Module): The module to transform + + Returns: + ast.AST: The transformed module + """ + self.generic_visit(node) + for import_name in sorted(self.need_imports): + typing_import = ast.ImportFrom( + module="typing", names=[ast.alias(name=import_name)], level=0 + ) + node.body.insert(0, typing_import) + + return node + + +class FutureImportTransformer(ast.NodeTransformer): + """Add __future__ imports to the top of the module + + Args: + ast (_type_): The abstract syntax tree to transform + """ + + def __init__(self): + self.future_imports = [] + + def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST: + """Remove existing __future__ imports and collect them for later re-insertion""" + if node.module == "__future__": + self.future_imports.append(node) + return None # Remove the node from its original position + + return node + + def visit_Module(self, node: ast.Module) -> ast.AST: + """Prepend collected __future__ imports to the module body + + Args: + node (ast.Module): The module to transform + + Returns: + ast.AST: The transformed module + """ + self.generic_visit(node) # First, process and collect all __future__ imports + # Prepend collected __future__ imports to the module body + node.body = self.future_imports + node.body + return node + + +def transform_to_legacy(source_code: str) -> str: + """Transform Python 3.10 code to Python 3.8 compatible code + + Args: + source_code (str): The Python 3.10 code to transform + + Returns: + str: The transformed Python 3.8 compatible code + """ + parsed_source = ast.parse(source_code) + # Transform match statements to if-else statements + transformed = MatchToIfElseTransformer().visit(parsed_source) + + # Transform type hints to Python 3.8 compatible type hints + transformed = TypeHintTransformer().visit(transformed) + + # Add __future__ imports to the top of the module + transformed = FutureImportTransformer().visit(transformed) + return ast.unparse(transformed) + + +if __name__ == "__main__": + import sys + + if len(sys.argv) != 2: + print("Usage: python transpiler.py ") + sys.exit(1) + + file_path = sys.argv[1] + + with open(file_path, "r") as file: + code = file.read() + + new_code = transform_to_legacy(code) + print(new_code)