From 0722e7820849582d16c5f55006d6f3b2fe159bfb Mon Sep 17 00:00:00 2001 From: Eloi DEMOLIS Date: Thu, 19 Dec 2024 14:41:03 +0100 Subject: [PATCH] Refactor Route, RouteResult and redirection flow Signed-off-by: Eloi DEMOLIS --- command/assets/custom_200.html | 7 + command/assets/custom_404.html | 4 +- command/assets/custom_503.html | 6 +- lib/src/http.rs | 10 +- lib/src/https.rs | 10 +- lib/src/protocol/kawa_h1/editor.rs | 6 +- lib/src/protocol/kawa_h1/mod.rs | 154 ++++++++-------- lib/src/router/mod.rs | 272 ++++++++++++++++------------- 8 files changed, 249 insertions(+), 220 deletions(-) create mode 100644 command/assets/custom_200.html diff --git a/command/assets/custom_200.html b/command/assets/custom_200.html new file mode 100644 index 000000000..73e5926d9 --- /dev/null +++ b/command/assets/custom_200.html @@ -0,0 +1,7 @@ +HTTP/1.1 200 OK +%Content-Length: %CONTENT_LENGTH +Sozu-Id: %REQUEST_ID + +

%CLUSTER_ID Custom 200

+

original url: %ROUTE

+

rewritten url: %REDIRECT_LOCATION

\ No newline at end of file diff --git a/command/assets/custom_404.html b/command/assets/custom_404.html index 34f6c80d0..8302d4319 100644 --- a/command/assets/custom_404.html +++ b/command/assets/custom_404.html @@ -1,7 +1,7 @@ HTTP/1.1 404 Not Found Cache-Control: no-cache Connection: close -Sozu-Id: %SOZU_ID +Sozu-Id: %REQUEST_ID

My own 404 error page

-

Your request %SOZU_ID found no frontend and cannot be redirected.

\ No newline at end of file +

Your request %REQUEST_ID found no frontend and cannot be redirected.

\ No newline at end of file diff --git a/command/assets/custom_503.html b/command/assets/custom_503.html index 8f174262b..ac12b8825 100644 --- a/command/assets/custom_503.html +++ b/command/assets/custom_503.html @@ -2,10 +2,10 @@ Cache-Control: no-cache Connection: close %Content-Length: %CONTENT_LENGTH -Sozu-Id: %SOZU_ID +Sozu-Id: %REQUEST_ID

MyCluster: 503 Service Unavailable

-

No server seems to be alive, could not redirect request %SOZU_ID.

+

No server seems to be alive, could not redirect request %REQUEST_ID.

-%DETAILS
+%MESSAGE
 
\ No newline at end of file
diff --git a/lib/src/http.rs b/lib/src/http.rs
index 37c388605..ce7104d55 100644
--- a/lib/src/http.rs
+++ b/lib/src/http.rs
@@ -2,11 +2,9 @@ use std::{
     cell::RefCell,
     collections::{hash_map::Entry, BTreeMap, HashMap},
     io::ErrorKind,
-    mem,
     net::{Shutdown, SocketAddr},
     os::unix::io::AsRawFd,
     rc::{Rc, Weak},
-    str::from_utf8_unchecked,
     time::{Duration, Instant},
 };
 
@@ -32,15 +30,11 @@ use crate::{
     backends::BackendMap,
     pool::Pool,
     protocol::{
-        http::{
-            answers::HttpAnswers,
-            parser::{hostname_and_port, Method},
-            ResponseStream,
-        },
+        http::{answers::HttpAnswers, parser::Method, ResponseStream},
         proxy_protocol::expect::ExpectProxyProtocol,
         Http, Pipe, SessionState,
     },
-    router::{RouteDirection, RouteResult, Router},
+    router::{RouteResult, Router},
     server::{ListenToken, SessionManager},
     socket::server_bind,
     timer::TimeoutContainer,
diff --git a/lib/src/https.rs b/lib/src/https.rs
index b1501c15b..c9781c3c6 100644
--- a/lib/src/https.rs
+++ b/lib/src/https.rs
@@ -5,7 +5,7 @@ use std::{
     net::{Shutdown, SocketAddr as StdSocketAddr},
     os::unix::io::AsRawFd,
     rc::{Rc, Weak},
-    str::{from_utf8, from_utf8_unchecked},
+    str::from_utf8,
     sync::Arc,
     time::{Duration, Instant},
 };
@@ -53,16 +53,12 @@ use crate::{
     pool::Pool,
     protocol::{
         h2::Http2,
-        http::{
-            answers::HttpAnswers,
-            parser::{hostname_and_port, Method},
-            ResponseStream,
-        },
+        http::{answers::HttpAnswers, parser::Method, ResponseStream},
         proxy_protocol::expect::ExpectProxyProtocol,
         rustls::TlsHandshake,
         Http, Pipe, SessionState,
     },
-    router::{RouteDirection, RouteResult, Router},
+    router::{RouteResult, Router},
     server::{ListenToken, SessionManager},
     socket::{server_bind, FrontRustls},
     timer::TimeoutContainer,
diff --git a/lib/src/protocol/kawa_h1/editor.rs b/lib/src/protocol/kawa_h1/editor.rs
index e7e870c5f..7c72ce598 100644
--- a/lib/src/protocol/kawa_h1/editor.rs
+++ b/lib/src/protocol/kawa_h1/editor.rs
@@ -23,6 +23,8 @@ pub struct HttpContext {
     pub keep_alive_frontend: bool,
     /// the value of the sticky session cookie in the request
     pub sticky_session_found: Option,
+    /// position of the last authentication header, only valid until prepare is called
+    pub authentication_found: Option,
     // ---------- Status Line
     /// the value of the method in the request line
     pub method: Option,
@@ -135,7 +137,7 @@ impl HttpContext {
         let mut has_x_port = false;
         let mut has_x_proto = false;
         let mut has_connection = false;
-        for block in &mut request.blocks {
+        for (i, block) in request.blocks.iter_mut().enumerate() {
             match block {
                 kawa::Block::Header(header) if !header.is_elided() => {
                     let key = header.key.data(buf);
@@ -182,6 +184,8 @@ impl HttpContext {
                             .data_opt(buf)
                             .and_then(|data| from_utf8(data).ok())
                             .map(ToOwned::to_owned);
+                    } else if compare_no_case(key, b"Proxy-Authenticate") {
+                        self.authentication_found = Some(i);
                     }
                 }
                 _ => {}
diff --git a/lib/src/protocol/kawa_h1/mod.rs b/lib/src/protocol/kawa_h1/mod.rs
index ba0a0fcf6..b95baeb1b 100644
--- a/lib/src/protocol/kawa_h1/mod.rs
+++ b/lib/src/protocol/kawa_h1/mod.rs
@@ -18,7 +18,7 @@ use rusty_ulid::Ulid;
 use sozu_command::{
     config::MAX_LOOP_ITERATIONS,
     logging::EndpointRecord,
-    proto::command::{Event, EventKind, ListenerType, RedirectScheme},
+    proto::command::{Event, EventKind, ListenerType, RedirectPolicy, RedirectScheme},
 };
 // use time::{Duration, Instant};
 
@@ -36,7 +36,7 @@ use crate::{
         SessionState,
     },
     retry::RetryPolicy,
-    router::{RouteDirection, RouteResult},
+    router::RouteResult,
     server::{push_event, CONN_RETRIES},
     socket::{stats::socket_rtt, SocketHandler, SocketResult, TransportProtocol},
     sozu_command::{logging::LogContext, ready::Ready},
@@ -245,6 +245,7 @@ impl Http Http route,
             Err(frontend_error) => {
                 self.set_answer(DefaultAnswer::Answer404 {});
@@ -1303,83 +1312,76 @@ impl Http {
+        if let Some(cluster_id) = &cluster_id {
+            time!(
+                "frontend_matching_time",
+                cluster_id,
+                start.elapsed().as_millis()
+            );
+        }
+
+        let host = rewritten_host.as_deref().unwrap_or(host);
+        let path = rewritten_path.as_deref().unwrap_or(path);
+        let port = rewritten_port.map_or_else(
+            || {
+                port.map_or(String::new(), |port| {
+                    format!(":{}", unsafe { from_utf8_unchecked(port) })
+                })
+            },
+            |port| format!(":{port}"),
+        );
+        let is_https = matches!(proxy.borrow().kind(), ListenerType::Https);
+        let proto = match (redirect_scheme, is_https) {
+            (RedirectScheme::UseHttp, _) | (RedirectScheme::UseSame, false) => "http",
+            (RedirectScheme::UseHttps, _) | (RedirectScheme::UseSame, true) => "https",
+        };
+
+        match (cluster_id, redirect, redirect_template) {
+            (_, RedirectPolicy::Unauthorized, _) | (None, RedirectPolicy::Forward, None) => {
                 self.set_answer(DefaultAnswer::Answer401 {});
-                Err(RetrieveClusterError::UnauthorizedRoute)
+                return Err(RetrieveClusterError::UnauthorizedRoute);
             }
-            RouteResult::Flow {
-                direction: flow,
-                rewritten_host,
-                rewritten_path,
-                rewritten_port,
-            } => {
-                let is_https = matches!(proxy.borrow().kind(), ListenerType::Https);
-                if let RouteDirection::Forward(cluster_id) = &flow {
-                    time!(
-                        "frontend_matching_time",
-                        cluster_id,
-                        start.elapsed().as_millis()
-                    );
-                    let (https_redirect, https_redirect_port, authentication) = proxy
-                        .borrow()
-                        .clusters()
-                        .get(cluster_id)
-                        .map(|cluster| {
-                            (
-                                cluster.https_redirect,
-                                cluster.https_redirect_port,
-                                None::<()>,
-                            )
-                        })
-                        .unwrap_or((false, None, None));
-                    if !is_https && https_redirect {
-                        let port = https_redirect_port
-                            .map_or(String::new(), |port| format!(":{}", port as u16));
-                        self.set_answer(DefaultAnswer::Answer301 {
-                            location: format!("https://{host}{port}{path}"),
-                        });
-                        return Err(RetrieveClusterError::Redirected);
-                    }
-                    if let Some(authentication) = authentication {
-                        return Err(RetrieveClusterError::UnauthorizedRoute);
-                    }
+            (_, RedirectPolicy::Permanent, _) => {
+                self.set_answer(DefaultAnswer::Answer301 {
+                    location: format!("{proto}://{host}{port}{path}"),
+                });
+                Err(RetrieveClusterError::Redirected)
+            }
+            (_, RedirectPolicy::Temporary, _) => todo!(),
+            (cluster_id, RedirectPolicy::Forward, Some(name)) => {
+                let location = format!("{proto}://{host}{port}{path}");
+                // TODO: this feels wrong
+                self.context.cluster_id = cluster_id;
+                self.set_answer(DefaultAnswer::AnswerCustom { name, location });
+                Err(RetrieveClusterError::Redirected)
+            }
+            (Some(cluster_id), RedirectPolicy::Forward, None) => {
+                let (https_redirect, https_redirect_port, authentication) = proxy
+                    .borrow()
+                    .clusters()
+                    .get(&cluster_id)
+                    .map(|cluster| {
+                        (
+                            cluster.https_redirect,
+                            cluster.https_redirect_port,
+                            None::<()>,
+                        )
+                    })
+                    .unwrap_or((false, None, None));
+                if !is_https && https_redirect {
+                    let port = rewritten_port
+                        .or_else(|| https_redirect_port.map(|port| port as u16))
+                        .map_or(String::new(), |port| format!(":{port}"));
+                    self.set_answer(DefaultAnswer::Answer301 {
+                        location: format!("https://{host}{port}{path}"),
+                    });
+                    return Err(RetrieveClusterError::Redirected);
                 }
-                let host = rewritten_host.as_deref().unwrap_or(host);
-                let path = rewritten_path.as_deref().unwrap_or(path);
-                let port = rewritten_port.map_or_else(
-                    || {
-                        port.map_or(String::new(), |port| {
-                            format!(":{}", unsafe { from_utf8_unchecked(port) })
-                        })
-                    },
-                    |port| format!(":{port}"),
-                );
-                match flow {
-                    RouteDirection::Forward(cluster_id) => Ok(cluster_id),
-                    RouteDirection::Permanent(redirect_scheme) => {
-                        let proto = match (redirect_scheme, is_https) {
-                            (RedirectScheme::UseHttp, _) | (RedirectScheme::UseSame, false) => {
-                                "http"
-                            }
-                            (RedirectScheme::UseHttps, _) | (RedirectScheme::UseSame, true) => {
-                                "https"
-                            }
-                        };
-                        self.set_answer(DefaultAnswer::Answer301 {
-                            location: format!("{proto}://{host}{port}{path}"),
-                        });
-                        Err(RetrieveClusterError::Redirected)
-                    }
-                    RouteDirection::Temporary(_) => todo!(),
-                    RouteDirection::Template(cluster_id, name) => {
-                        let location = format!("{host}{port}{path}");
-                        // TODO: this feels wrong
-                        self.context.cluster_id = cluster_id;
-                        self.set_answer(DefaultAnswer::AnswerCustom { name, location });
-                        Err(RetrieveClusterError::Redirected)
-                    }
+                if let Some(authentication) = authentication {
+                    self.set_answer(DefaultAnswer::Answer401 {});
+                    return Err(RetrieveClusterError::UnauthorizedRoute);
                 }
+                return Ok(cluster_id);
             }
         }
     }
diff --git a/lib/src/router/mod.rs b/lib/src/router/mod.rs
index bcb2a7f19..6d798f3b7 100644
--- a/lib/src/router/mod.rs
+++ b/lib/src/router/mod.rs
@@ -719,26 +719,19 @@ impl RewriteParts {
     }
 }
 
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RouteDirection {
-    Forward(ClusterId),
-    Temporary(RedirectScheme),
-    Permanent(RedirectScheme),
-    Template(Option, String),
-}
-
 /// What to do with the traffic
+/// TODO: tags should be moved here
 #[derive(Debug, Clone)]
-pub enum Route {
-    Deny,
-    Flow {
-        direction: RouteDirection,
-        capture_cap_host: usize,
-        capture_cap_path: usize,
-        rewrite_host: Option,
-        rewrite_path: Option,
-        rewrite_port: Option,
-    },
+pub struct Route {
+    cluster_id: Option,
+    redirect: RedirectPolicy,
+    redirect_scheme: RedirectScheme,
+    redirect_template: Option,
+    capture_cap_host: usize,
+    capture_cap_path: usize,
+    rewrite_host: Option,
+    rewrite_path: Option,
+    rewrite_port: Option,
 }
 
 impl Route {
@@ -753,14 +746,21 @@ impl Route {
         rewrite_path: Option,
         rewrite_port: Option,
     ) -> Result {
-        let flow = match (cluster_id, redirect, redirect_template) {
-            (cluster_id, RedirectPolicy::Forward, Some(template)) => {
-                RouteDirection::Template(cluster_id, template)
+        match (&cluster_id, redirect, &redirect_template) {
+            (None, RedirectPolicy::Forward, None) | (_, RedirectPolicy::Unauthorized, _) => {
+                return Ok(Self {
+                    cluster_id,
+                    redirect: RedirectPolicy::Unauthorized,
+                    redirect_scheme,
+                    redirect_template: None,
+                    capture_cap_host: 0,
+                    capture_cap_path: 0,
+                    rewrite_host: None,
+                    rewrite_path: None,
+                    rewrite_port: None,
+                })
             }
-            (Some(cluster_id), RedirectPolicy::Forward, _) => RouteDirection::Forward(cluster_id),
-            (_, RedirectPolicy::Temporary, _) => RouteDirection::Temporary(redirect_scheme),
-            (_, RedirectPolicy::Permanent, _) => RouteDirection::Permanent(redirect_scheme),
-            _ => return Ok(Route::Deny),
+            _ => {}
         };
         let mut capture_cap_host = match domain_rule {
             DomainRule::Any => 1,
@@ -809,8 +809,11 @@ impl Route {
         if used_capture_path == 0 {
             capture_cap_path = 0;
         }
-        Ok(Route::Flow {
-            direction: flow,
+        Ok(Route {
+            cluster_id,
+            redirect,
+            redirect_scheme,
+            redirect_template,
             capture_cap_host,
             capture_cap_path,
             rewrite_host,
@@ -821,8 +824,11 @@ impl Route {
 
     #[cfg(test)]
     pub fn simple(cluster_id: ClusterId) -> Self {
-        Self::Flow {
-            direction: RouteDirection::Forward(cluster_id),
+        Self {
+            cluster_id: Some(cluster_id),
+            redirect: RedirectPolicy::Forward,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
             capture_cap_host: 0,
             capture_cap_path: 0,
             rewrite_host: None,
@@ -833,63 +839,77 @@ impl Route {
 }
 
 #[derive(Debug, Clone, PartialEq, Eq)]
-pub enum RouteResult {
-    Deny,
-    Flow {
-        direction: RouteDirection,
-        rewritten_host: Option,
-        rewritten_path: Option,
-        rewritten_port: Option,
-    },
+pub struct RouteResult {
+    pub cluster_id: Option,
+    pub redirect: RedirectPolicy,
+    pub redirect_scheme: RedirectScheme,
+    pub redirect_template: Option,
+    pub rewritten_host: Option,
+    pub rewritten_path: Option,
+    pub rewritten_port: Option,
 }
 
 impl RouteResult {
+    fn deny(cluster_id: &Option) -> Self {
+        Self {
+            cluster_id: cluster_id.clone(),
+            redirect: RedirectPolicy::Unauthorized,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
+            rewritten_host: None,
+            rewritten_path: None,
+            rewritten_port: None,
+        }
+    }
     fn new<'a>(
         captures_host: Vec<&'a str>,
         path: &'a [u8],
         path_rule: &PathRule,
         route: &Route,
     ) -> Self {
-        match route {
-            Route::Deny => Self::Deny,
-            Route::Flow {
-                direction: flow,
-                capture_cap_path,
-                rewrite_host,
-                rewrite_path,
-                rewrite_port,
-                ..
-            } => {
-                let mut captures_path = Vec::with_capacity(*capture_cap_path);
-                if *capture_cap_path > 0 {
-                    captures_path.push(unsafe { from_utf8_unchecked(path) });
-                    match path_rule {
-                        PathRule::Prefix(prefix) => captures_path
-                            .push(unsafe { from_utf8_unchecked(&path[prefix.len()..]) }),
-                        PathRule::Regex(regex) => captures_path.extend(
-                            regex
-                                .captures(&path)
-                                .unwrap()
-                                .iter()
-                                .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
-                        ),
-                        _ => {}
-                    }
-                }
-                println!("========HOST_CAPTURES: {captures_host:?}");
-                println!("========PATH_CAPTURES: {captures_path:?}");
-                Self::Flow {
-                    direction: flow.clone(),
-                    rewritten_host: rewrite_host
-                        .as_ref()
-                        .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
-                    rewritten_path: rewrite_path
-                        .as_ref()
-                        .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
-                    rewritten_port: *rewrite_port,
+        let Route {
+            cluster_id,
+            redirect,
+            redirect_scheme,
+            redirect_template,
+            capture_cap_path,
+            rewrite_host,
+            rewrite_path,
+            rewrite_port,
+            ..
+        } = route;
+        let mut captures_path = Vec::with_capacity(*capture_cap_path);
+        if *capture_cap_path > 0 {
+            captures_path.push(unsafe { from_utf8_unchecked(path) });
+            match path_rule {
+                PathRule::Prefix(prefix) => {
+                    captures_path.push(unsafe { from_utf8_unchecked(&path[prefix.len()..]) })
                 }
+                PathRule::Regex(regex) => captures_path.extend(
+                    regex
+                        .captures(&path)
+                        .unwrap()
+                        .iter()
+                        .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
+                ),
+                _ => {}
             }
         }
+        println!("========HOST_CAPTURES: {captures_host:?}");
+        println!("========PATH_CAPTURES: {captures_path:?}");
+        Self {
+            cluster_id: cluster_id.clone(),
+            redirect: *redirect,
+            redirect_scheme: *redirect_scheme,
+            redirect_template: redirect_template.clone(),
+            rewritten_host: rewrite_host
+                .as_ref()
+                .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
+            rewritten_path: rewrite_path
+                .as_ref()
+                .map(|rewrite| rewrite.run(&captures_host, &captures_path)),
+            rewritten_port: *rewrite_port,
+        }
     }
     fn new_no_trie<'a>(
         domain: &'a [u8],
@@ -898,32 +918,33 @@ impl RouteResult {
         path_rule: &PathRule,
         route: &Route,
     ) -> Self {
-        match route {
-            Route::Deny => Self::Deny,
-            Route::Flow {
-                capture_cap_host, ..
-            } => {
-                let mut captures_host = Vec::with_capacity(*capture_cap_host);
-                if *capture_cap_host > 0 {
-                    captures_host.push(unsafe { from_utf8_unchecked(domain) });
-                    match domain_rule {
-                        DomainRule::Wildcard(suffix) => captures_host.push(unsafe {
-                            from_utf8_unchecked(&domain[..domain.len() - suffix.len()])
-                        }),
-                        DomainRule::Regex(regex) => captures_host.extend(
-                            regex
-                                .captures(&domain)
-                                .unwrap()
-                                .iter()
-                                .skip(1)
-                                .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
-                        ),
-                        _ => {}
-                    }
-                }
-                Self::new(captures_host, path, path_rule, route)
+        let Route {
+            cluster_id,
+            redirect,
+            capture_cap_host,
+            ..
+        } = route;
+        if *redirect == RedirectPolicy::Unauthorized {
+            return Self::deny(cluster_id);
+        }
+        let mut captures_host = Vec::with_capacity(*capture_cap_host);
+        if *capture_cap_host > 0 {
+            captures_host.push(unsafe { from_utf8_unchecked(domain) });
+            match domain_rule {
+                DomainRule::Wildcard(suffix) => captures_host
+                    .push(unsafe { from_utf8_unchecked(&domain[..domain.len() - suffix.len()]) }),
+                DomainRule::Regex(regex) => captures_host.extend(
+                    regex
+                        .captures(&domain)
+                        .unwrap()
+                        .iter()
+                        .skip(1)
+                        .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
+                ),
+                _ => {}
             }
         }
+        Self::new(captures_host, path, path_rule, route)
     }
     fn new_with_trie<'a>(
         domain: &'a [u8],
@@ -932,39 +953,44 @@ impl RouteResult {
         path_rule: &PathRule,
         route: &Route,
     ) -> Self {
-        match route {
-            Route::Deny => Self::Deny,
-            Route::Flow {
-                capture_cap_host, ..
-            } => {
-                let mut captures_host = Vec::with_capacity(*capture_cap_host);
-                if *capture_cap_host > 0 {
-                    captures_host.push(unsafe { from_utf8_unchecked(domain) });
-                    for submatch in domain_submatches {
-                        match submatch {
-                            TrieSubMatch::Wildcard(part) => {
-                                captures_host.push(unsafe { from_utf8_unchecked(part) })
-                            }
-                            TrieSubMatch::Regexp(part, regex) => captures_host.extend(
-                                regex
-                                    .captures(&part)
-                                    .unwrap()
-                                    .iter()
-                                    .skip(1)
-                                    .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
-                            ),
-                        }
+        let Route {
+            cluster_id,
+            redirect,
+            capture_cap_host,
+            ..
+        } = route;
+        if *redirect == RedirectPolicy::Unauthorized {
+            return Self::deny(cluster_id);
+        }
+        let mut captures_host = Vec::with_capacity(*capture_cap_host);
+        if *capture_cap_host > 0 {
+            captures_host.push(unsafe { from_utf8_unchecked(domain) });
+            for submatch in domain_submatches {
+                match submatch {
+                    TrieSubMatch::Wildcard(part) => {
+                        captures_host.push(unsafe { from_utf8_unchecked(part) })
                     }
+                    TrieSubMatch::Regexp(part, regex) => captures_host.extend(
+                        regex
+                            .captures(&part)
+                            .unwrap()
+                            .iter()
+                            .skip(1)
+                            .map(|c| unsafe { from_utf8_unchecked(c.unwrap().as_bytes()) }),
+                    ),
                 }
-                Self::new(captures_host, path, path_rule, route)
             }
         }
+        Self::new(captures_host, path, path_rule, route)
     }
 
     #[cfg(test)]
     pub fn simple(cluster_id: ClusterId) -> Self {
-        Self::Flow {
-            direction: RouteDirection::Forward(cluster_id),
+        Self {
+            cluster_id: Some(cluster_id),
+            redirect: RedirectPolicy::Forward,
+            redirect_scheme: RedirectScheme::UseSame,
+            redirect_template: None,
             rewritten_host: None,
             rewritten_path: None,
             rewritten_port: None,