Skip to main content

rws_gateway_api/gateway_api/
router.rs

1//! Routing one request on a Gateway listener port: pick the listener by
2//! hostname, the rule by the Gateway API's precedence, a backend by weight,
3//! an endpoint, and forward — keeping the client's `Host`.
4//!
5//! Precedence (HTTPRoute spec), highest first: the longest non-wildcard
6//! matching hostname, the longest matching hostname, an `Exact` path over a
7//! `PathPrefix`, the longest path, a method match, the most header matches,
8//! the most query-parameter matches; ties go to the oldest route, then the
9//! route's `namespace/name`, then rule and match order.
10
11// `Err(Response)` is the answer itself (a redirect, a 404, a 504), moved once.
12#![allow(clippy::result_large_err)]
13
14use std::sync::atomic::{AtomicUsize, Ordering};
15use std::sync::{Arc, RwLock};
16use std::time::Duration;
17
18use crate::application::Application;
19use crate::request::Request;
20use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
21
22use super::filters::{self, status, Cors};
23use crate::server::ConnectionInfo;
24
25use super::routes::{CompiledHttpRoute, CompiledRule, Mirror};
26use super::translate::ListenerConfig;
27use super::types::{HttpRouteFilter, HttpRouteMatch};
28
29/// The listeners served on one container port (one Gateway's, one port;
30/// several when they differ by hostname).
31pub type PortListeners = Arc<RwLock<Arc<Vec<ListenerConfig>>>>;
32
33#[derive(Clone)]
34pub struct PortRouter {
35    pub listeners: PortListeners,
36    /// Picks backends by weight.
37    counter: Arc<AtomicUsize>,
38    /// Picks a backend's endpoint, round robin: its own counter, so a retry
39    /// goes to the next endpoint (sharing one, two draws per attempt kept a
40    /// two-endpoint backend on the same one).
41    endpoint_counter: Arc<AtomicUsize>,
42}
43
44impl PortRouter {
45    pub fn new(listeners: PortListeners) -> PortRouter {
46        PortRouter { listeners, counter: Arc::new(AtomicUsize::new(0)), endpoint_counter: Arc::new(AtomicUsize::new(0)) }
47    }
48}
49
50/// The request's host, lower-cased, without a port.
51#[doc(hidden)]
52pub fn request_host(request: &Request) -> String {
53    let host = request.header("host").map(|h| h.value.to_string()).unwrap_or_default();
54    let host = if host.starts_with('[') {
55        host.split(']').next().map(|h| format!("{h}]")).unwrap_or(host)
56    } else {
57        host.rsplit_once(':').map(|(h, _)| h.to_string()).unwrap_or(host)
58    };
59    host.to_ascii_lowercase()
60}
61
62/// How specifically `pattern` (a hostname, maybe `*.`-wildcard) matches
63/// `host`: `None` for no match; non-wildcard matches score above wildcards,
64/// longer above shorter.
65#[doc(hidden)]
66pub fn host_score(pattern: &str, host: &str) -> Option<(usize, usize)> {
67    let pattern = pattern.to_ascii_lowercase();
68    match pattern.strip_prefix("*.") {
69        None => (pattern == host).then_some((pattern.len(), pattern.len())),
70        Some(suffix) => (host.len() > suffix.len() + 1 && host.ends_with(&format!(".{suffix}"))).then_some((0, pattern.len())),
71    }
72}
73
74/// The listener a request on this port belongs to: the most specific
75/// matching hostname; a listener without one matches any host.
76#[doc(hidden)]
77pub fn pick_listener<'a>(listeners: &'a [ListenerConfig], host: &str) -> Option<&'a ListenerConfig> {
78    listeners
79        .iter()
80        .filter_map(|l| match l.hostname.as_deref().filter(|h| !h.is_empty()) {
81            None => Some(((0, 0), l)),
82            Some(pattern) => host_score(pattern, host).map(|s| (s, l)),
83        })
84        .max_by_key(|(score, _)| *score)
85        .map(|(_, l)| l)
86}
87
88fn path_of(request: &Request) -> &str {
89    let uri = request.request_uri.as_str();
90    uri.split(['?', '#']).next().unwrap_or(uri)
91}
92
93fn query_value(request: &Request, name: &str) -> Option<String> {
94    let query = request.request_uri.split_once('?')?.1;
95    let query = query.split('#').next().unwrap_or(query);
96    query.split('&').find_map(|pair| {
97        let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
98        (crate::url::URL::percent_decode(k) == name).then(|| crate::url::URL::percent_decode(v))
99    })
100}
101
102/// The method of a gRPC path `/<service>/<method>`.
103fn grpc_method(path: &str) -> Option<&str> {
104    let (service, method) = path.strip_prefix('/')?.split_once('/')?;
105    (!service.is_empty() && !method.is_empty() && !method.contains('/')).then_some(method)
106}
107
108/// A path prefix matches whole segments: `/foo` matches `/foo` and
109/// `/foo/bar`, not `/foobar`; a trailing `/` on the prefix is ignored.
110#[doc(hidden)]
111pub fn prefix_matches(prefix: &str, path: &str) -> bool {
112    let prefix = if prefix.len() > 1 { prefix.trim_end_matches('/') } else { prefix };
113    prefix == "/" || path == prefix || (path.starts_with(prefix) && path.as_bytes().get(prefix.len()) == Some(&b'/'))
114}
115
116/// Whether a match accepts the request, and its precedence score:
117/// (exact path, path length, method, headers, query params).
118fn match_score(m: &HttpRouteMatch, request: &Request) -> Option<(u8, usize, u8, usize, usize)> {
119    let path = path_of(request);
120    let (exact, length) = match &m.path {
121        None => (0, 1),
122        Some(p) => {
123            let value = p.value.as_deref().unwrap_or("/");
124            match p.match_type.as_deref().unwrap_or("PathPrefix") {
125                "Exact" if path == value => (1, value.len()),
126                "PathPrefix" if prefix_matches(value, path) => (0, value.len()),
127                super::routes::GRPC_METHOD_MATCH if grpc_method(path) == Some(value) => (0, value.len()),
128                _ => return None,
129            }
130        }
131    };
132    if let Some(method) = &m.method {
133        if &request.method != method {
134            return None;
135        }
136    }
137    for header in &m.headers {
138        if header.match_type.as_deref().unwrap_or("Exact") != "Exact" {
139            return None;
140        }
141        if request.header(&header.name).map(|h| h.value.as_ref()) != Some(header.value.as_str()) {
142            return None;
143        }
144    }
145    for param in &m.query_params {
146        if param.match_type.as_deref().unwrap_or("Exact") != "Exact" {
147            return None;
148        }
149        if query_value(request, &param.name).as_deref() != Some(param.value.as_str()) {
150            return None;
151        }
152    }
153    Some((exact, length, m.method.is_some() as u8, m.headers.len(), m.query_params.len()))
154}
155
156/// The rule a request goes to on a listener, and the match that chose it.
157#[doc(hidden)]
158pub fn select<'a>(listener: &'a ListenerConfig, request: &Request, host: &str) -> Option<(&'a CompiledHttpRoute, &'a CompiledRule, &'a HttpRouteMatch)> {
159    type Key<'k> = ((usize, usize), (u8, usize, u8, usize, usize), std::cmp::Reverse<(&'k str, &'k (String, String), usize, usize)>);
160    let mut best: Option<(Key, &CompiledHttpRoute, &CompiledRule, &HttpRouteMatch)> = None;
161    for route in &listener.http_routes {
162        let host_match = if route.hostnames.is_empty() {
163            Some((0, 0))
164        } else {
165            route.hostnames.iter().filter_map(|h| host_score(h, host)).max()
166        };
167        let Some(host_match) = host_match else { continue };
168        for (rule_index, rule) in route.rules.iter().enumerate() {
169            for (match_index, m) in rule.matches.iter().enumerate() {
170                let Some(score) = match_score(m, request) else { continue };
171                let key = (host_match, score, std::cmp::Reverse((route.created.as_str(), &route.key, rule_index, match_index)));
172                if best.as_ref().is_none_or(|(b, ..)| key > *b) {
173                    best = Some((key, route, rule, m));
174                }
175            }
176        }
177    }
178    best.map(|(_, route, rule, m)| (route, rule, m))
179}
180
181/// True for about `share` (0.0–1.0) of calls.
182fn sample(share: f64) -> bool {
183    use std::hash::{BuildHasher, Hasher};
184    static KEYS: std::sync::OnceLock<std::collections::hash_map::RandomState> = std::sync::OnceLock::new();
185    static COUNTER: AtomicUsize = AtomicUsize::new(0);
186    if share >= 1.0 {
187        return true;
188    }
189    let mut hasher = KEYS.get_or_init(Default::default).build_hasher();
190    hasher.write_usize(COUNTER.fetch_add(1, Ordering::Relaxed));
191    ((hasher.finish() >> 11) as f64 / (1u64 << 53) as f64) < share
192}
193
194/// Send a copy of `request` to one of the mirror's endpoints on a thread
195/// of its own; the response is dropped.
196fn send_mirror(mirror: &Mirror, request: &Request, client: crate::proxy::Peer<'_>) {
197    let Ok(endpoints) = &mirror.endpoints else { return };
198    let Some(endpoint) = endpoints.get(MIRROR_COUNTER.fetch_add(1, Ordering::Relaxed) % endpoints.len().max(1)).cloned() else { return };
199    let request = request.clone();
200    let (client_ip, tls) = (client.ip.to_string(), client.tls);
201    std::thread::spawn(move || {
202        let Some((ip, port)) = endpoint.rsplit_once(':') else { return };
203        let ip = ip.trim_start_matches('[').trim_end_matches(']');
204        let Ok(port) = port.parse::<u16>() else { return };
205        let host = request.header("host").map(|h| h.value.to_string()).unwrap_or_else(|| endpoint.clone());
206        let _ = crate::proxy::forward_pooled_with_host(crate::proxy::shared_pool(), &request, crate::proxy::Peer { ip: &client_ip, tls }, ip, port, &host, Duration::from_secs(2), Duration::from_secs(10));
207    });
208}
209
210static MIRROR_COUNTER: AtomicUsize = AtomicUsize::new(0);
211
212/// How one attempt ended.
213enum Attempt {
214    /// A response to send (or to retry on its status).
215    Response(Response),
216    /// No response: the connection failed, or the attempt's timeout passed
217    /// (the forwarder's error and the endpoint, for `Proxy-Status`).
218    Failed { timed_out: bool, error: String, next_hop: String },
219}
220
221/// Read timeout of an attempt without a route timeout.
222const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
223/// Wait between retries without `retry.backoff`.
224const DEFAULT_BACKOFF: Duration = Duration::from_millis(25);
225
226/// A rule's `timeouts` and `retry`, as the forwarding loop applies them:
227/// `request` bounds the whole exchange, `backendRequest` each attempt (`0s`
228/// disables; passing either is a `504`); `attempts` retries on the listed
229/// `codes`, connection errors and backend timeouts, `backoff` apart.
230struct Policy<'a> {
231    rule: &'a CompiledRule,
232    deadline: Option<std::time::Instant>,
233    per_attempt: Option<Duration>,
234    retries: u32,
235    backoff: Duration,
236    attempt: u32,
237}
238
239impl<'a> Policy<'a> {
240    fn new(rule: &'a CompiledRule) -> Policy<'a> {
241        let limit = |value: Option<&String>| value.and_then(|v| filters::duration(v)).filter(|d| !d.is_zero());
242        let timeouts = rule.timeouts.as_ref();
243        Policy {
244            rule,
245            deadline: limit(timeouts.and_then(|t| t.request.as_ref())).map(|d| std::time::Instant::now() + d),
246            per_attempt: limit(timeouts.and_then(|t| t.backend_request.as_ref())),
247            retries: rule.retry.as_ref().map(|r| r.attempts.unwrap_or(1).max(0) as u32).unwrap_or(0),
248            backoff: rule.retry.as_ref().and_then(|r| r.backoff.as_deref()).and_then(filters::duration).unwrap_or(DEFAULT_BACKOFF),
249            attempt: 0,
250        }
251    }
252
253    /// The next attempt's timeout, or the `504` when the request's time is up.
254    fn timeout(&self) -> Result<Duration, Response> {
255        let left = self.deadline.map(|d| d.saturating_duration_since(std::time::Instant::now()));
256        if left.is_some_and(|l| l.is_zero()) {
257            return Err(status(STATUS_CODE_REASON_PHRASE.n504_gateway_timeout));
258        }
259        Ok([self.per_attempt, left].into_iter().flatten().min().unwrap_or(DEFAULT_TIMEOUT))
260    }
261
262    /// The response to send, or how long to wait before retrying.
263    fn settle(&mut self, outcome: Attempt) -> Result<Response, Duration> {
264        let retry_on = |code: i16| self.rule.retry.as_ref().is_some_and(|r| r.codes.contains(&(code as i32)));
265        let retry = self.attempt < self.retries
266            && match &outcome {
267                Attempt::Response(response) => retry_on(response.status_code),
268                Attempt::Failed { .. } => true,
269            };
270        if !retry {
271            return Ok(match outcome {
272                Attempt::Response(response) => response,
273                Attempt::Failed { timed_out: true, error, next_hop } => {
274                    let mut response = status(STATUS_CODE_REASON_PHRASE.n504_gateway_timeout);
275                    crate::proxy::status::failed(&mut response, crate::proxy::status::ProxyError::HttpResponseTimeout, Some(&next_hop), Some(&error));
276                    response
277                }
278                Attempt::Failed { timed_out: false, error, next_hop } => {
279                    let mut response = status(STATUS_CODE_REASON_PHRASE.n502_bad_gateway);
280                    crate::proxy::status::failed_with(&mut response, &error, Some(&next_hop));
281                    response
282                }
283            });
284        }
285        self.attempt += 1;
286        Err(self.deadline.map_or(self.backoff, |d| self.backoff.min(d.saturating_duration_since(std::time::Instant::now()))))
287    }
288}
289
290/// Where one attempt goes: the backend picked by weight, an endpoint of it,
291/// and the request as that backend's filters leave it.
292struct Target<'a> {
293    backend: &'a super::routes::Backend,
294    request: std::borrow::Cow<'a, Request>,
295    ip: String,
296    port: u16,
297    host_header: String,
298}
299
300/// The upstream timeouts of an attempt with `timeout` to go.
301fn connect_timeout(timeout: Duration) -> Duration {
302    timeout.min(Duration::from_secs(10))
303}
304
305impl PortRouter {
306    #[cfg(test)]
307    fn forward(&self, rule: &CompiledRule, request: &Request, connection: &ConnectionInfo) -> Response {
308        self.forward_with(rule, None, request, connection, None)
309    }
310
311    /// Forward within the rule's [`Policy`], presenting `client_cert` (the
312    /// Gateway's) to TLS backends; blocks for the whole exchange.
313    fn forward_with(&self, rule: &CompiledRule, matched: Option<&HttpRouteMatch>, request: &Request, connection: &ConnectionInfo, client_cert: Option<&(Vec<u8>, Vec<u8>)>) -> Response {
314        let mut policy = Policy::new(rule);
315        loop {
316            let timeout = match policy.timeout() {
317                Ok(timeout) => timeout,
318                Err(response) => return response,
319            };
320            match policy.settle(self.attempt(rule, matched, request, connection, timeout, client_cert)) {
321                Ok(response) => return response,
322                Err(pause) => std::thread::sleep(pause),
323            }
324        }
325    }
326
327    /// [`PortRouter::forward_with`] without blocking a thread: plain and
328    /// BackendTLSPolicy backends through the async forwarder, h2c ones on
329    /// the blocking pool.
330    async fn forward_async(&self, rule: &CompiledRule, matched: Option<&HttpRouteMatch>, request: &Request, connection: &ConnectionInfo, client_cert: Option<&(Vec<u8>, Vec<u8>)>) -> Response {
331        let mut policy = Policy::new(rule);
332        loop {
333            let timeout = match policy.timeout() {
334                Ok(timeout) => timeout,
335                Err(response) => return response,
336            };
337            match policy.settle(self.attempt_async(rule, matched, request, connection, timeout, client_cert).await) {
338                Ok(response) => return response,
339                Err(pause) => tokio::time::sleep(pause).await,
340            }
341        }
342    }
343
344    /// Pick a backend by weight and an endpoint; `Err` is the response
345    /// when there is nothing to send to.
346    fn target<'a>(&self, rule: &'a CompiledRule, matched: Option<&HttpRouteMatch>, request: &'a Request) -> Result<Target<'a>, Response> {
347        let total: u32 = rule.backends.iter().map(|b| b.weight).sum();
348        if total == 0 {
349            // No backends, or all weighted 0: the spec's 500.
350            return Err(status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error));
351        }
352        let roll = (self.counter.fetch_add(1, Ordering::Relaxed) as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> 32;
353        let mut pick = (roll % total as u64) as u32;
354        let backend = rule
355            .backends
356            .iter()
357            .find(|b| {
358                if pick < b.weight {
359                    true
360                } else {
361                    pick -= b.weight;
362                    false
363                }
364            })
365            .expect("weights sum to total");
366        let request = filters::backend_request(request, &backend.filters, matched);
367        let endpoints = match &backend.endpoints {
368            Err(_) => return Err(status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error)),
369            Ok(e) if e.is_empty() => return Err(status(STATUS_CODE_REASON_PHRASE.n503_service_unavailable)),
370            Ok(e) => e,
371        };
372        let endpoint = &endpoints[self.endpoint_counter.fetch_add(1, Ordering::Relaxed) % endpoints.len()];
373        let (ip, port) = endpoint
374            .rsplit_once(':')
375            .and_then(|(ip, port)| Some((ip.trim_start_matches('[').trim_end_matches(']').to_string(), port.parse::<u16>().ok()?)))
376            .ok_or_else(|| status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error))?;
377        let host_header = request.header("host").map(|h| h.value.to_string()).unwrap_or_else(|| endpoint.clone());
378        Ok(Target { backend, request, ip, port, host_header })
379    }
380
381    /// The BackendTLSPolicy configuration for a target, with the Gateway's
382    /// client certificate when it has one; `None` for a plain backend.
383    fn backend_tls(target: &Target, client_cert: Option<&(Vec<u8>, Vec<u8>)>) -> Result<Option<super::backend_tls::BackendTls>, Response> {
384        match &target.backend.tls {
385            None => Ok(None),
386            // A policy covers the backend but can't be used: never send it in the clear.
387            Some(Err(_)) => Err(status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error)),
388            Some(Ok(tls)) => match client_cert {
389                None => Ok(Some(tls.clone())),
390                Some((chain, key)) => tls.with_client_cert(chain, key).map(Some).map_err(|_| status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error)),
391            },
392        }
393    }
394
395    /// Forward once, blocking.
396    fn attempt(&self, rule: &CompiledRule, matched: Option<&HttpRouteMatch>, request: &Request, connection: &ConnectionInfo, timeout: Duration, client_cert: Option<&(Vec<u8>, Vec<u8>)>) -> Attempt {
397        let target = match self.target(rule, matched, request) {
398            Ok(target) => target,
399            Err(response) => return Attempt::Response(response),
400        };
401        let tls = match Self::backend_tls(&target, client_cert) {
402            Ok(tls) => tls,
403            Err(response) => return Attempt::Response(response),
404        };
405        let (request, ip, port, host_header) = (target.request.as_ref(), target.ip.as_str(), target.port, target.host_header.as_str());
406        let started = std::time::Instant::now();
407        let next_hop = format!("{}:{}", target.ip, target.port);
408        let h2 = target.backend.app_protocol.as_deref() == Some("kubernetes.io/h2c");
409        let forwarded = if let (Some(tls), true) = (&tls, h2) {
410            // gRPC and h2c backends a BackendTLSPolicy covers: HTTP/2 on TLS.
411            crate::proxy::forward_h2_tls_with_host(&tls.h2_config, request, crate::proxy::Peer::of(connection), ip, port, &tls.server_name, host_header, connect_timeout(timeout), timeout)
412        } else if let Some(tls) = &tls {
413            crate::proxy::forward_tls_with_host(&tls.config, &tls.label, request, crate::proxy::Peer::of(connection), ip, port, &tls.server_name, host_header, connect_timeout(timeout), timeout)
414        } else if super::upgrade::is_upgrade(request) {
415            super::upgrade::forward(request, crate::proxy::Peer::of(connection), ip, port, host_header, timeout)
416        } else if target.backend.app_protocol.as_deref() == Some("kubernetes.io/h2c") {
417            crate::proxy::forward_h2c_with_host(request, crate::proxy::Peer::of(connection), ip, port, host_header, connect_timeout(timeout), timeout)
418        } else {
419            crate::proxy::forward_pooled_with_host(crate::proxy::shared_pool(), request, crate::proxy::Peer::of(connection), ip, port, host_header, connect_timeout(timeout), timeout)
420        };
421        match forwarded {
422            Ok(mut response) => {
423                crate::proxy::status::received(&mut response, Some(&next_hop));
424                Attempt::Response(response)
425            }
426            Err(error) => Attempt::Failed { timed_out: started.elapsed() >= timeout, error, next_hop },
427        }
428    }
429
430    /// Forward once without blocking (upgrades never get here: they take
431    /// the blocking path, [`Application::execute`]).
432    async fn attempt_async(&self, rule: &CompiledRule, matched: Option<&HttpRouteMatch>, request: &Request, connection: &ConnectionInfo, timeout: Duration, client_cert: Option<&(Vec<u8>, Vec<u8>)>) -> Attempt {
433        let target = match self.target(rule, matched, request) {
434            Ok(target) => target,
435            Err(response) => return Attempt::Response(response),
436        };
437        let tls = match Self::backend_tls(&target, client_cert) {
438            Ok(tls) => tls,
439            Err(response) => return Attempt::Response(response),
440        };
441        let started = std::time::Instant::now();
442        let next_hop = format!("{}:{}", target.ip, target.port);
443        let forwarded = if target.backend.app_protocol.as_deref() == Some("kubernetes.io/h2c") {
444            // HTTP/2 upstreams have their own runtime and a blocking API.
445            let (request, client_ip, tls_client, ip, port, host) = (target.request.into_owned(), connection.client.ip.clone(), connection.tls, target.ip, target.port, target.host_header);
446            tokio::task::spawn_blocking(move || {
447                let peer = crate::proxy::Peer { ip: &client_ip, tls: tls_client };
448                match &tls {
449                    Some(tls) => crate::proxy::forward_h2_tls_with_host(&tls.h2_config, &request, peer, &ip, port, &tls.server_name, &host, connect_timeout(timeout), timeout),
450                    None => crate::proxy::forward_h2c_with_host(&request, peer, &ip, port, &host, connect_timeout(timeout), timeout),
451                }
452            })
453            .await
454            .unwrap_or_else(|e| Err(e.to_string()))
455        } else {
456            let tls = tls.as_ref().map(|t| (t.server_name.as_str(), crate::proxy::async_upstream::AsyncTls { config: std::sync::Arc::clone(&t.config), pool: crate::proxy::async_upstream::gateway_tls_pool(&t.label) }));
457            crate::proxy::async_upstream::forward_gateway(&target.request, crate::proxy::Peer::of(connection), &target.ip, target.port, &target.host_header, tls, connect_timeout(timeout), timeout).await
458        };
459        match forwarded {
460            Ok(mut response) => {
461                crate::proxy::status::received(&mut response, Some(&next_hop));
462                Attempt::Response(response)
463            }
464            Err(error) => Attempt::Failed { timed_out: started.elapsed() >= timeout, error, next_hop },
465        }
466    }
467}
468
469/// What the rule's filters made of a request, before it's forwarded.
470struct Prepared<'a> {
471    /// The request as the filters left it (`None`: unchanged).
472    request: Option<Request>,
473    response_filters: Vec<&'a HttpRouteFilter>,
474    cors_headers: Vec<crate::header::Header>,
475}
476
477impl PortRouter {
478    /// Run the rule's filters in order: a redirect or a CORS preflight is
479    /// the answer (`Err`); header modifiers, rewrites and mirrors act on the
480    /// request now; response modifiers and CORS headers wait for
481    /// [`PortRouter::finish`].
482    fn prepare<'a>(&self, listener: &ListenerConfig, rule: &'a CompiledRule, matched: &HttpRouteMatch, request: &Request, host: &str, connection: &ConnectionInfo) -> Result<Prepared<'a>, Response> {
483        let mut prepared = Prepared { request: None, response_filters: Vec::new(), cors_headers: Vec::new() };
484        if rule.filters.is_empty() {
485            return Ok(prepared);
486        }
487        let mut request = request.clone();
488        let mut mirrored = 0;
489        for filter in &rule.filters {
490            match filter.filter_type.as_str() {
491                "RequestHeaderModifier" => {
492                    if let Some(f) = &filter.request_header_modifier {
493                        filters::modify_headers(&mut request.headers, f);
494                    }
495                }
496                "ResponseHeaderModifier" => prepared.response_filters.push(filter),
497                "RequestRedirect" => {
498                    if let Some(f) = &filter.request_redirect {
499                        return Err(filters::redirect(&request, host, listener.protocol == "HTTPS", listener.port as u16, matched, f));
500                    }
501                }
502                "URLRewrite" => {
503                    if let Some(f) = &filter.url_rewrite {
504                        if let Some(hostname) = &f.hostname {
505                            request.headers.retain(|h| !h.name.eq_ignore_ascii_case("host"));
506                            request.headers.push(crate::header::Header::new("Host", hostname.clone()));
507                        }
508                        if let Some(modifier) = &f.path {
509                            let (path, query) = filters::split_target(&request.request_uri);
510                            request.request_uri = format!("{}{query}", filters::modified_path(path, matched, modifier));
511                        }
512                    }
513                }
514                "CORS" => {
515                    if let Some(f) = &filter.cors {
516                        match filters::cors(&request, f) {
517                            Cors::Pass => {}
518                            Cors::Preflight(headers) => {
519                                let mut response = status(STATUS_CODE_REASON_PHRASE.n204_no_content);
520                                response.headers.extend(headers);
521                                return Err(response);
522                            }
523                            Cors::Actual(headers) => prepared.cors_headers = headers,
524                        }
525                    }
526                }
527                "RequestMirror" => mirrored += filter.request_mirror.is_some() as usize,
528                // ExtensionRef and anything unknown: the spec's 500.
529                _ => return Err(status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error)),
530            }
531        }
532        // Mirrors get the request as the filters left it.
533        for mirror in rule.mirrors.iter().take(mirrored) {
534            if sample(mirror.share) {
535                send_mirror(mirror, &request, crate::proxy::Peer::of(connection));
536            }
537        }
538        prepared.request = Some(request);
539        Ok(prepared)
540    }
541
542    /// The response modifiers and CORS headers, on the backend's response.
543    fn finish(prepared: Prepared, mut response: Response) -> Response {
544        for filter in prepared.response_filters {
545            if let Some(f) = &filter.response_header_modifier {
546                filters::modify_headers(&mut response.headers, f);
547            }
548        }
549        for header in prepared.cors_headers {
550            response.headers.retain(|h| !h.name.eq_ignore_ascii_case(&header.name));
551            response.headers.push(header);
552        }
553        response
554    }
555
556    /// Filters, forwarding (blocking) and the response filters.
557    fn route(&self, listener: &ListenerConfig, rule: &CompiledRule, matched: &HttpRouteMatch, request: &Request, host: &str, connection: &ConnectionInfo) -> Response {
558        match self.prepare(listener, rule, matched, request, host, connection) {
559            Err(response) => response,
560            Ok(prepared) => {
561                let response = self.forward_with(rule, Some(matched), prepared.request.as_ref().unwrap_or(request), connection, listener.backend_client_cert.as_ref());
562                Self::finish(prepared, response)
563            }
564        }
565    }
566}
567
568/// Over TLS, a request whose `Host` belongs to another listener than the
569/// one its SNI chose (and whose certificate it got) is misdirected: `421`.
570fn misdirected(listeners: &[ListenerConfig], by_host: &ListenerConfig, connection: &ConnectionInfo) -> bool {
571    let Some(sni) = connection.sni_hostname.as_deref() else { return false };
572    pick_listener(listeners, &sni.to_ascii_lowercase()).is_some_and(|by_sni| !std::ptr::eq(by_sni, by_host))
573}
574
575impl PortRouter {
576    /// The listener a request belongs to, or the answer when none does
577    /// (`404`, `421`).
578    fn listener_of<'l>(listeners: &'l [ListenerConfig], host: &str, connection: &ConnectionInfo) -> Result<&'l ListenerConfig, Response> {
579        let Some(listener) = pick_listener(listeners, host) else {
580            return Err(status(STATUS_CODE_REASON_PHRASE.n404_not_found));
581        };
582        if misdirected(listeners, listener, connection) {
583            return Err(status(STATUS_CODE_REASON_PHRASE.n421_misdirected_request));
584        }
585        Ok(listener)
586    }
587
588    /// The rule a request goes to on its listener, or `404`.
589    fn rule_of<'l>(listener: &'l ListenerConfig, request: &Request, host: &str) -> Result<(&'l CompiledRule, &'l HttpRouteMatch), Response> {
590        match select(listener, request, host) {
591            Some((_, rule, matched)) => Ok((rule, matched)),
592            None => Err(status(STATUS_CODE_REASON_PHRASE.n404_not_found)),
593        }
594    }
595}
596
597impl Application for PortRouter {
598    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
599        let listeners = self.listeners.read().unwrap_or_else(|e| e.into_inner()).clone();
600        let host = request_host(request);
601        let listener = match Self::listener_of(&listeners, &host, connection) {
602            Ok(listener) => listener,
603            Err(response) => return Ok(response),
604        };
605        // Pre-routing filters (GEP-5224), then route selection on what they left.
606        let filtered = match super::ext_auth::run(&listener.request_filters, request, crate::proxy::Peer::of(connection), &self.endpoint_counter) {
607            Ok(filtered) => filtered,
608            Err(response) => return Ok(response),
609        };
610        let request = filtered.as_ref().unwrap_or(request);
611        Ok(match Self::rule_of(listener, request, &host) {
612            Ok((rule, matched)) => self.route(listener, rule, matched, request, &host, connection),
613            Err(response) => response,
614        })
615    }
616
617    /// Everything but upgrades runs here, on the connection's task: the
618    /// forwarding loop waits on tokio sockets instead of holding a
619    /// blocking-pool thread per request in flight.
620    #[cfg(feature = "http2")]
621    fn execute_async<'a>(&'a self, request: &'a Request, connection: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
622        if super::upgrade::is_upgrade(request) {
623            return None;
624        }
625        Some(Box::pin(async move {
626            let listeners = self.listeners.read().unwrap_or_else(|e| e.into_inner()).clone();
627            let host = request_host(request);
628            let listener = match Self::listener_of(&listeners, &host, connection) {
629                Ok(listener) => listener,
630                Err(response) => return Ok(response),
631            };
632            // Authorization calls block: they run on the blocking pool.
633            let filtered = if listener.request_filters.is_empty() {
634                None
635            } else {
636                let (filters, copy, client_ip, tls, counter) = (listener.request_filters.clone(), request.clone(), connection.client.ip.clone(), connection.tls, Arc::clone(&self.endpoint_counter));
637                match tokio::task::spawn_blocking(move || super::ext_auth::run(&filters, &copy, crate::proxy::Peer { ip: &client_ip, tls }, &counter)).await {
638                    Ok(Ok(filtered)) => filtered,
639                    Ok(Err(response)) => return Ok(response),
640                    Err(_) => return Ok(status(STATUS_CODE_REASON_PHRASE.n500_internal_server_error)),
641                }
642            };
643            let request = filtered.as_ref().unwrap_or(request);
644            let (rule, matched) = match Self::rule_of(listener, request, &host) {
645                Ok(found) => found,
646                Err(response) => return Ok(response),
647            };
648            let prepared = match self.prepare(listener, rule, matched, request, &host, connection) {
649                Ok(prepared) => prepared,
650                Err(response) => return Ok(response),
651            };
652            let response = self.forward_async(rule, Some(matched), prepared.request.as_ref().unwrap_or(request), connection, listener.backend_client_cert.as_ref()).await;
653            Ok(Self::finish(prepared, response))
654        }))
655    }
656}
657
658#[cfg(test)]
659mod tests;