Showing preview only (327K chars total). Download the full file or copy to clipboard to get everything.
Repository: ngrok/ngrok-rs
Branch: main
Commit: 4133242da599
Files: 50
Total size: 310.5 KB
Directory structure:
gitextract_5pce2p9h/
├── .envrc
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── docs.yml
│ ├── publish.yml
│ ├── release.yml
│ └── rust-cache/
│ └── action.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── cargo-doc-ngrok/
│ ├── Cargo.toml
│ └── src/
│ └── main.rs
├── flake.nix
├── ngrok/
│ ├── CHANGELOG.md
│ ├── Cargo.toml
│ ├── README.md
│ ├── assets/
│ │ ├── ngrok.ca.crt
│ │ ├── policy-inbound.json
│ │ └── policy.json
│ ├── examples/
│ │ ├── axum.rs
│ │ ├── connect.rs
│ │ ├── domain.crt
│ │ ├── domain.key
│ │ ├── labeled.rs
│ │ ├── mingrok.rs
│ │ └── tls.rs
│ └── src/
│ ├── config/
│ │ ├── common.rs
│ │ ├── headers.rs
│ │ ├── http.rs
│ │ ├── labeled.rs
│ │ ├── oauth.rs
│ │ ├── oidc.rs
│ │ ├── policies.rs
│ │ ├── tcp.rs
│ │ ├── tls.rs
│ │ └── webhook_verification.rs
│ ├── conn.rs
│ ├── forwarder.rs
│ ├── internals/
│ │ ├── proto.rs
│ │ ├── raw_session.rs
│ │ └── rpc.rs
│ ├── lib.rs
│ ├── online_tests.rs
│ ├── proxy_proto.rs
│ ├── session.rs
│ ├── tunnel.rs
│ └── tunnel_ext.rs
└── rustfmt.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .envrc
================================================
use flake
================================================
FILE: .github/workflows/ci.yml
================================================
on:
push:
branches: [main]
pull_request:
workflow_call:
secrets:
NGROK_AUTHTOKEN:
required: true
name: Continuous integration
jobs:
udeps:
name: Udeps
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jrobsonchase/direnv-action@v0.7
- uses: ./.github/workflows/rust-cache
- uses: actions-rs/cargo@v1
with:
command: udeps
args: --workspace --all-targets --all-features
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jrobsonchase/direnv-action@v0.7
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jrobsonchase/direnv-action@v0.7
- uses: ./.github/workflows/rust-cache
- uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all-features --workspace -- -D warnings
test-nix:
name: Test Nix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jrobsonchase/direnv-action@v0.7
- uses: ./.github/workflows/rust-cache
- uses: actions-rs/cargo@v1
env:
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
with:
command: test
args: --workspace --all-targets
test-stable:
name: Test Stable
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
# We don't actually have sccache installed here (yet), but it still
# benefits from the cargo cache.
- uses: ./.github/workflows/rust-cache
- uses: actions-rs/cargo@v1
env:
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
with:
command: test
args: --features=paid-tests,long-tests --workspace --all-targets
test-win:
name: Test Windows Stable
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
# We don't actually have sccache installed here (yet), but it still
# benefits from the cargo cache.
- uses: ./.github/workflows/rust-cache
- uses: actions-rs/cargo@v1
env:
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
with:
command: test
args: --workspace --all-targets
semver:
name: Semver Check
runs-on: ubuntu-latest
strategy:
matrix:
crate: [muxado, ngrok]
steps:
- uses: actions/checkout@v4
- uses: jrobsonchase/direnv-action@v0.7
- uses: ./.github/workflows/rust-cache
- uses: actions-rs/cargo@v1
name: semver checks
with:
command: semver-checks
args: check-release -p ${{ matrix.crate }}
================================================
FILE: .github/workflows/docs.yml
================================================
on:
push:
branches: [main]
name: Publish Docs
jobs:
build:
name: Build Rustdocs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: update apt
run: sudo apt-get update
- name: install protoc
run: sudo apt-get -o Acquire::Retries=3 install -y protobuf-compiler
- uses: actions-rs/cargo@v1
with:
command: doc
args: --no-deps
- name: Archive docs
shell: sh
run: |
echo "<meta http-equiv=\"refresh\" content=\"0; url=ngrok\">" > target/doc/index.html
chmod -c -R +r target/doc | while read line; do
echo "::warning title=Changed permissions on a file::$line"
done
- name: Upload static files as artifact
uses: actions/upload-pages-artifact@v3
with:
path: target/doc
# Deployment job
deploy:
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/publish.yml
================================================
on:
workflow_dispatch:
name: Publish All
jobs:
ci:
name: Run CI
uses: ./.github/workflows/ci.yml
secrets:
NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
# Publishing jobs - these run sequentially as before
publish-muxado:
name: Publish muxado
uses: ./.github/workflows/release.yml
needs: [ci]
permissions:
contents: write
with:
crate: muxado
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
publish-ngrok:
name: Publish ngrok
uses: ./.github/workflows/release.yml
needs: [publish-muxado]
if: needs.publish-muxado.result == 'success' || needs.publish-muxado.result == 'skipped'
permissions:
contents: write
with:
crate: ngrok
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
publish-cargo-doc-ngrok:
name: Publish cargo-doc-ngrok
uses: ./.github/workflows/release.yml
needs: [publish-ngrok]
if: needs.publish-ngrok.result == 'success' || needs.publish-ngrok.result == 'skipped'
permissions:
contents: write
with:
crate: cargo-doc-ngrok
secrets:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
================================================
FILE: .github/workflows/release.yml
================================================
on:
workflow_dispatch:
inputs:
crate:
description: 'Crate to release'
required: true
default: 'ngrok'
workflow_call:
inputs:
crate:
description: 'Crate to release'
required: true
type: string
secrets:
CARGO_REGISTRY_TOKEN:
required: true
name: Release
jobs:
cargo-publish:
name: Publish and Tag
runs-on: ubuntu-latest
permissions:
contents: write
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: jrobsonchase/direnv-action@v0.7
- name: cargo publish
run: |
version="$(extract-crate-version ${{inputs.crate}})"
crate="${{inputs.crate}}"
tag="${crate}-v${version}"
echo "Checking if crate $crate version $version exists on crates.io"
result=$(cargo search $crate --limit 1 | grep "$version" || true)
if [ -n "$result" ]; then
echo "Crate $crate version $version already exists on crates.io, skipping publish."
exit 0
fi
echo "Crate version $version not found on crates.io, proceeding with publish."
cargo publish -p $crate --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: tag release
run: |
version="$(extract-crate-version ${{inputs.crate}})"
git config user.name "GitHub Action"
git config user.email noreply@ngrok.com
tag="${{inputs.crate}}-v${version}"
echo "Version ${version}, tag ${tag}"
echo "Fetching all tags in the repository"
git fetch --tags
if git rev-parse "refs/tags/$tag" >/dev/null 2>&1; then
echo "Tag $tag already exists, skipping tag creation."
else
echo "Tag $tag does not exist, pushing tag."
git tag -a -m "Version ${version}" $tag
git push --tags
fi
================================================
FILE: .github/workflows/rust-cache/action.yml
================================================
name: 'rust cache setup'
description: 'Set up cargo and sccache caches'
inputs: {}
outputs: {}
runs:
using: "composite"
steps:
- name: configure sccache
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
core.exportVariable('SCCACHE_GHA_CACHE_TO', 'sccache-${{runner.os}}-${{github.ref_name}}');
core.exportVariable('SCCACHE_GHA_CACHE_FROM', 'sccache-${{runner.os}}-main,sccache-${{runner.os}}-');
- name: cargo registry cache
uses: actions/cache@v3
with:
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-${{ github.sha }}
restore-keys: |
cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.toml') }}-
cargo-${{ runner.os }}-
path: |
~/.cargo/registry
~/.cargo/git
================================================
FILE: .gitignore
================================================
.env
/target
.direnv
/.vscode
*.swp
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# ngrok Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
The ngrok documentation team is responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
The ngrok documentation team has the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the ngrok docs project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [support@ngrok.com](mailto:support@ngrok.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to ngrok-rust
Thank you for deciding to contribute to ngrok-rust!
## Reporting a bug
To report a bug, please [open a new issue](https://github.com/ngrok/ngrok-rust/issues/new) with clear reproduction steps. We will triage and investigate these issues at a regular interval.
## Contributing code
Bugfixes and small improvements are always appreciated!
For any larger changes or features, please [open a new issue](https://github.com/ngrok/ngrok-rust/issues/new) first to discuss whether the change makes sense. When in doubt, it's always okay to open an issue first.
================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "2"
members = [
"muxado",
"ngrok",
"cargo-doc-ngrok",
]
[profile.release]
debug = 1
================================================
FILE: LICENSE-APACHE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: LICENSE-MIT
================================================
Copyright 2022 ngrok, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: cargo-doc-ngrok/Cargo.toml
================================================
[package]
name = "cargo-doc-ngrok"
version = "0.2.2"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "A cargo subcommand to build and serve documentation via ngrok"
repository = "https://github.com/ngrok/ngrok-rust"
[dependencies]
awaitdrop = "0.1.2"
axum = "0.7.4"
bstr = "1.4.0"
cargo_metadata = "0.15.2"
clap = { version = "4.0.29", features = ["derive"] }
futures = "0.3.25"
http = "1.0.0"
hyper = { version = "1.1.0", features = ["server"] }
hyper-staticfile = "0.10.0"
hyper-util = { version = "0.1.3", features = ["server", "tokio", "server-auto", "http1"] }
ngrok = { path = "../ngrok", version = "0.18", features = ["hyper", "axum"] }
tokio = { version = "1.23.0", features = ["full"] }
watchexec = "2.3.0"
# watchexec-signals 1.0.1 causes a compilation error.
# this will likely be ironed out as they release watchexec 3.0.0 components.
# https://github.com/watchexec/watchexec/issues/701
watchexec-signals = "=1.0.0"
================================================
FILE: cargo-doc-ngrok/src/main.rs
================================================
use std::{
io,
path::PathBuf,
process::Stdio,
sync::Arc,
};
use axum::BoxError;
use clap::{
Args,
Parser,
Subcommand,
};
use futures::TryStreamExt;
use hyper::service::service_fn;
use hyper_util::{
rt::TokioExecutor,
server,
};
use ngrok::prelude::*;
use watchexec::{
action::{
Action,
Outcome,
},
command::Command,
config::{
InitConfig,
RuntimeConfig,
},
error::CriticalError,
handler::PrintDebug,
signal::source::MainSignal,
Watchexec,
};
#[derive(Parser, Debug)]
struct Cargo {
#[command(subcommand)]
cmd: Cmd,
}
#[derive(Debug, Subcommand)]
enum Cmd {
DocNgrok(DocNgrok),
}
#[derive(Debug, Args)]
struct DocNgrok {
#[arg(short)]
package: Option<String>,
#[arg(long, short)]
domain: Option<String>,
#[arg(long, short)]
watch: bool,
#[arg(last = true)]
doc_args: Vec<String>,
}
#[tokio::main]
async fn main() -> Result<(), BoxError> {
let Cmd::DocNgrok(args) = Cargo::parse().cmd;
std::process::Command::new("cargo")
.arg("doc")
.args(args.doc_args.iter())
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.spawn()?
.wait()?;
let meta = cargo_metadata::MetadataCommand::new().exec()?;
let default_package = args
.package
.or(meta.root_package().map(|p| p.name.clone()))
.ok_or("No default package found. You must provide one with -p")?;
let root_dir = meta.workspace_root;
let target_dir = meta.target_directory;
let doc_dir = target_dir.join("doc");
let sess = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let mut listen_cfg = sess.http_endpoint();
if let Some(domain) = args.domain {
listen_cfg.domain(domain);
}
let mut listener = listen_cfg.listen().await?;
let service = service_fn(move |req| {
let stat = hyper_staticfile::Static::new(&doc_dir);
stat.serve(req)
});
println!(
"serving docs on: {}/{}/",
listener.url(),
default_package.replace('-', "_")
);
let server = async move {
let (dropref, waiter) = awaitdrop::awaitdrop();
// Continuously accept new connections.
while let Some(conn) = listener.try_next().await? {
let service = service.clone();
let dropref = dropref.clone();
// Spawn a task to handle the connection. That way we can multiple connections
// concurrently.
tokio::spawn(async move {
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection(conn, service)
.await
{
eprintln!("failed to serve connection: {err:#}");
}
drop(dropref);
});
}
// Wait until all children have finished, not just the listener.
drop(dropref);
waiter.await;
Ok::<(), BoxError>(())
};
if args.watch {
let we = make_watcher(args.doc_args, root_dir, target_dir)?;
we.main().await??;
} else {
server.await?;
}
Ok(())
}
fn make_watcher(
args: Vec<String>,
root_dir: impl Into<PathBuf>,
target_dir: impl Into<PathBuf>,
) -> Result<Arc<Watchexec>, Box<CriticalError>> {
let target_dir = target_dir.into();
let root_dir = root_dir.into();
let mut init = InitConfig::default();
init.on_error(PrintDebug(std::io::stderr()));
let mut runtime = RuntimeConfig::default();
runtime.pathset([root_dir]);
runtime.command(Command::Exec {
prog: "cargo".into(),
args: [String::from("doc")].into_iter().chain(args).collect(),
});
runtime.on_action({
move |action: Action| {
let target_dir = target_dir.clone();
async move {
let sigs = action
.events
.iter()
.flat_map(|event| event.signals())
.collect::<Vec<_>>();
if sigs.iter().any(|sig| sig == &MainSignal::Interrupt) {
action.outcome(Outcome::Exit);
} else if action
.events
.iter()
.any(|e| e.paths().any(|(p, _)| !p.starts_with(&target_dir)))
{
action.outcome(Outcome::if_running(
Outcome::both(Outcome::Stop, Outcome::Start),
Outcome::Start,
));
}
Result::<_, io::Error>::Ok(())
}
}
});
Watchexec::new(init, runtime).map_err(Box::new)
}
================================================
FILE: flake.nix
================================================
{
description = "ngrok agent library in Rust";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
# Note: fenix packages are cached via cachix:
# cachix use nix-community
fenix-flake = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils = {
url = "github:numtide/flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, fenix-flake, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [
fenix-flake.overlays.default
];
};
toolchain = pkgs.fenix.complete.withComponents [
"cargo"
"clippy"
"rust-src"
"rustc"
"rustfmt"
"rust-analyzer"
];
fix-n-fmt = pkgs.writeShellScriptBin "fix-n-fmt" ''
set -euf -o pipefail
${toolchain}/bin/cargo clippy --fix --allow-staged --allow-dirty --all-targets --all-features
${toolchain}/bin/cargo fmt
'';
pre-commit = pkgs.writeShellScript "pre-commit" ''
cargo clippy --workspace --all-targets --all-features -- -D warnings
result=$?
if [[ ''${result} -ne 0 ]] ; then
cat <<\EOF
There are some linting issues, try `fix-n-fmt` to fix.
EOF
exit 1
fi
# Use a dedicated sub-target-dir for udeps. For some reason, it fights with clippy over the cache.
CARGO_TARGET_DIR=$(git rev-parse --show-toplevel)/target/udeps cargo udeps --workspace --all-targets --all-features
result=$?
if [[ ''${result} -ne 0 ]] ; then
cat <<\EOF
There are some unused dependencies.
EOF
exit 1
fi
diff=$(cargo fmt -- --check)
result=$?
if [[ ''${result} -ne 0 ]] ; then
cat <<\EOF
There are some code style issues, run `fix-n-fmt` first.
EOF
exit 1
fi
exit 0
'';
setup-hooks = pkgs.writeShellScriptBin "setup-hooks" ''
repo_root=$(git rev-parse --git-dir)
${toString (map (h: ''
ln -sf ${h} ''${repo_root}/hooks/${h.name}
'') [
pre-commit
])}
'';
# Make sure that cargo semver-checks uses the stable toolchain rather
# than the nightly one that we normally develop with.
semver-checks = with pkgs; symlinkJoin {
name = "cargo-semver-checks";
paths = [ cargo-semver-checks ];
buildInputs = [ makeWrapper ];
postBuild = ''
wrapProgram $out/bin/cargo-semver-checks \
--prefix PATH : ${rustc}/bin \
--prefix PATH : ${cargo}/bin
'';
};
extract-version = with pkgs; writeShellScriptBin "extract-crate-version" ''
${cargo}/bin/cargo metadata --format-version 1 --no-deps | \
${jq}/bin/jq -r ".packages[] | select(.name == \"$1\") | .version"
'';
in
{
devShell = pkgs.mkShell {
CHALK_OVERFLOW_DEPTH = 3000;
CHALK_SOLVER_MAX_SIZE = 1500;
OPENSSL_LIB_DIR = "${pkgs.openssl.out}/lib";
OPENSSL_INCLUDE_DIR = "${pkgs.openssl.dev}/include";
RUSTC_WRAPPER="${pkgs.sccache}/bin/sccache";
buildInputs = with pkgs; [
toolchain
fix-n-fmt
setup-hooks
cargo-udeps
semver-checks
extract-version
] ++ lib.optionals stdenv.isDarwin [
# nix darwin stdenv has broken libiconv: https://github.com/NixOS/nixpkgs/issues/158331
libiconv
pkgs.darwin.apple_sdk.frameworks.CoreServices
pkgs.darwin.apple_sdk.frameworks.Security
];
};
});
}
================================================
FILE: ngrok/CHANGELOG.md
================================================
## 0.18.0
- Add support for CEL filtering when listing resources.
- Add support for service users
- Add support for `vault_name` on Secrets
-Make `pooling_enabled` on Endpoints optional
## 0.17.0
### Breaking Changes
- **Binding is now optional**: Tests no longer hardcode `binding("public")`. The ngrok service will use its default binding configuration when not explicitly specified.
- **Binding validation**: The `binding()` method now validates input values and panics on invalid values or multiple calls.
### Added
- Added `Binding` enum with three variants: `Public`, `Internal`, and `Kubernetes`
- Added validation for binding values - only "public", "internal", and "kubernetes" are accepted (case-insensitive)
- Added `binding()` method documentation with examples for both string and typed enum usage
- Added panic behavior when `binding()` is called more than once (only one binding allowed)
### Changed
- `binding()` method now accepts both strings and the `Binding` enum via `Into<String>`
- Removed hardcoded "public" binding from all tests - bindings are now truly optional
## 0.15.0
- - Removes `hyper-proxy` and `ring` dependencies
## 0.14.0
- - Adds `pooling_enabled` option, allowing the endpoint to pool with other endpoints with the same host/port/binding
## 0.13.1
- Preserve the `ERR_NGROK` prefix for error codes.
## 0.13.0
- Add the `NgrokError` trait
- Add the `ErrResp` type
- Change the `RpcError::Response` variant to the `ErrResp` type (from `String`)
- Implement `NgrokError` for `ErrResp`, `RpcError`, and `ConnectError`
## 0.12.4
- Add `Win32_Foundation` feature
- Update nix for rust `1.72`
## 0.12.3
- Add `session.id()`
## 0.12.2
- Updated readme and changelog
## 0.12.1
- Add source error on reconnect
- Rename repository to ngrok-rust
## 0.12.0
- Add `client_info` to SessionBuilder
- Update UserAgent generation
- Make `circuit_breaker` test more reliable
## 0.11.3
- Update stream forwarding logic
- Add `ca_cert` option to SessionBuilder
- Unpin `bstr`
## 0.11.2
- Send UserAgent when authenticating
- Update readme documentation
## 0.11.0
- Include a session close method
- Mark errors as non-exhaustive
## 0.10.2
- Update default forwards-to
- Expose OAuth Client ID/Secret setters
- Muxado: close method on the opener
## 0.10.1
- Add windows pipe support
- Require tokio rt
## 0.10.0
- Some api-breaking consistency fixes for the session builder.
- Update the connector to be more in-line with the other handlers and to support
disconnect/reconnect error reporting.
- Add support for custom heartbeat handlers.
## 0.9.0
- Update docs to match ngrok-go
- Update the tls termination configuration methods to match those in ngrok-go
- Remove the `_string` suffix from the cidr restriction methods
## 0.8.1
- Fix cancellation bugs causing leaked muxado/ngrok sessions.
## 0.8.0
- Some breaking changes to builder method naming for consistency.
- Add dashboard command handlers
## 0.7.0
- Initial crates.io release.
## Pre-0.7.0
- There was originally a crate on crates.io named 'ngrok' that wrapped the agent
binary. It can be found [here](https://github.com/nkconnor/ngrok).
Thanks @nkconnor!
================================================
FILE: ngrok/Cargo.toml
================================================
[package]
name = "ngrok"
version = "0.18.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "The ngrok agent SDK"
repository = "https://github.com/ngrok/ngrok-rust"
[dependencies]
arc-swap = "1.5.2"
async-trait = "0.1.59"
awaitdrop = "0.1.1"
axum = { version = "0.7.4", features = ["tokio"], optional = true }
axum-core = "0.4.3"
base64 = "0.21.7"
bitflags = "2.4.2"
bytes = "1.10.1"
futures = "0.3.25"
futures-rustls = { version = "0.26.0", default-features = false, features = ["tls12", "logging"] }
futures-util = "0.3.30"
hostname = "0.3.1"
hyper = { version = "^1.1.0", optional = true }
hyper-http-proxy = "1.1.0"
hyper-util = { version = "0.1.3", features = ["tokio"] }
once_cell = "1.17.1"
muxado = { path = "../muxado", version = "0.5" }
pin-project = "1.1.3"
parking_lot = "0.12.1"
proxy-protocol = "0.5.0"
regex = "1.7.3"
rustls-native-certs = "0.7.0"
rustls-pemfile = "2.0.0"
serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89"
thiserror = "2"
tokio = { version = "1.23.0", features = [
"io-util",
"net",
"sync",
"time",
"rt",
] }
tokio-retry = "0.3.0"
tokio-socks = "0.5.1"
tokio-util = { version = "0.7.4", features = ["compat"] }
tower-service = { version = "0.3.3"}
tracing = "0.1.37"
url = "2.4.0"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.45.0", features = ["Win32_Foundation"] }
[dev-dependencies]
anyhow = "1.0.66"
axum = { version = "0.7.4", features = ["tokio"] }
flate2 = "1.0.25"
http-body-util = "0.1.3"
hyper = { version = "1.1.0", features = [ "client" ] }
hyper-util = { version = "0.1.3", features = [
"tokio",
"server",
"http1",
"http2",
]}
paste = "1.0.11"
rand = "0.8.5"
reqwest = "0.12"
tokio = { version = "1.23.0", features = ["full"] }
tokio-tungstenite = { version = "0.26.2", features = [
"rustls",
"rustls-tls-webpki-roots",
] }
tower = { version = "0.5", features = ["util"] }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing-test = "0.2.3"
[[example]]
name = "tls"
required-features = ["axum"]
[[example]]
name = "axum"
required-features = ["axum"]
[[example]]
name = "labeled"
required-features = ["axum"]
[[example]]
name = "mingrok"
required-features = ["hyper"]
[features]
default = ["aws-lc-rs"]
hyper = ["hyper/server", "hyper/http1", "dep:hyper"]
axum = ["dep:axum", "hyper"]
online-tests = ["axum", "hyper"]
long-tests = ["online-tests"]
authenticated-tests = ["online-tests"]
paid-tests = ["authenticated-tests"]
aws-lc-rs = ["futures-rustls/aws-lc-rs"]
ring = ["futures-rustls/ring"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
================================================
FILE: ngrok/README.md
================================================
# ngrok-rust
[![Crates.io][crates-badge]][crates-url]
[![docs.rs][docs-badge]][docs-url]
[![MIT licensed][mit-badge]][mit-url]
[![Apache-2.0 licensed][apache-badge]][apache-url]
[![Continuous integration][ci-badge]][ci-url]
[crates-badge]: https://img.shields.io/crates/v/ngrok.svg
[crates-url]: https://crates.io/crates/ngrok
[docs-badge]: https://img.shields.io/docsrs/ngrok.svg
[docs-url]: https://docs.rs/ngrok
[ci-badge]: https://github.com/ngrok/ngrok-rust/actions/workflows/ci.yml/badge.svg
[ci-url]: https://github.com/ngrok/ngrok-rust/actions/workflows/ci.yml
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/ngrok/ngrok-rust/blob/main/LICENSE-MIT
[apache-badge]: https://img.shields.io/badge/license-Apache_2.0-blue.svg
[apache-url]: https://github.com/ngrok/ngrok-rust/blob/main/LICENSE-APACHE
[API Docs (main)](https://ngrok.github.io/ngrok-rust/ngrok)
[ngrok](https://ngrok.com) is a simplified API-first ingress-as-a-service that adds connectivity,
security, and observability to your apps.
ngrok-rust, our native and idiomatic crate for adding a public internet address
with secure ingress traffic directly into your Rust apps 🦀. If you’ve used ngrok in
the past, you can think of ngrok-rust as the ngrok agent packaged as a Rust crate.
ngrok-rust lets developers serve Rust services on the internet in a single statement
without setting up low-level network primitives like IPs, NAT, certificates,
load balancers, and even ports! Applications using ngrok-rust listen on ngrok’s global
ingress network for TCP and HTTP traffic. ngrok-rust listeners are usable with
[hyper Servers](https://docs.rs/hyper/latest/hyper/server/index.html), and connections
implement [tokio’s AsyncRead and AsyncWrite traits](https://docs.rs/tokio/latest/tokio/io/index.html).
This makes it easy to add ngrok-rust into any application that’s built on hyper, such
as the popular [axum](https://docs.rs/axum/latest/axum/) HTTP framework.
See [`/ngrok/examples/`][examples] for example usage, or the tests in
[`/ngrok/src/online_tests.rs`][online-tests].
[examples]: https://github.com/ngrok/ngrok-rust/blob/main/ngrok/examples
[online-tests]: https://github.com/ngrok/ngrok-rust/blob/main/ngrok/src/online_tests.rs
For working with the [ngrok API](https://ngrok.com/docs/api/), check out the
[ngrok Rust API Client Library](https://github.com/ngrok/ngrok-api-rs).
## Installation
Add `ngrok` to the `[dependencies]` section of your `Cargo.toml` with `cargo add`:
```bash
$ cargo add ngrok
```
## Quickstart
Create a simple HTTP server using `ngrok` and `axum`:
`Cargo.toml`:
```toml
[package]
name = "ngrok-rust-demo"
version = "0.1.0"
edition = "2021"
[dependencies]
ngrok = {version = "0.14.0"}
tokio = { version = "1", features = [
"full"
] }
axum = { version = "0.7.4", features = ["tokio"] }
async-trait = "0.1.59"
hyper = {version = "1", features = ["full"]}
hyper-util = { version = "0.1", features = [
"full"
] }
url = "2.5.4"
```
`src/main.rs`:
```rust
#![deny(warnings)]
use axum::{routing::get, Router};
use ngrok::config::{ForwarderBuilder, TunnelBuilder};
use std::net::SocketAddr;
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Create Axum app
let app = Router::new().route("/", get(|| async { "Hello from Axum!" }));
// Spawn Axum server
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tokio::spawn(async move {
axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
.await
.unwrap();
});
// Set up ngrok tunnel
let sess1 = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let sess2 = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let _listener = sess1
.http_endpoint()
.domain("/* your domain*/")
.pooling_enabled(true)
.listen_and_forward(Url::parse("http://localhost:3000").unwrap())
.await?;
let _listener2 = sess2
.http_endpoint()
.domain("/* your domain */")
.pooling_enabled(true)
.listen_and_forward(Url::parse("http://localhost:8000").unwrap())
.await?;
// Wait indefinitely
tokio::signal::ctrl_c().await?;
Ok(())
}
```
# Changelog
Changes to `ngrok-rust` are tracked under [CHANGELOG.md](https://github.com/ngrok/ngrok-rust/blob/main/ngrok/CHANGELOG.md).
# Join the ngrok Community
- Check out [our official docs](https://docs.ngrok.com)
- Read about updates on [our blog](https://ngrok.com/blog)
- Open an [issue](https://github.com/ngrok/ngrok-rust/issues) or [pull request](https://github.com/ngrok/ngrok-rust/pulls)
- Join our [Slack community](https://ngrok.com/slack)
- Follow us on [X / Twitter (@ngrokHQ)](https://twitter.com/ngrokhq)
- Subscribe to our [Youtube channel (@ngrokHQ)](https://www.youtube.com/@ngrokhq)
# License
This project is licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE][apache-url] or
<http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT][mit-url] or
<http://opensource.org/licenses/MIT>)
at your option.
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in ngrok by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.
================================================
FILE: ngrok/assets/ngrok.ca.crt
================================================
-----BEGIN CERTIFICATE-----
MIID4TCCAsmgAwIBAgIUZqF2AkB17pISojTndgc2U5BDt74wDQYJKoZIhvcNAQEL
BQAwbzEQMA4GA1UEAwwHUm9vdCBDQTENMAsGA1UECwwEcHJvZDESMBAGA1UECgwJ
bmdyb2sgSW5jMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIDApDYWxp
Zm9ybmlhMQswCQYDVQQGEwJVUzAgFw0yMjA4MzExNTE3MjFaGA80NzYwMDcyODE1
MTcyMVowbzEQMA4GA1UEAwwHUm9vdCBDQTENMAsGA1UECwwEcHJvZDESMBAGA1UE
CgwJbmdyb2sgSW5jMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIDApD
YWxpZm9ybmlhMQswCQYDVQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAMPkZpOguChG8QXfp1eCu21wipptiWO9U6F2DRf5ln8XXAAokZyfo4IZ
795G+KdkEbq4KxSXHehhKQFDwlFnzIkZsDu6PHabXsutAmNLmoRQzsETTdh3gMEJ
JiCW+mtqmbWPH22GXnUXxe5R6dWbkXqrITy6nFpZWdFbKmo9/1VoyWdIgcXujq2D
aNCWm2BoQ9seCebc5+6gF2syXzvoKVZ4qg6O1anCl1K0ZH/2mDXu/22O2U4Tr/j7
6Da1Y7TWZYDU2dIz+tyfTOMrlaxXyxxmXewzOpYjBiHisfPpz7AtrTlAzaEVVhRk
c86vC2h42zqH8Jv0fjJdfMkVXe3eegECAwEAAaNzMHEwHQYDVR0OBBYEFNxeUxPI
M8G7cX0DhFc81pLD4W+HMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEG
MC8GA1UdHwQoMCYwJKAioCCGHmh0dHA6Ly9jcmwubmdyb2suY29tL25ncm9rLmNy
bDANBgkqhkiG9w0BAQsFAAOCAQEAChXl+eYIQbn0OOHLuCBvXxDKHqccJLPaxJR1
LeWj8HjWbyLXnS405YNn84NFirpYzemeYSex+os92kjjLhBXEOIEpAE9JebDk7N5
X4xSOkS7vrOepX4JFNhqVdxut7pqEmuj1Xf7KhHtFquFM5fhLJHnWEJGWOTRbRVp
KWqZI/HzaltSbgiikf3S2qu6oZHph/BpueCqLKwvJziPQGE+cYdYQzRKPJZbuorj
+CnYUXd7kHC3RZzs6egVIvUYy+bGgv1CeeAm9EccL2RmPkSzThOo6oXBLR50Zlke
1x7y/5om6zp9vGTW4PWVAW/VWw1x4zxtSQ7NrP1Ldh7Xmnb7sw==
-----END CERTIFICATE-----
================================================
FILE: ngrok/assets/policy-inbound.json
================================================
{
"inbound": [
{
"name": "test_in",
"expressions": [
"req.Method == 'PUT'"
],
"actions": [
{
"type": "deny"
}
]
}
]
}
================================================
FILE: ngrok/assets/policy.json
================================================
{
"inbound": [
{
"name": "test_in",
"expressions": [
"req.Method == 'PUT'"
],
"actions": [
{
"type": "deny"
}
]
}
],
"outbound": [
{
"name": "test_out",
"expressions": [
"res.StatusCode == '200'"
],
"actions": [
{
"type": "custom-response",
"config": {
"status_code": 201
}
}
]
}
]
}
================================================
FILE: ngrok/examples/axum.rs
================================================
use std::{
convert::Infallible,
net::SocketAddr,
};
use axum::{
extract::ConnectInfo,
routing::get,
Router,
};
use axum_core::BoxError;
use futures::stream::TryStreamExt;
use hyper::{
body::Incoming,
Request,
};
use hyper_util::{
rt::TokioExecutor,
server,
};
use ngrok::prelude::*;
use tower::{
util::ServiceExt,
Service,
};
#[tokio::main]
async fn main() -> Result<(), BoxError> {
// build our application with a single route
let app = Router::new().route(
"/",
get(
|ConnectInfo(remote_addr): ConnectInfo<SocketAddr>| async move {
format!("Hello, {remote_addr:?}!\r\n")
},
),
);
let mut listener = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?
.http_endpoint()
// .allow_cidr("0.0.0.0/0")
// .basic_auth("ngrok", "online1line")
// .circuit_breaker(0.5)
// .compression()
// .deny_cidr("10.1.1.1/32")
// .verify_upstream_tls(false)
// .domain("<somedomain>.ngrok.io")
// .forwards_to("example rust")
// .mutual_tlsca(CA_CERT.into())
// .oauth(
// OauthOptions::new("google")
// .allow_email("<user>@<domain>")
// .allow_domain("<domain>")
// .scope("<scope>"),
// )
// .oidc(
// OidcOptions::new("<url>", "<id>", "<secret>")
// .allow_email("<user>@<domain>")
// .allow_domain("<domain>")
// .scope("<scope>"),
// )
// .traffic_policy(POLICY_JSON)
// .pooling_enabled(false)
// .proxy_proto(ProxyProto::None)
// .remove_request_header("X-Req-Nope")
// .remove_response_header("X-Res-Nope")
// .request_header("X-Req-Yup", "true")
// .response_header("X-Res-Yup", "true")
// .scheme(ngrok::Scheme::HTTPS)
// .websocket_tcp_conversion()
// .webhook_verification("twilio", "asdf"),
.metadata("example tunnel metadata from rust")
.listen()
.await?;
println!("Listener started on URL: {:?}", listener.url());
let mut make_service = app.into_make_service_with_connect_info::<SocketAddr>();
let server = async move {
while let Some(conn) = listener.try_next().await? {
let remote_addr = conn.remote_addr();
let tower_service = unwrap_infallible(make_service.call(remote_addr).await);
tokio::spawn(async move {
let hyper_service =
hyper::service::service_fn(move |request: Request<Incoming>| {
tower_service.clone().oneshot(request)
});
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(conn, hyper_service)
.await
{
eprintln!("failed to serve connection: {err:#}");
}
});
}
Ok::<(), BoxError>(())
};
server.await?;
Ok(())
}
#[allow(dead_code)]
const POLICY_JSON: &str = r###"{
"inbound":[
{
"name":"deny_put",
"expressions":["req.Method == 'PUT'"],
"actions":[{"Type":"deny"}]
}],
"outbound":[
{
"name":"change success response",
"expressions":["res.StatusCode == '200'"],
"actions":[{
"type":"custom-response",
"config":{
"status_code":201,
"content": "Custom 200 response.",
"headers": {
"content_type": "text/html"
}
}
}]
}]
}"###;
#[allow(dead_code)]
const POLICY_YAML: &str = r###"
---
inbound:
- name: "deny_put"
expressions:
- "req.Method == 'PUT'"
actions:
- type: "deny"
outbound:
- name: "change success response"
expressions:
- "res.StatusCode == '200'"
actions:
- type: "custom-response"
config:
status_code: 201
content: "Custom 200 response."
headers:
content_type: "text/html"
"###;
#[allow(dead_code)]
fn create_policy() -> Result<Policy, InvalidPolicy> {
Ok(Policy::new()
.add_inbound(
Rule::new("deny_put")
.add_expression("req.Method == 'PUT'")
.add_action(Action::new("deny", None)?),
)
.add_outbound(
Rule::new("200_response")
.add_expression("res.StatusCode == '200'")
.add_action(Action::new(
"custom-response",
Some(
r###"{
"status_code": 200,
"content_type": "text/html",
"content": "Custom 200 response."
}"###,
),
)?),
)
.to_owned())
}
// const CA_CERT: &[u8] = include_bytes!("ca.crt");
fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
match result {
Ok(value) => value,
Err(err) => match err {},
}
}
================================================
FILE: ngrok/examples/connect.rs
================================================
use futures::TryStreamExt;
use ngrok::prelude::*;
use tokio::io::{
self,
AsyncBufReadExt,
AsyncWriteExt,
BufReader,
};
use tracing::info;
use tracing_subscriber::fmt::format::FmtSpan;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.pretty()
.with_span_events(FmtSpan::ENTER)
.with_env_filter(std::env::var("RUST_LOG").unwrap_or_default())
.init();
let sess = ngrok::Session::builder()
.authtoken_from_env()
.metadata("Online in One Line")
// .root_cas("trusted")?
.connect()
.await?;
let tunnel = sess
.tcp_endpoint()
// .allow_cidr("0.0.0.0/0")
// .deny_cidr("10.1.1.1/32")
// .verify_upstream_tls(false)
// .pooling_enabled(false)
// .forwards_to("example rust"),
// .proxy_proto(ProxyProto::None)
// .remote_addr("<n>.tcp.ngrok.io:<p>")
.metadata("example tunnel metadata from rust")
.listen()
.await?;
handle_tunnel(tunnel, sess);
futures::future::pending().await
}
fn handle_tunnel(mut tunnel: impl EndpointInfo + Tunnel, sess: ngrok::Session) {
info!("bound new tunnel: {}", tunnel.url());
tokio::spawn(async move {
loop {
let stream = if let Some(stream) = tunnel.try_next().await? {
stream
} else {
info!("tunnel closed!");
break;
};
let sess = sess.clone();
let id: String = tunnel.id().into();
tokio::spawn(async move {
info!("accepted connection: {:?}", stream.remote_addr());
let (rx, mut tx) = io::split(stream);
let mut lines = BufReader::new(rx);
loop {
let mut buf = String::new();
let len = lines.read_line(&mut buf).await?;
if len == 0 {
break;
}
if buf.contains("bye!") {
info!("unbind requested");
tx.write_all("later!".as_bytes()).await?;
sess.close_tunnel(id).await?;
return Ok(());
} else if buf.contains("another!") {
info!("another requested");
let new_tunnel = sess.tcp_endpoint().listen().await?;
tx.write_all(new_tunnel.url().as_bytes()).await?;
handle_tunnel(new_tunnel, sess.clone());
} else {
info!("read line: {}", buf);
tx.write_all(buf.as_bytes()).await?;
info!("echoed line");
}
tx.flush().await?;
info!("flushed");
}
Result::<(), anyhow::Error>::Ok(())
});
}
anyhow::Result::<()>::Ok(())
});
}
================================================
FILE: ngrok/examples/domain.crt
================================================
-----BEGIN CERTIFICATE-----
MIIC+jCCAeICCQDobWtly6PonjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJV
UzERMA8GA1UECgwITm90IFJlYWwxHTAbBgNVBAMMFHJ1c3Qtc2RrLmV4YW1wbGUu
Y29tMB4XDTIyMTIwMjE4MzMxM1oXDTMyMTEyOTE4MzMxM1owPzELMAkGA1UEBhMC
VVMxETAPBgNVBAoMCE5vdCBSZWFsMR0wGwYDVQQDDBRydXN0LXNkay5leGFtcGxl
LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKhsx8tZWzaqaz9i
gnyU9O/dCEX8qgCvU2yoeJBfGhwCnlFNQBUdBGlV+Cjf19ozagYlY6Cunu214AUR
CDHTZsgTmMhtHkJ3kWD0wgDu+uyUuW6akP1+o39lebDc6CbDV7j1ySBoPMROp5dB
pX+ltpH42CmJM6ciwfTD1uuW5LXJvb9d4HISZp2RWyHqb3a6pI7E+XLqXg/Yy9MY
eqQESZMrYCjC+Sn4blGhcQhjTVU2rM5ChoDtZuL8OJQ0UYmchlch8CNc5Lvj9hAT
BiafEAscGrdIAZkK50kjpcIOWPPSfjCRqz8elSQqoKFq/uQnHBF5NwmsEqE0sXhw
4UdngRMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKieeE6gzuxHGjVT2NKL5BFjL
XKxdQhI/Tt7ClKu39Ay62fXDRznTBpGRfyWsJ5r3wmsHFogw46a2HYZHyuTMfyPY
lKhE/9EPMf/faqhIa33nMBASNzuGB5yfcPaod4KJX6DBKZtIpgkm2+S6BivpuSEo
DJ0lNtlR80mcVPma9KR57A0oh/UIsHXxL0qIKdaxyZYOZ1Zhtm+hzZcZA4wHkqzN
olNk3SOfhC5vVFudg+5KtxPBZ/efS9sqDUstH8hmE1JnxCF9OBlHdKI4yUMnsEf7
aOy11K5g7Oc3m7EB1twEQkufBAJeYzMOCji17GyJHDojNuOLkrmoLgcgDym5LQ==
-----END CERTIFICATE-----
================================================
FILE: ngrok/examples/domain.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqGzHy1lbNqprP2KCfJT0790IRfyqAK9TbKh4kF8aHAKeUU1A
FR0EaVX4KN/X2jNqBiVjoK6e7bXgBREIMdNmyBOYyG0eQneRYPTCAO767JS5bpqQ
/X6jf2V5sNzoJsNXuPXJIGg8xE6nl0Glf6W2kfjYKYkzpyLB9MPW65bktcm9v13g
chJmnZFbIepvdrqkjsT5cupeD9jL0xh6pARJkytgKML5KfhuUaFxCGNNVTaszkKG
gO1m4vw4lDRRiZyGVyHwI1zku+P2EBMGJp8QCxwat0gBmQrnSSOlwg5Y89J+MJGr
Px6VJCqgoWr+5CccEXk3CawSoTSxeHDhR2eBEwIDAQABAoIBADKQLc8brWmU8gue
bGQwZ/RW3DP+rZ71A8ucLE3Tb0g3dQYddf6groFdINpMkUXdp5few7Eqm2Xr8ywy
N86Vk8a/M2AAelQkB04fTNrw4/4AjEbrOloQGc+WTFlPiJaSkJRjnZUQFiYtIt0j
BSd0PYJHPcYCfbJQmf/8h1pE+7ajNJlvEWrJ8UjDCjUuPPxq1aCOIA48aN8awDaO
2R0AeSBws6+6UgyBgy2juat0t8PvS+AiLv4rK3RGMD+x96KoPEoVVgOQLr5YTqRP
Q+HYrs5cSXx9Jb2cmuJzvPUJmE3HKhoshWrK7fz5Z8wVAqTGhX6dbuHoqMJnAdla
FFSBEokCgYEA0BAsrDrnSkls1uC54iqzrxPMvITj4UnBR+PK504NrtP2brlcVIDP
e0dTKPTqjIC0vpDIg2fhPPvKkeoyuL6huiUWL/DdYVphUlwTf2Mu6PUm3o4M1MWN
S7q09cqUp4HWCUbzN3MIJ8sOPY17Lq+fxi1Wf4mNh+8IIXcJQ4HgUiUCgYEAzzqx
L7ck6pBUTtpUFYFTCUQDOYdzPE72zOzHK/LpoWJEssQ479srKlmSnRPRZbPZGMGE
EXvhWROonux96rRrZjiBI4B5G4rzeY0Rs24kClEh+7s5Zw4xmfSJu5oSdLqiy+O+
IKMVhOm9qq+8+y9LwKyajwR27srLdHSijJoXNNcCgYEAgtc5EJH2MwwbisFFg8mw
t0+vN3omR91203uXdH/sMN4Qoa6lNmrOj0raK+5gtTyW7SPlRGWGCjCZQctSXEVd
NM7vtfQ1c2w/uWg3xqsbq9nGuLwBq6gT4+SkudDMTM5kR+87Mcp//W4/JUwcg85j
nl+Sfp+Exk/1//14cOByrZUCgYEAjrr7HUVEfPbJysHf1iwL2D7rBa3AdhJhNIYF
LMUTm59Gd+Zk3PeUxIeLTvs+Z5E2/zESWMR9UtASfNugYo6/xlk2wRAU2h6bUeYT
AgXjduOox2yNvehty389emRFP/boeAw1gN8yzCf+BdkjDdLmlx+LGORXUmOFPIG1
D6h2QWMCgYA0WysR3XMcRH/8GDAgNVry5JvKoxlVXTPqVScTjMRj3VAzPYPCV+ql
lNN6yh/TuJwdvNs+uhKd1Wu4cDIb9GqxkBbUTKoKBrVL1YB93IC7QIR5wVjhJF/i
lrFW1ogr3535UzHzyDD1oXvcnWV/JnTdadHf2oA3Em8n2oTQvXQAog==
-----END RSA PRIVATE KEY-----
================================================
FILE: ngrok/examples/labeled.rs
================================================
use std::{
convert::Infallible,
error::Error,
net::SocketAddr,
};
use axum::{
extract::ConnectInfo,
routing::get,
BoxError,
Router,
};
use futures::TryStreamExt;
use hyper::{
body::Incoming,
Request,
};
use hyper_util::{
rt::TokioExecutor,
server,
};
use ngrok::prelude::*;
use tower::{
util::ServiceExt,
Service,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// build our application with a single route
let app = Router::new().route(
"/",
get(
|ConnectInfo(remote_addr): ConnectInfo<SocketAddr>| async move {
format!("Hello, {remote_addr:?}!\r\n")
},
),
);
let sess = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let mut listener = sess
.labeled_tunnel()
// .app_protocol("http2")
// .verify_upstream_tls(false)
.label("edge", "edghts_<edge_id>")
.metadata("example tunnel metadata from rust")
.listen()
.await?;
println!("Labeled listener started!");
let mut make_service = app.into_make_service_with_connect_info::<SocketAddr>();
let server = async move {
while let Some(conn) = listener.try_next().await? {
let remote_addr = conn.remote_addr();
let tower_service = unwrap_infallible(make_service.call(remote_addr).await);
tokio::spawn(async move {
let hyper_service =
hyper::service::service_fn(move |request: Request<Incoming>| {
tower_service.clone().oneshot(request)
});
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(conn, hyper_service)
.await
{
eprintln!("failed to serve connection: {err:#}");
}
});
}
Ok::<(), BoxError>(())
};
server.await?;
Ok(())
}
fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
match result {
Ok(value) => value,
Err(err) => match err {},
}
}
================================================
FILE: ngrok/examples/mingrok.rs
================================================
use std::sync::{
Arc,
Mutex,
};
use anyhow::Error;
use futures::{
prelude::*,
select,
};
use ngrok::prelude::*;
use tokio::sync::oneshot;
use tracing::info;
use url::Url;
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.pretty()
.with_env_filter(std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()))
.init();
let forwards_to = std::env::args()
.nth(1)
.ok_or_else(|| anyhow::anyhow!("missing forwarding address"))
.and_then(|s| Ok(Url::parse(&s)?))?;
loop {
let (stop_tx, stop_rx) = oneshot::channel();
let stop_tx = Arc::new(Mutex::new(Some(stop_tx)));
let (restart_tx, restart_rx) = oneshot::channel();
let restart_tx = Arc::new(Mutex::new(Some(restart_tx)));
let mut fwd = ngrok::Session::builder()
.authtoken_from_env()
.handle_stop_command(move |req| {
let stop_tx = stop_tx.clone();
async move {
info!(?req, "received stop command");
let _ = stop_tx.lock().unwrap().take().unwrap().send(());
Ok(())
}
})
.handle_restart_command(move |req| {
let restart_tx = restart_tx.clone();
async move {
info!(?req, "received restart command");
let _ = restart_tx.lock().unwrap().take().unwrap().send(());
Ok(())
}
})
.handle_update_command(|req| async move {
info!(?req, "received update command");
Err("unable to update".into())
})
.connect()
.await?
.http_endpoint()
.listen_and_forward(forwards_to.clone())
.await?;
info!(url = fwd.url(), %forwards_to, "started forwarder");
let mut fut = fwd.join().fuse();
let mut stop_rx = stop_rx.fuse();
let mut restart_rx = restart_rx.fuse();
select! {
res = fut => info!("{:?}", res?),
_ = stop_rx => return Ok(()),
_ = restart_rx => {
drop(fut);
let _ = fwd.close().await;
continue
},
}
}
}
================================================
FILE: ngrok/examples/tls.rs
================================================
use std::{
convert::Infallible,
error::Error,
net::SocketAddr,
};
use axum::{
extract::ConnectInfo,
routing::get,
BoxError,
Router,
};
use futures::TryStreamExt;
use hyper::{
body::Incoming,
Request,
};
use hyper_util::{
rt::TokioExecutor,
server,
};
use ngrok::prelude::*;
use tower::{
util::ServiceExt,
Service,
};
const CERT: &[u8] = include_bytes!("domain.crt");
const KEY: &[u8] = include_bytes!("domain.key");
// const CA_CERT: &[u8] = include_bytes!("ca.crt");
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
// build our application with a single route
let app = Router::new().route(
"/",
get(
|ConnectInfo(remote_addr): ConnectInfo<SocketAddr>| async move {
format!("Hello, {remote_addr:?}!\r\n")
},
),
);
let sess = ngrok::Session::builder()
.authtoken_from_env()
.connect()
.await?;
let mut listener = sess
.tls_endpoint()
// .allow_cidr("0.0.0.0/0")
// .deny_cidr("10.1.1.1/32")
// .verify_upstream_tls(false)
// .domain("<somedomain>.ngrok.io")
// .forwards_to("example rust"),
// .mutual_tlsca(CA_CERT.into())
// .proxy_proto(ProxyProto::None)
.termination(CERT.into(), KEY.into())
.metadata("example tunnel metadata from rust")
.listen()
.await?;
let mut make_service = app.into_make_service_with_connect_info::<SocketAddr>();
let server = async move {
while let Some(conn) = listener.try_next().await? {
let remote_addr = conn.remote_addr();
let tower_service = unwrap_infallible(make_service.call(remote_addr).await);
tokio::spawn(async move {
let hyper_service =
hyper::service::service_fn(move |request: Request<Incoming>| {
tower_service.clone().oneshot(request)
});
if let Err(err) = server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection_with_upgrades(conn, hyper_service)
.await
{
eprintln!("failed to serve connection: {err:#}");
}
});
}
Ok::<(), BoxError>(())
};
server.await?;
Ok(())
}
fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
match result {
Ok(value) => value,
Err(err) => match err {},
}
}
================================================
FILE: ngrok/src/config/common.rs
================================================
use std::{
collections::HashMap,
env,
process,
};
use async_trait::async_trait;
use once_cell::sync::OnceCell;
use url::Url;
pub use crate::internals::proto::ProxyProto;
use crate::{
config::policies::Policy,
forwarder::Forwarder,
internals::proto::{
BindExtra,
BindOpts,
IpRestriction,
MutualTls,
},
session::RpcError,
Session,
Tunnel,
};
/// Represents the ingress configuration for an ngrok endpoint.
///
/// Bindings determine where and how your endpoint is exposed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Binding {
/// Publicly accessible endpoint (default for most configurations).
Public,
/// Internal-only endpoint, not accessible from the public internet.
Internal,
/// Kubernetes cluster binding for service mesh integration.
Kubernetes,
}
impl Binding {
/// Returns the string representation of this binding.
pub fn as_str(&self) -> &'static str {
match self {
Binding::Public => "public",
Binding::Internal => "internal",
Binding::Kubernetes => "kubernetes",
}
}
/// Validates if a string is a recognized binding value.
pub(crate) fn validate(s: &str) -> Result<(), String> {
match s.to_lowercase().as_str() {
"public" | "internal" | "kubernetes" => Ok(()),
_ => Err(format!(
"Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
s
)),
}
}
}
impl From<Binding> for String {
fn from(binding: Binding) -> String {
binding.as_str().to_string()
}
}
impl std::str::FromStr for Binding {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"public" => Ok(Binding::Public),
"internal" => Ok(Binding::Internal),
"kubernetes" => Ok(Binding::Kubernetes),
_ => Err(format!(
"Invalid binding value '{}'. Expected 'public', 'internal', or 'kubernetes'",
s
)),
}
}
}
impl std::fmt::Display for Binding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub(crate) fn default_forwards_to() -> &'static str {
static FORWARDS_TO: OnceCell<String> = OnceCell::new();
FORWARDS_TO
.get_or_init(|| {
let hostname = hostname::get()
.unwrap_or("<unknown>".into())
.to_string_lossy()
.into_owned();
let exe = env::current_exe()
.unwrap_or("<unknown>".into())
.to_string_lossy()
.into_owned();
let pid = process::id();
format!("app://{hostname}/{exe}?pid={pid}")
})
.as_str()
}
/// Trait representing things that can be built into an ngrok tunnel.
#[async_trait]
pub trait TunnelBuilder: From<Session> {
/// The ngrok tunnel type that this builder produces.
type Tunnel: Tunnel;
/// Begin listening for new connections on this tunnel.
async fn listen(&self) -> Result<Self::Tunnel, RpcError>;
}
/// Trait representing things that can be built into an ngrok tunnel and then
/// forwarded to a provided URL.
#[async_trait]
pub trait ForwarderBuilder: TunnelBuilder {
/// Start listening for new connections on this tunnel and forward all
/// connections to the provided URL.
///
/// This will also set the `forwards_to` metadata for the tunnel.
async fn listen_and_forward(&self, to_url: Url) -> Result<Forwarder<Self::Tunnel>, RpcError>;
}
macro_rules! impl_builder {
($(#[$m:meta])* $name:ident, $opts:ty, $tun:ident, $edgepoint:tt) => {
$(#[$m])*
#[derive(Clone)]
pub struct $name {
options: $opts,
// Note: This is only optional for testing purposes.
session: Option<Session>,
}
mod __builder_impl {
use $crate::forwarder::Forwarder;
use $crate::config::common::ForwarderBuilder;
use $crate::config::common::TunnelBuilder;
use $crate::session::RpcError;
use async_trait::async_trait;
use url::Url;
use super::*;
impl From<Session> for $name {
fn from(session: Session) -> Self {
$name {
options: Default::default(),
session: session.into(),
}
}
}
#[async_trait]
impl TunnelBuilder for $name {
type Tunnel = $tun;
async fn listen(&self) -> Result<$tun, RpcError> {
Ok($tun {
inner: self
.session
.as_ref()
.unwrap()
.start_tunnel(&self.options)
.await?,
})
}
}
#[async_trait]
impl ForwarderBuilder for $name {
async fn listen_and_forward(&self, to_url: Url) -> Result<Forwarder<$tun>, RpcError> {
let mut cfg = self.clone();
cfg.for_forwarding_to(&to_url).await;
let tunnel = cfg.listen().await?;
let info = tunnel.make_info();
$crate::forwarder::forward(tunnel, info, to_url)
}
}
}
};
}
/// Tunnel configuration trait, implemented by our top-level config objects.
pub(crate) trait TunnelConfig {
/// The "forwards to" metadata.
///
/// Only for display/informational purposes.
fn forwards_to(&self) -> String;
/// The L7 protocol the upstream service expects
fn forwards_proto(&self) -> String;
/// Whether to disable certificate verification for this tunnel.
fn verify_upstream_tls(&self) -> bool;
/// Internal-only, extra data sent when binding a tunnel.
fn extra(&self) -> BindExtra;
/// The protocol for this tunnel.
fn proto(&self) -> String;
/// The middleware and other configuration options for this tunnel.
fn opts(&self) -> Option<BindOpts>;
/// The labels for this tunnel.
fn labels(&self) -> HashMap<String, String>;
}
// delegate references
impl<T> TunnelConfig for &T
where
T: TunnelConfig,
{
fn forwards_to(&self) -> String {
(**self).forwards_to()
}
fn forwards_proto(&self) -> String {
(**self).forwards_proto()
}
fn verify_upstream_tls(&self) -> bool {
(**self).verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
(**self).extra()
}
fn proto(&self) -> String {
(**self).proto()
}
fn opts(&self) -> Option<BindOpts> {
(**self).opts()
}
fn labels(&self) -> HashMap<String, String> {
(**self).labels()
}
}
/// Restrictions placed on the origin of incoming connections to the edge.
#[derive(Clone, Default)]
pub(crate) struct CidrRestrictions {
/// Rejects connections that do not match the given CIDRs
pub(crate) allowed: Vec<String>,
/// Rejects connections that match the given CIDRs and allows all other CIDRs.
pub(crate) denied: Vec<String>,
}
impl CidrRestrictions {
pub(crate) fn allow(&mut self, cidr: impl Into<String>) {
self.allowed.push(cidr.into());
}
pub(crate) fn deny(&mut self, cidr: impl Into<String>) {
self.denied.push(cidr.into());
}
}
// Common
#[derive(Default, Clone)]
pub(crate) struct CommonOpts {
// Restrictions placed on the origin of incoming connections to the edge.
pub(crate) cidr_restrictions: CidrRestrictions,
// The version of PROXY protocol to use with this tunnel, zero if not
// using.
pub(crate) proxy_proto: ProxyProto,
// Tunnel-specific opaque metadata. Viewable via the API.
pub(crate) metadata: Option<String>,
// Tunnel backend metadata. Viewable via the dashboard and API, but has no
// bearing on tunnel behavior.
pub(crate) forwards_to: Option<String>,
// Tunnel L7 app protocol
pub(crate) forwards_proto: Option<String>,
// Whether to disable certificate verification for this tunnel.
verify_upstream_tls: Option<bool>,
// DEPRECATED: use traffic_policy instead.
pub(crate) policy: Option<Policy>,
// Policy that defines rules that should be applied to incoming or outgoing
// connections to the edge.
pub(crate) traffic_policy: Option<String>,
// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub(crate) pooling_enabled: Option<bool>,
}
impl CommonOpts {
// Get the proto version of cidr restrictions
pub(crate) fn ip_restriction(&self) -> Option<IpRestriction> {
(!self.cidr_restrictions.allowed.is_empty() || !self.cidr_restrictions.denied.is_empty())
.then_some(self.cidr_restrictions.clone().into())
}
pub(crate) fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.forwards_to = Some(to_url.as_str().into());
self
}
pub(crate) fn set_verify_upstream_tls(&mut self, verify_upstream_tls: bool) {
self.verify_upstream_tls = Some(verify_upstream_tls)
}
pub(crate) fn verify_upstream_tls(&self) -> bool {
self.verify_upstream_tls.unwrap_or(true)
}
}
// transform into the wire protocol format
impl From<CidrRestrictions> for IpRestriction {
fn from(cr: CidrRestrictions) -> Self {
IpRestriction {
allow_cidrs: cr.allowed,
deny_cidrs: cr.denied,
}
}
}
impl From<&[bytes::Bytes]> for MutualTls {
fn from(b: &[bytes::Bytes]) -> Self {
let mut aggregated = Vec::new();
b.iter().for_each(|c| aggregated.extend(c));
MutualTls {
mutual_tls_ca: aggregated,
}
}
}
================================================
FILE: ngrok/src/config/headers.rs
================================================
use std::collections::HashMap;
use crate::internals::proto::Headers as HeaderProto;
/// HTTP Headers to modify at the ngrok edge.
#[derive(Clone, Default)]
pub(crate) struct Headers {
/// Headers to add to requests or responses at the ngrok edge.
added: HashMap<String, String>,
/// Header names to remove from requests or responses at the ngrok edge.
removed: Vec<String>,
}
impl Headers {
pub(crate) fn add(&mut self, name: impl Into<String>, value: impl Into<String>) {
self.added.insert(name.into().to_lowercase(), value.into());
}
pub(crate) fn remove(&mut self, name: impl Into<String>) {
self.removed.push(name.into().to_lowercase());
}
pub(crate) fn has_entries(&self) -> bool {
!self.added.is_empty() || !self.removed.is_empty()
}
}
// transform into the wire protocol format
impl From<Headers> for HeaderProto {
fn from(headers: Headers) -> Self {
HeaderProto {
add: headers
.added
.iter()
.map(|a| format!("{}:{}", a.0, a.1))
.collect(),
remove: headers.removed,
add_parsed: HashMap::new(), // unused in this context
}
}
}
================================================
FILE: ngrok/src/config/http.rs
================================================
use std::{
borrow::Borrow,
collections::HashMap,
convert::From,
str::FromStr,
};
use bytes::Bytes;
use thiserror::Error;
use url::Url;
use super::{
common::ProxyProto,
Policy,
};
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::{
common::{
default_forwards_to,
Binding,
CommonOpts,
TunnelConfig,
},
headers::Headers,
oauth::OauthOptions,
oidc::OidcOptions,
webhook_verification::WebhookVerification,
},
internals::proto::{
BasicAuth,
BasicAuthCredential,
BindExtra,
BindOpts,
CircuitBreaker,
Compression,
HttpEndpoint,
UserAgentFilter,
WebsocketTcpConverter,
},
tunnel::HttpTunnel,
Session,
};
/// Error representing invalid string for Scheme
#[derive(Debug, Clone, Error)]
#[error("invalid scheme string: {}", .0)]
pub struct InvalidSchemeString(String);
/// The URL scheme for this HTTP endpoint.
///
/// [Scheme::HTTPS] will enable TLS termination at the ngrok edge.
#[derive(Clone, Default, Eq, PartialEq)]
pub enum Scheme {
/// The `http` URL scheme.
HTTP,
/// The `https` URL scheme.
#[default]
HTTPS,
}
impl FromStr for Scheme {
type Err = InvalidSchemeString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use Scheme::*;
Ok(match s.to_uppercase().as_str() {
"HTTP" => HTTP,
"HTTPS" => HTTPS,
_ => return Err(InvalidSchemeString(s.into())),
})
}
}
/// Restrictions placed on the origin of incoming connections to the edge.
#[derive(Clone, Default)]
pub(crate) struct UaFilter {
/// Rejects connections that do not match the given regular expression
pub(crate) allow: Vec<String>,
/// Rejects connections that match the given regular expression and allows
/// all other regular expressions.
pub(crate) deny: Vec<String>,
}
impl UaFilter {
pub(crate) fn allow(&mut self, allow: impl Into<String>) {
self.allow.push(allow.into());
}
pub(crate) fn deny(&mut self, deny: impl Into<String>) {
self.deny.push(deny.into());
}
}
impl From<UaFilter> for UserAgentFilter {
fn from(ua: UaFilter) -> Self {
UserAgentFilter {
allow: ua.allow,
deny: ua.deny,
}
}
}
/// The options for a HTTP edge.
#[derive(Default, Clone)]
struct HttpOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) scheme: Scheme,
pub(crate) domain: Option<String>,
pub(crate) mutual_tlsca: Vec<bytes::Bytes>,
pub(crate) compression: bool,
pub(crate) websocket_tcp_conversion: bool,
pub(crate) circuit_breaker: f64,
pub(crate) request_headers: Headers,
pub(crate) response_headers: Headers,
pub(crate) rewrite_host: bool,
pub(crate) basic_auth: Vec<(String, String)>,
pub(crate) oauth: Option<OauthOptions>,
pub(crate) oidc: Option<OidcOptions>,
pub(crate) webhook_verification: Option<WebhookVerification>,
// Flitering placed on the origin of incoming connections to the edge.
pub(crate) user_agent_filter: UaFilter,
pub(crate) bindings: Vec<String>,
}
impl HttpOptions {
fn user_agent_filter(&self) -> Option<UserAgentFilter> {
(!self.user_agent_filter.allow.is_empty() || !self.user_agent_filter.deny.is_empty())
.then_some(self.user_agent_filter.clone().into())
}
}
impl TunnelConfig for HttpOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn forwards_proto(&self) -> String {
self.common_opts.forwards_proto.clone().unwrap_or_default()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: self.bindings.clone(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
if self.scheme == Scheme::HTTP {
return "http".into();
}
"https".into()
}
fn opts(&self) -> Option<BindOpts> {
let http_endpoint = HttpEndpoint {
proxy_proto: self.common_opts.proxy_proto,
domain: self.domain.clone().unwrap_or_default(),
hostname: String::new(),
compression: self.compression.then_some(Compression {}),
circuit_breaker: (self.circuit_breaker != 0f64).then_some(CircuitBreaker {
error_threshold: self.circuit_breaker,
}),
ip_restriction: self.common_opts.ip_restriction(),
basic_auth: (!self.basic_auth.is_empty()).then_some(self.basic_auth.as_slice().into()),
oauth: self.oauth.clone().map(From::from),
oidc: self.oidc.clone().map(From::from),
webhook_verification: self.webhook_verification.clone().map(From::from),
mutual_tls_ca: (!self.mutual_tlsca.is_empty())
.then_some(self.mutual_tlsca.as_slice().into()),
request_headers: self
.request_headers
.has_entries()
.then_some(self.request_headers.clone().into()),
response_headers: self
.response_headers
.has_entries()
.then_some(self.response_headers.clone().into()),
websocket_tcp_converter: self
.websocket_tcp_conversion
.then_some(WebsocketTcpConverter {}),
user_agent_filter: self.user_agent_filter(),
traffic_policy: if self.common_opts.traffic_policy.is_some() {
self.common_opts.traffic_policy.clone().map(From::from)
} else if self.common_opts.policy.is_some() {
self.common_opts.policy.clone().map(From::from)
} else {
None
},
..Default::default()
};
Some(BindOpts::Http(http_endpoint))
}
fn labels(&self) -> HashMap<String, String> {
HashMap::new()
}
}
// transform into the wire protocol format
impl From<&[(String, String)]> for BasicAuth {
fn from(v: &[(String, String)]) -> Self {
BasicAuth {
credentials: v.iter().cloned().map(From::from).collect(),
}
}
}
// transform into the wire protocol format
impl From<(String, String)> for BasicAuthCredential {
fn from(b: (String, String)) -> Self {
BasicAuthCredential {
username: b.0,
cleartext_password: b.1,
hashed_password: vec![], // unused in this context
}
}
}
impl_builder! {
/// A builder for a tunnel backing an HTTP endpoint.
///
/// https://ngrok.com/docs/http/
HttpTunnelBuilder, HttpOptions, HttpTunnel, endpoint
}
impl HttpTunnelBuilder {
/// Add the provided CIDR to the allowlist.
///
/// https://ngrok.com/docs/http/ip-restrictions/
pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
self.options.common_opts.cidr_restrictions.allow(cidr);
self
}
/// Add the provided CIDR to the denylist.
///
/// https://ngrok.com/docs/http/ip-restrictions/
pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
self.options.common_opts.cidr_restrictions.deny(cidr);
self
}
/// Sets the PROXY protocol version for connections over this tunnel.
pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
self.options.common_opts.proxy_proto = proxy_proto;
self
}
/// Sets the opaque metadata string for this tunnel.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Sets the ingress configuration for this endpoint.
///
/// Valid binding values are:
/// - `"public"` - Publicly accessible endpoint
/// - `"internal"` - Internal-only endpoint
/// - `"kubernetes"` - Kubernetes cluster binding
///
/// If not specified, the ngrok service will use its default binding configuration.
///
/// # Panics
///
/// Panics if called more than once or if an invalid binding value is provided.
///
/// # Examples
///
/// ```no_run
/// # use ngrok::Session;
/// # use ngrok::config::TunnelBuilder;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let session = Session::builder().authtoken_from_env().connect().await?;
///
/// // Using string
/// let tunnel = session.http_endpoint().binding("internal").listen().await?;
///
/// // Using typed enum
/// use ngrok::config::Binding;
/// let tunnel = session.http_endpoint().binding(Binding::Public).listen().await?;
/// # Ok(())
/// # }
/// ```
pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
if !self.options.bindings.is_empty() {
panic!("binding() can only be called once");
}
let binding_str = binding.into();
if let Err(e) = Binding::validate(&binding_str) {
panic!("{}", e);
}
self.options.bindings.push(binding_str);
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Self {
self.options.common_opts.forwards_to = Some(forwards_to.into());
self
}
/// Sets the L7 protocol for this tunnel.
pub fn app_protocol(&mut self, app_protocol: impl Into<String>) -> &mut Self {
self.options.common_opts.forwards_proto = Some(app_protocol.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
/// Sets the scheme for this edge.
pub fn scheme(&mut self, scheme: Scheme) -> &mut Self {
self.options.scheme = scheme;
self
}
/// Sets the domain to request for this edge.
///
/// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#domains
pub fn domain(&mut self, domain: impl Into<String>) -> &mut Self {
self.options.domain = Some(domain.into());
self
}
/// Adds a certificate in PEM format to use for mutual TLS authentication.
///
/// These will be used to authenticate client certificates for requests at
/// the ngrok edge.
///
/// https://ngrok.com/docs/http/mutual-tls/
pub fn mutual_tlsca(&mut self, mutual_tlsca: Bytes) -> &mut Self {
self.options.mutual_tlsca.push(mutual_tlsca);
self
}
/// Enables gzip compression.
///
/// https://ngrok.com/docs/http/compression/
pub fn compression(&mut self) -> &mut Self {
self.options.compression = true;
self
}
/// Enables the websocket-to-tcp converter.
///
/// https://ngrok.com/docs/http/websocket-tcp-converter/
pub fn websocket_tcp_conversion(&mut self) -> &mut Self {
self.options.websocket_tcp_conversion = true;
self
}
/// Sets the 5XX response ratio at which the ngrok edge will stop sending
/// requests to this tunnel.
///
/// https://ngrok.com/docs/http/circuit-breaker/
pub fn circuit_breaker(&mut self, circuit_breaker: f64) -> &mut Self {
self.options.circuit_breaker = circuit_breaker;
self
}
/// Automatically rewrite the host header to the one in the provided URL
/// when calling [ForwarderBuilder::listen_and_forward]. Does nothing if
/// using [TunnelBuilder::listen]. Defaults to `false`.
///
/// If you need to set the host header to a specific value, use
/// `cfg.request_header("host", "some.host.com")` instead.
pub fn host_header_rewrite(&mut self, rewrite: bool) -> &mut Self {
self.options.rewrite_host = rewrite;
self
}
/// Adds a header to all requests to this edge.
///
/// https://ngrok.com/docs/http/request-headers/
pub fn request_header(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.options.request_headers.add(name, value);
self
}
/// Adds a header to all responses coming from this edge.
///
/// https://ngrok.com/docs/http/response-headers/
pub fn response_header(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.options.response_headers.add(name, value);
self
}
/// Removes a header from requests to this edge.
///
/// https://ngrok.com/docs/http/request-headers/
pub fn remove_request_header(&mut self, name: impl Into<String>) -> &mut Self {
self.options.request_headers.remove(name);
self
}
/// Removes a header from responses from this edge.
///
/// https://ngrok.com/docs/http/response-headers/
pub fn remove_response_header(&mut self, name: impl Into<String>) -> &mut Self {
self.options.response_headers.remove(name);
self
}
/// Adds the provided credentials to the list of basic authentication
/// credentials.
///
/// https://ngrok.com/docs/http/basic-auth/
pub fn basic_auth(
&mut self,
username: impl Into<String>,
password: impl Into<String>,
) -> &mut Self {
self.options
.basic_auth
.push((username.into(), password.into()));
self
}
/// Set the OAuth configuraton for this edge.
///
/// https://ngrok.com/docs/http/oauth/
pub fn oauth(&mut self, oauth: impl Borrow<OauthOptions>) -> &mut Self {
self.options.oauth = Some(oauth.borrow().to_owned());
self
}
/// Set the OIDC configuration for this edge.
///
/// https://ngrok.com/docs/http/openid-connect/
pub fn oidc(&mut self, oidc: impl Borrow<OidcOptions>) -> &mut Self {
self.options.oidc = Some(oidc.borrow().to_owned());
self
}
/// Configures webhook verification for this edge.
///
/// https://ngrok.com/docs/http/webhook-verification/
pub fn webhook_verification(
&mut self,
provider: impl Into<String>,
secret: impl Into<String>,
) -> &mut Self {
self.options.webhook_verification = Some(WebhookVerification {
provider: provider.into(),
secret: secret.into().into(),
});
self
}
/// Add the provided regex to the allowlist.
///
/// https://ngrok.com/docs/http/user-agent-filter/
pub fn allow_user_agent(&mut self, regex: impl Into<String>) -> &mut Self {
self.options.user_agent_filter.allow(regex);
self
}
/// Add the provided regex to the denylist.
///
/// https://ngrok.com/docs/http/user-agent-filter/
pub fn deny_user_agent(&mut self, regex: impl Into<String>) -> &mut Self {
self.options.user_agent_filter.deny(regex);
self
}
/// DEPRECATED: use traffic_policy instead.
pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
where
S: TryInto<Policy>,
{
self.options.common_opts.policy = Some(s.try_into()?);
Ok(self)
}
/// Set policy for this edge.
pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut Self {
self.options.common_opts.traffic_policy = Some(policy_str.into());
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
if let Some(host) = to_url.host_str().filter(|_| self.options.rewrite_host) {
self.request_header("host", host);
}
self
}
/// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &mut Self {
self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::policies::test::POLICY_JSON;
const METADATA: &str = "testmeta";
const TEST_FORWARD: &str = "testforward";
const TEST_FORWARD_PROTO: &str = "http2";
const ALLOW_CIDR: &str = "0.0.0.0/0";
const DENY_CIDR: &str = "10.1.1.1/32";
const CA_CERT: &[u8] = "test ca cert".as_bytes();
const CA_CERT2: &[u8] = "test ca cert2".as_bytes();
const DOMAIN: &str = "test domain";
const ALLOW_AGENT: &str = r"bar/(\d)+";
const DENY_AGENT: &str = r"foo/(\d)+";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&HttpTunnelBuilder {
session: None,
options: Default::default(),
}
.allow_user_agent(ALLOW_AGENT)
.deny_user_agent(DENY_AGENT)
.allow_cidr(ALLOW_CIDR)
.deny_cidr(DENY_CIDR)
.proxy_proto(ProxyProto::V2)
.metadata(METADATA)
.scheme(Scheme::from_str("hTtPs").unwrap())
.domain(DOMAIN)
.mutual_tlsca(CA_CERT.into())
.mutual_tlsca(CA_CERT2.into())
.compression()
.websocket_tcp_conversion()
.circuit_breaker(0.5)
.request_header("X-Req-Yup", "true")
.response_header("X-Res-Yup", "true")
.remove_request_header("X-Req-Nope")
.remove_response_header("X-Res-Nope")
.oauth(OauthOptions::new("google"))
.oauth(
OauthOptions::new("google")
.allow_email("<user>@<domain>")
.allow_domain("<domain>")
.scope("<scope>"),
)
.oidc(OidcOptions::new("<url>", "<id>", "<secret>"))
.oidc(
OidcOptions::new("<url>", "<id>", "<secret>")
.allow_email("<user>@<domain>")
.allow_domain("<domain>")
.scope("<scope>"),
)
.webhook_verification("twilio", "asdf")
.basic_auth("ngrok", "online1line")
.forwards_to(TEST_FORWARD)
.app_protocol("http2")
.policy(POLICY_JSON)
.unwrap()
.options,
);
}
fn tunnel_test<C>(tunnel_cfg: C)
where
C: TunnelConfig,
{
assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
assert_eq!(TEST_FORWARD_PROTO, tunnel_cfg.forwards_proto());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(Vec::<String>::new(), extra.bindings);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("https", tunnel_cfg.proto());
let opts = tunnel_cfg.opts().unwrap();
assert!(matches!(opts, BindOpts::Http { .. }));
if let BindOpts::Http(endpoint) = opts {
assert_eq!(DOMAIN, endpoint.domain);
assert_eq!(String::default(), endpoint.subdomain);
assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
let ip_restriction = endpoint.ip_restriction.unwrap();
assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
let mutual_tls = endpoint.mutual_tls_ca.unwrap();
let mut agg = CA_CERT.to_vec();
agg.extend(CA_CERT2.to_vec());
assert_eq!(agg, mutual_tls.mutual_tls_ca);
assert!(endpoint.compression.is_some());
assert!(endpoint.websocket_tcp_converter.is_some());
assert_eq!(0.5f64, endpoint.circuit_breaker.unwrap().error_threshold);
let request_headers = endpoint.request_headers.unwrap();
assert_eq!(["x-req-yup:true"].to_vec(), request_headers.add);
assert_eq!(["x-req-nope"].to_vec(), request_headers.remove);
let response_headers = endpoint.response_headers.unwrap();
assert_eq!(["x-res-yup:true"].to_vec(), response_headers.add);
assert_eq!(["x-res-nope"].to_vec(), response_headers.remove);
let webhook = endpoint.webhook_verification.unwrap();
assert_eq!("twilio", webhook.provider);
assert_eq!("asdf", *webhook.secret);
assert!(webhook.sealed_secret.is_empty());
let creds = endpoint.basic_auth.unwrap().credentials;
assert_eq!(1, creds.len());
assert_eq!("ngrok", creds[0].username);
assert_eq!("online1line", creds[0].cleartext_password);
assert!(creds[0].hashed_password.is_empty());
let oauth = endpoint.oauth.unwrap();
assert_eq!("google", oauth.provider);
assert_eq!(["<user>@<domain>"].to_vec(), oauth.allow_emails);
assert_eq!(["<domain>"].to_vec(), oauth.allow_domains);
assert_eq!(["<scope>"].to_vec(), oauth.scopes);
assert_eq!(String::default(), oauth.client_id);
assert_eq!(String::default(), *oauth.client_secret);
assert!(oauth.sealed_client_secret.is_empty());
let oidc = endpoint.oidc.unwrap();
assert_eq!("<url>", oidc.issuer_url);
assert_eq!(["<user>@<domain>"].to_vec(), oidc.allow_emails);
assert_eq!(["<domain>"].to_vec(), oidc.allow_domains);
assert_eq!(["<scope>"].to_vec(), oidc.scopes);
assert_eq!("<id>", oidc.client_id);
assert_eq!("<secret>", *oidc.client_secret);
assert!(oidc.sealed_client_secret.is_empty());
let user_agent_filter = endpoint.user_agent_filter.unwrap();
assert_eq!(Vec::from([ALLOW_AGENT]), user_agent_filter.allow);
assert_eq!(Vec::from([DENY_AGENT]), user_agent_filter.deny);
}
assert_eq!(HashMap::new(), tunnel_cfg.labels());
}
#[test]
fn test_binding_valid_values() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
// Test "public"
builder.binding("public");
assert_eq!(vec!["public"], builder.options.bindings);
// Test "internal"
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal");
assert_eq!(vec!["internal"], builder.options.bindings);
// Test "kubernetes"
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("kubernetes");
assert_eq!(vec!["kubernetes"], builder.options.bindings);
// Test with Binding enum
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding(Binding::Internal);
assert_eq!(vec!["internal"], builder.options.bindings);
}
#[test]
#[should_panic(expected = "Invalid binding value")]
fn test_binding_invalid_value() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("invalid");
}
#[test]
#[should_panic(expected = "binding() can only be called once")]
fn test_binding_called_twice() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("public");
builder.binding("internal");
}
#[test]
fn test_binding_with_domain() {
let mut builder = HttpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal").domain("foo.internal");
// Check that both binding and domain are set
assert_eq!(vec!["internal"], builder.options.bindings);
assert_eq!(Some("foo.internal".to_string()), builder.options.domain);
// Check that they're properly included in extra() and opts()
let extra = builder.options.extra();
assert_eq!(vec!["internal"], extra.bindings);
let opts = builder.options.opts().unwrap();
if let BindOpts::Http(endpoint) = opts {
assert_eq!("foo.internal", endpoint.domain);
} else {
panic!("Expected Http endpoint");
}
}
}
================================================
FILE: ngrok/src/config/labeled.rs
================================================
use std::collections::HashMap;
use url::Url;
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::common::{
default_forwards_to,
CommonOpts,
TunnelConfig,
},
internals::proto::{
BindExtra,
BindOpts,
},
tunnel::LabeledTunnel,
Session,
};
/// Options for labeled tunnels.
#[derive(Default, Clone)]
struct LabeledOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) labels: HashMap<String, String>,
}
impl TunnelConfig for LabeledOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn forwards_proto(&self) -> String {
self.common_opts.forwards_proto.clone().unwrap_or_default()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: Vec::new(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
"".into()
}
fn opts(&self) -> Option<BindOpts> {
None
}
fn labels(&self) -> HashMap<String, String> {
self.labels.clone()
}
}
impl_builder! {
/// A builder for a labeled tunnel.
LabeledTunnelBuilder, LabeledOptions, LabeledTunnel, edge
}
impl LabeledTunnelBuilder {
/// Sets the opaque metadata string for this tunnel.
/// Viewable via the API.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Add a label, value pair for this tunnel.
///
/// https://ngrok.com/docs/network-edge/edges/#tunnel-group
pub fn label(&mut self, label: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.options.labels.insert(label.into(), value.into());
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Self {
self.options.common_opts.forwards_to = forwards_to.into().into();
self
}
/// Sets the L7 protocol string for this tunnel.
pub fn app_protocol(&mut self, app_protocol: impl Into<String>) -> &mut Self {
self.options.common_opts.forwards_proto = Some(app_protocol.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
self
}
}
#[cfg(test)]
mod test {
use super::*;
const METADATA: &str = "testmeta";
const LABEL_KEY: &str = "edge";
const LABEL_VAL: &str = "edghts_2IC6RJ6CQnuh7waciWyaGKc50Nt";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&LabeledTunnelBuilder {
session: None,
options: Default::default(),
}
.metadata(METADATA)
.label(LABEL_KEY, LABEL_VAL)
.options,
);
}
fn tunnel_test<C>(tunnel_cfg: &C)
where
C: TunnelConfig,
{
assert_eq!(default_forwards_to(), tunnel_cfg.forwards_to());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("", tunnel_cfg.proto());
assert!(tunnel_cfg.opts().is_none());
let mut labels: HashMap<String, String> = HashMap::new();
labels.insert(LABEL_KEY.into(), LABEL_VAL.into());
assert_eq!(labels, tunnel_cfg.labels());
}
}
================================================
FILE: ngrok/src/config/oauth.rs
================================================
use crate::internals::proto::{
Oauth,
SecretString,
};
/// Oauth Options configuration
///
/// https://ngrok.com/docs/http/oauth/
#[derive(Clone, Default)]
pub struct OauthOptions {
/// The OAuth provider to use
provider: String,
/// The client ID, if a custom one is being used
client_id: String,
/// The client secret, if a custom one is being used
client_secret: SecretString,
/// Email addresses of users to authorize.
allow_emails: Vec<String>,
/// Email domains of users to authorize.
allow_domains: Vec<String>,
/// OAuth scopes to request from the provider.
scopes: Vec<String>,
}
impl OauthOptions {
/// Create a new [OauthOptions] for the given provider.
pub fn new(provider: impl Into<String>) -> Self {
OauthOptions {
provider: provider.into(),
..Default::default()
}
}
/// Provide an OAuth client ID for custom apps.
pub fn client_id(&mut self, id: impl Into<String>) -> &mut Self {
self.client_id = id.into();
self
}
/// Provide an OAuth client secret for custom apps.
pub fn client_secret(&mut self, secret: impl Into<String>) -> &mut Self {
self.client_secret = SecretString::from(secret.into());
self
}
/// Append an email address to the list of allowed emails.
pub fn allow_email(&mut self, email: impl Into<String>) -> &mut Self {
self.allow_emails.push(email.into());
self
}
/// Append an email domain to the list of allowed domains.
pub fn allow_domain(&mut self, domain: impl Into<String>) -> &mut Self {
self.allow_domains.push(domain.into());
self
}
/// Append a scope to the list of scopes to request.
pub fn scope(&mut self, scope: impl Into<String>) -> &mut Self {
self.scopes.push(scope.into());
self
}
}
// transform into the wire protocol format
impl From<OauthOptions> for Oauth {
fn from(o: OauthOptions) -> Self {
Oauth {
provider: o.provider,
client_id: o.client_id,
client_secret: o.client_secret,
sealed_client_secret: Default::default(), // unused in this context
allow_emails: o.allow_emails,
allow_domains: o.allow_domains,
scopes: o.scopes,
}
}
}
================================================
FILE: ngrok/src/config/oidc.rs
================================================
use crate::internals::proto::{
Oidc,
SecretString,
};
/// Oidc Options configuration
///
/// https://ngrok.com/docs/http/openid-connect/
#[derive(Clone, Default)]
pub struct OidcOptions {
issuer_url: String,
client_id: String,
client_secret: SecretString,
allow_emails: Vec<String>,
allow_domains: Vec<String>,
scopes: Vec<String>,
}
impl OidcOptions {
/// Create a new [OidcOptions] with the given issuer and client information.
pub fn new(
issuer_url: impl Into<String>,
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> Self {
OidcOptions {
issuer_url: issuer_url.into(),
client_id: client_id.into(),
client_secret: client_secret.into().into(),
..Default::default()
}
}
/// Allow the oidc user with the given email to access the tunnel.
pub fn allow_email(&mut self, email: impl Into<String>) -> &mut Self {
self.allow_emails.push(email.into());
self
}
/// Allow the oidc user with the given email domain to access the tunnel.
pub fn allow_domain(&mut self, domain: impl Into<String>) -> &mut Self {
self.allow_domains.push(domain.into());
self
}
/// Request the given scope from the oidc provider.
pub fn scope(&mut self, scope: impl Into<String>) -> &mut Self {
self.scopes.push(scope.into());
self
}
}
// transform into the wire protocol format
impl From<OidcOptions> for Oidc {
fn from(o: OidcOptions) -> Self {
Oidc {
issuer_url: o.issuer_url,
client_id: o.client_id,
client_secret: o.client_secret,
sealed_client_secret: Default::default(), // unused in this context
allow_emails: o.allow_emails,
allow_domains: o.allow_domains,
scopes: o.scopes,
}
}
}
================================================
FILE: ngrok/src/config/policies.rs
================================================
use std::{
fs::read_to_string,
io,
};
use serde::{
Deserialize,
Serialize,
};
use thiserror::Error;
use crate::internals::proto;
/// A policy that defines rules that should be applied to incoming or outgoing
/// connections to the edge.
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Policy {
inbound: Vec<Rule>,
outbound: Vec<Rule>,
}
/// A policy rule that should be applied
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Rule {
name: String,
expressions: Vec<String>,
actions: Vec<Action>,
}
/// An action that should be taken if the rule matches
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
#[serde(default)]
pub struct Action {
#[serde(rename = "type")]
type_: String,
config: Option<serde_json::Value>,
}
/// Errors in creating or serializing Policies
#[derive(Debug, Error)]
pub enum InvalidPolicy {
/// Error representing an invalid string for a Policy
#[error("failure to parse or generate policy")]
SerializationError(#[from] serde_json::Error),
/// An error loading a Policy from a file
#[error("failure to read policy file '{}'", .1)]
FileReadError(#[source] io::Error, String),
}
impl Policy {
/// Create a new empty [Policy] struct
pub fn new() -> Self {
Policy {
..Default::default()
}
}
/// Create a new [Policy] from a json string
fn from_json(json: impl AsRef<str>) -> Result<Self, InvalidPolicy> {
serde_json::from_str(json.as_ref()).map_err(InvalidPolicy::SerializationError)
}
/// Create a new [Policy] from a json file
pub fn from_file(json_file_path: impl AsRef<str>) -> Result<Self, InvalidPolicy> {
Policy::from_json(
read_to_string(json_file_path.as_ref()).map_err(|e| {
InvalidPolicy::FileReadError(e, json_file_path.as_ref().to_string())
})?,
)
}
/// Convert [Policy] to json string
pub fn to_json(&self) -> Result<String, InvalidPolicy> {
serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError)
}
/// Add an inbound policy
pub fn add_inbound(&mut self, rule: impl Into<Rule>) -> &mut Self {
self.inbound.push(rule.into());
self
}
/// Add an outbound policy
pub fn add_outbound(&mut self, rule: impl Into<Rule>) -> &mut Self {
self.outbound.push(rule.into());
self
}
}
impl TryFrom<&Policy> for Policy {
type Error = InvalidPolicy;
fn try_from(other: &Policy) -> Result<Policy, Self::Error> {
Ok(other.clone())
}
}
impl TryFrom<Result<Policy, InvalidPolicy>> for Policy {
type Error = InvalidPolicy;
fn try_from(other: Result<Policy, InvalidPolicy>) -> Result<Policy, Self::Error> {
other
}
}
impl TryFrom<&str> for Policy {
type Error = InvalidPolicy;
fn try_from(other: &str) -> Result<Policy, Self::Error> {
Policy::from_json(other)
}
}
impl Rule {
/// Create a new [Rule]
pub fn new(name: impl Into<String>) -> Self {
Rule {
name: name.into(),
..Default::default()
}
}
/// Convert [Rule] to json string
pub fn to_json(&self) -> Result<String, InvalidPolicy> {
serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError)
}
/// Add an expression
pub fn add_expression(&mut self, expression: impl Into<String>) -> &mut Self {
self.expressions.push(expression.into());
self
}
/// Add an action
pub fn add_action(&mut self, action: Action) -> &mut Self {
self.actions.push(action);
self
}
}
impl From<&mut Rule> for Rule {
fn from(other: &mut Rule) -> Self {
other.to_owned()
}
}
impl Action {
/// Create a new [Action]
pub fn new(type_: impl Into<String>, config: Option<&str>) -> Result<Self, InvalidPolicy> {
Ok(Action {
type_: type_.into(),
config: config
.map(|c| serde_json::from_str(c).map_err(InvalidPolicy::SerializationError))
.transpose()?,
})
}
/// Convert [Action] to json string
pub fn to_json(&self) -> Result<String, InvalidPolicy> {
serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError)
}
}
impl From<Policy> for proto::PolicyWrapper {
fn from(value: Policy) -> Self {
proto::PolicyWrapper::Policy(value.into())
}
}
// transform into the wire protocol format
impl From<Policy> for proto::Policy {
fn from(o: Policy) -> Self {
proto::Policy {
inbound: o.inbound.into_iter().map(|p| p.into()).collect(),
outbound: o.outbound.into_iter().map(|p| p.into()).collect(),
}
}
}
impl From<Rule> for proto::Rule {
fn from(p: Rule) -> Self {
proto::Rule {
name: p.name,
expressions: p.expressions,
actions: p.actions.into_iter().map(|a| a.into()).collect(),
}
}
}
impl From<Action> for proto::Action {
fn from(a: Action) -> Self {
proto::Action {
type_: a.type_,
config: a
.config
.map(|c| c.to_string().into_bytes())
.unwrap_or_default(),
}
}
}
#[cfg(test)]
pub(crate) mod test {
use super::*;
pub(crate) const POLICY_JSON: &str = r###"
{"inbound": [
{
"name": "test_in",
"expressions": ["req.Method == 'PUT'"],
"actions": [{"type": "deny"}]
}
],
"outbound": [
{
"name": "test_out",
"expressions": ["res.StatusCode == '200'"],
"actions": [{"type": "custom-response", "config": {"status_code":201}}]
}
]}
"###;
#[test]
fn test_json_to_policy() {
let policy: Policy = Policy::from_json(POLICY_JSON).unwrap();
assert_eq!(1, policy.inbound.len());
assert_eq!(1, policy.outbound.len());
let inbound = &policy.inbound[0];
let outbound = &policy.outbound[0];
assert_eq!("test_in", inbound.name);
assert_eq!(1, inbound.expressions.len());
assert_eq!(1, inbound.actions.len());
assert_eq!("req.Method == 'PUT'", inbound.expressions[0]);
assert_eq!("deny", inbound.actions[0].type_);
assert_eq!(None, inbound.actions[0].config);
assert_eq!("test_out", outbound.name);
assert_eq!(1, outbound.expressions.len());
assert_eq!(1, outbound.actions.len());
assert_eq!("res.StatusCode == '200'", outbound.expressions[0]);
assert_eq!("custom-response", outbound.actions[0].type_);
assert_eq!(
"{\"status_code\":201}",
outbound.actions[0].config.as_ref().unwrap().to_string()
);
}
#[test]
fn test_empty_json_to_policy() {
let policy: Policy = Policy::from_json("{}").unwrap();
assert_eq!(0, policy.inbound.len());
assert_eq!(0, policy.outbound.len());
}
#[test]
fn test_policy_to_json() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let json = policy.to_json().unwrap();
let policy2 = Policy::from_json(json).unwrap();
assert_eq!(policy, policy2);
}
#[test]
fn test_policy_to_json_error() {
let error = Policy::from_json("asdf").err().unwrap();
assert!(matches!(error, InvalidPolicy::SerializationError { .. }));
}
#[test]
fn test_rule_to_json() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let rule = &policy.outbound[0];
let json = rule.to_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let rule_map = parsed.as_object().unwrap();
assert_eq!("test_out", rule_map["name"]);
// expressions
let expressions = rule_map["expressions"].as_array().unwrap();
assert_eq!(1, expressions.len());
assert_eq!("res.StatusCode == '200'", expressions[0]);
// actions
let actions = rule_map["actions"].as_array().unwrap();
assert_eq!(1, actions.len());
assert_eq!("custom-response", actions[0]["type"]);
assert_eq!(201, actions[0]["config"]["status_code"]);
}
#[test]
fn test_action_to_json() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let action = &policy.outbound[0].actions[0];
let json = action.to_json().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
let action_map = parsed.as_object().unwrap();
assert_eq!("custom-response", action_map["type"]);
assert_eq!(201, action_map["config"]["status_code"]);
}
#[test]
fn test_builders() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let policy2 = Policy::new()
.add_inbound(
Rule::new("test_in")
.add_expression("req.Method == 'PUT'")
.add_action(Action::new("deny", None).unwrap()),
)
.add_outbound(
Rule::new("test_out")
.add_expression("res.StatusCode == '200'")
// .add_action(Action::new("deny", ""))
.add_action(
Action::new("custom-response", Some("{\"status_code\":201}")).unwrap(),
),
)
.to_owned();
assert_eq!(policy, policy2);
}
#[test]
fn test_load_file() {
let policy = Policy::from_json(POLICY_JSON).unwrap();
let policy2 = Policy::from_file("assets/policy.json").unwrap();
assert_eq!("test_in", policy2.inbound[0].name);
assert_eq!("test_out", policy2.outbound[0].name);
assert_eq!(policy, policy2);
}
#[test]
fn test_load_inbound_file() {
let policy = Policy::from_file("assets/policy-inbound.json").unwrap();
assert_eq!("test_in", policy.inbound[0].name);
assert_eq!(0, policy.outbound.len());
}
#[test]
fn test_load_file_error() {
let error = Policy::from_file("assets/absent.json").err().unwrap();
assert!(matches!(error, InvalidPolicy::FileReadError { .. }));
}
}
================================================
FILE: ngrok/src/config/tcp.rs
================================================
use std::{
collections::HashMap,
convert::From,
};
use url::Url;
use super::{
common::ProxyProto,
Policy,
};
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::common::{
default_forwards_to,
Binding,
CommonOpts,
TunnelConfig,
},
internals::proto::{
self,
BindExtra,
BindOpts,
},
tunnel::TcpTunnel,
Session,
};
/// The options for a TCP edge.
#[derive(Default, Clone)]
struct TcpOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) remote_addr: Option<String>,
pub(crate) bindings: Vec<String>,
}
impl TunnelConfig for TcpOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: self.bindings.clone(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
"tcp".into()
}
fn forwards_proto(&self) -> String {
// not supported
String::new()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn opts(&self) -> Option<BindOpts> {
// fill out all the options, translating to proto here
let mut tcp_endpoint = proto::TcpEndpoint::default();
if let Some(remote_addr) = self.remote_addr.as_ref() {
tcp_endpoint.addr = remote_addr.clone();
}
tcp_endpoint.proxy_proto = self.common_opts.proxy_proto;
tcp_endpoint.ip_restriction = self.common_opts.ip_restriction();
tcp_endpoint.traffic_policy = if self.common_opts.traffic_policy.is_some() {
self.common_opts.traffic_policy.clone().map(From::from)
} else if self.common_opts.policy.is_some() {
self.common_opts.policy.clone().map(From::from)
} else {
None
};
Some(BindOpts::Tcp(tcp_endpoint))
}
fn labels(&self) -> HashMap<String, String> {
HashMap::new()
}
}
impl_builder! {
/// A builder for a tunnel backing a TCP endpoint.
///
/// https://ngrok.com/docs/tcp/
TcpTunnelBuilder, TcpOptions, TcpTunnel, endpoint
}
/// The options for a TCP edge.
impl TcpTunnelBuilder {
/// Add the provided CIDR to the allowlist.
///
/// https://ngrok.com/docs/tcp/ip-restrictions/
pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
self.options.common_opts.cidr_restrictions.allow(cidr);
self
}
/// Add the provided CIDR to the denylist.
///
/// https://ngrok.com/docs/tcp/ip-restrictions/
pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
self.options.common_opts.cidr_restrictions.deny(cidr);
self
}
/// Sets the PROXY protocol version for connections over this tunnel.
pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
self.options.common_opts.proxy_proto = proxy_proto;
self
}
/// Sets the opaque metadata string for this tunnel.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Sets the ingress configuration for this endpoint.
///
/// Valid binding values are:
/// - `"public"` - Publicly accessible endpoint
/// - `"internal"` - Internal-only endpoint
/// - `"kubernetes"` - Kubernetes cluster binding
///
/// If not specified, the ngrok service will use its default binding configuration.
///
/// # Panics
///
/// Panics if called more than once or if an invalid binding value is provided.
///
/// # Examples
///
/// ```no_run
/// # use ngrok::Session;
/// # use ngrok::config::TunnelBuilder;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let session = Session::builder().authtoken_from_env().connect().await?;
///
/// // Using string
/// let tunnel = session.tcp_endpoint().binding("internal").listen().await?;
///
/// // Using typed enum
/// use ngrok::config::Binding;
/// let tunnel = session.tcp_endpoint().binding(Binding::Public).listen().await?;
/// # Ok(())
/// # }
/// ```
pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
if !self.options.bindings.is_empty() {
panic!("binding() can only be called once");
}
let binding_str = binding.into();
if let Err(e) = Binding::validate(&binding_str) {
panic!("{}", e);
}
self.options.bindings.push(binding_str);
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Self {
self.options.common_opts.forwards_to = Some(forwards_to.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
/// Sets the TCP address to request for this edge.
///
/// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#tcp-addresses
pub fn remote_addr(&mut self, remote_addr: impl Into<String>) -> &mut Self {
self.options.remote_addr = Some(remote_addr.into());
self
}
/// DEPRECATED: use traffic_policy instead.
pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
where
S: TryInto<Policy>,
{
self.options.common_opts.policy = Some(s.try_into()?);
Ok(self)
}
/// Set policy for this edge.
pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut Self {
self.options.common_opts.traffic_policy = Some(policy_str.into());
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
self
}
/// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &mut Self {
self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::policies::test::POLICY_JSON;
const METADATA: &str = "testmeta";
const TEST_FORWARD: &str = "testforward";
const REMOTE_ADDR: &str = "4.tcp.ngrok.io:1337";
const ALLOW_CIDR: &str = "0.0.0.0/0";
const DENY_CIDR: &str = "10.1.1.1/32";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&TcpTunnelBuilder {
session: None,
options: Default::default(),
}
.allow_cidr(ALLOW_CIDR)
.deny_cidr(DENY_CIDR)
.proxy_proto(ProxyProto::V2)
.metadata(METADATA)
.remote_addr(REMOTE_ADDR)
.forwards_to(TEST_FORWARD)
.policy(POLICY_JSON)
.unwrap()
.options,
);
}
fn tunnel_test<C>(tunnel_cfg: &C)
where
C: TunnelConfig,
{
assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(Vec::<String>::new(), extra.bindings);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("tcp", tunnel_cfg.proto());
let opts = tunnel_cfg.opts().unwrap();
assert!(matches!(opts, BindOpts::Tcp { .. }));
if let BindOpts::Tcp(endpoint) = opts {
assert_eq!(REMOTE_ADDR, endpoint.addr);
assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
let ip_restriction = endpoint.ip_restriction.unwrap();
assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
}
assert_eq!(HashMap::new(), tunnel_cfg.labels());
}
#[test]
fn test_binding_valid_values() {
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
// Test "public"
builder.binding("public");
assert_eq!(vec!["public"], builder.options.bindings);
// Test "internal"
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal");
assert_eq!(vec!["internal"], builder.options.bindings);
// Test "kubernetes"
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("kubernetes");
assert_eq!(vec!["kubernetes"], builder.options.bindings);
// Test with Binding enum
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding(Binding::Public);
assert_eq!(vec!["public"], builder.options.bindings);
}
#[test]
#[should_panic(expected = "Invalid binding value")]
fn test_binding_invalid_value() {
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("invalid");
}
#[test]
#[should_panic(expected = "binding() can only be called once")]
fn test_binding_called_twice() {
let mut builder = TcpTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("public");
builder.binding("internal");
}
}
================================================
FILE: ngrok/src/config/tls.rs
================================================
use std::collections::HashMap;
use bytes::Bytes;
use url::Url;
use super::{
common::ProxyProto,
Policy,
};
// These are used for doc comment links.
#[allow(unused_imports)]
use crate::config::{
ForwarderBuilder,
TunnelBuilder,
};
use crate::{
config::common::{
default_forwards_to,
Binding,
CommonOpts,
TunnelConfig,
},
internals::proto::{
self,
BindExtra,
BindOpts,
TlsTermination,
},
tunnel::TlsTunnel,
Session,
};
/// The options for TLS edges.
#[derive(Default, Clone)]
struct TlsOptions {
pub(crate) common_opts: CommonOpts,
pub(crate) domain: Option<String>,
pub(crate) mutual_tlsca: Vec<bytes::Bytes>,
pub(crate) key_pem: Option<bytes::Bytes>,
pub(crate) cert_pem: Option<bytes::Bytes>,
pub(crate) bindings: Vec<String>,
}
impl TunnelConfig for TlsOptions {
fn forwards_to(&self) -> String {
self.common_opts
.forwards_to
.clone()
.unwrap_or(default_forwards_to().into())
}
fn forwards_proto(&self) -> String {
// not supported
String::new()
}
fn verify_upstream_tls(&self) -> bool {
self.common_opts.verify_upstream_tls()
}
fn extra(&self) -> BindExtra {
BindExtra {
token: Default::default(),
ip_policy_ref: Default::default(),
metadata: self.common_opts.metadata.clone().unwrap_or_default(),
bindings: self.bindings.clone(),
pooling_enabled: self.common_opts.pooling_enabled.unwrap_or(false),
}
}
fn proto(&self) -> String {
"tls".into()
}
fn opts(&self) -> Option<BindOpts> {
// fill out all the options, translating to proto here
let mut tls_endpoint = proto::TlsEndpoint::default();
if let Some(domain) = self.domain.as_ref() {
tls_endpoint.domain = domain.clone();
}
tls_endpoint.proxy_proto = self.common_opts.proxy_proto;
// doing some backflips to check both cert_pem and key_pem are set, and avoid unwrapping
let tls_termination = self
.cert_pem
.as_ref()
.zip(self.key_pem.as_ref())
.map(|(c, k)| TlsTermination {
cert: c.to_vec(),
key: k.to_vec().into(),
sealed_key: Vec::new(),
});
tls_endpoint.ip_restriction = self.common_opts.ip_restriction();
tls_endpoint.mutual_tls_at_edge =
(!self.mutual_tlsca.is_empty()).then_some(self.mutual_tlsca.as_slice().into());
tls_endpoint.tls_termination = tls_termination;
tls_endpoint.traffic_policy = if self.common_opts.traffic_policy.is_some() {
self.common_opts.traffic_policy.clone().map(From::from)
} else if self.common_opts.policy.is_some() {
self.common_opts.policy.clone().map(From::from)
} else {
None
};
Some(BindOpts::Tls(tls_endpoint))
}
fn labels(&self) -> HashMap<String, String> {
HashMap::new()
}
}
impl_builder! {
/// A builder for a tunnel backing a TCP endpoint.
///
/// https://ngrok.com/docs/tls/
TlsTunnelBuilder, TlsOptions, TlsTunnel, endpoint
}
impl TlsTunnelBuilder {
/// Add the provided CIDR to the allowlist.
///
/// https://ngrok.com/docs/tls/ip-restrictions/
pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
self.options.common_opts.cidr_restrictions.allow(cidr);
self
}
/// Add the provided CIDR to the denylist.
///
/// https://ngrok.com/docs/tls/ip-restrictions/
pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
self.options.common_opts.cidr_restrictions.deny(cidr);
self
}
/// Sets the PROXY protocol version for connections over this tunnel.
pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
self.options.common_opts.proxy_proto = proxy_proto;
self
}
/// Sets the opaque metadata string for this tunnel.
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
self.options.common_opts.metadata = Some(metadata.into());
self
}
/// Sets the ingress configuration for this endpoint.
///
/// Valid binding values are:
/// - `"public"` - Publicly accessible endpoint
/// - `"internal"` - Internal-only endpoint
/// - `"kubernetes"` - Kubernetes cluster binding
///
/// If not specified, the ngrok service will use its default binding configuration.
///
/// # Panics
///
/// Panics if called more than once or if an invalid binding value is provided.
///
/// # Examples
///
/// ```no_run
/// # use ngrok::Session;
/// # use ngrok::config::TunnelBuilder;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let session = Session::builder().authtoken_from_env().connect().await?;
///
/// // Using string
/// let tunnel = session.tls_endpoint().binding("internal").listen().await?;
///
/// // Using typed enum
/// use ngrok::config::Binding;
/// let tunnel = session.tls_endpoint().binding(Binding::Public).listen().await?;
/// # Ok(())
/// # }
/// ```
pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
if !self.options.bindings.is_empty() {
panic!("binding() can only be called once");
}
let binding_str = binding.into();
if let Err(e) = Binding::validate(&binding_str) {
panic!("{}", e);
}
self.options.bindings.push(binding_str);
self
}
/// Sets the ForwardsTo string for this tunnel. This can be viewed via the
/// API or dashboard.
///
/// This overrides the default process info if using
/// [TunnelBuilder::listen], and is in turn overridden by the url provided
/// to [ForwarderBuilder::listen_and_forward].
///
/// https://ngrok.com/docs/api/resources/tunnels/#tunnel-fields
pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Self {
self.options.common_opts.forwards_to = Some(forwards_to.into());
self
}
/// Disables backend TLS certificate verification for forwards from this tunnel.
pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut Self {
self.options
.common_opts
.set_verify_upstream_tls(verify_upstream_tls);
self
}
/// Sets the domain to request for this edge.
///
/// https://ngrok.com/docs/network-edge/domains-and-tcp-addresses/#domains
pub fn domain(&mut self, domain: impl Into<String>) -> &mut Self {
self.options.domain = Some(domain.into());
self
}
/// Adds a certificate in PEM format to use for mutual TLS authentication.
///
/// These will be used to authenticate client certificates for requests at
/// the ngrok edge.
///
/// https://ngrok.com/docs/tls/mutual-tls/
pub fn mutual_tlsca(&mut self, mutual_tlsca: Bytes) -> &mut Self {
self.options.mutual_tlsca.push(mutual_tlsca);
self
}
/// Sets the key and certificate in PEM format for TLS termination at the
/// ngrok edge.
///
/// https://ngrok.com/docs/tls/tls-termination/
pub fn termination(&mut self, cert_pem: Bytes, key_pem: Bytes) -> &mut Self {
self.options.key_pem = Some(key_pem);
self.options.cert_pem = Some(cert_pem);
self
}
/// DEPRECATED: use traffic_policy instead.
pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
where
S: TryInto<Policy>,
{
self.options.common_opts.policy = Some(s.try_into()?);
Ok(self)
}
/// Set policy for this edge.
pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut Self {
self.options.common_opts.traffic_policy = Some(policy_str.into());
self
}
pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
self.options.common_opts.for_forwarding_to(to_url);
self
}
/// Allows the endpoint to pool with other endpoints with the same host/port/binding
pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &mut Self {
self.options.common_opts.pooling_enabled = Some(pooling_enabled.into());
self
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::config::policies::test::POLICY_JSON;
const METADATA: &str = "testmeta";
const TEST_FORWARD: &str = "testforward";
const ALLOW_CIDR: &str = "0.0.0.0/0";
const DENY_CIDR: &str = "10.1.1.1/32";
const CA_CERT: &[u8] = "test ca cert".as_bytes();
const CA_CERT2: &[u8] = "test ca cert2".as_bytes();
const KEY: &[u8] = "test cert".as_bytes();
const CERT: &[u8] = "test cert".as_bytes();
const DOMAIN: &str = "test domain";
#[test]
fn test_interface_to_proto() {
// pass to a function accepting the trait to avoid
// "creates a temporary which is freed while still in use"
tunnel_test(
&TlsTunnelBuilder {
session: None,
options: Default::default(),
}
.allow_cidr(ALLOW_CIDR)
.deny_cidr(DENY_CIDR)
.proxy_proto(ProxyProto::V2)
.metadata(METADATA)
.domain(DOMAIN)
.mutual_tlsca(CA_CERT.into())
.mutual_tlsca(CA_CERT2.into())
.termination(CERT.into(), KEY.into())
.forwards_to(TEST_FORWARD)
.policy(POLICY_JSON)
.unwrap()
.options,
);
}
fn tunnel_test<C>(tunnel_cfg: C)
where
C: TunnelConfig,
{
assert_eq!(TEST_FORWARD, tunnel_cfg.forwards_to());
let extra = tunnel_cfg.extra();
assert_eq!(String::default(), *extra.token);
assert_eq!(METADATA, extra.metadata);
assert_eq!(Vec::<String>::new(), extra.bindings);
assert_eq!(String::default(), extra.ip_policy_ref);
assert_eq!("tls", tunnel_cfg.proto());
let opts = tunnel_cfg.opts().unwrap();
assert!(matches!(opts, BindOpts::Tls { .. }));
if let BindOpts::Tls(endpoint) = opts {
assert_eq!(DOMAIN, endpoint.domain);
assert_eq!(String::default(), endpoint.subdomain);
assert!(matches!(endpoint.proxy_proto, ProxyProto::V2));
assert!(!endpoint.mutual_tls_at_agent);
let ip_restriction = endpoint.ip_restriction.unwrap();
assert_eq!(Vec::from([ALLOW_CIDR]), ip_restriction.allow_cidrs);
assert_eq!(Vec::from([DENY_CIDR]), ip_restriction.deny_cidrs);
let tls_termination = endpoint.tls_termination.unwrap();
assert_eq!(CERT, tls_termination.cert);
assert_eq!(KEY, *tls_termination.key);
assert!(tls_termination.sealed_key.is_empty());
let mutual_tls = endpoint.mutual_tls_at_edge.unwrap();
let mut agg = CA_CERT.to_vec();
agg.extend(CA_CERT2.to_vec());
assert_eq!(agg, mutual_tls.mutual_tls_ca);
}
assert_eq!(HashMap::new(), tunnel_cfg.labels());
}
#[test]
fn test_binding_valid_values() {
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
// Test "public"
builder.binding("public");
assert_eq!(vec!["public"], builder.options.bindings);
// Test "internal"
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("internal");
assert_eq!(vec!["internal"], builder.options.bindings);
// Test "kubernetes"
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("kubernetes");
assert_eq!(vec!["kubernetes"], builder.options.bindings);
// Test with Binding enum
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding(Binding::Kubernetes);
assert_eq!(vec!["kubernetes"], builder.options.bindings);
}
#[test]
#[should_panic(expected = "Invalid binding value")]
fn test_binding_invalid_value() {
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("invalid");
}
#[test]
#[should_panic(expected = "binding() can only be called once")]
fn test_binding_called_twice() {
let mut builder = TlsTunnelBuilder {
session: None,
options: Default::default(),
};
builder.binding("public");
builder.binding("internal");
}
}
================================================
FILE: ngrok/src/config/webhook_verification.rs
================================================
use crate::internals::proto::{
SecretString,
WebhookVerification as WebhookProto,
};
/// Configuration for webhook verification.
#[derive(Clone)]
pub(crate) struct WebhookVerification {
/// The webhook provider
pub(crate) provider: String,
/// The secret for verifying webhooks from this provider.
pub(crate) secret: SecretString,
}
impl WebhookVerification {}
// transform into the wire protocol format
impl From<WebhookVerification> for WebhookProto {
fn from(wv: WebhookVerification) -> Self {
WebhookProto {
provider: wv.provider,
secret: wv.secret,
sealed_secret: vec![], // unused in this context
}
}
}
================================================
FILE: ngrok/src/conn.rs
================================================
use std::{
net::SocketAddr,
pin::Pin,
task::{
Context,
Poll,
},
};
// Support for axum's connection info trait.
#[cfg(feature = "axum")]
use axum::extract::connect_info::Connected;
#[cfg(feature = "hyper")]
use hyper::rt::{
Read as HyperRead,
Write as HyperWrite,
};
use muxado::typed::TypedStream;
use tokio::io::{
AsyncRead,
AsyncWrite,
};
use crate::{
config::ProxyProto,
internals::proto::{
EdgeType,
ProxyHeader,
},
};
/// A connection from an ngrok tunnel.
///
/// This implements [AsyncRead]/[AsyncWrite], as well as providing access to the
/// address from which the connection to the ngrok edge originated.
pub(crate) struct ConnInner {
pub(crate) info: Info,
pub(crate) stream: TypedStream,
}
#[derive(Clone)]
pub(crate) struct Info {
pub(crate) header: ProxyHeader,
pub(crate) remote_addr: SocketAddr,
pub(crate) proxy_proto: ProxyProto,
pub(crate) app_protocol: Option<String>,
pub(crate) verify_upstream_tls: bool,
}
impl ConnInfo for Info {
fn remote_addr(&self) -> SocketAddr {
self.remote_addr
}
}
impl EdgeConnInfo for Info {
fn edge_type(&self) -> EdgeType {
self.header.edge_type
}
fn passthrough_tls(&self) -> bool {
self.header.passthrough_tls
}
}
impl EndpointConnInfo for Info {
fn proto(&self) -> &str {
self.header.proto.as_str()
}
}
// This codgen indirect is required to make the hyper io trait bounds
// dependent on the hyper feature. You can't put a #[cfg] on a single bound, so
// we're putting the whole trait def in a macro. Gross, but gets the job done.
macro_rules! conn_trait {
($($hyper_bound:tt)*) => {
/// An incoming connection over an ngrok tunnel.
/// Effectively a trait alias for async read+write, plus connection info.
pub trait Conn: ConnInfo + AsyncRead + AsyncWrite $($hyper_bound)* + Unpin + Send + 'static {}
}
}
#[cfg(not(feature = "hyper"))]
conn_trait!();
#[cfg(feature = "hyper")]
conn_trait! {
+ hyper::rt::Read + hyper::rt::Write
}
/// Information common to all ngrok connections.
pub trait ConnInfo {
/// Returns the client address that initiated the connection to the ngrok
/// edge.
fn remote_addr(&self) -> SocketAddr;
}
/// Information about connections via ngrok edges.
pub trait EdgeConnInfo {
/// Returns the edge type for this connection.
fn edge_type(&self) -> EdgeType;
/// Returns whether the connection includes the tls handshake and encrypted
/// stream.
fn passthrough_tls(&self) -> bool;
}
/// Information about connections via ngrok endpoints.
pub trait EndpointConnInfo {
/// Returns the endpoint protocol.
fn proto(&self) -> &str;
}
macro_rules! make_conn_type {
(info EdgeConnInfo, $wrapper:tt) => {
impl EdgeConnInfo for $wrapper {
fn edge_type(&self) -> EdgeType {
self.inner.info.edge_type()
}
fn passthrough_tls(&self) -> bool {
self.inner.info.passthrough_tls()
}
}
};
(info EndpointConnInfo, $wrapper:tt) => {
impl EndpointConnInfo for $wrapper {
fn proto(&self) -> &str {
self.inner.info.proto()
}
}
};
($(#[$outer:meta])* $wrapper:ident, $($m:tt),*) => {
$(#[$outer])*
pub struct $wrapper {
pub(crate) inner: ConnInner,
}
impl Conn for $wrapper {}
impl ConnInfo for $wrapper {
fn remote_addr(&self) -> SocketAddr {
self.inner.info.remote_addr()
}
}
impl AsyncRead for $wrapper {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut *self.inner.stream).poll_read(cx, buf)
}
}
#[cfg(feature = "hyper")]
impl HyperRead for $wrapper {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: hyper::rt::ReadBufCursor<'_>,
) -> Poll<std::io::Result<()>> {
let mut tokio_buf = tokio::io::ReadBuf::uninit(unsafe{ buf.as_mut() });
let res = std::task::ready!(Pin::new(&mut *self.inner.stream).poll_read(cx, &mut tokio_buf));
let filled = tokio_buf.filled().len();
unsafe { buf.advance(filled) };
Poll::Ready(res)
}
}
#[cfg(feature = "hyper")]
impl HyperWrite for $wrapper {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
Pin::new(&mut *self.inner.stream).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut *self.inner.stream).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut *self.inner.stream).poll_shutdown(cx)
}
}
impl AsyncWrite for $wrapper {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
Pin::new(&mut *self.inner.stream).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut *self.inner.stream).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut *self.inner.stream).poll_shutdown(cx)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
#[cfg(feature = "axum")]
impl Connected<&$wrapper> for SocketAddr {
fn connect_info(target: &$wrapper) -> Self {
target.inner.info.remote_addr()
}
}
$(
make_conn_type!(info $m, $wrapper);
)*
};
}
make_conn_type! {
/// A connection via an ngrok Edge.
EdgeConn, EdgeConnInfo
}
make_conn_type! {
/// A connection via an ngrok Endpoint.
EndpointConn, EndpointConnInfo
}
================================================
FILE: ngrok/src/forwarder.rs
================================================
use std::{
collections::HashMap,
error::Error as StdError,
};
use async_trait::async_trait;
use tokio::task::JoinHandle;
use url::Url;
use crate::{
prelude::{
EdgeInfo,
EndpointInfo,
TunnelCloser,
TunnelInfo,
},
session::RpcError,
Tunnel,
};
/// An ngrok forwarder.
///
/// Represents a tunnel that is being forwarded to a URL.
pub struct Forwarder<T> {
pub(crate) join: JoinHandle<Result<(), Box<dyn StdError + Send + Sync>>>,
pub(crate) inner: T,
}
impl<T> Forwarder<T> {
/// Wait for the forwarding task to exit.
pub fn join(&mut self) -> &mut JoinHandle<Result<(), Box<dyn StdError + Send + Sync>>> {
&mut self.join
}
}
#[async_trait]
impl<T> TunnelCloser for Forwarder<T>
where
T: TunnelCloser + Send,
{
async fn close(&mut self) -> Result<(), RpcError> {
self.inner.close().await
}
}
impl<T> TunnelInfo for Forwarder<T>
where
T: TunnelInfo,
{
fn id(&self) -> &str {
self.inner.id()
}
fn forwards_to(&self) -> &str {
self.inner.forwards_to()
}
fn metadata(&self) -> &str {
self.inner.metadata()
}
}
impl<T> EndpointInfo for Forwarder<T>
where
T: EndpointInfo,
{
fn proto(&self) -> &str {
self.inner.proto()
}
fn url(&self) -> &str {
self.inner.url()
}
}
impl<T> EdgeInfo for Forwarder<T>
where
T: EdgeInfo,
{
fn labels(&self) -> &HashMap<String, String> {
self.inner.labels()
}
}
pub(crate) fn forward<T>(mut listener: T, info: T, to_url: Url) -> Result<Forwarder<T>, RpcError>
where
T: Tunnel + Send + 'static,
<T as Tunnel>::Conn: crate::tunnel_ext::ConnExt,
{
let handle =
tokio::spawn(
async move { Ok(crate::tunnel_ext::forward_tunnel(&mut listener, to_url).await?) },
);
Ok(Forwarder {
join: handle,
inner: info,
})
}
================================================
FILE: ngrok/src/internals/proto.rs
================================================
use std::{
collections::HashMap,
error,
fmt,
io,
ops::{
Deref,
DerefMut,
},
str::FromStr,
string::FromUtf8Error,
sync::Arc,
};
use muxado::typed::StreamType;
use serde::{
de::{
DeserializeOwned,
Visitor,
},
Deserialize,
Serialize,
Serializer,
};
use thiserror::Error;
use tokio::io::{
AsyncRead,
AsyncReadExt,
};
use tracing::debug;
pub const AUTH_REQ: StreamType = StreamType::clamp(0);
pub const BIND_REQ: StreamType = StreamType::clamp(1);
pub const UNBIND_REQ: StreamType = StreamType::clamp(2);
pub const PROXY_REQ: StreamType = StreamType::clamp(3);
pub const RESTART_REQ: StreamType = StreamType::clamp(4);
pub const STOP_REQ: StreamType = StreamType::clamp(5);
pub const UPDATE_REQ: StreamType = StreamType::clamp(6);
pub const BIND_LABELED_REQ: StreamType = StreamType::clamp(7);
pub const STOP_TUNNEL_REQ: StreamType = StreamType::clamp(9);
pub const VERSION: &[&str] = &["3", "2"]; // integers in priority order
/// An error that may have an ngrok error code.
/// All ngrok error codes are documented at https://ngrok.com/docs/errors
pub trait Error: error::Error {
/// Return the ngrok error code, if one exists for this error.
fn error_code(&self) -> Option<&str> {
None
}
/// Return the error message minus the ngrok error code.
/// If this error has no error code, this is equivalent to
/// `format!("{error}")`.
fn msg(&self) -> String {
format!("{self}")
}
}
impl<E> Error for Box<E>
where
E: Error,
{
fn error_code(&self) -> Option<&str> {
<E as Error>::error_code(self)
}
fn msg(&self) -> String {
<E as Error>::msg(self)
}
}
impl<E> Error for Arc<E>
where
E: Error,
{
fn error_code(&self) -> Option<&str> {
<E as Error>::error_code(self)
}
fn msg(&self) -> String {
<E as Error>::msg(self)
}
}
impl<E> Error for &E
where
E: Error,
{
fn error_code(&self) -> Option<&str> {
<E as Error>::error_code(self)
}
fn msg(&self) -> String {
<E as Error>::msg(self)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct ErrResp {
pub msg: String,
pub error_code: Option<String>,
}
impl<'a> From<&'a str> for ErrResp {
fn from(value: &'a str) -> Self {
let mut error_code = None;
let mut msg_lines = vec![];
for line in value.lines().filter(|l| !l.is_empty()) {
if line.starts_with("ERR_NGROK_") {
error_code = Some(line.trim().into());
} else {
msg_lines.push(line);
}
}
ErrResp {
error_code,
msg: msg_lines.join("\n"),
}
}
}
impl error::Error for ErrResp {}
const ERR_URL: &str = "https://ngrok.com/docs/errors";
impl fmt::Display for ErrResp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.msg.fmt(f)?;
if let Some(code) = self.error_code.as_ref().map(|s| s.to_lowercase()) {
write!(f, "\n\n{ERR_URL}/{code}")?;
}
Ok(())
}
}
impl Error for ErrResp {
fn error_code(&self) -> Option<&str> {
self.error_code.as_deref()
}
fn msg(&self) -> String {
self.msg.clone()
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Auth {
pub version: Vec<String>, // protocol versions supported, ordered by preference
pub client_id: String, // empty for new sessions
pub extra: AuthExtra, // clients may add whatever data the like to auth messages
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct SecretBytes(#[serde(with = "base64bytes")] Vec<u8>);
impl Deref for SecretBytes {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SecretBytes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> From<&'a [u8]> for SecretBytes {
fn from(other: &'a [u8]) -> Self {
SecretBytes(other.into())
}
}
impl From<Vec<u8>> for SecretBytes {
fn from(other: Vec<u8>) -> Self {
SecretBytes(other)
}
}
impl fmt::Display for SecretBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "********")
}
}
impl fmt::Debug for SecretBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "********")
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct SecretString(String);
impl Deref for SecretString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SecretString {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> From<&'a str> for SecretString {
fn from(other: &'a str) -> Self {
SecretString(other.into())
}
}
impl From<String> for SecretString {
fn from(other: String) -> Self {
SecretString(other)
}
}
impl fmt::Display for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "********")
}
}
impl fmt::Debug for SecretString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "********")
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct AuthExtra {
#[serde(rename = "OS")]
pub os: String,
pub arch: String,
pub auth_token: SecretString,
pub version: String,
pub hostname: String,
pub user_agent: String,
pub metadata: String,
pub cookie: SecretString,
pub heartbeat_interval: i64,
pub heartbeat_tolerance: i64,
// for each remote operation, these variables define whether the ngrok
// client is capable of executing that operation. each capability
// is transmitted as a pointer to String, with the following meanings:
//
// null -> operation disallow beause the ngrok agent version is too old.
// this is true because older clients will never set this value
//
// "" (empty String) -> the operation is supported
//
// non-empty String -> the operation is not supported and this value is the user-facing
// error message describing why it is not supported
pub update_unsupported_error: Option<String>,
pub stop_unsupported_error: Option<String>,
pub restart_unsupported_error: Option<String>,
pub proxy_type: String,
#[serde(rename = "MutualTLS")]
pub mutual_tls: bool,
pub service_run: bool,
pub config_version: String,
pub custom_interface: bool,
#[serde(rename = "CustomCAs")]
pub custom_cas: bool,
pub client_type: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct AuthResp {
pub version: String,
pub client_id: String,
#[serde(default)]
pub extra: AuthRespExtra,
}
rpc_req!(Auth, AuthResp, AUTH_REQ);
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct AuthRespExtra {
pub version: Option<String>,
pub region: Option<String>,
pub cookie: Option<SecretString>,
pub account_name: Option<String>,
pub session_duration: Option<i64>,
pub plan_name: Option<String>,
pub banner: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Bind<T> {
#[serde(rename = "Id")]
pub client_id: String,
pub proto: String,
pub forwards_to: String,
pub forwards_proto: String,
pub opts: T,
pub extra: BindExtra,
}
#[derive(Debug, Clone)]
// allowing this since these aren't persistent values.
#[allow(clippy::large_enum_variant)]
pub enum BindOpts {
Http(HttpEndpoint),
Tcp(TcpEndpoint),
Tls(TlsEndpoint),
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct BindExtra {
pub token: SecretString,
#[serde(rename = "IPPolicyRef")]
pub ip_policy_ref: String,
pub metadata: String,
pub bindings: Vec<String>,
#[serde(rename = "PoolingEnabled")]
pub pooling_enabled: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct BindResp<T> {
#[serde(rename = "Id")]
pub client_id: String,
#[serde(rename = "URL")]
pub url: String,
pub proto: String,
#[serde(rename = "Opts")]
pub bind_opts: T,
pub extra: BindRespExtra,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct BindRespExtra {
pub token: SecretString,
}
rpc_req!(Bind<T>, BindResp<T>, BIND_REQ; T: std::fmt::Debug + Serialize + DeserializeOwned + Clone);
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct StartTunnelWithLabel {
pub labels: HashMap<String, String>,
pub forwards_to: String,
pub forwards_proto: String,
pub metadata: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct StartTunnelWithLabelResp {
pub id: String,
}
rpc_req!(
StartTunnelWithLabel,
StartTunnelWithLabelResp,
BIND_LABELED_REQ
);
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct Unbind {
#[serde(rename = "Id")]
pub client_id: String,
// extra: not sure what this field actually contains
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct UnbindResp {
// extra: not sure what this field actually contains
}
rpc_req!(Unbind, UnbindResp, UNBIND_REQ);
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct ProxyHeader {
pub id: String,
pub client_addr: String,
pub proto: String,
pub edge_type: EdgeType,
#[serde(rename = "PassthroughTLS")]
pub passthrough_tls: bool,
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ReadHeaderError {
#[error("error reading proxy header")]
Io(#[from] io::Error),
#[error("invalid utf-8 in proxy header")]
InvalidUtf8(#[from] FromUtf8Error),
#[error("invalid proxy header json")]
InvalidHeader(#[from] serde_json::Error),
}
impl ProxyHeader {
pub async fn read_from_stream(
mut stream: impl AsyncRead + Unpin,
) -> Result<Self, ReadHeaderError> {
let size = stream.read_i64_le().await?;
let mut buf = vec![0u8; size as usize];
stream.read_exact(&mut buf).await?;
let header = String::from_utf8(buf)?;
debug!(?header, "read header");
Ok(serde_json::from_str(&header)?)
}
}
/// The edge type for an incomming connection.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum EdgeType {
/// EdgeType Undefined
Undefined,
/// A TCP Edge
Tcp,
/// A TLS Edge
Tls,
/// A HTTPs Edge
Https,
}
impl FromStr for EdgeType {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"1" => EdgeType::Tcp,
"2" => EdgeType::Tls,
"3" => EdgeType::Https,
_ => EdgeType::Undefined,
})
}
}
impl EdgeType {
pub(crate) fn as_str(self) -> &'static str {
match self {
EdgeType::Undefined => "0",
EdgeType::Tcp => "1",
EdgeType::Tls => "2",
EdgeType::Https => "3",
}
}
}
impl Serialize for EdgeType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
struct EdgeTypeVisitor;
impl<'de> Visitor<'de> for EdgeTypeVisitor {
type Value = EdgeType;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(r#""0", "1", "2", or "3""#)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(EdgeType::from_str(v).unwrap())
}
}
impl<'de> Deserialize<'de> for EdgeType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(EdgeTypeVisitor)
}
}
/// A request from the ngrok dashboard for the agent to stop.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Stop {}
/// Common response structure for all remote commands originating from the ngrok
/// dashboard.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct CommandResp {
/// The error arising from command handling, if any.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
pub type StopResp = CommandResp;
rpc_req!(Stop, StopResp, STOP_REQ);
/// A request from the ngrok dashboard for the agent to restart.
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Restart {}
pub type RestartResp = CommandResp;
rpc_req!(Restart, RestartResp, RESTART_REQ);
/// A request from the ngrok dashboard for the agent to update itself.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Update {
/// The version that the agent is requested to update to.
pub version: String,
/// Whether or not updating to the same major version is sufficient.
pub permit_major_version: bool,
}
/// A request from remote to stop a tunnel
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct StopTunnel {
/// The id of the tunnel to stop
#[serde(rename = "Id")]
pub client_id: String,
/// The message on why this tunnel was stopped
pub message: String,
/// An optional ngrok error code
pub error_code: Option<String>,
}
pub type UpdateResp = CommandResp;
rpc_req!(Update, UpdateResp, UPDATE_REQ);
/// The version of [PROXY protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
/// to use with this tunnel.
///
/// [ProxyProto::None] disables PROXY protocol support.
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)]
pub enum ProxyProto {
/// No PROXY protocol
#[default]
None,
/// PROXY protocol v1
V1,
/// PROXY protocol v2
V2,
}
impl From<ProxyProto> for i64 {
fn from(other: ProxyProto) -> Self {
use ProxyProto::*;
match other {
None => 0,
V1 => 1,
V2 => 2,
}
}
}
impl From<i64> for ProxyProto {
fn from(other: i64) -> Self {
use ProxyProto::*;
match other {
1 => V1,
2 => V2,
_ => None,
}
}
}
#[derive(Debug, Clone, Error)]
#[error("invalid proxyproto string: {}", .0)]
pub struct InvalidProxyProtoString(String);
impl FromStr for ProxyProto {
type Err = InvalidProxyProtoString;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use ProxyProto::*;
Ok(match s {
"" => None,
"1" => V1,
"2" => V2,
_ => return Err(InvalidProxyProtoString(s.into())),
})
}
}
impl Serialize for ProxyProto {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i64(i64::from(*self))
}
}
struct ProxyProtoVisitor;
impl<'de> Visitor<'de> for ProxyProtoVisitor {
type Value = ProxyProto;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("0, 1, or 2")
}
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(ProxyProto::from(v))
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(ProxyProto::from(v as i64))
}
}
impl<'de> Deserialize<'de> for ProxyProto {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_i64(ProxyProtoVisitor)
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum PolicyWrapper {
#[serde(serialize_with = "serialize_policy")]
Policy(Policy),
String(String),
}
impl From<String> for PolicyWrapper {
fn from(value: String) -> Self {
PolicyWrapper::String(value)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct HttpEndpoint {
#[serde(default)]
pub domain: String,
pub hostname: String,
pub auth: String,
pub subdomain: String,
pub host_header_rewrite: bool,
pub local_url_scheme: Option<String>,
pub proxy_proto: ProxyProto,
pub compression: Option<Compression>,
pub circuit_breaker: Option<CircuitBreaker>,
#[serde(rename = "IPRestriction")]
pub ip_restriction: Option<IpRestriction>,
pub basic_auth: Option<BasicAuth>,
#[serde(rename = "OAuth")]
pub oauth: Option<Oauth>,
#[serde(rename = "OIDC")]
pub oidc: Option<Oidc>,
pub webhook_verification: Option<WebhookVerification>,
#[serde(rename = "MutualTLSCA")]
pub mutual_tls_ca: Option<MutualTls>,
#[serde(default)]
pub request_headers: Option<Headers>,
#[serde(default)]
pub response_headers: Option<Headers>,
#[serde(rename = "WebsocketTCPConverter")]
pub websocket_tcp_converter: Option<WebsocketTcpConverter>,
#[serde(rename = "UserAgentFilter")]
pub user_agent_filter: Option<UserAgentFilter>,
#[serde(rename = "TrafficPolicy")]
pub traffic_policy: Option<PolicyWrapper>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Compression {}
fn is_default<T>(v: &T) -> bool
where
T: PartialEq<T> + Default,
{
T::default() == *v
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct CircuitBreaker {
#[serde(default, skip_serializing_if = "is_default")]
pub error_threshold: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BasicAuth {
#[serde(default, skip_serializing_if = "is_default")]
pub credentials: Vec<BasicAuthCredential>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct BasicAuthCredential {
pub username: String,
#[serde(default, skip_serializing_if = "is_default")]
pub cleartext_password: String,
#[serde(default, skip_serializing_if = "is_default")]
#[serde(with = "base64bytes")]
pub hashed_password: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpRestriction {
#[serde(default, skip_serializing_if = "is_default")]
pub allow_cidrs: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub deny_cidrs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Oauth {
pub provider: String,
#[serde(default, skip_serializing_if = "is_default")]
pub client_id: String,
#[serde(default, skip_serializing_if = "is_default")]
pub client_secret: SecretString,
#[serde(default, skip_serializing_if = "is_default")]
#[serde(with = "base64bytes")]
pub sealed_client_secret: Vec<u8>,
#[serde(default, skip_serializing_if = "is_default")]
pub allow_emails: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub allow_domains: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Oidc {
pub issuer_url: String,
#[serde(default, skip_serializing_if = "is_default")]
pub client_id: String,
#[serde(default, skip_serializing_if = "is_default")]
pub client_secret: SecretString,
#[serde(default, skip_serializing_if = "is_default")]
#[serde(with = "base64bytes")]
pub sealed_client_secret: Vec<u8>,
#[serde(default, skip_serializing_if = "is_default")]
pub allow_emails: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub allow_domains: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookVerification {
pub provider: String,
#[serde(default, skip_serializing_if = "is_default")]
pub secret: SecretString,
#[serde(default, skip_serializing_if = "is_default")]
#[serde(with = "base64bytes")]
pub sealed_secret: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutualTls {
#[serde(default, skip_serializing_if = "is_default")]
#[serde(with = "base64bytes")]
// this is snake-case on the wire
pub mutual_tls_ca: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Headers {
#[serde(default, skip_serializing_if = "is_default")]
pub add: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub remove: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub add_parsed: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct WebsocketTcpConverter {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAgentFilter {
#[serde(default, skip_serializing_if = "is_default")]
pub allow: Vec<String>,
#[serde(default, skip_serializing_if = "is_default")]
pub deny: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct TcpEndpoint {
pub addr: String,
pub proxy_proto: ProxyProto,
#[serde(rename = "IPRestriction")]
pub ip_restriction: Option<IpRestriction>,
#[serde(rename = "TrafficPolicy")]
pub traffic_policy: Option<PolicyWrapper>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase")]
pub struct TlsEndpoint {
#[serde(default)]
pub domain: String,
pub hostname: String,
pub subdomain: String,
pub proxy_proto: ProxyProto,
#[serde(rename = "MutualTLSAtAgent")]
pub mutual_tls_at_agent: bool,
#[serde(rename = "MutualTLSAtEdge")]
pub mutual_tls_at_edge: Option<MutualTls>,
#[serde(rename = "TLSTermination")]
pub tls_termination: Option<TlsTermination>,
#[serde(rename = "IPRestriction")]
pub ip_restriction: Option<IpRestriction>,
#[serde(rename = "TrafficPolicy")]
pub traffic_policy: Option<PolicyWrapper>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct TlsTermination {
#[serde(default, with = "base64bytes", skip_serializing_if = "is_default")]
pub cert: Vec<u8>,
#[serde(skip_serializing_if = "is_default", default)]
pub key: SecretBytes,
#[serde(default, with = "base64bytes", skip_serializing_if = "is_default")]
pub sealed_key: Vec<u8>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase", default)]
pub struct Policy {
pub inbound: Vec<Rule>,
pub outbound: Vec<Rule>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase", default)]
pub struct Rule {
pub name: String,
pub expressions: Vec<String>,
pub actions: Vec<Action>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(rename_all = "PascalCase", default)]
pub struct Action {
#[serde(rename = "Type")]
pub type_: String,
#[serde(default, with = "vec_to_json", skip_serializing_if = "is_default")]
pub config: Vec<u8>,
}
// This function converts a Policy into a valid JSON string. This is used so legacy configurations will still work
// using the new string "TrafficPolicy" field.
fn serialize_policy<S: Serializer>(v: &Policy, s: S) -> Result<S::Ok, S::Error> {
let abc = match serde_json::to_string(v) {
Ok(t) => t,
Err(_) => {
return Err(serde::ser::Error::custom(
"policy could not be converted to valid json",
))
}
};
s.serialize_str(&abc)
}
// These are helpers to convert base64 strings to full, real json. The serialize helper also ensures that the resulting
// representation isn't a string-escaped string.
mod vec_to_json {
use serde::{
Deserialize,
Deserializer,
Serialize,
Serializer,
};
pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Error> {
let u: serde_json::Value = match serde_json::from_slice(v) {
Ok(k) => k,
Err(_) => return Err(serde::ser::Error::custom("Config is invalid JSON")),
};
u.serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let s = serde_json::Map::deserialize(d)?;
let v = serde_json::to_vec(&s).unwrap();
Ok(v)
}
}
// These are helpers to facilitate the Vec<u8> <-> base64-encoded bytes
// representation that the Go messages use
mod base64bytes {
use base64::prelude::*;
use serde::{
Deserialize,
Deserializer,
Serialize,
Serializer,
};
pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
BASE64_STANDARD.encode(v).serialize(s)
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
let s = String::deserialize(d)?;
BASE64_STANDARD
.decode(s.as_bytes())
.map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_proxy_proto_serde() {
let input = "2";
let p: ProxyProto = serde_json::from_str(input).unwrap();
assert!(matches!(p, ProxyProto::V2));
assert_eq!(serde_json::to_string(&p).unwrap(), "2");
}
pub(crate) const POLICY_JSON: &str = r###"{"Inbound":[{"Name":"test_in","Expressions":["req.Method == 'PUT'"],"Actions":[{"Type":"deny"}]}],"Outbound":[{"Name":"test_out","Expressions":["res.StatusCode == '200'"],"Actions":[{"Type":"custom-response","Config":{"status_code":201}}]}]}"###;
#[test]
fn test_policy_proto_serde() {
let policy: Policy = serde_json::from_str(POLICY_JSON).unwrap();
// mainly just interested in checking outbound, as that has the
// special vec serialization
assert_eq!(1, policy.outbound.len());
let outbound = &policy.outbound[0];
assert_eq!(1, outbound.actions.len());
let action = &outbound.actions[0];
assert_eq!(r#"{"status_code":201}"#.as_bytes(), action.config);
assert_eq!(serde_json::to_string(&policy).unwrap(), POLICY_JSON);
}
}
================================================
FILE: ngrok/src/internals/raw_session.rs
================================================
use std::{
collections::HashMap,
fmt::Debug,
future::Future,
io,
ops::{
Deref,
DerefMut,
},
sync::Arc,
};
use async_trait::async_trait;
use muxado::{
heartbeat::{
HeartbeatConfig,
HeartbeatCtl,
},
typed::{
StreamType,
TypedAccept,
TypedOpenClose,
TypedSession,
TypedStream,
},
Error as MuxadoError,
SessionBuilder,
};
use serde::{
de::DeserializeOwned,
Deserialize,
};
use thiserror::Error;
use tokio::{
io::{
AsyncRead,
AsyncReadExt,
AsyncWrite,
AsyncWriteExt,
},
runtime::Handle,
};
use tokio_util::either::Either;
use tracing::{
debug,
instrument,
warn,
};
use super::{
proto::{
Auth,
AuthExtra,
AuthResp,
Bind,
BindExtra,
BindOpts,
BindResp,
CommandResp,
ErrResp,
Error,
ProxyHeader,
ReadHeaderError,
Restart,
StartTunnelWithLabel,
StartTunnelWithLabelResp,
Stop,
StopTunnel,
Unbind,
UnbindResp,
Update,
PROXY_REQ,
RESTART_REQ,
STOP_REQ,
STOP_TUNNEL_REQ,
UPDATE_REQ,
VERSION,
},
rpc::RpcRequest,
};
use crate::{
tunnel::AcceptError::ListenerClosed,
Session,
};
/// Errors arising from tunneling protocol RPC calls.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum RpcError {
/// Failed to open a new stream to start the RPC call.
#[error("failed to open muxado stream")]
Open(#[source] MuxadoError),
/// Some non-Open transport error occurred
#[error("transport error")]
Transport(#[source] MuxadoError),
/// Failed to send the request over the stream.
#[error("error sending rpc request")]
Send(#[source] io::Error),
/// Failed to read the RPC response from the stream.
#[error("error reading rpc response")]
Receive(#[source] io::Error),
/// The RPC response was invalid.
#[error("failed to deserialize rpc response")]
InvalidResponse(#[from] serde_json::Error),
/// There was an error in the RPC response.
#[error("rpc error response:\n{0}")]
Response(ErrResp),
}
impl Error for RpcError {
fn error_code(&self) -> Option<&str> {
match self {
RpcError::Response(resp) => resp.error_code(),
_ => None,
}
}
fn msg(&self) -> String {
match self {
RpcError::Response(resp) => resp.msg(),
_ => format!("{self}"),
}
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum StartSessionError {
#[error("failed to start heartbeat task")]
StartHeartbeat(#[from] io::Error),
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum AcceptError {
#[error("transport error when accepting connection")]
Transport(#[from] MuxadoError),
#[error(transparent)]
Header(#[from] ReadHeaderError),
#[error("invalid stream type: {0}")]
InvalidType(StreamType),
}
pub struct RpcClient {
// This is held so that the heartbeat task doesn't get shutdown. Eventually
// we may use it to request heartbeats via the `Session`.
_heartbeat: HeartbeatCtl,
open: Box<dyn TypedOpenClose + Send>,
}
pub struct IncomingStreams {
runtime: Handle,
handlers: CommandHandlers,
pub(crate) session: Option<Session>,
accept: Box<dyn TypedAccept + Send>,
}
pub struct RawSession {
client: RpcClient,
incoming: IncomingStreams,
}
impl Deref for RawSession {
type Target = RpcClient;
fn deref(&self) -> &Self::Target {
&self.client
}
}
impl DerefMut for RawSession {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.client
}
}
/// Trait for a type that can handle a command from the ngrok dashboard.
#[async_trait]
pub trait CommandHandler<T>: Send + Sync + 'static {
/// Handle the remote command.
async fn handle_command(&self, req: T) -> Result<(), String>;
}
#[async_trait]
impl<R, T, F> CommandHandler<R> for T
where
R: Send + 'static,
T: Fn(R) -> F + Send + Sync + 'static,
F: Future<Output = Result<(), String>> + Send,
{
async fn handle_command(&self, req: R) -> Result<(), String> {
self(req).await
}
}
#[derive(Default, Clone)]
pub struct CommandHandlers {
pub on_restart: Option<Arc<dyn CommandHandler<Restart>>>,
pub on_update: Option<Arc<dyn CommandHandler<Update>>>,
pub on_stop: Option<Arc<dyn CommandHandler<Stop>>>,
}
impl RawSession {
pub async fn start<S, H>(
io_stream: S,
heartbeat: HeartbeatConfig,
handlers: H,
) -> Result<Self, StartSessionError>
where
S: AsyncRead + AsyncWrite + Send + 'static,
H: Into<Option<CommandHandlers>>,
{
let mux_sess = SessionBuilder::new(io_stream).start();
let handlers = handlers.into().unwrap_or_default();
let typed = muxado::typed::Typed::new(mux_sess);
let (heartbeat, hbctl) = muxado::heartbeat::Heartbeat::start(typed, heartbeat).await?;
let (open, accept) = heartbeat.split_typed();
let runtime = Handle::current();
let sess = RawSession {
client: RpcClient {
_heartbeat: hbctl,
open: Box::new(open),
},
incoming: IncomingStreams {
runtime,
handlers,
session: None,
accept: Box::new(accept),
},
};
Ok(sess)
}
pub fn split(self) -> (RpcClient, IncomingStreams) {
(self.client, self.incoming)
}
}
impl RpcClient {
#[instrument(level = "debug", skip(self))]
async fn rpc<R: RpcRequest>(&mut self, req: R) -> Result<R::Response, RpcError> {
let mut stream = self
.open
.open_typed(R::TYPE)
.await
.map_err(RpcError::Open)?;
let s = serde_json::to_string(&req)
// This should never happen, since we control the request types and
// know that they will always serialize correctly. Just in case
// though, call them "Send" errors.
.map_err(io::Error::other)
.map_err(RpcError::Send)?;
stream
.write_all(s.as_bytes())
.await
.map_err(RpcError::Send)?;
let mut buf = Vec::new();
stream
.read_to_end(&mut buf)
.await
.map_err(RpcError::Receive)?;
#[derive(Debug, Deserialize)]
struct ErrResp {
#[serde(rename = "Error")]
error: String,
}
let ok_resp = serde_json::from_slice::<R::Response>(&buf);
let err_resp = serde_json::from_slice::<ErrResp>(&buf);
if let Ok(err) = err_resp {
if !err.error.is_empty() {
debug!(?err, "decoded rpc error response");
return Err(RpcError::Response(err.error.as_str().into()));
}
}
debug!(resp = ?ok_resp, "decoded rpc response");
Ok(ok_resp?)
}
/// Close the raw ngrok session with a "None" muxado error.
pub async fn close(&mut self) -> Result<(), RpcError> {
self.open
.close(MuxadoError::None, "".into())
.await
.map_err(RpcError::Transport)?;
Ok(())
}
#[instrument(level = "debug", skip(self))]
pub async fn auth(
&mut self,
id: impl Into<String> + Debug,
extra: AuthExtra,
) -> Result<AuthResp, RpcError> {
let id = id.into();
let req = Auth {
client_id: id.clone(),
extra,
version: VERSION.iter().map(|&x| x.into()).collect(),
};
let resp = self.rpc(req).await?;
Ok(resp)
}
#[instrument(level = "debug", skip(self))]
pub async fn listen(
&mut self,
protocol: impl Into<String> + Debug,
opts: BindOpts,
extra: BindExtra,
id: impl Into<String> + Debug,
forwards_to: impl Into<String> + Debug,
forwards_proto: impl Into<String> + Debug,
) -> Result<BindResp<BindOpts>, RpcError> {
// Sorry, this is awful. Serde untagged unions are pretty fraught and
// hard to debug, so we're using this macro to specialize this call
// based on the enum variant. It drops down to the type wrapped in the
// enum for the actual request/response, and then re-wraps it on the way
// back out in the same variant.
// It's probably an artifact of the go -> rust translation, and could be
// fixed with enough refactoring and rearchitecting. But it works well
// enough for now and is pretty localized.
macro_rules! match_variant {
($v:expr, $($var:tt),*) => {
match opts {
$(BindOpts::$var (opts) => {
let req = Bind {
client_id: id.into(),
proto: protocol.into(),
forwards_to: forwards_to.into(),
forwards_proto: forwards_proto.into(),
opts,
extra,
};
let resp = self.rpc(req).await?;
BindResp {
bind_opts: BindOpts::$var(resp.bind_opts),
client_id: resp.client_id,
url: resp.url,
extra: resp.extra,
proto: resp.proto,
}
})*
}
};
}
Ok(match_variant!(opts, Http, Tcp, Tls))
}
#[instrument(level = "debug", skip(self))]
pub async fn listen_label(
&mut self,
labels: HashMap<String, String>,
metadata: impl Into<String> + Debug,
forwards_to: impl Into<String> + Debug,
forwards_proto: impl Into<String> + Debug,
) -> Result<StartTunnelWithLabelResp, RpcError> {
let req = StartTunnelWithLabel {
labels,
metadata: metadata.into(),
forwards_to: forwards_to.into(),
forwards_proto: forwards_proto.into(),
};
self.rpc(req).await
}
#[instrument(level = "debug", skip(self))]
pub async fn unlisten(
&mut self,
id: impl Into<String> + Debug,
) -> Result<UnbindResp, RpcError> {
self.rpc(Unbind {
client_id: id.into(),
})
.await
}
}
pub const NOT_IMPLEMENTED: &str = "the agent has not defined a callback for this operation";
async fn read_req<T>(stream: &mut TypedStream) -> Result<T, Either<io::Error, serde_json::Error>>
where
T: DeserializeOwned + Debug + 'static,
{
debug!("reading request from stream");
let mut buf = vec![];
let req = serde_json::from_value(loop {
let mut tmp = vec![0u8; 256];
let bytes = stream.read(&mut tmp).await.map_err(Either::Left)?;
buf.extend_from_slice(&tmp[..bytes]);
if let Ok(obj) = serde_json::from_slice::<serde_json::Value>(&buf) {
break obj;
}
})
.map_err(Either::Right)?;
debug!(?req, "read request from stream");
Ok(req)
}
async fn handle_req<T>(
handler: Option<Arc<dyn CommandHandler<T>>>,
mut stream: TypedStream,
) -> Result<(), Either<io::Error, serde_json::Error>>
where
T: DeserializeOwned + Debug + 'static,
{
let res = async {
let req = read_req(&mut stream).await?;
let resp = if let Some(handler) = handler {
debug!("running command handler");
handler.handle_command(req).await.err()
} else {
Some(NOT_IMPLEMENTED.into())
};
debug!(?resp, "writing response to stream");
let resp_json = serde_json::to_vec(&CommandResp { error: resp }).map_err(Either::Right)?;
stream
.write_all(resp_json.as_slice())
.await
.map_err(Either::Left)?;
Ok(())
}
.await;
if let Err(e) = &res {
warn!(?e, "error when handling
gitextract_5pce2p9h/ ├── .envrc ├── .github/ │ └── workflows/ │ ├── ci.yml │ ├── docs.yml │ ├── publish.yml │ ├── release.yml │ └── rust-cache/ │ └── action.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── cargo-doc-ngrok/ │ ├── Cargo.toml │ └── src/ │ └── main.rs ├── flake.nix ├── ngrok/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── assets/ │ │ ├── ngrok.ca.crt │ │ ├── policy-inbound.json │ │ └── policy.json │ ├── examples/ │ │ ├── axum.rs │ │ ├── connect.rs │ │ ├── domain.crt │ │ ├── domain.key │ │ ├── labeled.rs │ │ ├── mingrok.rs │ │ └── tls.rs │ └── src/ │ ├── config/ │ │ ├── common.rs │ │ ├── headers.rs │ │ ├── http.rs │ │ ├── labeled.rs │ │ ├── oauth.rs │ │ ├── oidc.rs │ │ ├── policies.rs │ │ ├── tcp.rs │ │ ├── tls.rs │ │ └── webhook_verification.rs │ ├── conn.rs │ ├── forwarder.rs │ ├── internals/ │ │ ├── proto.rs │ │ ├── raw_session.rs │ │ └── rpc.rs │ ├── lib.rs │ ├── online_tests.rs │ ├── proxy_proto.rs │ ├── session.rs │ ├── tunnel.rs │ └── tunnel_ext.rs └── rustfmt.toml
SYMBOL INDEX (608 symbols across 26 files)
FILE: cargo-doc-ngrok/src/main.rs
type Cargo (line 38) | struct Cargo {
type Cmd (line 44) | enum Cmd {
type DocNgrok (line 49) | struct DocNgrok {
function main (line 64) | async fn main() -> Result<(), BoxError> {
function make_watcher (line 146) | fn make_watcher(
FILE: ngrok/examples/axum.rs
function main (line 28) | async fn main() -> Result<(), BoxError> {
constant POLICY_JSON (line 111) | const POLICY_JSON: &str = r###"{
constant POLICY_YAML (line 136) | const POLICY_YAML: &str = r###"
function create_policy (line 158) | fn create_policy() -> Result<Policy, InvalidPolicy> {
function unwrap_infallible (line 184) | fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
FILE: ngrok/examples/connect.rs
function main (line 13) | async fn main() -> anyhow::Result<()> {
function handle_tunnel (line 45) | fn handle_tunnel(mut tunnel: impl EndpointInfo + Tunnel, sess: ngrok::Se...
FILE: ngrok/examples/labeled.rs
function main (line 29) | async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
function unwrap_infallible (line 84) | fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
FILE: ngrok/examples/mingrok.rs
function main (line 17) | async fn main() -> Result<(), Error> {
FILE: ngrok/examples/tls.rs
constant CERT (line 28) | const CERT: &[u8] = include_bytes!("domain.crt");
constant KEY (line 29) | const KEY: &[u8] = include_bytes!("domain.key");
function main (line 33) | async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
function unwrap_infallible (line 92) | fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
FILE: ngrok/src/config/common.rs
type Binding (line 30) | pub enum Binding {
method as_str (line 41) | pub fn as_str(&self) -> &'static str {
method validate (line 50) | pub(crate) fn validate(s: &str) -> Result<(), String> {
type Err (line 68) | type Err = String;
method from_str (line 70) | fn from_str(s: &str) -> Result<Self, Self::Err> {
method fmt (line 84) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
method from (line 62) | fn from(binding: Binding) -> String {
function default_forwards_to (line 89) | pub(crate) fn default_forwards_to() -> &'static str {
type TunnelBuilder (line 110) | pub trait TunnelBuilder: From<Session> {
method listen (line 115) | async fn listen(&self) -> Result<Self::Tunnel, RpcError>;
type ForwarderBuilder (line 121) | pub trait ForwarderBuilder: TunnelBuilder {
method listen_and_forward (line 126) | async fn listen_and_forward(&self, to_url: Url) -> Result<Forwarder<Se...
type TunnelConfig (line 189) | pub(crate) trait TunnelConfig {
method forwards_to (line 193) | fn forwards_to(&self) -> String;
method forwards_proto (line 195) | fn forwards_proto(&self) -> String;
method verify_upstream_tls (line 197) | fn verify_upstream_tls(&self) -> bool;
method extra (line 199) | fn extra(&self) -> BindExtra;
method proto (line 201) | fn proto(&self) -> String;
method opts (line 203) | fn opts(&self) -> Option<BindOpts>;
method labels (line 205) | fn labels(&self) -> HashMap<String, String>;
method forwards_to (line 213) | fn forwards_to(&self) -> String {
method forwards_proto (line 217) | fn forwards_proto(&self) -> String {
method verify_upstream_tls (line 220) | fn verify_upstream_tls(&self) -> bool {
method extra (line 223) | fn extra(&self) -> BindExtra {
method proto (line 226) | fn proto(&self) -> String {
method opts (line 229) | fn opts(&self) -> Option<BindOpts> {
method labels (line 232) | fn labels(&self) -> HashMap<String, String> {
type CidrRestrictions (line 239) | pub(crate) struct CidrRestrictions {
method allow (line 247) | pub(crate) fn allow(&mut self, cidr: impl Into<String>) {
method deny (line 250) | pub(crate) fn deny(&mut self, cidr: impl Into<String>) {
type CommonOpts (line 257) | pub(crate) struct CommonOpts {
method ip_restriction (line 283) | pub(crate) fn ip_restriction(&self) -> Option<IpRestriction> {
method for_forwarding_to (line 288) | pub(crate) fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self {
method set_verify_upstream_tls (line 293) | pub(crate) fn set_verify_upstream_tls(&mut self, verify_upstream_tls: ...
method verify_upstream_tls (line 297) | pub(crate) fn verify_upstream_tls(&self) -> bool {
method from (line 304) | fn from(cr: CidrRestrictions) -> Self {
method from (line 313) | fn from(b: &[bytes::Bytes]) -> Self {
FILE: ngrok/src/config/headers.rs
type Headers (line 7) | pub(crate) struct Headers {
method add (line 15) | pub(crate) fn add(&mut self, name: impl Into<String>, value: impl Into...
method remove (line 18) | pub(crate) fn remove(&mut self, name: impl Into<String>) {
method has_entries (line 21) | pub(crate) fn has_entries(&self) -> bool {
method from (line 28) | fn from(headers: Headers) -> Self {
FILE: ngrok/src/config/http.rs
type InvalidSchemeString (line 53) | pub struct InvalidSchemeString(String);
type Scheme (line 59) | pub enum Scheme {
type Err (line 68) | type Err = InvalidSchemeString;
method from_str (line 69) | fn from_str(s: &str) -> Result<Self, Self::Err> {
type UaFilter (line 81) | pub(crate) struct UaFilter {
method allow (line 90) | pub(crate) fn allow(&mut self, allow: impl Into<String>) {
method deny (line 93) | pub(crate) fn deny(&mut self, deny: impl Into<String>) {
method from (line 99) | fn from(ua: UaFilter) -> Self {
type HttpOptions (line 109) | struct HttpOptions {
method user_agent_filter (line 130) | fn user_agent_filter(&self) -> Option<UserAgentFilter> {
method forwards_to (line 137) | fn forwards_to(&self) -> String {
method forwards_proto (line 144) | fn forwards_proto(&self) -> String {
method verify_upstream_tls (line 148) | fn verify_upstream_tls(&self) -> bool {
method extra (line 152) | fn extra(&self) -> BindExtra {
method proto (line 161) | fn proto(&self) -> String {
method opts (line 167) | fn opts(&self) -> Option<BindOpts> {
method labels (line 207) | fn labels(&self) -> HashMap<String, String> {
method from (line 214) | fn from(v: &[(String, String)]) -> Self {
method from (line 223) | fn from(b: (String, String)) -> Self {
method allow_cidr (line 243) | pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
method deny_cidr (line 250) | pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
method proxy_proto (line 255) | pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
method metadata (line 262) | pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
method binding (line 297) | pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
method forwards_to (line 316) | pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Se...
method app_protocol (line 322) | pub fn app_protocol(&mut self, app_protocol: impl Into<String>) -> &mut ...
method verify_upstream_tls (line 328) | pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut...
method scheme (line 336) | pub fn scheme(&mut self, scheme: Scheme) -> &mut Self {
method domain (line 344) | pub fn domain(&mut self, domain: impl Into<String>) -> &mut Self {
method mutual_tlsca (line 354) | pub fn mutual_tlsca(&mut self, mutual_tlsca: Bytes) -> &mut Self {
method compression (line 361) | pub fn compression(&mut self) -> &mut Self {
method websocket_tcp_conversion (line 368) | pub fn websocket_tcp_conversion(&mut self) -> &mut Self {
method circuit_breaker (line 376) | pub fn circuit_breaker(&mut self, circuit_breaker: f64) -> &mut Self {
method host_header_rewrite (line 387) | pub fn host_header_rewrite(&mut self, rewrite: bool) -> &mut Self {
method request_header (line 395) | pub fn request_header(
method response_header (line 406) | pub fn response_header(
method remove_request_header (line 417) | pub fn remove_request_header(&mut self, name: impl Into<String>) -> &mut...
method remove_response_header (line 424) | pub fn remove_response_header(&mut self, name: impl Into<String>) -> &mu...
method basic_auth (line 433) | pub fn basic_auth(
method oauth (line 447) | pub fn oauth(&mut self, oauth: impl Borrow<OauthOptions>) -> &mut Self {
method oidc (line 455) | pub fn oidc(&mut self, oidc: impl Borrow<OidcOptions>) -> &mut Self {
method webhook_verification (line 463) | pub fn webhook_verification(
method allow_user_agent (line 478) | pub fn allow_user_agent(&mut self, regex: impl Into<String>) -> &mut Self {
method deny_user_agent (line 485) | pub fn deny_user_agent(&mut self, regex: impl Into<String>) -> &mut Self {
method policy (line 491) | pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
method traffic_policy (line 500) | pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut ...
method for_forwarding_to (line 505) | pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut S...
method pooling_enabled (line 514) | pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &...
constant METADATA (line 524) | const METADATA: &str = "testmeta";
constant TEST_FORWARD (line 525) | const TEST_FORWARD: &str = "testforward";
constant TEST_FORWARD_PROTO (line 526) | const TEST_FORWARD_PROTO: &str = "http2";
constant ALLOW_CIDR (line 527) | const ALLOW_CIDR: &str = "0.0.0.0/0";
constant DENY_CIDR (line 528) | const DENY_CIDR: &str = "10.1.1.1/32";
constant CA_CERT (line 529) | const CA_CERT: &[u8] = "test ca cert".as_bytes();
constant CA_CERT2 (line 530) | const CA_CERT2: &[u8] = "test ca cert2".as_bytes();
constant DOMAIN (line 531) | const DOMAIN: &str = "test domain";
constant ALLOW_AGENT (line 532) | const ALLOW_AGENT: &str = r"bar/(\d)+";
constant DENY_AGENT (line 533) | const DENY_AGENT: &str = r"foo/(\d)+";
function test_interface_to_proto (line 536) | fn test_interface_to_proto() {
function tunnel_test (line 585) | fn tunnel_test<C>(tunnel_cfg: C)
function test_binding_valid_values (line 665) | fn test_binding_valid_values() {
function test_binding_invalid_value (line 702) | fn test_binding_invalid_value() {
function test_binding_called_twice (line 712) | fn test_binding_called_twice() {
function test_binding_with_domain (line 722) | fn test_binding_with_domain() {
FILE: ngrok/src/config/labeled.rs
type LabeledOptions (line 27) | struct LabeledOptions {
method forwards_to (line 33) | fn forwards_to(&self) -> String {
method forwards_proto (line 40) | fn forwards_proto(&self) -> String {
method verify_upstream_tls (line 44) | fn verify_upstream_tls(&self) -> bool {
method extra (line 48) | fn extra(&self) -> BindExtra {
method proto (line 57) | fn proto(&self) -> String {
method opts (line 60) | fn opts(&self) -> Option<BindOpts> {
method labels (line 63) | fn labels(&self) -> HashMap<String, String> {
method metadata (line 78) | pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
method label (line 86) | pub fn label(&mut self, label: impl Into<String>, value: impl Into<Strin...
method forwards_to (line 99) | pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Se...
method app_protocol (line 105) | pub fn app_protocol(&mut self, app_protocol: impl Into<String>) -> &mut ...
method verify_upstream_tls (line 111) | pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut...
method for_forwarding_to (line 118) | pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut S...
constant METADATA (line 128) | const METADATA: &str = "testmeta";
constant LABEL_KEY (line 129) | const LABEL_KEY: &str = "edge";
constant LABEL_VAL (line 130) | const LABEL_VAL: &str = "edghts_2IC6RJ6CQnuh7waciWyaGKc50Nt";
function test_interface_to_proto (line 133) | fn test_interface_to_proto() {
function tunnel_test (line 147) | fn tunnel_test<C>(tunnel_cfg: &C)
FILE: ngrok/src/config/oauth.rs
type OauthOptions (line 10) | pub struct OauthOptions {
method new (line 29) | pub fn new(provider: impl Into<String>) -> Self {
method client_id (line 37) | pub fn client_id(&mut self, id: impl Into<String>) -> &mut Self {
method client_secret (line 43) | pub fn client_secret(&mut self, secret: impl Into<String>) -> &mut Self {
method allow_email (line 49) | pub fn allow_email(&mut self, email: impl Into<String>) -> &mut Self {
method allow_domain (line 54) | pub fn allow_domain(&mut self, domain: impl Into<String>) -> &mut Self {
method scope (line 59) | pub fn scope(&mut self, scope: impl Into<String>) -> &mut Self {
method from (line 67) | fn from(o: OauthOptions) -> Self {
FILE: ngrok/src/config/oidc.rs
type OidcOptions (line 10) | pub struct OidcOptions {
method new (line 21) | pub fn new(
method allow_email (line 35) | pub fn allow_email(&mut self, email: impl Into<String>) -> &mut Self {
method allow_domain (line 40) | pub fn allow_domain(&mut self, domain: impl Into<String>) -> &mut Self {
method scope (line 45) | pub fn scope(&mut self, scope: impl Into<String>) -> &mut Self {
method from (line 53) | fn from(o: OidcOptions) -> Self {
FILE: ngrok/src/config/policies.rs
type Policy (line 18) | pub struct Policy {
method new (line 54) | pub fn new() -> Self {
method from_json (line 61) | fn from_json(json: impl AsRef<str>) -> Result<Self, InvalidPolicy> {
method from_file (line 66) | pub fn from_file(json_file_path: impl AsRef<str>) -> Result<Self, Inva...
method to_json (line 75) | pub fn to_json(&self) -> Result<String, InvalidPolicy> {
method add_inbound (line 80) | pub fn add_inbound(&mut self, rule: impl Into<Rule>) -> &mut Self {
method add_outbound (line 86) | pub fn add_outbound(&mut self, rule: impl Into<Rule>) -> &mut Self {
type Error (line 93) | type Error = InvalidPolicy;
method try_from (line 95) | fn try_from(other: &Policy) -> Result<Policy, Self::Error> {
type Error (line 101) | type Error = InvalidPolicy;
method try_from (line 103) | fn try_from(other: Result<Policy, InvalidPolicy>) -> Result<Policy, Se...
type Error (line 109) | type Error = InvalidPolicy;
method try_from (line 111) | fn try_from(other: &str) -> Result<Policy, Self::Error> {
type Rule (line 26) | pub struct Rule {
method new (line 118) | pub fn new(name: impl Into<String>) -> Self {
method to_json (line 126) | pub fn to_json(&self) -> Result<String, InvalidPolicy> {
method add_expression (line 131) | pub fn add_expression(&mut self, expression: impl Into<String>) -> &mu...
method add_action (line 137) | pub fn add_action(&mut self, action: Action) -> &mut Self {
method from (line 144) | fn from(other: &mut Rule) -> Self {
type Action (line 35) | pub struct Action {
method new (line 151) | pub fn new(type_: impl Into<String>, config: Option<&str>) -> Result<S...
method to_json (line 161) | pub fn to_json(&self) -> Result<String, InvalidPolicy> {
type InvalidPolicy (line 43) | pub enum InvalidPolicy {
function from (line 167) | fn from(value: Policy) -> Self {
function from (line 174) | fn from(o: Policy) -> Self {
function from (line 183) | fn from(p: Rule) -> Self {
function from (line 193) | fn from(a: Action) -> Self {
constant POLICY_JSON (line 208) | pub(crate) const POLICY_JSON: &str = r###"
function test_json_to_policy (line 226) | fn test_json_to_policy() {
function test_empty_json_to_policy (line 252) | fn test_empty_json_to_policy() {
function test_policy_to_json (line 259) | fn test_policy_to_json() {
function test_policy_to_json_error (line 267) | fn test_policy_to_json_error() {
function test_rule_to_json (line 273) | fn test_rule_to_json() {
function test_action_to_json (line 294) | fn test_action_to_json() {
function test_builders (line 305) | fn test_builders() {
function test_load_file (line 326) | fn test_load_file() {
function test_load_inbound_file (line 335) | fn test_load_inbound_file() {
function test_load_file_error (line 342) | fn test_load_file_error() {
FILE: ngrok/src/config/tcp.rs
type TcpOptions (line 36) | struct TcpOptions {
method forwards_to (line 43) | fn forwards_to(&self) -> String {
method extra (line 49) | fn extra(&self) -> BindExtra {
method proto (line 58) | fn proto(&self) -> String {
method forwards_proto (line 62) | fn forwards_proto(&self) -> String {
method verify_upstream_tls (line 67) | fn verify_upstream_tls(&self) -> bool {
method opts (line 71) | fn opts(&self) -> Option<BindOpts> {
method labels (line 91) | fn labels(&self) -> HashMap<String, String> {
method allow_cidr (line 108) | pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
method deny_cidr (line 115) | pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
method proxy_proto (line 120) | pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
method metadata (line 127) | pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
method binding (line 162) | pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
method forwards_to (line 181) | pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Se...
method verify_upstream_tls (line 187) | pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut...
method remote_addr (line 197) | pub fn remote_addr(&mut self, remote_addr: impl Into<String>) -> &mut Se...
method policy (line 203) | pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
method traffic_policy (line 212) | pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut ...
method for_forwarding_to (line 217) | pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut S...
method pooling_enabled (line 223) | pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &...
constant METADATA (line 233) | const METADATA: &str = "testmeta";
constant TEST_FORWARD (line 234) | const TEST_FORWARD: &str = "testforward";
constant REMOTE_ADDR (line 235) | const REMOTE_ADDR: &str = "4.tcp.ngrok.io:1337";
constant ALLOW_CIDR (line 236) | const ALLOW_CIDR: &str = "0.0.0.0/0";
constant DENY_CIDR (line 237) | const DENY_CIDR: &str = "10.1.1.1/32";
function test_interface_to_proto (line 240) | fn test_interface_to_proto() {
function tunnel_test (line 260) | fn tunnel_test<C>(tunnel_cfg: &C)
function test_binding_valid_values (line 289) | fn test_binding_valid_values() {
function test_binding_invalid_value (line 326) | fn test_binding_invalid_value() {
function test_binding_called_twice (line 336) | fn test_binding_called_twice() {
FILE: ngrok/src/config/tls.rs
type TlsOptions (line 35) | struct TlsOptions {
method forwards_to (line 45) | fn forwards_to(&self) -> String {
method forwards_proto (line 52) | fn forwards_proto(&self) -> String {
method verify_upstream_tls (line 57) | fn verify_upstream_tls(&self) -> bool {
method extra (line 61) | fn extra(&self) -> BindExtra {
method proto (line 70) | fn proto(&self) -> String {
method opts (line 74) | fn opts(&self) -> Option<BindOpts> {
method labels (line 107) | fn labels(&self) -> HashMap<String, String> {
method allow_cidr (line 123) | pub fn allow_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
method deny_cidr (line 130) | pub fn deny_cidr(&mut self, cidr: impl Into<String>) -> &mut Self {
method proxy_proto (line 135) | pub fn proxy_proto(&mut self, proxy_proto: ProxyProto) -> &mut Self {
method metadata (line 142) | pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
method binding (line 177) | pub fn binding(&mut self, binding: impl Into<String>) -> &mut Self {
method forwards_to (line 196) | pub fn forwards_to(&mut self, forwards_to: impl Into<String>) -> &mut Se...
method verify_upstream_tls (line 202) | pub fn verify_upstream_tls(&mut self, verify_upstream_tls: bool) -> &mut...
method domain (line 212) | pub fn domain(&mut self, domain: impl Into<String>) -> &mut Self {
method mutual_tlsca (line 223) | pub fn mutual_tlsca(&mut self, mutual_tlsca: Bytes) -> &mut Self {
method termination (line 232) | pub fn termination(&mut self, cert_pem: Bytes, key_pem: Bytes) -> &mut S...
method policy (line 239) | pub fn policy<S>(&mut self, s: S) -> Result<&mut Self, S::Error>
method traffic_policy (line 248) | pub fn traffic_policy(&mut self, policy_str: impl Into<String>) -> &mut ...
method for_forwarding_to (line 253) | pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut S...
method pooling_enabled (line 259) | pub fn pooling_enabled(&mut self, pooling_enabled: impl Into<bool>) -> &...
constant METADATA (line 270) | const METADATA: &str = "testmeta";
constant TEST_FORWARD (line 271) | const TEST_FORWARD: &str = "testforward";
constant ALLOW_CIDR (line 272) | const ALLOW_CIDR: &str = "0.0.0.0/0";
constant DENY_CIDR (line 273) | const DENY_CIDR: &str = "10.1.1.1/32";
constant CA_CERT (line 274) | const CA_CERT: &[u8] = "test ca cert".as_bytes();
constant CA_CERT2 (line 275) | const CA_CERT2: &[u8] = "test ca cert2".as_bytes();
constant KEY (line 276) | const KEY: &[u8] = "test cert".as_bytes();
constant CERT (line 277) | const CERT: &[u8] = "test cert".as_bytes();
constant DOMAIN (line 278) | const DOMAIN: &str = "test domain";
function test_interface_to_proto (line 281) | fn test_interface_to_proto() {
function tunnel_test (line 304) | fn tunnel_test<C>(tunnel_cfg: C)
function test_binding_valid_values (line 345) | fn test_binding_valid_values() {
function test_binding_invalid_value (line 382) | fn test_binding_invalid_value() {
function test_binding_called_twice (line 392) | fn test_binding_called_twice() {
FILE: ngrok/src/config/webhook_verification.rs
type WebhookVerification (line 8) | pub(crate) struct WebhookVerification {
method from (line 19) | fn from(wv: WebhookVerification) -> Self {
FILE: ngrok/src/conn.rs
type ConnInner (line 35) | pub(crate) struct ConnInner {
type Info (line 41) | pub(crate) struct Info {
type ConnInfo (line 90) | pub trait ConnInfo {
method remote_addr (line 50) | fn remote_addr(&self) -> SocketAddr {
method remote_addr (line 93) | fn remote_addr(&self) -> SocketAddr;
type EdgeConnInfo (line 97) | pub trait EdgeConnInfo {
method edge_type (line 56) | fn edge_type(&self) -> EdgeType {
method passthrough_tls (line 59) | fn passthrough_tls(&self) -> bool {
method edge_type (line 99) | fn edge_type(&self) -> EdgeType;
method passthrough_tls (line 102) | fn passthrough_tls(&self) -> bool;
type EndpointConnInfo (line 106) | pub trait EndpointConnInfo {
method proto (line 65) | fn proto(&self) -> &str {
method proto (line 108) | fn proto(&self) -> &str;
FILE: ngrok/src/forwarder.rs
type Forwarder (line 24) | pub struct Forwarder<T> {
function join (line 31) | pub fn join(&mut self) -> &mut JoinHandle<Result<(), Box<dyn StdError + ...
method close (line 41) | async fn close(&mut self) -> Result<(), RpcError> {
method id (line 50) | fn id(&self) -> &str {
method forwards_to (line 54) | fn forwards_to(&self) -> &str {
method metadata (line 58) | fn metadata(&self) -> &str {
method proto (line 67) | fn proto(&self) -> &str {
method url (line 71) | fn url(&self) -> &str {
method labels (line 80) | fn labels(&self) -> &HashMap<String, String> {
function forward (line 85) | pub(crate) fn forward<T>(mut listener: T, info: T, to_url: Url) -> Resul...
FILE: ngrok/src/internals/proto.rs
constant AUTH_REQ (line 32) | pub const AUTH_REQ: StreamType = StreamType::clamp(0);
constant BIND_REQ (line 33) | pub const BIND_REQ: StreamType = StreamType::clamp(1);
constant UNBIND_REQ (line 34) | pub const UNBIND_REQ: StreamType = StreamType::clamp(2);
constant PROXY_REQ (line 35) | pub const PROXY_REQ: StreamType = StreamType::clamp(3);
constant RESTART_REQ (line 36) | pub const RESTART_REQ: StreamType = StreamType::clamp(4);
constant STOP_REQ (line 37) | pub const STOP_REQ: StreamType = StreamType::clamp(5);
constant UPDATE_REQ (line 38) | pub const UPDATE_REQ: StreamType = StreamType::clamp(6);
constant BIND_LABELED_REQ (line 39) | pub const BIND_LABELED_REQ: StreamType = StreamType::clamp(7);
constant STOP_TUNNEL_REQ (line 40) | pub const STOP_TUNNEL_REQ: StreamType = StreamType::clamp(9);
constant VERSION (line 42) | pub const VERSION: &[&str] = &["3", "2"];
type Error (line 46) | pub trait Error: error::Error {
method error_code (line 48) | fn error_code(&self) -> Option<&str> {
method msg (line 54) | fn msg(&self) -> String {
method error_code (line 63) | fn error_code(&self) -> Option<&str> {
method msg (line 66) | fn msg(&self) -> String {
method error_code (line 75) | fn error_code(&self) -> Option<&str> {
method msg (line 78) | fn msg(&self) -> String {
method error_code (line 87) | fn error_code(&self) -> Option<&str> {
method msg (line 90) | fn msg(&self) -> String {
method error_code (line 134) | fn error_code(&self) -> Option<&str> {
method msg (line 137) | fn msg(&self) -> String {
type ErrResp (line 96) | pub struct ErrResp {
method from (line 102) | fn from(value: &'a str) -> Self {
method fmt (line 124) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
constant ERR_URL (line 121) | const ERR_URL: &str = "https://ngrok.com/docs/errors";
type Auth (line 144) | pub struct Auth {
type SecretBytes (line 152) | pub struct SecretBytes(#[serde(with = "base64bytes")] Vec<u8>);
method from (line 168) | fn from(other: &'a [u8]) -> Self {
method from (line 174) | fn from(other: Vec<u8>) -> Self {
method fmt (line 180) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method fmt (line 186) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Target (line 155) | type Target = Vec<u8>;
method deref (line 156) | fn deref(&self) -> &Self::Target {
method deref_mut (line 162) | fn deref_mut(&mut self) -> &mut Self::Target {
type SecretString (line 193) | pub struct SecretString(String);
method from (line 209) | fn from(other: &'a str) -> Self {
method from (line 215) | fn from(other: String) -> Self {
method fmt (line 221) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method fmt (line 227) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Target (line 196) | type Target = String;
method deref (line 197) | fn deref(&self) -> &Self::Target {
method deref_mut (line 203) | fn deref_mut(&mut self) -> &mut Self::Target {
type AuthExtra (line 234) | pub struct AuthExtra {
type AuthResp (line 276) | pub struct AuthResp {
type AuthRespExtra (line 287) | pub struct AuthRespExtra {
type Bind (line 299) | pub struct Bind<T> {
type BindOpts (line 312) | pub enum BindOpts {
type BindExtra (line 320) | pub struct BindExtra {
type BindResp (line 332) | pub struct BindResp<T> {
type BindRespExtra (line 345) | pub struct BindRespExtra {
type StartTunnelWithLabel (line 353) | pub struct StartTunnelWithLabel {
type StartTunnelWithLabelResp (line 362) | pub struct StartTunnelWithLabelResp {
type Unbind (line 374) | pub struct Unbind {
type UnbindResp (line 382) | pub struct UnbindResp {
type ProxyHeader (line 390) | pub struct ProxyHeader {
method read_from_stream (line 411) | pub async fn read_from_stream(
type ReadHeaderError (line 401) | pub enum ReadHeaderError {
type EdgeType (line 429) | pub enum EdgeType {
method as_str (line 453) | pub(crate) fn as_str(self) -> &'static str {
method deserialize (line 489) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
type Err (line 441) | type Err = ();
method from_str (line 442) | fn from_str(s: &str) -> Result<Self, Self::Err> {
method serialize (line 464) | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
type EdgeTypeVisitor (line 472) | struct EdgeTypeVisitor;
type Value (line 475) | type Value = EdgeType;
method expecting (line 476) | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::...
method visit_str (line 480) | fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
type Stop (line 500) | pub struct Stop {}
type CommandResp (line 506) | pub struct CommandResp {
type StopResp (line 512) | pub type StopResp = CommandResp;
type Restart (line 519) | pub struct Restart {}
type RestartResp (line 521) | pub type RestartResp = CommandResp;
type Update (line 527) | pub struct Update {
type StopTunnel (line 537) | pub struct StopTunnel {
type UpdateResp (line 547) | pub type UpdateResp = CommandResp;
type ProxyProto (line 555) | pub enum ProxyProto {
method from (line 577) | fn from(other: i64) -> Self {
method deserialize (line 637) | fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
function from (line 566) | fn from(other: ProxyProto) -> Self {
type InvalidProxyProtoString (line 589) | pub struct InvalidProxyProtoString(String);
type Err (line 592) | type Err = InvalidProxyProtoString;
method from_str (line 593) | fn from_str(s: &str) -> Result<Self, Self::Err> {
method serialize (line 605) | fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
type ProxyProtoVisitor (line 613) | struct ProxyProtoVisitor;
type Value (line 616) | type Value = ProxyProto;
method expecting (line 617) | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::...
method visit_i64 (line 621) | fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
method visit_u64 (line 628) | fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
type PolicyWrapper (line 647) | pub enum PolicyWrapper {
method from (line 654) | fn from(value: String) -> Self {
type HttpEndpoint (line 661) | pub struct HttpEndpoint {
type Compression (line 696) | pub struct Compression {}
function is_default (line 698) | fn is_default<T>(v: &T) -> bool
type CircuitBreaker (line 706) | pub struct CircuitBreaker {
type BasicAuth (line 712) | pub struct BasicAuth {
type BasicAuthCredential (line 718) | pub struct BasicAuthCredential {
type IpRestriction (line 728) | pub struct IpRestriction {
type Oauth (line 736) | pub struct Oauth {
type Oidc (line 754) | pub struct Oidc {
type WebhookVerification (line 772) | pub struct WebhookVerification {
type MutualTls (line 782) | pub struct MutualTls {
type Headers (line 791) | pub struct Headers {
type WebsocketTcpConverter (line 801) | pub struct WebsocketTcpConverter {}
type UserAgentFilter (line 804) | pub struct UserAgentFilter {
type TcpEndpoint (line 813) | pub struct TcpEndpoint {
type TlsEndpoint (line 824) | pub struct TlsEndpoint {
type TlsTermination (line 844) | pub struct TlsTermination {
type Policy (line 855) | pub struct Policy {
type Rule (line 862) | pub struct Rule {
type Action (line 870) | pub struct Action {
function serialize_policy (line 879) | fn serialize_policy<S: Serializer>(v: &Policy, s: S) -> Result<S::Ok, S:...
function serialize (line 901) | pub fn serialize<S: Serializer>(v: &[u8], s: S) -> Result<S::Ok, S::Erro...
function deserialize (line 910) | pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D...
function serialize (line 928) | pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::E...
function deserialize (line 932) | pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D...
function test_proxy_proto_serde (line 946) | fn test_proxy_proto_serde() {
constant POLICY_JSON (line 956) | pub(crate) const POLICY_JSON: &str = r###"{"Inbound":[{"Name":"test_in",...
function test_policy_proto_serde (line 959) | fn test_policy_proto_serde() {
FILE: ngrok/src/internals/raw_session.rs
type RpcError (line 89) | pub enum RpcError {
method error_code (line 111) | fn error_code(&self) -> Option<&str> {
method msg (line 118) | fn msg(&self) -> String {
type StartSessionError (line 128) | pub enum StartSessionError {
type AcceptError (line 135) | pub enum AcceptError {
type RpcClient (line 144) | pub struct RpcClient {
method rpc (line 245) | async fn rpc<R: RpcRequest>(&mut self, req: R) -> Result<R::Response, ...
method close (line 291) | pub async fn close(&mut self) -> Result<(), RpcError> {
method auth (line 300) | pub async fn auth(
method listen (line 318) | pub async fn listen(
method listen_label (line 364) | pub async fn listen_label(
method unlisten (line 382) | pub async fn unlisten(
type IncomingStreams (line 151) | pub struct IncomingStreams {
method accept (line 452) | pub async fn accept(&mut self) -> Result<TunnelStream, AcceptError> {
type RawSession (line 158) | pub struct RawSession {
method start (line 203) | pub async fn start<S, H>(
method split (line 238) | pub fn split(self) -> (RpcClient, IncomingStreams) {
type Target (line 164) | type Target = RpcClient;
method deref (line 165) | fn deref(&self) -> &Self::Target {
method deref_mut (line 171) | fn deref_mut(&mut self) -> &mut Self::Target {
type CommandHandler (line 178) | pub trait CommandHandler<T>: Send + Sync + 'static {
method handle_command (line 180) | async fn handle_command(&self, req: T) -> Result<(), String>;
method handle_command (line 190) | async fn handle_command(&self, req: R) -> Result<(), String> {
type CommandHandlers (line 196) | pub struct CommandHandlers {
constant NOT_IMPLEMENTED (line 393) | pub const NOT_IMPLEMENTED: &str = "the agent has not defined a callback ...
function read_req (line 395) | async fn read_req<T>(stream: &mut TypedStream) -> Result<T, Either<io::E...
function handle_req (line 415) | async fn handle_req<T>(
type TunnelStream (line 501) | pub struct TunnelStream {
FILE: ngrok/src/internals/rpc.rs
type RpcRequest (line 9) | pub trait RpcRequest: Serialize + Debug {
constant TYPE (line 11) | const TYPE: StreamType;
FILE: ngrok/src/online_tests.rs
function setup_session (line 90) | async fn setup_session() -> Result<Session, BoxError> {
function listen (line 96) | async fn listen() -> Result<(), BoxError> {
function tunnel (line 109) | async fn tunnel() -> Result<(), BoxError> {
type TunnelGuard (line 124) | struct TunnelGuard {
method drop (line 130) | fn drop(&mut self) {
function serve_http (line 138) | async fn serve_http(
function start_http_server (line 152) | fn start_http_server<T>(mut tun: T, router: Router) -> TunnelGuard
function defaults (line 189) | fn defaults<T>(opts: &mut T) -> &mut T {
function hello_router (line 193) | fn hello_router() -> Router {
function check_body (line 197) | async fn check_body(url: impl AsRef<str>, expected: impl AsRef<str>) -> ...
function https (line 205) | async fn https() -> Result<(), BoxError> {
function http (line 218) | async fn http() -> Result<(), BoxError> {
function http_compression (line 231) | async fn http_compression() -> Result<(), BoxError> {
function http_headers (line 260) | async fn http_headers() -> Result<(), BoxError> {
function user_agent (line 325) | async fn user_agent() -> Result<(), BoxError> {
function basic_auth (line 352) | async fn basic_auth() -> Result<(), BoxError> {
function oauth (line 378) | async fn oauth() -> Result<(), BoxError> {
function custom_domain (line 399) | async fn custom_domain() -> Result<(), BoxError> {
function policy (line 420) | async fn policy() -> Result<(), BoxError> {
function create_policy (line 435) | fn create_policy() -> Result<Policy, InvalidPolicy> {
function circuit_breaker (line 456) | async fn circuit_breaker() -> Result<(), BoxError> {
function find_subsequence (line 513) | fn find_subsequence<T>(haystack: &[T], needle: &[T]) -> Option<usize>
function http_ip_restriction (line 582) | async fn http_ip_restriction() -> Result<(), BoxError> {
function tcp_ip_restriction (line 600) | async fn tcp_ip_restriction() -> Result<(), BoxError> {
function websocket_conversion (line 623) | async fn websocket_conversion() -> Result<(), BoxError> {
function tcp (line 672) | async fn tcp() -> Result<(), BoxError> {
constant CERT (line 690) | const CERT: &[u8] = include_bytes!("../examples/domain.crt");
constant KEY (line 691) | const KEY: &[u8] = include_bytes!("../examples/domain.key");
function tls (line 696) | async fn tls() -> Result<(), BoxError> {
function app_protocol (line 744) | async fn app_protocol() -> Result<(), BoxError> {
function verify_upstream_tls (line 765) | async fn verify_upstream_tls() -> Result<(), BoxError> {
function session_root_cas (line 786) | async fn session_root_cas() -> Result<(), BoxError> {
function session_ca_cert (line 840) | async fn session_ca_cert() -> Result<(), BoxError> {
function session_tls_config (line 865) | async fn session_tls_config() -> Result<(), BoxError> {
function tls_client_config (line 879) | fn tls_client_config() -> Result<Arc<ClientConfig>, &'static io::Error> {
function connect_proxy_http (line 897) | async fn connect_proxy_http() -> Result<(), BoxError> {
function run_proxy (line 951) | pub async fn run_proxy(listener: tokio::net::TcpListener, shutdown: Canc...
function proxy (line 978) | async fn proxy(
function host_addr (line 1040) | fn host_addr(uri: &http::Uri) -> Option<String> {
function empty (line 1044) | fn empty() -> BoxBody<Bytes, hyper::Error> {
function full (line 1050) | fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
function tunnel (line 1058) | async fn tunnel(upgraded: Upgraded, addr: String) -> std::io::Result<()> {
function forward_proxy_protocol_tls (line 1080) | async fn forward_proxy_protocol_tls() -> Result<(), BoxError> {
function unwrap_infallible (line 1132) | fn unwrap_infallible<T>(result: Result<T, Infallible>) -> T {
FILE: ngrok/src/proxy_proto.rs
constant MAX_HEADER_LEN (line 32) | const MAX_HEADER_LEN: usize = 536;
constant MIN_HEADER_LEN (line 34) | const MIN_HEADER_LEN: usize = 16;
type ReadState (line 37) | enum ReadState {
method new (line 45) | fn new() -> ReadState {
method header (line 49) | fn header(&self) -> Result<Option<&ProxyHeader>, &ParseError> {
method poll_read_header_once (line 60) | fn poll_read_header_once(
type WriteState (line 150) | enum WriteState {
method new (line 156) | fn new(hdr: proxy_protocol::ProxyHeader) -> Result<WriteState, proxy_p...
method poll_write_header_once (line 163) | fn poll_write_header_once(
type Stream (line 200) | pub struct Stream<S> {
function outgoing (line 208) | pub fn outgoing(stream: S, header: ProxyHeader) -> Result<Self, proxy_pr...
function incoming (line 216) | pub fn incoming(stream: S) -> Self {
function disabled (line 224) | pub fn disabled(stream: S) -> Self {
function proxy_header (line 238) | pub async fn proxy_header(&mut self) -> io::Result<Result<Option<&ProxyH...
method poll_read (line 259) | fn poll_read(
method poll_write (line 292) | fn poll_write(
method poll_flush (line 305) | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result...
method poll_shutdown (line 308) | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Res...
method poll_write (line 327) | fn poll_write(
method poll_flush (line 334) | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result...
method poll_shutdown (line 337) | fn poll_shutdown(
method poll_read (line 349) | fn poll_read(
type ShortReader (line 398) | struct ShortReader<S> {
method poll_read (line 409) | fn poll_read(
function new (line 430) | fn new(inner: S, min: usize, max: usize) -> Self {
constant INPUT (line 435) | const INPUT: &str = "PROXY TCP4 192.168.0.1 192.168.0.11 56324 443\r\n";
constant PARTIAL_INPUT (line 436) | const PARTIAL_INPUT: &str = "PROXY TCP4 192.168.0.1";
constant FINAL_INPUT (line 437) | const FINAL_INPUT: &str = " 192.168.0.11 56324 443\r\n";
function test_proxy_protocol (line 442) | fn test_proxy_protocol() {
function test_header_stream_v2 (line 458) | async fn test_header_stream_v2() {
function test_header_stream (line 519) | async fn test_header_stream() {
function test_noheader (line 572) | async fn test_noheader() {
FILE: ngrok/src/session.rs
constant CERT_BYTES (line 126) | pub(crate) const CERT_BYTES: &[u8] = include_bytes!("../assets/ngrok.ca....
constant CLIENT_TYPE (line 127) | const CLIENT_TYPE: &str = "ngrok-rust";
constant VERSION (line 128) | const VERSION: &str = env!("CARGO_PKG_VERSION");
type BoundTunnel (line 131) | struct BoundTunnel {
type TunnelConns (line 142) | type TunnelConns = HashMap<String, BoundTunnel>;
type Session (line 149) | pub struct Session {
method builder (line 852) | pub fn builder() -> SessionBuilder {
method http_endpoint (line 859) | pub fn http_endpoint(&self) -> HttpTunnelBuilder {
method tcp_endpoint (line 866) | pub fn tcp_endpoint(&self) -> TcpTunnelBuilder {
method tls_endpoint (line 873) | pub fn tls_endpoint(&self) -> TlsTunnelBuilder {
method labeled_tunnel (line 880) | pub fn labeled_tunnel(&self) -> LabeledTunnelBuilder {
method id (line 885) | pub fn id(&self) -> String {
method start_tunnel (line 896) | pub(crate) async fn start_tunnel<C>(&self, tunnel_cfg: C) -> Result<Tu...
method close_tunnel_with_error (line 1001) | pub(crate) async fn close_tunnel_with_error(&self, id: impl AsRef<str>...
method close_tunnel (line 1010) | pub async fn close_tunnel(&self, id: impl AsRef<str>) -> Result<(), Rp...
method runtime (line 1018) | pub(crate) fn runtime(&self) -> Handle {
method close (line 1023) | pub async fn close(&mut self) -> Result<(), RpcError> {
type SessionInner (line 156) | struct SessionInner {
type IoStream (line 169) | pub trait IoStream: AsyncRead + AsyncWrite + Unpin + Send + 'static {}
type Connector (line 174) | pub trait Connector: Sync + Send + 'static {
method connect (line 185) | async fn connect(
method connect (line 200) | async fn connect(
function default_connect (line 218) | pub async fn default_connect(
type ProxyUnsupportedError (line 242) | pub struct ProxyUnsupportedError(Url);
function connect_proxy (line 244) | fn connect_proxy(url: Url) -> Result<Arc<dyn Connector>, ProxyUnsupporte...
function connect_http_proxy (line 256) | fn connect_http_proxy(url: Url) -> impl Connector {
function connect_socks_proxy (line 292) | fn connect_socks_proxy(proxy_addr: String) -> impl Connector {
type SessionBuilder (line 320) | pub struct SessionBuilder {
method authtoken (line 452) | pub fn authtoken(&mut self, authtoken: impl Into<String>) -> &mut Self {
method authtoken_from_env (line 458) | pub fn authtoken_from_env(&mut self) -> &mut Self {
method heartbeat_interval (line 470) | pub fn heartbeat_interval(
method heartbeat_tolerance (line 487) | pub fn heartbeat_tolerance(
method metadata (line 505) | pub fn metadata(&mut self, metadata: impl Into<String>) -> &mut Self {
method server_addr (line 516) | pub fn server_addr(&mut self, addr: impl Into<String>) -> Result<&mut ...
method root_cas (line 543) | pub fn root_cas(&mut self, root_cas: impl Into<String>) -> Result<&mut...
method ca_cert (line 561) | pub fn ca_cert(&mut self, ca_cert: Bytes) -> &mut Self {
method tls_config (line 575) | pub fn tls_config(&mut self, config: rustls::ClientConfig) -> &mut Self {
method connector (line 584) | pub fn connector(&mut self, connect: impl Connector) -> &mut Self {
method proxy_url (line 596) | pub fn proxy_url(&mut self, url: Url) -> Result<&mut Self, ProxyUnsupp...
method handle_stop_command (line 611) | pub fn handle_stop_command(&mut self, handler: impl CommandHandler<Sto...
method handle_restart_command (line 627) | pub fn handle_restart_command(&mut self, handler: impl CommandHandler<...
method handle_update_command (line 643) | pub fn handle_update_command(&mut self, handler: impl CommandHandler<U...
method handle_heartbeat (line 652) | pub fn handle_heartbeat(&mut self, callback: impl HeartbeatHandler) ->...
method client_info (line 666) | pub fn client_info(
method connect (line 683) | pub async fn connect(&self) -> Result<Session, ConnectError> {
method get_or_create_tls_config (line 707) | pub(crate) fn get_or_create_tls_config(&self) -> rustls::ClientConfig {
method connect_inner (line 735) | async fn connect_inner(
type ConnectError (line 342) | pub enum ConnectError {
method error_code (line 377) | fn error_code(&self) -> Option<&str> {
method msg (line 383) | fn msg(&self) -> String {
type InvalidHeartbeatInterval (line 397) | pub struct InvalidHeartbeatInterval(u128);
type InvalidHeartbeatTolerance (line 404) | pub struct InvalidHeartbeatTolerance(u128);
type InvalidServerAddr (line 409) | pub struct InvalidServerAddr(String);
method default (line 412) | fn default() -> Self {
function sanitize_ua_string (line 434) | fn sanitize_ua_string(s: impl AsRef<str>) -> String {
function host_certs_tls_config (line 1031) | pub(crate) fn host_certs_tls_config() -> Result<rustls::ClientConfig, &'...
function accept_one (line 1048) | async fn accept_one(
function try_reconnect (line 1119) | async fn try_reconnect(
function accept_incoming (line 1174) | async fn accept_incoming(mut incoming: IncomingStreams, inner: Arc<ArcSw...
function test_sanitize_ua (line 1217) | fn test_sanitize_ua() {
FILE: ngrok/src/tunnel.rs
type AcceptError (line 37) | pub enum AcceptError {
type TunnelInnerInfo (line 55) | pub(crate) struct TunnelInnerInfo {
type TunnelInner (line 64) | pub(crate) struct TunnelInner {
method id (line 173) | pub fn id(&self) -> &str {
method url (line 179) | pub fn url(&self) -> &str {
method close (line 185) | pub async fn close(&mut self) -> Result<(), RpcError> {
method proto (line 194) | pub fn proto(&self) -> &str {
method labels (line 200) | pub fn labels(&self) -> &HashMap<String, String> {
method forwards_to (line 205) | pub fn forwards_to(&self) -> &str {
method metadata (line 210) | pub fn metadata(&self) -> &str {
method make_info (line 216) | pub(crate) fn make_info(&self) -> TunnelInner {
method drop (line 76) | fn drop(&mut self) {
type EndpointInfo (line 144) | pub trait EndpointInfo {
method url (line 146) | fn url(&self) -> &str;
method proto (line 149) | fn proto(&self) -> &str;
type EdgeInfo (line 155) | pub trait EdgeInfo {
method labels (line 157) | fn labels(&self) -> &HashMap<String, String>;
type Item (line 161) | type Item = Result<ConnInner, AcceptError>;
method poll_next (line 163) | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Opt...
FILE: ngrok/src/tunnel_ext.rs
type TunnelExt (line 71) | pub trait TunnelExt: Tunnel + Send {
method forward (line 63) | async fn forward(&mut self, url: Url) -> Result<(), io::Error> {
method forward (line 86) | async fn forward(&mut self, url: Url) -> Result<(), io::Error>;
type ConnExt (line 89) | pub(crate) trait ConnExt {
method forward_to (line 90) | fn forward_to(self, url: &Url) -> JoinHandle<io::Result<()>>;
method forward_to (line 115) | fn forward_to(mut self, url: &Url) -> JoinHandle<io::Result<()>> {
method forward_to (line 145) | fn forward_to(self, url: &Url) -> JoinHandle<Result<(), io::Error>> {
function forward_tunnel (line 94) | pub(crate) async fn forward_tunnel<T>(tun: &mut T, url: Url) -> Result<(...
function tls_config (line 216) | fn tls_config(
function connect (line 277) | async fn connect(
function connect_tcp (line 399) | async fn connect_tcp(host: &str, port: u16) -> Result<TcpStream, io::Err...
function serve_gateway_error (line 408) | fn serve_gateway_error(
type NoCertificateVerification (line 451) | pub struct NoCertificateVerification(CryptoProvider);
method new (line 454) | pub fn new(provider: CryptoProvider) -> Self {
method verify_server_cert (line 460) | fn verify_server_cert(
method verify_tls12_signature (line 471) | fn verify_tls12_signature(
method verify_tls13_signature (line 485) | fn verify_tls13_signature(
method supported_verify_schemes (line 499) | fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (333K chars).
[
{
"path": ".envrc",
"chars": 10,
"preview": "use flake\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 3074,
"preview": "on:\n push:\n branches: [main]\n pull_request:\n workflow_call:\n secrets:\n NGROK_AUTHTOKEN:\n required: "
},
{
"path": ".github/workflows/docs.yml",
"chars": 1506,
"preview": "on:\n push:\n branches: [main]\n\nname: Publish Docs\n\njobs:\n build:\n name: Build Rustdocs\n runs-on: ubuntu-latest"
},
{
"path": ".github/workflows/publish.yml",
"chars": 1207,
"preview": "on:\n workflow_dispatch:\nname: Publish All\n\njobs:\n ci:\n name: Run CI\n uses: ./.github/workflows/ci.yml\n secret"
},
{
"path": ".github/workflows/release.yml",
"chars": 1915,
"preview": "on:\n workflow_dispatch:\n inputs:\n crate:\n description: 'Crate to release'\n required: true\n "
},
{
"path": ".github/workflows/rust-cache/action.yml",
"chars": 982,
"preview": "name: 'rust cache setup'\ndescription: 'Set up cargo and sccache caches'\ninputs: {}\noutputs: {}\nruns:\n using: \"composite"
},
{
"path": ".gitignore",
"chars": 36,
"preview": ".env\n/target\n.direnv\n/.vscode\n*.swp\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3310,
"preview": "# ngrok Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as contributo"
},
{
"path": "CONTRIBUTING.md",
"chars": 587,
"preview": "# Contributing to ngrok-rust\n\nThank you for deciding to contribute to ngrok-rust!\n\n## Reporting a bug\n\nTo report a bug, "
},
{
"path": "Cargo.toml",
"chars": 111,
"preview": "[workspace]\nresolver = \"2\"\nmembers = [\n\t\"muxado\",\n\t\"ngrok\",\n\t\"cargo-doc-ngrok\",\n]\n\n[profile.release]\ndebug = 1\n"
},
{
"path": "LICENSE-APACHE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "LICENSE-MIT",
"chars": 1050,
"preview": "Copyright 2022 ngrok, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this softwar"
},
{
"path": "cargo-doc-ngrok/Cargo.toml",
"chars": 942,
"preview": "[package]\nname = \"cargo-doc-ngrok\"\nversion = \"0.2.2\"\nedition = \"2021\"\nlicense = \"MIT OR Apache-2.0\"\ndescription = \"A car"
},
{
"path": "cargo-doc-ngrok/src/main.rs",
"chars": 4795,
"preview": "use std::{\n io,\n path::PathBuf,\n process::Stdio,\n sync::Arc,\n};\n\nuse axum::BoxError;\nuse clap::{\n Args,\n "
},
{
"path": "flake.nix",
"chars": 3988,
"preview": "{\n description = \"ngrok agent library in Rust\";\n\n inputs = {\n nixpkgs.url = \"github:nixos/nixpkgs/nixpkgs-unstable\""
},
{
"path": "ngrok/CHANGELOG.md",
"chars": 3184,
"preview": "## 0.18.0\n- Add support for CEL filtering when listing resources.\n- Add support for service users\n- Add support for `vau"
},
{
"path": "ngrok/Cargo.toml",
"chars": 2632,
"preview": "[package]\nname = \"ngrok\"\nversion = \"0.18.0\"\nedition = \"2021\"\nlicense = \"MIT OR Apache-2.0\"\ndescription = \"The ngrok agen"
},
{
"path": "ngrok/README.md",
"chars": 5464,
"preview": "# ngrok-rust\n\n[![Crates.io][crates-badge]][crates-url]\n[![docs.rs][docs-badge]][docs-url]\n[![MIT licensed][mit-badge]][m"
},
{
"path": "ngrok/assets/ngrok.ca.crt",
"chars": 1407,
"preview": "-----BEGIN CERTIFICATE-----\nMIID4TCCAsmgAwIBAgIUZqF2AkB17pISojTndgc2U5BDt74wDQYJKoZIhvcNAQEL\nBQAwbzEQMA4GA1UEAwwHUm9vdCB"
},
{
"path": "ngrok/assets/policy-inbound.json",
"chars": 193,
"preview": "{\n \"inbound\": [\n {\n \"name\": \"test_in\",\n \"expressions\": [\n \"req.Method == 'PUT'\"\n ],\n \"act"
},
{
"path": "ngrok/assets/policy.json",
"chars": 467,
"preview": "{\n \"inbound\": [\n {\n \"name\": \"test_in\",\n \"expressions\": [\n \"req.Method == 'PUT'\"\n ],\n \"act"
},
{
"path": "ngrok/examples/axum.rs",
"chars": 5312,
"preview": "use std::{\n convert::Infallible,\n net::SocketAddr,\n};\n\nuse axum::{\n extract::ConnectInfo,\n routing::get,\n "
},
{
"path": "ngrok/examples/connect.rs",
"chars": 3024,
"preview": "use futures::TryStreamExt;\nuse ngrok::prelude::*;\nuse tokio::io::{\n self,\n AsyncBufReadExt,\n AsyncWriteExt,\n "
},
{
"path": "ngrok/examples/domain.crt",
"chars": 1094,
"preview": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeICCQDobWtly6PonjANBgkqhkiG9w0BAQsFADA/MQswCQYDVQQGEwJV\nUzERMA8GA1UECgwITm90IFJlYWw"
},
{
"path": "ngrok/examples/domain.key",
"chars": 1679,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqGzHy1lbNqprP2KCfJT0790IRfyqAK9TbKh4kF8aHAKeUU1A\nFR0EaVX4KN/X2jNqBiVjoK6"
},
{
"path": "ngrok/examples/labeled.rs",
"chars": 2241,
"preview": "use std::{\n convert::Infallible,\n error::Error,\n net::SocketAddr,\n};\n\nuse axum::{\n extract::ConnectInfo,\n "
},
{
"path": "ngrok/examples/mingrok.rs",
"chars": 2333,
"preview": "use std::sync::{\n Arc,\n Mutex,\n};\n\nuse anyhow::Error;\nuse futures::{\n prelude::*,\n select,\n};\nuse ngrok::pre"
},
{
"path": "ngrok/examples/tls.rs",
"chars": 2558,
"preview": "use std::{\n convert::Infallible,\n error::Error,\n net::SocketAddr,\n};\n\nuse axum::{\n extract::ConnectInfo,\n "
},
{
"path": "ngrok/src/config/common.rs",
"chars": 10035,
"preview": "use std::{\n collections::HashMap,\n env,\n process,\n};\n\nuse async_trait::async_trait;\nuse once_cell::sync::OnceCe"
},
{
"path": "ngrok/src/config/headers.rs",
"chars": 1226,
"preview": "use std::collections::HashMap;\n\nuse crate::internals::proto::Headers as HeaderProto;\n\n/// HTTP Headers to modify at the "
},
{
"path": "ngrok/src/config/http.rs",
"chars": 25611,
"preview": "use std::{\n borrow::Borrow,\n collections::HashMap,\n convert::From,\n str::FromStr,\n};\n\nuse bytes::Bytes;\nuse "
},
{
"path": "ngrok/src/config/labeled.rs",
"chars": 4807,
"preview": "use std::collections::HashMap;\n\nuse url::Url;\n\n// These are used for doc comment links.\n#[allow(unused_imports)]\nuse cra"
},
{
"path": "ngrok/src/config/oauth.rs",
"chars": 2351,
"preview": "use crate::internals::proto::{\n Oauth,\n SecretString,\n};\n\n/// Oauth Options configuration\n///\n/// https://ngrok.co"
},
{
"path": "ngrok/src/config/oidc.rs",
"chars": 1913,
"preview": "use crate::internals::proto::{\n Oidc,\n SecretString,\n};\n\n/// Oidc Options configuration\n///\n/// https://ngrok.com/"
},
{
"path": "ngrok/src/config/policies.rs",
"chars": 10435,
"preview": "use std::{\n fs::read_to_string,\n io,\n};\n\nuse serde::{\n Deserialize,\n Serialize,\n};\nuse thiserror::Error;\n\nus"
},
{
"path": "ngrok/src/config/tcp.rs",
"chars": 10819,
"preview": "use std::{\n collections::HashMap,\n convert::From,\n};\n\nuse url::Url;\n\nuse super::{\n common::ProxyProto,\n Poli"
},
{
"path": "ngrok/src/config/tls.rs",
"chars": 13096,
"preview": "use std::collections::HashMap;\n\nuse bytes::Bytes;\nuse url::Url;\n\nuse super::{\n common::ProxyProto,\n Policy,\n};\n// "
},
{
"path": "ngrok/src/config/webhook_verification.rs",
"chars": 696,
"preview": "use crate::internals::proto::{\n SecretString,\n WebhookVerification as WebhookProto,\n};\n\n/// Configuration for webh"
},
{
"path": "ngrok/src/conn.rs",
"chars": 5906,
"preview": "use std::{\n net::SocketAddr,\n pin::Pin,\n task::{\n Context,\n Poll,\n },\n};\n\n// Support for axum'"
},
{
"path": "ngrok/src/forwarder.rs",
"chars": 1918,
"preview": "use std::{\n collections::HashMap,\n error::Error as StdError,\n};\n\nuse async_trait::async_trait;\nuse tokio::task::Jo"
},
{
"path": "ngrok/src/internals/proto.rs",
"chars": 27015,
"preview": "use std::{\n collections::HashMap,\n error,\n fmt,\n io,\n ops::{\n Deref,\n DerefMut,\n },\n "
},
{
"path": "ngrok/src/internals/raw_session.rs",
"chars": 14394,
"preview": "use std::{\n collections::HashMap,\n fmt::Debug,\n future::Future,\n io,\n ops::{\n Deref,\n Deref"
},
{
"path": "ngrok/src/internals/rpc.rs",
"chars": 670,
"preview": "use std::fmt::Debug;\n\nuse muxado::typed::StreamType;\nuse serde::{\n de::DeserializeOwned,\n Serialize,\n};\n\npub trait"
},
{
"path": "ngrok/src/lib.rs",
"chars": 2093,
"preview": "#![doc = include_str!(\"../README.md\")]\n#![warn(missing_docs)]\n#![cfg_attr(docsrs, feature(doc_cfg))]\n\nmod internals {\n "
},
{
"path": "ngrok/src/online_tests.rs",
"chars": 31730,
"preview": "use std::{\n convert::Infallible,\n error::Error,\n io,\n io::prelude::*,\n net::SocketAddr,\n str::FromStr,"
},
{
"path": "ngrok/src/proxy_proto.rs",
"chars": 17909,
"preview": "use std::{\n io,\n mem,\n pin::{\n pin,\n Pin,\n },\n task::{\n ready,\n Context,\n "
},
{
"path": "ngrok/src/session.rs",
"chars": 42824,
"preview": "use std::{\n collections::{\n HashMap,\n VecDeque,\n },\n env,\n io,\n sync::{\n atomic::{\n "
},
{
"path": "ngrok/src/tunnel.rs",
"chars": 9646,
"preview": "use std::{\n collections::HashMap,\n pin::Pin,\n sync::Arc,\n task::{\n Context,\n Poll,\n },\n};\n\n"
},
{
"path": "ngrok/src/tunnel_ext.rs",
"chars": 16317,
"preview": "use std::{\n collections::HashMap,\n io,\n sync::Arc,\n};\n#[cfg(feature = \"hyper\")]\nuse std::{\n convert::Infalli"
},
{
"path": "rustfmt.toml",
"chars": 109,
"preview": "imports_layout = \"Vertical\"\nimports_granularity = \"Crate\"\ngroup_imports = \"StdExternalCrate\"\nedition = \"2021\""
}
]
About this extraction
This page contains the full source code of the ngrok/ngrok-rs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (310.5 KB), approximately 77.7k tokens, and a symbol index with 608 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.