1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum MigrationMode {
10 Mock,
12 Shadow,
14 Real,
16 Auto,
18}
19
20impl Default for MigrationMode {
21 fn default() -> Self {
22 Self::Auto
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ProxyConfig {
29 pub enabled: bool,
31 pub target_url: Option<String>,
33 pub timeout_seconds: u64,
35 pub follow_redirects: bool,
37 pub headers: HashMap<String, String>,
39 pub prefix: Option<String>,
41 pub passthrough_by_default: bool,
43 pub rules: Vec<ProxyRule>,
45 #[serde(default)]
47 pub migration_enabled: bool,
48 #[serde(default)]
51 pub migration_groups: HashMap<String, MigrationMode>,
52 #[serde(default)]
54 pub request_replacements: Vec<BodyTransformRule>,
55 #[serde(default)]
57 pub response_replacements: Vec<BodyTransformRule>,
58}
59
60#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
62pub struct ProxyRule {
63 pub path_pattern: String,
65 pub target_url: String,
67 pub enabled: bool,
69 pub pattern: String,
71 pub upstream_url: String,
73 #[serde(default)]
75 pub migration_mode: MigrationMode,
76 #[serde(default)]
78 pub migration_group: Option<String>,
79 #[serde(default)]
87 pub condition: Option<String>,
88}
89
90impl Default for ProxyRule {
91 fn default() -> Self {
92 Self {
93 path_pattern: "/".to_string(),
94 target_url: "http://localhost:9080".to_string(),
95 enabled: true,
96 pattern: "/".to_string(),
97 upstream_url: "http://localhost:9080".to_string(),
98 migration_mode: MigrationMode::Auto,
99 migration_group: None,
100 condition: None,
101 }
102 }
103}
104
105impl ProxyConfig {
106 pub fn new(upstream_url: String) -> Self {
108 Self {
109 enabled: true,
110 target_url: Some(upstream_url),
111 timeout_seconds: 30,
112 follow_redirects: true,
113 headers: HashMap::new(),
114 prefix: Some("/proxy/".to_string()),
115 passthrough_by_default: true,
116 rules: Vec::new(),
117 migration_enabled: false,
118 migration_groups: HashMap::new(),
119 request_replacements: Vec::new(),
120 response_replacements: Vec::new(),
121 }
122 }
123
124 pub fn get_effective_migration_mode(&self, path: &str) -> Option<MigrationMode> {
127 if !self.migration_enabled {
128 return None;
129 }
130
131 for rule in &self.rules {
133 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
134 if let Some(ref group) = rule.migration_group {
136 if let Some(&group_mode) = self.migration_groups.get(group) {
137 return Some(group_mode);
138 }
139 }
140 return Some(rule.migration_mode);
142 }
143 }
144
145 None
146 }
147
148 pub fn should_proxy(&self, _method: &axum::http::Method, path: &str) -> bool {
152 if !self.enabled {
153 return false;
154 }
155
156 if self.migration_enabled {
158 if let Some(mode) = self.get_effective_migration_mode(path) {
159 match mode {
160 MigrationMode::Mock => return false, MigrationMode::Shadow => return true, MigrationMode::Real => return true, MigrationMode::Auto => {
164 }
166 }
167 }
168 }
169
170 for rule in &self.rules {
172 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
173 if rule.condition.is_none() {
176 return true;
177 }
178 }
179 }
180
181 match &self.prefix {
183 None => true, Some(prefix) => path.starts_with(prefix),
185 }
186 }
187
188 pub fn should_proxy_with_condition(
191 &self,
192 method: &axum::http::Method,
193 uri: &axum::http::Uri,
194 headers: &axum::http::HeaderMap,
195 body: Option<&[u8]>,
196 ) -> bool {
197 use crate::proxy::conditional::find_matching_rule;
198
199 if !self.enabled {
200 return false;
201 }
202
203 let path = uri.path();
204
205 if self.migration_enabled {
207 if let Some(mode) = self.get_effective_migration_mode(path) {
208 match mode {
209 MigrationMode::Mock => return false, MigrationMode::Shadow => return true, MigrationMode::Real => return true, MigrationMode::Auto => {
213 }
215 }
216 }
217 }
218
219 if !self.rules.is_empty() {
221 if find_matching_rule(&self.rules, method, uri, headers, body, |pattern, path| {
222 self.path_matches_pattern(pattern, path)
223 })
224 .is_some()
225 {
226 return true;
227 }
228 }
229
230 let has_conditional_rules = self.rules.iter().any(|r| r.enabled && r.condition.is_some());
232 if !has_conditional_rules {
233 match &self.prefix {
234 None => true, Some(prefix) => path.starts_with(prefix),
236 }
237 } else {
238 false }
240 }
241
242 pub fn should_shadow(&self, path: &str) -> bool {
244 if !self.migration_enabled {
245 return false;
246 }
247
248 if let Some(mode) = self.get_effective_migration_mode(path) {
249 return mode == MigrationMode::Shadow;
250 }
251
252 false
253 }
254
255 pub fn get_upstream_url(&self, path: &str) -> String {
257 for rule in &self.rules {
259 if rule.enabled && self.path_matches_pattern(&rule.path_pattern, path) {
260 return rule.target_url.clone();
261 }
262 }
263
264 if let Some(base_url) = &self.target_url {
266 base_url.clone()
267 } else {
268 path.to_string()
269 }
270 }
271
272 pub fn strip_prefix(&self, path: &str) -> String {
274 match &self.prefix {
275 Some(prefix) => {
276 if path.starts_with(prefix) {
277 let stripped = path.strip_prefix(prefix).unwrap_or(path);
278 if stripped.starts_with('/') {
280 stripped.to_string()
281 } else {
282 format!("/{}", stripped)
283 }
284 } else {
285 path.to_string()
286 }
287 }
288 None => path.to_string(), }
290 }
291
292 fn path_matches_pattern(&self, pattern: &str, path: &str) -> bool {
294 if let Some(prefix) = pattern.strip_suffix("/*") {
295 path.starts_with(prefix)
296 } else {
297 path == pattern
298 }
299 }
300
301 pub fn update_rule_migration_mode(&mut self, pattern: &str, mode: MigrationMode) -> bool {
304 for rule in &mut self.rules {
305 if rule.path_pattern == pattern || rule.pattern == pattern {
306 rule.migration_mode = mode;
307 return true;
308 }
309 }
310 false
311 }
312
313 pub fn update_group_migration_mode(&mut self, group: &str, mode: MigrationMode) {
316 self.migration_groups.insert(group.to_string(), mode);
317 }
318
319 pub fn toggle_route_migration(&mut self, pattern: &str) -> Option<MigrationMode> {
322 for rule in &mut self.rules {
323 if rule.path_pattern == pattern || rule.pattern == pattern {
324 rule.migration_mode = match rule.migration_mode {
325 MigrationMode::Mock => MigrationMode::Shadow,
326 MigrationMode::Shadow => MigrationMode::Real,
327 MigrationMode::Real => MigrationMode::Mock,
328 MigrationMode::Auto => MigrationMode::Mock, };
330 return Some(rule.migration_mode);
331 }
332 }
333 None
334 }
335
336 pub fn toggle_group_migration(&mut self, group: &str) -> MigrationMode {
339 let current_mode = self.migration_groups.get(group).copied().unwrap_or(MigrationMode::Auto);
340 let new_mode = match current_mode {
341 MigrationMode::Mock => MigrationMode::Shadow,
342 MigrationMode::Shadow => MigrationMode::Real,
343 MigrationMode::Real => MigrationMode::Mock,
344 MigrationMode::Auto => MigrationMode::Mock, };
346 self.migration_groups.insert(group.to_string(), new_mode);
347 new_mode
348 }
349
350 pub fn get_migration_routes(&self) -> Vec<MigrationRouteInfo> {
352 self.rules
353 .iter()
354 .map(|rule| {
355 let effective_mode = if let Some(ref group) = rule.migration_group {
356 self.migration_groups.get(group).copied().unwrap_or(rule.migration_mode)
357 } else {
358 rule.migration_mode
359 };
360
361 MigrationRouteInfo {
362 pattern: rule.path_pattern.clone(),
363 upstream_url: rule.target_url.clone(),
364 migration_mode: effective_mode,
365 route_mode: rule.migration_mode,
366 migration_group: rule.migration_group.clone(),
367 enabled: rule.enabled,
368 }
369 })
370 .collect()
371 }
372
373 pub fn get_migration_groups(&self) -> HashMap<String, MigrationGroupInfo> {
375 let mut group_info: HashMap<String, MigrationGroupInfo> = HashMap::new();
376
377 for rule in &self.rules {
379 if let Some(ref group) = rule.migration_group {
380 let entry = group_info.entry(group.clone()).or_insert_with(|| MigrationGroupInfo {
381 name: group.clone(),
382 migration_mode: self
383 .migration_groups
384 .get(group)
385 .copied()
386 .unwrap_or(rule.migration_mode),
387 route_count: 0,
388 });
389 entry.route_count += 1;
390 }
391 }
392
393 for (group_name, &mode) in &self.migration_groups {
395 group_info.entry(group_name.clone()).or_insert_with(|| MigrationGroupInfo {
396 name: group_name.clone(),
397 migration_mode: mode,
398 route_count: 0,
399 });
400 }
401
402 group_info
403 }
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct MigrationRouteInfo {
409 pub pattern: String,
411 pub upstream_url: String,
413 pub migration_mode: MigrationMode,
415 pub route_mode: MigrationMode,
417 pub migration_group: Option<String>,
419 pub enabled: bool,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct MigrationGroupInfo {
426 pub name: String,
428 pub migration_mode: MigrationMode,
430 pub route_count: usize,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct BodyTransformRule {
437 pub pattern: String,
439 #[serde(default)]
441 pub status_codes: Vec<u16>,
442 pub body_transforms: Vec<BodyTransform>,
444 #[serde(default = "default_true")]
446 pub enabled: bool,
447}
448
449fn default_true() -> bool {
450 true
451}
452
453impl BodyTransformRule {
454 pub fn matches_url(&self, url: &str) -> bool {
456 if !self.enabled {
457 return false;
458 }
459
460 if self.pattern.ends_with("/*") {
462 let prefix = &self.pattern[..self.pattern.len() - 2];
463 url.starts_with(prefix)
464 } else {
465 url == self.pattern || url.starts_with(&self.pattern)
466 }
467 }
468
469 pub fn matches_status_code(&self, status_code: u16) -> bool {
471 if self.status_codes.is_empty() {
472 true } else {
474 self.status_codes.contains(&status_code)
475 }
476 }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct BodyTransform {
482 pub path: String,
484 pub replace: String,
486 #[serde(default)]
488 pub operation: TransformOperation,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "lowercase")]
494pub enum TransformOperation {
495 Replace,
497 Add,
499 Remove,
501}
502
503impl Default for TransformOperation {
504 fn default() -> Self {
505 Self::Replace
506 }
507}
508
509impl Default for ProxyConfig {
510 fn default() -> Self {
511 Self {
512 enabled: false,
513 target_url: None,
514 timeout_seconds: 30,
515 follow_redirects: true,
516 headers: HashMap::new(),
517 prefix: None,
518 passthrough_by_default: false,
519 rules: Vec::new(),
520 migration_enabled: false,
521 migration_groups: HashMap::new(),
522 request_replacements: Vec::new(),
523 response_replacements: Vec::new(),
524 }
525 }
526}