Skip to content

Commit f90c223

Browse files
authored
Add unit tests for hostname parsing (#715)
Add unit tests for subdomain parsing
1 parent af9a374 commit f90c223

File tree

3 files changed

+114
-32
lines changed

3 files changed

+114
-32
lines changed

plane/src/proxy/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ mod proxy_service;
1919
mod rewriter;
2020
mod route_map;
2121
mod shutdown_signal;
22+
mod subdomain;
2223
mod tls;
2324

2425
#[derive(Debug, Clone, Copy)]

plane/src/proxy/rewriter.rs

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::ForwardableRequestInfo;
1+
use super::{subdomain::subdomain_from_host, ForwardableRequestInfo};
22
use crate::{
33
protocol::RouteInfo,
44
types::{BearerToken, ClusterName},
@@ -21,7 +21,7 @@ const BACKEND_ID_HEADER: &str = "x-verified-backend";
2121
const X_FORWARDED_FOR_HEADER: &str = "x-forwarded-for";
2222
const X_FORWARDED_PROTO_HEADER: &str = "x-forwarded-proto";
2323

24-
#[derive(Debug, thiserror::Error)]
24+
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
2525
pub enum RequestRewriterError {
2626
#[error("Invalid `host` header")]
2727
InvalidHostHeader,
@@ -105,36 +105,7 @@ impl RequestRewriter {
105105
}
106106
};
107107

108-
// Remove port from hostname if present and port is 443.
109-
// We are already a bit magic with regards to HTTP/HTTPS: we assume that if a
110-
// cluster name has a colon, it's HTTP and runs on that port, and if a cluster
111-
// name does not have a colon, it is HTTPS and runs on 443. In other words,
112-
// in production, there's no way for URLs generated and sent to clients to have
113-
// a port other than the standard HTTPS port 443.
114-
let hostname = if let Some(hostname) = hostname.strip_suffix(":443") {
115-
hostname
116-
} else {
117-
hostname
118-
};
119-
120-
let Some(subdomain) = hostname.strip_suffix(cluster.as_str()) else {
121-
tracing::warn!(hostname, "Host header does not end in cluster name.");
122-
return Err(RequestRewriterError::InvalidHostHeader);
123-
};
124-
125-
if subdomain.is_empty() {
126-
return Ok(None);
127-
}
128-
129-
let subdomain = match subdomain.strip_suffix('.') {
130-
Some(subdomain) => subdomain,
131-
None => {
132-
tracing::warn!(hostname, "Host header does not start with a dot.");
133-
return Err(RequestRewriterError::InvalidHostHeader);
134-
}
135-
};
136-
137-
Ok(Some(subdomain))
108+
subdomain_from_host(hostname, cluster)
138109
}
139110

140111
fn into_parts(self) -> (request::Parts, Body, Uri, ForwardableRequestInfo) {

plane/src/proxy/subdomain.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use super::rewriter::RequestRewriterError;
2+
use crate::types::ClusterName;
3+
4+
// If a cluster name does not specify a port, :443 is implied.
5+
// Most browsers will not specify it, but some (e.g. the `ws` websocket client in Node.js)
6+
// will, so we strip it.
7+
const HTTPS_PORT_SUFFIX: &str = ":443";
8+
9+
/// Returns Ok(Some(subdomain)) if a subdomain is found.
10+
/// Returns Ok(None) if no subdomain is found, but the host header matches the cluster name.
11+
/// Returns Err(RequestRewriterError::InvalidHostHeader) if the host header does not
12+
/// match the cluster name.
13+
pub fn subdomain_from_host<'a>(
14+
host: &'a str,
15+
cluster: &ClusterName,
16+
) -> Result<Option<&'a str>, RequestRewriterError> {
17+
let host = if let Some(host) = host.strip_suffix(HTTPS_PORT_SUFFIX) {
18+
host
19+
} else {
20+
host
21+
};
22+
23+
if let Some(subdomain) = host.strip_suffix(cluster.as_str()) {
24+
if subdomain.is_empty() {
25+
// Subdomain exactly matches cluster name.
26+
Ok(None)
27+
} else if let Some(subdomain) = subdomain.strip_suffix('.') {
28+
Ok(Some(subdomain))
29+
} else {
30+
Err(RequestRewriterError::InvalidHostHeader)
31+
}
32+
} else {
33+
tracing::warn!(host, "Host header does not end in cluster name.");
34+
Err(RequestRewriterError::InvalidHostHeader)
35+
}
36+
}
37+
38+
#[cfg(test)]
39+
mod tests {
40+
use super::*;
41+
use std::str::FromStr;
42+
43+
#[test]
44+
fn no_subdomains() {
45+
let host = "foo.bar.baz";
46+
let cluster = ClusterName::from_str("foo.bar.baz").unwrap();
47+
assert_eq!(subdomain_from_host(host, &cluster), Ok(None));
48+
}
49+
50+
#[test]
51+
fn valid_subdomain() {
52+
let host = "foobar.example.com";
53+
let cluster = ClusterName::from_str("example.com").unwrap();
54+
assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar")));
55+
}
56+
57+
#[test]
58+
fn valid_suffix_no_dot() {
59+
let host = "foobarexample.com";
60+
let cluster = ClusterName::from_str("example.com").unwrap();
61+
assert_eq!(
62+
subdomain_from_host(host, &cluster),
63+
Err(RequestRewriterError::InvalidHostHeader)
64+
);
65+
}
66+
67+
#[test]
68+
fn invalid_suffix() {
69+
let host = "abc.abc.com";
70+
let cluster = ClusterName::from_str("example.com").unwrap();
71+
assert_eq!(
72+
subdomain_from_host(host, &cluster),
73+
Err(RequestRewriterError::InvalidHostHeader)
74+
);
75+
}
76+
77+
#[test]
78+
fn allowed_port() {
79+
let host = "foobar.myhost:8080";
80+
let cluster = ClusterName::from_str("myhost:8080").unwrap();
81+
assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar")));
82+
}
83+
84+
#[test]
85+
fn port_required() {
86+
let host = "foobar.myhost";
87+
let cluster = ClusterName::from_str("myhost:8080").unwrap();
88+
assert_eq!(
89+
subdomain_from_host(host, &cluster),
90+
Err(RequestRewriterError::InvalidHostHeader)
91+
);
92+
}
93+
94+
#[test]
95+
fn port_must_match() {
96+
let host = "foobar.myhost:8080";
97+
let cluster = ClusterName::from_str("myhost").unwrap();
98+
assert_eq!(
99+
subdomain_from_host(host, &cluster),
100+
Err(RequestRewriterError::InvalidHostHeader)
101+
);
102+
}
103+
104+
#[test]
105+
fn port_443_optional() {
106+
let host = "foobar.myhost:443";
107+
let cluster = ClusterName::from_str("myhost").unwrap();
108+
assert_eq!(subdomain_from_host(host, &cluster), Ok(Some("foobar")));
109+
}
110+
}

0 commit comments

Comments
 (0)