bulwark_wasm_sdk_macros/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::{quote, quote_spanned};
use syn::{
parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Ident,
ItemFn, ItemImpl, LitBool, LitStr, ReturnType, Signature, Visibility,
};
extern crate proc_macro;
/// The `bulwark_plugin` attribute generates default implementations for all handler traits in a module
/// and produces friendly errors for common mistakes.
///
/// All trait functions for `Handlers` are optional when used in conjunction with this macro. A no-op
/// implementation will be automatically generated if a handler function has not been defined. Handler
/// functions are called in sequence, in the order below. All `*_decision` handlers render an updated
/// decision. In the case of a `restricted` outcome, no further processing will occur. Otherwise,
/// processing will continue to the next handler.
///
/// # Trait Functions
/// - `on_init` - Not typically used. Called when the plugin is first loaded. If defined, overrides the
/// default macro behavior of calling
/// [`receive_request_body(true)`](https://docs.rs/bulwark-wasm-sdk/latest/bulwark_wasm_sdk/fn.receive_request_body.html)
/// or [`receive_response_body(true)`](https://docs.rs/bulwark-wasm-sdk/latest/bulwark_wasm_sdk/fn.receive_response_body.html)
/// when the corresponding handlers have been defined.
/// - `on_request` - This handler is called for every incoming request, before any decision-making will occur.
/// It is typically used to perform enrichment tasks with the
/// [`set_param_value`](https://docs.rs/bulwark-wasm-sdk/latest/bulwark_wasm_sdk/fn.set_param_value.html) function.
/// The request body will not yet be available when this handler is called.
/// - `on_request_decision` - This handler is called to make an initial decision.
/// - `on_request_body_decision` - This handler is called once the request body is available. The decision may be updated
/// with any new evidence found in the request body.
/// - `on_response_decision` - This handler is called once the interior service has received the request, processed it, and
/// returned a response, but prior to that response being sent onwards to the original exterior client. Notably, a `restricted`
/// outcome here does not cancel any actions or side-effects from the interior service that may have taken place already.
/// This handler is often used to process response status codes.
/// - `on_response_body_decision` - This handler is called once the response body is available. The decision may be updated
/// with any new evidence found in the response body.
/// - `on_decision_feedback` - This handler is called once a final verdict has been reached. The combined decision
/// of all plugins is available here, not just the decision of the currently executing plugin. This handler may be
/// used for any form of feedback loop, counter-based detections, or to train a model. Additionally, in the case of a
/// `restricted` outcome, this handler may be used to perform logouts or otherwise cancel or attempt to roll back undesired
/// side-effects that could have occurred prior to the verdict being rendered.
///
/// # Example
///
/// ```no_compile
/// use bulwark_wasm_sdk::*;
///
/// struct ExamplePlugin;
///
/// #[bulwark_plugin]
/// impl Handlers for ExamplePlugin {
/// fn on_request_decision() -> Result {
/// println!("hello world");
/// // implement detection logic here
/// Ok(())
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn bulwark_plugin(_: TokenStream, input: TokenStream) -> TokenStream {
// Parse the input token stream as an impl, or return an error.
let raw_impl = parse_macro_input!(input as ItemImpl);
// The trait must be specified by the developer even though there's only one valid value.
// If we inject it, that leads to a very surprising result when developers try to define helper functions
// in the same struct impl and can't because it's really a trait impl.
if let Some((_, path, _)) = raw_impl.trait_ {
let trait_name = path.get_ident().map_or(String::new(), |id| id.to_string());
if &trait_name != "Handlers" {
return syn::Error::new(
path.span(),
format!(
"`bulwark_plugin` encountered unexpected trait `{}` for the impl",
trait_name
),
)
.to_compile_error()
.into();
}
} else {
return syn::Error::new(
raw_impl.self_ty.span(),
"`bulwark_plugin` requires an impl for the `Guest` trait",
)
.to_compile_error()
.into();
}
let struct_type = &raw_impl.self_ty;
let mut init_handler_found = false;
let mut request_body_handler_found = false;
let mut response_body_handler_found = false;
let mut handlers = vec![
"on_request",
"on_request_decision",
"on_request_body_decision",
"on_response_decision",
"on_response_body_decision",
"on_decision_feedback",
];
let mut new_items = Vec::with_capacity(raw_impl.items.len());
for item in &raw_impl.items {
if let syn::ImplItem::Fn(iifn) = item {
match iifn.sig.ident.to_string().as_str() {
"on_init" => init_handler_found = true,
"on_request_body_decision" => request_body_handler_found = true,
"on_response_body_decision" => response_body_handler_found = true,
_ => {}
}
let initial_len = handlers.len();
// Find and record the implemented handlers, removing any we find from the list above.
handlers.retain(|h| *h != iifn.sig.ident.to_string().as_str());
// Verify that any functions with a handler name we find have set the `handler` attribute.
let mut use_original_item = true;
if handlers.len() < initial_len {
let mut handler_attr_found = false;
for attr in &iifn.attrs {
if let Some(ident) = attr.meta.path().get_ident() {
if ident.to_string().as_str() == "handler" {
handler_attr_found = true;
break;
}
}
}
if !handler_attr_found {
use_original_item = false;
let mut new_iifn = iifn.clone();
new_iifn.attrs.push(parse_quote! {
#[handler]
});
new_items.push(syn::ImplItem::Fn(new_iifn));
}
}
if use_original_item {
new_items.push(item.clone());
}
} else {
new_items.push(item.clone());
}
}
// Define the missing handlers with no-op defaults
let noop_handlers = handlers
.iter()
.map(|handler_name| {
let handler_ident = Ident::new(handler_name, Span::call_site());
quote! {
#[handler]
fn #handler_ident() -> Result {
Ok(())
}
}
})
.collect::<Vec<proc_macro2::TokenStream>>();
let init_handler = if init_handler_found {
// Empty token stream if an init handler was already defined, we'll generate nothing and use that instead
quote! {}
} else {
let receive_request_body = LitBool::new(request_body_handler_found, Span::call_site());
let receive_response_body = LitBool::new(response_body_handler_found, Span::call_site());
quote! {
#[handler]
fn on_init() -> Result {
receive_request_body(#receive_request_body);
receive_response_body(#receive_response_body);
Ok(())
}
}
};
let output = quote! {
mod handlers {
use super::#struct_type;
wit_bindgen::generate!({
world: "bulwark:plugin/handlers",
exports: {
world: #struct_type
}
});
}
use handlers::Guest as Handlers;
impl Handlers for #struct_type {
#init_handler
#(#new_items)*
#(#noop_handlers)*
}
};
output.into()
}
/// The `handler` attribute makes the associated function into a Bulwark event handler.
///
/// The `handler` attribute is normally applied automatically by the `bulwark_plugin` macro and
/// need not be specified explicitly.
///
/// The associated function must take no parameters and return a `bulwark_wasm_sdk::Result`. It may only be
/// named one of the following:
/// - `on_init`
/// - `on_request`
/// - `on_request_decision`
/// - `on_request_body_decision`
/// - `on_response_decision`
/// - `on_response_body_decision`
/// - `on_decision_feedback`
#[doc(hidden)]
#[proc_macro_attribute]
pub fn handler(_: TokenStream, input: TokenStream) -> TokenStream {
// Parse the input token stream as a free-standing function, or return an error.
let raw_handler = parse_macro_input!(input as ItemFn);
// Check that the function signature looks okay-ish. If we have the wrong number of arguments,
// or no return type is specified , print a friendly spanned error with the expected signature.
if !check_impl_signature(&raw_handler.sig) {
return syn::Error::new(
raw_handler.sig.span(),
format!(
"`handler` expects a function such as:
#[handler]
fn {}() -> Result {{
...
}}
",
raw_handler.sig.ident
),
)
.to_compile_error()
.into();
}
// Get the attributes and signature of the outer function. Then, update the
// attributes and visibility of the inner function that we will inline.
let (attrs, sig) = outer_handler_info(&raw_handler);
let (name, inner_fn) = inner_fn_info(raw_handler);
let name_str = LitStr::new(name.to_string().as_str(), Span::call_site());
let output;
match name.to_string().as_str() {
"on_init"
| "on_request"
| "on_request_decision"
| "on_request_body_decision"
| "on_response_decision"
| "on_response_body_decision"
| "on_decision_feedback" => {
output = quote_spanned! {inner_fn.span() =>
#(#attrs)*
#sig {
// Declares the inlined inner function, calls it, then performs very
// basic error handling on the result
#[inline(always)]
#inner_fn
let result = #name().map_err(|e| {
eprintln!("error in '{}' handler: {}", #name_str, e);
append_tags(["error"]);
// Absorbs the error, returning () to match desired signature
});
#[allow(unused_must_use)]
{
// Apparently we can exit the guest environment before IO is flushed,
// causing it to never be captured? This ensures IO is flushed and captured.
use std::io::Write;
std::io::stdout().flush();
std::io::stderr().flush();
}
result
}
}
}
_ => {
return syn::Error::new(
inner_fn.sig.span(),
"`handler` expects a function named one of:
- `on_init`
- `on_request`
- `on_request_decision`
- `on_request_body_decision`
- `on_response_decision`
- `on_response_body_decision`
- `on_decision_feedback`
",
)
.to_compile_error()
.into()
}
}
output.into()
}
/// Check if the signature of the `#[handler]` function seems correct.
///
/// Unfortunately, precisely typecheck in a procedural macro attribute is not possible, because we
/// are dealing with [`TokenStream`]s. This checks that our signature takes one input, and has a
/// return type. Specific type errors are caught later, after the macro has been expanded.
///
/// This is used by the [`handler`] procedural macro attribute to help provide friendly errors
/// when given a function with the incorrect signature.
///
/// [`TokenStream`]: proc_macro/struct.TokenStream.html
fn check_impl_signature(sig: &Signature) -> bool {
if sig.inputs.iter().len() != 0 {
false // Return false if the signature takes no inputs, or more than one input.
} else if let ReturnType::Default = sig.output {
false // Return false if the signature's output type is empty.
} else {
true
}
}
/// Returns a 2-tuple containing the attributes and signature of our outer `handler`.
///
/// The outer handler function will use the same attributes and visibility as our raw handler
/// function.
///
/// The signature of the outer function will be changed to have inputs and outputs of the form
/// `fn handler_name() -> Result<(), ()>`. The name of the outer handler function will be the same
/// as the inlined function.
fn outer_handler_info(inner_handler: &ItemFn) -> (Vec<Attribute>, Signature) {
let attrs = inner_handler.attrs.clone();
let name = inner_handler.sig.ident.to_string();
let sig = {
let mut sig = inner_handler.sig.clone();
sig.ident = Ident::new(&name, Span::call_site());
sig.inputs = Punctuated::new();
sig.output = parse_quote!(-> ::std::result::Result<(), ()>);
sig
};
(attrs, sig)
}
/// Prepare our inner function to be inlined into our outer handler function.
///
/// This changes its visibility to [`Inherited`], and removes [`no_mangle`] from the attributes of
/// the inner function if it is there.
///
/// This function returns a 2-tuple of the inner function's identifier and the function itself.
///
/// [`Inherited`]: syn/enum.Visibility.html#variant.Inherited
/// [`no_mangle`]: https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute
fn inner_fn_info(mut inner_handler: ItemFn) -> (Ident, ItemFn) {
let name = inner_handler.sig.ident.clone();
inner_handler.vis = Visibility::Inherited;
inner_handler
.attrs
.retain(|attr| !attr.path().is_ident("no_mangle"));
(name, inner_handler)
}