junction_api/
http.rs

1//! HTTP [Route] configuration. [Route]s dynamically congfigure things you might
2//! put directly in client code like timeouts and retries, failure detection, or
3//! picking a different backend based on request data.
4
5use std::{cmp::Ordering, collections::BTreeMap, str::FromStr};
6
7use crate::{
8    backend::BackendId,
9    shared::{Duration, Fraction, Regex},
10    Hostname, Name, Service,
11};
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "typeinfo")]
15use junction_typeinfo::TypeInfo;
16
17#[doc(hidden)]
18pub mod tags {
19    //! Well known tags for Routes.
20
21    /// Marks a Route as generated by an automated process and NOT authored by a
22    /// human being. Any Route with this tag will have lower priority than a
23    /// Route authored by a human.
24    ///
25    /// The value of this tag should be a process or application ID that
26    /// indicates where the route came from and what generated it.
27    pub const GENERATED_BY: &str = "junctionlabs.io/generated-by";
28}
29
30/// A matcher for URL hostnames.
31#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
32#[serde(try_from = "String", into = "String")]
33pub enum HostnameMatch {
34    /// Matches any valid subdomain of this hostname.
35    ///
36    /// ```rust
37    /// # use junction_api::http::HostnameMatch;
38    /// # use std::str::FromStr;
39    ///
40    /// let matcher = HostnameMatch::from_str("*.foo.example").unwrap();
41    ///
42    /// assert!(matcher.matches_str("bar.foo.example"));
43    ///
44    /// assert!(!matcher.matches_str("foo.example"));
45    /// assert!(!matcher.matches_str("barfoo.example"));
46    /// ```
47    Subdomain(Hostname),
48
49    /// An exact match for a hostname.
50    Exact(Hostname),
51}
52
53impl HostnameMatch {
54    /// Returns true if hostname is matched by this matcher.
55    pub fn matches(&self, hostname: &Hostname) -> bool {
56        self.matches_str_validated(hostname)
57    }
58
59    /// Returns true if the string s is a valid hostname and matches this pattern.
60    pub fn matches_str(&self, s: &str) -> bool {
61        if Hostname::validate(s.as_bytes()).is_err() {
62            return false;
63        }
64        self.matches_str_validated(s)
65    }
66
67    fn matches_str_validated(&self, s: &str) -> bool {
68        match self {
69            HostnameMatch::Subdomain(d) => {
70                let (subdomain, domain) = s.split_at(s.len() - d.len());
71                domain == &d[..] && subdomain.ends_with('.')
72            }
73            HostnameMatch::Exact(e) => s == e.as_ref(),
74        }
75    }
76}
77
78#[cfg(feature = "typeinfo")]
79impl junction_typeinfo::TypeInfo for HostnameMatch {
80    fn kind() -> junction_typeinfo::Kind {
81        junction_typeinfo::Kind::String
82    }
83}
84
85impl From<Hostname> for HostnameMatch {
86    fn from(hostname: Hostname) -> Self {
87        Self::Exact(hostname)
88    }
89}
90
91impl std::fmt::Display for HostnameMatch {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        match self {
94            HostnameMatch::Subdomain(hostname) => write!(f, "*.{hostname}"),
95            HostnameMatch::Exact(hostname) => f.write_str(hostname),
96        }
97    }
98}
99
100impl FromStr for HostnameMatch {
101    type Err = crate::Error;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        Ok(match s.strip_prefix("*.") {
105            Some(hostname) => Self::Subdomain(Hostname::from_str(hostname)?),
106            None => Self::Exact(Hostname::from_str(s)?),
107        })
108    }
109}
110
111// implemented so we can use serde(try_from = "String")
112impl TryFrom<String> for HostnameMatch {
113    type Error = crate::Error;
114
115    fn try_from(s: String) -> Result<Self, Self::Error> {
116        Ok(match s.strip_prefix("*.") {
117            // if there's a prefix match, use FromStr. copying and tossing
118            // the allocated String is probably just as fine as removing the
119            // first two chars and doing a memcopy.
120            Some(hostname) => Self::Subdomain(Hostname::from_str(hostname)?),
121            // if this is an exact match, use Hostname::try_from which
122            // can move the value into the Hostname
123            None => Self::Exact(Hostname::try_from(s)?),
124        })
125    }
126}
127// implemented so we can use serde(into = "String")
128impl From<HostnameMatch> for String {
129    fn from(value: HostnameMatch) -> Self {
130        match value {
131            HostnameMatch::Subdomain(_) => value.to_string(),
132            HostnameMatch::Exact(inner) => inner.0.to_string(),
133        }
134    }
135}
136
137/// A Route is a policy that describes how a request to a specific virtual
138/// host should be routed.
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140#[serde(deny_unknown_fields)]
141#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
142pub struct Route {
143    /// A globally unique identifier for this Route.
144    ///
145    /// Route IDs must be valid RFC 1035 DNS label names - they must start with
146    /// a lowercase ascii character, and can only contain lowercase ascii
147    /// alphanumeric characters and the `-` character.
148    pub id: Name,
149
150    /// A list of arbitrary tags that can be added to a Route.
151    #[serde(default)]
152    // TODO: limit this a-la kube annotation keys/values.
153    pub tags: BTreeMap<String, String>,
154
155    /// The hostnames that match this Route.
156    #[serde(default)]
157    pub hostnames: Vec<HostnameMatch>,
158
159    /// The ports that match this Route.
160    #[serde(default)]
161    pub ports: Vec<u16>,
162
163    /// The rules that determine whether a request matches and where traffic
164    /// should be routed.
165    #[serde(default)]
166    pub rules: Vec<RouteRule>,
167}
168
169impl Route {
170    /// Create a trivial route that passes all traffic for a target directly to
171    /// the given Service. The request port will be used to identify a
172    /// specific backend at request time.
173    pub fn passthrough_route(id: Name, service: Service) -> Route {
174        Route {
175            id,
176            hostnames: vec![service.hostname().into()],
177            ports: vec![],
178            tags: Default::default(),
179            rules: vec![RouteRule {
180                matches: vec![RouteMatch {
181                    path: Some(PathMatch::empty_prefix()),
182                    ..Default::default()
183                }],
184                backends: vec![BackendRef {
185                    service,
186                    port: None,
187                    weight: 1,
188                }],
189                ..Default::default()
190            }],
191        }
192    }
193}
194
195/// A RouteRule contains a set of matches that define which requests it applies
196/// to, processing rules, and the final destination(s) for matching traffic.
197///
198/// See the Junction docs for a high level description of how Routes and
199/// RouteRules behave.
200///
201/// # Ordering Rules
202///
203/// Route rules may be ordered by comparing their maximum
204/// [matches][Self::matches], breaking ties by comparing their next-highest
205/// match. This provides a total ordering on rules. Note that having a sorted
206/// list of rules does not mean that the list of all matches across all rules is
207/// totally sorted.
208///
209/// This ordering is provided for convenience - clients match rules in the order
210/// they're listed in a Route.
211#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
212#[serde(deny_unknown_fields)]
213#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
214pub struct RouteRule {
215    /// A human-readable name for this rule.
216    ///
217    /// This name is completely optional, and will only be used in diagnostics
218    /// to make it easier to debug. Diagnostics that don't have a name will be
219    /// referred to by their index in a Route's list of rules.
220    #[serde(default, skip_serializing_if = "Option::is_none")]
221    pub name: Option<Name>,
222
223    /// A list of match rules applied to an outgoing request.  Each match is
224    /// independent; this rule will be matched if **any** of the listed matches
225    /// is satisfied.
226    ///
227    /// If no matches are specified, this Rule matches any outgoing request.
228    #[serde(default, skip_serializing_if = "Vec::is_empty")]
229    pub matches: Vec<RouteMatch>,
230
231    /// Define the filters that are applied to requests that match this rule.
232    ///
233    /// The effects of ordering of multiple behaviors are currently unspecified.
234    ///
235    /// Specifying the same filter multiple times is not supported unless
236    /// explicitly indicated in the filter.
237    ///
238    /// All filters are compatible with each other except for the URLRewrite and
239    /// RequestRedirect filters, which may not be combined.
240    #[serde(default, skip_serializing_if = "Vec::is_empty")]
241    #[doc(hidden)]
242    pub filters: Vec<RouteFilter>,
243
244    // The timeouts set on any request that matches route.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub timeouts: Option<RouteTimeouts>,
247
248    /// How to retry requests. If not specified, requests are not retried.
249    #[serde(default, skip_serializing_if = "Option::is_none")]
250    pub retry: Option<RouteRetry>,
251
252    /// Where the traffic should route if this rule matches.
253    ///
254    /// If no backends are specified, this route becomes a black hole for
255    /// traffic and all matching requests return an error.
256    #[serde(default)]
257    pub backends: Vec<BackendRef>,
258}
259
260impl PartialOrd for RouteRule {
261    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
262        Some(self.cmp(other))
263    }
264}
265
266impl Ord for RouteRule {
267    fn cmp(&self, other: &Self) -> Ordering {
268        let mut self_matches: Vec<_> = self.matches.iter().collect();
269        self_matches.sort();
270        let mut self_matches = self_matches.iter().rev();
271
272        let mut other_matches: Vec<_> = other.matches.iter().collect();
273        other_matches.sort();
274        let mut other_matches = other_matches.iter().rev();
275
276        loop {
277            match (self_matches.next(), other_matches.next()) {
278                (None, None) => return Ordering::Equal,
279                (None, Some(_)) => return Ordering::Less,
280                (Some(_), None) => return Ordering::Greater,
281                (Some(a), Some(b)) => match a.cmp(b) {
282                    Ordering::Equal => {}
283                    ord => return ord,
284                },
285            }
286        }
287    }
288}
289
290/// Defines timeouts that can be configured for a HTTP Route.
291#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
292#[serde(deny_unknown_fields)]
293#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
294pub struct RouteTimeouts {
295    /// Specifies the maximum duration for a HTTP request. This timeout is
296    /// intended to cover as close to the whole request-response transaction as
297    /// possible.
298    ///
299    /// An entire client HTTP transaction may result in more than one call to
300    /// destination backends, for example, if automatic retries are configured.
301    #[serde(default, skip_serializing_if = "Option::is_none")]
302    pub request: Option<Duration>,
303
304    /// Specifies a timeout for an individual request to a backend. This covers
305    /// the time from when the request first starts being sent to when the full
306    /// response has been received from the backend.
307    ///
308    /// Because the overall request timeout encompasses the backend request
309    /// timeout, the value of this timeout must be less than or equal to the
310    /// value of the overall timeout.
311    #[serde(
312        default,
313        skip_serializing_if = "Option::is_none",
314        alias = "backendRequest"
315    )]
316    pub backend_request: Option<Duration>,
317}
318
319/// Defines the predicate used to match requests to a given action. Multiple
320/// match types are ANDed together; the match will evaluate to true only if all
321/// conditions are satisfied. For example, if a match specifies a `path` match
322/// and two `query_params` matches, it will match only if the request's path
323/// matches and both of the `query_params` are matches.
324///
325/// The default RouteMatch functions like a path match on the empty prefix,
326/// which matches every request.
327///
328/// ## Match Ordering
329///
330/// Route matches are [Ordered][Ord] to help break ties. While once a Route is
331/// constructed, rules are matched in-order, sorting matches and rules can be
332/// useful while constructing a Route in case there isn't an obvious order
333/// matches should apply in. From highest-value to lowest value, routes are
334/// ordered by:
335///
336/// - "Exact" path match.
337/// - "Prefix" path match with largest number of characters.
338/// - Method match.
339/// - Largest number of header matches.
340/// - Largest number of query param matches.
341///
342/// Note that this means a route that matches on a path is greater than a route
343/// that only matches on headers. To get a natural sort order by precedence, you
344/// may want to reverse-sort a list of matches.
345#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
346#[serde(deny_unknown_fields)]
347#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
348pub struct RouteMatch {
349    /// Specifies a HTTP request path matcher.
350    #[serde(default, skip_serializing_if = "Option::is_none")]
351    pub path: Option<PathMatch>,
352
353    /// Specifies HTTP request header matchers. Multiple match values are ANDed
354    /// together, meaning, a request must match all the specified headers.
355    #[serde(default, skip_serializing_if = "Vec::is_empty")]
356    pub headers: Vec<HeaderMatch>,
357
358    /// Specifies HTTP query parameter matchers. Multiple match values are ANDed
359    /// together, meaning, a request must match all the specified query
360    /// parameters.
361    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "queryParams")]
362    pub query_params: Vec<QueryParamMatch>,
363
364    /// Specifies HTTP method matcher. When specified, this route will be
365    /// matched only if the request has the specified method.
366    #[serde(default, skip_serializing_if = "Option::is_none")]
367    pub method: Option<Method>,
368}
369
370impl PartialOrd for RouteMatch {
371    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
372        Some(self.cmp(other))
373    }
374}
375
376impl Ord for RouteMatch {
377    fn cmp(&self, other: &Self) -> Ordering {
378        match self.path.cmp(&other.path) {
379            Ordering::Equal => (),
380            cmp => return cmp,
381        }
382
383        match (&self.method, &other.method) {
384            (None, Some(_)) => return Ordering::Less,
385            (Some(_), None) => return Ordering::Greater,
386            _ => (),
387        }
388
389        match self.headers.len().cmp(&other.headers.len()) {
390            Ordering::Equal => (),
391            cmp => return cmp,
392        }
393
394        self.query_params.len().cmp(&other.query_params.len())
395    }
396}
397
398/// Describes how to select a HTTP route by matching the HTTP request path.  The
399/// `type` of a match specifies how HTTP paths should be compared.
400///
401/// PathPrefix and Exact paths must be syntactically valid:
402/// - Must begin with the `/` character
403/// - Must not contain consecutive `/` characters (e.g. `/foo///`, `//`)
404#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
405#[serde(tag = "type")]
406#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
407pub enum PathMatch {
408    #[serde(alias = "prefix")]
409    Prefix { value: String },
410
411    #[serde(alias = "regularExpression", alias = "regular_expression")]
412    RegularExpression { value: Regex },
413
414    #[serde(untagged)]
415    Exact { value: String },
416}
417
418impl PathMatch {
419    /// Return a [PathMatch] that matches the empty prefix.
420    ///
421    /// The empty prefix matches every path, so any matcher using this will
422    /// always return `true`.
423    pub fn empty_prefix() -> Self {
424        Self::Prefix {
425            value: String::new(),
426        }
427    }
428}
429
430impl PartialOrd for PathMatch {
431    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
432        Some(self.cmp(other))
433    }
434}
435
436impl Ord for PathMatch {
437    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
438        match (self, other) {
439            // exact.cmp
440            (Self::Exact { value: v1 }, Self::Exact { value: v2 }) => v1.len().cmp(&v2.len()),
441            (Self::Exact { .. }, _) => Ordering::Greater,
442            // prefix.cmp
443            (Self::Prefix { .. }, Self::Exact { .. }) => Ordering::Less,
444            (Self::Prefix { value: v1 }, Self::Prefix { value: v2 }) => v1.len().cmp(&v2.len()),
445            (Self::Prefix { .. }, _) => Ordering::Greater,
446            // regex.cmp
447            (Self::RegularExpression { value: v1 }, Self::RegularExpression { value: v2 }) => {
448                v1.as_str().len().cmp(&v2.as_str().len())
449            }
450            (Self::RegularExpression { .. }, _) => Ordering::Less,
451        }
452    }
453}
454
455/// The name of an HTTP header.
456///
457/// Valid values include:
458///
459/// * "Authorization"
460/// * "Set-Cookie"
461///
462/// Invalid values include:
463///
464/// * ":method" - ":" is an invalid character. This means that HTTP/2 pseudo
465///   headers are not currently supported by this type.
466///
467/// * "/invalid" - "/" is an invalid character
468//
469// FIXME: newtype and validate this. probably also make this Bytes or SmolString
470pub type HeaderName = String;
471
472/// Describes how to select a HTTP route by matching HTTP request headers.
473///
474/// `name` is the name of the HTTP Header to be matched. Name matching is case
475/// insensitive. (See <https://tools.ietf.org/html/rfc7230#section-3.2>).
476///
477/// If multiple entries specify equivalent header names, only the first entry
478/// with an equivalent name WILL be considered for a match. Subsequent entries
479/// with an equivalent header name WILL be ignored. Due to the
480/// case-insensitivity of header names, "foo" and "Foo" are considered
481/// equivalent.
482//
483// FIXME: actually do this only-the-first-entry matching thing
484#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
485#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
486#[serde(tag = "type", deny_unknown_fields)]
487pub enum HeaderMatch {
488    #[serde(
489        alias = "regex",
490        alias = "regular_expression",
491        alias = "regularExpression"
492    )]
493    RegularExpression { name: String, value: Regex },
494
495    #[serde(untagged)]
496    Exact { name: String, value: String },
497}
498
499impl HeaderMatch {
500    pub fn name(&self) -> &str {
501        match self {
502            HeaderMatch::RegularExpression { name, .. } => name,
503            HeaderMatch::Exact { name, .. } => name,
504        }
505    }
506
507    pub fn is_match(&self, header_value: &str) -> bool {
508        match self {
509            HeaderMatch::RegularExpression { value, .. } => value.is_match(header_value),
510            HeaderMatch::Exact { value, .. } => value == header_value,
511        }
512    }
513}
514
515/// Describes how to select a HTTP route by matching HTTP query parameters.
516#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
517#[serde(deny_unknown_fields, tag = "type")]
518#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
519pub enum QueryParamMatch {
520    #[serde(
521        alias = "regex",
522        alias = "regular_expression",
523        alias = "regularExpression"
524    )]
525    RegularExpression { name: String, value: Regex },
526
527    #[serde(untagged)]
528    Exact { name: String, value: String },
529}
530
531impl QueryParamMatch {
532    pub fn name(&self) -> &str {
533        match self {
534            QueryParamMatch::RegularExpression { name, .. } => name,
535            QueryParamMatch::Exact { name, .. } => name,
536        }
537    }
538
539    pub fn is_match(&self, param_value: &str) -> bool {
540        match self {
541            QueryParamMatch::RegularExpression { value, .. } => value.is_match(param_value),
542            QueryParamMatch::Exact { value, .. } => value == param_value,
543        }
544    }
545}
546
547/// Describes how to select a HTTP route by matching the HTTP method as defined by [RFC
548/// 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-4) and [RFC
549/// 5789](https://datatracker.ietf.org/doc/html/rfc5789#section-2). The value is expected in upper
550/// case.
551//
552// FIXME: replace with http::Method
553pub type Method = String;
554
555/// Defines processing steps that must be completed during the request or
556/// response lifecycle.
557//
558// TODO: This feels very gateway-ey and redundant to type out in config. Should we switch to
559// untagged here? Something else?
560#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
561#[serde(tag = "type", deny_unknown_fields)]
562#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
563pub enum RouteFilter {
564    /// Defines a schema for a filter that modifies request headers.
565    RequestHeaderModifier {
566        /// A Header filter.
567        #[serde(alias = "requestHeaderModifier")]
568        request_header_modifier: HeaderFilter,
569    },
570
571    ///  Defines a schema for a filter that modifies response headers.
572    ResponseHeaderModifier {
573        /// A Header filter.
574        #[serde(alias = "responseHeaderModifier")]
575        response_header_modifier: HeaderFilter,
576    },
577
578    /// Defines a schema for a filter that mirrors requests. Requests are sent to the specified
579    /// destination, but responses from that destination are ignored.
580    ///
581    /// This filter can be used multiple times within the same rule. Note that not all
582    /// implementations will be able to support mirroring to multiple backends.
583    RequestMirror {
584        #[serde(alias = "requestMirror")]
585        request_mirror: RequestMirrorFilter,
586    },
587
588    /// Defines a schema for a filter that responds to the request with an HTTP redirection.
589    RequestRedirect {
590        #[serde(alias = "requestRedirect")]
591        /// A redirect filter.
592        request_redirect: RequestRedirectFilter,
593    },
594
595    /// Defines a schema for a filter that modifies a request during forwarding.
596    URLRewrite {
597        /// A URL rewrite filter.
598        #[serde(alias = "urlRewrite")]
599        url_rewrite: UrlRewriteFilter,
600    },
601}
602
603/// Defines configuration for the RequestHeaderModifier filter.
604#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
605#[serde(deny_unknown_fields)]
606#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
607pub struct HeaderFilter {
608    /// Overwrites the request with the given header (name, value) before the action. Note that the
609    /// header names are case-insensitive (see
610    /// <https://datatracker.ietf.org/doc/html/rfc2616#section-4.2>).
611    ///
612    /// Input: GET /foo HTTP/1.1 my-header: foo
613    ///
614    /// Config: set:
615    ///   - name: "my-header" value: "bar"
616    ///
617    /// Output: GET /foo HTTP/1.1 my-header: bar
618    #[serde(default, skip_serializing_if = "Vec::is_empty")]
619    pub set: Vec<HeaderValue>,
620
621    /// Add adds the given header(s) (name, value) to the request before the action. It appends to
622    /// any existing values associated with the header name.
623    ///
624    /// Input: GET /foo HTTP/1.1 my-header: foo
625    ///
626    /// Config: add:
627    ///   - name: "my-header" value: "bar"
628    ///
629    /// Output: GET /foo HTTP/1.1 my-header: foo my-header: bar
630    #[serde(default, skip_serializing_if = "Vec::is_empty")]
631    pub add: Vec<HeaderValue>,
632
633    /// Remove the given header(s) from the HTTP request before the action. The value of Remove is a
634    /// list of HTTP header names. Note that the header names are case-insensitive (see
635    /// <https://datatracker.ietf.org/doc/html/rfc2616#section-4.2>).
636    ///
637    /// Input: GET /foo HTTP/1.1 my-header1: foo my-header2: bar my-header3: baz
638    ///
639    /// Config: remove: ["my-header1", "my-header3"]
640    ///
641    /// Output: GET /foo HTTP/1.1 my-header2: bar
642    #[serde(default, skip_serializing_if = "Vec::is_empty")]
643    pub remove: Vec<String>,
644}
645
646#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
647#[serde(deny_unknown_fields)]
648#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
649pub struct HeaderValue {
650    /// The name of the HTTP Header. Note header names are case insensitive. (See
651    /// <https://tools.ietf.org/html/rfc7230#section-3.2>).
652    pub name: HeaderName,
653
654    /// The value of HTTP Header.
655    pub value: String,
656}
657
658/// Defines configuration for path modifiers.
659#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
660#[serde(tag = "type", deny_unknown_fields)]
661#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
662pub enum PathModifier {
663    /// Specifies the value with which to replace the full path of a request during a rewrite or
664    /// redirect.
665    ReplaceFullPath {
666        /// The value to replace the path with.
667        #[serde(alias = "replaceFullPath")]
668        replace_full_path: String,
669    },
670
671    /// Specifies the value with which to replace the prefix match of a request during a rewrite or
672    /// redirect. For example, a request to "/foo/bar" with a prefix match of "/foo" and a
673    /// ReplacePrefixMatch of "/xyz" would be modified to "/xyz/bar".
674    ///
675    /// Note that this matches the behavior of the PathPrefix match type. This matches full path
676    /// elements. A path element refers to the list of labels in the path split by the `/`
677    /// separator. When specified, a trailing `/` is ignored. For example, the paths `/abc`,
678    /// `/abc/`, and `/abc/def` would all match the prefix `/abc`, but the path `/abcd` would not.
679    ///
680    /// ReplacePrefixMatch is only compatible with a `PathPrefix` route match::
681    ///
682    ///  ```plaintext,no_run
683    ///  Request Path | Prefix Match | Replace Prefix | Modified Path
684    ///  -------------|--------------|----------------|----------
685    ///  /foo/bar     | /foo         | /xyz           | /xyz/bar
686    ///  /foo/bar     | /foo         | /xyz/          | /xyz/bar
687    ///  /foo/bar     | /foo/        | /xyz           | /xyz/bar
688    ///  /foo/bar     | /foo/        | /xyz/          | /xyz/bar
689    ///  /foo         | /foo         | /xyz           | /xyz
690    ///  /foo/        | /foo         | /xyz           | /xyz/
691    ///  /foo/bar     | /foo         | <empty string> | /bar
692    ///  /foo/        | /foo         | <empty string> | /
693    ///  /foo         | /foo         | <empty string> | /
694    ///  /foo/        | /foo         | /              | /
695    ///  /foo         | /foo         | /              | /
696    ///  ```
697    ReplacePrefixMatch {
698        #[serde(alias = "replacePrefixMatch")]
699        replace_prefix_match: String,
700    },
701}
702
703/// Defines a filter that redirects a request. This filter MUST not be used on the same Route rule
704/// as a URL Rewrite filter.
705#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
706#[serde(deny_unknown_fields)]
707#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
708pub struct RequestRedirectFilter {
709    /// The scheme to be used in the value of the `Location` header in the response. When empty, the
710    /// scheme of the request is used.
711    ///
712    /// Scheme redirects can affect the port of the redirect, for more information, refer to the
713    /// documentation for the port field of this filter.
714    #[serde(default, skip_serializing_if = "Option::is_none")]
715    pub scheme: Option<String>,
716
717    /// The hostname to be used in the value of the `Location` header in the response. When empty,
718    /// the hostname in the `Host` header of the request is used.
719    #[serde(default, skip_serializing_if = "Option::is_none")]
720    pub hostname: Option<Name>,
721
722    /// Defines parameters used to modify the path of the incoming request. The modified path is
723    /// then used to construct the `Location` header. When empty, the request path is used as-is.
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub path: Option<PathModifier>,
726
727    /// The port to be used in the value of the `Location` header in the response.
728    ///
729    /// If no port is specified, the redirect port MUST be derived using the following rules:
730    ///
731    /// * If redirect scheme is not-empty, the redirect port MUST be the well-known port associated
732    ///   with the redirect scheme. Specifically "http" to port 80 and "https" to port 443. If the
733    ///   redirect scheme does not have a well-known port, the listener port of the Gateway SHOULD
734    ///   be used.
735    /// * If redirect scheme is empty, the redirect port MUST be the Gateway Listener port.
736    ///
737    /// Will not add the port number in the 'Location' header in the following cases:
738    ///
739    /// * A Location header that will use HTTP (whether that is determined via the Listener protocol
740    ///   or the Scheme field) _and_ use port 80.
741    /// * A Location header that will use HTTPS (whether that is determined via the Listener
742    ///   protocol or the Scheme field) _and_ use port 443.
743    #[serde(default, skip_serializing_if = "Option::is_none")]
744    pub port: Option<u16>,
745
746    /// The HTTP status code to be used in response.
747    #[serde(default, skip_serializing_if = "Option::is_none", alias = "statusCode")]
748    pub status_code: Option<u16>,
749}
750
751/// Defines a filter that modifies a request during forwarding. At most one of these filters may be
752/// used on a Route rule. This may not be used on the same Route rule as a RequestRedirect filter.
753#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
754#[serde(deny_unknown_fields)]
755#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
756pub struct UrlRewriteFilter {
757    /// The value to be used to replace the Host header value during forwarding.
758    #[serde(default, skip_serializing_if = "Option::is_none")]
759    pub hostname: Option<Hostname>,
760
761    /// Defines a path rewrite.
762    #[serde(default, skip_serializing_if = "Option::is_none")]
763    pub path: Option<PathModifier>,
764}
765
766/// Defines configuration for the RequestMirror filter.
767#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
768#[serde(deny_unknown_fields)]
769#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
770pub struct RequestMirrorFilter {
771    /// Represents the percentage of requests that should be mirrored to BackendRef. Its minimum
772    /// value is 0 (indicating 0% of requests) and its maximum value is 100 (indicating 100% of
773    /// requests).
774    ///
775    /// Only one of Fraction or Percent may be specified. If neither field is specified, 100% of
776    /// requests will be mirrored.
777    pub percent: Option<i32>,
778
779    /// Only one of Fraction or Percent may be specified. If neither field is specified, 100% of
780    /// requests will be mirrored.
781    pub fraction: Option<Fraction>,
782
783    pub backend: Service,
784}
785
786/// Configure client retry policy.
787#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
788#[serde(deny_unknown_fields)]
789#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
790pub struct RouteRetry {
791    /// The HTTP error codes that retries should be applied to.
792    //
793    // TODO: should this be http::StatusCode?
794    #[serde(default, skip_serializing_if = "Vec::is_empty")]
795    pub codes: Vec<u16>,
796
797    /// The total number of attempts to make when retrying this request.
798    #[serde(default, skip_serializing_if = "Option::is_none")]
799    pub attempts: Option<u32>,
800
801    /// The amount of time to back off between requests during a series of
802    /// retries.
803    #[serde(default, skip_serializing_if = "Option::is_none")]
804    pub backoff: Option<Duration>,
805}
806
807const fn default_weight() -> u32 {
808    1
809}
810
811// TODO: gateway API also allows filters here under an extended support
812// condition we need to decide whether this is one where its simpler just to
813// drop it.
814#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
815#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
816pub struct BackendRef {
817    /// The Serivce to route to when traffic matches. This Service will always
818    /// be combined with a `port` to uniquely identify the
819    /// [Backend][crate::backend::Backend] traffic should be routed to.
820    #[serde(flatten)]
821    pub service: Service,
822
823    /// The port to route traffic to, used in combination with
824    /// [service][Self::service] to identify the
825    /// [Backend][crate::backend::Backend] to route traffic to.
826    ///
827    /// If omitted, the port of the incoming request is used to route traffic.
828    pub port: Option<u16>,
829
830    /// The relative weight of this backend relative to any other backends in
831    /// [the list][RouteRule::backends].
832    ///
833    /// If not specified, defaults to `1`.
834    ///
835    /// An individual backend may have a weight of `0`, but specifying every
836    /// backend with `0` weight is an error.
837    #[serde(default = "default_weight")]
838    pub weight: u32,
839}
840
841impl BackendRef {
842    #[doc(hidden)]
843    pub fn into_backend_id(&self, default_port: u16) -> BackendId {
844        let port = self.port.unwrap_or(default_port);
845
846        BackendId {
847            service: self.service.clone(),
848            port,
849        }
850    }
851
852    #[doc(hidden)]
853    pub fn as_backend_id(&self) -> Option<BackendId> {
854        let port = self.port?;
855
856        Some(BackendId {
857            service: self.service.clone(),
858            port,
859        })
860    }
861
862    #[cfg(feature = "xds")]
863    pub(crate) fn name(&self) -> String {
864        let mut buf = String::new();
865        self.write_name(&mut buf).unwrap();
866        buf
867    }
868
869    #[cfg(feature = "xds")]
870    fn write_name(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
871        self.service.write_name(w)?;
872        if let Some(port) = self.port {
873            write!(w, ":{port}")?;
874        }
875
876        Ok(())
877    }
878}
879
880impl FromStr for BackendRef {
881    type Err = crate::Error;
882
883    fn from_str(s: &str) -> Result<Self, Self::Err> {
884        let (name, port) = super::parse_port(s)?;
885        let backend = Service::from_str(name)?;
886
887        Ok(Self {
888            service: backend,
889            port,
890            weight: default_weight(),
891        })
892    }
893}
894
895#[cfg(test)]
896mod test {
897    use std::str::FromStr;
898
899    use rand::seq::SliceRandom;
900    use serde::de::DeserializeOwned;
901    use serde_json::json;
902
903    use super::*;
904    use crate::{
905        http::{HeaderMatch, RouteRule},
906        shared::Regex,
907        Service,
908    };
909
910    #[test]
911    fn test_hostname_match() {
912        let exact_matcher = HostnameMatch::from_str("foo.bar").unwrap();
913        let subdomain_matcher = HostnameMatch::from_str("*.foo.bar").unwrap();
914
915        for invalid_hostname in [
916            "",
917            "*",
918            ".*",
919            ".",
920            "!@#@!#!@",
921            "foo....bar",
922            ".foo.bar",
923            "...foo.bar",
924        ] {
925            assert!(!exact_matcher.matches_str(invalid_hostname));
926            assert!(!subdomain_matcher.matches_str(invalid_hostname));
927        }
928
929        for not_matching in ["blahfoo.bar", "bfoo.bar", "bar.foo"] {
930            assert!(!exact_matcher.matches_str(not_matching));
931            assert!(!subdomain_matcher.matches_str(not_matching));
932        }
933
934        assert!(exact_matcher.matches_str("foo.bar"));
935        assert!(!subdomain_matcher.matches_str("foo.bar"));
936
937        assert!(!exact_matcher.matches_str("blah.foo.bar"));
938        assert!(subdomain_matcher.matches_str("blah.foo.bar"));
939        assert!(subdomain_matcher.matches_str("b.foo.bar"));
940    }
941
942    #[test]
943    fn test_hostname_match_json() {
944        let json_value = json!(["foo.bar.baz", "*.foo.bar.baz",]);
945        let matchers = vec![
946            HostnameMatch::Exact(Hostname::from_static("foo.bar.baz")),
947            HostnameMatch::Subdomain(Hostname::from_static("foo.bar.baz")),
948        ];
949
950        assert_eq!(
951            serde_json::from_value::<Vec<HostnameMatch>>(json_value.clone()).unwrap(),
952            matchers,
953        );
954        assert_eq!(serde_json::to_value(&matchers).unwrap(), json_value,);
955    }
956
957    #[test]
958    fn test_header_matcher_json() {
959        let test_json = json!([
960            { "name":"bar", "type" : "RegularExpression", "value": ".*foo"},
961            { "name":"bar", "value": "a literal"},
962        ]);
963        let obj: Vec<HeaderMatch> = serde_json::from_value(test_json.clone()).unwrap();
964
965        assert_eq!(
966            obj,
967            vec![
968                HeaderMatch::RegularExpression {
969                    name: "bar".to_string(),
970                    value: Regex::from_str(".*foo").unwrap(),
971                },
972                HeaderMatch::Exact {
973                    name: "bar".to_string(),
974                    value: "a literal".to_string(),
975                }
976            ]
977        );
978
979        let output_json = serde_json::to_value(&obj).unwrap();
980        assert_eq!(test_json, output_json);
981    }
982
983    #[test]
984    fn test_retry_policy_json() {
985        let test_json = json!({
986            "codes":[ 1, 2 ],
987            "attempts": 3,
988            // NOTE: serde will happily read an int here, but Duration serializes as a float
989            "backoff": 60.0,
990        });
991        let obj: RouteRetry = serde_json::from_value(test_json.clone()).unwrap();
992        let output_json = serde_json::to_value(obj).unwrap();
993        assert_eq!(test_json, output_json);
994    }
995
996    #[test]
997    fn test_route_rule_json() {
998        let test_json = json!({
999            "matches":[
1000                {
1001                    "method": "GET",
1002                    "path": { "value": "foo" },
1003                    "headers": [
1004                        {"name":"ian", "value": "foo"},
1005                        {"name": "bar", "type":"RegularExpression", "value": ".*foo"}
1006                    ]
1007                },
1008                {
1009                    "query_params": [
1010                        {"name":"ian", "value": "foo"},
1011                        {"name": "bar", "type":"RegularExpression", "value": ".*foo"}
1012                    ]
1013                }
1014            ],
1015            "filters":[{
1016                "type": "URLRewrite",
1017                "url_rewrite":{
1018                    "hostname":"ian.com",
1019                    "path": {"type":"ReplacePrefixMatch", "replace_prefix_match":"/"}
1020                }
1021            }],
1022            "backends":[
1023                {
1024                    "type": "kube",
1025                    "name": "timeout-svc",
1026                    "namespace": "foo",
1027                    "port": 80,
1028                    "weight": 1,
1029                }
1030            ],
1031            "timeouts": {
1032                "request": 1.0,
1033            }
1034        });
1035        let obj: RouteRule = serde_json::from_value(test_json.clone()).unwrap();
1036        let output_json = serde_json::to_value(&obj).unwrap();
1037        assert_eq!(test_json, output_json);
1038    }
1039
1040    #[test]
1041    fn test_route_json() {
1042        assert_deserialize(
1043            json!({
1044                "id": "sweet-potato",
1045                "hostnames": ["foo.bar.svc.cluster.local"],
1046                "rules": [
1047                    {
1048                        "backends": [
1049                            {
1050                                "type": "kube",
1051                                "name": "foo",
1052                                "namespace": "bar",
1053                                "port": 80,
1054                            }
1055                        ],
1056                    }
1057                ]
1058            }),
1059            Route {
1060                id: Name::from_static("sweet-potato"),
1061                hostnames: vec![Hostname::from_static("foo.bar.svc.cluster.local").into()],
1062                ports: vec![],
1063                tags: Default::default(),
1064                rules: vec![RouteRule {
1065                    name: None,
1066                    matches: vec![],
1067                    filters: vec![],
1068                    timeouts: None,
1069                    retry: None,
1070                    backends: vec![BackendRef {
1071                        service: Service::kube("bar", "foo").unwrap(),
1072                        port: Some(80),
1073                        weight: 1,
1074                    }],
1075                }],
1076            },
1077        );
1078    }
1079
1080    #[test]
1081    fn test_route_json_missing_fields() {
1082        assert_deserialize_err::<Route>(json!({
1083            "uhhhh": ["foo.bar"],
1084            "rules": [
1085                {
1086                    "matches": [],
1087                }
1088            ]
1089        }));
1090    }
1091
1092    #[track_caller]
1093    fn assert_deserialize<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
1094        json: serde_json::Value,
1095        expected: T,
1096    ) {
1097        let actual: T = serde_json::from_value(json).unwrap();
1098        assert_eq!(expected, actual);
1099    }
1100
1101    #[track_caller]
1102    fn assert_deserialize_err<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
1103        json: serde_json::Value,
1104    ) -> serde_json::Error {
1105        serde_json::from_value::<T>(json).unwrap_err()
1106    }
1107
1108    #[test]
1109    fn test_path_match() {
1110        // test that exact cmps are only based on length
1111        arbtest::arbtest(|u| {
1112            let s1: String = u.arbitrary()?;
1113            let s2: String = u.arbitrary()?;
1114
1115            let m1 = PathMatch::Exact { value: s1.clone() };
1116            let m2 = PathMatch::Exact { value: s2.clone() };
1117
1118            assert_eq!(s1.len().cmp(&s2.len()), m1.cmp(&m2));
1119
1120            Ok(())
1121        });
1122
1123        // test that prefix cmps are only based on length
1124        arbtest::arbtest(|u| {
1125            let s1: String = u.arbitrary()?;
1126            let s2: String = u.arbitrary()?;
1127
1128            let m1 = PathMatch::Prefix { value: s1.clone() };
1129            let m2 = PathMatch::Prefix { value: s2.clone() };
1130
1131            assert_eq!(s1.len().cmp(&s2.len()), m1.cmp(&m2));
1132
1133            Ok(())
1134        });
1135
1136        // test that exact > prefix always
1137        arbtest::arbtest(|u| {
1138            let m1 = PathMatch::Exact {
1139                value: u.arbitrary()?,
1140            };
1141            let m2 = PathMatch::Prefix {
1142                value: u.arbitrary()?,
1143            };
1144            assert!(m1 > m2);
1145
1146            Ok(())
1147        });
1148    }
1149
1150    #[test]
1151    fn test_order_route_match() {
1152        let path_match = RouteMatch {
1153            path: Some(PathMatch::Exact {
1154                value: "/potato".to_string(),
1155            }),
1156            ..Default::default()
1157        };
1158        let method_match = RouteMatch {
1159            method: Some("PUT".to_string()),
1160            ..Default::default()
1161        };
1162        let header_match = RouteMatch {
1163            headers: vec![HeaderMatch::Exact {
1164                name: "x-user".to_string(),
1165                value: "a-user".to_string(),
1166            }],
1167            ..Default::default()
1168        };
1169        let query_match = RouteMatch {
1170            query_params: vec![QueryParamMatch::Exact {
1171                name: "q".to_string(),
1172                value: "value".to_string(),
1173            }],
1174            ..Default::default()
1175        };
1176
1177        // order some simple combos
1178        assert_eq!(
1179            vec![&query_match, &header_match, &method_match, &path_match],
1180            shuffle_and_sort([&path_match, &query_match, &header_match, &method_match]),
1181        );
1182
1183        // tie-breaking within a field
1184        let m1 = RouteMatch {
1185            path: Some(PathMatch::Exact {
1186                value: "fooooooooooo".to_string(),
1187            }),
1188            query_params: query_match.query_params.clone(),
1189            ..Default::default()
1190        };
1191        let m2 = RouteMatch {
1192            path: Some(PathMatch::Exact {
1193                value: "foo".to_string(),
1194            }),
1195            query_params: query_match.query_params.clone(),
1196            ..Default::default()
1197        };
1198        assert!(m1 > m2, "should tie break by comparing path_match");
1199
1200        // tie-breaking with other fields
1201        let m1 = RouteMatch {
1202            path: path_match.path.clone(),
1203            query_params: query_match.query_params.clone(),
1204            ..Default::default()
1205        };
1206        let m2 = RouteMatch {
1207            path: path_match.path.clone(),
1208            ..Default::default()
1209        };
1210        assert!(m1 > m2, "should tie-break with query params");
1211
1212        let m1 = RouteMatch {
1213            method: Some("GET".to_string()),
1214            query_params: query_match.query_params.clone(),
1215            ..Default::default()
1216        };
1217        let m2 = RouteMatch {
1218            method: Some("PUT".to_string()),
1219            ..Default::default()
1220        };
1221        assert!(m1 > m2, "should tie-break with query params");
1222    }
1223
1224    #[test]
1225    fn test_order_route_rule() {
1226        let path_match = RouteMatch {
1227            path: Some(PathMatch::Exact {
1228                value: "/potato".to_string(),
1229            }),
1230            ..Default::default()
1231        };
1232        let header_match = RouteMatch {
1233            headers: vec![HeaderMatch::Exact {
1234                name: "x-user".to_string(),
1235                value: "a-user".to_string(),
1236            }],
1237            ..Default::default()
1238        };
1239        let query_match = RouteMatch {
1240            query_params: vec![QueryParamMatch::Exact {
1241                name: "q".to_string(),
1242                value: "value".to_string(),
1243            }],
1244            ..Default::default()
1245        };
1246
1247        // simple single match
1248        let r1 = RouteRule {
1249            matches: vec![path_match.clone()],
1250            ..Default::default()
1251        };
1252        let r2 = RouteRule {
1253            matches: vec![header_match.clone()],
1254            ..Default::default()
1255        };
1256        assert!(r1 > r2);
1257        assert!(r2 < r1);
1258
1259        // tie break with extra matches
1260        let r1 = RouteRule {
1261            matches: vec![path_match.clone()],
1262            ..Default::default()
1263        };
1264        let r2 = RouteRule {
1265            matches: vec![path_match.clone(), header_match.clone()],
1266            ..Default::default()
1267        };
1268        assert!(r1 < r2);
1269        assert!(r2 > r1);
1270
1271        // empty matches sorts last
1272        let r1 = RouteRule {
1273            matches: vec![query_match.clone()],
1274            ..Default::default()
1275        };
1276        let r2 = RouteRule {
1277            matches: vec![],
1278            ..Default::default()
1279        };
1280        assert!(r2 < r1);
1281        assert!(r1 > r2);
1282    }
1283
1284    fn shuffle_and_sort<T: Ord>(xs: impl IntoIterator<Item = T>) -> Vec<T> {
1285        let mut rng = rand::thread_rng();
1286
1287        let mut v: Vec<_> = xs.into_iter().collect();
1288        v.shuffle(&mut rng);
1289        v.sort();
1290        v
1291    }
1292}