From 6956c8fd35e8af7bd0d0edeffd7f51a9febd67ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Wed, 5 Nov 2025 17:03:41 -0800 Subject: [PATCH 1/6] feat(inspect): add support for NGWAF inspect api --- Cargo.lock | 49 +++++++--------- Cargo.toml | 6 +- src/lib.rs | 71 +++++++++++++++++++++++ src/security.rs | 149 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 243 insertions(+), 32 deletions(-) create mode 100644 src/security.rs diff --git a/Cargo.lock b/Cargo.lock index 4ea9643..457fa64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "fastly" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9506877e1713a00a6a676191c29cc449aa60129da123d81245b203a4f6734dd3" +checksum = "ac590af69cdea42ebbbaa566d0e603c6c0d7d6f53a507fe82cea65260419ab88" dependencies = [ "anyhow", "bytes", @@ -254,7 +254,7 @@ dependencies = [ "elsa", "fastly-macros", "fastly-shared", - "fastly-sys 0.11.5", + "fastly-sys 0.11.9", "http", "itertools", "lazy_static", @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "fastly-macros" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3331e33178c193355093de80c370b3d65ed7a61631a38ad32bf84bedec6ca9e8" +checksum = "b012bd5c924ede9a1363ad29a232c4e95c9eb520a124979ad06043a6e44025dc" dependencies = [ "proc-macro2", "quote", @@ -283,9 +283,9 @@ dependencies = [ [[package]] name = "fastly-shared" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80cbb4ef1d9c7f4321179814dd51b39f2a91f0993a7303396bb34581ae4f5daa" +checksum = "fe8aaf17b8c0b689ce8370052e129c7722f3bd9c5ca27790db7624cf64b8c9b1" dependencies = [ "bitflags 1.3.2", "http", @@ -310,14 +310,14 @@ dependencies = [ [[package]] name = "fastly-sys" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583231411fcb7ba818ca7126b2e97038efc04d29950902b3cd11b33741303d80" +checksum = "a784af8ed4e5f3d32aac54f687b6a2dd844af304390d3bc70d50cbe6a772c1a7" dependencies = [ "bitflags 1.3.2", "fastly-shared", - "wasi", - "wit-bindgen-rt 0.42.1", + "wasip2", + "wit-bindgen", ] [[package]] @@ -544,9 +544,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "log-fastly" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5cb1cc40a2a1daaa295854ae3c9e0ead63c3f7ad267e6979aaee6ac9635b9c" +checksum = "c67b1d4ff825b027926a385cea5a9f43d0b5a900030d48736833a552120e2b59" dependencies = [ "fastly", "log", @@ -934,12 +934,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt 0.39.0", + "wit-bindgen", ] [[package]] @@ -1025,19 +1025,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051105bab12bc78e161f8dfb3596e772dd6a01ebf9c4840988e00347e744966a" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" dependencies = [ "bitflags 2.9.1", ] diff --git a/Cargo.toml b/Cargo.toml index e260689..7d7c521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,11 @@ edition = "2024" [dependencies] cxx = { version = "1.0.158", features = ["c++17"] } -fastly = "0.11.4" -fastly-shared = "0.11.5" +fastly = "0.11.9" +fastly-shared = "0.11.9" http = "1.3.1" log = "0.4.27" -log-fastly = "0.11.5" +log-fastly = "0.11.9" thiserror = "2.0.12" esi = "0.6.1" quick-xml = "0.38.3" diff --git a/src/lib.rs b/src/lib.rs index 6bf491a..3573dd0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ use http::{ use kv_store::*; use log::*; use secret_store::*; +use security::*; mod backend; mod config_store; @@ -25,6 +26,7 @@ mod http; mod kv_store; mod log; mod secret_store; +mod security; // Unfortunately, due to some limitations with cxx, the ENTIRE bridge basically // has to be under a single ffi module, or cross-referencing ffi types is gonna @@ -817,6 +819,75 @@ mod ffi { fn init(&mut self); } + #[namespace = "fastly::sys::security"] + #[derive(Copy, Clone, Debug)] + #[repr(usize)] + pub enum InspectErrorCode { + DeserializeError, + InvalidConfig, + RequestError, + BufferSizeError, + Unexpected, + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + type InspectConfig; + fn m_static_inspect_inspect_config_new() -> Box; + fn client_ip(&mut self, ip: &CxxString, err: Pin<&mut *mut FastlyError>); + fn workspace(&mut self, workspace: &CxxString, err: Pin<&mut *mut FastlyError>); + fn corp(&mut self, corp: &CxxString, err: Pin<&mut *mut FastlyError>); + fn buffer_size(&mut self, buffer_size: usize); + } + + #[namespace = "fastly::sys::security"] + #[derive(Copy, Clone, Debug)] + pub enum InspectVerdict { + /// Security indicated that this request is allowed. + Allow, + + /// Security indicated that this request should be blocked. + Block, + + /// Security indicated that this service is not authorized to inspect a request. + Unauthorized, + + /// Security returned an unrecognized verdict. + /// + /// This variant exists to allow for the possibility of future + /// additions, but should normally not be seen. + Other, + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + type InspectResponse; + fn status(&self) -> i16; + fn is_redirect(&self) -> bool; + fn decision_ms(&self) -> u32; + fn redirect_url(&self, out: Pin<&mut CxxString>) -> bool; + fn tags(&self) -> Vec; + fn verdict(&self) -> InspectVerdict; + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + fn f_security_lookup( + request: &Request, + config: Box, + mut out: Pin<&mut *mut InspectResponse>, + mut err: Pin<&mut *mut InspectError>, + ); + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + type InspectError; + fn error_msg(&self, mut out: Pin<&mut CxxString>); + fn error_code(&self) -> InspectErrorCode; + fn f_security_inspect_error_force_symbols(x: Box) -> Box; + } + #[namespace = "fastly::sys::kv_store"] #[derive(Copy, Clone, Debug)] #[repr(usize)] diff --git a/src/security.rs b/src/security.rs new file mode 100644 index 0000000..5c1403a --- /dev/null +++ b/src/security.rs @@ -0,0 +1,149 @@ +use std::{io::Write as _, net::IpAddr, pin::Pin}; + +use cxx::CxxString; + +use crate::{ + error::FastlyError, + ffi::{InspectErrorCode, InspectVerdict}, + http::request::Request, + try_fe, +}; + +#[derive(Debug, Default)] +pub struct InspectConfig { + client_ip: Option, + workspace: Option, + corp: Option, + buffer_size: Option, +} + +pub struct InspectResponse(pub(crate) fastly::security::InspectResponse); + +pub struct InspectError(pub(crate) fastly::security::InspectError); + +impl InspectError { + pub fn error_msg(&self, mut out: Pin<&mut CxxString>) { + write!(out, "{}", self.0).expect("This should never fail."); + } + + pub fn error_code(&self) -> InspectErrorCode { + match self.0 { + fastly::security::InspectError::DeserializeError(_) => { + InspectErrorCode::DeserializeError + } + fastly::security::InspectError::InvalidConfig => InspectErrorCode::InvalidConfig, + fastly::security::InspectError::RequestError(_) => InspectErrorCode::RequestError, + fastly::security::InspectError::BufferSizeError(_) => InspectErrorCode::BufferSizeError, + _ => InspectErrorCode::Unexpected, + } + } +} + +#[macro_export] +macro_rules! try_ie { + ( $err:ident, $x:expr ) => { + match $x { + std::result::Result::Ok(val) => { + $err.set(std::ptr::null_mut()); + val + } + std::result::Result::Err(e) => { + $err.set(Box::into_raw(Box::new(InspectError(e)))); + return Default::default(); + } + } + }; +} + +pub fn f_security_lookup( + request: &Request, + config: Box, + mut out: Pin<&mut *mut InspectResponse>, + mut err: Pin<&mut *mut InspectError>, +) { + let mut icfg = fastly::security::InspectConfig::from_request(&request.0); + if let Some(ip_addr) = config.client_ip { + icfg = icfg.client_ip(ip_addr); + } + if let Some(corp) = config.corp { + icfg = icfg.corp(corp); + } + if let Some(ws) = config.workspace { + icfg = icfg.workspace(ws); + } + if let Some(sz) = config.buffer_size { + icfg = icfg.buffer_size(sz); + } + out.set(try_ie!( + err, + fastly::security::inspect(icfg) + .map(InspectResponse) + .map(Box::new) + .map(Box::into_raw) + )) +} + +pub fn m_static_inspect_inspect_config_new() -> Box { + Box::default() +} + +impl InspectConfig { + pub fn client_ip(&mut self, ip: &CxxString, mut err: Pin<&mut *mut FastlyError>) { + let s = try_fe!(err, ip.to_str()); + self.client_ip = Some(try_fe!(err, s.parse())); + } + + pub fn workspace(&mut self, workspace: &CxxString, mut err: Pin<&mut *mut FastlyError>) { + let s = try_fe!(err, workspace.to_str()); + self.workspace = Some(s.into()); + } + + pub fn corp(&mut self, corp: &CxxString, mut err: Pin<&mut *mut FastlyError>) { + let s = try_fe!(err, corp.to_str()); + self.corp = Some(s.into()); + } + + pub fn buffer_size(&mut self, buffer_size: usize) { + self.buffer_size = Some(buffer_size); + } +} + +impl InspectResponse { + pub fn status(&self) -> i16 { + self.0.status() + } + + pub fn is_redirect(&self) -> bool { + self.0.is_redirect() + } + + pub fn decision_ms(&self) -> u32 { + self.0.decision_ms().as_millis().min(u32::MAX as u128) as u32 + } + + pub fn redirect_url(&self, out: Pin<&mut CxxString>) -> bool { + if let Some(url) = self.0.redirect_url() { + out.push_str(url); + true + } else { + false + } + } + + pub fn tags(&self) -> Vec { + self.0.tags().into_iter().map(|x| x.into()).collect() + } + + pub fn verdict(&self) -> InspectVerdict { + match self.0.verdict() { + fastly::security::InspectVerdict::Allow => InspectVerdict::Allow, + fastly::security::InspectVerdict::Block => InspectVerdict::Block, + fastly::security::InspectVerdict::Unauthorized => InspectVerdict::Unauthorized, + fastly::security::InspectVerdict::Other(_) => InspectVerdict::Other, + } + } +} + +pub fn f_security_inspect_error_force_symbols(x: Box) -> Box { + x +} From e73aed8414084b735f6819638f19b3448ab0e38f Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 6 Nov 2025 12:58:37 +0000 Subject: [PATCH 2/6] Implement c++ side --- include/fastly/security.h | 102 ++++++++++++++++++++++++++++++++++++++ src/cpp/security.cpp | 66 ++++++++++++++++++++++++ src/lib.rs | 22 ++++---- src/security.rs | 97 +++++++++++++++++++----------------- 4 files changed, 229 insertions(+), 58 deletions(-) create mode 100644 include/fastly/security.h create mode 100644 src/cpp/security.cpp diff --git a/include/fastly/security.h b/include/fastly/security.h new file mode 100644 index 0000000..6371da0 --- /dev/null +++ b/include/fastly/security.h @@ -0,0 +1,102 @@ +#ifndef FASTLY_SECURITY_H +#define FASTLY_SECURITY_H + +#include +#include +#include +#include +#include + +namespace fastly::security { +/// Configuration for inspecting a `Request` using Security. +class InspectConfig { +public: + /// Create a new default `InspectConfig` + InspectConfig() = default; + + /// Specify an explicity client IP address to inspect. + /// By default, inspect will use the IP address that made the request to the + /// running Compute service, but you may want to use a different IP when + /// service chaining or if requests are proxied from outside of Fastly’s + /// network. + InspectConfig with_client_ip(std::string ip) && { + this->client_ip_ = std::move(ip); + return std::move(*this); + } + + /// Set a corp name for the configuration. + InspectConfig with_corp(std::string name) && { + this->corp_ = std::move(name); + return std::move(*this); + } + + /// Set a workspace name for the configuration. + InspectConfig with_workspace(std::string name) && { + this->workspace_ = std::move(name); + return std::move(*this); + } + + /// Set a buffer size for the response. + InspectConfig with_buffer_size(std::size_t size) && { + this->buffer_size_ = size; + return std::move(*this); + } + + const std::optional &client_ip() const { return client_ip_; } + const std::optional &corp() const { return corp_; } + const std::optional &workspace() const { return workspace_; } + const std::optional &buffer_size() const { return buffer_size_; } + +private: + std::optional client_ip_; + std::optional corp_; + std::optional workspace_; + std::optional buffer_size_; +}; +using fastly::sys::security::InspectErrorCode; +using fastly::sys::security::InspectVerdict; +class InspectError { +public: + InspectError(fastly::sys::security::InspectError *e) + : err_(rust::Box::from_raw(e)) {}; + InspectError(rust::Box e) + : err_(std::move(e)) {}; + InspectErrorCode error_code(); + std::string error_msg(); + +private: + rust::Box err_; +}; + +/// Results of asking Security to inspect a `Request` +class InspectResponse { + friend detail::AccessBridgeInternals; + +public: + /// Security status code. + std::int16_t status() const; + /// A redirect URL returned from Security + std::optional redirect_url() const; + /// Tags returned by Security + std::vector tags() const; + /// Get Security's verdict on how to handle this request. + InspectVerdict verdict() const; + /// Get additional information for verdicts where `this->verdict()` is + /// `Other`. + std::optional unrecognized_verdict_info() const; + /// How long Security spent determining its verdict. + std::chrono::milliseconds decision_ms() const; + /// A redirect URI returned by Security. + bool is_redirect() const; + /// Convert a redirect URI returned by Security into a `Response`. + std::optional into_redirect(); + +private: + rust::Box ir_; + InspectResponse(rust::Box ir) + : ir_(std::move(ir)) {}; +}; +tl::expected inspect(InspectConfig config); +} // namespace fastly::security + +#endif \ No newline at end of file diff --git a/src/cpp/security.cpp b/src/cpp/security.cpp new file mode 100644 index 0000000..f86c6ba --- /dev/null +++ b/src/cpp/security.cpp @@ -0,0 +1,66 @@ +#include "util.h" +#include +#include +#include + +namespace fastly::security { +std::int16_t InspectResponse::status() const { return ir_->status(); } + +std::optional InspectResponse::redirect_url() const { + std::string ret; + if (ir_->redirect_url(ret)) { + return ret; + } + return std::nullopt; +} + +std::vector InspectResponse::tags() const { + auto tags = ir_->tags(); + std::vector ret; + ret.resize(tags.size()); + std::transform(tags.begin(), tags.end(), std::back_inserter(ret), + [](auto str) { return std::string(str); }); + return ret; +} + +InspectVerdict InspectResponse::verdict() const { return ir_->verdict(); } +std::optional InspectResponse::unrecognized_verdict_info() const { + std::string ret; + if (ir_->unrecognized_verdict_info(ret)) { + return ret; + } + return std::nullopt; +} + +std::chrono::milliseconds InspectResponse::decision_ms() const { + return std::chrono::milliseconds(ir_->decision_ms()); +} + +bool InspectResponse::is_redirect() const { return ir_->is_redirect(); } +std::optional InspectResponse::into_redirect() { + fastly::sys::http::Response *resp; + if (fastly::sys::security::m_security_inspect_response_into_redirect( + std::move(ir_), resp)) { + return detail::AccessBridgeInternals::from_raw(resp); + } + return std::nullopt; +} + +tl::expected +inspect(fastly::http::Request &request, InspectConfig config) { + fastly::sys::security::InspectResponse *out; + fastly::sys::security::InspectError *err; + auto client_ip = config.client_ip() ? &*config.client_ip() : nullptr; + auto corp = config.corp() ? &*config.corp() : nullptr; + auto workspace = config.workspace() ? &*config.workspace() : nullptr; + auto buffer_size = config.buffer_size() ? &*config.buffer_size() : nullptr; + fastly::sys::security::f_security_lookup( + *detail::AccessBridgeInternals::get(request), client_ip, corp, workspace, + buffer_size, out, err); + if (err == nullptr) { + return detail::AccessBridgeInternals::from_raw(out); + } else { + return tl::unexpected(err); + } +} +} // namespace fastly::security \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 3573dd0..6be061a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -830,16 +830,6 @@ mod ffi { Unexpected, } - #[namespace = "fastly::sys::security"] - extern "Rust" { - type InspectConfig; - fn m_static_inspect_inspect_config_new() -> Box; - fn client_ip(&mut self, ip: &CxxString, err: Pin<&mut *mut FastlyError>); - fn workspace(&mut self, workspace: &CxxString, err: Pin<&mut *mut FastlyError>); - fn corp(&mut self, corp: &CxxString, err: Pin<&mut *mut FastlyError>); - fn buffer_size(&mut self, buffer_size: usize); - } - #[namespace = "fastly::sys::security"] #[derive(Copy, Clone, Debug)] pub enum InspectVerdict { @@ -868,13 +858,21 @@ mod ffi { fn redirect_url(&self, out: Pin<&mut CxxString>) -> bool; fn tags(&self) -> Vec; fn verdict(&self) -> InspectVerdict; + fn unrecognized_verdict_info(&self, out: Pin<&mut CxxString>) -> bool; } #[namespace = "fastly::sys::security"] extern "Rust" { - fn f_security_lookup( + fn m_security_inspect_response_into_redirect( + response: Box, + mut out: Pin<&mut *mut Response>, + ) -> bool; + unsafe fn f_security_lookup( request: &Request, - config: Box, + client_ip: *const CxxString, + corp: *const CxxString, + workspace: *const CxxString, + buffer_size: *const usize, mut out: Pin<&mut *mut InspectResponse>, mut err: Pin<&mut *mut InspectError>, ); diff --git a/src/security.rs b/src/security.rs index 5c1403a..92feb51 100644 --- a/src/security.rs +++ b/src/security.rs @@ -1,4 +1,4 @@ -use std::{io::Write as _, net::IpAddr, pin::Pin}; +use std::{io::Write as _, net::IpAddr, pin::Pin, str::FromStr}; use cxx::CxxString; @@ -6,17 +6,10 @@ use crate::{ error::FastlyError, ffi::{InspectErrorCode, InspectVerdict}, http::request::Request, + http::response::Response, try_fe, }; -#[derive(Debug, Default)] -pub struct InspectConfig { - client_ip: Option, - workspace: Option, - corp: Option, - buffer_size: Option, -} - pub struct InspectResponse(pub(crate) fastly::security::InspectResponse); pub struct InspectError(pub(crate) fastly::security::InspectError); @@ -57,22 +50,38 @@ macro_rules! try_ie { pub fn f_security_lookup( request: &Request, - config: Box, + client_ip: *const CxxString, + corp: *const CxxString, + workspace: *const CxxString, + buffer_size: *const usize, mut out: Pin<&mut *mut InspectResponse>, mut err: Pin<&mut *mut InspectError>, ) { let mut icfg = fastly::security::InspectConfig::from_request(&request.0); - if let Some(ip_addr) = config.client_ip { - icfg = icfg.client_ip(ip_addr); - } - if let Some(corp) = config.corp { - icfg = icfg.corp(corp); - } - if let Some(ws) = config.workspace { - icfg = icfg.workspace(ws); - } - if let Some(sz) = config.buffer_size { - icfg = icfg.buffer_size(sz); + unsafe { + if !client_ip.is_null() { + let ip = IpAddr::from_str(try_ie!( + err, + client_ip + .read() + .to_str() + .map_err(|_| fastly::security::InspectError::InvalidConfig) + )); + let ip = try_ie!( + err, + ip.map_err(|_| fastly::security::InspectError::InvalidConfig) + ); + icfg = icfg.client_ip(ip); + } + if !corp.is_null() { + icfg = icfg.corp(corp.read().to_string()); + } + if !workspace.is_null() { + icfg = icfg.workspace(workspace.read().to_string()); + } + if !buffer_size.is_null() { + icfg = icfg.buffer_size(*buffer_size); + } } out.set(try_ie!( err, @@ -83,31 +92,6 @@ pub fn f_security_lookup( )) } -pub fn m_static_inspect_inspect_config_new() -> Box { - Box::default() -} - -impl InspectConfig { - pub fn client_ip(&mut self, ip: &CxxString, mut err: Pin<&mut *mut FastlyError>) { - let s = try_fe!(err, ip.to_str()); - self.client_ip = Some(try_fe!(err, s.parse())); - } - - pub fn workspace(&mut self, workspace: &CxxString, mut err: Pin<&mut *mut FastlyError>) { - let s = try_fe!(err, workspace.to_str()); - self.workspace = Some(s.into()); - } - - pub fn corp(&mut self, corp: &CxxString, mut err: Pin<&mut *mut FastlyError>) { - let s = try_fe!(err, corp.to_str()); - self.corp = Some(s.into()); - } - - pub fn buffer_size(&mut self, buffer_size: usize) { - self.buffer_size = Some(buffer_size); - } -} - impl InspectResponse { pub fn status(&self) -> i16 { self.0.status() @@ -142,6 +126,27 @@ impl InspectResponse { fastly::security::InspectVerdict::Other(_) => InspectVerdict::Other, } } + + pub fn unrecognized_verdict_info(&self, out: Pin<&mut CxxString>) -> bool { + if let fastly::security::InspectVerdict::Other(s) = self.0.verdict() { + out.push_str(s); + true + } else { + false + } + } +} + +pub fn m_security_inspect_response_into_redirect( + response: Box, + mut out: Pin<&mut *mut Response>, +) -> bool { + if let Some(resp) = response.0.into_redirect() { + out.set(Box::into_raw(Box::new(Response(resp)))); + true + } else { + false + } } pub fn f_security_inspect_error_force_symbols(x: Box) -> Box { From ad303f67bfc9f40fbacd77ea595efd9769cc1838 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 6 Nov 2025 15:08:49 +0000 Subject: [PATCH 3/6] Finish NGWAF --- include/fastly/security.h | 5 +++-- src/cpp/security.cpp | 7 +++++++ src/security.rs | 13 ++++++------- test/security.cpp | 41 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 test/security.cpp diff --git a/include/fastly/security.h b/include/fastly/security.h index 6371da0..9bc091b 100644 --- a/include/fastly/security.h +++ b/include/fastly/security.h @@ -1,8 +1,8 @@ #ifndef FASTLY_SECURITY_H #define FASTLY_SECURITY_H -#include #include +#include #include #include #include @@ -96,7 +96,8 @@ class InspectResponse { InspectResponse(rust::Box ir) : ir_(std::move(ir)) {}; }; -tl::expected inspect(InspectConfig config); +tl::expected +inspect(fastly::http::Request &request, InspectConfig config); } // namespace fastly::security #endif \ No newline at end of file diff --git a/src/cpp/security.cpp b/src/cpp/security.cpp index f86c6ba..809632f 100644 --- a/src/cpp/security.cpp +++ b/src/cpp/security.cpp @@ -4,6 +4,13 @@ #include namespace fastly::security { +InspectErrorCode InspectError::error_code() { return err_->error_code(); } +std::string InspectError::error_msg() { + std::string msg; + err_->error_msg(msg); + return msg; +} + std::int16_t InspectResponse::status() const { return ir_->status(); } std::optional InspectResponse::redirect_url() const { diff --git a/src/security.rs b/src/security.rs index 92feb51..895a0bc 100644 --- a/src/security.rs +++ b/src/security.rs @@ -59,11 +59,10 @@ pub fn f_security_lookup( ) { let mut icfg = fastly::security::InspectConfig::from_request(&request.0); unsafe { - if !client_ip.is_null() { + if let Some(client_ip) = client_ip.as_ref() { let ip = IpAddr::from_str(try_ie!( err, client_ip - .read() .to_str() .map_err(|_| fastly::security::InspectError::InvalidConfig) )); @@ -73,13 +72,13 @@ pub fn f_security_lookup( ); icfg = icfg.client_ip(ip); } - if !corp.is_null() { - icfg = icfg.corp(corp.read().to_string()); + if let Some(corp) = corp.as_ref() { + icfg = icfg.corp(corp.to_string()); } - if !workspace.is_null() { - icfg = icfg.workspace(workspace.read().to_string()); + if let Some(workspace) = workspace.as_ref() { + icfg = icfg.workspace(workspace.to_string()); } - if !buffer_size.is_null() { + if let Some(buffer_size) = buffer_size.as_ref() { icfg = icfg.buffer_size(*buffer_size); } } diff --git a/test/security.cpp b/test/security.cpp new file mode 100644 index 0000000..9543e63 --- /dev/null +++ b/test/security.cpp @@ -0,0 +1,41 @@ +#include +#include + +using namespace fastly::security; + +TEST_CASE("Mock NGWAF returns allow", "[security]") { + auto request = fastly::http::Request::get("https://example.com"); + auto config = InspectConfig() + .with_corp("test") + .with_workspace("test") + .with_buffer_size(256) + .with_client_ip("10.10.10.10"); + auto res = inspect(request, config); + REQUIRE(res.has_value()); + REQUIRE(res->verdict() == InspectVerdict::Allow); +} + +// This is currently the case in the implementation, but not reflected in the +// API because it is planned to change +TEST_CASE("NGWAF requires corp and workspace", "[security]") { + auto request = fastly::http::Request::get("https://example.com"); + SECTION("Missing corp") { + auto res = inspect(request, InspectConfig().with_workspace("test")); + REQUIRE(!res.has_value()); + REQUIRE(res.error().error_code() == InspectErrorCode::InvalidConfig); + } + SECTION("Missing workspace") { + auto res = inspect(request, InspectConfig().with_corp("test")); + REQUIRE(!res.has_value()); + REQUIRE(res.error().error_code() == InspectErrorCode::InvalidConfig); + } + SECTION("Missing both") { + auto res = inspect(request, InspectConfig()); + REQUIRE(!res.has_value()); + REQUIRE(res.error().error_code() == InspectErrorCode::InvalidConfig); + } +} + +// Required due to https://github.com/WebAssembly/wasi-libc/issues/485 +#include +int main(int argc, char *argv[]) { return Catch::Session().run(argc, argv); } \ No newline at end of file From aff5f7e1609e3b2d4f021a9f16eb7ad0f82580b4 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Thu, 6 Nov 2025 15:30:11 +0000 Subject: [PATCH 4/6] Clippy --- src/security.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/security.rs b/src/security.rs index 895a0bc..d5cb2d5 100644 --- a/src/security.rs +++ b/src/security.rs @@ -3,11 +3,9 @@ use std::{io::Write as _, net::IpAddr, pin::Pin, str::FromStr}; use cxx::CxxString; use crate::{ - error::FastlyError, ffi::{InspectErrorCode, InspectVerdict}, http::request::Request, http::response::Response, - try_fe, }; pub struct InspectResponse(pub(crate) fastly::security::InspectResponse); From ca8cf2a0a7621dbdef9b8a9c830bd276dd8f3cd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Thu, 6 Nov 2025 09:58:46 -0800 Subject: [PATCH 5/6] finish things up and add docs --- examples/ngwaf_inspect.cpp | 26 ++++++++++++++++++++++++++ include/fastly/security.h | 9 ++++++++- src/cpp/security.cpp | 15 ++++++++++++--- src/lib.rs | 1 + src/security.rs | 10 ++++++++++ 5 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 examples/ngwaf_inspect.cpp diff --git a/examples/ngwaf_inspect.cpp b/examples/ngwaf_inspect.cpp new file mode 100644 index 0000000..7d2cd0a --- /dev/null +++ b/examples/ngwaf_inspect.cpp @@ -0,0 +1,26 @@ +//! @example ngwaf_inspect.cpp +#include "fastly/sdk.h" + +int main() { + fastly::log::init_simple("logs"); + auto req{fastly::Request::from_client()}; + + auto ires{fastly::security::inspect( + req, + fastly::security::InspectConfig().with_corp("my_corp").with_workspace( + "my_workspace"))}; + auto verdict{ires->verdict()}; + + fastly::Body body{"NGWAF Verdict: "}; + if (verdict == fastly::security::InspectVerdict::Allow) { + body << "Allow"; + } else if (verdict == fastly::security::InspectVerdict::Block) { + body << "Block"; + } else if (verdict == fastly::security::InspectVerdict::Unauthorized) { + body << "Unauthorized"; + } else if (verdict == fastly::security::InspectVerdict::Other) { + body << *ires->unrecognized_verdict_info() << " (Other)"; + } + + fastly::Response::from_body(std::move(body)).send_to_client(); +} diff --git a/include/fastly/security.h b/include/fastly/security.h index 9bc091b..f021503 100644 --- a/include/fastly/security.h +++ b/include/fastly/security.h @@ -63,6 +63,9 @@ class InspectError { : err_(std::move(e)) {}; InspectErrorCode error_code(); std::string error_msg(); + /// When getting `InspectErrorCode::BufferSizeError`, this can be used to get + /// the required size of the buffer before re-attempting the call. + std::optional required_buffer_size(); private: rust::Box err_; @@ -96,8 +99,12 @@ class InspectResponse { InspectResponse(rust::Box ir) : ir_(std::move(ir)) {}; }; + +/// Inspect a `Request` using the [Fastly Next-Gen +/// WAF](https://docs.fastly.com/en/ngwaf/). tl::expected inspect(fastly::http::Request &request, InspectConfig config); + } // namespace fastly::security -#endif \ No newline at end of file +#endif diff --git a/src/cpp/security.cpp b/src/cpp/security.cpp index 809632f..2eae686 100644 --- a/src/cpp/security.cpp +++ b/src/cpp/security.cpp @@ -1,7 +1,6 @@ #include "util.h" #include #include -#include namespace fastly::security { InspectErrorCode InspectError::error_code() { return err_->error_code(); } @@ -10,8 +9,18 @@ std::string InspectError::error_msg() { err_->error_msg(msg); return msg; } +std::optional InspectError::required_buffer_size(){ + std::size_t buf_size{0}; + if (err_->required_buffer_size(buf_size)) { + return buf_size; + } else { + return std::nullopt; + } +} -std::int16_t InspectResponse::status() const { return ir_->status(); } +std::int16_t InspectResponse::status() const { + return ir_->status(); +} std::optional InspectResponse::redirect_url() const { std::string ret; @@ -70,4 +79,4 @@ inspect(fastly::http::Request &request, InspectConfig config) { return tl::unexpected(err); } } -} // namespace fastly::security \ No newline at end of file +} // namespace fastly::security diff --git a/src/lib.rs b/src/lib.rs index 6be061a..414aa42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -883,6 +883,7 @@ mod ffi { type InspectError; fn error_msg(&self, mut out: Pin<&mut CxxString>); fn error_code(&self) -> InspectErrorCode; + fn required_buffer_size(&self, out: Pin<&mut usize>) -> bool; fn f_security_inspect_error_force_symbols(x: Box) -> Box; } diff --git a/src/security.rs b/src/security.rs index d5cb2d5..7ce7d0a 100644 --- a/src/security.rs +++ b/src/security.rs @@ -28,6 +28,16 @@ impl InspectError { _ => InspectErrorCode::Unexpected, } } + + pub fn required_buffer_size(&self, mut out: Pin<&mut usize>) -> bool { + match self.0 { + fastly::security::InspectError::BufferSizeError(n) => { + out.set(n); + true + } + _ => false, + } + } } #[macro_export] From 13b5d9da3b8efa689db7a586f42259a3522ee790 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Thu, 6 Nov 2025 10:20:08 -0800 Subject: [PATCH 6/6] clangfmt --- .github/workflows/ci.yml | 2 +- include/fastly/backend.h | 2 +- include/fastly/http/status_code.h | 4 ++-- src/cpp/http/http.h | 2 +- src/cpp/http/response.cpp | 28 +++++++++++----------------- src/cpp/security.cpp | 18 ++++++++---------- 6 files changed, 24 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc27d04..1b7a225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: with: version: "25" - name: Check C++ Format - run: /opt/wasi-sdk/bin/clang-format --dry-run --Werror src/**/*.h src/**/*.cpp + run: /opt/wasi-sdk/bin/clang-format --dry-run --Werror include/**/*.h src/**/*.h src/**/*.cpp test: runs-on: ubuntu-latest diff --git a/include/fastly/backend.h b/include/fastly/backend.h index 67a00e3..f506c65 100644 --- a/include/fastly/backend.h +++ b/include/fastly/backend.h @@ -1,10 +1,10 @@ #ifndef FASTLY_BACKEND_H #define FASTLY_BACKEND_H +#include #include #include #include -#include #include #include diff --git a/include/fastly/http/status_code.h b/include/fastly/http/status_code.h index d5c548e..6537df5 100644 --- a/include/fastly/http/status_code.h +++ b/include/fastly/http/status_code.h @@ -1,11 +1,11 @@ #ifndef FASTLY_HTTP_STATUS_CODE_H #define FASTLY_HTTP_STATUS_CODE_H +#include +#include #include #include #include -#include -#include #include #include diff --git a/src/cpp/http/http.h b/src/cpp/http/http.h index f24c494..fa4fbd4 100644 --- a/src/cpp/http/http.h +++ b/src/cpp/http/http.h @@ -6,6 +6,6 @@ namespace fastly::http { using fastly::sys::http::Method; using fastly::sys::http::Version; -} +} // namespace fastly::http #endif diff --git a/src/cpp/http/response.cpp b/src/cpp/http/response.cpp index d2e9471..e6a8456 100644 --- a/src/cpp/http/response.cpp +++ b/src/cpp/http/response.cpp @@ -168,8 +168,8 @@ fastly::expected Response::with_header(std::string_view name, }); } -fastly::expected Response::with_set_header(std::string_view name, - std::string_view value) && { +fastly::expected +Response::with_set_header(std::string_view name, std::string_view value) && { return this->set_header(name, value).map([this]() { return std::move(*this); }); @@ -180,12 +180,13 @@ Response::get_header(std::string_view name) { std::vector value; bool is_sensitive{false}; fastly::sys::error::FastlyError *err; - bool has_header{ - this->res->get_header(static_cast(name), value, is_sensitive, err)}; + bool has_header{this->res->get_header(static_cast(name), value, + is_sensitive, err)}; if (err != nullptr) { return fastly::unexpected(err); } else if (has_header) { - return std::optional(std::in_place, std::string(value.begin(), value.end()), is_sensitive); + return std::optional( + std::in_place, std::string(value.begin(), value.end()), is_sensitive); } else { return std::nullopt; } @@ -204,16 +205,13 @@ Response::get_header_all(std::string_view name) { } } -fastly::expected -Response::get_headers() { +fastly::expected Response::get_headers() { fastly::sys::http::HeadersIter *out; this->res->get_headers(out); - return HeadersRange( - rust::Box::from_raw(out)); + return HeadersRange(rust::Box::from_raw(out)); } -fastly::expected -Response::get_header_names() { +fastly::expected Response::get_header_names() { fastly::sys::http::HeaderNamesIter *out; this->res->get_header_names(out); return HeaderNamesRange( @@ -344,13 +342,9 @@ Response::get_stale_while_revalidate() { } } -Version Response::get_version() { - return this->res->get_version(); -} +Version Response::get_version() { return this->res->get_version(); } -void Response::set_version(Version version) { - this->res->set_version(version); -} +void Response::set_version(Version version) { this->res->set_version(version); } Response Response::with_version(Version version) && { this->set_version(version); diff --git a/src/cpp/security.cpp b/src/cpp/security.cpp index 2eae686..9b54090 100644 --- a/src/cpp/security.cpp +++ b/src/cpp/security.cpp @@ -9,18 +9,16 @@ std::string InspectError::error_msg() { err_->error_msg(msg); return msg; } -std::optional InspectError::required_buffer_size(){ - std::size_t buf_size{0}; - if (err_->required_buffer_size(buf_size)) { - return buf_size; - } else { - return std::nullopt; - } +std::optional InspectError::required_buffer_size() { + std::size_t buf_size{0}; + if (err_->required_buffer_size(buf_size)) { + return buf_size; + } else { + return std::nullopt; + } } -std::int16_t InspectResponse::status() const { - return ir_->status(); -} +std::int16_t InspectResponse::status() const { return ir_->status(); } std::optional InspectResponse::redirect_url() const { std::string ret;