Skip to content

Commit

Permalink
adding the root_cas option to SessionBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
bobzilladev committed May 21, 2024
1 parent 8c1a453 commit ae760af
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 6 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mio = { version = "=0.8.6" }
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
napi = { version = "2.12.1", default-features = false, features = ["napi4", "tokio_rt"] }
napi-derive = "2.12.1"
ngrok = { version = "0.14.0-pre.12" }
ngrok = { version = "=0.14.0-pre.13" }
parking_lot = "0.12.1"
regex = "1.9.5"
rustls = "0.22.2"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ const listener = await ngrok.forward({
console.log(`disconnected, addr ${addr} error: ${error}`);
},
session_metadata: "Online in One Line",
// advanced session connection configuration
server_addr: "example.com:443",
root_cas: "trusted",
session_ca_cert: fs.readFileSync("ca.pem", "utf8"),
// listener configuration
metadata: "example listener metadata from javascript",
domain: "<domain>",
Expand Down
38 changes: 36 additions & 2 deletions __test__/connect.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ test("forward https", async (t) => {
});

test("forward http2", async (t) => {
const httpServer = await makeHttp({useHttp2: true});
const httpServer = await makeHttp({ useHttp2: true });
const listener = await ngrok.forward({
// numeric port
addr: parseInt(httpServer.listenTo.split(":")[1], 10),
Expand All @@ -100,7 +100,7 @@ test("forward http2", async (t) => {
});

test("forward http2 no cert validation", async (t) => {
const httpServer = await makeHttp({useHttp2: true});
const httpServer = await makeHttp({ useHttp2: true });
const listener = await ngrok.forward({
// numeric port
addr: parseInt(httpServer.listenTo.split(":")[1], 10),
Expand Down Expand Up @@ -282,6 +282,40 @@ test.serial("forward bad domain", async (t) => {
t.is("ERR_NGROK_326", error.errorCode, error.message);
});

// serial to not run into double error on a session issue
test.serial("root_cas", async (t) => {
const httpServer = await makeHttp();
ngrok.authtoken(process.env["NGROK_AUTHTOKEN"]);

// tls error connecting to marketing site
var error = await t.throwsAsync(
async () => {
await ngrok.forward({
addr: httpServer.listenTo,
force_new_session: true,
root_cas: "trusted",
server_addr: "ngrok.com:443",
});
},
{ instanceOf: Error }
);
t.true(error.message.includes("tls handshake"), error.message);

// non-tls error connecting to marketing site with "host" root_cas
error = await t.throwsAsync(
async () => {
await ngrok.forward({
addr: httpServer.listenTo,
force_new_session: true,
root_cas: "host",
server_addr: "ngrok.com:443",
});
},
{ instanceOf: Error }
);
t.false(error.message.includes("tls handshake"), error.message);
});

test("policy", async (t) => {
const policy = fs.readFileSync(path.resolve("__test__", "policy.json"), "utf8");

Expand Down
4 changes: 3 additions & 1 deletion __test__/online.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ test("tls backend", async (t) => {

test("unverified tls backend", async (t) => {
const session = await makeSession();
const listener = await session.httpEndpoint().verifyUpstreamTls(false)
const listener = await session
.httpEndpoint()
.verifyUpstreamTls(false)
.listenAndForward("https://dashboard.ngrok.com");

const error = await t.throwsAsync(
Expand Down
1 change: 0 additions & 1 deletion examples/nextjs/ngrok.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const ngrok = require("@ngrok/ngrok");


// setup ngrok ingress in the parent process,
// in forked processes "send" will exist.
const makeListener = process.send === undefined;
Expand Down
45 changes: 45 additions & 0 deletions index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ pub struct Config {
/// and the API.
#[napi(js_name = "forwards_to")]
pub forwards_to: Option<String>,
/// Force a new session connection to be made.
#[napi(js_name = "force_new_session")]
pub force_new_session: Option<bool>,
/// Unused, will warn and be ignored
#[napi(js_name = "host_header")]
pub host_header: Option<String>,
Expand Down Expand Up @@ -249,11 +252,32 @@ pub struct Config {
/// [ngrok dashboard]: https://dashboard.ngrok.com/cloud-edge/tcp-addresses
#[napi(js_name = "remote_addr")]
pub remote_addr: Option<String>,
/// Sets the file path to a default certificate in PEM format to validate ngrok Session TLS connections.
/// Setting to "trusted" is the default, using the ngrok CA certificate.
/// Setting to "host" will verify using the certificates on the host operating system.
/// A client config set via tls_config after calling root_cas will override this value.
///
/// Corresponds to the [root_cas parameter in the ngrok docs]
///
/// [root_cas parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#root_cas
#[napi(js_name = "root_cas")]
pub root_cas: Option<String>,
/// The scheme that this edge should use.
/// "HTTPS" or "HTTP", defaults to "HTTPS".
/// If multiple are given only the last one is used.
#[napi(ts_type = "string|Array<string>")]
pub schemes: Option<Vec<String>>,
/// Configures the TLS certificate used to connect to the ngrok service while
/// establishing the session. Use this option only if you are connecting through
/// a man-in-the-middle or deep packet inspection proxy. Pass in the bytes of the certificate
/// to be used to validate the connection, then override the address to connect to via
/// the server_addr call.
///
/// Roughly corresponds to the [root_cas parameter in the ngrok docs].
///
/// [root_cas parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#root_cas
#[napi(js_name = "session_ca_cert")]
pub session_ca_cert: Option<String>,
/// Configures the opaque, machine-readable metadata string for this session.
/// Metadata is made available to you in the ngrok dashboard and the Agents API
/// resource. It is a useful way to allow you to uniquely identify sessions. We
Expand All @@ -264,6 +288,14 @@ pub struct Config {
/// [metdata parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#metadata
#[napi(js_name = "session_metadata")]
pub session_metadata: Option<String>,
/// Configures the network address to dial to connect to the ngrok service.
/// Use this option only if you are connecting to a custom agent ingress.
///
/// See the [server_addr parameter in the ngrok docs] for additional details.
///
/// [server_addr parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#server_addr
#[napi(js_name = "server_addr")]
pub server_addr: Option<String>,
/// Unused, use domain instead, will warn and be ignored
pub subdomain: Option<String>,
/// Unused, will warn and be ignored
Expand Down
18 changes: 17 additions & 1 deletion src/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ macro_rules! plumb {
};
}

/// Single string configuration with result
macro_rules! plumb_with_result {
($builder:tt, $config:tt, $name:tt, $config_name:tt) => {
if let Some(ref $name) = $config.$config_name {
$builder.$name($name.clone())?;
}
};
}

/// Boolean configuration
macro_rules! plumb_bool {
($builder:tt, $config:tt, $name:tt) => {
Expand Down Expand Up @@ -148,6 +157,11 @@ pub fn forward(
plumb!(s_builder, cfg, authtoken);
plumb_bool!(s_builder, cfg, authtoken_from_env);
plumb!(s_builder, cfg, metadata, session_metadata);
if let Some(ref ca_cert) = cfg.session_ca_cert {
s_builder.ca_cert(Uint8Array::new(ca_cert.as_bytes().to_vec()));
}
plumb_with_result!(s_builder, cfg, root_cas, root_cas);
plumb_with_result!(s_builder, cfg, server_addr, server_addr);
if let Some(func) = on_connection {
s_builder.handle_connection(env, func);
}
Expand All @@ -161,9 +175,11 @@ pub fn forward(

/// Connect the session, configure and start the listener
async fn async_connect(s_builder: SessionBuilder, config: Config) -> Result<Listener> {
let force_new_session = config.force_new_session.unwrap_or(false);

// Using a singleton session for connect use cases
let mut opt = SESSION.lock().await;
if opt.is_none() {
if opt.is_none() || force_new_session {
opt.replace(s_builder.connect().await?);
}
let session = opt.as_ref().unwrap();
Expand Down
17 changes: 17 additions & 0 deletions src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,23 @@ impl SessionBuilder {
Ok(self)
}

/// Sets the file path to a default certificate in PEM format to validate ngrok Session TLS connections.
/// Setting to "trusted" is the default, using the ngrok CA certificate.
/// Setting to "host" will verify using the certificates on the host operating system.
/// A client config set via tls_config after calling root_cas will override this value.
///
/// Corresponds to the [root_cas parameter in the ngrok docs]
///
/// [root_cas parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#root_cas
#[napi]
pub fn root_cas(&mut self, root_cas: String) -> Result<&Self> {
let mut builder = self.raw_builder.lock();
builder
.root_cas(root_cas)
.map_err(|e| napi_err(format!("{e}")))?;
Ok(self)
}

/// Configures the TLS certificate used to connect to the ngrok service while
/// establishing the session. Use this option only if you are connecting through
/// a man-in-the-middle or deep packet inspection proxy. Pass in the bytes of the certificate
Expand Down

0 comments on commit ae760af

Please sign in to comment.