bulwark_wasm_sdk/host_api.rs
1use {
2 std::{net::IpAddr, str, str::FromStr},
3 validator::Validate,
4};
5
6// For some reason, doc-tests in this module trigger a linker error, so they're set to no_run
7
8use crate::bulwark_host::DecisionInterface;
9
10pub use crate::{Decision, Outcome};
11pub use http::{Extensions, Method, StatusCode, Uri, Version};
12pub use serde_json::json as value;
13pub use serde_json::{Map, Value};
14
15/// An HTTP request combines a head consisting of a [`Method`], [`Uri`], and headers with a [`BodyChunk`], which provides
16/// access to the first chunk of a request body.
17pub type Request = http::Request<BodyChunk>;
18/// An HTTP response combines a head consisting of a [`StatusCode`] and headers with a [`BodyChunk`], which provides
19/// access to the first chunk of a response body.
20pub type Response = http::Response<BodyChunk>;
21
22// NOTE: fields are documented via Markdown instead of normal rustdoc because the underlying type is from the macro.
23/// A `Breaker` contains the values needed to implement a circuit-breaker pattern within a plugin.
24///
25/// # Fields
26///
27/// * `generation` - The number of times a breaker has been incremented within the expiration window.
28/// * `successes` - The number of total success outcomes tracked within the expiration window.
29/// * `failures` - The number of total failure outcomes tracked within the expiration window.
30/// * `consecutive_successes` - The number of consecutive success outcomes.
31/// * `consecutive_failures` - The number of consecutive failure outcomes.
32/// * `expiration` - The expiration timestamp in seconds since the epoch.
33pub type Breaker = crate::bulwark_host::BreakerInterface;
34/// A `Rate` contains the values needed to implement a rate-limiter pattern within a plugin.
35///
36/// # Fields
37///
38/// * `attempts` - The number of attempts made within the expiration window.
39/// * `expiration` - The expiration timestamp in seconds since the epoch.
40pub type Rate = crate::bulwark_host::RateInterface;
41
42/// The number of successes or failures to increment the breaker by.
43pub enum BreakerDelta {
44 Success(i64),
45 Failure(i64),
46}
47
48/// The first chunk of an HTTP body.
49///
50/// Bulwark does not send the entire body to the guest plugin environment. This limitation limits the impact of
51/// copying a large number of bytes from the host into guest VMs. A full body copy would be required for each
52/// plugin for every request or response otherwise.
53///
54/// This has consequences for any plugin that wants to parse the body it receives. Some data formats like JSON
55/// may be significantly more difficult to work with if only partially received, and streaming parsers which may be
56/// more tolerant to trunctation are recommended in such cases. There will be some situations where this limitation
57/// prevents useful parsing entirely and plugins may need to make use of the `unknown` result value to express this.
58pub struct BodyChunk {
59 pub received: bool,
60 pub end_of_stream: bool,
61 pub size: u64,
62 pub start: u64,
63 // TODO: use bytes crate to avoid copies
64 pub content: Vec<u8>,
65}
66
67/// An empty HTTP body
68pub const NO_BODY: BodyChunk = BodyChunk {
69 received: true,
70 end_of_stream: true,
71 size: 0,
72 start: 0,
73 content: vec![],
74};
75
76/// An unavailable HTTP body
77pub const UNAVAILABLE_BODY: BodyChunk = BodyChunk {
78 received: false,
79 end_of_stream: true,
80 size: 0,
81 start: 0,
82 content: vec![],
83};
84
85// TODO: might need either get_remote_addr or an extension on the request for non-forwarded IP address
86
87/// Returns the incoming request.
88pub fn get_request() -> Request {
89 let raw_request: crate::bulwark_host::RequestInterface = crate::bulwark_host::get_request();
90 let chunk: Vec<u8> = raw_request.chunk;
91 // This code shouldn't be reachable if the method is invalid
92 let method = Method::from_str(raw_request.method.as_str()).expect("should be a valid method");
93 let mut request = http::Request::builder()
94 .method(method)
95 .uri(raw_request.uri)
96 .version(match raw_request.version.as_str() {
97 "HTTP/0.9" => http::Version::HTTP_09,
98 "HTTP/1.0" => http::Version::HTTP_10,
99 "HTTP/1.1" => http::Version::HTTP_11,
100 "HTTP/2.0" => http::Version::HTTP_2,
101 "HTTP/3.0" => http::Version::HTTP_3,
102 _ => http::Version::HTTP_11,
103 });
104 for (name, value) in raw_request.headers {
105 request = request.header(name, value);
106 }
107 request
108 .body(BodyChunk {
109 received: raw_request.body_received,
110 content: chunk,
111 size: raw_request.chunk_length,
112 start: raw_request.chunk_start,
113 end_of_stream: raw_request.end_of_stream,
114 })
115 // Everything going into the builder should have already been validated somewhere else
116 // Proxy layer shouldn't send it through if it's invalid
117 .expect("should be a valid request")
118}
119
120/// Returns the response received from the interior service.
121pub fn get_response() -> Option<Response> {
122 let raw_response: crate::bulwark_host::ResponseInterface = crate::bulwark_host::get_response()?;
123 let chunk: Vec<u8> = raw_response.chunk;
124 let status = raw_response.status as u16;
125 let mut response = http::Response::builder().status(status);
126 for (name, value) in raw_response.headers {
127 response = response.header(name, value);
128 }
129 Some(
130 response
131 .body(BodyChunk {
132 received: raw_response.body_received,
133 content: chunk,
134 size: raw_response.chunk_length,
135 start: raw_response.chunk_start,
136 end_of_stream: raw_response.end_of_stream,
137 })
138 // Everything going into the builder should have already been validated somewhere else
139 // Proxy layer shouldn't send it through if it's invalid
140 .expect("should be a valid response"),
141 )
142}
143
144/// Determines whether the `on_request_body_decision` handler will be called with a request body or not.
145///
146/// The [`bulwark_plugin`](bulwark_wasm_sdk_macros::bulwark_plugin) macro will automatically call this function
147/// within an auto-generated `on_init` handler. Normally, plugin authors do not need to call it directly.
148/// However, the default may be overriden if a plugin intends to cancel processing of the request body despite
149/// having a handler available for processing it.
150///
151/// However, if the `on_init` handler is replaced, this function will need to be called manually. Most plugins
152/// will not need to do this.
153pub fn receive_request_body(body: bool) {
154 crate::bulwark_host::receive_request_body(body)
155}
156
157/// Determines whether the `on_response_body_decision` handler will be called with a response body or not.
158///
159/// The [`bulwark_plugin`](bulwark_wasm_sdk_macros::bulwark_plugin) macro will automatically call this function
160/// within an auto-generated `on_init` handler. Normally, plugin authors do not need to call it directly.
161/// However, the default may be overriden if a plugin intends to cancel processing of the response body despite
162/// having a handler available for processing it.
163///
164/// However, if the `on_init` handler is replaced, this function will need to be called manually. Most plugins
165/// will not need to do this.
166pub fn receive_response_body(body: bool) {
167 crate::bulwark_host::receive_response_body(body)
168}
169
170/// Returns the originating client's IP address, if available.
171pub fn get_client_ip() -> Option<IpAddr> {
172 crate::bulwark_host::get_client_ip().map(|ip| ip.into())
173}
174
175/// Returns a named value from the request context's params.
176///
177/// # Arguments
178///
179/// * `key` - The key name corresponding to the param value.
180pub fn get_param_value(key: &str) -> Result<Value, crate::Error> {
181 let raw_value = crate::bulwark_host::get_param_value(key)?;
182 let value: serde_json::Value = serde_json::from_slice(&raw_value).unwrap();
183 Ok(value)
184}
185
186/// Set a named value in the request context's params.
187///
188/// # Arguments
189///
190/// * `key` - The key name corresponding to the param value.
191/// * `value` - The value to record. Values are serialized JSON.
192pub fn set_param_value(key: &str, value: Value) -> Result<(), crate::Error> {
193 let json = serde_json::to_vec(&value)?;
194 crate::bulwark_host::set_param_value(key, &json)?;
195 Ok(())
196}
197
198/// Returns the guest environment's configuration value as a JSON [`Value`].
199///
200/// By convention this will return a [`Value::Object`].
201pub fn get_config() -> Value {
202 let raw_config = crate::bulwark_host::get_config();
203 serde_json::from_slice(&raw_config).unwrap()
204}
205
206/// Returns a named guest environment configuration value as a JSON [`Value`].
207///
208/// A shortcut for calling [`get_config`], reading it as an `Object`, and then retrieving a named [`Value`] from it.
209///
210/// # Arguments
211///
212/// * `key` - A key indexing into a configuration [`Map`]
213pub fn get_config_value(key: &str) -> Option<Value> {
214 // TODO: this should return a result
215 let raw_config = crate::bulwark_host::get_config();
216 let object: serde_json::Value = serde_json::from_slice(&raw_config).unwrap();
217 match object {
218 Value::Object(v) => v.get(&key.to_string()).cloned(),
219 _ => panic!("unexpected config value"),
220 }
221}
222
223/// Returns a named environment variable value as a [`String`].
224///
225/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
226/// the environment variable being requested. This function will panic if permission has not been granted.
227///
228/// # Arguments
229///
230/// * `key` - The environment variable name. Case-sensitive.
231pub fn get_env(key: &str) -> Result<String, crate::EnvVarError> {
232 Ok(String::from_utf8(crate::bulwark_host::get_env_bytes(key)?)?)
233}
234
235/// Returns a named environment variable value as bytes.
236///
237/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
238/// the environment variable being requested. This function will panic if permission has not been granted.
239///
240/// # Arguments
241///
242/// * `key` - The environment variable name. Case-sensitive.
243pub fn get_env_bytes(key: &str) -> Result<Vec<u8>, crate::EnvVarError> {
244 Ok(crate::bulwark_host::get_env_bytes(key)?)
245}
246
247/// Records the decision value the plugin wants to return.
248///
249/// # Arguments
250///
251/// * `decision` - The [`Decision`] output of the plugin.
252pub fn set_decision(decision: Decision) -> Result<(), crate::Error> {
253 // Validate here because it should provide a better error than the one that the host will give.
254 decision.validate()?;
255 crate::bulwark_host::set_decision(DecisionInterface {
256 accepted: decision.accept,
257 restricted: decision.restrict,
258 unknown: decision.unknown,
259 })
260 .expect("should not be able to produce an invalid result");
261 Ok(())
262}
263
264/// Records a decision indicating that the plugin wants to accept a request.
265///
266/// This function is sugar for `set_decision(Decision { value, 0.0, 0.0 }.scale())`
267/// If used with a 1.0 value it should be given a weight in its config.
268///
269/// # Arguments
270///
271/// * `value` - The `accept` value to set.
272pub fn set_accepted(value: f64) {
273 crate::bulwark_host::set_decision(
274 Decision {
275 accept: value,
276 restrict: 0.0,
277 unknown: 0.0,
278 }
279 .scale()
280 .into(),
281 )
282 .expect("should not be able to produce an invalid result");
283}
284
285/// Records a decision indicating that the plugin wants to restrict a request.
286///
287/// This function is sugar for `set_decision(Decision { 0.0, value, 0.0 }.scale())`.
288/// If used with a 1.0 value it should be given a weight in its config.
289///
290/// # Arguments
291///
292/// * `value` - The `restrict` value to set.
293pub fn set_restricted(value: f64) {
294 crate::bulwark_host::set_decision(
295 Decision {
296 accept: 0.0,
297 restrict: value,
298 unknown: 0.0,
299 }
300 .scale()
301 .into(),
302 )
303 .expect("should not be able to produce an invalid result");
304}
305
306/// Records the tags the plugin wants to associate with its decision.
307///
308/// # Arguments
309///
310/// * `tags` - The list of tags to associate with a [`Decision`]
311///
312/// # Examples
313///
314/// All of these are valid:
315///
316/// ```no_run
317/// use bulwark_wasm_sdk::set_tags;
318///
319/// set_tags(["tag"]);
320/// set_tags(vec!["tag"]);
321/// set_tags([String::from("tag")]);
322/// set_tags(vec![String::from("tag")]);
323/// // Clear tags, rarely needed
324/// set_tags::<[_; 0], String>([]);
325/// set_tags::<Vec<_>, String>(vec![]);
326/// ```
327#[inline]
328pub fn set_tags<I: IntoIterator<Item = V>, V: Into<String>>(tags: I) {
329 let tags: Vec<String> = tags.into_iter().map(|s| s.into()).collect();
330 crate::bulwark_host::set_tags(tags.as_slice())
331}
332
333/// Records additional tags the plugin wants to associate with its decision.
334///
335/// # Arguments
336///
337/// * `tags` - The list of additional tags to associate with a [`Decision`]
338///
339/// # Examples
340///
341/// All of these are valid:
342///
343/// ```no_run
344/// use bulwark_wasm_sdk::append_tags;
345///
346/// append_tags(["tag"]);
347/// append_tags(vec!["tag"]);
348/// append_tags([String::from("tag")]);
349/// append_tags(vec![String::from("tag")]);
350/// ```
351#[inline]
352pub fn append_tags<I: IntoIterator<Item = V>, V: Into<String>>(tags: I) -> Vec<String> {
353 let tags: Vec<String> = tags.into_iter().map(|s| s.into()).collect();
354 crate::bulwark_host::append_tags(tags.as_slice())
355}
356
357/// Returns the combined decision, if available.
358///
359/// Typically used in the feedback phase.
360pub fn get_combined_decision() -> Option<Decision> {
361 crate::bulwark_host::get_combined_decision().map(|decision| decision.into())
362}
363
364/// Returns the combined set of tags associated with a decision, if available.
365///
366/// Typically used in the feedback phase.
367#[inline]
368pub fn get_combined_tags() -> Option<Vec<String>> {
369 crate::bulwark_host::get_combined_tags()
370}
371
372/// Returns the outcome of the combined decision, if available.
373///
374/// Typically used in the feedback phase.
375pub fn get_outcome() -> Option<Outcome> {
376 crate::bulwark_host::get_outcome().map(|outcome| outcome.into())
377}
378
379/// Sends an outbound HTTP request.
380///
381/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
382/// the host being requested. This function will panic if permission has not been granted.
383///
384/// # Arguments
385///
386/// * `request` - The HTTP request to send.
387pub fn send_request(request: Request) -> Result<Response, crate::HttpError> {
388 let request = crate::bulwark_host::RequestInterface::from(request);
389 Ok(Response::from(crate::bulwark_host::send_request(&request)?))
390}
391
392/// Returns the named state value retrieved from Redis.
393///
394/// Also used to retrieve a counter value.
395///
396/// # Arguments
397///
398/// * `key` - The key name corresponding to the state value.
399#[inline]
400pub fn get_remote_state(key: &str) -> Result<Vec<u8>, crate::RemoteStateError> {
401 Ok(crate::bulwark_host::get_remote_state(key)?)
402}
403
404/// Parses a counter value from state stored as a string.
405///
406/// # Arguments
407///
408/// * `value` - The string representation of a counter.
409#[inline]
410pub fn parse_counter(value: Vec<u8>) -> Result<i64, crate::ParseCounterError> {
411 Ok(str::from_utf8(value.as_slice())?.parse::<i64>()?)
412}
413
414/// Set a named value in Redis.
415///
416/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
417/// the prefix of the key being requested. This function will panic if permission has not been granted.
418///
419/// # Arguments
420///
421/// * `key` - The key name corresponding to the state value.
422/// * `value` - The value to record. Values are byte strings, but may be interpreted differently by Redis depending on context.
423#[inline]
424pub fn set_remote_state(key: &str, value: &[u8]) -> Result<(), crate::RemoteStateError> {
425 Ok(crate::bulwark_host::set_remote_state(key, value)?)
426}
427
428/// Increments a named counter in Redis.
429///
430/// Returns the value of the counter after it's incremented.
431///
432/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
433/// the prefix of the key being requested. This function will panic if permission has not been granted.
434///
435/// # Arguments
436///
437/// * `key` - The key name corresponding to the state counter.
438#[inline]
439pub fn increment_remote_state(key: &str) -> Result<i64, crate::RemoteStateError> {
440 Ok(crate::bulwark_host::increment_remote_state(key)?)
441}
442
443/// Increments a named counter in Redis by a specified delta value.
444///
445/// Returns the value of the counter after it's incremented.
446///
447/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
448/// the prefix of the key being requested. This function will panic if permission has not been granted.
449///
450/// # Arguments
451///
452/// * `key` - The key name corresponding to the state counter.
453/// * `delta` - The amount to increase the counter by.
454#[inline]
455pub fn increment_remote_state_by(key: &str, delta: i64) -> Result<i64, crate::RemoteStateError> {
456 Ok(crate::bulwark_host::increment_remote_state_by(key, delta)?)
457}
458
459/// Sets an expiration on a named value in Redis.
460///
461/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
462/// the prefix of the key being requested. This function will panic if permission has not been granted.
463///
464/// # Arguments
465///
466/// * `key` - The key name corresponding to the state value.
467/// * `ttl` - The time-to-live for the value in seconds.
468#[inline]
469pub fn set_remote_ttl(key: &str, ttl: i64) -> Result<(), crate::RemoteStateError> {
470 Ok(crate::bulwark_host::set_remote_ttl(key, ttl)?)
471}
472
473// TODO: needs an example
474/// Increments a rate limit, returning the number of attempts so far and the expiration time.
475///
476/// The rate limiter is a counter over a period of time. At the end of the period, it will expire,
477/// beginning a new period. Window periods should be set to the longest amount of time that a client should
478/// be locked out for. The plugin is responsible for performing all rate-limiting logic with the counter
479/// value it receives.
480///
481/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
482/// the prefix of the key being requested. This function will panic if permission has not been granted.
483///
484/// # Arguments
485///
486/// * `key` - The key name corresponding to the state counter.
487/// * `delta` - The amount to increase the counter by.
488/// * `window` - How long each period should be in seconds.
489#[inline]
490pub fn increment_rate_limit(
491 key: &str,
492 delta: i64,
493 window: i64,
494) -> Result<Rate, crate::RemoteStateError> {
495 Ok(crate::bulwark_host::increment_rate_limit(
496 key, delta, window,
497 )?)
498}
499
500/// Checks a rate limit, returning the number of attempts so far and the expiration time.
501///
502/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
503/// the prefix of the key being requested. This function will panic if permission has not been granted.
504///
505/// See [`increment_rate_limit`].
506///
507/// # Arguments
508///
509/// * `key` - The key name corresponding to the state counter.
510#[inline]
511pub fn check_rate_limit(key: &str) -> Result<Rate, crate::RemoteStateError> {
512 Ok(crate::bulwark_host::check_rate_limit(key)?)
513}
514
515/// Increments a circuit breaker, returning the generation count, success count, failure count,
516/// consecutive success count, consecutive failure count, and expiration time.
517///
518/// The plugin is responsible for performing all circuit-breaking logic with the counter
519/// values it receives. The host environment does as little as possible to maximize how much
520/// control the plugin has over the behavior of the breaker.
521///
522/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
523/// the prefix of the key being requested. This function will panic if permission has not been granted.
524///
525/// # Arguments
526///
527/// * `key` - The key name corresponding to the state counter.
528/// * `delta` - The amount to increase the success or failure counter by.
529/// * `window` - How long each period should be in seconds.
530///
531/// # Examples
532///
533/// ```ignore
534/// use bulwark_wasm_sdk::*;
535///
536/// struct CircuitBreaker;
537///
538/// #[bulwark_plugin]
539/// impl Handlers for CircuitBreaker {
540/// fn on_response_decision() -> Result {
541/// if let Some(ip) = get_client_ip() {
542/// let key = format!("client.ip:{ip}");
543/// // "failure" could be determined by other methods besides status code
544/// let failure = get_response().map(|r| r.status().as_u16() >= 500).unwrap_or(true);
545/// let breaker = increment_breaker(
546/// &key,
547/// if !failure {
548/// BreakerDelta::Success(1)
549/// } else {
550/// BreakerDelta::Failure(1)
551/// },
552/// 60 * 60, // 1 hour
553/// )?;
554/// // use breaker here
555/// }
556/// Ok(())
557/// }
558/// }
559/// ```
560pub fn increment_breaker(
561 key: &str,
562 delta: BreakerDelta,
563 window: i64,
564) -> Result<Breaker, crate::RemoteStateError> {
565 let (success_delta, failure_delta) = match delta {
566 BreakerDelta::Success(d) => (d, 0),
567 BreakerDelta::Failure(d) => (0, d),
568 };
569 Ok(crate::bulwark_host::increment_breaker(
570 key,
571 success_delta,
572 failure_delta,
573 window,
574 )?)
575}
576
577/// Checks a circuit breaker, returning the generation count, success count, failure count,
578/// consecutive success count, consecutive failure count, and expiration time.
579///
580/// In order for this function to succeed, a plugin's configuration must explicitly declare a permission grant for
581/// the prefix of the key being requested. This function will panic if permission has not been granted.
582///
583/// See [`increment_breaker`].
584///
585/// # Arguments
586///
587/// * `key` - The key name corresponding to the state counter.
588#[inline]
589pub fn check_breaker(key: &str) -> Result<Breaker, crate::RemoteStateError> {
590 Ok(crate::bulwark_host::check_breaker(key)?)
591}