[
  {
    "path": ".github/actions-rs/grcov.yml",
    "content": "output-type: lcov\noutput-path: ./lcov.info\nsource-dir: ./src\nignore-dir:\n  - \"*.cargo/*\"\nignore:\n  - \"*.cargo*\"\n  - \"*rust*\"\n  - \"*configuration.rs\"\n  - \"*main.rs\"\n  - \"*proxy_target.rs\"\nignore-not-existing: true\nllvm: true\nexcl-start: (.*)begin-ignore-line(.*)\nexcl-stop: (.*)end-ignore-line(.*)\n"
  },
  {
    "path": ".github/workflows/clippy.yml",
    "content": "on: [push, pull_request]\nname: Clippy/Fmt\njobs:\n  clippy:\n    name: Clippy\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install nightly toolchain with clippy available\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: nightly\n          override: true\n          components: clippy\n\n      - name: Run cargo clippy\n        uses: actions-rs/cargo@v1\n        with:\n          command: clippy\n          args: -- -D warnings\n\n  rustfmt:\n    name: Format\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install nightly toolchain with rustfmt available\n        uses: actions-rs/toolchain@v1\n        with:\n          profile: minimal\n          toolchain: nightly\n          override: true\n          components: rustfmt\n\n      - name: Run cargo fmt\n        uses: actions-rs/cargo@v1\n        with:\n          command: fmt\n          args: --all -- --check\n"
  },
  {
    "path": ".github/workflows/grcov.yml",
    "content": "on: [push, pull_request]\n\nname: Code coverage with grcov\n\njobs:\n  grcov:\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os:\n          - ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v2\n\n      - name: Install toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: nightly\n          override: true\n          profile: minimal\n\n      - name: Execute tests\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --features plain_text\n        env:\n          CARGO_INCREMENTAL: 0\n          RUSTFLAGS: \"-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests\"\n\n      - name: Gather coverage data\n        id: coverage\n        uses: actions-rs/grcov@v0.1\n\n      - name: Pre-installing rust-covfix\n        uses: actions-rs/install@v0.1\n        with:\n          crate: rust-covfix\n          use-tool-cache: true\n\n      - name: Fix coverage data\n        id: fix-coverage\n        continue-on-error: true\n        run: rust-covfix lcov.info -o lcov.info\n\n      - name: Coveralls upload\n        uses: coverallsapp/github-action@master\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          parallel: true\n          path-to-lcov: lcov.info\n\n  grcov_finalize:\n    runs-on: ubuntu-latest\n    needs: grcov\n    steps:\n      - name: Coveralls finalization\n        uses: coverallsapp/github-action@master\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          parallel-finished: true\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "on: [push, pull_request]\nname: Tests\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Build\n      run: cargo build --verbose\n    - name: Run tests\n      run: cargo test --verbose\n    - name: Run tests for plain_text\n      run: cargo test --verbose --features plain_text\n\n"
  },
  {
    "path": ".gitignore",
    "content": "/target\n/.idea\nCargo.lock\n/log/*.log\n"
  },
  {
    "path": "COPYRIGHT",
    "content": "Copyrights in the http-tunnel project are retained by their contributors. No\ncopyright assignment is required to contribute to the http-tunnel project.\n\nFor full authorship information, see the version control history.\n\nExcept as otherwise noted (below and/or in individual files), http-tunnel is\nlicensed under the Apache License, Version 2.0 <LICENSE-APACHE> or\n<http://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n<LICENSE-MIT> or <http://opensource.org/licenses/MIT>, at your option.\n\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"http-tunnel\"\nversion = \"0.1.12\"\nauthors = [\"Eugene Retunsky\"]\nlicense = \"MIT OR Apache-2.0\"\nedition = \"2021\"\npublish = true\nreadme = \"README.md\"\nrepository = \"https://github.com/xnuter/http-tunnel\"\nhomepage = \"https://github.com/xnuter/http-tunnel\"\ndescription = \"\"\"\nHTTP Tunnel/TCP Proxy example written in Rust.\n\"\"\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\ntokio-native-tls = \"0.3\"\nnative-tls = \"0.2\"\nclap = { version = \"3.1.6\", features = [\"derive\"] }\nregex = \"1.3\"\nrand = \"0.8\"\nyaml-rust = \"0.4\"\nserde = { version = \"1.0\", features = [\"derive\"] }\nserde_derive = \"1.0\"\nserde_yaml = \"0.8\"\nserde_json = \"1.0\"\nserde_regex = \"1.1\"\nhumantime-serde = \"1.0\"\nasync-trait = \"0.1\"\nstrum = \"0.19\"\nstrum_macros = \"0.19\"\nderive_builder = \"0.9\"\nlog = \"0.4\"\nlog4rs = \"1.0.0-alpha-1\"\ntokio = { version = \"1\", features = [\"full\"] }\ntokio-util = { version = \"0.6\", features = [\"full\"] }\nbytes = \"1\"\nfutures = \"0.3\"\ntime = \"0.1\"\n\n[dev-dependencies]\ntokio-test = \"0.4\"\n\n[features]\n# For legacy software you can enable plain_text tunnelling\ndefault = []\nplain_text = []\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "[![Crate](https://img.shields.io/crates/v/http-tunnel.svg)](https://crates.io/crates/http-tunnel)\n![Clippy/Fmt](https://github.com/xnuter/http-tunnel/workflows/Clippy/Fmt/badge.svg)\n![Tests](https://github.com/xnuter/http-tunnel/workflows/Tests/badge.svg)\n[![Coverage Status](https://coveralls.io/repos/github/xnuter/http-tunnel/badge.svg?branch=main)](https://coveralls.io/github/xnuter/http-tunnel?branch=main)\n\n### Overview\n\nAn implementation of [HTTP Tunnel](https://en.wikipedia.org/wiki/HTTP_tunnel) in Rust, which can also function as a TCP proxy.\n\nThe core code is entirely abstract from the tunnel protocol or transport protocols.\nIn this example, it supports both `HTTP` and `HTTPS` with minimal additional code.\n\n*Please note*, this tunnel doesn't allow tunneling of plain text over HTTP tunnels (only HTTPS connections can be tunneled).\nIf you need this functionality you need to build the `http-tunnel` with the `plain_text` feature:\n\n```bash\ncargo build --release --features plain_text\n```\n\nE.g. it can be extended to run the tunnel over `QUIC+HTTP/3` or connect to another tunnel (as long as `AsyncRead + AsyncWrite` is satisfied for the implementation).\n\nYou can check [benchmarks](https://github.com/xnuter/perf-gauge/wiki/Benchmarking-TCP-Proxies-written-in-different-languages:-C,-CPP,-Rust,-Golang,-Java,-Python).\n\n[Read more](https://medium.com/@xnuter/writing-a-modern-http-s-tunnel-in-rust-56e70d898700) about the design.\n\n### Quick overview of source files\n\n* `configuration.rs` - contains configuration structures + a basic CLI\n  * see `config/` with configuration files/TLS materials\n* `http_tunnel_codec.rs` - a codec to process the initial HTTP request and encode a corresponding response.\n* `proxy_target.rs` - an abstraction + basic TCP implementation to connect target servers.\n  * contains a DNS resolver with a basic caching strategy (cache for a given `TTL`)\n* `relay.rs` - relaying data from one stream to another, `tunnel = upstream_relay + downstream_relay`\n  * also, contains basic `relay_policy`\n* `tunnel.rs` - a tunnel. It's built from:\n  * a tunnel handshake codec (e.g. `HttpTunnelCodec`)\n  * a target connector\n  * client connection as a stream\n* `main.rs` - application. May start `HTTP` or `HTTPS` tunnel (based on the command line parameters).\n  * emits log to `logs/application.log` (`log/` contains the actual output of the app from the browser session)\n  * metrics to `logs/metrics.log` - very basic, to demonstrate the concept.`\n          \n### Run demo\n\nInstall via `cargo`:\n\n```\ncargo install http-tunnel\n```\n\nNow you can start it without any configuration:\n\n```\n$ http-tunnel --bind 0.0.0.0:8080 http\n```\n\nThere are three modes.\n\n* `HTTPS`:\n```\n$ http-tunnel --config ./config/config.yaml \\\n              --bind 0.0.0.0:8443 \\\n              https --pk \"./config/domain.pfx\" --password \"6B9mZ*1hJ#xk\"\n```\n\n* `HTTP`:\n```\n$ http-tunnel --config ./config/config-browser.yaml --bind 0.0.0.0:8080 http\n```\n\n* `TCP Proxy`:\n```\n$ http-tunnel --config ./config/config-browser.yaml --bind 0.0.0.0:8080 tcp --destination $REMOTE_HOST:$REMOTE_PORT\n```\n\n### Testing with a browser (HTTP)\n\nIn Firefox, you can set the HTTP proxy to `localhost:8080`. Make sure you run it with the right configuration:\n\nhttps://support.mozilla.org/en-US/kb/connection-settings-firefox\n\n(use HTTP Proxy and check \"use this proxy for FTP and HTTPS\")\n\n```\n$ ./target/release/http-tunnel --config ./config/config-browser.yaml --bind 0.0.0.0:8080 http\n```\n\n### Testing with cURL (HTTPS)\n\nThis proxy can be tested with `cURL`:\n\nAdd `simple.rust-http-tunnel.org'` to `/etc/hosts`:\n```\n$ echo '127.0.0.1       simple.rust-http-tunnel.org' | sudo tee -a /etc/hosts\n```\n\nThen try access-listed targets (see `./config/config.yaml`), e.g:\n\n```\ncurl -vp --proxy https://simple.rust-http-tunnel.org:8443  --proxy-cacert ./config/domain.crt https://www.wikipedia.org\n``` \n\nYou can also play around with targets that are not allowed.\n\n### Privacy\n\nThe application cannot see the plaintext data.\n\nThe application doesn't log any information that may help identify clients (such as IP, auth tokens).\nOnly general information (events, errors, data sizes) is logged for monitoring purposes. \n\n#### DDoS protection\n\n* `Slowloris` attack (opening tons of slow connections)\n* Sending requests resulting in large responses\n\nSome of them can be solved by introducing rate/age limits and inactivity timeouts.\n\n### Build\n\nInstall `cargo` - [follow these instructions](https://doc.rust-lang.org/cargo/getting-started/installation.html)\n\nOn `Debian` to fix [OpenSSL build issue](https://docs.rs/openssl/0.10.30/openssl/):\n\n```\nsudo apt-get install pkg-config libssl-dev\n```\n\n### Installation\n\nOn MacOS:\n\n```\ncurl https://sh.rustup.rs -sSf | sh\ncargo install http-tunnel\nhttp-tunnel --bind 0.0.0.0:8080 http\n```\n\nOn Debian based Linux:\n\n```\ncurl https://sh.rustup.rs -sSf | sh\nsudo apt-get -y install gcc pkg-config libssl-dev\ncargo install http-tunnel\nhttp-tunnel --bind 0.0.0.0:8080 http\n```\n"
  },
  {
    "path": "config/README.md",
    "content": "### PCKS12 generation\n\nIn case you want to generate your own TLS materials.\nThese self-signed certs are generated for `simple.rust-http-tunnel.org` domain.\n\nGenerate cert/pk:\n\n```\nopenssl req \\\n       -newkey rsa:2048 -nodes -keyout domain.key \\\n       -x509 -days 365 -out domain.crt\n```\n\nCreate a `pkcs12` file:\n\n```\nopenssl pkcs12 \\\n       -inkey domain.key \\\n       -in domain.crt \\\n       -export -out domain.pfx\n```\n"
  },
  {
    "path": "config/config-browser.yaml",
    "content": "# a configuration to test it with your browser\n# set `localhost:8080` as HTTP/HTTPS proxy (run it in HTTP mode)\n\nclient_connection:\n  initiation_timeout: 100s\n  relay_policy:\n    idle_timeout: 300s\n    min_rate_bpm: 0\n    max_rate_bps: 10000000\n\ntarget_connection:\n  dns_cache_ttl: 60s\n  allowed_targets: \".*\" # anything\n  connect_timeout: 100s\n  relay_policy:\n    idle_timeout: 100s\n    min_rate_bpm: 0\n    max_rate_bps: 10000000\n\n"
  },
  {
    "path": "config/config.yaml",
    "content": "client_connection:\n  initiation_timeout: 10s\n  # we want to make sure connections are not under-utilized or over-utilized\n  relay_policy:\n    idle_timeout: 30s\n    min_rate_bpm: 1000\n    max_rate_bps: 10000\n\ntarget_connection:\n  dns_cache_ttl: 60s\n  allowed_targets: \"^(?i)([a-z]+)\\\\.(wikipedia|rust-lang)\\\\.org:443$\"\n  connect_timeout: 10s\n  relay_policy:\n    idle_timeout: 10s\n    min_rate_bpm: 1000\n    max_rate_bps: 10000\n    # Other traffic policies may go here\n    # max_lifetime: 100s\n    # max_total_payload: 10mb\n\n  # we can extend this with TCP policies, if necessary. E.g.\n  # tcp_policies:\n  #   keep_idle: 60s\n  #   linger, nodelay, ....\n"
  },
  {
    "path": "config/domain.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIID0DCCArgCCQC39O6tC7bK5DANBgkqhkiG9w0BAQsFADCBqTELMAkGA1UEBhMC\nVVMxEzARBgNVBAgMCldhc2hpbmd0b24xETAPBgNVBAcMCEJlbGxldnVlMQ8wDQYD\nVQQKDAZ4bnV0ZXIxGDAWBgNVBAsMD0V1Z2VuZSBSZXR1bnNreTEkMCIGA1UEAwwb\nc2ltcGxlLnJ1c3QtaHR0cC10dW5uZWwub3JnMSEwHwYJKoZIhvcNAQkBFhJyZXR1\nbnNreUBnbWFpbC5jb20wHhcNMjAwOTEzMDAwNjI2WhcNMzAwOTExMDAwNjI2WjCB\nqTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0b24xETAPBgNVBAcMCEJl\nbGxldnVlMQ8wDQYDVQQKDAZ4bnV0ZXIxGDAWBgNVBAsMD0V1Z2VuZSBSZXR1bnNr\neTEkMCIGA1UEAwwbc2ltcGxlLnJ1c3QtaHR0cC10dW5uZWwub3JnMSEwHwYJKoZI\nhvcNAQkBFhJyZXR1bnNreUBnbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB\nDwAwggEKAoIBAQCiyxVy9mmbJVRjAK6sOkcU21lmZRUQEa8LZx09tuU89K81+CvZ\nJLA/QjBPQq3pbIxWYIcb+pjro1kPqGw/z3gEcpTF6v5b//DbSCi/BGAHTRyaGqJ4\nGHrh8kMmnEcxrzX5hIONwgnOP1b5s5Ih+0Psiyh+cwItJWuVemwa/BWmIxZEqpC/\nm4vKzRTV5a50PTH3LCW9bHKzuj7OIU+dfTiR0CVJIY2tJjZldflXdhmzWxFAb5jf\nQFNXKqMfc4lCGYz37dyDZf4m56FKEa5c41r32dMlq0PIlqWA41gtCr7zhDUB9yk9\nzU61MmGRTk7qAFB1SakwMVQl5bFn8N3U4Em5AgMBAAEwDQYJKoZIhvcNAQELBQAD\nggEBAJasp7TuZcv+vDljgm/TMr8+x+8qY9v7hfefqNz+p1LNqvpRsC4m9immTYOM\naw0FUUv20Syb4thxAE52GImpboqVwiYNK0huX62Htvs8DnxsiOs7y/rC29MxQMiR\nFkbYVZFsUOVVRCru8ZORRT1io7NnlueQN3s7/NOCl5XYssoH1868/+TqPxGG9rQw\nmk+KqDRDk2CNsixxu7MYpRFWd5+mglYwS9EJRLXfMgjJIYBQ+ID3jNP39qK7lnxa\njb1rKBK0g04XtBUkKH3Y8V11JRD+I/Ylnn1qkg8ZJAoMTQV4TMgh4hQWFYOAgHWC\neGIwbeixjlk3N6pdYCaGLMfZkhk=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "config/domain.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCiyxVy9mmbJVRj\nAK6sOkcU21lmZRUQEa8LZx09tuU89K81+CvZJLA/QjBPQq3pbIxWYIcb+pjro1kP\nqGw/z3gEcpTF6v5b//DbSCi/BGAHTRyaGqJ4GHrh8kMmnEcxrzX5hIONwgnOP1b5\ns5Ih+0Psiyh+cwItJWuVemwa/BWmIxZEqpC/m4vKzRTV5a50PTH3LCW9bHKzuj7O\nIU+dfTiR0CVJIY2tJjZldflXdhmzWxFAb5jfQFNXKqMfc4lCGYz37dyDZf4m56FK\nEa5c41r32dMlq0PIlqWA41gtCr7zhDUB9yk9zU61MmGRTk7qAFB1SakwMVQl5bFn\n8N3U4Em5AgMBAAECggEADLSjAO0Agw5fzrZP67tErvkLujUrdqyap94tZxKuQ5qp\nTiIHchQt+VH2KUl//1bsgRVZljJx2vpNyi4P/M75hAdZWzUjExUfvE2eeIIj6I35\nLIHlqk/mz1m0KgBKgjM1mDridZ7uWv2QkT6VqjdNLtoRmATr55AjHHCInXaNTgER\nLiSYJdxOubllLmXUuRZk/7/R1hDajmcR06QGJs994+/9ZyWzow82VuCJLUzFRSF3\nkj3Cqb1p9ShpL2e/DN+QpgNq1gs7b3YDNl2fgix6c7V2LxM/G6qfT+i4rGAUhONN\n9Jx49cjTA7TsjD+NvoFY0GvVt8Gn4igaviVZA9jWAQKBgQDPNKxQd3PLDWujsHIk\nbepG6Bm4S3/mCmsVnSvqaIsEXJQgAdMXt60J5vo0pNFrxIcrQAp+7JJPtdMm/uOB\nTFOosV2Y8oDwhPXboTI/QHtnS6624ziUJbF2qfVRGbIPdu76OVv/7PCxZsUhDBjI\nF0ZN9iFP5y7cLA7e2JHrDDQrwQKBgQDJIQVkvqXyYvx/hUtYB6tZqOGZvnbaeXsx\nQ3vv5omRE8H8AzwaiBL0PBMp2tXMhIl9nTtD6femtf5UpdihFB8z7KwmEGp1pka4\nrZXrJGofZ+fzPh9LpJxDSHBgNTEaJHSn2CZeZ6UR/mbVQcnFlNyT2aneSRdOmEtk\nMJLkHBx7+QKBgQCYLvGYMAOl0PeLw94xj2EQLwwk5Z7MUD6SI1vL0Hi5/Vz1nSFz\nO/4lVbXS0HLXmgJE68ZJrmtPjBXHgFGL94lCTvKVkRbOkHkalGwZNLzuAxIRVRWL\nCZwrsWxx4lN7NDkVIufFMjsdsIN8YCwbWazTOcEBtKQgJWPOnHWfktkGgQKBgQC9\nytY/GhSYZMYmQ480k5AjPFUe8ndPdIFGnIrQd/hqmX1dJWRLGQrhw+rFfUZxBsSD\nb6KkVJ0oiOZl1FZWshk7s2NDTAxZ1r03uj4VNTibSD6972oyxDPc3feFIcyjAbG/\nTR3vydgf4bQCG2Gee/ml3ykHpGtE9Dt4YMnMTaanaQKBgQC7kiA/r0Ob05OCI5iz\nLyhLLBJneA4OeSoEq64HHnjOfKxsfe6d/SDmUAvwcvB+2GO5B7XubMu5nrg47izp\nny/eAVVmMYRzCY7mrCt5CC/1DBtdkAgdjmXXFlGZ74wjl6O65ZKsgIbkn41dGR2N\nj+2D+93dPvODA2LMD1VaRFBqQQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "config/log4rs.yaml",
    "content": "refresh_rate: 30 seconds\n\nappenders:\n  stdout:\n    kind: console\n\n  metrics:\n    kind: rolling_file\n    path: log/metrics.log\n    policy:\n      kind: compound\n      roller:\n        kind: fixed_window\n        count: 10\n        base: 1\n        pattern: log/metrics.{}.log\n      trigger:\n        kind: size\n        limit: 10mb\n    encoder:\n      pattern: \"{d} - {m}{n}\"\n\n  application:\n    kind: rolling_file\n    path: log/application.log\n    policy:\n      kind: compound\n      roller:\n        kind: fixed_window\n        count: 10\n        base: 1\n        pattern: log/application.{}.log\n      trigger:\n        kind: size\n        limit: 10mb\n    encoder:\n      pattern: \"{d} - [{l}] {f}:{L} - {m}{n}\"\n\nroot:\n  level: info\n  appenders:\n    - application\n\nloggers:\n  metrics:\n    level: info\n    appenders:\n      - metrics\n    additive: false"
  },
  {
    "path": "misc/diagrams/components-high-level.puml",
    "content": "@startuml\nClient <--> Tunnel: L4 handshake\nClient -> Tunnel: Negotiate Target\nactivate Tunnel\nTunnel <-> Target: L4 Handshake\nactivate Target\nTunnel -> Client: Success\ngroup Full-Duplex, endless loop\nClient --> Tunnel: upstream\nTunnel --> Target: upstream\nTunnel <-- Target: downstream\nClient <-- Tunnel: downstream\nend\ndeactivate Tunnel\ndeactivate Target\n@enduml"
  },
  {
    "path": "misc/diagrams/components.puml",
    "content": "@startuml\nClient <--> Tunnel: TCP handshake\ngroup HTTPS Tunnel only\nClient <--> Tunnel: TLS handshake\nend\nClient -> Tunnel: HTTP CONNECT (target)\nactivate Tunnel\nTunnel <-> Target: TCP Handshake\nactivate Target\nTunnel -> Client: 200 OK\ngroup Full-Duplex, endless loop\nClient --> Tunnel: upstream\nTunnel --> Target: upstream\nTunnel <-- Target: downstream\nClient <-- Tunnel: downstream\nend\ndeactivate Tunnel\ndeactivate Target\n@enduml"
  },
  {
    "path": "src/configuration.rs",
    "content": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or\n/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your\n/// option. This file may not be copied, modified, or distributed\n/// except according to those terms.\nuse crate::relay::{RelayPolicy, NO_BANDWIDTH_LIMIT, NO_TIMEOUT};\nuse clap::Args;\nuse clap::Parser;\nuse clap::Subcommand;\nuse log::{error, info};\nuse native_tls::Identity;\nuse regex::Regex;\nuse std::fs::File;\nuse std::io::{Error, ErrorKind, Read};\nuse std::time::Duration;\nuse tokio::io;\n\n#[derive(Deserialize, Clone)]\npub struct ClientConnectionConfig {\n    #[serde(with = \"humantime_serde\")]\n    pub initiation_timeout: Duration,\n    pub relay_policy: RelayPolicy,\n}\n\n#[derive(Deserialize, Clone)]\npub struct TargetConnectionConfig {\n    #[serde(with = \"humantime_serde\")]\n    pub dns_cache_ttl: Duration,\n    #[serde(with = \"serde_regex\")]\n    pub allowed_targets: Regex,\n    #[serde(with = \"humantime_serde\")]\n    pub connect_timeout: Duration,\n    pub relay_policy: RelayPolicy,\n}\n\n#[derive(Deserialize, Clone)]\npub struct TunnelConfig {\n    pub client_connection: ClientConnectionConfig,\n    pub target_connection: TargetConnectionConfig,\n}\n\n#[derive(Clone)]\npub enum ProxyMode {\n    Http,\n    Https(Identity),\n    Tcp(String),\n}\n\n#[derive(Clone, Builder)]\npub struct ProxyConfiguration {\n    pub mode: ProxyMode,\n    pub bind_address: String,\n    pub tunnel_config: TunnelConfig,\n}\n\n#[derive(Parser, Debug)]\n#[clap(author, version, about, long_about = None)]\n#[clap(propagate_version = true)]\nstruct Cli {\n    /// Configuration file.\n    #[clap(long)]\n    config: Option<String>,\n    /// Bind address, e.g. 0.0.0.0:8443.\n    #[clap(long)]\n    bind: String,\n    #[clap(subcommand)]\n    command: Commands,\n}\n\n#[derive(Subcommand, Debug)]\nenum Commands {\n    Http(HttpOptions),\n    Https(HttpsOptions),\n    Tcp(TcpOptions),\n}\n\n#[derive(Args, Debug)]\n#[clap(about = \"Run the tunnel in HTTP mode\", long_about = None)]\n#[clap(author, version, long_about = None)]\n#[clap(propagate_version = true)]\nstruct HttpOptions {}\n\n#[derive(Args, Debug)]\n#[clap(about = \"Run the tunnel in HTTPS mode\", long_about = None)]\n#[clap(author, version, long_about = None)]\n#[clap(propagate_version = true)]\nstruct HttpsOptions {\n    /// pkcs12 filename.\n    #[clap(long)]\n    pk: String,\n    /// Password for the pkcs12 file.\n    #[clap(long)]\n    password: String,\n}\n\n#[derive(Args, Debug)]\n#[clap(about = \"Run the tunnel in TCP proxy mode\", long_about = None)]\n#[clap(author, version, long_about = None)]\n#[clap(propagate_version = true)]\nstruct TcpOptions {\n    /// Destination address, e.g. 10.0.0.2:8443.\n    #[clap(short, long)]\n    destination: String,\n}\n\nimpl Default for TunnelConfig {\n    fn default() -> Self {\n        // by default no restrictions\n        Self {\n            client_connection: ClientConnectionConfig {\n                initiation_timeout: NO_TIMEOUT,\n                relay_policy: RelayPolicy {\n                    idle_timeout: NO_TIMEOUT,\n                    min_rate_bpm: 0,\n                    max_rate_bps: NO_BANDWIDTH_LIMIT,\n                },\n            },\n            target_connection: TargetConnectionConfig {\n                dns_cache_ttl: NO_TIMEOUT,\n                allowed_targets: Regex::new(\".*\").expect(\"Bug: bad default regexp\"),\n                connect_timeout: NO_TIMEOUT,\n                relay_policy: RelayPolicy {\n                    idle_timeout: NO_TIMEOUT,\n                    min_rate_bpm: 0,\n                    max_rate_bps: NO_BANDWIDTH_LIMIT,\n                },\n            },\n        }\n    }\n}\n\nimpl ProxyConfiguration {\n    /// For this demo the app reads the key/certs from the disk.\n    /// In production more secure approaches should be used (at least encryption with regularly\n    /// rotated keys, storing sensitive data on RAM disk only, etc.)\n    pub fn from_command_line() -> io::Result<ProxyConfiguration> {\n        let cli: Cli = Cli::parse();\n\n        let config = cli.config;\n        let bind_address = cli.bind;\n\n        let mode = match cli.command {\n            Commands::Http(_) => {\n                info!(\n                    \"Starting in HTTP mode: bind: {}, configuration: {:?}\",\n                    bind_address, config\n                );\n                ProxyMode::Http\n            }\n            Commands::Https(https) => {\n                let pkcs12_file = https.pk.as_str();\n                let password = https.password.as_str();\n\n                let identity = ProxyConfiguration::tls_identity_from_file(pkcs12_file, password)?;\n                info!(\n                    \"Starting in HTTPS mode: pkcs12: {}, password: {}, bind: {}, configuration: {:?}\",\n                    pkcs12_file,\n                    !password.is_empty(),\n                    bind_address,\n                    config\n                );\n                ProxyMode::Https(identity)\n            }\n            Commands::Tcp(tcp) => {\n                let destination = tcp.destination;\n                info!(\n                    \"Starting in TCP mode: destination: {}, configuration: {:?}\",\n                    destination, config\n                );\n                ProxyMode::Tcp(destination)\n            }\n        };\n\n        let tunnel_config = match config {\n            None => TunnelConfig::default(),\n            Some(config) => ProxyConfiguration::read_tunnel_config(config.as_str())?,\n        };\n\n        Ok(ProxyConfigurationBuilder::default()\n            .bind_address(bind_address)\n            .mode(mode)\n            .tunnel_config(tunnel_config)\n            .build()\n            .expect(\"ProxyConfigurationBuilder failed\"))\n    }\n\n    fn tls_identity_from_file(filename: &str, password: &str) -> io::Result<Identity> {\n        let mut file = File::open(filename).map_err(|e| {\n            error!(\"Error opening PKSC12 file {}: {}\", filename, e);\n            e\n        })?;\n\n        let mut identity = vec![];\n\n        file.read_to_end(&mut identity).map_err(|e| {\n            error!(\"Error reading file {}: {}\", filename, e);\n            e\n        })?;\n\n        Identity::from_pkcs12(&identity, password).map_err(|e| {\n            error!(\"Cannot process PKCS12 file {}: {}\", filename, e);\n            Error::from(ErrorKind::InvalidInput)\n        })\n    }\n\n    fn read_tunnel_config(filename: &str) -> io::Result<TunnelConfig> {\n        let mut file = File::open(filename).map_err(|e| {\n            error!(\"Error opening config file {}: {}\", filename, e);\n            e\n        })?;\n\n        let mut yaml = vec![];\n\n        file.read_to_end(&mut yaml).map_err(|e| {\n            error!(\"Error reading file {}: {}\", filename, e);\n            e\n        })?;\n\n        let result: TunnelConfig = serde_yaml::from_slice(&yaml).map_err(|e| {\n            error!(\"Error parsing yaml {}: {}\", filename, e);\n            Error::from(ErrorKind::InvalidInput)\n        })?;\n\n        Ok(result)\n    }\n}\n"
  },
  {
    "path": "src/http_tunnel_codec.rs",
    "content": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or\n/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your\n/// option. This file may not be copied, modified, or distributed\n/// except according to those terms.\nuse std::fmt::Write;\n\nuse async_trait::async_trait;\nuse bytes::BytesMut;\nuse log::debug;\nuse regex::Regex;\nuse tokio::io::{Error, ErrorKind};\nuse tokio_util::codec::{Decoder, Encoder};\n\nuse crate::proxy_target::Nugget;\nuse crate::tunnel::{EstablishTunnelResult, TunnelCtx, TunnelTarget};\nuse core::fmt;\nuse std::str::Split;\n\nconst REQUEST_END_MARKER: &[u8] = b\"\\r\\n\\r\\n\";\n/// A reasonable value to limit possible header size.\nconst MAX_HTTP_REQUEST_SIZE: usize = 16384;\n\n/// HTTP/1.1 request representation\n/// Supports only `CONNECT` method, unless the `plain_text` feature is enabled\nstruct HttpConnectRequest {\n    uri: String,\n    nugget: Option<Nugget>,\n    // out of scope of this demo, but let's put it here for extensibility\n    // e.g. Authorization/Policies headers\n    // headers: Vec<(String, String)>,\n}\n\n#[derive(Builder, Eq, PartialEq, Debug, Clone)]\npub struct HttpTunnelTarget {\n    pub target: String,\n    pub nugget: Option<Nugget>,\n    // easily can be extended with something like\n    // policies: Vec<TunnelPolicy>\n}\n\n/// Codec to extract `HTTP/1.1 CONNECT` requests and build a corresponding `HTTP` response.\n#[derive(Clone, Builder)]\npub struct HttpTunnelCodec {\n    tunnel_ctx: TunnelCtx,\n    enabled_targets: Regex,\n}\n\nimpl Decoder for HttpTunnelCodec {\n    type Item = HttpTunnelTarget;\n    type Error = EstablishTunnelResult;\n\n    fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {\n        if !got_http_request(src) {\n            return Ok(None);\n        }\n\n        match HttpConnectRequest::parse(src) {\n            Ok(parsed_request) => {\n                if !self.enabled_targets.is_match(&parsed_request.uri) {\n                    debug!(\n                        \"Target `{}` is not allowed. Allowed: `{}`, CTX={}\",\n                        parsed_request.uri, self.enabled_targets, self.tunnel_ctx\n                    );\n                    Err(EstablishTunnelResult::Forbidden)\n                } else {\n                    Ok(Some(\n                        HttpTunnelTargetBuilder::default()\n                            .target(parsed_request.uri)\n                            .nugget(parsed_request.nugget)\n                            .build()\n                            .expect(\"HttpTunnelTargetBuilder failed\"),\n                    ))\n                }\n            }\n            Err(e) => Err(e),\n        }\n    }\n}\n\nimpl Encoder<EstablishTunnelResult> for HttpTunnelCodec {\n    type Error = std::io::Error;\n\n    fn encode(\n        &mut self,\n        item: EstablishTunnelResult,\n        dst: &mut BytesMut,\n    ) -> Result<(), Self::Error> {\n        let (code, message) = match item {\n            EstablishTunnelResult::Ok => (200, \"OK\"),\n            EstablishTunnelResult::OkWithNugget => {\n                // do nothing, the upstream should respond instead\n                return Ok(());\n            }\n            EstablishTunnelResult::BadRequest => (400, \"BAD_REQUEST\"),\n            EstablishTunnelResult::Forbidden => (403, \"FORBIDDEN\"),\n            EstablishTunnelResult::OperationNotAllowed => (405, \"NOT_ALLOWED\"),\n            EstablishTunnelResult::RequestTimeout => (408, \"TIMEOUT\"),\n            EstablishTunnelResult::TooManyRequests => (429, \"TOO_MANY_REQUESTS\"),\n            EstablishTunnelResult::ServerError => (500, \"SERVER_ERROR\"),\n            EstablishTunnelResult::BadGateway => (502, \"BAD_GATEWAY\"),\n            EstablishTunnelResult::GatewayTimeout => (504, \"GATEWAY_TIMEOUT\"),\n        };\n\n        dst.write_fmt(format_args!(\"HTTP/1.1 {} {}\\r\\n\\r\\n\", code as u32, message))\n            .map_err(|_| std::io::Error::from(std::io::ErrorKind::Other))\n    }\n}\n\n#[async_trait]\nimpl TunnelTarget for HttpTunnelTarget {\n    type Addr = String;\n\n    fn target_addr(&self) -> Self::Addr {\n        self.target.clone()\n    }\n\n    fn has_nugget(&self) -> bool {\n        self.nugget.is_some()\n    }\n\n    fn nugget(&self) -> &Nugget {\n        self.nugget\n            .as_ref()\n            .expect(\"Cannot use this method without checking `has_nugget`\")\n    }\n}\n\n// cov:begin-ignore-line\nimpl fmt::Display for HttpTunnelTarget {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.target)\n    }\n}\n// cov:end-ignore-line\n\n#[cfg(not(feature = \"plain_text\"))]\nfn got_http_request(buffer: &BytesMut) -> bool {\n    buffer.len() >= MAX_HTTP_REQUEST_SIZE || buffer.ends_with(REQUEST_END_MARKER)\n}\n\n#[cfg(feature = \"plain_text\")]\nfn got_http_request(buffer: &BytesMut) -> bool {\n    buffer.len() >= MAX_HTTP_REQUEST_SIZE\n        || buffer\n            .windows(REQUEST_END_MARKER.len())\n            .find(|w| *w == REQUEST_END_MARKER)\n            .is_some()\n}\n\nimpl From<Error> for EstablishTunnelResult {\n    fn from(e: Error) -> Self {\n        match e.kind() {\n            ErrorKind::TimedOut => EstablishTunnelResult::GatewayTimeout,\n            _ => EstablishTunnelResult::BadGateway,\n        }\n    }\n}\n\n/// Basic HTTP Request parser which only purpose is to parse `CONNECT` requests.\nimpl HttpConnectRequest {\n    pub fn parse(http_request: &[u8]) -> Result<Self, EstablishTunnelResult> {\n        HttpConnectRequest::precondition_size(http_request)?;\n        HttpConnectRequest::precondition_legal_characters(http_request)?;\n\n        let http_request_as_string =\n            String::from_utf8(http_request.to_vec()).expect(\"Contains only ASCII\");\n\n        let mut lines = http_request_as_string.split(\"\\r\\n\");\n\n        let request_line = HttpConnectRequest::parse_request_line(\n            lines\n                .next()\n                .expect(\"At least a single line is present at this point\"),\n        )?;\n\n        let has_nugget = request_line.3;\n\n        if has_nugget {\n            Ok(Self {\n                uri: HttpConnectRequest::extract_destination_host(&mut lines, request_line.1)\n                    .unwrap_or_else(|| request_line.1.to_string()),\n                nugget: Some(Nugget::new(http_request)),\n            })\n        } else {\n            Ok(Self {\n                uri: request_line.1.to_string(),\n                nugget: None,\n            })\n        }\n    }\n\n    fn extract_destination_host(lines: &mut Split<&str>, endpoint: &str) -> Option<String> {\n        const HOST_HEADER: &str = \"host:\";\n\n        lines\n            .find(|line| line.to_ascii_lowercase().starts_with(HOST_HEADER))\n            .map(|line| line[HOST_HEADER.len()..].trim())\n            .map(|host| {\n                let mut host = String::from(host);\n                if host.rfind(':').is_none() {\n                    let default_port = if endpoint.to_ascii_lowercase().starts_with(\"https://\") {\n                        \":443\"\n                    } else {\n                        \":80\"\n                    };\n                    host.push_str(default_port);\n                }\n                host\n            })\n    }\n\n    fn parse_request_line(\n        request_line: &str,\n    ) -> Result<(&str, &str, &str, bool), EstablishTunnelResult> {\n        let request_line_items = request_line.split(' ').collect::<Vec<&str>>();\n        HttpConnectRequest::precondition_well_formed(request_line, &request_line_items)?;\n\n        let method = request_line_items[0];\n        let uri = request_line_items[1];\n        let version = request_line_items[2];\n\n        let has_nugget = HttpConnectRequest::check_method(method)?;\n        HttpConnectRequest::check_version(version)?;\n\n        Ok((method, uri, version, has_nugget))\n    }\n\n    fn precondition_well_formed(\n        request_line: &str,\n        request_line_items: &[&str],\n    ) -> Result<(), EstablishTunnelResult> {\n        if request_line_items.len() != 3 {\n            debug!(\"Bad request line: `{:?}`\", request_line,);\n            Err(EstablishTunnelResult::BadRequest)\n        } else {\n            Ok(())\n        }\n    }\n\n    fn check_version(version: &str) -> Result<(), EstablishTunnelResult> {\n        if version != \"HTTP/1.1\" {\n            debug!(\"Bad version {}\", version);\n            Err(EstablishTunnelResult::BadRequest)\n        } else {\n            Ok(())\n        }\n    }\n\n    #[cfg(not(feature = \"plain_text\"))]\n    fn check_method(method: &str) -> Result<bool, EstablishTunnelResult> {\n        if method != \"CONNECT\" {\n            debug!(\"Not allowed method {}\", method);\n            Err(EstablishTunnelResult::OperationNotAllowed)\n        } else {\n            Ok(false)\n        }\n    }\n\n    #[cfg(feature = \"plain_text\")]\n    fn check_method(method: &str) -> Result<bool, EstablishTunnelResult> {\n        Ok(method != \"CONNECT\")\n    }\n\n    fn precondition_legal_characters(http_request: &[u8]) -> Result<(), EstablishTunnelResult> {\n        for b in http_request {\n            match b {\n                // non-ascii characters don't make sense in this context\n                32..=126 | 9 | 10 | 13 => {}\n                _ => {\n                    debug!(\"Bad request header. Illegal character: {:#04x}\", b);\n                    return Err(EstablishTunnelResult::BadRequest);\n                }\n            }\n        }\n        Ok(())\n    }\n\n    fn precondition_size(http_request: &[u8]) -> Result<(), EstablishTunnelResult> {\n        if http_request.len() >= MAX_HTTP_REQUEST_SIZE {\n            debug!(\n                \"Bad request header. Size {} exceeds limit {}\",\n                http_request.len(),\n                MAX_HTTP_REQUEST_SIZE\n            );\n            Err(EstablishTunnelResult::BadRequest)\n        } else {\n            Ok(())\n        }\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use bytes::{BufMut, BytesMut};\n    use regex::Regex;\n    use tokio_util::codec::{Decoder, Encoder};\n\n    use crate::http_tunnel_codec::{\n        EstablishTunnelResult, HttpTunnelCodec, HttpTunnelCodecBuilder, HttpTunnelTargetBuilder,\n        MAX_HTTP_REQUEST_SIZE, REQUEST_END_MARKER,\n    };\n    #[cfg(feature = \"plain_text\")]\n    use crate::proxy_target::Nugget;\n    #[cfg(feature = \"plain_text\")]\n    use crate::tunnel::EstablishTunnelResult::Forbidden;\n    use crate::tunnel::TunnelCtxBuilder;\n\n    #[test]\n    fn test_got_http_request_partial() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(result, Ok(None));\n\n        buffer.put_slice(b\"CONNECT foo.bar.com:443 HTTP/1.1\");\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(result, Ok(None));\n    }\n\n    #[test]\n    fn test_got_http_request_full() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"CONNECT foo.bar.com:443 HTTP/1.1\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(\n            result,\n            Ok(Some(\n                HttpTunnelTargetBuilder::default()\n                    .target(\"foo.bar.com:443\".to_string())\n                    .nugget(None)\n                    .build()\n                    .unwrap(),\n            ))\n        );\n    }\n\n    #[test]\n    fn test_got_http_request_exceeding() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        while buffer.len() <= MAX_HTTP_REQUEST_SIZE {\n            buffer.put_slice(b\"CONNECT foo.bar.com:443 HTTP/1.1\\r\\n\");\n        }\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(result, Err(EstablishTunnelResult::BadRequest));\n    }\n\n    #[test]\n    fn test_parse_valid() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"CONNECT foo.bar.com:443 HTTP/1.1\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_parse_valid_with_headers() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(\n            b\"CONNECT foo.bar.com:443 HTTP/1.1\\r\\n\\\n                   Host: ignored\\r\\n\\\n                   Auithorization: ignored\",\n        );\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    #[cfg(not(feature = \"plain_text\"))]\n    fn test_parse_not_allowed_method() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"GET foo.bar.com:443 HTTP/1.1\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(result, Err(EstablishTunnelResult::OperationNotAllowed));\n    }\n\n    #[test]\n    #[cfg(feature = \"plain_text\")]\n    fn test_parse_plain_text_method() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"GET https://foo.bar.com:443/get HTTP/1.1\\r\\n\");\n        buffer.put_slice(b\"connection: keep-alive\\r\\n\");\n        buffer.put_slice(b\"Host: \\tfoo.bar.com:443 \\t\\r\\n\");\n        buffer.put_slice(b\"User-Agent: whatever\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert!(result.is_ok());\n        let result = result.unwrap();\n        assert!(result.is_some());\n        assert_eq!(result.unwrap().target, \"foo.bar.com:443\");\n    }\n\n    #[test]\n    #[cfg(feature = \"plain_text\")]\n    fn test_parse_plain_text_default_https_port() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"GET https://foo.bar.com/get HTTP/1.1\\r\\n\");\n        buffer.put_slice(b\"connection: keep-alive\\r\\n\");\n        buffer.put_slice(b\"Host: \\tfoo.bar.com \\t\\r\\n\");\n        buffer.put_slice(b\"User-Agent: whatever\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert!(result.is_ok());\n        let result = result.unwrap();\n        assert!(result.is_some());\n        assert_eq!(result.unwrap().target, \"foo.bar.com:443\");\n    }\n\n    #[test]\n    #[cfg(feature = \"plain_text\")]\n    fn test_parse_plain_text_default_http_port() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"GET http://foo.bar.com/get HTTP/1.1\\r\\n\");\n        buffer.put_slice(b\"connection: keep-alive\\r\\n\");\n        buffer.put_slice(b\"Host: \\tfoo.bar.com \\t\\r\\n\");\n        buffer.put_slice(b\"User-Agent: whatever\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert!(result.is_ok());\n        let result = result.unwrap();\n        assert!(result.is_some());\n        assert_eq!(result.unwrap().target, \"foo.bar.com:80\");\n    }\n\n    #[test]\n    #[cfg(feature = \"plain_text\")]\n    fn test_parse_plain_text_nugget() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"GET https://foo.bar.com:443/get HTTP/1.1\\r\\n\");\n        buffer.put_slice(b\"connection: keep-alive\\r\\n\");\n        buffer.put_slice(b\"Host: \\tfoo.bar.com:443 \\t\\r\\n\");\n        buffer.put_slice(b\"User-Agent: whatever\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert!(result.is_ok());\n        let result = result.unwrap();\n        assert!(result.is_some());\n        let result = result.unwrap();\n        assert!(result.nugget.is_some());\n        let nugget = result.nugget.unwrap();\n        assert_eq!(nugget, Nugget::new(buffer.to_vec()));\n    }\n\n    #[test]\n    #[cfg(feature = \"plain_text\")]\n    fn test_parse_plain_text_with_body() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"POST https://foo.bar.com:443/get HTTP/1.1\\r\\n\");\n        buffer.put_slice(b\"connection: keep-alive\\r\\n\");\n        buffer.put_slice(b\"Host: \\tfoo.bar.com:443 \\t\\r\\n\");\n        buffer.put_slice(b\"User-Agent: whatever\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        buffer.put_slice(b\"{body: 'some json body'}\");\n        let result = codec.decode(&mut buffer);\n\n        assert!(result.is_ok());\n        let result = result.unwrap();\n        assert!(result.is_some());\n        let result = result.unwrap();\n        assert!(result.nugget.is_some());\n        let nugget = result.nugget.unwrap();\n        assert_eq!(nugget, Nugget::new(buffer.to_vec()));\n    }\n\n    #[test]\n    #[cfg(feature = \"plain_text\")]\n    fn test_parse_plain_text_method_forbidden_domain() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"GET https://foo.bar.com:443/get HTTP/1.1\\r\\n\");\n        buffer.put_slice(b\"connection: keep-alive\\r\\n\");\n        buffer.put_slice(b\"Host: \\tsome.uknown.site.com:443 \\t\\r\\n\");\n        buffer.put_slice(b\"User-Agent: whatever\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(result, Err(Forbidden));\n    }\n\n    #[test]\n    fn test_parse_bad_version() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        buffer.put_slice(b\"CONNECT foo.bar.com:443 HTTP/1.0\");\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n        assert!(result.is_err());\n\n        let code = result.err().unwrap();\n        assert_eq!(code, EstablishTunnelResult::BadRequest);\n    }\n\n    #[test]\n    fn test_parse_bad_requests() {\n        let bad_requests = [\n            \"bad request\\r\\n\\r\\n\",                       // 2 tokens\n            \"yet another bad request\\r\\n\\r\\n\",           // 4 tokens\n            \"CONNECT foo.bar.cøm:443 HTTP/1.1\\r\\n\\r\\n\",  // non-ascii\n            \"CONNECT  foo.bar.com:443 HTTP/1.1\\r\\n\\r\\n\", // double-space\n            \"CONNECT foo.bar.com:443\\tHTTP/1.1\\r\\n\\r\\n\", // CTL\n        ];\n        bad_requests.iter().for_each(|r| {\n            let mut codec = build_codec();\n\n            let mut buffer = BytesMut::new();\n            buffer.put_slice(r.as_bytes());\n            let result = codec.decode(&mut buffer);\n\n            assert_eq!(\n                result,\n                Err(EstablishTunnelResult::BadRequest),\n                \"Didn't reject {r}\"\n            );\n        });\n    }\n\n    #[test]\n    fn test_parse_request_exceeds_size() {\n        let mut codec = build_codec();\n        let mut buffer = BytesMut::new();\n        while !buffer.len() <= MAX_HTTP_REQUEST_SIZE {\n            buffer.put_slice(b\"CONNECT foo.bar.com:443 HTTP/1.1\\r\\n\");\n        }\n\n        buffer.put_slice(REQUEST_END_MARKER);\n        let result = codec.decode(&mut buffer);\n\n        assert_eq!(result, Err(EstablishTunnelResult::BadRequest));\n    }\n\n    #[test]\n    fn test_http_tunnel_encoder() {\n        let mut codec = build_codec();\n\n        let pattern = Regex::new(r\"^HTTP/1\\.1 ([2-5][\\d]{2}) [A-Z_]{2,20}\\r\\n\\r\\n\").unwrap();\n\n        for code in &[\n            EstablishTunnelResult::Ok,\n            EstablishTunnelResult::BadGateway,\n            EstablishTunnelResult::Forbidden,\n            EstablishTunnelResult::GatewayTimeout,\n            EstablishTunnelResult::OperationNotAllowed,\n            EstablishTunnelResult::RequestTimeout,\n            EstablishTunnelResult::ServerError,\n            EstablishTunnelResult::TooManyRequests,\n        ] {\n            let mut buffer = BytesMut::new();\n            let encoded = codec.encode(code.clone(), &mut buffer);\n            assert!(encoded.is_ok());\n\n            let str = String::from_utf8(Vec::from(&buffer[..])).expect(\"Must be valid ASCII\");\n\n            assert!(pattern.is_match(&str), \"Malformed response `{code:?}`\");\n        }\n    }\n\n    fn build_codec() -> HttpTunnelCodec {\n        let ctx = TunnelCtxBuilder::default().id(1).build().unwrap();\n\n        HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar\\.com:(443|80)\").unwrap())\n            .build()\n            .unwrap()\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or\n/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your\n/// option. This file may not be copied, modified, or distributed\n/// except according to those terms.\n\n#[macro_use]\nextern crate derive_builder;\n#[macro_use]\nextern crate serde_derive;\n\nuse log::{error, info, LevelFilter};\nuse rand::{thread_rng, Rng};\nuse tokio::io;\nuse tokio::net::{TcpListener, TcpStream};\nuse tokio::time::timeout;\nuse tokio_native_tls::TlsAcceptor;\n\nuse crate::configuration::{ProxyConfiguration, ProxyMode};\nuse crate::http_tunnel_codec::{HttpTunnelCodec, HttpTunnelCodecBuilder, HttpTunnelTarget};\nuse crate::proxy_target::{SimpleCachingDnsResolver, SimpleTcpConnector, TargetConnector};\nuse crate::tunnel::{\n    relay_connections, ConnectionTunnel, TunnelCtx, TunnelCtxBuilder, TunnelStats,\n};\nuse log4rs::append::console::ConsoleAppender;\nuse log4rs::config::{Appender, Root};\nuse log4rs::Config;\nuse std::io::{Error, ErrorKind};\nuse tokio::io::{AsyncRead, AsyncWrite};\n\nmod configuration;\nmod http_tunnel_codec;\nmod proxy_target;\nmod relay;\nmod tunnel;\n\ntype DnsResolver = SimpleCachingDnsResolver;\n\n#[tokio::main]\nasync fn main() -> io::Result<()> {\n    init_logger();\n\n    let proxy_configuration = ProxyConfiguration::from_command_line().map_err(|e| {\n        println!(\"Failed to process parameters. See ./log/application.log for details\");\n        e\n    })?;\n\n    info!(\"Starting listener on: {}\", proxy_configuration.bind_address);\n\n    let dns_resolver = SimpleCachingDnsResolver::new(\n        proxy_configuration\n            .tunnel_config\n            .target_connection\n            .dns_cache_ttl,\n    );\n\n    match &proxy_configuration.mode {\n        ProxyMode::Http => {\n            serve_plain_text(proxy_configuration, dns_resolver).await?;\n        }\n        ProxyMode::Https(tls_identity) => {\n            let acceptor = native_tls::TlsAcceptor::new(tls_identity.clone()).map_err(|e| {\n                error!(\"Error setting up TLS {}\", e);\n                Error::from(ErrorKind::InvalidInput)\n            })?;\n\n            let tls_acceptor = TlsAcceptor::from(acceptor);\n\n            serve_tls(proxy_configuration, tls_acceptor, dns_resolver).await?;\n        }\n        ProxyMode::Tcp(d) => {\n            let destination = d.clone();\n            serve_tcp(proxy_configuration, dns_resolver, destination).await?;\n        }\n    };\n\n    info!(\"Proxy stopped\");\n\n    Ok(())\n}\n\nasync fn start_listening_tcp(config: &ProxyConfiguration) -> Result<TcpListener, Error> {\n    let bind_address = &config.bind_address;\n\n    match TcpListener::bind(bind_address).await {\n        Ok(s) => {\n            info!(\"Serving requests on: {bind_address}\");\n            Ok(s)\n        }\n        Err(e) => {\n            error!(\"Error binding TCP socket {bind_address}: {e}\");\n            Err(e)\n        }\n    }\n}\n\nasync fn serve_tls(\n    config: ProxyConfiguration,\n    tls_acceptor: TlsAcceptor,\n    dns_resolver: DnsResolver,\n) -> io::Result<()> {\n    let listener = start_listening_tcp(&config).await?;\n\n    loop {\n        // Asynchronously wait for an inbound socket.\n        let socket = listener.accept().await;\n\n        let dns_resolver_ref = dns_resolver.clone();\n\n        match socket {\n            Ok((stream, _)) => {\n                stream.nodelay().unwrap_or_default();\n                let stream_tls_acceptor = tls_acceptor.clone();\n                let config = config.clone();\n                // handle accepted connections asynchronously\n                tokio::spawn(async move {\n                    handle_client_tls_connection(\n                        config,\n                        stream_tls_acceptor,\n                        stream,\n                        dns_resolver_ref,\n                    )\n                    .await\n                });\n            }\n            Err(e) => error!(\"Failed TCP handshake {}\", e),\n        }\n    }\n}\n\nasync fn serve_plain_text(config: ProxyConfiguration, dns_resolver: DnsResolver) -> io::Result<()> {\n    let listener = start_listening_tcp(&config).await?;\n\n    loop {\n        // Asynchronously wait for an inbound socket.\n        let socket = listener.accept().await;\n\n        let dns_resolver_ref = dns_resolver.clone();\n\n        match socket {\n            Ok((stream, _)) => {\n                stream.nodelay().unwrap_or_default();\n                let config = config.clone();\n                // handle accepted connections asynchronously\n                tokio::spawn(async move { tunnel_stream(&config, stream, dns_resolver_ref).await });\n            }\n            Err(e) => error!(\"Failed TCP handshake {}\", e),\n        }\n    }\n}\n\nasync fn serve_tcp(\n    config: ProxyConfiguration,\n    dns_resolver: DnsResolver,\n    destination: String,\n) -> io::Result<()> {\n    let listener = start_listening_tcp(&config).await?;\n\n    loop {\n        // Asynchronously wait for an inbound socket.\n        let socket = listener.accept().await;\n\n        let dns_resolver_ref = dns_resolver.clone();\n        let destination_copy = destination.clone();\n        let config_copy = config.clone();\n\n        match socket {\n            Ok((stream, _)) => {\n                let config = config.clone();\n                stream.nodelay().unwrap_or_default();\n                // handle accepted connections asynchronously\n                tokio::spawn(async move {\n                    let ctx = TunnelCtxBuilder::default()\n                        .id(thread_rng().gen::<u128>())\n                        .build()\n                        .expect(\"TunnelCtxBuilder failed\");\n\n                    let mut connector: SimpleTcpConnector<HttpTunnelTarget, DnsResolver> =\n                        SimpleTcpConnector::new(\n                            dns_resolver_ref,\n                            config.tunnel_config.target_connection.connect_timeout,\n                            ctx,\n                        );\n\n                    match connector\n                        .connect(&HttpTunnelTarget {\n                            target: destination_copy,\n                            nugget: None,\n                        })\n                        .await\n                    {\n                        Ok(destination) => {\n                            let stats = relay_connections(\n                                stream,\n                                destination,\n                                ctx,\n                                config_copy.tunnel_config.client_connection.relay_policy,\n                                config_copy.tunnel_config.target_connection.relay_policy,\n                            )\n                            .await;\n\n                            report_tunnel_metrics(ctx, stats);\n                        }\n                        Err(e) => error!(\"Failed to establish TCP upstream connection {:?}\", e),\n                    }\n                });\n            }\n            Err(e) => error!(\"Failed TCP handshake {}\", e),\n        }\n    }\n}\n\nasync fn handle_client_tls_connection(\n    config: ProxyConfiguration,\n    tls_acceptor: TlsAcceptor,\n    stream: TcpStream,\n    dns_resolver: DnsResolver,\n) -> io::Result<()> {\n    let timed_tls_handshake = timeout(\n        config.tunnel_config.client_connection.initiation_timeout,\n        tls_acceptor.accept(stream),\n    )\n    .await;\n\n    if let Ok(tls_result) = timed_tls_handshake {\n        match tls_result {\n            Ok(downstream) => {\n                tunnel_stream(&config, downstream, dns_resolver).await?;\n            }\n            Err(e) => {\n                error!(\n                    \"Client opened a TCP connection but TLS handshake failed: {}.\",\n                    e\n                );\n            }\n        }\n    } else {\n        error!(\n            \"Client opened TCP connection but didn't complete TLS handshake in time: {:?}.\",\n            config.tunnel_config.client_connection.initiation_timeout\n        );\n    }\n    Ok(())\n}\n\n/// Tunnel via a client connection.\n/// This method constructs `HttpTunnelCodec` and `SimpleTcpConnector`\n/// to create an `HTTP` tunnel.\nasync fn tunnel_stream<C: AsyncRead + AsyncWrite + Send + Unpin + 'static>(\n    config: &ProxyConfiguration,\n    client: C,\n    dns_resolver: DnsResolver,\n) -> io::Result<()> {\n    let ctx = TunnelCtxBuilder::default()\n        .id(thread_rng().gen::<u128>())\n        .build()\n        .expect(\"TunnelCtxBuilder failed\");\n\n    // here it can be any codec.\n    let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n        .tunnel_ctx(ctx)\n        .enabled_targets(\n            config\n                .tunnel_config\n                .target_connection\n                .allowed_targets\n                .clone(),\n        )\n        .build()\n        .expect(\"HttpTunnelCodecBuilder failed\");\n\n    // any `TargetConnector` would do.\n    let connector: SimpleTcpConnector<HttpTunnelTarget, DnsResolver> = SimpleTcpConnector::new(\n        dns_resolver,\n        config.tunnel_config.target_connection.connect_timeout,\n        ctx,\n    );\n\n    let stats = ConnectionTunnel::new(codec, connector, client, config.tunnel_config.clone(), ctx)\n        .start()\n        .await;\n\n    report_tunnel_metrics(ctx, stats);\n\n    Ok(())\n}\n\n/// Placeholder for proper metrics emission.\n/// Here we just write to a file without any aggregation.\nfn report_tunnel_metrics(ctx: TunnelCtx, stats: io::Result<TunnelStats>) {\n    match stats {\n        Ok(s) => {\n            info!(target: \"metrics\", \"{}\", serde_json::to_string(&s).expect(\"JSON serialization failed\"));\n        }\n        Err(_) => error!(\"Failed to get stats for TID={}\", ctx),\n    }\n}\n\nfn init_logger() {\n    let logger_configuration = \"./config/log4rs.yaml\";\n    if let Err(e) = log4rs::init_file(logger_configuration, Default::default()) {\n        println!(\n            \"Cannot initialize logger from {logger_configuration}, error=[{e}]. Logging to the console.\");\n        let config = Config::builder()\n            .appender(\n                Appender::builder()\n                    .build(\"application\", Box::new(ConsoleAppender::builder().build())),\n            )\n            .build(\n                Root::builder()\n                    .appender(\"application\")\n                    .build(LevelFilter::Info),\n            )\n            .unwrap();\n        log4rs::init_config(config).expect(\"Bug: bad default config\");\n    }\n}\n"
  },
  {
    "path": "src/proxy_target.rs",
    "content": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or\n/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your\n/// option. This file may not be copied, modified, or distributed\n/// except according to those terms.\nuse crate::tunnel::{TunnelCtx, TunnelTarget};\nuse async_trait::async_trait;\nuse log::{debug, error, info};\nuse rand::prelude::thread_rng;\nuse rand::Rng;\nuse std::collections::HashMap;\nuse std::marker::PhantomData;\nuse std::net::SocketAddr;\nuse std::sync::Arc;\nuse std::time::Instant;\nuse tokio::io;\nuse tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, Error, ErrorKind};\nuse tokio::net::TcpStream;\nuse tokio::sync::RwLock;\nuse tokio::time::timeout;\nuse tokio::time::Duration;\n\n#[async_trait]\npub trait TargetConnector {\n    type Target: TunnelTarget + Send + Sync + Sized;\n    type Stream: AsyncRead + AsyncWrite + Send + Sized + 'static;\n\n    async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::Stream>;\n}\n\n#[async_trait]\npub trait DnsResolver {\n    async fn resolve(&mut self, target: &str) -> io::Result<SocketAddr>;\n}\n\n#[derive(Clone, Builder)]\npub struct SimpleTcpConnector<D, R: DnsResolver> {\n    connect_timeout: Duration,\n    tunnel_ctx: TunnelCtx,\n    dns_resolver: R,\n    #[builder(setter(skip))]\n    _phantom_target: PhantomData<D>,\n}\n\n#[derive(Eq, PartialEq, Debug, Clone)]\npub struct Nugget {\n    data: Arc<Vec<u8>>,\n}\n\ntype CachedSocketAddrs = (Vec<SocketAddr>, u128);\n\n/// Caching DNS resolution to minimize DNS look-ups.\n/// The cache has relaxed consistency, it allows concurrent DNS look-ups of the same key,\n/// without any guarantees which result is going to be cached.\n///\n/// Given it's used for DNS look-ups this trade-off seems to be reasonable.\n#[derive(Clone)]\npub struct SimpleCachingDnsResolver {\n    // mostly reads, occasional writes\n    cache: Arc<RwLock<HashMap<String, CachedSocketAddrs>>>,\n    ttl: Duration,\n    start_time: Instant,\n}\n\n#[async_trait]\nimpl<D, R> TargetConnector for SimpleTcpConnector<D, R>\nwhere\n    D: TunnelTarget<Addr = String> + Send + Sync + Sized,\n    R: DnsResolver + Send + Sync + 'static,\n{\n    type Target = D;\n    type Stream = TcpStream;\n\n    async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::Stream> {\n        let target_addr = &target.target_addr();\n\n        let addr = self.dns_resolver.resolve(target_addr).await?;\n\n        if let Ok(tcp_stream) = timeout(self.connect_timeout, TcpStream::connect(addr)).await {\n            let mut stream = tcp_stream?;\n            stream.nodelay()?;\n            if target.has_nugget() {\n                if let Ok(written_successfully) = timeout(\n                    self.connect_timeout,\n                    stream.write_all(&target.nugget().data()),\n                )\n                .await\n                {\n                    written_successfully?;\n                } else {\n                    error!(\n                        \"Timeout sending nugget to {}, {}, CTX={}\",\n                        addr, target_addr, self.tunnel_ctx\n                    );\n                    return Err(Error::from(ErrorKind::TimedOut));\n                }\n            }\n            Ok(stream)\n        } else {\n            error!(\n                \"Timeout connecting to {}, {}, CTX={}\",\n                addr, target_addr, self.tunnel_ctx\n            );\n            Err(Error::from(ErrorKind::TimedOut))\n        }\n    }\n}\n\n#[async_trait]\nimpl DnsResolver for SimpleCachingDnsResolver {\n    async fn resolve(&mut self, target: &str) -> io::Result<SocketAddr> {\n        match self.try_find(target).await {\n            Some(a) => Ok(a),\n            _ => Ok(self.resolve_and_cache(target).await?),\n        }\n    }\n}\n\nimpl<D, R> SimpleTcpConnector<D, R>\nwhere\n    R: DnsResolver,\n{\n    pub fn new(dns_resolver: R, connect_timeout: Duration, tunnel_ctx: TunnelCtx) -> Self {\n        Self {\n            dns_resolver,\n            connect_timeout,\n            tunnel_ctx,\n            _phantom_target: PhantomData,\n        }\n    }\n}\n\nimpl SimpleCachingDnsResolver {\n    pub fn new(ttl: Duration) -> Self {\n        Self {\n            cache: Arc::new(RwLock::new(HashMap::new())),\n            ttl,\n            start_time: Instant::now(),\n        }\n    }\n\n    fn pick(&self, addrs: &[SocketAddr]) -> SocketAddr {\n        addrs[thread_rng().gen::<usize>() % addrs.len()]\n    }\n\n    async fn try_find(&mut self, target: &str) -> Option<SocketAddr> {\n        let map = self.cache.read().await;\n\n        let addr = match map.get(target) {\n            None => None,\n            Some((cached, expiration)) => {\n                // expiration with jitter to avoid expiration \"waves\"\n                let expiration_jitter = *expiration + thread_rng().gen_range(0..5_000);\n                if Instant::now().duration_since(self.start_time).as_millis() < expiration_jitter {\n                    Some(self.pick(cached))\n                } else {\n                    None\n                }\n            }\n        };\n\n        addr\n    }\n\n    async fn resolve_and_cache(&mut self, target: &str) -> io::Result<SocketAddr> {\n        let resolved = SimpleCachingDnsResolver::resolve(target).await?;\n\n        let mut map = self.cache.write().await;\n        map.insert(\n            target.to_string(),\n            (\n                resolved.clone(),\n                Instant::now().duration_since(self.start_time).as_millis() + self.ttl.as_millis(),\n            ),\n        );\n\n        Ok(self.pick(&resolved))\n    }\n\n    async fn resolve(target: &str) -> io::Result<Vec<SocketAddr>> {\n        debug!(\"Resolving DNS {}\", target,);\n        let resolved: Vec<SocketAddr> = tokio::net::lookup_host(target).await?.collect();\n        info!(\"Resolved DNS {} to {:?}\", target, resolved);\n\n        if resolved.is_empty() {\n            error!(\"Cannot resolve DNS {}\", target,);\n            return Err(Error::from(ErrorKind::AddrNotAvailable));\n        }\n\n        Ok(resolved)\n    }\n}\n\nimpl Nugget {\n    pub fn new<T: Into<Vec<u8>>>(v: T) -> Self {\n        Self {\n            data: Arc::new(v.into()),\n        }\n    }\n\n    pub fn data(&self) -> Arc<Vec<u8>> {\n        self.data.clone()\n    }\n}\n"
  },
  {
    "path": "src/relay.rs",
    "content": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or\n/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your\n/// option. This file may not be copied, modified, or distributed\n/// except according to those terms.\nuse core::fmt;\nuse std::future::Future;\nuse std::time::{Duration, Instant};\n\nuse crate::tunnel::TunnelCtx;\nuse log::{debug, error, info};\nuse tokio::io;\nuse tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};\nuse tokio::time::timeout;\n\npub const NO_TIMEOUT: Duration = Duration::from_secs(300);\npub const NO_BANDWIDTH_LIMIT: u64 = 1_000_000_000_000_u64;\nconst BUFFER_SIZE: usize = 16 * 1024;\n\n#[derive(Debug, Clone, Eq, PartialEq, Serialize)]\npub enum RelayShutdownReasons {\n    /// If a reader connection was gracefully closed\n    GracefulShutdown,\n    ReadError,\n    WriteError,\n    ReaderTimeout,\n    WriterTimeout,\n    TooSlow,\n    TooFast,\n}\n\n/// Relays traffic from one stream to another in a single direction.\n/// To relay two sockets in full-duplex mode you need to create two `Relays` in both directions.\n/// It doesn't really matter what is the protocol, as it only requires `AsyncReadExt`\n/// and `AsyncWriteExt` traits from the source and the target.\n#[derive(Builder, Clone)]\npub struct Relay {\n    name: &'static str,\n    relay_policy: RelayPolicy,\n    tunnel_ctx: TunnelCtx,\n}\n\n/// Stats after the relay is closed. Can be used for telemetry/monitoring.\n#[derive(Builder, Clone, Debug, Serialize)]\npub struct RelayStats {\n    pub shutdown_reason: RelayShutdownReasons,\n    pub total_bytes: usize,\n    pub event_count: usize,\n    pub duration: Duration,\n}\n\n/// Relay policy is meant to protect targets and proxy servers from\n/// different sorts of abuse. Currently it only checks too slow or too fast connections,\n/// which may lead to different capacity issues.\n#[derive(Builder, Deserialize, Clone)]\npub struct RelayPolicy {\n    #[serde(with = \"humantime_serde\")]\n    pub idle_timeout: Duration,\n    /// Min bytes-per-minute (bpm)\n    pub min_rate_bpm: u64,\n    // Max bytes-per-second (bps)\n    pub max_rate_bps: u64,\n}\n\nimpl Relay {\n    /// Relays data in a single direction. E.g.\n    /// ```ignore\n    /// let upstream = tokio::spawn(async move {\n    ///     upstream_relay.relay_data(client_recv, target_send).await\n    /// });\n    /// let downstream = tokio::spawn(async move {\n    ///     downstream_relay.relay_data(target_recv, client_send).await\n    /// });\n    /// let downstream_stats = downstream.await??;\n    /// let upstream_stats = upstream.await??;\n    /// ```\n    pub async fn relay_data<R: AsyncReadExt + Sized, W: AsyncWriteExt + Sized>(\n        self,\n        mut source: ReadHalf<R>,\n        mut dest: WriteHalf<W>,\n    ) -> io::Result<RelayStats> {\n        let mut buffer = [0; BUFFER_SIZE];\n\n        let mut total_bytes = 0;\n        let mut event_count = 0;\n        let start_time = Instant::now();\n        let shutdown_reason;\n\n        loop {\n            let read_result = self\n                .relay_policy\n                .timed_operation(source.read(&mut buffer))\n                .await;\n\n            if read_result.is_err() {\n                shutdown_reason = RelayShutdownReasons::ReaderTimeout;\n                break;\n            }\n\n            let n = match read_result.unwrap() {\n                Ok(0) => {\n                    shutdown_reason = RelayShutdownReasons::GracefulShutdown;\n                    break;\n                }\n                Ok(n) => n,\n                Err(e) => {\n                    error!(\n                        \"{} failed to read. Err = {:?}, CTX={}\",\n                        self.name, e, self.tunnel_ctx\n                    );\n                    shutdown_reason = RelayShutdownReasons::ReadError;\n                    break;\n                }\n            };\n\n            let write_result = self\n                .relay_policy\n                .timed_operation(dest.write_all(&buffer[..n]))\n                .await;\n\n            if write_result.is_err() {\n                shutdown_reason = RelayShutdownReasons::WriterTimeout;\n                break;\n            }\n\n            if let Err(e) = write_result.unwrap() {\n                error!(\n                    \"{} failed to write {} bytes. Err = {:?}, CTX={}\",\n                    self.name, n, e, self.tunnel_ctx\n                );\n                shutdown_reason = RelayShutdownReasons::WriteError;\n                break;\n            }\n\n            total_bytes += n;\n            event_count += 1;\n\n            if let Err(rate_violation) = self\n                .relay_policy\n                .check_transmission_rates(&start_time, total_bytes)\n            {\n                shutdown_reason = rate_violation;\n                break;\n            }\n        }\n\n        self.shutdown(&mut dest, &shutdown_reason).await;\n\n        let duration = Instant::now().duration_since(start_time);\n\n        let stats = RelayStatsBuilder::default()\n            .shutdown_reason(shutdown_reason)\n            .total_bytes(total_bytes)\n            .event_count(event_count)\n            .duration(duration)\n            .build()\n            .expect(\"RelayStatsBuilder failed\");\n\n        info!(\"{} closed: {}, CTX={}\", self.name, stats, self.tunnel_ctx);\n\n        Ok(stats)\n    }\n\n    async fn shutdown<W: AsyncWriteExt + Sized>(\n        &self,\n        dest: &mut WriteHalf<W>,\n        reason: &RelayShutdownReasons,\n    ) {\n        match dest.shutdown().await {\n            Ok(_) => {\n                debug!(\n                    \"{} shutdown due do {:?}, CTX={}\",\n                    self.name, reason, self.tunnel_ctx\n                );\n            }\n            Err(e) => {\n                error!(\n                    \"{} failed to shutdown. Err = {:?}, CTX={}\",\n                    self.name, e, self.tunnel_ctx\n                );\n            }\n        }\n    }\n}\n\nimpl RelayPolicy {\n    /// Basic rate limiting. Placeholder for more sophisticated policy handling,\n    /// e.g. sliding windows, detecting heavy hitters, etc.\n    pub fn check_transmission_rates(\n        &self,\n        start: &Instant,\n        total_bytes: usize,\n    ) -> Result<(), RelayShutdownReasons> {\n        if self.min_rate_bpm == 0 && self.max_rate_bps >= NO_BANDWIDTH_LIMIT {\n            return Ok(());\n        }\n        let elapsed = Instant::now().duration_since(*start);\n        if elapsed.as_secs_f32() > 5. && total_bytes as u64 / elapsed.as_secs() > self.max_rate_bps\n        {\n            // prevent bandwidth abuse\n            Err(RelayShutdownReasons::TooFast)\n        } else if elapsed.as_secs_f32() >= 30.\n            && total_bytes as f64 / elapsed.as_secs_f64() / 60. < self.min_rate_bpm as f64\n        {\n            // prevent slowloris: https://en.wikipedia.org/wiki/Slowloris_(computer_security)\n            Err(RelayShutdownReasons::TooSlow)\n        } else {\n            Ok(())\n        }\n    }\n\n    /// Each async operation must be time-bound.\n    pub async fn timed_operation<T: Future>(&self, f: T) -> Result<<T as Future>::Output, ()> {\n        if self.idle_timeout >= NO_TIMEOUT {\n            return Ok(f.await);\n        }\n        let result = timeout(self.idle_timeout, f).await;\n\n        if let Ok(r) = result {\n            Ok(r)\n        } else {\n            Err(())\n        }\n    }\n}\n\n// cov:begin-ignore-line\nimpl fmt::Display for RelayStats {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(\n            f,\n            \"shutdown_reason={:?}, bytes={}, event_count={}, duration={:?}, rate_kbps={:.3}\",\n            self.shutdown_reason,\n            self.total_bytes,\n            self.event_count,\n            self.duration,\n            self.total_bytes as f64 / 1024. / self.duration.as_secs_f64()\n        )\n    }\n}\n// cov:end-ignore-line\n\n#[cfg(test)]\nmod test_relay_policy {\n    extern crate tokio;\n\n    use std::ops::Sub;\n    use std::time::{Duration, Instant};\n\n    use tokio_test::io::Builder;\n    use tokio_test::io::Mock;\n\n    use crate::relay::{RelayPolicy, RelayPolicyBuilder, RelayShutdownReasons};\n\n    use self::tokio::io::{AsyncReadExt, Error, ErrorKind};\n\n    #[test]\n    fn test_enforce_policy_ok() {\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(1))\n            .build()\n            .unwrap();\n        let start = Instant::now().sub(Duration::from_secs(10));\n        // 100k in 10 second is OK\n        let result = relay_policy.check_transmission_rates(&start, 100_000);\n        assert!(result.is_ok());\n    }\n\n    #[test]\n    fn test_enforce_policy_too_fast() {\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(1))\n            .build()\n            .unwrap();\n        let start = Instant::now().sub(Duration::from_secs(10));\n        // 10m in 10 second is way too fast\n        let result = relay_policy.check_transmission_rates(&start, 10_000_000);\n        assert!(result.is_err());\n        assert_eq!(RelayShutdownReasons::TooFast, result.unwrap_err());\n    }\n\n    #[test]\n    fn test_enforce_policy_too_slow() {\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(1))\n            .build()\n            .unwrap();\n        // 100 bytes in 40 seconds is too slow\n        let start = Instant::now().sub(Duration::from_secs(40));\n        let result = relay_policy.check_transmission_rates(&start, 100);\n        assert!(result.is_err());\n        assert_eq!(RelayShutdownReasons::TooSlow, result.unwrap_err());\n    }\n\n    #[tokio::test]\n    async fn test_timed_operation_ok() {\n        let data = b\"data on the wire\";\n        let mut mock_connection: Mock = Builder::new().read(data).build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(5))\n            .build()\n            .unwrap();\n\n        let mut buf = [0; 1024];\n        let timed_future = relay_policy\n            .timed_operation(mock_connection.read(&mut buf))\n            .await;\n        assert!(timed_future.is_ok());\n        assert_eq!(data, &buf[..timed_future.unwrap().unwrap()])\n    }\n\n    #[tokio::test]\n    async fn test_timed_operation_failed_io() {\n        let mut mock_connection: Mock = Builder::new()\n            .read_error(Error::from(ErrorKind::BrokenPipe))\n            .build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(5))\n            .build()\n            .unwrap();\n\n        let mut buf = [0; 1024];\n        let timed_future = relay_policy\n            .timed_operation(mock_connection.read(&mut buf))\n            .await;\n        assert!(timed_future.is_ok()); // no timeout\n        assert!(timed_future.unwrap().is_err()); // but io-error\n    }\n\n    #[tokio::test]\n    async fn test_timed_operation_timeout() {\n        let time_duration = 1;\n        let mut mock_connection: Mock = Builder::new()\n            .wait(Duration::from_secs(time_duration * 2))\n            .build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(time_duration))\n            .build()\n            .unwrap();\n\n        let mut buf = [0; 1024];\n        let timed_future = relay_policy\n            .timed_operation(mock_connection.read(&mut buf))\n            .await;\n        assert!(timed_future.is_err());\n    }\n}\n\n#[cfg(test)]\nmod test_relay {\n    extern crate tokio;\n\n    use std::time::Duration;\n\n    use tokio::io;\n    use tokio_test::io::Builder;\n    use tokio_test::io::Mock;\n\n    use crate::relay::{\n        Relay, RelayBuilder, RelayPolicy, RelayPolicyBuilder, RelayShutdownReasons,\n    };\n\n    use self::tokio::io::{Error, ErrorKind};\n    use crate::tunnel::{TunnelCtx, TunnelCtxBuilder};\n\n    #[tokio::test]\n    async fn test_relay_ok() {\n        let data = b\"data on the wire\";\n        let reader: Mock = Builder::new().read(data).read(data).read(data).build();\n        let writer: Mock = Builder::new().write(data).write(data).write(data).build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(5))\n            .build()\n            .unwrap();\n\n        let relay: Relay = build_relay(relay_policy);\n\n        let (client_recv, _) = io::split(reader);\n        let (_, target_send) = io::split(writer);\n\n        let result = relay.relay_data(client_recv, target_send).await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n\n        assert_eq!(\n            RelayShutdownReasons::GracefulShutdown,\n            stats.shutdown_reason\n        );\n\n        assert_eq!(data.len() * 3, stats.total_bytes);\n        assert_eq!(3, stats.event_count);\n    }\n\n    #[tokio::test]\n    async fn test_relay_reader_error() {\n        let data = b\"data on the wire\";\n        let reader: Mock = Builder::new()\n            .read(data)\n            .read_error(Error::from(ErrorKind::BrokenPipe))\n            .build();\n        let writer: Mock = Builder::new().write(data).build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(5))\n            .build()\n            .unwrap();\n\n        let relay: Relay = build_relay(relay_policy);\n\n        let (client_recv, _) = io::split(reader);\n        let (_, target_send) = io::split(writer);\n\n        let result = relay.relay_data(client_recv, target_send).await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n\n        assert_eq!(RelayShutdownReasons::ReadError, stats.shutdown_reason);\n    }\n\n    #[tokio::test]\n    async fn test_relay_reader_timeout() {\n        let data = b\"data on the wire\";\n        let reader: Mock = Builder::new()\n            .read(data)\n            .wait(Duration::from_secs(3))\n            .build();\n        let writer: Mock = Builder::new().write(data).build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(1))\n            .build()\n            .unwrap();\n\n        let relay: Relay = build_relay(relay_policy);\n\n        let (client_recv, _) = io::split(reader);\n        let (_, target_send) = io::split(writer);\n\n        let result = relay.relay_data(client_recv, target_send).await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n\n        assert_eq!(RelayShutdownReasons::ReaderTimeout, stats.shutdown_reason);\n    }\n\n    #[tokio::test]\n    async fn test_relay_writer_error() {\n        let data = b\"data on the wire\";\n        let reader: Mock = Builder::new().read(data).read(data).build();\n        let writer: Mock = Builder::new()\n            .write(data)\n            .write_error(Error::from(ErrorKind::BrokenPipe))\n            .build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(5))\n            .build()\n            .unwrap();\n\n        let relay: Relay = build_relay(relay_policy);\n\n        let (client_recv, _) = io::split(reader);\n        let (_, target_send) = io::split(writer);\n\n        let result = relay.relay_data(client_recv, target_send).await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n\n        assert_eq!(RelayShutdownReasons::WriteError, stats.shutdown_reason);\n    }\n\n    #[tokio::test]\n    async fn test_relay_writer_timeout() {\n        let data = b\"data on the wire\";\n        let reader: Mock = Builder::new().read(data).build();\n        let writer: Mock = Builder::new().wait(Duration::from_secs(3)).build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1000)\n            .max_rate_bps(100_000)\n            .idle_timeout(Duration::from_secs(1))\n            .build()\n            .unwrap();\n\n        let relay: Relay = build_relay(relay_policy);\n\n        let (client_recv, _) = io::split(reader);\n        let (_, target_send) = io::split(writer);\n\n        let result = relay.relay_data(client_recv, target_send).await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n\n        assert_eq!(RelayShutdownReasons::WriterTimeout, stats.shutdown_reason);\n    }\n\n    #[tokio::test]\n    async fn test_relay_reader_violates_rate_limits() {\n        let data = b\"waaaay too much data on the wire\";\n        let reader: Mock = Builder::new()\n            .read(data)\n            .wait(Duration::from_secs_f32(5.5))\n            .read(data)\n            .build();\n        let writer: Mock = Builder::new().write(data).write(data).build();\n\n        let relay_policy: RelayPolicy = RelayPolicyBuilder::default()\n            .min_rate_bpm(1)\n            .max_rate_bps(1) // ok, let's be like unreasonably restrictive\n            .idle_timeout(Duration::from_secs(10))\n            .build()\n            .unwrap();\n\n        let relay: Relay = build_relay(relay_policy);\n\n        let (client_recv, _) = io::split(reader);\n        let (_, target_send) = io::split(writer);\n\n        let result = relay.relay_data(client_recv, target_send).await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n\n        assert_eq!(RelayShutdownReasons::TooFast, stats.shutdown_reason);\n\n        assert_eq!(data.len() * 2, stats.total_bytes);\n        assert_eq!(2, stats.event_count);\n    }\n\n    fn build_relay(relay_policy: RelayPolicy) -> Relay {\n        let ctx: TunnelCtx = TunnelCtxBuilder::default().id(1).build().unwrap();\n\n        RelayBuilder::default()\n            .relay_policy(relay_policy)\n            .tunnel_ctx(ctx)\n            .name(\"Test\")\n            .build()\n            .unwrap()\n    }\n}\n"
  },
  {
    "path": "src/tunnel.rs",
    "content": "/// Copyright 2020 Developers of the http-tunnel project.\n///\n/// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or\n/// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license\n/// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your\n/// option. This file may not be copied, modified, or distributed\n/// except according to those terms.\nuse async_trait::async_trait;\nuse futures::{SinkExt, StreamExt};\nuse log::{debug, error};\nuse tokio::io;\nuse tokio::io::{AsyncRead, AsyncWrite};\nuse tokio::time::timeout;\nuse tokio_util::codec::{Decoder, Encoder, Framed};\n\nuse crate::configuration::TunnelConfig;\nuse crate::proxy_target::{Nugget, TargetConnector};\nuse crate::relay::{Relay, RelayBuilder, RelayPolicy, RelayStats};\nuse core::fmt;\nuse futures::stream::SplitStream;\nuse std::fmt::Display;\nuse std::time::Duration;\n\n#[derive(Eq, PartialEq, Debug, Clone, Serialize)]\n#[allow(dead_code)]\npub enum EstablishTunnelResult {\n    /// Successfully connected to target.\n    Ok,\n    /// Successfully connected to target but has a nugget to send after connection establishment.\n    OkWithNugget,\n    /// Malformed request\n    BadRequest,\n    /// Target is not allowed\n    Forbidden,\n    /// Unsupported operation, however valid for the protocol.\n    OperationNotAllowed,\n    /// The client failed to send a tunnel request timely.\n    RequestTimeout,\n    /// Cannot connect to target.\n    BadGateway,\n    /// Connection attempt timed out.\n    GatewayTimeout,\n    /// Busy. Try again later.\n    TooManyRequests,\n    /// Any other error. E.g. an abrupt I/O error.\n    ServerError,\n}\n\n/// A connection tunnel.\n///\n/// # Parameters\n/// * `<H>` - proxy handshake codec for initiating a tunnel.\n///    It extracts the request message, which contains the target, and, potentially policies.\n///    It also takes care of encoding a response.\n/// * `<C>` - a connection from from client.\n/// * `<T>` - target connector. It takes result produced by the codec and establishes a connection\n///           to a target.\n///\n/// Once the target connection is established, it relays data until any connection is closed or an\n/// error happens.\npub struct ConnectionTunnel<H, C, T> {\n    tunnel_request_codec: Option<H>,\n    tunnel_ctx: TunnelCtx,\n    target_connector: T,\n    client: Option<C>,\n    tunnel_config: TunnelConfig,\n}\n\n#[async_trait]\npub trait TunnelTarget {\n    type Addr;\n    fn target_addr(&self) -> Self::Addr;\n    fn has_nugget(&self) -> bool;\n    fn nugget(&self) -> &Nugget;\n}\n\n/// We need to be able to trace events in logs/metrics.\n#[derive(Builder, Copy, Clone, Default, Serialize)]\npub struct TunnelCtx {\n    /// We can easily extend it, if necessary. For now just a random u128.\n    id: u128,\n}\n\n/// Statistics. No sensitive information.\n#[derive(Serialize, Builder)]\npub struct TunnelStats {\n    tunnel_ctx: TunnelCtx,\n    result: EstablishTunnelResult,\n    upstream_stats: Option<RelayStats>,\n    downstream_stats: Option<RelayStats>,\n}\n\nimpl<H, C, T> ConnectionTunnel<H, C, T>\nwhere\n    H: Decoder<Error = EstablishTunnelResult> + Encoder<EstablishTunnelResult>,\n    H::Item: TunnelTarget + Sized + Display + Send + Sync,\n    C: AsyncRead + AsyncWrite + Sized + Send + Unpin + 'static,\n    T: TargetConnector<Target = H::Item>,\n{\n    pub fn new(\n        handshake_codec: H,\n        target_connector: T,\n        client: C,\n        tunnel_config: TunnelConfig,\n        tunnel_ctx: TunnelCtx,\n    ) -> Self {\n        Self {\n            tunnel_request_codec: Some(handshake_codec),\n            target_connector,\n            tunnel_ctx,\n            client: Some(client),\n            tunnel_config,\n        }\n    }\n\n    /// Once the client connected we wait for a tunnel establishment handshake.\n    /// For instance, an `HTTP/1.1 CONNECT` for HTTP tunnels.\n    ///\n    /// During handshake we obtained the target target, and if we were able to connect to it,\n    /// a message indicating success is sent back to client (or an error response otherwise).\n    ///\n    /// At that point we start relaying data in full-duplex mode.\n    ///\n    /// # Note\n    /// This method consumes `self` and thus can be called only once.\n    pub async fn start(mut self) -> io::Result<TunnelStats> {\n        let stream = self.client.take().expect(\"downstream can be taken once\");\n\n        let tunnel_result = self\n            .establish_tunnel(stream, self.tunnel_config.clone())\n            .await;\n\n        if let Err(error) = tunnel_result {\n            return Ok(TunnelStats {\n                tunnel_ctx: self.tunnel_ctx,\n                result: error,\n                upstream_stats: None,\n                downstream_stats: None,\n            });\n        }\n\n        let (client, target) = tunnel_result.unwrap();\n        relay_connections(\n            client,\n            target,\n            self.tunnel_ctx,\n            self.tunnel_config.client_connection.relay_policy,\n            self.tunnel_config.target_connection.relay_policy,\n        )\n        .await\n    }\n\n    async fn establish_tunnel(\n        &mut self,\n        stream: C,\n        configuration: TunnelConfig,\n    ) -> Result<(C, T::Stream), EstablishTunnelResult> {\n        debug!(\"Accepting HTTP tunnel request: CTX={}\", self.tunnel_ctx);\n\n        let (mut write, mut read) = self\n            .tunnel_request_codec\n            .take()\n            .expect(\"establish_tunnel can be called only once\")\n            .framed(stream)\n            .split();\n\n        let (response, target) = self.process_tunnel_request(&configuration, &mut read).await;\n\n        let response_sent = match response {\n            EstablishTunnelResult::OkWithNugget => true,\n            _ => timeout(\n                configuration.client_connection.initiation_timeout,\n                write.send(response.clone()),\n            )\n            .await\n            .is_ok(),\n        };\n\n        if response_sent {\n            match target {\n                None => Err(response),\n                Some(u) => {\n                    // lets take the original stream to either relay data, or to drop it on error\n                    let framed = write.reunite(read).expect(\"Uniting previously split parts\");\n                    let original_stream = framed.into_inner();\n\n                    Ok((original_stream, u))\n                }\n            }\n        } else {\n            Err(EstablishTunnelResult::RequestTimeout)\n        }\n    }\n\n    async fn process_tunnel_request(\n        &mut self,\n        configuration: &TunnelConfig,\n        read: &mut SplitStream<Framed<C, H>>,\n    ) -> (\n        EstablishTunnelResult,\n        Option<<T as TargetConnector>::Stream>,\n    ) {\n        let connect_request = timeout(\n            configuration.client_connection.initiation_timeout,\n            read.next(),\n        )\n        .await;\n\n        let response;\n        let mut target = None;\n\n        if connect_request.is_err() {\n            error!(\"Client established TLS connection but failed to send an HTTP request within {:?}, CTX={}\",\n                   configuration.client_connection.initiation_timeout,\n                   self.tunnel_ctx);\n            response = EstablishTunnelResult::RequestTimeout;\n        } else if let Ok(Some(event)) = connect_request {\n            match event {\n                Ok(decoded_target) => {\n                    let has_nugget = decoded_target.has_nugget();\n                    response = match self\n                        .connect_to_target(\n                            decoded_target,\n                            configuration.target_connection.connect_timeout,\n                        )\n                        .await\n                    {\n                        Ok(t) => {\n                            target = Some(t);\n                            if has_nugget {\n                                EstablishTunnelResult::OkWithNugget\n                            } else {\n                                EstablishTunnelResult::Ok\n                            }\n                        }\n                        Err(e) => e,\n                    }\n                }\n                Err(e) => {\n                    response = e;\n                }\n            }\n        } else {\n            response = EstablishTunnelResult::BadRequest;\n        }\n\n        (response, target)\n    }\n\n    async fn connect_to_target(\n        &mut self,\n        target: T::Target,\n        connect_timeout: Duration,\n    ) -> Result<T::Stream, EstablishTunnelResult> {\n        debug!(\n            \"Establishing HTTP tunnel target connection: {}, CTX={}\",\n            target, self.tunnel_ctx,\n        );\n\n        let timed_connection_result =\n            timeout(connect_timeout, self.target_connector.connect(&target)).await;\n\n        if let Ok(timed_connection_result) = timed_connection_result {\n            match timed_connection_result {\n                Ok(tcp_stream) => Ok(tcp_stream),\n                Err(e) => Err(EstablishTunnelResult::from(e)),\n            }\n        } else {\n            Err(EstablishTunnelResult::GatewayTimeout)\n        }\n    }\n}\n\npub async fn relay_connections<\n    D: AsyncRead + AsyncWrite + Sized + Send + Unpin + 'static,\n    U: AsyncRead + AsyncWrite + Sized + Send + 'static,\n>(\n    client: D,\n    target: U,\n    ctx: TunnelCtx,\n    downstream_relay_policy: RelayPolicy,\n    upstream_relay_policy: RelayPolicy,\n) -> io::Result<TunnelStats> {\n    let (client_recv, client_send) = io::split(client);\n    let (target_recv, target_send) = io::split(target);\n\n    let downstream_relay: Relay = RelayBuilder::default()\n        .name(\"Downstream\")\n        .tunnel_ctx(ctx)\n        .relay_policy(downstream_relay_policy)\n        .build()\n        .expect(\"RelayBuilder failed\");\n\n    let upstream_relay: Relay = RelayBuilder::default()\n        .name(\"Upstream\")\n        .tunnel_ctx(ctx)\n        .relay_policy(upstream_relay_policy)\n        .build()\n        .expect(\"RelayBuilder failed\");\n\n    let upstream_task =\n        tokio::spawn(async move { downstream_relay.relay_data(client_recv, target_send).await });\n\n    let downstream_task =\n        tokio::spawn(async move { upstream_relay.relay_data(target_recv, client_send).await });\n\n    let downstream_stats = downstream_task.await??;\n    let upstream_stats = upstream_task.await??;\n\n    Ok(TunnelStats {\n        tunnel_ctx: ctx,\n        result: EstablishTunnelResult::Ok,\n        upstream_stats: Some(upstream_stats),\n        downstream_stats: Some(downstream_stats),\n    })\n}\n\n// cov:begin-ignore-line\nimpl fmt::Display for TunnelCtx {\n    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {\n        write!(f, \"{}\", self.id)\n    }\n}\n// cov:end-ignore-line\n\n#[cfg(test)]\nmod test {\n    extern crate tokio;\n\n    use async_trait::async_trait;\n    use std::time::Duration;\n\n    use tokio::io;\n    use tokio_test::io::Builder;\n    use tokio_test::io::Mock;\n\n    use crate::relay::RelayPolicy;\n\n    use self::tokio::io::{AsyncWriteExt, Error, ErrorKind};\n    use crate::configuration::{ClientConnectionConfig, TargetConnectionConfig, TunnelConfig};\n    use crate::http_tunnel_codec::{HttpTunnelCodec, HttpTunnelCodecBuilder, HttpTunnelTarget};\n    use crate::proxy_target::TargetConnector;\n    use crate::tunnel::{ConnectionTunnel, EstablishTunnelResult, TunnelCtxBuilder, TunnelTarget};\n    use rand::{thread_rng, Rng};\n    use regex::Regex;\n\n    #[tokio::test]\n    async fn test_tunnel_ok() {\n        let handshake_request = b\"CONNECT foo.bar:80 HTTP/1.1\\r\\n\\r\\n\";\n        let handshake_response = b\"HTTP/1.1 200 OK\\r\\n\\r\\n\";\n        let tunneled_request = b\"0: Some arbibrary request\";\n        let tunneled_response = b\"1: Some arbibrary response\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(handshake_response)\n            .read(tunneled_request)\n            .write(tunneled_response)\n            .build();\n\n        let target: Mock = Builder::new()\n            .write(tunneled_request)\n            .read(tunneled_response)\n            .build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"ConnectRequestCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: Some(target),\n            delay: None,\n            error: None,\n        };\n\n        let default_timeout = Duration::from_secs(5);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::Ok);\n        assert!(stats.upstream_stats.is_some());\n        assert!(stats.downstream_stats.is_some());\n\n        let upstream_stats = stats.upstream_stats.unwrap();\n        let downstream_stats = stats.downstream_stats.unwrap();\n\n        assert_eq!(upstream_stats.total_bytes, tunneled_request.len());\n        assert_eq!(downstream_stats.total_bytes, tunneled_response.len());\n    }\n\n    #[tokio::test]\n    #[cfg(feature = \"plain_text\")]\n    async fn test_tunnel_plain_text_ok() {\n        let handshake_request =\n            b\"GET https://foo.bar/index.html HTTP/1.1\\r\\nHost: foo.bar:443\\r\\n\\r\\n\";\n        let tunneled_response = b\"HTTP/1.1 200 OK\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(tunneled_response)\n            .build();\n\n        let target: Mock = Builder::new()\n            .write(handshake_request)\n            .read(tunneled_response)\n            .build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:443\").unwrap())\n            .build()\n            .expect(\"ConnectRequestCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:443\".to_string(),\n            mock: Some(target),\n            delay: None,\n            error: None,\n        };\n\n        let default_timeout = Duration::from_secs(5);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::Ok);\n        assert!(stats.upstream_stats.is_some());\n        assert!(stats.downstream_stats.is_some());\n\n        let upstream_stats = stats.upstream_stats.unwrap();\n        let downstream_stats = stats.downstream_stats.unwrap();\n\n        assert_eq!(upstream_stats.total_bytes, 0);\n        assert_eq!(downstream_stats.total_bytes, tunneled_response.len());\n    }\n\n    #[tokio::test]\n    async fn test_tunnel_request_timeout() {\n        let handshake_response = b\"HTTP/1.1 408 TIMEOUT\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .wait(Duration::from_secs(2))\n            .write(handshake_response)\n            .build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: None,\n            delay: None,\n            error: None,\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::RequestTimeout);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_tunnel_response_timeout() {\n        let handshake_request = b\"CONNECT foo.bar:80 HTTP/1.1\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .wait(Duration::from_secs(2))\n            .build();\n\n        let target: Mock = Builder::new().build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: Some(target),\n            delay: None,\n            error: None,\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::RequestTimeout);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_tunnel_upstream_timeout() {\n        let handshake_request = b\"CONNECT foo.bar:80 HTTP/1.1\\r\\n\\r\\n\";\n        let handshake_response = b\"HTTP/1.1 504 GATEWAY_TIMEOUT\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(handshake_response)\n            .build();\n\n        let target: Mock = Builder::new().build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: Some(target),\n            delay: Some(Duration::from_secs(3)),\n            error: None,\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::GatewayTimeout);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_tunnel_bad_target() {\n        let handshake_request = b\"CONNECT disallowed.com:80 HTTP/1.1\\r\\n\\r\\n\";\n        let handshake_response = b\"HTTP/1.1 403 FORBIDDEN\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(handshake_response)\n            .build();\n\n        let target: Mock = Builder::new().build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: Some(target),\n            delay: None,\n            error: None,\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::Forbidden);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_tunnel_bad_gateway() {\n        let handshake_request = b\"CONNECT foo.bar:80 HTTP/1.1\\r\\n\\r\\n\";\n        let handshake_response = b\"HTTP/1.1 502 BAD_GATEWAY\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(handshake_response)\n            .build();\n\n        let _target: Mock = Builder::new().build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: None,\n            delay: None,\n            error: Some(ErrorKind::BrokenPipe),\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::BadGateway);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    #[tokio::test]\n    async fn test_tunnel_bad_request() {\n        let handshake_request = b\"CONNECT\\tfoo.bar:80\\tHTTP/1.1\\r\\n\\r\\n\";\n        let handshake_response = b\"HTTP/1.1 400 BAD_REQUEST\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(handshake_response)\n            .build();\n\n        let _target: Mock = Builder::new().build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: None,\n            delay: None,\n            error: Some(ErrorKind::BrokenPipe),\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::BadRequest);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    #[tokio::test]\n    #[cfg(not(feature = \"plain_text\"))]\n    async fn test_tunnel_not_allowed() {\n        let handshake_request = b\"GET foo.bar:80 HTTP/1.1\\r\\n\\r\\n\";\n        let handshake_response = b\"HTTP/1.1 405 NOT_ALLOWED\\r\\n\\r\\n\";\n\n        let client: Mock = Builder::new()\n            .read(handshake_request)\n            .write(handshake_response)\n            .build();\n\n        let _target: Mock = Builder::new().build();\n\n        let ctx = TunnelCtxBuilder::default()\n            .id(thread_rng().gen::<u128>())\n            .build()\n            .expect(\"TunnelCtxBuilder failed\");\n\n        let codec: HttpTunnelCodec = HttpTunnelCodecBuilder::default()\n            .tunnel_ctx(ctx)\n            .enabled_targets(Regex::new(r\"foo\\.bar:80\").unwrap())\n            .build()\n            .expect(\"HttpTunnelCodecBuilder failed\");\n\n        let connector = MockTargetConnector {\n            target: \"foo.bar:80\".to_string(),\n            mock: None,\n            delay: None,\n            error: Some(ErrorKind::BrokenPipe),\n        };\n\n        let default_timeout = Duration::from_secs(1);\n        let config = build_config(default_timeout);\n\n        let result = ConnectionTunnel::new(codec, connector, client, config, ctx)\n            .start()\n            .await;\n\n        assert!(result.is_ok());\n        let stats = result.unwrap();\n        assert_eq!(stats.result, EstablishTunnelResult::OperationNotAllowed);\n        assert!(stats.upstream_stats.is_none());\n        assert!(stats.downstream_stats.is_none());\n    }\n\n    fn build_config(default_timeout: Duration) -> TunnelConfig {\n        TunnelConfig {\n            client_connection: ClientConnectionConfig {\n                initiation_timeout: default_timeout,\n                relay_policy: RelayPolicy {\n                    idle_timeout: default_timeout,\n                    min_rate_bpm: 0,\n                    max_rate_bps: 120410065,\n                },\n            },\n            target_connection: TargetConnectionConfig {\n                dns_cache_ttl: default_timeout,\n                allowed_targets: Regex::new(r\"foo\\.bar:80\").unwrap(),\n                connect_timeout: default_timeout,\n                relay_policy: RelayPolicy {\n                    idle_timeout: default_timeout,\n                    min_rate_bpm: 0,\n                    max_rate_bps: 170310180,\n                },\n            },\n        }\n    }\n\n    struct MockTargetConnector {\n        target: String,\n        mock: Option<Mock>,\n        delay: Option<Duration>,\n        error: Option<ErrorKind>,\n    }\n\n    #[async_trait]\n    impl TargetConnector for MockTargetConnector {\n        type Target = HttpTunnelTarget;\n        type Stream = Mock;\n\n        async fn connect(&mut self, target: &Self::Target) -> io::Result<Self::Stream> {\n            let target_addr = &target.target_addr();\n            assert_eq!(&self.target, target_addr);\n\n            if let Some(d) = self.delay {\n                tokio::time::sleep(d).await;\n            }\n\n            match self.error {\n                None => {\n                    let mut stream = self.mock.take().unwrap();\n                    if target.has_nugget() {\n                        stream.write_all(&target.nugget().data()).await?;\n                    }\n                    Ok(stream)\n                }\n                Some(e) => Err(Error::from(e)),\n            }\n        }\n    }\n}\n"
  }
]