1
//! Input matcher support for dynamic modules.
2
//!
3
//! This module provides traits and types for implementing custom input matchers as dynamic modules.
4
//! A matcher evaluates HTTP request/response data and returns a boolean match result.
5

            
6
use crate::abi;
7
use std::ffi::c_void;
8
use std::{ptr, slice};
9

            
10
/// Read-only context for accessing HTTP matching data during match evaluation.
11
///
12
/// This is passed to [`MatcherConfig::on_matcher_match`] to provide access to HTTP headers.
13
/// The context is only valid during the match callback and must not be stored.
14
pub struct MatchContext {
15
  envoy_ptr: *mut c_void,
16
}
17

            
18
impl MatchContext {
19
  /// Create a new MatchContext. Used internally by the macro.
20
  #[doc(hidden)]
21
  pub fn new(envoy_ptr: *mut c_void) -> Self {
22
    Self { envoy_ptr }
23
  }
24

            
25
  /// Get a request header value by key.
26
  ///
27
  /// Returns the header value as bytes, or `None` if the header is not present.
28
  pub fn get_request_header(&self, key: &str) -> Option<&[u8]> {
29
    self.get_header_value(
30
      abi::envoy_dynamic_module_type_http_header_type::RequestHeader,
31
      key,
32
    )
33
  }
34

            
35
  /// Get a response header value by key.
36
  ///
37
  /// Returns the header value as bytes, or `None` if the header is not present.
38
  pub fn get_response_header(&self, key: &str) -> Option<&[u8]> {
39
    self.get_header_value(
40
      abi::envoy_dynamic_module_type_http_header_type::ResponseHeader,
41
      key,
42
    )
43
  }
44

            
45
  /// Get the number of headers in the specified header map.
46
  pub fn get_headers_size(
47
    &self,
48
    header_type: abi::envoy_dynamic_module_type_http_header_type,
49
  ) -> usize {
50
    unsafe {
51
      abi::envoy_dynamic_module_callback_matcher_get_headers_size(self.envoy_ptr, header_type)
52
    }
53
  }
54

            
55
  /// Get all headers from the specified header map as key-value pairs.
56
  ///
57
  /// Returns a vector of `(key, value)` byte slices, or `None` if the header map
58
  /// is not available.
59
  pub fn get_all_headers(
60
    &self,
61
    header_type: abi::envoy_dynamic_module_type_http_header_type,
62
  ) -> Option<Vec<(&[u8], &[u8])>> {
63
    let size = self.get_headers_size(header_type);
64
    if size == 0 {
65
      return None;
66
    }
67

            
68
    let mut headers = vec![
69
      abi::envoy_dynamic_module_type_envoy_http_header {
70
        key_ptr: ptr::null_mut(),
71
        key_length: 0,
72
        value_ptr: ptr::null_mut(),
73
        value_length: 0,
74
      };
75
      size
76
    ];
77

            
78
    let result = unsafe {
79
      abi::envoy_dynamic_module_callback_matcher_get_headers(
80
        self.envoy_ptr,
81
        header_type,
82
        headers.as_mut_ptr(),
83
      )
84
    };
85

            
86
    if !result {
87
      return None;
88
    }
89

            
90
    Some(
91
      headers
92
        .iter()
93
        .map(|h| unsafe {
94
          (
95
            slice::from_raw_parts(h.key_ptr as *const u8, h.key_length),
96
            slice::from_raw_parts(h.value_ptr as *const u8, h.value_length),
97
          )
98
        })
99
        .collect(),
100
    )
101
  }
102

            
103
  /// Get a header value by type and key.
104
  fn get_header_value(
105
    &self,
106
    header_type: abi::envoy_dynamic_module_type_http_header_type,
107
    key: &str,
108
  ) -> Option<&[u8]> {
109
    let key_buf = abi::envoy_dynamic_module_type_module_buffer {
110
      ptr: key.as_ptr() as *const _,
111
      length: key.len(),
112
    };
113
    let mut result = abi::envoy_dynamic_module_type_envoy_buffer {
114
      ptr: ptr::null_mut(),
115
      length: 0,
116
    };
117
    if unsafe {
118
      abi::envoy_dynamic_module_callback_matcher_get_header_value(
119
        self.envoy_ptr,
120
        header_type,
121
        key_buf,
122
        &mut result,
123
        0,
124
        ptr::null_mut(),
125
      )
126
    } {
127
      unsafe {
128
        Some(slice::from_raw_parts(
129
          result.ptr as *const u8,
130
          result.length,
131
        ))
132
      }
133
    } else {
134
      None
135
    }
136
  }
137
}
138

            
139
/// Trait that the dynamic module must implement to provide matcher configuration and logic.
140
///
141
/// The implementation is created once during configuration loading and shared across all
142
/// match evaluations. It must be `Send + Sync` since match evaluation can happen on any
143
/// worker thread.
144
pub trait MatcherConfig: Sized + Send + Sync + 'static {
145
  /// Create a new matcher configuration from the provided name and config bytes.
146
  ///
147
  /// # Arguments
148
  /// * `name` - The matcher configuration name from the proto config.
149
  /// * `config` - The raw configuration bytes from the proto config.
150
  ///
151
  /// # Returns
152
  /// The matcher configuration on success, or an error string on failure.
153
  fn new(name: &str, config: &[u8]) -> Result<Self, String>;
154

            
155
  /// Evaluate whether the input matches.
156
  ///
157
  /// This is called on worker threads during match evaluation. The `ctx` provides
158
  /// access to HTTP headers and other matching data. The context is only valid during
159
  /// this call and must not be stored.
160
  ///
161
  /// # Returns
162
  /// `true` if the input matches, `false` otherwise.
163
  fn on_matcher_match(&self, ctx: &MatchContext) -> bool;
164
}
165

            
166
/// Macro to declare matcher entry points.
167
///
168
/// This macro generates the required C ABI functions that Envoy calls to interact with
169
/// the matcher implementation.
170
///
171
/// # Example
172
///
173
/// ```ignore
174
/// use envoy_dynamic_modules_rust_sdk::{matcher::*, declare_matcher};
175
///
176
/// struct MyMatcherConfig {
177
///     expected_value: String,
178
/// }
179
///
180
/// impl MatcherConfig for MyMatcherConfig {
181
///     fn new(_name: &str, config: &[u8]) -> Result<Self, String> {
182
///         Ok(Self {
183
///             expected_value: String::from_utf8_lossy(config).to_string(),
184
///         })
185
///     }
186
///
187
///     fn on_matcher_match(&self, ctx: &MatchContext) -> bool {
188
///         if let Some(value) = ctx.get_request_header("x-match-header") {
189
///             value == self.expected_value.as_bytes()
190
///         } else {
191
///             false
192
///         }
193
///     }
194
/// }
195
///
196
/// declare_matcher!(MyMatcherConfig);
197
/// ```
198
#[macro_export]
199
macro_rules! declare_matcher {
200
  ($config_type:ty) => {
201
    #[no_mangle]
202
    pub extern "C" fn envoy_dynamic_module_on_matcher_config_new(
203
      _config_envoy_ptr: *mut ::std::ffi::c_void,
204
      name: $crate::abi::envoy_dynamic_module_type_envoy_buffer,
205
      config: $crate::abi::envoy_dynamic_module_type_envoy_buffer,
206
    ) -> *const ::std::ffi::c_void {
207
      let name_str = unsafe {
208
        let slice = ::std::slice::from_raw_parts(name.ptr as *const u8, name.length);
209
        ::std::str::from_utf8(slice).unwrap_or("")
210
      };
211
      let config_bytes =
212
        unsafe { ::std::slice::from_raw_parts(config.ptr as *const u8, config.length) };
213

            
214
      match <$config_type as $crate::matcher::MatcherConfig>::new(name_str, config_bytes) {
215
        Ok(c) => Box::into_raw(Box::new(c)) as *const ::std::ffi::c_void,
216
        Err(_) => ::std::ptr::null(),
217
      }
218
    }
219

            
220
    #[no_mangle]
221
    pub extern "C" fn envoy_dynamic_module_on_matcher_config_destroy(
222
      config_ptr: *const ::std::ffi::c_void,
223
    ) {
224
      unsafe {
225
        drop(Box::from_raw(config_ptr as *mut $config_type));
226
      }
227
    }
228

            
229
    #[no_mangle]
230
    pub extern "C" fn envoy_dynamic_module_on_matcher_match(
231
      config_ptr: *const ::std::ffi::c_void,
232
      matcher_input_envoy_ptr: *mut ::std::ffi::c_void,
233
    ) -> bool {
234
      let config = unsafe { &*(config_ptr as *const $config_type) };
235
      let ctx = $crate::matcher::MatchContext::new(matcher_input_envoy_ptr);
236
      config.on_matcher_match(&ctx)
237
    }
238
  };
239
}