├── .github ├── ISSUE_TEMPLATE │ └── bug_report.yaml ├── dependabot.yml └── workflows │ ├── build-docker-image.yml │ ├── build-release.yml │ ├── build-test.yml │ └── maturin.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── build ├── build-host-release.sh └── build-release.sh ├── crates ├── core │ ├── Cargo.toml │ └── src │ │ ├── ca.rs │ │ ├── error.rs │ │ ├── handler.rs │ │ ├── http_client.rs │ │ ├── lib.rs │ │ ├── mitm.rs │ │ └── sni_reader.rs ├── rule │ ├── Cargo.toml │ └── src │ │ ├── action │ │ ├── js.rs │ │ ├── log.rs │ │ ├── mod.rs │ │ └── modify.rs │ │ ├── cache.rs │ │ ├── filter.rs │ │ ├── handler.rs │ │ └── lib.rs └── trust_cert │ ├── Cargo.toml │ └── src │ ├── lib.rs │ ├── linux.rs │ └── windows.rs ├── docs ├── .nojekyll ├── CNAME ├── README.md ├── _sidebar.md ├── guide │ ├── 0_cert.md │ ├── 1_rule.md │ ├── 2_proxy.md │ ├── README.md │ └── transparent_proxy.md ├── index.html ├── others.md └── rule │ ├── README.md │ ├── action.md │ ├── filter.md │ └── modify.md ├── pyproject.toml ├── rules ├── ads.yaml ├── demo.yaml ├── dxx.yaml ├── js.yaml ├── log.yaml ├── map_modify.yaml ├── rule_chain.yaml ├── text_modify.yaml ├── youtube.yaml ├── yuanshen.yaml └── yxbj.yaml ├── rust-toolchain ├── rustfmt.toml └── src ├── ca.rs ├── error.rs ├── file ├── frule.rs ├── mod.rs └── single_multi.rs ├── lib.rs └── main.rs /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | title: "[Bug] " 4 | body: 5 | - type: checkboxes 6 | id: ensure 7 | attributes: 8 | label: Verify steps 9 | description: " 10 | 在提交之前,请确认 11 | Please verify that you've followed these steps 12 | " 13 | options: 14 | - label: " 15 | 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 16 | I have searched on the [issue tracker](……/) for a related issue. 17 | " 18 | required: true 19 | - label: " 20 | 我已经仔细看过 [Documentation](https://good-mitm.zu1k.com/) 并无法自行解决问题 21 | I have read the [documentation](https://good-mitm.zu1k.com/) and was unable to solve the issue. 22 | " 23 | required: true 24 | - type: input 25 | attributes: 26 | label: Version 27 | validations: 28 | required: true 29 | - type: dropdown 30 | id: os 31 | attributes: 32 | label: What OS are you seeing the problem on? 33 | multiple: true 34 | options: 35 | - Linux 36 | - Windows 37 | - macOS 38 | - OpenBSD/FreeBSD 39 | - type: textarea 40 | attributes: 41 | render: yaml 42 | label: "Rule file" 43 | description: " 44 | 在下方附上规则文件,请确保配置文件中没有敏感信息 45 | Paste the rule file below, please make sure that there is no sensitive information in the configuration file 46 | " 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | render: shell 52 | label: log 53 | description: " 54 | 在下方附上运行日志 55 | Paste the log below. 56 | " 57 | - type: textarea 58 | attributes: 59 | label: Description 60 | validations: 61 | required: true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 0 8 | -------------------------------------------------------------------------------- /.github/workflows/build-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Images 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Setup Docker Buildx 13 | uses: docker/setup-buildx-action@v1 14 | - name: Login to GitHub Container Registry 15 | uses: docker/login-action@v1 16 | with: 17 | registry: ghcr.io 18 | username: ${{ github.repository_owner }} 19 | password: ${{ secrets.GITHUB_TOKEN }} 20 | - name: Docker metadata 21 | id: metadata 22 | uses: docker/metadata-action@v3 23 | with: 24 | images: ghcr.io/${{ github.repository_owner }}/good-mitm 25 | - name: Build and release Docker images 26 | uses: docker/build-push-action@v2 27 | with: 28 | platforms: linux/amd64,linux/arm64/v8 29 | target: good-mitm 30 | tags: ${{ steps.metadata.outputs.tags }} 31 | push: true 32 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build Releases 2 | on: 3 | release: 4 | types: [published] 5 | 6 | env: 7 | CARGO_TERM_COLOR: always 8 | 9 | jobs: 10 | build-cross: 11 | runs-on: ubuntu-latest 12 | env: 13 | RUST_BACKTRACE: full 14 | strategy: 15 | matrix: 16 | target: 17 | - i686-unknown-linux-musl 18 | - x86_64-pc-windows-gnu 19 | - x86_64-unknown-linux-gnu 20 | - x86_64-unknown-linux-musl 21 | - armv7-unknown-linux-musleabihf 22 | - armv7-unknown-linux-gnueabihf 23 | - aarch64-unknown-linux-gnu 24 | - aarch64-unknown-linux-musl 25 | 26 | steps: 27 | - uses: actions/checkout@v2 28 | 29 | - name: Install dependences 30 | run: sudo apt-get update -y && sudo apt-get install -y upx; 31 | 32 | - name: Install Rust 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | target: ${{ matrix.target }} 37 | toolchain: nightly 38 | default: true 39 | override: true 40 | 41 | - name: Install cross 42 | run: cargo install cross 43 | 44 | - name: Build ${{ matrix.target }} 45 | timeout-minutes: 120 46 | env: 47 | PKG_CONFIG_ALLOW_CROSS: "1" 48 | run: | 49 | compile_target=${{ matrix.target }} 50 | 51 | if [[ "$compile_target" == "mips-"* || "$compile_target" == "mipsel-"* || "$compile_target" == "mips64-"* || "$compile_target" == "mips64el-"* ]]; then 52 | if [[ "$?" == "0" ]]; then 53 | compile_compress="-u" 54 | fi 55 | fi 56 | 57 | cd build 58 | ./build-release.sh -t ${{ matrix.target }} $compile_features $compile_compress 59 | 60 | - name: Upload Github Assets 61 | uses: softprops/action-gh-release@v1 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | files: build/release/* 66 | prerelease: ${{ contains(github.ref, '-') }} 67 | 68 | build-unix: 69 | runs-on: ${{ matrix.os }} 70 | env: 71 | RUST_BACKTRACE: full 72 | strategy: 73 | matrix: 74 | os: [macos-latest] 75 | target: 76 | - x86_64-apple-darwin 77 | - aarch64-apple-darwin 78 | steps: 79 | - uses: actions/checkout@v2 80 | 81 | - name: Install Dependences 82 | if: runner.os == 'macOS' 83 | run: | 84 | brew install gnu-tar pkg-config openssl 85 | echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH 86 | 87 | - name: Install Rust 88 | uses: actions-rs/toolchain@v1 89 | with: 90 | profile: minimal 91 | target: ${{ matrix.target }} 92 | toolchain: nightly 93 | default: true 94 | override: true 95 | 96 | - name: Build release 97 | shell: bash 98 | run: | 99 | ./build/build-host-release.sh -t ${{ matrix.target }} 100 | 101 | - name: Upload Github Assets 102 | uses: softprops/action-gh-release@v1 103 | env: 104 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | with: 106 | files: build/release/* 107 | prerelease: ${{ contains(github.ref, '-') }} 108 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request_target] 2 | 3 | name: Build Test 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/cache@v2 11 | with: 12 | path: | 13 | ~/.cargo/bin/ 14 | ~/.cargo/registry/index/ 15 | ~/.cargo/registry/cache/ 16 | ~/.cargo/git/db/ 17 | target/ 18 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} 19 | - name: Setup rust toolchain 20 | run: rustup show 21 | - name: cargo build 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: build 25 | args: --release --all-targets 26 | - name: cargo fmt 27 | uses: actions-rs/cargo@v1 28 | with: 29 | command: fmt 30 | args: --all -- --check 31 | - name: cargo clippy 32 | uses: actions-rs/clippy-check@v1 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | args: --all-features 36 | -------------------------------------------------------------------------------- /.github/workflows/maturin.yml: -------------------------------------------------------------------------------- 1 | # This file is autogenerated by maturin v0.15.2 2 | # To update, run 3 | # 4 | # maturin generate-ci github 5 | # 6 | name: Maturin CI 7 | 8 | on: 9 | push: 10 | branches: 11 | - master 12 | tags: 13 | - '*' 14 | pull_request: 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | linux: 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | target: [x86_64, x86, aarch64, armv7] 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Build wheels 29 | uses: PyO3/maturin-action@v1 30 | with: 31 | target: ${{ matrix.target }} 32 | args: --release --out dist 33 | sccache: 'true' 34 | manylinux: auto 35 | - name: Upload wheels 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: wheels 39 | path: dist 40 | 41 | windows: 42 | runs-on: windows-latest 43 | strategy: 44 | matrix: 45 | target: [x64, x86] 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: Build wheels 49 | uses: PyO3/maturin-action@v1 50 | with: 51 | target: ${{ matrix.target }} 52 | args: --release --out dist 53 | sccache: 'true' 54 | - name: Upload wheels 55 | uses: actions/upload-artifact@v3 56 | with: 57 | name: wheels 58 | path: dist 59 | 60 | macos: 61 | runs-on: macos-latest 62 | strategy: 63 | matrix: 64 | target: [x86_64, aarch64] 65 | steps: 66 | - uses: actions/checkout@v3 67 | - name: Build wheels 68 | uses: PyO3/maturin-action@v1 69 | with: 70 | target: ${{ matrix.target }} 71 | args: --release --out dist 72 | sccache: 'true' 73 | - name: Upload wheels 74 | uses: actions/upload-artifact@v3 75 | with: 76 | name: wheels 77 | path: dist 78 | 79 | release: 80 | name: Release 81 | runs-on: ubuntu-latest 82 | if: "startsWith(github.ref, 'refs/tags/')" 83 | needs: [linux, windows, macos] 84 | steps: 85 | - uses: actions/download-artifact@v3 86 | with: 87 | name: wheels 88 | - name: Publish to PyPI 89 | uses: PyO3/maturin-action@v1 90 | env: 91 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 92 | with: 93 | command: upload 94 | args: --skip-existing * 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /bin 3 | /ca 4 | .vscode 5 | .idea 6 | 7 | /crates/adblocker 8 | 9 | 10 | /target 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | .pytest_cache/ 15 | *.py[cod] 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | .venv/ 23 | env/ 24 | bin/ 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | include/ 35 | man/ 36 | venv/ 37 | *.egg-info/ 38 | .installed.cfg 39 | *.egg 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | pip-selfcheck.json 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .coverage 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | 54 | # Translations 55 | *.mo 56 | 57 | # Mr Developer 58 | .mr.developer.cfg 59 | .project 60 | .pydevproject 61 | 62 | # Rope 63 | .ropeproject 64 | 65 | # Django stuff: 66 | *.log 67 | *.pot 68 | 69 | .DS_Store 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyCharm 75 | .idea/ 76 | 77 | # VSCode 78 | .vscode/ 79 | 80 | # Pyenv 81 | .python-version -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "good-mitm" 3 | version = "0.4.2" 4 | authors = ["zu1k "] 5 | edition = "2021" 6 | description = "Good Man in the Middle: Use MITM technology to provide features like rewrite, redirect." 7 | readme = "README.md" 8 | homepage = "https://github.com/zu1k/good-mitm" 9 | repository = "https://github.com/zu1k/good-mitm" 10 | license = "MIT" 11 | keywords = ["proxy", "MITM"] 12 | exclude = [".github/", "docs/", "rules/"] 13 | 14 | [profile.release] 15 | strip = true 16 | lto = true 17 | opt-level = "s" 18 | codegen-units = 1 19 | 20 | [dependencies] 21 | mitm-core = { path = "crates/core", package = "good-mitm-core" } 22 | rule = { path = "crates/rule", package = "good-mitm-rule" } 23 | 24 | anyhow = "1.0" 25 | clap = { version = "4", features = ["derive"] } 26 | thiserror = "1" 27 | log = "0.4" 28 | env_logger = "0.10" 29 | serde = { version = "1.0", features = ["derive"] } 30 | serde_yaml = "0.9" 31 | hyper-proxy = { version = "0.9", default-features = false } 32 | rustls-pemfile = "1.0" 33 | tokio = { version = "1", features = ["rt-multi-thread", "signal"] } 34 | rustls = "0.21" 35 | trust_cert = { path = "crates/trust_cert", optional = true } 36 | 37 | [features] 38 | default = [] 39 | trust-cert = ["dep:trust_cert"] 40 | js = ["rule/js"] 41 | 42 | [workspace] 43 | members = [ 44 | "crates/core", 45 | "crates/rule", 46 | "crates/trust_cert" 47 | ] 48 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM rust:1.61.0-buster AS build 2 | 3 | ARG TARGETARCH 4 | 5 | RUN apt-get update && apt-get install -y build-essential curl musl-tools upx pkg-config libssl-dev 6 | 7 | WORKDIR /root/good-mitm 8 | 9 | ADD . . 10 | 11 | RUN rustup install nightly && rustup default nightly && \ 12 | case "$TARGETARCH" in \ 13 | "amd64") \ 14 | RUST_TARGET="x86_64-unknown-linux-musl" \ 15 | MUSL="x86_64-linux-musl" \ 16 | ;; \ 17 | "arm64") \ 18 | RUST_TARGET="aarch64-unknown-linux-musl" \ 19 | MUSL="aarch64-linux-musl" \ 20 | ;; \ 21 | *) \ 22 | echo "Doesn't support $TARGETARCH architecture" \ 23 | exit 1 \ 24 | ;; \ 25 | esac && \ 26 | wget -qO- "https://musl.cc/$MUSL-cross.tgz" | tar -xzC /root/ && \ 27 | CC=/root/$MUSL-cross/bin/$MUSL-gcc && \ 28 | rustup target add $RUST_TARGET && \ 29 | PKG_CONFIG_ALLOW_CROSS=1 RUSTFLAGS="-C linker=$CC" CC=$CC cargo build --target "$RUST_TARGET" --release && \ 30 | mv target/$RUST_TARGET/release/good-mitm target/release/ && \ 31 | upx -9 target/release/good-mitm 32 | 33 | FROM alpine:3.14 AS good-mitm 34 | 35 | COPY --from=build /root/good-mitm/target/release/good-mitm /usr/bin 36 | ENTRYPOINT [ "good-mitm" ] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 zu1k i@zu1k.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=good-mitm 2 | BINDIR=bin 3 | VERSION=$(shell git describe --tags || echo "unknown version") 4 | UPX=upx --best 5 | STRIP=llvm-strip -s 6 | CROSS_BUILD=cross build --release --target 7 | 8 | all: fmt clippy build 9 | 10 | build: 11 | cargo build 12 | 13 | clean: 14 | cargo clean 15 | 16 | deps: 17 | cargo install cargo-strip xargo cross 18 | 19 | a: fmt clippy 20 | 21 | fmt: 22 | cargo fmt --all 23 | 24 | fix: 25 | cargo fix 26 | 27 | check: 28 | cargo check 29 | 30 | clippy: 31 | cargo clippy 32 | 33 | prepare: fmt check clippy fix 34 | 35 | CROSS_TARGET_LIST = \ 36 | x86_64-unknown-linux-musl \ 37 | i686-unknown-linux-musl \ 38 | aarch64-unknown-linux-musl \ 39 | armv7-unknown-linux-musleabihf \ 40 | x86_64-apple-darwin \ 41 | aarch64-apple-darwin \ 42 | 43 | $(CROSS_TARGET_LIST): 44 | $(CROSS_BUILD) $@ 45 | cp "target/$@/release/$(NAME)" "$(BINDIR)/$(NAME)-$@" 46 | $(STRIP) "$(BINDIR)/$(NAME)-$@" 47 | $(UPX) "$(BINDIR)/$(NAME)-$@" 48 | 49 | windows: 50 | cargo build --target x86_64-pc-windows-gnu --release 51 | cp "target/x86_64-pc-windows-gnu/release/$(NAME).exe" "$(BINDIR)/$(NAME)-x86_64-pc-windows-gnu-$(VERSION).exe" 52 | $(STRIP) "$(BINDIR)/$(NAME)-x86_64-pc-windows-gnu-$(VERSION).exe" 53 | zip -q -m $(BINDIR)/$(NAME)-x86_64-pc-windows-gnu-$(VERSION).zip "$(BINDIR)/$(NAME)-x86_64-pc-windows-gnu-$(VERSION).exe" 54 | 55 | bindir: 56 | rm -rf $(BINDIR) 57 | mkdir $(BINDIR) 58 | 59 | bin_gz=$(addsuffix .gz, $(CROSS_TARGET_LIST)) 60 | 61 | $(bin_gz): %.gz : % 62 | chmod +x $(BINDIR)/$(NAME)-$(basename $@) 63 | gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@) 64 | 65 | gz_release: $(bin_gz) 66 | 67 | release: bindir gz_release windows 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Good Man in the Middle 2 | 3 | [![GitHub stars](https://img.shields.io/github/stars/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/stargazers) 4 | [![GitHub forks](https://img.shields.io/github/forks/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/network) 5 | [![Release](https://img.shields.io/github/release/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/releases) 6 | [![GitHub issues](https://img.shields.io/github/issues/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/issues) 7 | [![Build](https://github.com/zu1k/good-mitm/actions/workflows/build-test.yml/badge.svg)](https://github.com/zu1k/good-mitm/actions/workflows/build-test.yml) 8 | [![GitHub license](https://img.shields.io/github/license/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/blob/master/LICENSE) 9 | [![Docs](https://img.shields.io/badge/docs-read-blue.svg?style=flat)](https://good-mitm.zu1k.com/) 10 | 11 | #### [中文版](https://github.com/zu1k/good-mitm/blob/master/README_zh.md) 12 | 13 | Rule-based MITM engine. Rewriting, redirecting and rejecting on HTTP(S) requests and responses, supports JavaScript. 14 | 15 | ## Features 16 | 17 | - Signing certificate automatically based on TLS ClientHello SNI extension 18 | - Support selective MITM for specific domains 19 | - Rule description language based on YAML format: rewrite, reject, redirect 20 | - Flexible rule matching capabilities 21 | - Domain name prefix/suffix/exact match 22 | - Regular expression matching 23 | - Multiple filter rules 24 | - Flexible text content rewriting 25 | - Erase/replace 26 | - Regular expression substitution 27 | - Flexible dictionary-based content rewriting 28 | - HTTP header rewriting 29 | - Cookie rewriting 30 | - Support for multiple actions per rule 31 | - JavaScript script rules support (programmatic intervention) 32 | - Transparent proxy support 33 | - Support HTTPS and HTTP multiplexing on a single port 34 | - Install CA certificate to the system trust zone 35 | 36 | ## Usage 37 | 38 | ### Certificate Preparation 39 | 40 | Due to the requirement of the `MITM` technique, you need to generate and trust your own root certificate. 41 | 42 | #### Generate Root Certificate 43 | 44 | For security reasons, please do not blindly trust any root certificate provided by strangers. You need to generate your own root certificate and private key. 45 | 46 | Experienced users can use OpenSSL to perform the necessary operations. However, for users without experience in this area, you can use the following command to generate the required content. The generated certificate and private key will be stored in the `ca` directory. 47 | 48 | ```shell 49 | good-mitm.exe genca 50 | ``` 51 | 52 | After using the proxy provided by Good-MITM in your browser, you can directly download the certificate by visiting [http://cert.mitm.plus](http://cert.mitm.plus). This is particularly useful when providing services to other devices. 53 | 54 | #### Trusting the Certificate 55 | 56 | You can add the root certificate to the trust zone of your operating system or browser, depending on your needs. 57 | 58 | ### Proxy 59 | 60 | Start Good-MITM and specify the rule file or directory to use. 61 | 62 | ```shell 63 | good-mitm.exe run -r rules 64 | ``` 65 | 66 | Use the HTTP proxy provided by Good-MITM in your browser or operating system: `http://127.0.0.1:34567`. 67 | 68 | #### Transparent Proxy 69 | 70 | See https://docs.mitmproxy.org/stable/howto-transparent/ for docs. 71 | 72 | ```shell 73 | sudo sysctl -w net.ipv4.ip_forward=1 74 | sudo sysctl -w net.ipv6.conf.all.forwarding=1 75 | sudo sysctl -w net.ipv4.conf.all.send_redirects=0 76 | 77 | sudo useradd --create-home mitm 78 | sudo -u mitm -H bash -c 'good-mitm run -r rules/log.yaml -b 0.0.0.0:34567' 79 | 80 | sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 80 -j REDIRECT --to-port 34567 81 | sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 443 -j REDIRECT --to-port 34567 82 | sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 80 -j REDIRECT --to-port 34567 83 | sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 443 -j REDIRECT --to-port 34567 84 | ``` 85 | 86 | ## Rule 87 | 88 | `Rule` is used to manipulate Good-MITM. 89 | 90 | A valid rule should include the following components: 91 | 92 | - `Name`:Used to differentiate different rules for easier maintenance. 93 | - [`Filter`](#filter):Used to select the content to be processed from a set of `requests` and `responses`. 94 | - [`Action`](#action):Used to perform desired actions, including `redirect`, `reject`, `modification`, etc. 95 | - Optionally, specify the domain name that requires MITM. 96 | 97 | ```yaml 98 | - name: "Block YouTube tracking" 99 | mitm: "*.youtube.com" 100 | filter: 101 | url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 102 | action: reject 103 | ``` 104 | 105 | Additionally, a valid rule should meet the following requirements: 106 | 107 | - Focus: Each rule should be designed to perform a single task. 108 | - Simplicity: Use straightforward methods for processing to ensure easy maintenance. 109 | - Efficiency: Use efficient methods whenever possible, such as using domain suffixes and prefixes instead of complex regular expressions for domain matching. 110 | 111 | ### Filter 112 | 113 | `Filter`is used to select the requests and responses that need to be processed. 114 | 115 | #### Available Options 116 | 117 | Currently, `Filter` includes the following types: 118 | 119 | - All 120 | - Domain(String) 121 | - DomainKeyword(String) 122 | - DomainPrefix(String) 123 | - DomainSuffix(String) 124 | - UrlRegex(fancy_regex::Regex) 125 | 126 | > **Note** 127 | > In the current version, the `domain` related types match the `host` field, which usually does not affect the results. 128 | > If a website is using a non-standard port, the rule needs to specify the port. 129 | > This behavior will be optimized in future versions. 130 | 131 | ##### All 132 | 133 | When specifying the filter as `all`, it will match all requests and responses. This is typically used for performing logging actions. 134 | 135 | ```yaml 136 | - name: "log" 137 | filter: all 138 | action: 139 | - log-req 140 | - log-res 141 | ``` 142 | 143 | ##### Domain 144 | 145 | `domain` performs a full match against the domain name. 146 | 147 | ```yaml 148 | - name: "redirect" 149 | filter: 150 | domain: 'none.zu1k.com' 151 | action: 152 | redirect: "https://zu1k.com/" 153 | ``` 154 | 155 | ##### DomainKeyword 156 | 157 | `domain-keyword` performs a keyword match against the domain name. 158 | 159 | ```yaml 160 | - name: "reject CSDN" 161 | filter: 162 | domain-keyword: 'csdn' 163 | action: reject 164 | ``` 165 | 166 | ##### DomainPrefix 167 | 168 | `domain-prefix` performs a prefix match against the domain name. 169 | 170 | ```yaml 171 | - name: "ad prefix" 172 | filter: 173 | domain-prefix: 'ads' // example: "ads.xxxxx.com" 174 | action: reject 175 | ``` 176 | 177 | ##### DomainSuffix 178 | 179 | `domain-suffix` performs a suffix match against the domain name. 180 | 181 | 182 | ```yaml 183 | - name: "redirect" 184 | filter: 185 | domain-suffix: 'google.com.cn' 186 | action: 187 | redirect: "https://google.com" 188 | ``` 189 | 190 | ##### UrlRegex Url 191 | 192 | `url-regex` performs a regular expression match against the entire URL. 193 | 194 | ```yaml 195 | - name: "youtube tracking" 196 | mitm: "*.youtube.com" 197 | filter: 198 | url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 199 | action: reject 200 | ``` 201 | 202 | #### Multiple Filter 203 | 204 | The `filters` field supports both single filters and multiple filters, with the relationship between multiple filters being `OR`. 205 | 206 | ```yaml 207 | - name: "youtube-2" 208 | mitm: 209 | - "*.youtube.com" 210 | - "*.googlevideo.com" 211 | filters: 212 | - url-regex: '^https?:\/\/[\w-]+\.googlevideo\.com\/(?!(dclk_video_ads|videoplayback\?)).+(&oad|ctier)' 213 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/api\/stats\/ads' 214 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 215 | - url-regex: '^https?:\/\/\s.youtube.com/api/stats/qoe?.*adformat=' 216 | action: reject 217 | ``` 218 | 219 | Multiple rules with the same action can be aggregated into a single rule for easier maintenance. 220 | 221 | ### Action 222 | 223 | `Action` is used to perform operations on requests or responses. 224 | 225 | #### Available Options 226 | 227 | Currently, `Action` includes the following options: 228 | 229 | - Reject 230 | - Redirect(String) 231 | - ModifyRequest(Modify) 232 | - ModifyResponse(Modify) 233 | - LogRes 234 | - LogReq 235 | 236 | ##### Reject 237 | 238 | The `reject` type directly returns `502` status code, which is used to reject certain requests. It can be used to block tracking and ads. 239 | 240 | ```yaml 241 | - name: "reject CSDN" 242 | filter: 243 | domain-keyword: 'csdn' 244 | action: reject 245 | ``` 246 | 247 | ##### Redirect 248 | 249 | The `redirect` type directly returns `302` status code for redirection. 250 | 251 | ```yaml 252 | - name: "youtube-1" 253 | filter: 254 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 255 | action: 256 | redirect: "$1$4" 257 | ``` 258 | 259 | ##### ModifyRequest 260 | 261 | `modify-request` is used to modify the request. For specific modification rules, refer to the [Modify](#modify) section. 262 | 263 | ##### ModifyResponse 264 | 265 | `modify-response` is used to modify the response. For specific modification rules, refer to the [Modify](#modify) section. 266 | 267 | ##### Log 268 | 269 | `log-req` is used to log the request, and `log-res` is used to log the response. 270 | 271 | #### Multiple Action 272 | 273 | The `actions` field supports both single actions and multiple actions. When multiple actions need to be performed, an array should be used. 274 | 275 | ```yaml 276 | - name: "youtube-1" 277 | filter: 278 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 279 | actions: 280 | - log-req: 281 | - redirect: "$1$4" 282 | ``` 283 | 284 | ### Modify 285 | 286 | Modify are used to perform modification operations, including modifying requests and modifying responses. 287 | 288 | #### Available Options 289 | 290 | Based on the location of the content to be modified, the modifiers can be categorized as follows: 291 | 292 | - Header(MapModify) 293 | - Cookie(MapModify) 294 | - Body(TextModify) 295 | 296 | ##### TextModify 297 | 298 | `TextModify` is mainly used for modifying text. Currently, it supports two methods: 299 | 300 | - Setting the text content directly. 301 | - Simple replacement or regular expression replacement. 302 | 303 | ###### Setting Text Directly 304 | 305 | For the plain type, the content will be directly set to the specified text. 306 | 307 | ```yaml 308 | - name: "modify response body plain" 309 | filter: 310 | domain: '126.com' 311 | action: 312 | modify-response: 313 | body: "Hello 126.com, from Good-MITM" 314 | ``` 315 | 316 | ###### Replacement 317 | 318 | Replacement supports both simple replacement and regular expression replacement. 319 | 320 | Simple Replacement 321 | 322 | ```yaml 323 | - name: "modify response body replace" 324 | filter: 325 | domain-suffix: '163.com' 326 | action: 327 | modify-response: 328 | body: 329 | origin: "NetEase homepage" 330 | new: "Good-MITM homepage" 331 | ``` 332 | 333 | Regular expression replacement. 334 | 335 | ```yaml 336 | - name: "modify response body regex replace" 337 | filter: 338 | domain-suffix: 'zu1k.com' 339 | action: 340 | - modify-response: 341 | body: 342 | re: '(\d{4})' 343 | new: 'maybe $1' 344 | 345 | ``` 346 | 347 | ##### MapModify 348 | 349 | `MapModify` is a modifier used to modify dictionary-type locations, such as `header` and `cookies`. 350 | 351 | The `key` represents the key in the dictionary and must be specified. 352 | 353 | The `value` is of type `TextModify` and follows the methods mentioned above. 354 | 355 | If `remove` is set to `true`, the key-value pair will be removed. 356 | 357 | ```yaml 358 | - name: "modify response header" 359 | filter: 360 | domain: '126.com' 361 | action: 362 | - modify-response: 363 | header: 364 | key: date 365 | value: 366 | origin: "2022" 367 | new: "1999" 368 | - modify-response: 369 | header: 370 | key: new-header-item 371 | value: Good-MITM 372 | - modify-response: 373 | header: 374 | key: server 375 | remove: true 376 | ``` 377 | 378 | ##### Header Modification 379 | 380 | Refer to the methods in the `MapModify` section. 381 | 382 | ##### Cookie Modification 383 | 384 | Same as the Header modification method. 385 | 386 | If `remove` is set to `true`, the corresponding `set-cookie` item will also be removed. 387 | 388 | ##### Body Modification 389 | 390 | Refer to the methods in the `TextModify` section. 391 | 392 | ## License 393 | 394 | **Good-MITM** © [zu1k](https://github.com/zu1k), Released under the [MIT](./LICENSE) License. 395 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # Good Man in the Middle 2 | 3 | [![GitHub stars](https://img.shields.io/github/stars/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/stargazers) 4 | [![GitHub forks](https://img.shields.io/github/forks/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/network) 5 | [![Release](https://img.shields.io/github/release/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/releases) 6 | [![GitHub issues](https://img.shields.io/github/issues/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/issues) 7 | [![Build](https://github.com/zu1k/good-mitm/actions/workflows/build-test.yml/badge.svg)](https://github.com/zu1k/good-mitm/actions/workflows/build-test.yml) 8 | [![GitHub license](https://img.shields.io/github/license/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/blob/master/LICENSE) 9 | [![Docs](https://img.shields.io/badge/docs-read-blue.svg?style=flat)](https://good-mitm.zu1k.com/) 10 | 11 | #### [English](https://github.com/zu1k/good-mitm/blob/master/README.md) 12 | 13 | 基于规则的 MITM 引擎,支持 HTTP(S) 请求和返回的重写、重定向和阻断操作,支持 JavaScript 脚本介入 14 | 15 | ## 功能 16 | 17 | - 基于 TLS ClientHello 的自动证书签署 18 | - 支持选择性 MITM 19 | - 基于 YAML 格式的规则描述语言:重写/阻断/重定向 20 | - 灵活的规则匹配器 21 | - 域名前缀/后缀/全匹配 22 | - 正则匹配 23 | - 多筛选器规则 24 | - 灵活的文本内容改写 25 | - 抹除/替换 26 | - 正则替换 27 | - 灵活的字典类型内容改写 28 | - HTTP Header 改写 29 | - Cookie 改写 30 | - 支持单条规则多个行为 31 | - 支持 JavaScript 脚本规则 (编程介入) 32 | - 支持透明代理 33 | - 透明代理 HTTPS 和 HTTP 复用单端口 34 | - 支持自动安装 CA 证书到系统信任区 35 | 36 | ## 使用方法 37 | 38 | ### 证书准备 39 | 40 | 由于`MITM`技术的需要,需要你生成并信任自己的根证书 41 | 42 | #### 生成根证书 43 | 44 | 出于安全考虑,请不要随意信任任何陌生人提供的根证书,你需要自己生成属于自己的根证书和私钥 45 | 46 | 经验丰富的用户可以自行使用OpenSSL进行相关操作,考虑到没有相关经验的用户,可以使用以下命令直接生成相关内容,生成的证书和私钥将存储在`ca`目录下 47 | 48 | ```shell 49 | good-mitm.exe genca 50 | ``` 51 | 52 | 在浏览器使用了Good-MITM提供的代理后,通过访问 [http://cert.mitm.plus](http://cert.mitm.plus) 可以直接下载证书,这在给其他设备提供服务时非常有用 53 | 54 | #### 信任证书 55 | 56 | 你可以将根证书添加到操作系统或者浏览器的信任区中,根据你的需要自行选择 57 | 58 | ### 代理 59 | 60 | 启动Good-MITM,指定使用的规则文件或目录 61 | 62 | ```shell 63 | good-mitm.exe run -r rules 64 | ``` 65 | 66 | 在浏览器或操作系统中使用Good-MITM提供的http代理:`http://127.0.0.1:34567` 67 | 68 | #### 透明代理 69 | 70 | See https://docs.mitmproxy.org/stable/howto-transparent/ for docs. 71 | 72 | ```shell 73 | sudo sysctl -w net.ipv4.ip_forward=1 74 | sudo sysctl -w net.ipv6.conf.all.forwarding=1 75 | sudo sysctl -w net.ipv4.conf.all.send_redirects=0 76 | 77 | sudo useradd --create-home mitm 78 | sudo -u mitm -H bash -c 'good-mitm run -r rules/log.yaml -b 0.0.0.0:34567' 79 | 80 | sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 80 -j REDIRECT --to-port 34567 81 | sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 443 -j REDIRECT --to-port 34567 82 | sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 80 -j REDIRECT --to-port 34567 83 | sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 443 -j REDIRECT --to-port 34567 84 | ``` 85 | 86 | ## Rule 规则 87 | 88 | `Rule`用来操控 Good-MITM 89 | 90 | 一条合格的规则需要包含以下内容: 91 | 92 | - `规则名`:用来区分不同的规则,便与维护 93 | - [`筛选器`](#filter):用于从众多`请求`和`返回`中筛选出需要处理的内容 94 | - [`动作`](#action):用于执行想要的行为,包括`重定向`、`阻断`、`修改`等 95 | - 必要时指定需要MITM的域名 96 | 97 | ```yaml 98 | - name: "屏蔽Yutube追踪" 99 | mitm: "*.youtube.com" 100 | filter: 101 | url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 102 | action: reject 103 | ``` 104 | 105 | 同时一条合格的规则需要符合以下要求: 106 | 107 | - 专注:一条规则只用来做一件事 108 | - 简单:使用简单的方法来处理,便与维护 109 | - 高效:尽量使用高效的方法,比如使用域名后缀和域名前缀来替换域名正则表达式 110 | 111 | 112 | ### Filter 筛选器 113 | 114 | `Filter`用来筛选需要处理的请求和返回 115 | 116 | #### 候选项 117 | 118 | `Filter`目前包含以下类型: 119 | 120 | - All 121 | - Domain(String) 122 | - DomainKeyword(String) 123 | - DomainPrefix(String) 124 | - DomainSuffix(String) 125 | - UrlRegex(fancy_regex::Regex) 126 | 127 | > **注意** 128 | > 当前版本中,`domain`相关类型匹配的是`host`,通常情况下不会影响结果 129 | > 在网站使用非常规端口时,规则需要注明端口 130 | > 后续版本将会对此行为进行优化 131 | 132 | ##### All 全部 133 | 134 | 指定筛选器为`all`时将会命中全部请求和返回,通常用来执行日志记录行为 135 | 136 | ```yaml 137 | - name: "log" 138 | filter: all 139 | action: 140 | - log-req 141 | - log-res 142 | ``` 143 | 144 | ##### Domain 域名 145 | 146 | `domain`对域名进行全量匹配 147 | 148 | ```yaml 149 | - name: "redirect" 150 | filter: 151 | domain: 'none.zu1k.com' 152 | action: 153 | redirect: "https://zu1k.com/" 154 | ``` 155 | 156 | ##### DomainKeyword 域名关键词 157 | 158 | `domain-keyword`对域名进行关键词匹配 159 | 160 | ```yaml 161 | - name: "reject CSDN" 162 | filter: 163 | domain-keyword: 'csdn' 164 | action: reject 165 | ``` 166 | 167 | ##### DomainPrefix 域名前缀 168 | 169 | `domain-prefix`对域名进行前缀匹配 170 | 171 | ```yaml 172 | - name: "ad prefix" 173 | filter: 174 | domain-prefix: 'ads' // example: "ads.xxxxx.com" 175 | action: reject 176 | ``` 177 | 178 | ##### DomainSuffix 域名后缀 179 | 180 | `domain-suffix`对域名进行后缀匹配 181 | 182 | 183 | ```yaml 184 | - name: "redirect" 185 | filter: 186 | domain-suffix: 'google.com.cn' 187 | action: 188 | redirect: "https://google.com" 189 | ``` 190 | 191 | ##### UrlRegex Url正则 192 | 193 | `url-regex`对整个url进行正则匹配 194 | 195 | ```yaml 196 | - name: "youtube追踪" 197 | mitm: "*.youtube.com" 198 | filter: 199 | url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 200 | action: reject 201 | ``` 202 | 203 | #### 多个筛选器 204 | 205 | `filters`字段支持单个筛选器和多个筛选器,多个筛选器之间的关系为`或` 206 | 207 | ```yaml 208 | - name: "youtube-2" 209 | mitm: 210 | - "*.youtube.com" 211 | - "*.googlevideo.com" 212 | filters: 213 | - url-regex: '^https?:\/\/[\w-]+\.googlevideo\.com\/(?!(dclk_video_ads|videoplayback\?)).+(&oad|ctier)' 214 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/api\/stats\/ads' 215 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 216 | - url-regex: '^https?:\/\/\s.youtube.com/api/stats/qoe?.*adformat=' 217 | action: reject 218 | ``` 219 | 220 | 具有相同动作的多个规则可聚合为一个规则以便于维护 221 | 222 | ### Action 动作 223 | 224 | `Action` 用来对请求或者返回进行操作 225 | 226 | #### 候选项 227 | 228 | `Action`目前包含以下选项: 229 | 230 | - Reject 231 | - Redirect(String) 232 | - ModifyRequest(Modify) 233 | - ModifyResponse(Modify) 234 | - LogRes 235 | - LogReq 236 | 237 | ##### Reject 拒绝 238 | 239 | `reject`类型直接返回`502`,用来拒绝某些请求,可以用来拒绝追踪和广告 240 | 241 | ```yaml 242 | - name: "reject CSDN" 243 | filter: 244 | domain-keyword: 'csdn' 245 | action: reject 246 | ``` 247 | 248 | ##### Redirect 重定向 249 | 250 | `redirect`类型直接返回`302`重定向 251 | 252 | ```yaml 253 | - name: "youtube-1" 254 | filter: 255 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 256 | action: 257 | redirect: "$1$4" 258 | ``` 259 | 260 | ##### ModifyRequest 修改请求 261 | 262 | `modify-request`用来修改请求,具体修改规则见 [修改器](#modify) 263 | 264 | ##### ModifyResponse 修改返回 265 | 266 | `modify-response`用来修改返回,具体修改规则见 [修改器](#modify) 267 | 268 | ##### Log 记录日志 269 | 270 | `log-req` 用来记录请求,`log-res` 用来记录返回 271 | 272 | #### 多个动作 273 | 274 | `actions`字段支持单个动作和多个动作,当需要执行多个动作时,应使用数组 275 | 276 | ```yaml 277 | - name: "youtube-1" 278 | filter: 279 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 280 | actions: 281 | - log-req: 282 | - redirect: "$1$4" 283 | ``` 284 | 285 | ### 修改器 286 | 287 | 修改器用来执行修改操作,包括修改请求和修改返回 288 | 289 | #### 候选项 290 | 291 | 根据需要修改的内容的位置,修改器分为以下几类: 292 | 293 | - Header(MapModify) 294 | - Cookie(MapModify) 295 | - Body(TextModify) 296 | 297 | ##### TextModify 文本修改器 298 | 299 | `TextModify` 主要对文本就行修改,目前支持两种方式: 300 | 301 | - 直接设置文本内容 302 | - 普通替换或者正则替换 303 | 304 | ###### 直接设置 305 | 306 | 对于plain类型直接设置,内容将被直接重置为指定文本 307 | 308 | ```yaml 309 | - name: "modify response body plain" 310 | filter: 311 | domain: '126.com' 312 | action: 313 | modify-response: 314 | body: "Hello 126.com, from Good-MITM" 315 | ``` 316 | 317 | ###### 替换 318 | 319 | 替换支持简单替换和正则替换两种 320 | 321 | 简单替换 322 | 323 | ```yaml 324 | - name: "modify response body replace" 325 | filter: 326 | domain-suffix: '163.com' 327 | action: 328 | modify-response: 329 | body: 330 | origin: "网易首页" 331 | new: "Good-MITM 首页" 332 | ``` 333 | 334 | 正则替换 335 | 336 | ```yaml 337 | - name: "modify response body regex replace" 338 | filter: 339 | domain-suffix: 'zu1k.com' 340 | action: 341 | - modify-response: 342 | body: 343 | re: '(\d{4})' 344 | new: 'maybe $1' 345 | 346 | ``` 347 | 348 | ##### MapModify 字典修改器 349 | 350 | `MapModify` 字典修改器主要针对字典类型的位置进行修改,例如 `header` 和 `cookies` 351 | 352 | `key` 代表字典的键,必须指定 353 | 354 | `value` 是 `TextModify` 类型,按照上文方法书写 355 | 356 | 如果指定 `remove` 为 `true`,则会删除该键值对 357 | 358 | ```yaml 359 | - name: "modify response header" 360 | filter: 361 | domain: '126.com' 362 | action: 363 | - modify-response: 364 | header: 365 | key: date 366 | value: 367 | origin: "2022" 368 | new: "1999" 369 | - modify-response: 370 | header: 371 | key: new-header-item 372 | value: Good-MITM 373 | - modify-response: 374 | header: 375 | key: server 376 | remove: true 377 | ``` 378 | 379 | ##### Header 修改 380 | 381 | 见 `MapModify` 部分方法 382 | 383 | ##### Cookie 修改 384 | 385 | 与 Header 修改方法一致 386 | 387 | 如果指定 `remove` 为 `true` 还会同时对应的移除`set-cookie`项 388 | 389 | ##### Body修改 390 | 391 | 见 `TextModify` 部分 392 | 393 | ## License 394 | 395 | **Good-MITM** © [zu1k](https://github.com/zu1k), Released under the [MIT](./LICENSE) License. 396 | -------------------------------------------------------------------------------- /build/build-host-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BUILD_TARGET="" 4 | BUILD_FEATURES=() 5 | while getopts "t:f:" opt; do 6 | case $opt in 7 | t) 8 | BUILD_TARGET=$OPTARG 9 | ;; 10 | f) 11 | BUILD_FEATURES+=($OPTARG) 12 | ;; 13 | ?) 14 | echo "Usage: $(basename $0) [-t ] [-f ]" 15 | ;; 16 | esac 17 | done 18 | 19 | BUILD_FEATURES+=${BUILD_EXTRA_FEATURES} 20 | 21 | ROOT_DIR=$( cd $( dirname $0 ) && pwd ) 22 | VERSION=$(grep -E '^version' "${ROOT_DIR}/../Cargo.toml" | awk '{print $3}' | sed 's/"//g') 23 | HOST_TRIPLE=$(rustc -Vv | grep 'host:' | awk '{print $2}') 24 | 25 | echo "Started build release ${VERSION} for ${HOST_TRIPLE} (target: ${BUILD_TARGET}) with features \"${BUILD_FEATURES}\"..." 26 | 27 | if [[ "${BUILD_TARGET}" != "" ]]; then 28 | if [[ "${BUILD_FEATURES}" != "" ]]; then 29 | cargo build --release --features "${BUILD_FEATURES}" --target "${BUILD_TARGET}" 30 | else 31 | cargo build --release --target "${BUILD_TARGET}" 32 | fi 33 | else 34 | if [[ "${BUILD_FEATURES}" != "" ]]; then 35 | cargo build --release --features "${BUILD_FEATURES}" 36 | else 37 | cargo build --release 38 | fi 39 | fi 40 | 41 | if [[ "$?" != "0" ]]; then 42 | exit 1; 43 | fi 44 | 45 | if [[ "${BUILD_TARGET}" == "" ]]; then 46 | BUILD_TARGET=$HOST_TRIPLE 47 | fi 48 | 49 | TARGET_SUFFIX="" 50 | if [[ "${BUILD_TARGET}" == *"-windows-"* ]]; then 51 | TARGET_SUFFIX=".exe" 52 | fi 53 | 54 | TARGET="good-mitm${TARGET_SUFFIX}" 55 | 56 | RELEASE_FOLDER="${ROOT_DIR}/release" 57 | RELEASE_PACKAGE_NAME="good-mitm-${VERSION}-${BUILD_TARGET}" 58 | 59 | mkdir -p "${RELEASE_FOLDER}" 60 | 61 | # Into release folder 62 | if [[ "${BUILD_TARGET}" != "" ]]; then 63 | cd "${ROOT_DIR}/../target/${BUILD_TARGET}/release" 64 | else 65 | cd "${ROOT_DIR}/../target/release" 66 | fi 67 | 68 | if [[ "${BUILD_TARGET}" == *"-windows-"* ]]; then 69 | # For Windows, use zip 70 | 71 | RELEASE_PACKAGE_FILE_NAME="${RELEASE_PACKAGE_NAME}.zip" 72 | RELEASE_PACKAGE_FILE_PATH="${RELEASE_FOLDER}/${RELEASE_PACKAGE_FILE_NAME}" 73 | zip "${RELEASE_PACKAGE_FILE_PATH}" "${TARGET}" 74 | 75 | if [[ $? != "0" ]]; then 76 | exit 1 77 | fi 78 | 79 | # Checksum 80 | cd "${RELEASE_FOLDER}" 81 | shasum -a 256 "${RELEASE_PACKAGE_FILE_NAME}" > "${RELEASE_PACKAGE_FILE_NAME}.sha256" 82 | else 83 | # For others, Linux, OS X, uses tar.xz 84 | 85 | # For Darwin, .DS_Store and other related files should be ignored 86 | if [[ "$(uname -s)" == "Darwin" ]]; then 87 | export COPYFILE_DISABLE=1 88 | fi 89 | 90 | RELEASE_PACKAGE_FILE_NAME="${RELEASE_PACKAGE_NAME}.tar.xz" 91 | RELEASE_PACKAGE_FILE_PATH="${RELEASE_FOLDER}/${RELEASE_PACKAGE_FILE_NAME}" 92 | tar -cJf "${RELEASE_PACKAGE_FILE_PATH}" "${TARGET}" 93 | 94 | if [[ $? != "0" ]]; then 95 | exit 1 96 | fi 97 | 98 | # Checksum 99 | cd "${RELEASE_FOLDER}" 100 | shasum -a 256 "${RELEASE_PACKAGE_FILE_NAME}" > "${RELEASE_PACKAGE_FILE_NAME}.sha256" 101 | fi 102 | 103 | echo "Finished build release ${RELEASE_PACKAGE_FILE_PATH}" 104 | -------------------------------------------------------------------------------- /build/build-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CUR_DIR=$( cd $( dirname $0 ) && pwd ) 4 | VERSION=$(grep -E '^version' ${CUR_DIR}/../Cargo.toml | awk '{print $3}' | sed 's/"//g') 5 | 6 | ## Disable macos ACL file 7 | if [[ "$(uname -s)" == "Darwin" ]]; then 8 | export COPYFILE_DISABLE=1 9 | fi 10 | 11 | targets=() 12 | features=() 13 | use_upx=false 14 | 15 | while getopts "t:f:u" opt; do 16 | case $opt in 17 | t) 18 | targets+=($OPTARG) 19 | ;; 20 | f) 21 | features+=($OPTARG) 22 | ;; 23 | u) 24 | use_upx=true 25 | ;; 26 | ?) 27 | echo "Usage: $(basename $0) [-t ] [-f features] [-u]" 28 | ;; 29 | esac 30 | done 31 | 32 | features+=${EXTRA_FEATURES} 33 | 34 | if [[ "${#targets[@]}" == "0" ]]; then 35 | echo "Specifying compile target with -t " 36 | exit 1 37 | fi 38 | 39 | if [[ "${use_upx}" = true ]]; then 40 | if [[ -z "$upx" ]] && command -v upx &> /dev/null; then 41 | upx="upx -9" 42 | fi 43 | 44 | if [[ "x$upx" == "x" ]]; then 45 | echo "Couldn't find upx in PATH, consider specifying it with variable \$upx" 46 | exit 1 47 | fi 48 | fi 49 | 50 | 51 | function build() { 52 | cd "$CUR_DIR/.." 53 | 54 | TARGET=$1 55 | 56 | RELEASE_DIR="target/${TARGET}/release" 57 | TARGET_FEATURES="${features[@]}" 58 | 59 | if [[ "${TARGET_FEATURES}" != "" ]]; then 60 | echo "* Building ${TARGET} package ${VERSION} with features \"${TARGET_FEATURES}\" ..." 61 | 62 | cross build --target "${TARGET}" \ 63 | --features "${TARGET_FEATURES}" \ 64 | --release 65 | else 66 | echo "* Building ${TARGET} package ${VERSION} ..." 67 | 68 | cross build --target "${TARGET}" \ 69 | --release 70 | fi 71 | 72 | if [[ $? != "0" ]]; then 73 | exit 1 74 | fi 75 | 76 | PKG_DIR="${CUR_DIR}/release" 77 | mkdir -p "${PKG_DIR}" 78 | 79 | if [[ "$TARGET" == *"-linux-"* ]]; then 80 | PKG_NAME="good-mitm-${VERSION}-${TARGET}.tar.xz" 81 | PKG_PATH="${PKG_DIR}/${PKG_NAME}" 82 | 83 | cd ${RELEASE_DIR} 84 | 85 | if [[ "${use_upx}" = true ]]; then 86 | # Enable upx for MIPS. 87 | $upx good-mitm #>/dev/null 88 | fi 89 | 90 | echo "* Packaging XZ in ${PKG_PATH} ..." 91 | tar -cJf ${PKG_PATH} "good-mitm" 92 | 93 | if [[ $? != "0" ]]; then 94 | exit 1 95 | fi 96 | 97 | cd "${PKG_DIR}" 98 | shasum -a 256 "${PKG_NAME}" > "${PKG_NAME}.sha256" 99 | elif [[ "$TARGET" == *"-windows-"* ]]; then 100 | PKG_NAME="good-mitm-${VERSION}-${TARGET}.zip" 101 | PKG_PATH="${PKG_DIR}/${PKG_NAME}" 102 | 103 | echo "* Packaging ZIP in ${PKG_PATH} ..." 104 | cd ${RELEASE_DIR} 105 | zip ${PKG_PATH} "good-mitm.exe" 106 | 107 | if [[ $? != "0" ]]; then 108 | exit 1 109 | fi 110 | 111 | cd "${PKG_DIR}" 112 | shasum -a 256 "${PKG_NAME}" > "${PKG_NAME}.sha256" 113 | fi 114 | 115 | echo "* Done build package ${PKG_NAME}" 116 | } 117 | 118 | for target in "${targets[@]}"; do 119 | build "$target"; 120 | done 121 | -------------------------------------------------------------------------------- /crates/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "good-mitm-core" 3 | version = "0.4.1" 4 | edition = "2021" 5 | description = "Use MITM technology to provide features like rewrite, redirect." 6 | homepage = "https://github.com/zu1k/good-mitm" 7 | repository = "https://github.com/zu1k/good-mitm" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | async-trait = "0.1" 12 | bytes = { version = "1", features = ["serde"] } 13 | byteorder = "1.4" 14 | cfg-if = "1" 15 | http = "0.2" 16 | hyper = { version = "0.14", features = ["http1", "http2", "server", "stream", "tcp", "runtime"] } 17 | hyper-proxy = { version = "0.9" } 18 | hyper-rustls = { version = "0.24" } 19 | hyper-tls = { version = "0.5", optional = true } 20 | log = "0.4" 21 | moka = { version = "0.11", features = ["future"] } 22 | openssl = { version = "0.10", features = ["vendored"], optional = true } 23 | pin-project = "1" 24 | rcgen = { version = "0.10", features = ["x509-parser"] } 25 | serde = { version = "1.0", features = ["derive"] } 26 | thiserror = "1" 27 | time = "0.3" 28 | typed-builder = "0.14" 29 | tokio = { version = "1", features = ["rt"] } 30 | tokio-rustls = { version = "0.24", default-features = false, features = ["tls12"] } 31 | tokio-util = { version = "0.7", features = ["io"] } 32 | wildmatch = "2.1" 33 | rustls = { version = "0.21", features = ["dangerous_configuration"] } 34 | rand = "0.8" 35 | 36 | [features] 37 | default = ["h2", "request-native-tls"] 38 | request-native-tls = ["hyper-tls", "openssl"] 39 | h2 = ["hyper-rustls/http2"] 40 | -------------------------------------------------------------------------------- /crates/core/src/ca.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use moka::sync::Cache; 3 | use rand::{thread_rng, Rng}; 4 | use rcgen::{ 5 | BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, 6 | ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, RcgenError, SanType, 7 | }; 8 | use std::sync::Arc; 9 | use time::{ext::NumericalDuration, OffsetDateTime}; 10 | use tokio_rustls::rustls::{ 11 | server::{ClientHello, ResolvesServerCert}, 12 | sign::CertifiedKey, 13 | ServerConfig, 14 | }; 15 | 16 | const CERT_TTL_DAYS: u64 = 365; 17 | const CERT_CACHE_TTL_SECONDS: u64 = CERT_TTL_DAYS * 24 * 60 * 60 / 2; 18 | 19 | /// Issues certificates for use when communicating with clients. 20 | /// 21 | /// Issues certificates for communicating with clients over TLS. Certificates are cached in memory 22 | /// up to a max size that is provided when creating the authority. Clients should be configured to 23 | /// either trust the provided root certificate, or to ignore certificate errors. 24 | #[derive(Clone)] 25 | pub struct CertificateAuthority { 26 | private_key: rustls::PrivateKey, 27 | ca_cert: rustls::Certificate, 28 | ca_cert_string: String, 29 | cache: Cache>, 30 | } 31 | 32 | impl CertificateAuthority { 33 | pub fn gen_ca() -> Result { 34 | let mut params = CertificateParams::default(); 35 | let mut distinguished_name = DistinguishedName::new(); 36 | distinguished_name.push(DnType::CommonName, "Good-MITM"); 37 | distinguished_name.push(DnType::OrganizationName, "Good-MITM"); 38 | distinguished_name.push(DnType::CountryName, "CN"); 39 | distinguished_name.push(DnType::LocalityName, "CN"); 40 | params.distinguished_name = distinguished_name; 41 | params.key_usages = vec![ 42 | KeyUsagePurpose::DigitalSignature, 43 | KeyUsagePurpose::KeyCertSign, 44 | KeyUsagePurpose::CrlSign, 45 | ]; 46 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 47 | Certificate::from_params(params) 48 | } 49 | 50 | /// Attempts to create a new certificate authority. 51 | /// 52 | /// This will fail if the provided key or certificate is invalid, or if the key does not match 53 | /// the certificate. 54 | pub fn new( 55 | private_key: rustls::PrivateKey, 56 | ca_cert: rustls::Certificate, 57 | ca_cert_string: String, 58 | cache_size: u64, 59 | ) -> Result { 60 | let ca = CertificateAuthority { 61 | private_key, 62 | ca_cert, 63 | ca_cert_string, 64 | cache: Cache::builder() 65 | .max_capacity(cache_size) 66 | .time_to_live(std::time::Duration::from_secs(CERT_CACHE_TTL_SECONDS)) 67 | .build(), 68 | }; 69 | 70 | ca.validate()?; 71 | Ok(ca) 72 | } 73 | 74 | pub(crate) fn get_certified_key(&self, server_name: &str) -> Arc { 75 | if let Some(server_cfg) = self.cache.get(server_name) { 76 | return server_cfg; 77 | } 78 | 79 | let certs = vec![self.gen_cert(server_name)]; 80 | let key = rustls::sign::any_supported_type(&self.private_key) 81 | .expect("parse any supported private key"); 82 | let certified_key = Arc::new(CertifiedKey::new(certs, key)); 83 | 84 | self.cache 85 | .insert(server_name.to_string(), certified_key.clone()); 86 | 87 | certified_key 88 | } 89 | 90 | fn gen_cert(&self, server_name: &str) -> rustls::Certificate { 91 | let mut params = rcgen::CertificateParams::default(); 92 | 93 | params.serial_number = Some(thread_rng().gen::()); 94 | params.not_before = OffsetDateTime::now_utc().saturating_sub(1.days()); 95 | params.not_after = OffsetDateTime::now_utc().saturating_add((CERT_TTL_DAYS as i64).days()); 96 | params 97 | .subject_alt_names 98 | .push(SanType::DnsName(server_name.to_string())); 99 | let mut distinguished_name = DistinguishedName::new(); 100 | distinguished_name.push(DnType::CommonName, server_name); 101 | params.distinguished_name = distinguished_name; 102 | 103 | params.key_usages = vec![KeyUsagePurpose::DigitalSignature]; 104 | params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; 105 | 106 | let key_pair = KeyPair::from_der(&self.private_key.0).expect("Failed to parse private key"); 107 | params.alg = key_pair 108 | .compatible_algs() 109 | .next() 110 | .expect("Failed to find compatible algorithm"); 111 | params.key_pair = Some(key_pair); 112 | 113 | let key_pair = KeyPair::from_der(&self.private_key.0).expect("Failed to parse private key"); 114 | 115 | let ca_cert_params = rcgen::CertificateParams::from_ca_cert_der(&self.ca_cert.0, key_pair) 116 | .expect("Failed to parse CA certificate"); 117 | let ca_cert = rcgen::Certificate::from_params(ca_cert_params) 118 | .expect("Failed to generate CA certificate"); 119 | 120 | let cert = rcgen::Certificate::from_params(params).expect("Failed to generate certificate"); 121 | 122 | rustls::Certificate( 123 | cert.serialize_der_with_signer(&ca_cert) 124 | .expect("Failed to serialize certificate"), 125 | ) 126 | } 127 | 128 | fn validate(&self) -> Result<(), RcgenError> { 129 | let key_pair = rcgen::KeyPair::from_der(&self.private_key.0)?; 130 | rcgen::CertificateParams::from_ca_cert_der(&self.ca_cert.0, key_pair)?; 131 | Ok(()) 132 | } 133 | 134 | pub fn get_cert(&self) -> String { 135 | self.ca_cert_string.clone() 136 | } 137 | 138 | pub fn gen_server_config(self: Arc) -> Arc { 139 | let server_cfg = ServerConfig::builder() 140 | .with_safe_defaults() 141 | .with_no_client_auth() 142 | .with_cert_resolver(self); 143 | Arc::new(server_cfg) 144 | } 145 | } 146 | 147 | impl ResolvesServerCert for CertificateAuthority { 148 | fn resolve(&self, client_hello: ClientHello) -> Option> { 149 | client_hello 150 | .server_name() 151 | .map(|name| self.get_certified_key(name)) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /crates/core/src/error.rs: -------------------------------------------------------------------------------- 1 | use rcgen::RcgenError; 2 | use std::io; 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error("invalid CA")] 8 | Tls(#[from] RcgenError), 9 | #[error("network error")] 10 | HyperError(#[from] hyper::Error), 11 | #[error("TlsConnector error")] 12 | TlsConnectorError(#[from] hyper_tls::native_tls::Error), 13 | #[error("IO error")] 14 | IO(#[from] io::Error), 15 | #[error("unable to decode response body")] 16 | Decode, 17 | #[error("unknown error")] 18 | Unknown, 19 | } 20 | -------------------------------------------------------------------------------- /crates/core/src/handler.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use hyper::{Body, Request, Response}; 3 | use std::{ 4 | marker::PhantomData, 5 | sync::{Arc, RwLock}, 6 | }; 7 | use wildmatch::WildMatch; 8 | 9 | use crate::mitm::{HttpContext, RequestOrResponse}; 10 | 11 | pub trait CustomContextData: Clone + Default + Send + Sync + 'static {} 12 | 13 | #[async_trait] 14 | pub trait HttpHandler: Clone + Send + Sync + 'static { 15 | async fn handle_request( 16 | &self, 17 | _ctx: &mut HttpContext, 18 | req: Request, 19 | ) -> RequestOrResponse { 20 | RequestOrResponse::Request(req) 21 | } 22 | 23 | async fn handle_response( 24 | &self, 25 | _ctx: &mut HttpContext, 26 | res: Response, 27 | ) -> Response { 28 | res 29 | } 30 | } 31 | 32 | #[derive(Clone, Default)] 33 | pub struct MitmFilter { 34 | filters: Arc>>, 35 | 36 | _custom_contex_data: PhantomData, 37 | } 38 | 39 | impl MitmFilter { 40 | pub fn new(filters: Vec) -> Self { 41 | let filters = filters.iter().map(|f| WildMatch::new(f)).collect(); 42 | Self { 43 | filters: Arc::new(RwLock::new(filters)), 44 | ..Default::default() 45 | } 46 | } 47 | 48 | pub async fn filter_req(&self, _ctx: &HttpContext, req: &Request) -> bool { 49 | let host = req.uri().host().unwrap_or_default(); 50 | let list = self.filters.read().unwrap(); 51 | for m in list.iter() { 52 | if m.matches(host) { 53 | return true; 54 | } 55 | } 56 | false 57 | } 58 | 59 | pub async fn filter(&self, host: &str) -> bool { 60 | let list = self.filters.read().unwrap(); 61 | for m in list.iter() { 62 | if m.matches(host) { 63 | return true; 64 | } 65 | } 66 | false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /crates/core/src/http_client.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use hyper::{client::HttpConnector, Client}; 3 | use hyper_proxy::{Proxy as UpstreamProxy, ProxyConnector}; 4 | use rustls::client::{ServerCertVerified, ServerCertVerifier}; 5 | use std::time::SystemTime; 6 | 7 | cfg_if::cfg_if! { 8 | if #[cfg(feature = "request-native-tls")] { 9 | use hyper_tls::{HttpsConnector, native_tls::TlsConnector}; 10 | } else { 11 | use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; 12 | use rustls::ClientConfig; 13 | use std::sync::Arc; 14 | } 15 | } 16 | 17 | #[derive(Clone)] 18 | pub enum HttpClient { 19 | Proxy(Client>>), 20 | Https(Client>), 21 | } 22 | 23 | pub fn gen_client(upstream_proxy: Option) -> Result { 24 | cfg_if::cfg_if! { 25 | if #[cfg(feature = "request-native-tls")] { 26 | let https = { 27 | let tls = TlsConnector::builder() 28 | .danger_accept_invalid_certs(true) 29 | .danger_accept_invalid_hostnames(true) 30 | .disable_built_in_roots(true) 31 | .build()?; 32 | let mut http = HttpConnector::new(); 33 | http.enforce_http(false); 34 | HttpsConnector::from((http, tls.into())) 35 | }; 36 | } else { 37 | let https = { 38 | let https_builder = HttpsConnectorBuilder::new() 39 | .with_tls_config({ 40 | let cert_resolver = Arc::new(TrustAllCertVerifier::default()); 41 | ClientConfig::builder() 42 | .with_safe_defaults() 43 | .with_custom_certificate_verifier(cert_resolver) 44 | .with_no_client_auth() 45 | }) 46 | .https_or_http() 47 | .enable_http1(); 48 | #[cfg(feature = "h2")] 49 | let https_builder = https_builder.enable_http2(); 50 | 51 | https_builder.build() 52 | }; 53 | } 54 | } 55 | 56 | if let Some(proxy) = upstream_proxy { 57 | let connector = ProxyConnector::from_proxy(https, proxy)?; 58 | return Ok(HttpClient::Proxy( 59 | Client::builder() 60 | .http1_title_case_headers(true) 61 | .http1_preserve_header_case(true) 62 | .build(connector), 63 | )); 64 | } else { 65 | Ok(HttpClient::Https( 66 | Client::builder() 67 | .http1_title_case_headers(true) 68 | .http1_preserve_header_case(true) 69 | .build(https), 70 | )) 71 | } 72 | } 73 | 74 | #[derive(Default)] 75 | struct TrustAllCertVerifier; 76 | 77 | impl ServerCertVerifier for TrustAllCertVerifier { 78 | fn verify_server_cert( 79 | &self, 80 | _end_entity: &rustls::Certificate, 81 | _intermediates: &[rustls::Certificate], 82 | _server_name: &rustls::ServerName, 83 | _scts: &mut dyn Iterator, 84 | _ocsp_response: &[u8], 85 | _n_ow: SystemTime, 86 | ) -> Result { 87 | Ok(ServerCertVerified::assertion()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use error::Error; 2 | use handler::{CustomContextData, HttpHandler, MitmFilter}; 3 | use http_client::gen_client; 4 | use hyper_proxy::Proxy as UpstreamProxy; 5 | use mitm::MitmProxy; 6 | use std::{future::Future, marker::PhantomData, net::SocketAddr, sync::Arc}; 7 | use tokio::net::TcpListener; 8 | use typed_builder::TypedBuilder; 9 | 10 | pub use ca::CertificateAuthority; 11 | pub use hyper; 12 | pub use rcgen; 13 | pub use tokio_rustls; 14 | 15 | mod ca; 16 | mod error; 17 | pub mod handler; 18 | mod http_client; 19 | pub mod mitm; 20 | mod sni_reader; 21 | 22 | #[derive(TypedBuilder)] 23 | pub struct Proxy 24 | where 25 | F: Future, 26 | H: HttpHandler, 27 | D: CustomContextData, 28 | { 29 | /// The address to listen on. 30 | pub listen_addr: SocketAddr, 31 | /// A future that once resolved will cause the proxy server to shut down. 32 | pub shutdown_signal: F, 33 | /// The certificate authority to use. 34 | pub ca: CertificateAuthority, 35 | pub upstream_proxy: Option, 36 | 37 | pub mitm_filters: Vec, 38 | pub handler: H, 39 | 40 | #[builder(default)] 41 | _custom_contex_data: PhantomData, 42 | } 43 | 44 | impl Proxy 45 | where 46 | F: Future, 47 | H: HttpHandler, 48 | D: CustomContextData, 49 | { 50 | pub async fn start_proxy(self) -> Result<(), Error> { 51 | let client = gen_client(self.upstream_proxy)?; 52 | let ca = Arc::new(self.ca); 53 | let http_handler = Arc::new(self.handler); 54 | let mitm_filter = Arc::new(MitmFilter::new(self.mitm_filters)); 55 | 56 | let tcp_listener = TcpListener::bind(self.listen_addr).await?; 57 | loop { 58 | let client = client.clone(); 59 | let ca = Arc::clone(&ca); 60 | let http_handler = Arc::clone(&http_handler); 61 | let mitm_filter = Arc::clone(&mitm_filter); 62 | 63 | if let Ok((tcp_stream, _)) = tcp_listener.accept().await { 64 | tokio::spawn(async move { 65 | let mitm_proxy = MitmProxy { 66 | ca: ca.clone(), 67 | client: client.clone(), 68 | http_handler: Arc::clone(&http_handler), 69 | mitm_filter: Arc::clone(&mitm_filter), 70 | custom_contex_data: Default::default(), 71 | }; 72 | 73 | let mut tls_content_type = [0; 1]; 74 | if tcp_stream.peek(&mut tls_content_type).await.is_ok() { 75 | if tls_content_type[0] <= 0x40 { 76 | // ASCII < 'A', assuming tls 77 | mitm_proxy.serve_tls(tcp_stream).await; 78 | } else { 79 | // assuming http 80 | _ = mitm_proxy.serve_stream(tcp_stream).await; 81 | } 82 | } 83 | }); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/core/src/mitm.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | ca::CertificateAuthority, 3 | handler::{CustomContextData, HttpHandler, MitmFilter}, 4 | http_client::HttpClient, 5 | sni_reader::{ 6 | read_sni_host_name_from_client_hello, HandshakeRecordReader, PrefixedReaderWriter, 7 | RecordingBufReader, 8 | }, 9 | }; 10 | use http::{header, uri::Scheme, HeaderValue, Uri}; 11 | use hyper::{ 12 | body::HttpBody, server::conn::Http, service::service_fn, Body, Method, Request, Response, 13 | }; 14 | use log::*; 15 | use std::{marker::PhantomData, sync::Arc, time::Duration}; 16 | use tokio::{ 17 | io::{AsyncRead, AsyncWrite}, 18 | net::TcpStream, 19 | pin, 20 | }; 21 | use tokio_rustls::TlsAcceptor; 22 | 23 | /// Enum representing either an HTTP request or response. 24 | #[derive(Debug)] 25 | pub enum RequestOrResponse { 26 | Request(Request), 27 | Response(Response), 28 | } 29 | 30 | /// Context for HTTP requests and responses. 31 | #[derive(Default, Debug)] 32 | pub struct HttpContext { 33 | pub uri: Option, 34 | 35 | pub should_modify_response: bool, 36 | pub custom_data: D, 37 | } 38 | 39 | #[derive(Clone)] 40 | pub(crate) struct MitmProxy 41 | where 42 | H: HttpHandler, 43 | D: CustomContextData, 44 | { 45 | pub ca: Arc, 46 | pub client: HttpClient, 47 | 48 | pub http_handler: Arc, 49 | pub mitm_filter: Arc>, 50 | 51 | pub custom_contex_data: PhantomData, 52 | } 53 | 54 | impl MitmProxy 55 | where 56 | H: HttpHandler, 57 | D: CustomContextData, 58 | { 59 | pub(crate) async fn proxy_req( 60 | self, 61 | req: Request, 62 | ) -> Result, hyper::Error> { 63 | let res = if req.method() == Method::CONNECT { 64 | self.process_connect(req).await 65 | } else { 66 | self.process_request(req, Scheme::HTTP).await 67 | }; 68 | 69 | match res { 70 | Ok(mut res) => { 71 | allow_all_cros(&mut res); 72 | Ok(res) 73 | } 74 | Err(err) => { 75 | error!("proxy request failed: {err:?}"); 76 | Err(err) 77 | } 78 | } 79 | } 80 | 81 | async fn process_request( 82 | self, 83 | mut req: Request, 84 | scheme: Scheme, 85 | ) -> Result, hyper::Error> { 86 | if req.uri().path().starts_with("/mitm/cert") { 87 | return Ok(self.get_cert_res()); 88 | } 89 | 90 | let mut ctx = HttpContext { 91 | uri: None, 92 | should_modify_response: false, 93 | ..Default::default() 94 | }; 95 | 96 | // if req.uri().authority().is_none() { 97 | if req.version() == http::Version::HTTP_10 || req.version() == http::Version::HTTP_11 { 98 | let (mut parts, body) = req.into_parts(); 99 | 100 | if let Some(Ok(authority)) = parts 101 | .headers 102 | .get(http::header::HOST) 103 | .map(|host| host.to_str()) 104 | { 105 | let mut uri = parts.uri.into_parts(); 106 | uri.scheme = Some(scheme.clone()); 107 | uri.authority = authority.try_into().ok(); 108 | parts.uri = Uri::from_parts(uri).expect("build uri"); 109 | } 110 | 111 | req = Request::from_parts(parts, body); 112 | }; 113 | // } 114 | 115 | let mut req = match self.http_handler.handle_request(&mut ctx, req).await { 116 | RequestOrResponse::Request(req) => req, 117 | RequestOrResponse::Response(res) => return Ok(res), 118 | }; 119 | 120 | { 121 | let header_mut = req.headers_mut(); 122 | header_mut.remove(http::header::HOST); 123 | header_mut.remove(http::header::ACCEPT_ENCODING); 124 | header_mut.remove(http::header::CONTENT_LENGTH); 125 | } 126 | 127 | let res = match self.client { 128 | HttpClient::Proxy(client) => client.request(req).await?, 129 | HttpClient::Https(client) => client.request(req).await?, 130 | }; 131 | 132 | let mut res = self.http_handler.handle_response(&mut ctx, res).await; 133 | let length = res.size_hint().lower(); 134 | 135 | { 136 | let header_mut = res.headers_mut(); 137 | 138 | if let Some(content_length) = header_mut.get_mut(http::header::CONTENT_LENGTH) { 139 | *content_length = HeaderValue::from_str(&length.to_string()).unwrap(); 140 | } 141 | 142 | // Remove `Strict-Transport-Security` to avoid HSTS 143 | // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 144 | header_mut.remove(header::STRICT_TRANSPORT_SECURITY); 145 | } 146 | 147 | Ok(res) 148 | } 149 | 150 | async fn process_connect(self, req: Request) -> Result, hyper::Error> { 151 | let ctx = HttpContext { 152 | uri: None, 153 | should_modify_response: false, 154 | ..Default::default() 155 | }; 156 | 157 | if self.mitm_filter.filter_req(&ctx, &req).await { 158 | tokio::task::spawn(async move { 159 | let authority = req 160 | .uri() 161 | .authority() 162 | .expect("URI does not contain authority") 163 | .clone(); 164 | 165 | match hyper::upgrade::on(req).await { 166 | Ok(upgraded) => { 167 | self.serve_tls(upgraded).await; 168 | } 169 | Err(e) => debug!("upgrade error for {}: {}", authority, e), 170 | }; 171 | }); 172 | } else { 173 | tokio::task::spawn(async move { 174 | let remote_addr = host_addr(req.uri()).unwrap(); 175 | let upgraded = hyper::upgrade::on(req).await.unwrap(); 176 | tunnel(upgraded, remote_addr).await 177 | }); 178 | } 179 | Ok(Response::new(Body::empty())) 180 | } 181 | 182 | pub async fn serve_tls( 183 | self, 184 | mut stream: IO, 185 | ) { 186 | // Read SNI hostname. 187 | let mut recording_reader = RecordingBufReader::new(&mut stream); 188 | let reader = HandshakeRecordReader::new(&mut recording_reader); 189 | pin!(reader); 190 | let sni_hostname = tokio::time::timeout( 191 | Duration::from_secs(5), 192 | read_sni_host_name_from_client_hello(reader), 193 | ) 194 | .await 195 | .unwrap() 196 | .unwrap(); 197 | 198 | let read_buf = recording_reader.buf(); 199 | let client_stream = PrefixedReaderWriter::new(stream, read_buf); 200 | 201 | if !self.mitm_filter.filter(&sni_hostname).await { 202 | let remote_addr = format!("{sni_hostname}:443"); 203 | tokio::task::spawn(async move { tunnel(client_stream, remote_addr).await }); 204 | return; 205 | } 206 | 207 | let server_config = self.ca.clone().gen_server_config(); 208 | 209 | match TlsAcceptor::from(server_config).accept(client_stream).await { 210 | Ok(stream) => { 211 | if let Err(e) = Http::new() 212 | .http1_preserve_header_case(true) 213 | .http1_title_case_headers(true) 214 | .serve_connection( 215 | stream, 216 | service_fn(|req| self.clone().process_request(req, Scheme::HTTPS)), 217 | ) 218 | .with_upgrades() 219 | .await 220 | { 221 | let e_string = e.to_string(); 222 | if !e_string.starts_with("error shutting down connection") { 223 | debug!("res:: {}", e); 224 | } 225 | } 226 | } 227 | Err(err) => { 228 | error!("Tls accept failed: {err}") 229 | } 230 | } 231 | } 232 | 233 | pub async fn serve_stream(self, stream: S) -> Result<(), hyper::Error> 234 | where 235 | S: AsyncRead + AsyncWrite + Unpin + Send + 'static, 236 | { 237 | Http::new() 238 | .http1_preserve_header_case(true) 239 | .http1_title_case_headers(true) 240 | .serve_connection(stream, service_fn(|req| self.clone().proxy_req(req))) 241 | .with_upgrades() 242 | .await 243 | } 244 | 245 | fn get_cert_res(&self) -> hyper::Response { 246 | Response::builder() 247 | .header( 248 | http::header::CONTENT_DISPOSITION, 249 | "attachment; filename=good-mitm.crt", 250 | ) 251 | .header(http::header::CONTENT_TYPE, "application/octet-stream") 252 | .status(http::StatusCode::OK) 253 | .body(Body::from(self.ca.clone().get_cert())) 254 | .unwrap() 255 | } 256 | } 257 | 258 | fn allow_all_cros(res: &mut Response) { 259 | let header_mut = res.headers_mut(); 260 | let all = HeaderValue::from_str("*").unwrap(); 261 | header_mut.insert(http::header::ACCESS_CONTROL_ALLOW_ORIGIN, all.clone()); 262 | header_mut.insert(http::header::ACCESS_CONTROL_ALLOW_METHODS, all.clone()); 263 | header_mut.insert(http::header::ACCESS_CONTROL_ALLOW_METHODS, all); 264 | } 265 | 266 | fn host_addr(uri: &http::Uri) -> Option { 267 | uri.authority().map(|auth| auth.to_string()) 268 | } 269 | 270 | async fn tunnel(mut client_stream: A, addr: String) -> std::io::Result<()> 271 | where 272 | A: AsyncRead + AsyncWrite + Unpin, 273 | { 274 | let mut server = TcpStream::connect(addr).await?; 275 | tokio::io::copy_bidirectional(&mut client_stream, &mut server).await?; 276 | Ok(()) 277 | } 278 | -------------------------------------------------------------------------------- /crates/core/src/sni_reader.rs: -------------------------------------------------------------------------------- 1 | /// from https://github.com/branlwyd/rspd/blob/master/src/main.rs 2 | use byteorder::{ByteOrder, NetworkEndian}; 3 | use pin_project::pin_project; 4 | use std::{ 5 | cmp::min, 6 | io::ErrorKind, 7 | mem, 8 | pin::Pin, 9 | task::{Context, Poll}, 10 | }; 11 | use tokio::{ 12 | io::{self, AsyncRead, AsyncReadExt, AsyncWrite, Error, ReadBuf}, 13 | pin, 14 | }; 15 | 16 | #[pin_project] 17 | pub struct RecordingBufReader { 18 | #[pin] 19 | reader: R, 20 | buf: Vec, 21 | read_offset: usize, 22 | } 23 | 24 | const RECORDING_READER_BUF_SIZE: usize = 1 << 10; // 1 KiB 25 | 26 | impl RecordingBufReader { 27 | pub fn new(reader: R) -> RecordingBufReader { 28 | RecordingBufReader { 29 | reader, 30 | buf: Vec::with_capacity(RECORDING_READER_BUF_SIZE), 31 | read_offset: 0, 32 | } 33 | } 34 | 35 | pub fn buf(self) -> Vec { 36 | self.buf 37 | } 38 | } 39 | 40 | impl AsyncRead for RecordingBufReader { 41 | fn poll_read( 42 | self: Pin<&mut Self>, 43 | cx: &mut Context<'_>, 44 | caller_buf: &mut ReadBuf<'_>, 45 | ) -> Poll> { 46 | // if we don't have any buffered bytes, read some bytes into our buffer. 47 | let mut this = self.project(); 48 | if *this.read_offset == this.buf.len() { 49 | this.buf.reserve(RECORDING_READER_BUF_SIZE); 50 | let mut read_buf = ReadBuf::uninit(this.buf.spare_capacity_mut()); 51 | match this.reader.as_mut().poll_read(cx, &mut read_buf) { 52 | Poll::Ready(Ok(())) => { 53 | let bytes_read = read_buf.filled().len(); 54 | let new_len = this.buf.len() + bytes_read; 55 | unsafe { 56 | this.buf.set_len(new_len); // lol 57 | } 58 | } 59 | rslt => return rslt, 60 | }; 61 | } 62 | 63 | // read from the buffered bytes into the caller's buffer. 64 | let unread_bytes = &this.buf[*this.read_offset..]; 65 | let n = min(caller_buf.remaining(), unread_bytes.len()); 66 | caller_buf.put_slice(&unread_bytes[..n]); 67 | *this.read_offset += n; 68 | Poll::Ready(Ok(())) 69 | } 70 | } 71 | 72 | #[pin_project] 73 | pub struct PrefixedReaderWriter { 74 | #[pin] 75 | inner: T, 76 | prefix: Vec, 77 | prefix_read_offset: usize, 78 | } 79 | 80 | impl PrefixedReaderWriter { 81 | pub fn new(inner: T, prefix: Vec) -> PrefixedReaderWriter { 82 | PrefixedReaderWriter { 83 | inner, 84 | prefix, 85 | prefix_read_offset: 0, 86 | } 87 | } 88 | } 89 | 90 | impl AsyncRead for PrefixedReaderWriter { 91 | fn poll_read( 92 | self: Pin<&mut Self>, 93 | cx: &mut Context<'_>, 94 | buf: &mut ReadBuf<'_>, 95 | ) -> Poll> { 96 | let this = self.project(); 97 | if this.prefix.is_empty() { 98 | return this.inner.poll_read(cx, buf); 99 | } 100 | 101 | let prefix = &this.prefix[*this.prefix_read_offset..]; 102 | let n = min(buf.remaining(), prefix.len()); 103 | buf.put_slice(&prefix[..n]); 104 | *this.prefix_read_offset += n; 105 | 106 | if *this.prefix_read_offset == this.prefix.len() { 107 | mem::take(this.prefix); 108 | } 109 | 110 | Poll::Ready(Ok(())) 111 | } 112 | } 113 | 114 | impl AsyncWrite for PrefixedReaderWriter { 115 | fn poll_write( 116 | self: Pin<&mut Self>, 117 | cx: &mut Context<'_>, 118 | buf: &[u8], 119 | ) -> Poll> { 120 | let this = self.project(); 121 | this.inner.poll_write(cx, buf) 122 | } 123 | 124 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 125 | let this = self.project(); 126 | this.inner.poll_flush(cx) 127 | } 128 | 129 | fn poll_shutdown( 130 | self: Pin<&mut Self>, 131 | cx: &mut Context<'_>, 132 | ) -> Poll> { 133 | let this = self.project(); 134 | this.inner.poll_shutdown(cx) 135 | } 136 | } 137 | 138 | #[pin_project] 139 | pub struct HandshakeRecordReader { 140 | #[pin] 141 | reader: R, 142 | currently_reading: HandshakeRecordReaderReading, 143 | } 144 | 145 | impl HandshakeRecordReader { 146 | pub fn new(reader: R) -> HandshakeRecordReader { 147 | HandshakeRecordReader { 148 | reader, 149 | currently_reading: HandshakeRecordReaderReading::ContentType, 150 | } 151 | } 152 | } 153 | 154 | enum HandshakeRecordReaderReading { 155 | ContentType, 156 | MajorMinorVersion(usize), 157 | RecordSize([u8; 2], usize), 158 | Record(usize), 159 | } 160 | 161 | impl AsyncRead for HandshakeRecordReader { 162 | fn poll_read( 163 | self: Pin<&mut Self>, 164 | cx: &mut Context<'_>, 165 | caller_buf: &mut ReadBuf<'_>, 166 | ) -> Poll> { 167 | let mut this = self.project(); 168 | loop { 169 | match this.currently_reading { 170 | HandshakeRecordReaderReading::ContentType => { 171 | const CONTENT_TYPE_HANDSHAKE: u8 = 22; 172 | let mut buf = [0]; 173 | let mut buf = ReadBuf::new(&mut buf[..]); 174 | match this.reader.as_mut().poll_read(cx, &mut buf) { 175 | Poll::Ready(Ok(())) if buf.filled().len() == 1 => { 176 | if buf.filled()[0] != CONTENT_TYPE_HANDSHAKE { 177 | return Poll::Ready(Err(io::Error::new( 178 | io::ErrorKind::InvalidData, 179 | format!( 180 | "got wrong content type (wanted {}, got {})", 181 | CONTENT_TYPE_HANDSHAKE, 182 | buf.filled()[0] 183 | ), 184 | ))); 185 | } 186 | *this.currently_reading = 187 | HandshakeRecordReaderReading::MajorMinorVersion(0); 188 | } 189 | rslt => return rslt, 190 | } 191 | } 192 | 193 | HandshakeRecordReaderReading::MajorMinorVersion(bytes_read) => { 194 | let mut buf = [0, 0]; 195 | let mut buf = ReadBuf::new(&mut buf[..]); 196 | buf.advance(*bytes_read); 197 | match this.reader.as_mut().poll_read(cx, &mut buf) { 198 | Poll::Ready(Ok(())) => { 199 | *bytes_read = buf.filled().len(); 200 | if *bytes_read == 2 { 201 | *this.currently_reading = 202 | HandshakeRecordReaderReading::RecordSize([0, 0], 0); 203 | } 204 | } 205 | rslt => return rslt, 206 | } 207 | } 208 | 209 | HandshakeRecordReaderReading::RecordSize(backing_array, bytes_read) => { 210 | const MAX_RECORD_SIZE: usize = 1 << 14; 211 | let mut buf = ReadBuf::new(&mut backing_array[..]); 212 | buf.advance(*bytes_read); 213 | match this.reader.as_mut().poll_read(cx, &mut buf) { 214 | Poll::Ready(Ok(())) => { 215 | *bytes_read = buf.filled().len(); 216 | if *bytes_read == 2 { 217 | let record_size = u16::from_be_bytes(*backing_array).into(); 218 | if record_size > MAX_RECORD_SIZE { 219 | return Poll::Ready(Err(io::Error::new( 220 | io::ErrorKind::InvalidData, 221 | format!( 222 | "record too large ({} > {})", 223 | record_size, MAX_RECORD_SIZE 224 | ), 225 | ))); 226 | } 227 | *this.currently_reading = 228 | HandshakeRecordReaderReading::Record(record_size) 229 | } 230 | } 231 | rslt => return rslt, 232 | } 233 | } 234 | 235 | HandshakeRecordReaderReading::Record(remaining_record_bytes) => { 236 | // We ultimately want to read record bytes into `caller_buf`, but we need to 237 | // ensure that we don't read more bytes than there are record bytes (and end 238 | // up handing the caller record header bytes). So we call `caller_buf.take()`. 239 | // Since `take` returns an independent `ReadBuf`, we have to update `caller_buf` 240 | // once we're done reading: first we call `assume_init` to assert that the 241 | // `bytes_read` bytes we read are initialized, then we call `advance` to assert 242 | // that the appropriate section of the buffer is filled. 243 | 244 | let mut buf = caller_buf.take(*remaining_record_bytes); 245 | let rslt = this.reader.as_mut().poll_read(cx, &mut buf); 246 | if let Poll::Ready(Ok(())) = rslt { 247 | let bytes_read = buf.filled().len(); 248 | unsafe { 249 | caller_buf.assume_init(bytes_read); 250 | } 251 | caller_buf.advance(bytes_read); 252 | *remaining_record_bytes -= bytes_read; 253 | if *remaining_record_bytes == 0 { 254 | *this.currently_reading = HandshakeRecordReaderReading::ContentType; 255 | } 256 | } 257 | return rslt; 258 | } 259 | } 260 | } 261 | } 262 | } 263 | 264 | pub async fn read_sni_host_name_from_client_hello( 265 | mut reader: Pin<&mut R>, 266 | ) -> io::Result { 267 | // Handshake message type. 268 | const HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 1; 269 | let typ = reader.read_u8().await?; 270 | if typ != HANDSHAKE_TYPE_CLIENT_HELLO { 271 | return Err(io::Error::new( 272 | io::ErrorKind::InvalidData, 273 | format!( 274 | "handshake message not a ClientHello (type {}, expected {})", 275 | typ, HANDSHAKE_TYPE_CLIENT_HELLO 276 | ), 277 | )); 278 | } 279 | 280 | // Handshake message length. 281 | let len = read_u24(reader.as_mut()).await?; 282 | let reader = reader.take(len.into()); 283 | pin!(reader); 284 | 285 | // ProtocolVersion (2 bytes) & random (32 bytes). 286 | skip(reader.as_mut(), 34).await?; 287 | 288 | // Session ID (u8-length vec), cipher suites (u16-length vec), compression methods (u8-length vec). 289 | skip_vec_u8(reader.as_mut()).await?; 290 | skip_vec_u16(reader.as_mut()).await?; 291 | skip_vec_u8(reader.as_mut()).await?; 292 | 293 | // Extensions. 294 | let ext_len = reader.read_u16().await?; 295 | let new_limit = min(reader.limit(), ext_len.into()); 296 | reader.set_limit(new_limit); 297 | loop { 298 | // Extension type & length. 299 | let ext_typ = reader.read_u16().await?; 300 | let ext_len = reader.read_u16().await?; 301 | 302 | const EXTENSION_TYPE_SNI: u16 = 0; 303 | if ext_typ != EXTENSION_TYPE_SNI { 304 | skip(reader.as_mut(), ext_len.into()).await?; 305 | continue; 306 | } 307 | let new_limit = min(reader.limit(), ext_len.into()); 308 | reader.set_limit(new_limit); 309 | 310 | // ServerNameList length. 311 | let snl_len = reader.read_u16().await?; 312 | let new_limit = min(reader.limit(), snl_len.into()); 313 | reader.set_limit(new_limit); 314 | 315 | // ServerNameList. 316 | loop { 317 | // NameType & length. 318 | let name_typ = reader.read_u8().await?; 319 | 320 | const NAME_TYPE_HOST_NAME: u8 = 0; 321 | if name_typ != NAME_TYPE_HOST_NAME { 322 | skip_vec_u16(reader.as_mut()).await?; 323 | continue; 324 | } 325 | 326 | let name_len = reader.read_u16().await?; 327 | let new_limit = min(reader.limit(), name_len.into()); 328 | reader.set_limit(new_limit); 329 | let mut name_buf = vec![0; name_len.into()]; 330 | reader.read_exact(&mut name_buf).await?; 331 | return String::from_utf8(name_buf) 332 | .map_err(|err| io::Error::new(ErrorKind::InvalidData, err)); 333 | } 334 | } 335 | } 336 | 337 | async fn skip(reader: Pin<&mut R>, len: u64) -> io::Result<()> { 338 | let bytes_read = io::copy(&mut reader.take(len), &mut io::sink()).await?; 339 | if bytes_read < len { 340 | return Err(io::Error::new( 341 | ErrorKind::UnexpectedEof, 342 | format!("skip read {} < {} bytes", bytes_read, len), 343 | )); 344 | } 345 | Ok(()) 346 | } 347 | 348 | async fn skip_vec_u8(mut reader: Pin<&mut R>) -> io::Result<()> { 349 | let sz = reader.read_u8().await?; 350 | skip(reader.as_mut(), sz.into()).await 351 | } 352 | 353 | async fn skip_vec_u16(mut reader: Pin<&mut R>) -> io::Result<()> { 354 | let sz = reader.read_u16().await?; 355 | skip(reader.as_mut(), sz.into()).await 356 | } 357 | 358 | async fn read_u24(mut reader: Pin<&mut R>) -> io::Result { 359 | let mut buf = [0; 3]; 360 | reader 361 | .as_mut() 362 | .read_exact(&mut buf) 363 | .await 364 | .map(|_| NetworkEndian::read_u24(&buf)) 365 | } 366 | -------------------------------------------------------------------------------- /crates/rule/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "good-mitm-rule" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "Use MITM technology to provide features like rewrite, redirect." 6 | homepage = "https://github.com/zu1k/good-mitm" 7 | repository = "https://github.com/zu1k/good-mitm" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | mitm-core = { path = "../core", package = "good-mitm-core" } 12 | # mitm-core = { version = "0.2", package = "good-mitm-core" } 13 | 14 | anyhow = "1.0" 15 | async-trait = "0.1" 16 | cached = "0.43" 17 | cookie = "0.17" 18 | fancy-regex = "0.11" 19 | http = "0.2" 20 | hyper = { version = "0.14", features = ["client", "http1", "server", "stream", "tcp"] } 21 | log = "0.4" 22 | quick-js = { version = "0.4", features = ["log"], optional = true } 23 | serde = { version = "1.0", features = ["derive"] } 24 | 25 | [features] 26 | default = [] 27 | js = ["quick-js"] 28 | -------------------------------------------------------------------------------- /crates/rule/src/action/js.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use http::{header::HeaderName, Response}; 3 | use hyper::{ 4 | body::{to_bytes, Body, Bytes}, 5 | Request, 6 | }; 7 | use quick_js::{console::LogConsole, Context, JsValue}; 8 | use std::collections::HashMap; 9 | 10 | macro_rules! to_js_value_map { 11 | ($parts:ident, $body_bytes:ident) => {{ 12 | let mut req_js = HashMap::new(); 13 | 14 | // headers 15 | let headers = { 16 | let mut headers = HashMap::new(); 17 | for (name, value) in &$parts.headers { 18 | headers.insert( 19 | name.to_string(), 20 | JsValue::String(value.to_str().unwrap_or_default().to_owned()), 21 | ); 22 | } 23 | headers 24 | }; 25 | req_js.insert("headers".to_owned(), JsValue::Object(headers)); 26 | 27 | // body text 28 | if let Ok(text) = String::from_utf8($body_bytes.to_vec()) { 29 | req_js.insert("body".to_owned(), JsValue::String(text)); 30 | } else { 31 | req_js.insert("body".to_owned(), JsValue::Undefined); 32 | } 33 | 34 | req_js 35 | }}; 36 | } 37 | 38 | pub async fn modify_req(code: &str, req: Request) -> Result> { 39 | let (mut parts, body) = req.into_parts(); 40 | let body_bytes = to_bytes(body).await.unwrap_or_default(); 41 | let mut req_js = to_js_value_map!(parts, body_bytes); 42 | req_js.insert( 43 | "method".to_owned(), 44 | JsValue::String(parts.method.to_string()), 45 | ); 46 | req_js.insert("url".to_owned(), JsValue::String(parts.uri.to_string())); 47 | let mut data = HashMap::new(); 48 | data.insert("request".to_owned(), JsValue::Object(req_js)); 49 | 50 | let context = Context::builder().console(LogConsole).build()?; 51 | context.set_global("data", JsValue::Object(data))?; 52 | match context.eval(code) { 53 | Ok(req_js) => { 54 | if let JsValue::Object(req_js) = req_js { 55 | if let Some(JsValue::Object(headers)) = req_js.get("headers") { 56 | for (key, value) in headers { 57 | if let JsValue::String(value) = value { 58 | parts.headers.insert( 59 | HeaderName::from_bytes(key.as_bytes()).unwrap(), 60 | value.parse().unwrap(), 61 | ); 62 | } 63 | } 64 | } 65 | 66 | if let Some(JsValue::String(url)) = req_js.get("url") { 67 | parts.uri = url.parse().unwrap(); 68 | } 69 | 70 | let body = if let Some(JsValue::String(body)) = req_js.get("body") { 71 | Bytes::from(body.to_owned()) 72 | } else { 73 | body_bytes 74 | }; 75 | Ok(Request::from_parts(parts, Body::from(body))) 76 | } else { 77 | Err(anyhow!("can not get js eval ret")) 78 | } 79 | } 80 | Err(err) => Err(err.into()), 81 | } 82 | } 83 | 84 | pub async fn modify_res(code: &str, res: Response) -> Result> { 85 | let (mut parts, body) = res.into_parts(); 86 | let body_bytes = to_bytes(body).await.unwrap_or_default(); 87 | let res_js = to_js_value_map!(parts, body_bytes); 88 | let mut data = HashMap::new(); 89 | data.insert("response".to_owned(), JsValue::Object(res_js)); 90 | 91 | let context = Context::builder().console(LogConsole).build()?; 92 | context.set_global("data", JsValue::Object(data))?; 93 | match context.eval(code) { 94 | Ok(req_js) => { 95 | if let JsValue::Object(req_js) = req_js { 96 | if let Some(JsValue::Object(headers)) = req_js.get("headers") { 97 | for (key, value) in headers { 98 | if let JsValue::String(value) = value { 99 | parts.headers.insert( 100 | HeaderName::from_bytes(key.as_bytes()).unwrap(), 101 | value.parse().unwrap(), 102 | ); 103 | } 104 | } 105 | } 106 | 107 | let body = if let Some(JsValue::String(body)) = req_js.get("body") { 108 | Bytes::from(body.to_owned()) 109 | } else { 110 | body_bytes 111 | }; 112 | Ok(Response::from_parts(parts, Body::from(body))) 113 | } else { 114 | Err(anyhow!("can not get js eval ret")) 115 | } 116 | } 117 | Err(err) => Err(err.into()), 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /crates/rule/src/action/log.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Request, Response}; 2 | use log::info; 3 | use std::fmt::Write; 4 | 5 | pub async fn log_req(req: &Request) { 6 | let headers = req.headers(); 7 | let mut header_formated = String::new(); 8 | for (key, value) in headers { 9 | let v = match value.to_str() { 10 | Ok(v) => v.to_string(), 11 | Err(_) => { 12 | format!("[u8]; {}", value.len()) 13 | } 14 | }; 15 | write!( 16 | &mut header_formated, 17 | "\t{:<20}{}\r\n", 18 | format!("{}:", key.as_str()), 19 | v 20 | ) 21 | .unwrap(); 22 | } 23 | 24 | info!( 25 | "{} {} 26 | Headers: 27 | {}", 28 | req.method(), 29 | req.uri().to_string(), 30 | header_formated 31 | ) 32 | } 33 | 34 | pub async fn log_res(res: &Response) { 35 | let headers = res.headers(); 36 | let mut header_formated = String::new(); 37 | for (key, value) in headers { 38 | let v = match value.to_str() { 39 | Ok(v) => v.to_string(), 40 | Err(_) => { 41 | format!("[u8]; {}", value.len()) 42 | } 43 | }; 44 | write!( 45 | &mut header_formated, 46 | "\t{:<20}{}\r\n", 47 | format!("{}:", key.as_str()), 48 | v 49 | ) 50 | .unwrap(); 51 | } 52 | 53 | info!( 54 | "{} {:?} 55 | Headers: 56 | {}", 57 | res.status(), 58 | res.version(), 59 | header_formated 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /crates/rule/src/action/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "js")] 2 | pub mod js; 3 | mod log; 4 | mod modify; 5 | 6 | pub use self::log::*; 7 | pub use modify::Modify; 8 | use serde::{Deserialize, Serialize}; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | #[serde(rename_all = "kebab-case")] 12 | pub enum Action { 13 | Reject, 14 | Redirect(String), 15 | ModifyRequest(Modify), 16 | ModifyResponse(Modify), 17 | LogRes, 18 | LogReq, 19 | 20 | #[cfg(feature = "js")] 21 | Js(String), 22 | } 23 | -------------------------------------------------------------------------------- /crates/rule/src/action/modify.rs: -------------------------------------------------------------------------------- 1 | use cookie::{Cookie, CookieJar}; 2 | use http::{header::HeaderName, HeaderValue, Uri}; 3 | use hyper::{body::*, header, Body, HeaderMap, Request, Response, StatusCode}; 4 | use log::error; 5 | use serde::{Deserialize, Serialize}; 6 | use std::str::FromStr; 7 | 8 | use crate::cache::get_regex; 9 | 10 | #[derive(Debug, Clone, Deserialize, Serialize)] 11 | #[serde(untagged)] 12 | pub enum TextModify { 13 | Set(String), 14 | Complex(TextModifyComplex), 15 | } 16 | 17 | #[derive(Debug, Clone, Deserialize, Serialize)] 18 | #[serde(rename_all = "kebab-case")] 19 | pub struct TextModifyComplex { 20 | pub origin: Option, 21 | pub re: Option, 22 | pub new: String, 23 | } 24 | 25 | impl TextModify { 26 | fn exec_action(&self, text: &str) -> String { 27 | match self { 28 | TextModify::Set(new) => new.to_string(), 29 | TextModify::Complex(md) => { 30 | if let Some(ref origin) = md.origin { 31 | return text.replace(origin, &md.new); 32 | } 33 | 34 | if let Some(ref re) = md.re { 35 | return get_regex(re).replace_all(text, &md.new).to_string(); 36 | } 37 | 38 | md.new.clone() 39 | } 40 | } 41 | } 42 | } 43 | 44 | #[derive(Default, Debug, Clone, Deserialize, Serialize)] 45 | #[serde(rename_all = "kebab-case")] 46 | pub struct MapModify { 47 | pub key: String, 48 | #[serde(default)] 49 | pub value: Option, 50 | #[serde(default)] 51 | pub remove: bool, 52 | } 53 | 54 | #[derive(Debug, Clone, Deserialize, Serialize)] 55 | #[serde(rename_all = "kebab-case")] 56 | pub enum Modify { 57 | Url(TextModify), 58 | Header(MapModify), 59 | Cookie(MapModify), 60 | Body(TextModify), 61 | } 62 | 63 | impl Modify { 64 | pub async fn modify_req(&self, mut req: Request) -> Option> { 65 | match self { 66 | Modify::Url(md) => { 67 | let origin = req.uri().to_string(); 68 | let new_url = md.exec_action(&origin); 69 | match Uri::from_str(&new_url) { 70 | Ok(uri) => *req.uri_mut() = uri, 71 | Err(err) => error!("url modify error: {}", err), 72 | } 73 | Some(req) 74 | } 75 | Modify::Body(bm) => { 76 | let (parts, body) = req.into_parts(); 77 | if match parts.headers.get(header::CONTENT_TYPE) { 78 | Some(content_type) => { 79 | let content_type = content_type.to_str().unwrap_or_default(); 80 | content_type.contains("text") || content_type.contains("javascript") 81 | } 82 | None => false, 83 | } { 84 | match to_bytes(body).await { 85 | Ok(content) => match String::from_utf8(content.to_vec()) { 86 | Ok(text) => { 87 | let text = bm.exec_action(&text); 88 | Some(Request::from_parts(parts, Body::from(text))) 89 | } 90 | Err(_) => Some(Request::from_parts(parts, Body::from(content))), 91 | }, 92 | // req body read failed 93 | Err(_) => None, 94 | } 95 | } else { 96 | Some(Request::from_parts(parts, body)) 97 | } 98 | } 99 | Modify::Header(hm) => { 100 | let mut req = req; 101 | self.modify_header(req.headers_mut(), hm); 102 | Some(req) 103 | } 104 | Modify::Cookie(md) => { 105 | let mut req = req; 106 | let mut cookies_jar = CookieJar::new(); 107 | 108 | if let Some(cookies) = req.headers().get(header::COOKIE) { 109 | let cookies = cookies.to_str().unwrap().to_string(); 110 | let cookies: Vec = cookies.split("; ").map(String::from).collect(); 111 | for c in cookies { 112 | if let Ok(c) = Cookie::parse(c) { 113 | cookies_jar.add(c); 114 | } 115 | } 116 | } 117 | 118 | if md.remove { 119 | cookies_jar.remove(Cookie::named(md.key.clone())) 120 | } else { 121 | let new_cookie_value = md 122 | .value 123 | .to_owned() 124 | .map(|text_md| { 125 | let origin_cookie_value = cookies_jar 126 | .get(&md.key) 127 | .map(|c| c.value().to_string()) 128 | .unwrap_or_default(); 129 | text_md.exec_action(&origin_cookie_value) 130 | }) 131 | .unwrap_or_default(); 132 | cookies_jar.add(Cookie::new(md.key.clone(), new_cookie_value)) 133 | } 134 | 135 | let cookies: Vec = cookies_jar.iter().map(|c| c.to_string()).collect(); 136 | let cookies = cookies.join("; "); 137 | req.headers_mut() 138 | .insert(header::COOKIE, HeaderValue::from_str(&cookies).unwrap()); 139 | 140 | Some(req) 141 | } 142 | } 143 | } 144 | 145 | pub async fn modify_res(&self, res: Response) -> Response { 146 | match self { 147 | Modify::Body(bm) => { 148 | let (parts, body) = res.into_parts(); 149 | if match parts.headers.get(header::CONTENT_TYPE) { 150 | Some(content_type) => { 151 | let content_type = content_type.to_str().unwrap_or_default(); 152 | content_type.contains("text") || content_type.contains("javascript") 153 | } 154 | None => false, 155 | } { 156 | match to_bytes(body).await { 157 | Ok(content) => match String::from_utf8(content.to_vec()) { 158 | Ok(text) => { 159 | let text = bm.exec_action(&text); 160 | Response::from_parts(parts, Body::from(text)) 161 | } 162 | Err(_) => Response::from_parts(parts, Body::from(content)), 163 | }, 164 | Err(err) => Response::builder() 165 | .status(StatusCode::BAD_GATEWAY) 166 | .body(Body::from(err.to_string())) 167 | .unwrap(), 168 | } 169 | } else { 170 | Response::from_parts(parts, body) 171 | } 172 | } 173 | Modify::Header(md) => { 174 | let mut res = res; 175 | self.modify_header(res.headers_mut(), md); 176 | res 177 | } 178 | Modify::Cookie(md) => { 179 | let mut res = res; 180 | 181 | let mut cookies_jar = CookieJar::new(); 182 | if let Some(cookies) = res.headers().get(header::COOKIE) { 183 | let cookies = String::from_utf8_lossy(cookies.as_bytes()).to_string(); 184 | Cookie::split_parse(cookies) 185 | .filter_map(Result::ok) 186 | .for_each(|cookie| cookies_jar.add(cookie)); 187 | } 188 | 189 | let mut set_cookies_jar = CookieJar::new(); 190 | let set_cookies = res.headers().get_all(header::SET_COOKIE); 191 | for sc in set_cookies { 192 | if let Ok(c) = Cookie::parse(String::from_utf8_lossy(sc.as_bytes()).to_string()) 193 | { 194 | set_cookies_jar.add(c); 195 | } 196 | } 197 | 198 | if md.remove { 199 | cookies_jar.remove(Cookie::named(md.key.clone())); 200 | set_cookies_jar.remove(Cookie::named(md.key.clone())); 201 | } else { 202 | let new_cookie_value = md 203 | .value 204 | .to_owned() 205 | .map(|text_md| { 206 | let origin_cookie_value = cookies_jar 207 | .get(&md.key) 208 | .map(|c| c.value().to_string()) 209 | .or_else(|| { 210 | set_cookies_jar.get(&md.key).map(|c| c.value().to_string()) 211 | }) 212 | .unwrap_or_default(); 213 | text_md.exec_action(&origin_cookie_value) 214 | }) 215 | .unwrap_or_default(); 216 | 217 | let c = Cookie::new(md.key.clone(), new_cookie_value); 218 | cookies_jar.add(c.clone()); 219 | set_cookies_jar.add(c.clone()); 220 | } 221 | 222 | let cookies: Vec = cookies_jar.iter().map(|c| c.to_string()).collect(); 223 | let cookies = cookies.join("; "); 224 | let header = res.headers_mut(); 225 | header.insert(header::COOKIE, HeaderValue::from_str(&cookies).unwrap()); 226 | 227 | header.remove(header::SET_COOKIE); 228 | for sc in set_cookies_jar.iter() { 229 | header.append( 230 | header::SET_COOKIE, 231 | HeaderValue::from_str(&sc.to_string()).unwrap(), 232 | ); 233 | } 234 | 235 | res 236 | } 237 | Modify::Url(_) => { 238 | error!("modify response url not supported"); 239 | res 240 | } 241 | } 242 | } 243 | 244 | fn modify_header(&self, header: &mut HeaderMap, md: &MapModify) { 245 | if md.remove { 246 | header.remove(&md.key); 247 | } else if let Some(ref text_md) = md.value { 248 | if let Some(h) = header.get_mut(&md.key) { 249 | let new_header_value = text_md.exec_action(h.to_str().unwrap_or_default()); 250 | *h = header::HeaderValue::from_str(new_header_value.as_str()).unwrap(); 251 | } else { 252 | let new_header_value = text_md.exec_action(""); 253 | header.append( 254 | HeaderName::from_str(&md.key).unwrap(), 255 | header::HeaderValue::from_str(new_header_value.as_str()).unwrap(), 256 | ); 257 | } 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /crates/rule/src/cache.rs: -------------------------------------------------------------------------------- 1 | use cached::{proc_macro::cached, SizedCache}; 2 | use fancy_regex::Regex; 3 | 4 | #[cached( 5 | type = "SizedCache", 6 | create = "{ SizedCache::with_size(100) }", 7 | convert = r#"{ re.to_string() }"# 8 | )] 9 | pub fn get_regex(re: &str) -> Regex { 10 | fancy_regex::Regex::new(re).unwrap() 11 | } 12 | -------------------------------------------------------------------------------- /crates/rule/src/filter.rs: -------------------------------------------------------------------------------- 1 | use hyper::{Body, Request}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::cache::get_regex; 5 | 6 | #[derive(Debug, Clone, Deserialize, Serialize)] 7 | #[serde(rename_all = "kebab-case")] 8 | pub enum Filter { 9 | All, 10 | Domain(String), 11 | DomainKeyword(String), 12 | DomainPrefix(String), 13 | DomainSuffix(String), 14 | UrlRegex(String), 15 | } 16 | 17 | impl Filter { 18 | pub fn init(&self) -> Self { 19 | match self { 20 | Filter::All => self.to_owned(), 21 | Filter::Domain(d) => Self::Domain(d.to_lowercase()), 22 | Filter::DomainKeyword(d) => Self::DomainKeyword(d.to_lowercase()), 23 | Filter::DomainPrefix(d) => Self::DomainPrefix(d.to_lowercase()), 24 | Filter::DomainSuffix(d) => Self::DomainSuffix(d.to_lowercase()), 25 | Filter::UrlRegex(re) => Self::UrlRegex(re.to_owned()), 26 | } 27 | } 28 | 29 | pub fn is_match_req(&self, req: &Request) -> bool { 30 | let host = req.uri().host().unwrap_or_default().to_lowercase(); 31 | match self { 32 | Self::All => true, 33 | Self::Domain(target) => host == *target, 34 | Self::DomainKeyword(target) => host.contains(target), 35 | Self::DomainPrefix(target) => host.starts_with(target), 36 | Self::DomainSuffix(target) => host.ends_with(target), 37 | Self::UrlRegex(target) => { 38 | let url = req.uri().to_string(); 39 | get_regex(target).is_match(&url).unwrap() 40 | } 41 | } 42 | } 43 | 44 | pub fn mitm_filtter_pattern(&self) -> Option { 45 | match self { 46 | Self::All => Some("*".to_owned()), 47 | Self::Domain(d) => Some(d.to_owned()), 48 | Self::DomainKeyword(d) => Some(format!("*{}*", d)), 49 | Self::DomainPrefix(d) => Some(format!("{}*", d)), 50 | Self::DomainSuffix(d) => Some(format!("*{}", d)), 51 | _ => None, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /crates/rule/src/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::Rule; 2 | use async_trait::async_trait; 3 | use hyper::{header, Body, Request, Response}; 4 | use log::info; 5 | use mitm_core::{ 6 | handler::{CustomContextData, HttpHandler}, 7 | mitm::{HttpContext, RequestOrResponse}, 8 | }; 9 | use std::sync::Arc; 10 | 11 | #[derive(Clone)] 12 | pub struct RuleHttpHandler { 13 | rules: Arc>, 14 | } 15 | 16 | #[derive(Default, Clone)] 17 | pub struct RuleHandlerCtx { 18 | rules: Vec, 19 | } 20 | 21 | impl CustomContextData for RuleHandlerCtx {} 22 | 23 | impl RuleHttpHandler { 24 | pub fn new(rules: Arc>) -> Self { 25 | Self { rules } 26 | } 27 | 28 | fn match_rules(&self, req: &Request) -> Vec { 29 | let mut matched = vec![]; 30 | for rule in self.rules.iter() { 31 | for filter in &rule.filters { 32 | if filter.is_match_req(req) { 33 | matched.push(rule.clone()); 34 | } 35 | } 36 | } 37 | matched 38 | } 39 | } 40 | 41 | #[async_trait] 42 | impl HttpHandler for RuleHttpHandler { 43 | async fn handle_request( 44 | &self, 45 | ctx: &mut HttpContext, 46 | req: Request, 47 | ) -> RequestOrResponse { 48 | ctx.uri = Some(req.uri().clone()); 49 | 50 | // remove accept-encoding to avoid encoded body 51 | let mut req = req; 52 | req.headers_mut().remove(header::ACCEPT_ENCODING); 53 | 54 | let rules = self.match_rules(&req); 55 | if !rules.is_empty() { 56 | ctx.should_modify_response = true; 57 | } 58 | 59 | for mut rule in rules { 60 | ctx.custom_data.rules.push(rule.clone()); 61 | let rt = rule.do_req(req).await; 62 | if let RequestOrResponse::Request(r) = rt { 63 | req = r; 64 | } else { 65 | return rt; 66 | } 67 | } 68 | 69 | RequestOrResponse::Request(req) 70 | } 71 | 72 | async fn handle_response( 73 | &self, 74 | ctx: &mut HttpContext, 75 | res: Response, 76 | ) -> Response { 77 | if !ctx.should_modify_response || ctx.custom_data.rules.is_empty() { 78 | return res; 79 | } 80 | let uri = ctx.uri.as_ref().unwrap(); 81 | let content_type = match res.headers().get(header::CONTENT_TYPE) { 82 | Some(content_type) => content_type.to_str().unwrap_or_default(), 83 | None => "unknown", 84 | }; 85 | info!( 86 | "[Response] {} {} {}", 87 | res.status(), 88 | uri.host().unwrap_or_default(), 89 | content_type 90 | ); 91 | 92 | let mut res = res; 93 | for rule in &ctx.custom_data.rules { 94 | res = rule.do_res(res).await; 95 | } 96 | res 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /crates/rule/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use action::Action; 2 | pub use filter::Filter; 3 | pub use handler::*; 4 | use hyper::{header, header::HeaderValue, Body, Request, Response, StatusCode}; 5 | use log::*; 6 | use mitm_core::mitm::RequestOrResponse; 7 | use std::vec::Vec; 8 | 9 | mod action; 10 | mod cache; 11 | mod filter; 12 | mod handler; 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct Rule { 16 | pub filters: Vec, 17 | pub actions: Vec, 18 | 19 | pub url: Option, 20 | } 21 | 22 | impl Rule { 23 | pub async fn do_req(&mut self, req: Request) -> RequestOrResponse { 24 | let url = req.uri().to_string(); 25 | self.url = Some(url.clone()); 26 | let mut tmp_req = req; 27 | 28 | for action in &self.actions { 29 | match action { 30 | Action::Reject => { 31 | info!("[Reject] {}", url); 32 | let res = Response::builder() 33 | .status(StatusCode::BAD_GATEWAY) 34 | .body(Body::default()) 35 | .unwrap(); 36 | 37 | return RequestOrResponse::Response(res); 38 | } 39 | 40 | Action::Redirect(target) => { 41 | if target.contains('$') { 42 | for filter in self.filters.clone() { 43 | if let Filter::UrlRegex(re) = filter { 44 | let target = cache::get_regex(&re) 45 | .replace(tmp_req.uri().to_string().as_str(), target.as_str()) 46 | .to_string(); 47 | if let Ok(target_url) = HeaderValue::from_str(target.as_str()) { 48 | let mut res = Response::builder() 49 | .status(StatusCode::FOUND) 50 | .body(Body::default()) 51 | .unwrap(); 52 | res.headers_mut().insert(header::LOCATION, target_url); 53 | info!("[Redirect] {} -> {}", url, target); 54 | return RequestOrResponse::Response(res); 55 | } 56 | } 57 | } 58 | } 59 | if let Ok(target_url) = HeaderValue::from_str(target.as_str()) { 60 | let mut res = Response::builder() 61 | .status(StatusCode::FOUND) 62 | .body(Body::default()) 63 | .unwrap(); 64 | res.headers_mut().insert(header::LOCATION, target_url); 65 | info!("[Redirect] {} -> {}", url, target); 66 | return RequestOrResponse::Response(res); 67 | }; 68 | } 69 | 70 | Action::ModifyRequest(modify) => { 71 | info!("[ModifyRequest] {}", url); 72 | match modify.modify_req(tmp_req).await { 73 | Some(new_req) => tmp_req = new_req, 74 | None => { 75 | return RequestOrResponse::Response( 76 | Response::builder() 77 | .status(StatusCode::BAD_REQUEST) 78 | .body(Body::default()) 79 | .unwrap(), 80 | ); 81 | } 82 | } 83 | } 84 | 85 | Action::LogReq => { 86 | info!("[LogRequest] {}", url); 87 | action::log_req(&tmp_req).await; 88 | } 89 | 90 | #[cfg(feature = "js")] 91 | Action::Js(ref code) => { 92 | info!("[LogRequest] {}", url); 93 | if let Ok(req) = action::js::modify_req(code, tmp_req).await { 94 | return RequestOrResponse::Request(req); 95 | } else { 96 | return RequestOrResponse::Response( 97 | Response::builder() 98 | .status(StatusCode::BAD_REQUEST) 99 | .body(Body::default()) 100 | .unwrap(), 101 | ); 102 | } 103 | } 104 | _ => {} 105 | } 106 | } 107 | 108 | RequestOrResponse::Request(tmp_req) 109 | } 110 | 111 | pub async fn do_res(&self, res: Response) -> Response { 112 | let url = self.url.clone().unwrap_or_default(); 113 | let mut tmp_res = res; 114 | 115 | for action in &self.actions { 116 | match action { 117 | Action::ModifyResponse(modify) => { 118 | info!("[ModifyResponse] {}", url); 119 | tmp_res = modify.modify_res(tmp_res).await 120 | } 121 | Action::LogRes => { 122 | info!("[LogResponse] {}", url); 123 | action::log_res(&tmp_res).await; 124 | } 125 | 126 | #[cfg(feature = "js")] 127 | Action::Js(ref code) => { 128 | info!("[LogResponse] {}", url); 129 | if let Ok(res) = action::js::modify_res(code, tmp_res).await { 130 | return res; 131 | } else { 132 | return Response::builder() 133 | .status(StatusCode::BAD_REQUEST) 134 | .body(Body::default()) 135 | .unwrap(); 136 | } 137 | } 138 | _ => {} 139 | } 140 | } 141 | 142 | tmp_res 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /crates/trust_cert/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "trust_cert" 3 | version = "0.0.5" 4 | edition = "2021" 5 | description = "Install certificate to your system trust zone." 6 | homepage = "https://github.com/zu1k/good-mitm" 7 | repository = "https://github.com/zu1k/good-mitm" 8 | license = "MIT" 9 | 10 | [dependencies] 11 | rcgen = { version = "0.10", features = ["x509-parser"] } 12 | 13 | [target.'cfg(unix)'.dependencies] 14 | nix = { version = "0.26", default-features = false, features = ["user"] } 15 | 16 | [target.'cfg(windows)'.dependencies] 17 | windows = { version = "0.48", features = ["Win32_Security_Cryptography", "Win32_Foundation"] } 18 | -------------------------------------------------------------------------------- /crates/trust_cert/src/lib.rs: -------------------------------------------------------------------------------- 1 | use rcgen::Certificate; 2 | 3 | #[cfg(windows)] 4 | mod windows; 5 | 6 | #[cfg(target_os = "linux")] 7 | mod linux; 8 | 9 | #[allow(unused_variables)] 10 | pub fn trust_cert(cert: Certificate) { 11 | #[cfg(windows)] 12 | return windows::install_cert(cert); 13 | #[cfg(target_os = "linux")] 14 | return linux::install_cert(cert); 15 | } 16 | -------------------------------------------------------------------------------- /crates/trust_cert/src/linux.rs: -------------------------------------------------------------------------------- 1 | use nix::unistd::getegid; 2 | use rcgen::Certificate; 3 | use std::{env, fs, path::Path, process::Command}; 4 | 5 | pub fn install_cert(cert: Certificate) { 6 | if getegid().as_raw() != 0 { 7 | println!("Please run with root permission"); 8 | return; 9 | } 10 | 11 | let (system_trust_filename, trust_cmd, trust_cmd_args) = { 12 | if path_exist("/etc/pki/ca-trust/source/anchors/") { 13 | ( 14 | "/etc/pki/ca-trust/source/anchors/{cert-name}.pem", 15 | "update-ca-trust", 16 | vec!["extract"], 17 | ) 18 | } else if path_exist("/usr/local/share/ca-certificates/") { 19 | ( 20 | "/usr/local/share/ca-certificates/{cert-name}.crt", 21 | "update-ca-certificates", 22 | vec![], 23 | ) 24 | } else if path_exist("/etc/ca-certificates/trust-source/anchors/") { 25 | ( 26 | "/etc/ca-certificates/trust-source/anchors/{cert-name}.crt", 27 | "trust", 28 | vec!["extract-compat"], 29 | ) 30 | } else if path_exist("/usr/share/pki/trust/anchors") { 31 | ( 32 | "/usr/share/pki/trust/anchors/{cert-name}.pem", 33 | "update-ca-certificates", 34 | vec![], 35 | ) 36 | } else { 37 | ("good-mitm.pem", "", vec![]) 38 | } 39 | }; 40 | 41 | let cert = cert.serialize_pem().expect("serialize cert to pem format"); 42 | let system_trust_name = system_trust_filename.replace("{cert-name}", "good-mitm"); 43 | fs::write(system_trust_name, cert).expect("write cert to system trust ca location"); 44 | 45 | if trust_cmd.is_empty() { 46 | println!( 47 | "Installing to the system store is not yet supported on this Linux 😣 but Firefox and/or Chrome/Chromium will still work.", 48 | ); 49 | let cert_path = Path::new(&get_ca_root()).join("good-mitm.pem"); 50 | println!( 51 | "You can also manually install the root certificate at {}.", 52 | cert_path.to_str().unwrap() 53 | ); 54 | } else { 55 | Command::new(trust_cmd) 56 | .args(trust_cmd_args) 57 | .status() 58 | .expect("failed to execute trust command"); 59 | } 60 | } 61 | 62 | fn get_ca_root() -> String { 63 | if let Ok(v) = env::var("CAROOT") { 64 | return v; 65 | } 66 | 67 | let mut dir = { 68 | if let Ok(v) = env::var("XDG_DATA_HOME") { 69 | return v; 70 | } 71 | if let Ok(v) = env::var("HOME") { 72 | return Path::new(&v) 73 | .join(".local") 74 | .join("share") 75 | .to_str() 76 | .map(|s| s.to_string()) 77 | .unwrap(); 78 | } 79 | String::new() 80 | }; 81 | 82 | if !dir.is_empty() { 83 | dir = Path::new(&dir) 84 | .join("mitm") 85 | .into_os_string() 86 | .into_string() 87 | .unwrap() 88 | } 89 | 90 | dir 91 | } 92 | 93 | #[inline] 94 | fn path_exist(path: &str) -> bool { 95 | Path::new(path).exists() 96 | } 97 | -------------------------------------------------------------------------------- /crates/trust_cert/src/windows.rs: -------------------------------------------------------------------------------- 1 | use rcgen::Certificate; 2 | use windows::{ 3 | w, 4 | Win32::Security::Cryptography::{ 5 | CertAddEncodedCertificateToStore, CertCloseStore, CertOpenSystemStoreW, 6 | CERT_STORE_ADD_REPLACE_EXISTING, HCRYPTPROV_LEGACY, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING, 7 | }, 8 | }; 9 | 10 | pub fn install_cert(cert: Certificate) { 11 | let mut cert = cert.serialize_der().unwrap(); 12 | unsafe { 13 | // get root store 14 | let store = CertOpenSystemStoreW(HCRYPTPROV_LEGACY(0), w!("ROOT")) 15 | .expect("open system root ca store"); 16 | 17 | // add cert 18 | if !CertAddEncodedCertificateToStore( 19 | store, 20 | X509_ASN_ENCODING.0 | PKCS_7_ASN_ENCODING.0, 21 | cert.as_mut_ptr(), 22 | cert.len() as u32, 23 | CERT_STORE_ADD_REPLACE_EXISTING, 24 | 0 as _, 25 | ) 26 | .as_bool() 27 | { 28 | panic!("CertAddEncodedCertificateToStore failed") 29 | } 30 | 31 | if !CertCloseStore(store, 0).as_bool() { 32 | panic!("CertCloseStore failed") 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zu1k/Good-MITM/e0ad482d12a289ea5801398786ebc80fd5df1e28/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | good-mitm.zu1k.com -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Good Man in the Middle 2 | 3 | [![GitHub stars](https://img.shields.io/github/stars/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/stargazers) 4 | [![GitHub forks](https://img.shields.io/github/forks/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/network) 5 | [![GitHub issues](https://img.shields.io/github/issues/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/issues) 6 | [![Build](https://github.com/zu1k/good-mitm/actions/workflows/build-test.yml/badge.svg)](https://github.com/zu1k/good-mitm/actions/workflows/build-test.yml) 7 | [![GitHub license](https://img.shields.io/github/license/zu1k/good-mitm)](https://github.com/zu1k/good-mitm/blob/master/LICENSE) 8 | 9 | 使用MITM技术来提供 `rewrite`、`redirect`、`reject` 等功能 10 | 11 | ## 功能 12 | 13 | - 基于 TLS ClientHello 的自动证书签署 14 | - 支持选择性 MITM 15 | - 基于 YAML 格式的规则描述语言:重写/阻断/重定向 16 | - 灵活的规则匹配器 17 | - 域名前缀/后缀/全匹配 18 | - 正则匹配 19 | - 多筛选器规则 20 | - 灵活的文本内容改写 21 | - 抹除/替换 22 | - 正则替换 23 | - 灵活的字典类型内容改写 24 | - HTTP Header 改写 25 | - Cookie 改写 26 | - 支持单条规则多个行为 27 | - 支持 JavaScript 脚本规则 (编程介入) 28 | - 支持透明代理 29 | - 透明代理 HTTPS 和 HTTP 复用单端口 30 | - 支持自动安装 CA 证书到系统信任区 31 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * [介绍](/) 2 | * [指南](guide/README.md) 3 | * [证书准备](guide/0_cert.md) 4 | * [透明代理](guide/transparent_proxy.md) 5 | * [Rule 规则](rule/README.md) 6 | * [Filter 筛选器](rule/filter.md) 7 | * [Action 动作](rule/action.md) 8 | * [Modify 修改器](rule/modify.md) 9 | * [其他](others.md) -------------------------------------------------------------------------------- /docs/guide/0_cert.md: -------------------------------------------------------------------------------- 1 | # 证书准备 2 | 3 | 为了实现对HTTPS流量进行MITM,同时为了浏览器等不显示安全警告,需要生成并信任自签名CA证书 4 | 5 | ## 生成CA证书 6 | 7 | 出于安全考虑,用户必须自己生成自己的CA证书,随意使用不可信的CA证书将会留下严重的安全隐患 8 | 9 | 经验丰富的用户可以自行使用OpenSSL进行相关操作,考虑到没有相关经验的用户,可以使用以下命令直接生成相关内容,生成的证书和私钥将存储在`ca`目录下 10 | 11 | ```shell 12 | good-mitm.exe genca 13 | ``` 14 | 15 | 在浏览器使用了Good-MITM提供的代理后,通过访问 [http://cert.mitm.plus](http://cert.mitm.plus) 可以直接下载证书,这在给其他设备提供服务时非常有用 16 | 17 | ## 信任生成的证书 18 | 19 | 你需要在浏览器或者操作系统中信任刚刚生成的证书,具体方法后期补充 20 | -------------------------------------------------------------------------------- /docs/guide/1_rule.md: -------------------------------------------------------------------------------- 1 | # 配置规则 -------------------------------------------------------------------------------- /docs/guide/2_proxy.md: -------------------------------------------------------------------------------- 1 | # 设置代理 2 | 3 | ### Use the proxy provided by `good-MITM` 4 | 5 | Adding `http` and `https` proxies to the browser, `http://127.0.0.1:34567` if not modified. -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # 指南 2 | 3 | 这是指南,不是指北 4 | -------------------------------------------------------------------------------- /docs/guide/transparent_proxy.md: -------------------------------------------------------------------------------- 1 | # 透明代理 2 | 3 | See https://docs.mitmproxy.org/stable/howto-transparent/ for docs. 4 | 5 | ```shell 6 | sudo sysctl -w net.ipv4.ip_forward=1 7 | sudo sysctl -w net.ipv6.conf.all.forwarding=1 8 | sudo sysctl -w net.ipv4.conf.all.send_redirects=0 9 | 10 | sudo useradd --create-home mitm 11 | sudo -u mitm -H bash -c 'good-mitm run -r rules/log.yaml -b 0.0.0.0:34567' 12 | 13 | sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 80 -j REDIRECT --to-port 34567 14 | sudo iptables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 443 -j REDIRECT --to-port 34567 15 | sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 80 -j REDIRECT --to-port 34567 16 | sudo ip6tables -t nat -A OUTPUT -p tcp -m owner ! --uid-owner mitm --dport 443 -j REDIRECT --to-port 34567 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Good-MITM 文档 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/others.md: -------------------------------------------------------------------------------- 1 | # 其他 2 | 3 | ## 开源证书 4 | 5 | MIT License 6 | 7 | Copyright (c) 2021 zu1k 8 | 9 | ## 感谢列表 10 | 11 | - [**hudsucker**](https://github.com/omjadas/hudsucker): a Rust crate providing MITM features 12 | 13 | -------------------------------------------------------------------------------- /docs/rule/README.md: -------------------------------------------------------------------------------- 1 | # Rule 规则 2 | 3 | `Rule`用来操控 Good-MITM 4 | 5 | 一条合格的规则需要包含以下内容: 6 | 7 | - `规则名`:用来区分不同的规则,便与维护 8 | - [`筛选器`](rule/filter.md):用于从众多`请求`和`返回`中筛选出需要处理的内容 9 | - [`动作`](rule/action.md):用于执行想要的行为,包括`重定向`、`阻断`、`修改`等 10 | - 必要时指定需要MITM的域名 11 | 12 | ```yaml 13 | - name: "屏蔽Yutube追踪" 14 | mitm: "*.youtube.com" 15 | filter: 16 | url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 17 | action: reject 18 | ``` 19 | 20 | 同时一条合格的规则需要符合以下要求: 21 | 22 | - 专注:一条规则只用来做一件事 23 | - 简单:使用简单的方法来处理,便与维护 24 | - 高效:尽量使用高效的方法,比如使用域名后缀和域名前缀来替换域名正则表达式 25 | -------------------------------------------------------------------------------- /docs/rule/action.md: -------------------------------------------------------------------------------- 1 | # Action 动作 2 | 3 | `Action` 用来对请求或者返回进行操作 4 | 5 | ## 候选项 6 | 7 | `Action`目前包含以下选项: 8 | 9 | - Reject 10 | - Redirect(String) 11 | - ModifyRequest(Modify) 12 | - ModifyResponse(Modify) 13 | - LogRes 14 | - LogReq 15 | 16 | ### Reject 拒绝 17 | 18 | `reject`类型直接返回`502`,用来拒绝某些请求,可以用来拒绝追踪和广告 19 | 20 | ```yaml 21 | - name: "reject CSDN" 22 | filter: 23 | domain-keyword: 'csdn' 24 | action: reject 25 | ``` 26 | 27 | ### Redirect 重定向 28 | 29 | `redirect`类型直接返回`302`重定向 30 | 31 | ```yaml 32 | - name: "youtube-1" 33 | filter: 34 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 35 | action: 36 | redirect: "$1$4" 37 | ``` 38 | 39 | ### ModifyRequest 修改请求 40 | 41 | `modify-request`用来修改请求,具体修改规则见 [修改器](rule/modify.md) 42 | 43 | ### ModifyResponse 修改返回 44 | 45 | `modify-response`用来修改返回,具体修改规则见 [修改器](rule/modify.md) 46 | 47 | ### Log 记录日志 48 | 49 | `log-req` 用来记录请求,`log-res` 用来记录返回 50 | 51 | ## 多个动作 52 | 53 | `actions`字段支持单个动作和多个动作,当需要执行多个动作时,应使用数组 54 | 55 | ```yaml 56 | - name: "youtube-1" 57 | filter: 58 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 59 | actions: 60 | - log-req: 61 | - redirect: "$1$4" 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/rule/filter.md: -------------------------------------------------------------------------------- 1 | # Filter 筛选器 2 | 3 | `Filter`用来筛选需要处理的请求和返回 4 | 5 | ## 候选项 6 | 7 | `Filter`目前包含以下类型: 8 | 9 | - All 10 | - Domain(String) 11 | - DomainKeyword(String) 12 | - DomainPrefix(String) 13 | - DomainSuffix(String) 14 | - UrlRegex(fancy_regex::Regex) 15 | 16 | > **注意** 17 | > 当前版本中,`domain`相关类型匹配的是`host`,通常情况下不会影响结果 18 | > 在网站使用非常规端口时,规则需要注明端口 19 | > 后续版本将会对此行为进行优化 20 | 21 | ### All 全部 22 | 23 | 指定筛选器为`all`时将会命中全部请求和返回,通常用来执行日志记录行为 24 | 25 | ```yaml 26 | - name: "log" 27 | filter: all 28 | action: 29 | - log-req 30 | - log-res 31 | ``` 32 | 33 | ### Domain 域名 34 | 35 | `domain`对域名进行全量匹配 36 | 37 | ```yaml 38 | - name: "redirect" 39 | filter: 40 | domain: 'none.zu1k.com' 41 | action: 42 | redirect: "https://zu1k.com/" 43 | ``` 44 | 45 | ### DomainKeyword 域名关键词 46 | 47 | `domain-keyword`对域名进行关键词匹配 48 | 49 | ```yaml 50 | - name: "reject CSDN" 51 | filter: 52 | domain-keyword: 'csdn' 53 | action: reject 54 | ``` 55 | 56 | ### DomainPrefix 域名前缀 57 | 58 | `domain-prefix`对域名进行前缀匹配 59 | 60 | ```yaml 61 | - name: "ad prefix" 62 | filter: 63 | domain-prefix: 'ads' // example: "ads.xxxxx.com" 64 | action: reject 65 | ``` 66 | 67 | ### DomainSuffix 域名后缀 68 | 69 | `domain-suffix`对域名进行后缀匹配 70 | 71 | 72 | ```yaml 73 | - name: "redirect" 74 | filter: 75 | domain-suffix: 'google.com.cn' 76 | action: 77 | redirect: "https://google.com" 78 | ``` 79 | 80 | ### UrlRegex Url正则 81 | 82 | `url-regex`对整个url进行正则匹配 83 | 84 | ```yaml 85 | - name: "youtube追踪" 86 | mitm: "*.youtube.com" 87 | filter: 88 | url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 89 | action: reject 90 | ``` 91 | 92 | ## 多个筛选器 93 | 94 | `filters`字段支持单个筛选器和多个筛选器,多个筛选器之间的关系为`或` 95 | 96 | ```yaml 97 | - name: "youtube-2" 98 | mitm: 99 | - "*.youtube.com" 100 | - "*.googlevideo.com" 101 | filters: 102 | - url-regex: '^https?:\/\/[\w-]+\.googlevideo\.com\/(?!(dclk_video_ads|videoplayback\?)).+(&oad|ctier)' 103 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/api\/stats\/ads' 104 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 105 | - url-regex: '^https?:\/\/\s.youtube.com/api/stats/qoe?.*adformat=' 106 | action: reject 107 | ``` 108 | 109 | 具有相同动作的多个规则可聚合为一个规则以便于维护 110 | -------------------------------------------------------------------------------- /docs/rule/modify.md: -------------------------------------------------------------------------------- 1 | # 修改器 2 | 3 | 修改器用来执行修改操作,包括修改请求和修改返回 4 | 5 | ## 候选项 6 | 7 | 根据需要修改的内容的位置,修改器分为以下几类: 8 | 9 | - Header(MapModify) 10 | - Cookie(MapModify) 11 | - Body(TextModify) 12 | 13 | ### TextModify 文本修改器 14 | 15 | `TextModify` 主要对文本就行修改,目前支持两种方式: 16 | 17 | - 直接设置文本内容 18 | - 普通替换或者正则替换 19 | 20 | #### 直接设置 21 | 22 | 对于plain类型直接设置,内容将被直接重置为指定文本 23 | 24 | ```yaml 25 | - name: "modify response body plain" 26 | filter: 27 | domain: '126.com' 28 | action: 29 | modify-response: 30 | body: "Hello 126.com, from Good-MITM" 31 | ``` 32 | 33 | #### 替换 34 | 35 | 替换支持简单替换和正则替换两种 36 | 37 | ##### 简单替换 38 | 39 | ```yaml 40 | - name: "modify response body replace" 41 | filter: 42 | domain-suffix: '163.com' 43 | action: 44 | modify-response: 45 | body: 46 | origin: "网易首页" 47 | new: "Good-MITM 首页" 48 | ``` 49 | 50 | ##### 正则替换 51 | 52 | ```yaml 53 | - name: "modify response body regex replace" 54 | filter: 55 | domain-suffix: 'zu1k.com' 56 | action: 57 | - modify-response: 58 | body: 59 | re: '(\d{4})' 60 | new: 'maybe $1' 61 | 62 | ``` 63 | 64 | ### MapModify 字典修改器 65 | 66 | `MapModify` 字典修改器主要针对字典类型的位置进行修改,例如 `header` 和 `cookies` 67 | 68 | `key` 代表字典的键,必须指定 69 | 70 | `value` 是 `TextModify` 类型,按照上文方法书写 71 | 72 | 如果指定 `remove` 为 `true`,则会删除该键值对 73 | 74 | ```yaml 75 | - name: "modify response header" 76 | filter: 77 | domain: '126.com' 78 | action: 79 | - modify-response: 80 | header: 81 | key: date 82 | value: 83 | origin: "2022" 84 | new: "1999" 85 | - modify-response: 86 | header: 87 | key: new-header-item 88 | value: Good-MITM 89 | - modify-response: 90 | header: 91 | key: server 92 | remove: true 93 | ``` 94 | 95 | ### Header 修改 96 | 97 | 见 `MapModify` 部分方法 98 | 99 | ### Cookie 修改 100 | 101 | 与 Header 修改方法一致 102 | 103 | 如果指定 `remove` 为 `true` 还会同时对应的移除`set-cookie`项 104 | 105 | ### Body修改 106 | 107 | 见 `TextModify` 部分 108 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=0.15,<0.16"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "good-mitm" 7 | requires-python = ">=3.7" 8 | classifiers = [ 9 | "Programming Language :: Rust", 10 | "Programming Language :: Python :: Implementation :: CPython", 11 | "Programming Language :: Python :: Implementation :: PyPy", 12 | ] 13 | 14 | 15 | [tool.maturin] 16 | bindings = "bin" 17 | -------------------------------------------------------------------------------- /rules/ads.yaml: -------------------------------------------------------------------------------- 1 | - name: "奈菲影视去广告" 2 | mitm: "*nfmovies*" 3 | filters: 4 | url-regex: '(nfmovies)(?!.*?(\.css|\.js|\.jpeg|\.png|\.gif)).*' 5 | actions: 6 | modify-response: 7 | body: 8 | origin: '' 9 | new: '' 10 | 11 | - name: "低端影视去广告" 12 | filters: 13 | domain-prefix: 'ddrk.me' 14 | actions: 15 | modify-response: 16 | body: 17 | origin: '' 18 | new: '' 19 | 20 | - name: "www.pianku.li 片库网" 21 | filters: 22 | domain-keyword: 'pianku' 23 | actions: 24 | modify-response: 25 | body: 26 | origin: '' 27 | new: '' 28 | 29 | - name: "m.yhdm.io 樱花动漫" 30 | filters: 31 | url-regex: '^http:\/\/m\.yhdm\.io\/bar\/yfgg\.js' 32 | actions: reject 33 | -------------------------------------------------------------------------------- /rules/demo.yaml: -------------------------------------------------------------------------------- 1 | - name: "redirect" 2 | filter: 3 | domain: "redirect.zu1k.com" 4 | action: 5 | redirect: "https://zu1k.com/" 6 | 7 | - name: "redirect regex" 8 | mitm: "*.zu1k.com" 9 | filter: 10 | url-regex: 'https://r.zu1k.com(.*)' 11 | action: 12 | redirect: "https://zu1k.com/$1" 13 | 14 | - name: "reject CSDN" 15 | filter: 16 | domain-keyword: 'csdn' 17 | action: reject 18 | -------------------------------------------------------------------------------- /rules/dxx.yaml: -------------------------------------------------------------------------------- 1 | - name: "dxx" 2 | mitm: "dxxsv.cyol.com" 3 | filter: 4 | url-regex: '^https:\/\/dxxsv\.cyol\.com.*\.mp4' 5 | action: 6 | redirect: "https://1s.video.zu1k.com/1s.mp4" 7 | 8 | -------------------------------------------------------------------------------- /rules/js.yaml: -------------------------------------------------------------------------------- 1 | - name: "js_modify_request" 2 | mitm: "*" 3 | filters: 4 | url-regex: '^https?:\/\/www\.baidu\.com' 5 | actions: 6 | js: | 7 | function process() { 8 | console.log("from quick_js"); 9 | 10 | if (data.request != undefined) { 11 | let req = data.request; 12 | console.log(req.headers["user-agent"]); 13 | return req; 14 | } 15 | if (data.response != undefined) { 16 | let res = data.response; 17 | if (res.body != undefined) { 18 | res.body = res.body.replaceAll("百度", "百毒"); 19 | } 20 | return res; 21 | } 22 | } 23 | process() 24 | 25 | -------------------------------------------------------------------------------- /rules/log.yaml: -------------------------------------------------------------------------------- 1 | - name: "log" 2 | filter: all 3 | action: 4 | - log-req 5 | - log-res 6 | -------------------------------------------------------------------------------- /rules/map_modify.yaml: -------------------------------------------------------------------------------- 1 | - name: "modify response header" 2 | filter: 3 | domain: '126.com' 4 | action: 5 | - modify-response: 6 | header: 7 | key: date 8 | value: 9 | origin: "2022" 10 | new: "1999" 11 | - modify-response: 12 | header: 13 | key: new-header-item 14 | value: Good-MITM 15 | - modify-response: 16 | header: 17 | key: server 18 | remove: true 19 | -------------------------------------------------------------------------------- /rules/rule_chain.yaml: -------------------------------------------------------------------------------- 1 | - name: "log-zu1k.com" 2 | filter: 3 | domain: "zu1k.com" 4 | action: 5 | - log-req 6 | 7 | - name: "modify res zu1k.com" 8 | filter: 9 | domain-suffix: "zu1k.com" 10 | action: 11 | modify-response: 12 | body: 13 | re: '(\d{4})' 14 | new: 'maybe $1' -------------------------------------------------------------------------------- /rules/text_modify.yaml: -------------------------------------------------------------------------------- 1 | - name: "modify response body plain" 2 | filter: 3 | domain: '126.com' 4 | action: 5 | modify-response: 6 | body: "Hello 126.com, from Good-MITM" 7 | 8 | - name: "modify response body replace" 9 | filter: 10 | domain-suffix: '163.com' 11 | action: 12 | modify-response: 13 | body: 14 | origin: "网易首页" 15 | new: "Good-MITM 首页" 16 | 17 | - name: "modify response body regex replace" 18 | filter: 19 | domain-suffix: 'zu1k.com' 20 | action: 21 | - modify-response: 22 | body: 23 | re: '(\d{4})' 24 | new: 'maybe $1' 25 | 26 | # access: https://www.baidu.com/s?wd=good-mitm 27 | - name: "modify request url" 28 | filter: 29 | domain-suffix: 'baidu.com' 30 | action: 31 | - modify-request: 32 | url: 33 | re: 'https:\/\/www\.baidu\.com\/s\?wd=(.*)' 34 | new: 'https://www.google.com/search?q=$1' 35 | -------------------------------------------------------------------------------- /rules/youtube.yaml: -------------------------------------------------------------------------------- 1 | # Youtube Ads 2 | - name: "youtube-1" 3 | mitm: "*.googlevideo.com" 4 | filter: 5 | url-regex: '(^https?:\/\/(?!redirector)[\w-]+\.googlevideo\.com\/(?!dclk_video_ads).+)(ctier=L)(&.+)' 6 | action: 7 | redirect: "$1$4" 8 | 9 | - name: "youtube-2" 10 | mitm: 11 | - "*.youtube.com" 12 | - "*.googlevideo.com" 13 | filters: 14 | - url-regex: '^https?:\/\/[\w-]+\.googlevideo\.com\/(?!(dclk_video_ads|videoplayback\?)).+(&oad|ctier)' 15 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/api\/stats\/ads' 16 | - url-regex: '^https?:\/\/(www|s)\.youtube\.com\/(pagead|ptracking)' 17 | - url-regex: '^https?:\/\/\s.youtube.com/api/stats/qoe?.*adformat=' 18 | action: reject 19 | -------------------------------------------------------------------------------- /rules/yuanshen.yaml: -------------------------------------------------------------------------------- 1 | - name: "redirect yuanshen" 2 | filter: 3 | - domain-suffix: 'mihoyo.com' 4 | - domain-suffix: 'hoyoverse.com' 5 | - domain-suffix: 'yuanshen.com' 6 | action: 7 | modify-request: 8 | url: 9 | re: 'https?:\/\/(.+)\.(.+)\.com(.*)' 10 | new: 'https://192.168.226.200:443$3' 11 | -------------------------------------------------------------------------------- /rules/yxbj.yaml: -------------------------------------------------------------------------------- 1 | - name: "yxbj" 2 | mitm: "config.app.yinxiang.com" 3 | filters: 4 | url-regex: 'https:\/\/config\.app\.yinxiang\.com\/configfiles\/json\/com\.yinxiang\.ios\/default\/YXFrontEnd\.mobile\.common' 5 | actions: 6 | js: | 7 | function process() { 8 | console.log("yxbj ===================================================="); 9 | 10 | if (data.request != undefined) { 11 | let req = data.request; 12 | return req; 13 | } 14 | 15 | console.log(data); 16 | if (data.response != undefined) { 17 | let res = data.response; 18 | if (res.body != undefined) { 19 | var body = JSON.parse(res.body); 20 | console.log(body); 21 | 22 | body['showAdsInPricingTier'] = "false"; 23 | body['is_pen_entrance_hidden'] = "true"; 24 | body['is_pen_purchase_button_hidden'] = "true"; 25 | 26 | body['pen_purchase_url'] = ""; 27 | body['pen_upgrade_enabled'] = "false"; 28 | body['space_beta_enabled'] = "true"; 29 | 30 | body['discovery_feed_visible'] = "false"; 31 | body['discovery_homepage_visible'] = "false"; 32 | 33 | body['personal_center_ad_info'] = ""; 34 | body['is_scan_pen_entrance_hidden'] = "true"; 35 | body['is_scan_pen_purchase_button_hidden'] = "true"; 36 | body['scan_pen_purchase_url'] = ""; 37 | 38 | body['home_promotion_for_eight_years'] = "{}"; 39 | body['home_scan_pen_cover_icon'] = "{}"; 40 | body['home_ever_pen_cover_icon'] = "{}"; 41 | 42 | body['ever_pen_guide_background'] = "{}"; 43 | body['ever_pen_guide_purchase_icon'] = "{}"; 44 | body['scan_pen_guide_purchase_icon'] = "{}"; 45 | 46 | body['everscan_android_purchase_url'] = ""; 47 | body['everpen_feedback_show'] = "false"; 48 | body['everhub_entrance_icon_show'] = "false"; 49 | body['everhub_entrance_icon_sourceurl'] = "{}"; 50 | body['everhub_pop_show'] = "false"; 51 | body['everhub_pop_sourceurl'] = "{}"; 52 | body['everhub_pop_count'] = "0"; 53 | body['home_recommended_banner_on_off_enable'] = "true"; 54 | 55 | body['is_everrec_entrance_hidden'] = "true"; 56 | body['everrec_upgrade_enabled'] = "false"; 57 | body['is_everrec_purchase_button_hidden'] = "true"; 58 | 59 | body['home_page_new_user_banner_freetrial_configuration'] = "{}"; 60 | body['home_everrec_pen_cover_icon'] = "{}"; 61 | 62 | body['everrec_pen_guide_purchase_icon'] = "{}"; 63 | body['personal_center_ad_info_plus_deeplink'] = "{}"; 64 | body['mine_wallet_promotion_enable'] = "false"; 65 | body['personal_center_ad_banner_list'] = "{}"; 66 | body['home_top_earphone_entrance'] = "{}"; 67 | body['home_top_course_entrance'] = "{}"; 68 | body['unpaid_order_entrance_enable'] = "false"; 69 | 70 | body['is_everbuds_entrance_hidden'] = "true"; 71 | body['is_everbuds_purchase_button_hidden'] = "true"; 72 | body['scan_pods_guide_purchase_icon'] = "{}"; 73 | body['home_everbuds_cover_icon'] = "{}"; 74 | body['is_everrec_pro_purchase_button_hidden'] = "true"; 75 | body['everrec_pen_pro_guide_purchase_icon'] = ""; 76 | body['everrec_pro_purchase_url'] = ""; 77 | body['is_everrec_pro_user_guide_button_hidden'] = "true"; 78 | 79 | body['show_evertime_feature_svip'] = "true"; 80 | body['show_evertime_feature_pro'] = "true"; 81 | body['show_verse_feature_svip'] = "true"; 82 | body['show_verse_feature_pro'] = "true"; 83 | body['ten_year_active'] = "false"; 84 | body['personal_center_ad_banner_newlist'] = "{\"banners\":[]}"; 85 | 86 | body['svip_show_scannable_feature'] = "true"; 87 | body['show_lightnote_feature'] = "true"; 88 | body['show_evermind_feature'] = "true"; 89 | body['show_kollector_feature'] = "true"; 90 | body['show_more_feature'] = "true"; 91 | 92 | 93 | body['realtime_transcription_duration'] = "9999"; 94 | 95 | res.body = JSON.stringify(body); 96 | } 97 | return res; 98 | } 99 | } 100 | process() 101 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | version = "Two" 2 | 3 | indent_style = "Block" 4 | imports_granularity = "Crate" 5 | -------------------------------------------------------------------------------- /src/ca.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use mitm_core::rcgen::*; 3 | use std::fs; 4 | 5 | pub fn gen_ca() -> Certificate { 6 | let cert = mitm_core::CertificateAuthority::gen_ca().expect("mitm-core generate cert"); 7 | let cert_crt = cert.serialize_pem().unwrap(); 8 | 9 | fs::create_dir("ca").unwrap(); 10 | 11 | println!("{}", cert_crt); 12 | if let Err(err) = fs::write("ca/cert.crt", cert_crt) { 13 | error!("cert file write failed: {}", err); 14 | } 15 | 16 | let private_key = cert.serialize_private_key_pem(); 17 | println!("{}", private_key); 18 | if let Err(err) = fs::write("ca/private.key", private_key) { 19 | error!("private key file write failed: {}", err); 20 | } 21 | 22 | cert 23 | } 24 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use mitm_core::rcgen::RcgenError; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum Error { 6 | #[error("invalid CA")] 7 | Tls(#[from] RcgenError), 8 | #[error("unable to decode response body")] 9 | Decode, 10 | #[error("unknown error")] 11 | Unknown, 12 | } 13 | -------------------------------------------------------------------------------- /src/file/frule.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use super::SingleOrMulti; 4 | 5 | #[derive(Debug, Clone, Deserialize, Serialize)] 6 | pub struct Rule { 7 | pub name: String, 8 | #[serde(alias = "mitm")] 9 | pub mitm_list: Option>, 10 | #[serde(alias = "filter")] 11 | pub filters: SingleOrMulti, 12 | #[serde(alias = "action")] 13 | pub actions: SingleOrMulti, 14 | } 15 | 16 | impl From for (rule::Rule, Vec) { 17 | fn from(rule: Rule) -> Self { 18 | let filters: Vec = rule 19 | .filters 20 | .into_vec() 21 | .iter() 22 | .map(rule::Filter::init) 23 | .collect(); 24 | 25 | let mut mitm_filters: Vec = filters 26 | .iter() 27 | .filter_map(rule::Filter::mitm_filtter_pattern) 28 | .collect(); 29 | 30 | let mut mitm_list_2 = match rule.mitm_list { 31 | Some(s) => s.into_vec().into_iter().collect(), 32 | None => vec![], 33 | }; 34 | mitm_filters.append(&mut mitm_list_2); 35 | 36 | let rule = rule::Rule { 37 | filters, 38 | actions: rule.actions.into_vec(), 39 | url: None, 40 | }; 41 | 42 | (rule, mitm_filters) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/file/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::error; 3 | use single_multi::SingleOrMulti; 4 | use std::{fs, io::BufReader, path::Path}; 5 | 6 | pub mod frule; 7 | mod single_multi; 8 | 9 | pub fn load_rules_amd_mitm_filters + Clone>( 10 | path: P, 11 | ) -> Result<(Vec, Vec)> { 12 | let m = fs::metadata(&path).expect("Not a valid path"); 13 | if m.file_type().is_dir() { 14 | load_rules_amd_mitm_filters_from_dir(path) 15 | } else { 16 | load_rules_amd_mitm_filters_from_file(path) 17 | } 18 | } 19 | 20 | fn load_rules_amd_mitm_filters_from_file + Clone>( 21 | path: P, 22 | ) -> Result<(Vec, Vec)> { 23 | let file = fs::File::open(path.clone())?; 24 | let reader = BufReader::new(file); 25 | let rules: Vec = match serde_yaml::from_reader(reader) { 26 | Ok(rules) => rules, 27 | Err(err) => { 28 | error!( 29 | "load rule ({}) failed: {err}", 30 | path.as_ref().to_str().unwrap() 31 | ); 32 | return Err(err.into()); 33 | } 34 | }; 35 | 36 | let (rules, filters) = rules 37 | .into_iter() 38 | .fold((vec![], vec![]), |(mut a, mut b), r| { 39 | let (rule, mut filters) = r.into(); 40 | a.push(rule); 41 | b.append(&mut filters); 42 | (a, b) 43 | }); 44 | 45 | Ok((rules, filters)) 46 | } 47 | 48 | fn load_rules_amd_mitm_filters_from_dir>( 49 | path: P, 50 | ) -> Result<(Vec, Vec)> { 51 | let dir = fs::read_dir(path).expect("Not a valid dir"); 52 | 53 | let (rules, filters) = dir 54 | .flatten() 55 | .filter(|f| f.file_type().is_ok()) 56 | .filter(|f| f.file_type().ok().unwrap().is_file()) 57 | .map(|f| load_rules_amd_mitm_filters_from_file(f.path())) 58 | .filter_map(|r| r.ok()) 59 | .fold( 60 | (vec![], vec![]), 61 | |(mut a, mut b), (mut rule, mut filters)| { 62 | a.append(&mut rule); 63 | b.append(&mut filters); 64 | (a, b) 65 | }, 66 | ); 67 | 68 | Ok((rules, filters)) 69 | } 70 | -------------------------------------------------------------------------------- /src/file/single_multi.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::vec::Vec; 3 | 4 | #[derive(Debug, Clone, Deserialize, Serialize)] 5 | #[serde(untagged)] 6 | pub enum SingleOrMulti { 7 | Single(T), 8 | Multi(Vec), 9 | } 10 | 11 | impl SingleOrMulti { 12 | pub fn into_vec(self) -> Vec { 13 | self.into() 14 | } 15 | } 16 | 17 | impl From> for Vec { 18 | fn from(sm: SingleOrMulti) -> Vec { 19 | match sm { 20 | SingleOrMulti::Single(v) => vec![v], 21 | SingleOrMulti::Multi(mv) => mv, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod ca; 2 | pub mod error; 3 | pub mod file; 4 | 5 | pub use hyper_proxy; 6 | pub use mitm_core; 7 | #[cfg(feature = "trust-cert")] 8 | pub use trust_cert; 9 | 10 | pub async fn shutdown_signal() { 11 | tokio::signal::ctrl_c() 12 | .await 13 | .expect("failed to install CTRL+C signal handler"); 14 | } 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use anyhow::{Ok, Result}; 4 | use clap::Parser; 5 | use hyper_proxy::Intercept; 6 | use log::*; 7 | use mitm_core::{CertificateAuthority, Proxy}; 8 | use rule::RuleHttpHandler; 9 | use rustls_pemfile as pemfile; 10 | use std::{fs, sync::Arc}; 11 | 12 | use good_mitm::*; 13 | 14 | #[derive(Parser)] 15 | #[clap(name = "Good Man in the Middle", version, about, author)] 16 | struct AppOpts { 17 | #[clap(subcommand)] 18 | subcmd: SubCommand, 19 | } 20 | 21 | #[derive(Parser)] 22 | enum SubCommand { 23 | /// run proxy serve 24 | Run(Run), 25 | /// gen your own ca cert and private key 26 | Genca(Genca), 27 | } 28 | 29 | #[derive(Parser)] 30 | struct Run { 31 | #[clap( 32 | short, 33 | long, 34 | default_value = "ca/private.key", 35 | help = "private key file path" 36 | )] 37 | key: String, 38 | #[clap(short, long, default_value = "ca/cert.crt", help = "cert file path")] 39 | cert: String, 40 | #[clap(short, long, help = "load rules from file or dir")] 41 | rule: String, 42 | #[clap(short, long, default_value = "127.0.0.1:34567", help = "bind address")] 43 | bind: String, 44 | #[clap(short, long, help = "upstream proxy")] 45 | proxy: Option, 46 | } 47 | 48 | #[derive(Parser)] 49 | struct Genca { 50 | #[clap(short, long, help = "install cert on your trust zone")] 51 | trust: bool, 52 | } 53 | 54 | fn main() { 55 | env_logger::builder().filter_level(LevelFilter::Info).init(); 56 | 57 | let opts = AppOpts::parse(); 58 | match opts.subcmd { 59 | SubCommand::Run(opts) => { 60 | run(&opts).unwrap(); 61 | } 62 | SubCommand::Genca(opts) => { 63 | #[allow(unused_variables)] 64 | let cert = ca::gen_ca(); 65 | if opts.trust { 66 | #[cfg(feature = "trust-cert")] 67 | trust_cert::trust_cert(cert); 68 | } 69 | } 70 | } 71 | } 72 | 73 | #[tokio::main] 74 | async fn run(opts: &Run) -> Result<()> { 75 | info!("CA Private key use: {}", opts.key); 76 | let private_key_bytes = fs::read(&opts.key).expect("ca private key file path not valid!"); 77 | let private_key = pemfile::pkcs8_private_keys(&mut private_key_bytes.as_slice()) 78 | .expect("Failed to parse private key"); 79 | let private_key = rustls::PrivateKey(private_key[0].clone()); 80 | 81 | info!("CA Certificate use: {}", opts.cert); 82 | let ca_cert_bytes = fs::read(&opts.cert).expect("ca cert file path not valid!"); 83 | let ca_cert = 84 | pemfile::certs(&mut ca_cert_bytes.as_slice()).expect("Failed to parse CA certificate"); 85 | let ca_cert = rustls::Certificate(ca_cert[0].clone()); 86 | 87 | let ca = CertificateAuthority::new( 88 | private_key, 89 | ca_cert, 90 | String::from_utf8(ca_cert_bytes).unwrap(), 91 | 1_000, 92 | ) 93 | .expect("Failed to create Certificate Authority"); 94 | 95 | info!("Http Proxy listen on: http://{}", opts.bind); 96 | 97 | let (rules, mitm_filters) = file::load_rules_amd_mitm_filters(&opts.rule)?; 98 | let rules = Arc::new(rules); 99 | let http_handler = RuleHttpHandler::new(rules); 100 | 101 | let proxy = Proxy::builder() 102 | .ca(ca.clone()) 103 | .listen_addr(opts.bind.parse().expect("bind address not valid!")) 104 | .upstream_proxy( 105 | opts.proxy 106 | .clone() 107 | .map(|proxy| hyper_proxy::Proxy::new(Intercept::All, proxy.parse().unwrap())), 108 | ) 109 | .shutdown_signal(shutdown_signal()) 110 | .mitm_filters(mitm_filters.clone()) 111 | .handler(http_handler.clone()) 112 | .build(); 113 | 114 | tokio::spawn(proxy.start_proxy()); 115 | 116 | tokio::signal::ctrl_c() 117 | .await 118 | .expect("failed to listen for event"); 119 | Ok(()) 120 | } 121 | --------------------------------------------------------------------------------