From 2f90545c60bd17b7af0b3af868470af0d3709ae5 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Wed, 17 Jan 2024 12:47:31 -0500 Subject: [PATCH 1/6] Add policies configuration --- ngrok/assets/policies.json | 33 +++++ ngrok/examples/axum.rs | 26 ++++ ngrok/src/config/common.rs | 4 + ngrok/src/config/http.rs | 14 +- ngrok/src/config/policies.rs | 250 +++++++++++++++++++++++++++++++++++ ngrok/src/config/tcp.rs | 20 ++- ngrok/src/config/tls.rs | 19 ++- ngrok/src/internals/proto.rs | 27 ++++ ngrok/src/lib.rs | 14 ++ ngrok/src/online_tests.rs | 42 +++++- 10 files changed, 438 insertions(+), 11 deletions(-) create mode 100644 ngrok/assets/policies.json create mode 100644 ngrok/src/config/policies.rs diff --git a/ngrok/assets/policies.json b/ngrok/assets/policies.json new file mode 100644 index 0000000..5235624 --- /dev/null +++ b/ngrok/assets/policies.json @@ -0,0 +1,33 @@ +{ + "policies": { + "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 + } + } + ] + } + ] + } +} diff --git a/ngrok/examples/axum.rs b/ngrok/examples/axum.rs index 43853dd..6aa4f42 100644 --- a/ngrok/examples/axum.rs +++ b/ngrok/examples/axum.rs @@ -61,6 +61,7 @@ async fn start_tunnel() -> anyhow::Result { // .allow_domain("") // .scope(""), // ) + // .policies(create_policies()?) // .proxy_proto(ProxyProto::None) // .remove_request_header("X-Req-Nope") // .remove_response_header("X-Res-Nope") @@ -77,3 +78,28 @@ async fn start_tunnel() -> anyhow::Result { Ok(tun) } + +#[allow(dead_code)] +fn create_policies() -> Result { + Ok(Policies::new() + .add_inbound( + Policy::new("deny_put") + .add_expression("req.Method == 'PUT'") + .add_action(Action::new("deny", None::)?), + ) + .add_outbound( + Policy::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()) +} diff --git a/ngrok/src/config/common.rs b/ngrok/src/config/common.rs index 6391e49..d2d131a 100644 --- a/ngrok/src/config/common.rs +++ b/ngrok/src/config/common.rs @@ -10,6 +10,7 @@ use url::Url; pub use crate::internals::proto::ProxyProto; use crate::{ + config::policies::Policies, forwarder::Forwarder, internals::proto::{ BindExtra, @@ -198,6 +199,9 @@ pub(crate) struct CommonOpts { pub(crate) forwards_to: Option, // Tunnel L7 app protocol pub(crate) forwards_proto: Option, + // Policies that define rules that should be applied to incoming or outgoing + // connections to the edge. + pub(crate) policies: Option, } impl CommonOpts { diff --git a/ngrok/src/config/http.rs b/ngrok/src/config/http.rs index 7483d5a..96ffe0d 100644 --- a/ngrok/src/config/http.rs +++ b/ngrok/src/config/http.rs @@ -11,7 +11,10 @@ use bytes::{ use thiserror::Error; use url::Url; -use super::common::ProxyProto; +use super::{ + common::ProxyProto, + Policies, +}; // These are used for doc comment links. #[allow(unused_imports)] use crate::config::{ @@ -182,6 +185,7 @@ impl TunnelConfig for HttpOptions { .websocket_tcp_conversion .then_some(WebsocketTcpConverter {}), user_agent_filter: self.user_agent_filter(), + policies: self.common_opts.policies.clone().map(From::from), ..Default::default() }; @@ -420,6 +424,12 @@ impl HttpTunnelBuilder { self } + /// Set the policies for this edge. + pub fn policies(&mut self, policies: impl Borrow) -> &mut Self { + self.options.common_opts.policies = Some(policies.borrow().to_owned()); + 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) { @@ -432,6 +442,7 @@ impl HttpTunnelBuilder { #[cfg(test)] mod test { use super::*; + use crate::config::policies::test::POLICY_JSON; const METADATA: &str = "testmeta"; const TEST_FORWARD: &str = "testforward"; @@ -488,6 +499,7 @@ mod test { .basic_auth("ngrok", "online1line") .forwards_to(TEST_FORWARD) .app_protocol("http2") + .policies(Policies::from_json(POLICY_JSON).unwrap()) .options, ); } diff --git a/ngrok/src/config/policies.rs b/ngrok/src/config/policies.rs new file mode 100644 index 0000000..b3305df --- /dev/null +++ b/ngrok/src/config/policies.rs @@ -0,0 +1,250 @@ +use std::{ + borrow::Borrow, + fs::read_to_string, +}; + +use serde::{ + Deserialize, + Serialize, +}; +use thiserror::Error; + +use crate::internals::proto; + +/// A set of policies that define rules that should be applied to incoming or outgoing +/// connections to the edge. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +#[serde(default)] +pub struct Policies { + inbound: Vec, + outbound: Vec, +} + +/// A policy that defines rules that should be applied +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +#[serde(default)] +pub struct Policy { + name: String, + expressions: Vec, + actions: Vec, +} + +/// An action that should be taken if the policy matches +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +#[serde(default)] +pub struct Action { + #[serde(rename = "type")] + type_: String, + config: Option, +} + +/// Error representing invalid string for Policies +#[derive(Debug, Clone, Error)] +#[error("invalid policies, err: {}", .0)] +pub struct InvalidPolicies(String); + +/// Used just for json parsing +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +struct Envelope { + policies: Policies, +} + +impl Policies { + /// Create a new empty [Policies] struct + pub fn new() -> Self { + Policies { + ..Default::default() + } + } + + /// Create a new [Policies] from a json string + pub fn from_json(json: impl Into) -> Result { + let envelope: Envelope = + serde_json::from_str(&json.into()).map_err(|e| InvalidPolicies(e.to_string()))?; + Ok(envelope.policies) + } + + /// Create a new [Policies] from a json file + pub fn from_file(json_file_path: impl Into) -> Result { + Policies::from_json( + read_to_string(json_file_path.into()).map_err(|e| InvalidPolicies(e.to_string()))?, + ) + } + + /// Convert [Policies] to json string + pub fn to_json(&self) -> Result { + let envelope = Envelope { + policies: self.clone(), + }; + serde_json::to_string(&envelope).map_err(|e| InvalidPolicies(e.to_string())) + } + + /// Add an inbound policy + pub fn add_inbound(&mut self, policy: impl Borrow) -> &mut Self { + self.inbound.push(policy.borrow().to_owned()); + self + } + + /// Add an outbound policy + pub fn add_outbound(&mut self, policy: impl Borrow) -> &mut Self { + self.outbound.push(policy.borrow().to_owned()); + self + } +} + +impl Policy { + /// Create a new [Policy] + pub fn new(name: impl Into) -> Self { + Policy { + name: name.into(), + ..Default::default() + } + } + + /// Add an expression + pub fn add_expression(&mut self, expression: impl Into) -> &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 Action { + /// Create a new [Action] + pub fn new( + type_: impl Into, + config: Option>, + ) -> Result { + Ok(Action { + type_: type_.into(), + config: config + .map(|c| { + serde_json::from_str(&c.into()).map_err(|e| InvalidPolicies(e.to_string())) + }) + .transpose()?, + }) + } +} + +// transform into the wire protocol format +impl From for proto::Policies { + fn from(o: Policies) -> Self { + proto::Policies { + inbound: o.inbound.into_iter().map(|p| p.into()).collect(), + outbound: o.outbound.into_iter().map(|p| p.into()).collect(), + } + } +} + +impl From for proto::Policy { + fn from(p: Policy) -> Self { + proto::Policy { + name: p.name, + expressions: p.expressions, + actions: p.actions.into_iter().map(|a| a.into()).collect(), + } + } +} + +impl From 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###" + {"policies": + {"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_policies() { + let policies = Policies::from_json(POLICY_JSON).unwrap(); + assert_eq!(1, policies.inbound.len()); + assert_eq!(1, policies.outbound.len()); + let inbound = &policies.inbound[0]; + let outbound = &policies.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_policies_to_json() { + let policies = Policies::from_json(POLICY_JSON).unwrap(); + let json = policies.to_json().unwrap(); + let policies2 = Policies::from_json(json).unwrap(); + assert_eq!(policies, policies2); + } + + #[test] + fn test_builders() { + let policies = Policies::from_json(POLICY_JSON).unwrap(); + let policies2 = Policies::new() + .add_inbound( + Policy::new("test_in") + .add_expression("req.Method == 'PUT'") + .add_action(Action::new("deny", None::).unwrap()), + ) + .add_outbound( + Policy::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!(policies, policies2); + } + + #[test] + fn test_load_file() { + let policies = Policies::from_json(POLICY_JSON).unwrap(); + let policies2 = Policies::from_file("assets/policies.json").unwrap(); + assert_eq!(policies, policies2); + } +} diff --git a/ngrok/src/config/tcp.rs b/ngrok/src/config/tcp.rs index 54d9def..c5a0919 100644 --- a/ngrok/src/config/tcp.rs +++ b/ngrok/src/config/tcp.rs @@ -1,8 +1,14 @@ -use std::collections::HashMap; +use std::{ + borrow::Borrow, + collections::HashMap, +}; use url::Url; -use super::common::ProxyProto; +use super::{ + common::ProxyProto, + Policies, +}; // These are used for doc comment links. #[allow(unused_imports)] use crate::config::{ @@ -65,6 +71,8 @@ impl TunnelConfig for TcpOptions { tcp_endpoint.ip_restriction = self.common_opts.ip_restriction(); + tcp_endpoint.policies = self.common_opts.policies.clone().map(From::from); + Some(BindOpts::Tcp(tcp_endpoint)) } fn labels(&self) -> HashMap { @@ -127,6 +135,12 @@ impl TcpTunnelBuilder { self } + /// Set the policies for this edge. + pub fn policies(&mut self, policies: impl Borrow) -> &mut Self { + self.options.common_opts.policies = Some(policies.borrow().to_owned()); + 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 @@ -136,6 +150,7 @@ impl TcpTunnelBuilder { #[cfg(test)] mod test { use super::*; + use crate::config::policies::test::POLICY_JSON; const METADATA: &str = "testmeta"; const TEST_FORWARD: &str = "testforward"; @@ -158,6 +173,7 @@ mod test { .metadata(METADATA) .remote_addr(REMOTE_ADDR) .forwards_to(TEST_FORWARD) + .policies(Policies::from_json(POLICY_JSON).unwrap()) .options, ); } diff --git a/ngrok/src/config/tls.rs b/ngrok/src/config/tls.rs index 7dd2576..25342a2 100644 --- a/ngrok/src/config/tls.rs +++ b/ngrok/src/config/tls.rs @@ -1,4 +1,7 @@ -use std::collections::HashMap; +use std::{ + borrow::Borrow, + collections::HashMap, +}; use bytes::{ self, @@ -6,7 +9,10 @@ use bytes::{ }; use url::Url; -use super::common::ProxyProto; +use super::{ + common::ProxyProto, + Policies, +}; // These are used for doc comment links. #[allow(unused_imports)] use crate::config::{ @@ -88,6 +94,7 @@ impl TunnelConfig for TlsOptions { 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.policies = self.common_opts.policies.clone().map(From::from); Some(BindOpts::Tls(tls_endpoint)) } @@ -171,6 +178,12 @@ impl TlsTunnelBuilder { self } + /// Set the policies for this edge. + pub fn policies(&mut self, policies: impl Borrow) -> &mut Self { + self.options.common_opts.policies = Some(policies.borrow().to_owned()); + 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 @@ -180,6 +193,7 @@ impl TlsTunnelBuilder { #[cfg(test)] mod test { use super::*; + use crate::config::policies::test::POLICY_JSON; const METADATA: &str = "testmeta"; const TEST_FORWARD: &str = "testforward"; @@ -209,6 +223,7 @@ mod test { .mutual_tlsca(CA_CERT2.into()) .termination(CERT.into(), KEY.into()) .forwards_to(TEST_FORWARD) + .policies(Policies::from_json(POLICY_JSON).unwrap()) .options, ); } diff --git a/ngrok/src/internals/proto.rs b/ngrok/src/internals/proto.rs index fe7a095..cf3cef9 100644 --- a/ngrok/src/internals/proto.rs +++ b/ngrok/src/internals/proto.rs @@ -681,6 +681,7 @@ pub struct HttpEndpoint { pub websocket_tcp_converter: Option, #[serde(rename = "UserAgentFilter")] pub user_agent_filter: Option, + pub policies: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -806,6 +807,7 @@ pub struct TcpEndpoint { pub proxy_proto: ProxyProto, #[serde(rename = "IPRestriction")] pub ip_restriction: Option, + pub policies: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -823,6 +825,7 @@ pub struct TlsEndpoint { pub tls_termination: Option, #[serde(rename = "IPRestriction")] pub ip_restriction: Option, + pub policies: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -841,6 +844,30 @@ pub struct LabelEndpoint { pub labels: HashMap, } +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "PascalCase", default)] +pub struct Policies { + pub inbound: Vec, + pub outbound: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "PascalCase", default)] +pub struct Policy { + pub name: String, + pub expressions: Vec, + pub actions: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(rename_all = "PascalCase", default)] +pub struct Action { + #[serde(rename = "Type")] + pub type_: String, + #[serde(default, with = "base64bytes", skip_serializing_if = "is_default")] + pub config: Vec, +} + // These are helpers to facilitate the Vec <-> base64-encoded bytes // representation that the Go messages use mod base64bytes { diff --git a/ngrok/src/lib.rs b/ngrok/src/lib.rs index 6009836..5e7b1cb 100644 --- a/ngrok/src/lib.rs +++ b/ngrok/src/lib.rs @@ -23,6 +23,8 @@ pub mod config { mod oauth; pub use oauth::*; mod oidc; + pub use policies::*; + mod policies; pub use oidc::*; mod tcp; pub use tcp::*; @@ -64,7 +66,19 @@ pub mod prelude { #[doc(inline)] pub use crate::{ config::{ + Action, ForwarderBuilder, + HttpTunnelBuilder, + InvalidPolicies, + LabeledTunnelBuilder, + OauthOptions, + OidcOptions, + Policies, + Policy, + ProxyProto, + Scheme, + TcpTunnelBuilder, + TlsTunnelBuilder, TunnelBuilder, }, conn::{ diff --git a/ngrok/src/online_tests.rs b/ngrok/src/online_tests.rs index 33c19e7..d03b577 100644 --- a/ngrok/src/online_tests.rs +++ b/ngrok/src/online_tests.rs @@ -68,12 +68,6 @@ use tracing_test::traced_test; use url::Url; use crate::{ - config::{ - HttpTunnelBuilder, - OauthOptions, - ProxyProto, - Scheme, - }, prelude::*, session::{ SessionBuilder, @@ -392,6 +386,42 @@ async fn custom_domain() -> Result<(), Error> { Ok(()) } +#[traced_test] +#[cfg_attr(not(feature = "paid-tests"), ignore)] +#[test] +async fn policies() -> Result<(), Error> { + let tun = serve_http( + defaults, + |tun| tun.policies(create_policies().unwrap()), + hello_router(), + ) + .await?; + + let client = reqwest::Client::new(); + let resp = client.get(&tun.url).send().await?; + assert_eq!(resp.status(), 222); + + Ok(()) +} + +fn create_policies() -> Result { + Ok(Policies::new() + .add_inbound( + Policy::new("deny_put") + .add_expression("req.Method == 'PUT'") + .add_action(Action::new("deny", None::)?), + ) + .add_outbound( + Policy::new("222_response") + .add_expression("res.StatusCode == '200'") + .add_action(Action::new( + "custom-response", + Some("{\"status_code\": 222}"), + )?), + ) + .to_owned()) +} + #[traced_test] #[cfg_attr(not(all(feature = "paid-tests", feature = "long-tests")), ignore)] #[test] From f5b706ede06a77c3b6d041ff1e0b3c9a067e4c49 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Fri, 19 Jan 2024 16:38:24 -0500 Subject: [PATCH 2/6] Policies: move to AsRef, distinct errors, str typed option, more to_json --- ngrok/examples/axum.rs | 2 +- ngrok/src/config/policies.rs | 93 +++++++++++++++++++++++++++++------- ngrok/src/online_tests.rs | 2 +- 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/ngrok/examples/axum.rs b/ngrok/examples/axum.rs index 6aa4f42..2ba8e78 100644 --- a/ngrok/examples/axum.rs +++ b/ngrok/examples/axum.rs @@ -85,7 +85,7 @@ fn create_policies() -> Result { .add_inbound( Policy::new("deny_put") .add_expression("req.Method == 'PUT'") - .add_action(Action::new("deny", None::)?), + .add_action(Action::new("deny", None)?), ) .add_outbound( Policy::new("200_response") diff --git a/ngrok/src/config/policies.rs b/ngrok/src/config/policies.rs index b3305df..d1f0ddb 100644 --- a/ngrok/src/config/policies.rs +++ b/ngrok/src/config/policies.rs @@ -40,8 +40,17 @@ pub struct Action { /// Error representing invalid string for Policies #[derive(Debug, Clone, Error)] -#[error("invalid policies, err: {}", .0)] -pub struct InvalidPolicies(String); +pub enum InvalidPolicies { + /// Error representing invalid string for Policies + #[error("failure to parse policies, err: {}", .0)] + ParseError(String), + /// Error representing invalid string for Policies + #[error("failure to serialize policies, err: {}", .0)] + GenerationError(String), + /// Error representing invalid string for Policies + #[error("failure to read policies file '{}', err: {}", .0, .1)] + FileReadError(String, String), +} /// Used just for json parsing #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] @@ -58,17 +67,17 @@ impl Policies { } /// Create a new [Policies] from a json string - pub fn from_json(json: impl Into) -> Result { - let envelope: Envelope = - serde_json::from_str(&json.into()).map_err(|e| InvalidPolicies(e.to_string()))?; + pub fn from_json(json: impl AsRef) -> Result { + let envelope: Envelope = serde_json::from_str(json.as_ref()) + .map_err(|e| InvalidPolicies::ParseError(e.to_string()))?; Ok(envelope.policies) } /// Create a new [Policies] from a json file - pub fn from_file(json_file_path: impl Into) -> Result { - Policies::from_json( - read_to_string(json_file_path.into()).map_err(|e| InvalidPolicies(e.to_string()))?, - ) + pub fn from_file(json_file_path: impl AsRef) -> Result { + Policies::from_json(read_to_string(json_file_path.as_ref()).map_err(|e| { + InvalidPolicies::FileReadError(json_file_path.as_ref().to_string(), e.to_string()) + })?) } /// Convert [Policies] to json string @@ -76,7 +85,8 @@ impl Policies { let envelope = Envelope { policies: self.clone(), }; - serde_json::to_string(&envelope).map_err(|e| InvalidPolicies(e.to_string())) + serde_json::to_string(&envelope) + .map_err(|e| InvalidPolicies::GenerationError(e.to_string())) } /// Add an inbound policy @@ -101,6 +111,11 @@ impl Policy { } } + /// Convert [Policy] to json string + pub fn to_json(&self) -> Result { + serde_json::to_string(&self).map_err(|e| InvalidPolicies::GenerationError(e.to_string())) + } + /// Add an expression pub fn add_expression(&mut self, expression: impl Into) -> &mut Self { self.expressions.push(expression.into()); @@ -116,19 +131,21 @@ impl Policy { impl Action { /// Create a new [Action] - pub fn new( - type_: impl Into, - config: Option>, - ) -> Result { + pub fn new(type_: impl Into, config: Option<&str>) -> Result { Ok(Action { type_: type_.into(), config: config .map(|c| { - serde_json::from_str(&c.into()).map_err(|e| InvalidPolicies(e.to_string())) + serde_json::from_str(c).map_err(|e| InvalidPolicies::ParseError(e.to_string())) }) .transpose()?, }) } + + /// Convert [Action] to json string + pub fn to_json(&self) -> Result { + serde_json::to_string(&self).map_err(|e| InvalidPolicies::GenerationError(e.to_string())) + } } // transform into the wire protocol format @@ -220,6 +237,44 @@ pub(crate) mod test { assert_eq!(policies, policies2); } + #[test] + fn test_policies_to_json_error() { + let error = Policies::from_json("asdf").err().unwrap(); + assert!(matches!(error, InvalidPolicies::ParseError { .. })); + } + + #[test] + fn test_policy_to_json() { + let policies = Policies::from_json(POLICY_JSON).unwrap(); + let policy = &policies.outbound[0]; + let json = policy.to_json().unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + let policy_map = parsed.as_object().unwrap(); + assert_eq!("test_out", policy_map["name"]); + + // expressions + let expressions = policy_map["expressions"].as_array().unwrap(); + assert_eq!(1, expressions.len()); + assert_eq!("res.StatusCode == '200'", expressions[0]); + + // actions + let actions = policy_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 policies = Policies::from_json(POLICY_JSON).unwrap(); + let action = &policies.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 policies = Policies::from_json(POLICY_JSON).unwrap(); @@ -227,7 +282,7 @@ pub(crate) mod test { .add_inbound( Policy::new("test_in") .add_expression("req.Method == 'PUT'") - .add_action(Action::new("deny", None::).unwrap()), + .add_action(Action::new("deny", None).unwrap()), ) .add_outbound( Policy::new("test_out") @@ -247,4 +302,10 @@ pub(crate) mod test { let policies2 = Policies::from_file("assets/policies.json").unwrap(); assert_eq!(policies, policies2); } + + #[test] + fn test_load_file_error() { + let error = Policies::from_file("assets/absent.json").err().unwrap(); + assert!(matches!(error, InvalidPolicies::FileReadError { .. })); + } } diff --git a/ngrok/src/online_tests.rs b/ngrok/src/online_tests.rs index d03b577..103394b 100644 --- a/ngrok/src/online_tests.rs +++ b/ngrok/src/online_tests.rs @@ -409,7 +409,7 @@ fn create_policies() -> Result { .add_inbound( Policy::new("deny_put") .add_expression("req.Method == 'PUT'") - .add_action(Action::new("deny", None::)?), + .add_action(Action::new("deny", None)?), ) .add_outbound( Policy::new("222_response") From 42a8bc33ffa68fbdb56525267c4fb6bb2df4b5b0 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Mon, 22 Jan 2024 11:01:32 -0500 Subject: [PATCH 3/6] policies.Envelope parent replaced with PolicySet child --- ngrok/src/config/policies.rs | 52 +++++++++++++++++------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/ngrok/src/config/policies.rs b/ngrok/src/config/policies.rs index d1f0ddb..f50012b 100644 --- a/ngrok/src/config/policies.rs +++ b/ngrok/src/config/policies.rs @@ -14,8 +14,14 @@ use crate::internals::proto; /// A set of policies that define rules that should be applied to incoming or outgoing /// connections to the edge. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] -#[serde(default)] pub struct Policies { + policies: PolicySet, +} + +/// A private layer to hold the inbound and outbound policies +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] +#[serde(default)] +struct PolicySet { inbound: Vec, outbound: Vec, } @@ -52,12 +58,6 @@ pub enum InvalidPolicies { FileReadError(String, String), } -/// Used just for json parsing -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] -struct Envelope { - policies: Policies, -} - impl Policies { /// Create a new empty [Policies] struct pub fn new() -> Self { @@ -68,9 +68,7 @@ impl Policies { /// Create a new [Policies] from a json string pub fn from_json(json: impl AsRef) -> Result { - let envelope: Envelope = serde_json::from_str(json.as_ref()) - .map_err(|e| InvalidPolicies::ParseError(e.to_string()))?; - Ok(envelope.policies) + serde_json::from_str(json.as_ref()).map_err(|e| InvalidPolicies::ParseError(e.to_string())) } /// Create a new [Policies] from a json file @@ -82,22 +80,18 @@ impl Policies { /// Convert [Policies] to json string pub fn to_json(&self) -> Result { - let envelope = Envelope { - policies: self.clone(), - }; - serde_json::to_string(&envelope) - .map_err(|e| InvalidPolicies::GenerationError(e.to_string())) + serde_json::to_string(&self).map_err(|e| InvalidPolicies::GenerationError(e.to_string())) } /// Add an inbound policy pub fn add_inbound(&mut self, policy: impl Borrow) -> &mut Self { - self.inbound.push(policy.borrow().to_owned()); + self.policies.inbound.push(policy.borrow().to_owned()); self } /// Add an outbound policy pub fn add_outbound(&mut self, policy: impl Borrow) -> &mut Self { - self.outbound.push(policy.borrow().to_owned()); + self.policies.outbound.push(policy.borrow().to_owned()); self } } @@ -152,8 +146,8 @@ impl Action { impl From for proto::Policies { fn from(o: Policies) -> Self { proto::Policies { - inbound: o.inbound.into_iter().map(|p| p.into()).collect(), - outbound: o.outbound.into_iter().map(|p| p.into()).collect(), + inbound: o.policies.inbound.into_iter().map(|p| p.into()).collect(), + outbound: o.policies.outbound.into_iter().map(|p| p.into()).collect(), } } } @@ -205,11 +199,11 @@ pub(crate) mod test { #[test] fn test_json_to_policies() { - let policies = Policies::from_json(POLICY_JSON).unwrap(); - assert_eq!(1, policies.inbound.len()); - assert_eq!(1, policies.outbound.len()); - let inbound = &policies.inbound[0]; - let outbound = &policies.outbound[0]; + let pol = Policies::from_json(POLICY_JSON).unwrap(); + assert_eq!(1, pol.policies.inbound.len()); + assert_eq!(1, pol.policies.outbound.len()); + let inbound = &pol.policies.inbound[0]; + let outbound = &pol.policies.outbound[0]; assert_eq!("test_in", inbound.name); assert_eq!(1, inbound.expressions.len()); @@ -245,8 +239,8 @@ pub(crate) mod test { #[test] fn test_policy_to_json() { - let policies = Policies::from_json(POLICY_JSON).unwrap(); - let policy = &policies.outbound[0]; + let pol = Policies::from_json(POLICY_JSON).unwrap(); + let policy = &pol.policies.outbound[0]; let json = policy.to_json().unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); let policy_map = parsed.as_object().unwrap(); @@ -266,8 +260,8 @@ pub(crate) mod test { #[test] fn test_action_to_json() { - let policies = Policies::from_json(POLICY_JSON).unwrap(); - let action = &policies.outbound[0].actions[0]; + let pol = Policies::from_json(POLICY_JSON).unwrap(); + let action = &pol.policies.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(); @@ -300,6 +294,8 @@ pub(crate) mod test { fn test_load_file() { let policies = Policies::from_json(POLICY_JSON).unwrap(); let policies2 = Policies::from_file("assets/policies.json").unwrap(); + assert_eq!("test_in", policies2.policies.inbound[0].name); + assert_eq!("test_out", policies2.policies.outbound[0].name); assert_eq!(policies, policies2); } From 63daaff5571d8c5c9a603d599340f544da802b74 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Wed, 24 Jan 2024 15:28:09 -0500 Subject: [PATCH 4/6] Rename Policies to Policy, Policy to Rule --- ngrok/assets/policies.json | 33 ------ ngrok/assets/policy.json | 31 ++++++ ngrok/examples/axum.rs | 10 +- ngrok/src/config/common.rs | 6 +- ngrok/src/config/http.rs | 12 +- ngrok/src/config/policies.rs | 209 +++++++++++++++++------------------ ngrok/src/config/tcp.rs | 12 +- ngrok/src/config/tls.rs | 12 +- ngrok/src/internals/proto.rs | 14 +-- ngrok/src/lib.rs | 4 +- ngrok/src/online_tests.rs | 12 +- 11 files changed, 172 insertions(+), 183 deletions(-) delete mode 100644 ngrok/assets/policies.json create mode 100644 ngrok/assets/policy.json diff --git a/ngrok/assets/policies.json b/ngrok/assets/policies.json deleted file mode 100644 index 5235624..0000000 --- a/ngrok/assets/policies.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "policies": { - "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 - } - } - ] - } - ] - } -} diff --git a/ngrok/assets/policy.json b/ngrok/assets/policy.json new file mode 100644 index 0000000..751f27f --- /dev/null +++ b/ngrok/assets/policy.json @@ -0,0 +1,31 @@ +{ + "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 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/ngrok/examples/axum.rs b/ngrok/examples/axum.rs index 2ba8e78..f10c5fb 100644 --- a/ngrok/examples/axum.rs +++ b/ngrok/examples/axum.rs @@ -61,7 +61,7 @@ async fn start_tunnel() -> anyhow::Result { // .allow_domain("") // .scope(""), // ) - // .policies(create_policies()?) + // .policy(create_policy()?) // .proxy_proto(ProxyProto::None) // .remove_request_header("X-Req-Nope") // .remove_response_header("X-Res-Nope") @@ -80,15 +80,15 @@ async fn start_tunnel() -> anyhow::Result { } #[allow(dead_code)] -fn create_policies() -> Result { - Ok(Policies::new() +fn create_policy() -> Result { + Ok(Policy::new() .add_inbound( - Policy::new("deny_put") + Rule::new("deny_put") .add_expression("req.Method == 'PUT'") .add_action(Action::new("deny", None)?), ) .add_outbound( - Policy::new("200_response") + Rule::new("200_response") .add_expression("res.StatusCode == '200'") .add_action(Action::new( "custom-response", diff --git a/ngrok/src/config/common.rs b/ngrok/src/config/common.rs index d2d131a..30b42ad 100644 --- a/ngrok/src/config/common.rs +++ b/ngrok/src/config/common.rs @@ -10,7 +10,7 @@ use url::Url; pub use crate::internals::proto::ProxyProto; use crate::{ - config::policies::Policies, + config::policies::Policy, forwarder::Forwarder, internals::proto::{ BindExtra, @@ -199,9 +199,9 @@ pub(crate) struct CommonOpts { pub(crate) forwards_to: Option, // Tunnel L7 app protocol pub(crate) forwards_proto: Option, - // Policies that define rules that should be applied to incoming or outgoing + // Policy that defines rules that should be applied to incoming or outgoing // connections to the edge. - pub(crate) policies: Option, + pub(crate) policy: Option, } impl CommonOpts { diff --git a/ngrok/src/config/http.rs b/ngrok/src/config/http.rs index 96ffe0d..83a2b70 100644 --- a/ngrok/src/config/http.rs +++ b/ngrok/src/config/http.rs @@ -13,7 +13,7 @@ use url::Url; use super::{ common::ProxyProto, - Policies, + Policy, }; // These are used for doc comment links. #[allow(unused_imports)] @@ -185,7 +185,7 @@ impl TunnelConfig for HttpOptions { .websocket_tcp_conversion .then_some(WebsocketTcpConverter {}), user_agent_filter: self.user_agent_filter(), - policies: self.common_opts.policies.clone().map(From::from), + policy: self.common_opts.policy.clone().map(From::from), ..Default::default() }; @@ -424,9 +424,9 @@ impl HttpTunnelBuilder { self } - /// Set the policies for this edge. - pub fn policies(&mut self, policies: impl Borrow) -> &mut Self { - self.options.common_opts.policies = Some(policies.borrow().to_owned()); + /// Set the policy for this edge. + pub fn policy(&mut self, policy: impl Borrow) -> &mut Self { + self.options.common_opts.policy = Some(policy.borrow().to_owned()); self } @@ -499,7 +499,7 @@ mod test { .basic_auth("ngrok", "online1line") .forwards_to(TEST_FORWARD) .app_protocol("http2") - .policies(Policies::from_json(POLICY_JSON).unwrap()) + .policy(Policy::from_json(POLICY_JSON).unwrap()) .options, ); } diff --git a/ngrok/src/config/policies.rs b/ngrok/src/config/policies.rs index f50012b..dff0bd5 100644 --- a/ngrok/src/config/policies.rs +++ b/ngrok/src/config/policies.rs @@ -11,31 +11,24 @@ use thiserror::Error; use crate::internals::proto; -/// A set of policies that define rules that should be applied to incoming or outgoing +/// A policy that defines rules that should be applied to incoming or outgoing /// connections to the edge. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] -pub struct Policies { - policies: PolicySet, -} - -/// A private layer to hold the inbound and outbound policies -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] -#[serde(default)] -struct PolicySet { - inbound: Vec, - outbound: Vec, +pub struct Policy { + inbound: Vec, + outbound: Vec, } -/// A policy that defines rules that should be applied +/// A policy rule that should be applied #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] #[serde(default)] -pub struct Policy { +pub struct Rule { name: String, expressions: Vec, actions: Vec, } -/// An action that should be taken if the policy matches +/// An action that should be taken if the rule matches #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] #[serde(default)] pub struct Action { @@ -44,70 +37,70 @@ pub struct Action { config: Option, } -/// Error representing invalid string for Policies +/// Errors in creating or serializing Policies #[derive(Debug, Clone, Error)] -pub enum InvalidPolicies { - /// Error representing invalid string for Policies - #[error("failure to parse policies, err: {}", .0)] +pub enum InvalidPolicy { + /// Error representing an invalid string for a Policy + #[error("failure to parse policy, err: {}", .0)] ParseError(String), - /// Error representing invalid string for Policies - #[error("failure to serialize policies, err: {}", .0)] + /// An error generate a Policy json string + #[error("failure to serialize policy, err: {}", .0)] GenerationError(String), - /// Error representing invalid string for Policies - #[error("failure to read policies file '{}', err: {}", .0, .1)] + /// An error loading a Policy from a file + #[error("failure to read policy file '{}', err: {}", .0, .1)] FileReadError(String, String), } -impl Policies { - /// Create a new empty [Policies] struct +impl Policy { + /// Create a new empty [Policy] struct pub fn new() -> Self { - Policies { + Policy { ..Default::default() } } - /// Create a new [Policies] from a json string - pub fn from_json(json: impl AsRef) -> Result { - serde_json::from_str(json.as_ref()).map_err(|e| InvalidPolicies::ParseError(e.to_string())) + /// Create a new [Policy] from a json string + pub fn from_json(json: impl AsRef) -> Result { + serde_json::from_str(json.as_ref()).map_err(|e| InvalidPolicy::ParseError(e.to_string())) } - /// Create a new [Policies] from a json file - pub fn from_file(json_file_path: impl AsRef) -> Result { - Policies::from_json(read_to_string(json_file_path.as_ref()).map_err(|e| { - InvalidPolicies::FileReadError(json_file_path.as_ref().to_string(), e.to_string()) + /// Create a new [Policy] from a json file + pub fn from_file(json_file_path: impl AsRef) -> Result { + Policy::from_json(read_to_string(json_file_path.as_ref()).map_err(|e| { + InvalidPolicy::FileReadError(json_file_path.as_ref().to_string(), e.to_string()) })?) } - /// Convert [Policies] to json string - pub fn to_json(&self) -> Result { - serde_json::to_string(&self).map_err(|e| InvalidPolicies::GenerationError(e.to_string())) + /// Convert [Policy] to json string + pub fn to_json(&self) -> Result { + serde_json::to_string(&self).map_err(|e| InvalidPolicy::GenerationError(e.to_string())) } /// Add an inbound policy - pub fn add_inbound(&mut self, policy: impl Borrow) -> &mut Self { - self.policies.inbound.push(policy.borrow().to_owned()); + pub fn add_inbound(&mut self, policy: impl Borrow) -> &mut Self { + self.inbound.push(policy.borrow().to_owned()); self } /// Add an outbound policy - pub fn add_outbound(&mut self, policy: impl Borrow) -> &mut Self { - self.policies.outbound.push(policy.borrow().to_owned()); + pub fn add_outbound(&mut self, policy: impl Borrow) -> &mut Self { + self.outbound.push(policy.borrow().to_owned()); self } } -impl Policy { - /// Create a new [Policy] +impl Rule { + /// Create a new [Rule] pub fn new(name: impl Into) -> Self { - Policy { + Rule { name: name.into(), ..Default::default() } } - /// Convert [Policy] to json string - pub fn to_json(&self) -> Result { - serde_json::to_string(&self).map_err(|e| InvalidPolicies::GenerationError(e.to_string())) + /// Convert [Rule] to json string + pub fn to_json(&self) -> Result { + serde_json::to_string(&self).map_err(|e| InvalidPolicy::GenerationError(e.to_string())) } /// Add an expression @@ -125,36 +118,36 @@ impl Policy { impl Action { /// Create a new [Action] - pub fn new(type_: impl Into, config: Option<&str>) -> Result { + pub fn new(type_: impl Into, config: Option<&str>) -> Result { Ok(Action { type_: type_.into(), config: config .map(|c| { - serde_json::from_str(c).map_err(|e| InvalidPolicies::ParseError(e.to_string())) + serde_json::from_str(c).map_err(|e| InvalidPolicy::ParseError(e.to_string())) }) .transpose()?, }) } /// Convert [Action] to json string - pub fn to_json(&self) -> Result { - serde_json::to_string(&self).map_err(|e| InvalidPolicies::GenerationError(e.to_string())) + pub fn to_json(&self) -> Result { + serde_json::to_string(&self).map_err(|e| InvalidPolicy::GenerationError(e.to_string())) } } // transform into the wire protocol format -impl From for proto::Policies { - fn from(o: Policies) -> Self { - proto::Policies { - inbound: o.policies.inbound.into_iter().map(|p| p.into()).collect(), - outbound: o.policies.outbound.into_iter().map(|p| p.into()).collect(), +impl From 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 for proto::Policy { - fn from(p: Policy) -> Self { - proto::Policy { +impl From 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(), @@ -179,31 +172,29 @@ pub(crate) mod test { use super::*; pub(crate) const POLICY_JSON: &str = r###" - {"policies": - {"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}}] - } - ]} - } + {"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_policies() { - let pol = Policies::from_json(POLICY_JSON).unwrap(); - assert_eq!(1, pol.policies.inbound.len()); - assert_eq!(1, pol.policies.outbound.len()); - let inbound = &pol.policies.inbound[0]; - let outbound = &pol.policies.outbound[0]; + 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()); @@ -224,35 +215,35 @@ pub(crate) mod test { } #[test] - fn test_policies_to_json() { - let policies = Policies::from_json(POLICY_JSON).unwrap(); - let json = policies.to_json().unwrap(); - let policies2 = Policies::from_json(json).unwrap(); - assert_eq!(policies, policies2); + 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_policies_to_json_error() { - let error = Policies::from_json("asdf").err().unwrap(); - assert!(matches!(error, InvalidPolicies::ParseError { .. })); + fn test_policy_to_json_error() { + let error = Policy::from_json("asdf").err().unwrap(); + assert!(matches!(error, InvalidPolicy::ParseError { .. })); } #[test] - fn test_policy_to_json() { - let pol = Policies::from_json(POLICY_JSON).unwrap(); - let policy = &pol.policies.outbound[0]; - let json = policy.to_json().unwrap(); + 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 policy_map = parsed.as_object().unwrap(); - assert_eq!("test_out", policy_map["name"]); + let rule_map = parsed.as_object().unwrap(); + assert_eq!("test_out", rule_map["name"]); // expressions - let expressions = policy_map["expressions"].as_array().unwrap(); + let expressions = rule_map["expressions"].as_array().unwrap(); assert_eq!(1, expressions.len()); assert_eq!("res.StatusCode == '200'", expressions[0]); // actions - let actions = policy_map["actions"].as_array().unwrap(); + 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"]); @@ -260,8 +251,8 @@ pub(crate) mod test { #[test] fn test_action_to_json() { - let pol = Policies::from_json(POLICY_JSON).unwrap(); - let action = &pol.policies.outbound[0].actions[0]; + 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(); @@ -271,15 +262,15 @@ pub(crate) mod test { #[test] fn test_builders() { - let policies = Policies::from_json(POLICY_JSON).unwrap(); - let policies2 = Policies::new() + let policy = Policy::from_json(POLICY_JSON).unwrap(); + let policy2 = Policy::new() .add_inbound( - Policy::new("test_in") + Rule::new("test_in") .add_expression("req.Method == 'PUT'") .add_action(Action::new("deny", None).unwrap()), ) .add_outbound( - Policy::new("test_out") + Rule::new("test_out") .add_expression("res.StatusCode == '200'") // .add_action(Action::new("deny", "")) .add_action( @@ -287,21 +278,21 @@ pub(crate) mod test { ), ) .to_owned(); - assert_eq!(policies, policies2); + assert_eq!(policy, policy2); } #[test] fn test_load_file() { - let policies = Policies::from_json(POLICY_JSON).unwrap(); - let policies2 = Policies::from_file("assets/policies.json").unwrap(); - assert_eq!("test_in", policies2.policies.inbound[0].name); - assert_eq!("test_out", policies2.policies.outbound[0].name); - assert_eq!(policies, policies2); + 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_file_error() { - let error = Policies::from_file("assets/absent.json").err().unwrap(); - assert!(matches!(error, InvalidPolicies::FileReadError { .. })); + let error = Policy::from_file("assets/absent.json").err().unwrap(); + assert!(matches!(error, InvalidPolicy::FileReadError { .. })); } } diff --git a/ngrok/src/config/tcp.rs b/ngrok/src/config/tcp.rs index c5a0919..1b7094a 100644 --- a/ngrok/src/config/tcp.rs +++ b/ngrok/src/config/tcp.rs @@ -7,7 +7,7 @@ use url::Url; use super::{ common::ProxyProto, - Policies, + Policy, }; // These are used for doc comment links. #[allow(unused_imports)] @@ -71,7 +71,7 @@ impl TunnelConfig for TcpOptions { tcp_endpoint.ip_restriction = self.common_opts.ip_restriction(); - tcp_endpoint.policies = self.common_opts.policies.clone().map(From::from); + tcp_endpoint.policy = self.common_opts.policy.clone().map(From::from); Some(BindOpts::Tcp(tcp_endpoint)) } @@ -135,9 +135,9 @@ impl TcpTunnelBuilder { self } - /// Set the policies for this edge. - pub fn policies(&mut self, policies: impl Borrow) -> &mut Self { - self.options.common_opts.policies = Some(policies.borrow().to_owned()); + /// Set the policy for this edge. + pub fn policy(&mut self, policy: impl Borrow) -> &mut Self { + self.options.common_opts.policy = Some(policy.borrow().to_owned()); self } @@ -173,7 +173,7 @@ mod test { .metadata(METADATA) .remote_addr(REMOTE_ADDR) .forwards_to(TEST_FORWARD) - .policies(Policies::from_json(POLICY_JSON).unwrap()) + .policy(Policy::from_json(POLICY_JSON).unwrap()) .options, ); } diff --git a/ngrok/src/config/tls.rs b/ngrok/src/config/tls.rs index 25342a2..ccb0373 100644 --- a/ngrok/src/config/tls.rs +++ b/ngrok/src/config/tls.rs @@ -11,7 +11,7 @@ use url::Url; use super::{ common::ProxyProto, - Policies, + Policy, }; // These are used for doc comment links. #[allow(unused_imports)] @@ -94,7 +94,7 @@ impl TunnelConfig for TlsOptions { 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.policies = self.common_opts.policies.clone().map(From::from); + tls_endpoint.policy = self.common_opts.policy.clone().map(From::from); Some(BindOpts::Tls(tls_endpoint)) } @@ -178,9 +178,9 @@ impl TlsTunnelBuilder { self } - /// Set the policies for this edge. - pub fn policies(&mut self, policies: impl Borrow) -> &mut Self { - self.options.common_opts.policies = Some(policies.borrow().to_owned()); + /// Set the policy for this edge. + pub fn policy(&mut self, policy: impl Borrow) -> &mut Self { + self.options.common_opts.policy = Some(policy.borrow().to_owned()); self } @@ -223,7 +223,7 @@ mod test { .mutual_tlsca(CA_CERT2.into()) .termination(CERT.into(), KEY.into()) .forwards_to(TEST_FORWARD) - .policies(Policies::from_json(POLICY_JSON).unwrap()) + .policy(Policy::from_json(POLICY_JSON).unwrap()) .options, ); } diff --git a/ngrok/src/internals/proto.rs b/ngrok/src/internals/proto.rs index cf3cef9..1a835a4 100644 --- a/ngrok/src/internals/proto.rs +++ b/ngrok/src/internals/proto.rs @@ -681,7 +681,7 @@ pub struct HttpEndpoint { pub websocket_tcp_converter: Option, #[serde(rename = "UserAgentFilter")] pub user_agent_filter: Option, - pub policies: Option, + pub policy: Option, } #[derive(Debug, Clone, Copy, Serialize, Deserialize)] @@ -807,7 +807,7 @@ pub struct TcpEndpoint { pub proxy_proto: ProxyProto, #[serde(rename = "IPRestriction")] pub ip_restriction: Option, - pub policies: Option, + pub policy: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -825,7 +825,7 @@ pub struct TlsEndpoint { pub tls_termination: Option, #[serde(rename = "IPRestriction")] pub ip_restriction: Option, - pub policies: Option, + pub policy: Option, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] @@ -846,14 +846,14 @@ pub struct LabelEndpoint { #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(rename_all = "PascalCase", default)] -pub struct Policies { - pub inbound: Vec, - pub outbound: Vec, +pub struct Policy { + pub inbound: Vec, + pub outbound: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, Default)] #[serde(rename_all = "PascalCase", default)] -pub struct Policy { +pub struct Rule { pub name: String, pub expressions: Vec, pub actions: Vec, diff --git a/ngrok/src/lib.rs b/ngrok/src/lib.rs index 5e7b1cb..e56bfde 100644 --- a/ngrok/src/lib.rs +++ b/ngrok/src/lib.rs @@ -69,13 +69,13 @@ pub mod prelude { Action, ForwarderBuilder, HttpTunnelBuilder, - InvalidPolicies, + InvalidPolicy, LabeledTunnelBuilder, OauthOptions, OidcOptions, - Policies, Policy, ProxyProto, + Rule, Scheme, TcpTunnelBuilder, TlsTunnelBuilder, diff --git a/ngrok/src/online_tests.rs b/ngrok/src/online_tests.rs index 103394b..ab73b22 100644 --- a/ngrok/src/online_tests.rs +++ b/ngrok/src/online_tests.rs @@ -389,10 +389,10 @@ async fn custom_domain() -> Result<(), Error> { #[traced_test] #[cfg_attr(not(feature = "paid-tests"), ignore)] #[test] -async fn policies() -> Result<(), Error> { +async fn policy() -> Result<(), Error> { let tun = serve_http( defaults, - |tun| tun.policies(create_policies().unwrap()), + |tun| tun.policy(create_policy().unwrap()), hello_router(), ) .await?; @@ -404,15 +404,15 @@ async fn policies() -> Result<(), Error> { Ok(()) } -fn create_policies() -> Result { - Ok(Policies::new() +fn create_policy() -> Result { + Ok(Policy::new() .add_inbound( - Policy::new("deny_put") + Rule::new("deny_put") .add_expression("req.Method == 'PUT'") .add_action(Action::new("deny", None)?), ) .add_outbound( - Policy::new("222_response") + Rule::new("222_response") .add_expression("res.StatusCode == '200'") .add_action(Action::new( "custom-response", From b5b57114ddcfbfc1e87812de6d1449e44f5a4b83 Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Thu, 25 Jan 2024 14:01:47 -0500 Subject: [PATCH 5/6] Policy: source errors, Borrow to Into --- ngrok/examples/axum.rs | 2 +- ngrok/src/config/http.rs | 12 ++++-- ngrok/src/config/policies.rs | 77 ++++++++++++++++++++++++------------ ngrok/src/config/tcp.rs | 17 ++++---- ngrok/src/config/tls.rs | 17 ++++---- ngrok/src/online_tests.rs | 2 +- 6 files changed, 80 insertions(+), 47 deletions(-) diff --git a/ngrok/examples/axum.rs b/ngrok/examples/axum.rs index f10c5fb..b5281ae 100644 --- a/ngrok/examples/axum.rs +++ b/ngrok/examples/axum.rs @@ -61,7 +61,7 @@ async fn start_tunnel() -> anyhow::Result { // .allow_domain("") // .scope(""), // ) - // .policy(create_policy()?) + // .policy(create_policy())? // .proxy_proto(ProxyProto::None) // .remove_request_header("X-Req-Nope") // .remove_response_header("X-Res-Nope") diff --git a/ngrok/src/config/http.rs b/ngrok/src/config/http.rs index 83a2b70..577d08b 100644 --- a/ngrok/src/config/http.rs +++ b/ngrok/src/config/http.rs @@ -425,9 +425,12 @@ impl HttpTunnelBuilder { } /// Set the policy for this edge. - pub fn policy(&mut self, policy: impl Borrow) -> &mut Self { - self.options.common_opts.policy = Some(policy.borrow().to_owned()); - self + pub fn policy(&mut self, s: S) -> Result<&mut Self, S::Error> + where + S: TryInto, + { + self.options.common_opts.policy = Some(s.try_into()?); + Ok(self) } pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self { @@ -499,7 +502,8 @@ mod test { .basic_auth("ngrok", "online1line") .forwards_to(TEST_FORWARD) .app_protocol("http2") - .policy(Policy::from_json(POLICY_JSON).unwrap()) + .policy(POLICY_JSON) + .unwrap() .options, ); } diff --git a/ngrok/src/config/policies.rs b/ngrok/src/config/policies.rs index dff0bd5..28aa2c8 100644 --- a/ngrok/src/config/policies.rs +++ b/ngrok/src/config/policies.rs @@ -1,6 +1,6 @@ use std::{ - borrow::Borrow, fs::read_to_string, + io, }; use serde::{ @@ -38,17 +38,14 @@ pub struct Action { } /// Errors in creating or serializing Policies -#[derive(Debug, Clone, Error)] +#[derive(Debug, Error)] pub enum InvalidPolicy { /// Error representing an invalid string for a Policy - #[error("failure to parse policy, err: {}", .0)] - ParseError(String), - /// An error generate a Policy json string - #[error("failure to serialize policy, err: {}", .0)] - GenerationError(String), + #[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 '{}', err: {}", .0, .1)] - FileReadError(String, String), + #[error("failure to read policy file '{}'", .1)] + FileReadError(#[source] io::Error, String), } impl Policy { @@ -60,35 +57,61 @@ impl Policy { } /// Create a new [Policy] from a json string - pub fn from_json(json: impl AsRef) -> Result { - serde_json::from_str(json.as_ref()).map_err(|e| InvalidPolicy::ParseError(e.to_string())) + fn from_json(json: impl AsRef) -> Result { + 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) -> Result { - Policy::from_json(read_to_string(json_file_path.as_ref()).map_err(|e| { - InvalidPolicy::FileReadError(json_file_path.as_ref().to_string(), e.to_string()) - })?) + 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 { - serde_json::to_string(&self).map_err(|e| InvalidPolicy::GenerationError(e.to_string())) + serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError) } /// Add an inbound policy - pub fn add_inbound(&mut self, policy: impl Borrow) -> &mut Self { - self.inbound.push(policy.borrow().to_owned()); + pub fn add_inbound(&mut self, rule: impl Into) -> &mut Self { + self.inbound.push(rule.into()); self } /// Add an outbound policy - pub fn add_outbound(&mut self, policy: impl Borrow) -> &mut Self { - self.outbound.push(policy.borrow().to_owned()); + pub fn add_outbound(&mut self, rule: impl Into) -> &mut Self { + self.outbound.push(rule.into()); self } } +impl TryFrom<&Policy> for Policy { + type Error = InvalidPolicy; + + fn try_from(other: &Policy) -> Result { + Ok(other.clone()) + } +} + +impl TryFrom> for Policy { + type Error = InvalidPolicy; + + fn try_from(other: Result) -> Result { + other + } +} + +impl TryFrom<&str> for Policy { + type Error = InvalidPolicy; + + fn try_from(other: &str) -> Result { + Policy::from_json(other) + } +} + impl Rule { /// Create a new [Rule] pub fn new(name: impl Into) -> Self { @@ -100,7 +123,7 @@ impl Rule { /// Convert [Rule] to json string pub fn to_json(&self) -> Result { - serde_json::to_string(&self).map_err(|e| InvalidPolicy::GenerationError(e.to_string())) + serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError) } /// Add an expression @@ -116,22 +139,26 @@ impl Rule { } } +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, config: Option<&str>) -> Result { Ok(Action { type_: type_.into(), config: config - .map(|c| { - serde_json::from_str(c).map_err(|e| InvalidPolicy::ParseError(e.to_string())) - }) + .map(|c| serde_json::from_str(c).map_err(InvalidPolicy::SerializationError)) .transpose()?, }) } /// Convert [Action] to json string pub fn to_json(&self) -> Result { - serde_json::to_string(&self).map_err(|e| InvalidPolicy::GenerationError(e.to_string())) + serde_json::to_string(&self).map_err(InvalidPolicy::SerializationError) } } @@ -225,7 +252,7 @@ pub(crate) mod test { #[test] fn test_policy_to_json_error() { let error = Policy::from_json("asdf").err().unwrap(); - assert!(matches!(error, InvalidPolicy::ParseError { .. })); + assert!(matches!(error, InvalidPolicy::SerializationError { .. })); } #[test] diff --git a/ngrok/src/config/tcp.rs b/ngrok/src/config/tcp.rs index 1b7094a..a1edcd3 100644 --- a/ngrok/src/config/tcp.rs +++ b/ngrok/src/config/tcp.rs @@ -1,7 +1,4 @@ -use std::{ - borrow::Borrow, - collections::HashMap, -}; +use std::collections::HashMap; use url::Url; @@ -136,9 +133,12 @@ impl TcpTunnelBuilder { } /// Set the policy for this edge. - pub fn policy(&mut self, policy: impl Borrow) -> &mut Self { - self.options.common_opts.policy = Some(policy.borrow().to_owned()); - self + pub fn policy(&mut self, s: S) -> Result<&mut Self, S::Error> + where + S: TryInto, + { + self.options.common_opts.policy = Some(s.try_into()?); + Ok(self) } pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self { @@ -173,7 +173,8 @@ mod test { .metadata(METADATA) .remote_addr(REMOTE_ADDR) .forwards_to(TEST_FORWARD) - .policy(Policy::from_json(POLICY_JSON).unwrap()) + .policy(POLICY_JSON) + .unwrap() .options, ); } diff --git a/ngrok/src/config/tls.rs b/ngrok/src/config/tls.rs index ccb0373..02f1846 100644 --- a/ngrok/src/config/tls.rs +++ b/ngrok/src/config/tls.rs @@ -1,7 +1,4 @@ -use std::{ - borrow::Borrow, - collections::HashMap, -}; +use std::collections::HashMap; use bytes::{ self, @@ -179,9 +176,12 @@ impl TlsTunnelBuilder { } /// Set the policy for this edge. - pub fn policy(&mut self, policy: impl Borrow) -> &mut Self { - self.options.common_opts.policy = Some(policy.borrow().to_owned()); - self + pub fn policy(&mut self, s: S) -> Result<&mut Self, S::Error> + where + S: TryInto, + { + self.options.common_opts.policy = Some(s.try_into()?); + Ok(self) } pub(crate) async fn for_forwarding_to(&mut self, to_url: &Url) -> &mut Self { @@ -223,7 +223,8 @@ mod test { .mutual_tlsca(CA_CERT2.into()) .termination(CERT.into(), KEY.into()) .forwards_to(TEST_FORWARD) - .policy(Policy::from_json(POLICY_JSON).unwrap()) + .policy(POLICY_JSON) + .unwrap() .options, ); } diff --git a/ngrok/src/online_tests.rs b/ngrok/src/online_tests.rs index ab73b22..cefabb0 100644 --- a/ngrok/src/online_tests.rs +++ b/ngrok/src/online_tests.rs @@ -392,7 +392,7 @@ async fn custom_domain() -> Result<(), Error> { async fn policy() -> Result<(), Error> { let tun = serve_http( defaults, - |tun| tun.policy(create_policy().unwrap()), + |tun| tun.policy(create_policy()).unwrap(), hello_router(), ) .await?; From a9acaebe49876efaba93c0aed24e4e5880d5231a Mon Sep 17 00:00:00 2001 From: bobzilladev Date: Tue, 30 Jan 2024 10:33:16 -0500 Subject: [PATCH 6/6] Bump version to "0.14.0-pre.10" --- ngrok/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngrok/Cargo.toml b/ngrok/Cargo.toml index 28d9137..6dfec98 100644 --- a/ngrok/Cargo.toml +++ b/ngrok/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ngrok" -version = "0.14.0-pre.9" +version = "0.14.0-pre.10" edition = "2021" license = "MIT OR Apache-2.0" description = "The ngrok agent SDK"