From ae760af511ba14b4c9cba926c5d79c64a1bcdae2 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Tue, 21 May 2024 11:09:07 -0400 Subject: [PATCH 1/2] adding the root_cas option to SessionBuilder --- Cargo.toml | 2 +- README.md | 4 +++ __test__/connect.spec.mjs | 38 ++++++++++++++++++++++++++-- __test__/online.spec.mjs | 4 ++- examples/nextjs/ngrok.config.js | 1 - index.d.ts | 45 +++++++++++++++++++++++++++++++++ src/config.rs | 32 +++++++++++++++++++++++ src/connect.rs | 18 ++++++++++++- src/session.rs | 17 +++++++++++++ 9 files changed, 155 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 34e1c4f..a80ac06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index 7f96a72..0341465 100644 --- a/README.md +++ b/README.md @@ -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: "", diff --git a/__test__/connect.spec.mjs b/__test__/connect.spec.mjs index f50cafd..60f3e38 100644 --- a/__test__/connect.spec.mjs +++ b/__test__/connect.spec.mjs @@ -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), @@ -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), @@ -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"); diff --git a/__test__/online.spec.mjs b/__test__/online.spec.mjs index 6010bf5..aa181de 100644 --- a/__test__/online.spec.mjs +++ b/__test__/online.spec.mjs @@ -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( diff --git a/examples/nextjs/ngrok.config.js b/examples/nextjs/ngrok.config.js index 7235d44..44971d4 100644 --- a/examples/nextjs/ngrok.config.js +++ b/examples/nextjs/ngrok.config.js @@ -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; diff --git a/index.d.ts b/index.d.ts index c6e33e0..5c44e8a 100644 --- a/index.d.ts +++ b/index.d.ts @@ -76,6 +76,8 @@ export interface Config { * and the API. */ forwards_to?: string + /** Force a new session connection to be made. */ + force_new_session?: boolean /** Unused, will warn and be ignored */ host_header?: string /** @@ -268,12 +270,35 @@ export interface Config { * [ngrok dashboard]: https://dashboard.ngrok.com/cloud-edge/tcp-addresses */ remote_addr?: 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 + */ + root_cas?: string /** * The scheme that this edge should use. * "HTTPS" or "HTTP", defaults to "HTTPS". * If multiple are given only the last one is used. */ schemes?: string|Array + /** + * 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 + */ + session_ca_cert?: 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 @@ -285,6 +310,15 @@ export interface Config { * [metdata parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#metadata */ session_metadata?: 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 + */ + server_addr?: string /** Unused, use domain instead, will warn and be ignored */ subdomain?: string /** Unused, will warn and be ignored */ @@ -794,6 +828,17 @@ export class SessionBuilder { * [server_addr parameter in the ngrok docs]: https://ngrok.com/docs/ngrok-agent/config#server_addr */ serverAddr(addr: string): this + /** + * 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 + */ + rootCas(rootCas: string): this /** * Configures the TLS certificate used to connect to the ngrok service while * establishing the session. Use this option only if you are connecting through diff --git a/src/config.rs b/src/config.rs index 752ca29..4a53387 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,6 +65,9 @@ pub struct Config { /// and the API. #[napi(js_name = "forwards_to")] pub forwards_to: Option, + /// Force a new session connection to be made. + #[napi(js_name = "force_new_session")] + pub force_new_session: Option, /// Unused, will warn and be ignored #[napi(js_name = "host_header")] pub host_header: Option, @@ -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, + /// 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, /// 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")] pub schemes: Option>, + /// 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, /// 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 @@ -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, + /// 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, /// Unused, use domain instead, will warn and be ignored pub subdomain: Option, /// Unused, will warn and be ignored diff --git a/src/connect.rs b/src/connect.rs index cce0336..250c8f0 100644 --- a/src/connect.rs +++ b/src/connect.rs @@ -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) => { @@ -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); } @@ -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 { + 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(); diff --git a/src/session.rs b/src/session.rs index 7e1ba12..fbdcab4 100644 --- a/src/session.rs +++ b/src/session.rs @@ -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 From f824605436532ad1065e2c4a77b512f2aa93a5f9 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Wed, 22 May 2024 11:09:25 -0400 Subject: [PATCH 2/2] add missing shutdown --- __test__/connect.spec.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/__test__/connect.spec.mjs b/__test__/connect.spec.mjs index 60f3e38..d57eb16 100644 --- a/__test__/connect.spec.mjs +++ b/__test__/connect.spec.mjs @@ -280,10 +280,15 @@ test.serial("forward bad domain", async (t) => { { instanceOf: Error } ); t.is("ERR_NGROK_326", error.errorCode, error.message); + + await shutdown(null, httpServer.socket); }); // serial to not run into double error on a session issue test.serial("root_cas", async (t) => { + // remove any lingering sessions + await ngrok.disconnect(); + const httpServer = await makeHttp(); ngrok.authtoken(process.env["NGROK_AUTHTOKEN"]);