Skip to content

Commit

Permalink
Allow host overrides in TOML configuration
Browse files Browse the repository at this point in the history
This adds a new field to the backend definition, override_host,
allowing to override the Host header that will be send to the
backend.

Addresses #9.
  • Loading branch information
fgsch committed Jul 19, 2021
1 parent 28e4078 commit a1091bb
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 29 deletions.
4 changes: 3 additions & 1 deletion cli/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ impl Test {
}

/// Add a backend definition to this test.
pub fn backend(mut self, name: &str, url: &str) -> Self {
pub fn backend(mut self, name: &str, url: &str, override_host: Option<&str>) -> Self {
let backend = Backend {
uri: url.parse().expect("invalid backend URL"),
override_host: override_host
.and_then(|s| Some(s.parse().expect("can parse override_host"))),
};
self.backends.insert(name.to_owned(), Arc::new(backend));
self
Expand Down
4 changes: 2 additions & 2 deletions cli/tests/http-semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async fn framing_headers_are_overridden() -> TestResult {
// Set up the test harness
let test = Test::using_fixture("bad-framing-headers.wasm")
// The "TheOrigin" backend checks framing headers on the request and then echos its body.
.backend("TheOrigin", "http://127.0.0.1:9000/")
.backend("TheOrigin", "http://127.0.0.1:9000/", None)
.host(9000, |req| {
assert!(!req.headers().contains_key(header::TRANSFER_ENCODING));
assert_eq!(
Expand Down Expand Up @@ -47,7 +47,7 @@ async fn content_length_is_computed_correctly() -> TestResult {
// Set up the test harness
let test = Test::using_fixture("content-length.wasm")
// The "TheOrigin" backend supplies a fixed-size body.
.backend("TheOrigin", "http://127.0.0.1:9000/")
.backend("TheOrigin", "http://127.0.0.1:9000/", None)
.host(9000, |_| {
Response::new(Vec::from(&b"ABCDEFGHIJKLMNOPQRST"[..]))
});
Expand Down
4 changes: 2 additions & 2 deletions cli/tests/upstream-async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ async fn upstream_async_methods() -> TestResult {
// Set up the test harness
let test = Test::using_fixture("upstream-async.wasm")
// Set up the backends, which just return responses with an identifying header
.backend("backend1", "http://127.0.0.1:9000/")
.backend("backend1", "http://127.0.0.1:9000/", None)
.host(9000, |_| {
Response::builder()
.header("Backend-1-Response", "")
.status(StatusCode::OK)
.body(vec![])
.unwrap()
})
.backend("backend2", "http://127.0.0.1:9001/")
.backend("backend2", "http://127.0.0.1:9001/", None)
.host(9001, |_| {
Response::builder()
.header("Backend-2-Response", "")
Expand Down
2 changes: 1 addition & 1 deletion cli/tests/upstream-streaming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async fn upstream_streaming() -> TestResult {
// Set up the test harness
let test = Test::using_fixture("upstream-streaming.wasm")
// The "origin" backend simply echos the request body
.backend("origin", "http://127.0.0.1:9000/")
.backend("origin", "http://127.0.0.1:9000/", None)
.host(9000, |req| Response::new(req.into_body()));

// Test with an empty request
Expand Down
35 changes: 31 additions & 4 deletions cli/tests/upstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod common;

use {
common::{Test, TestResult},
hyper::{Request, Response, StatusCode},
hyper::{header, Request, Response, StatusCode},
};

#[tokio::test(flavor = "multi_thread")]
Expand All @@ -13,18 +13,31 @@ async fn upstream_sync() -> TestResult {

// Set up the test harness
let test = Test::using_fixture("upstream.wasm")
.backend("origin", "http://127.0.0.1:9000/")
.backend("origin", "http://127.0.0.1:9000/", None)
// The "origin" backend simply echos the request body
.host(9000, |req| {
let body = req.into_body();
Response::new(body)
})
// The "prefix-*" backends return the request URL as the response body
.backend("prefix-hello", "http://127.0.0.1:9001/hello")
.backend("prefix-hello-slash", "http://127.0.0.1:9001/hello/")
.backend("prefix-hello", "http://127.0.0.1:9001/hello", None)
.backend("prefix-hello-slash", "http://127.0.0.1:9001/hello/", None)
.host(9001, |req| {
let body = req.uri().to_string().into_bytes();
Response::new(body)
})
// The "override-host" backend checks the Host header
.backend(
"override-host",
"http://127.0.0.1:9002/",
Some("otherhost.com"),
)
.host(9002, |req| {
assert_eq!(
req.headers().get(header::HOST),
Some(&hyper::header::HeaderValue::from_static("otherhost.com"))
);
Response::new(vec![])
});

////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -100,6 +113,20 @@ async fn upstream_sync() -> TestResult {
"/hello/greeting.html"
);

////////////////////////////////////////////////////////////////////////////////////
// Test that override_host works as intended
////////////////////////////////////////////////////////////////////////////////////

let resp = test
.against(
Request::get("http://localhost/override")
.header("Viceroy-Backend", "override-host")
.body("")
.unwrap(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);

////////////////////////////////////////////////////////////////////////////////////
// Test that non-existent backends produce an error
////////////////////////////////////////////////////////////////////////////////////
Expand Down
18 changes: 15 additions & 3 deletions lib/src/config/backends.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use {
hyper::Uri,
hyper::{header::HeaderValue, Uri},
std::{collections::HashMap, sync::Arc},
};

Expand All @@ -9,6 +9,7 @@ use {
#[derive(Clone, Debug)]
pub struct Backend {
pub uri: Uri,
pub override_host: Option<HeaderValue>,
}

/// A map of [`Backend`] definitions, keyed by their name.
Expand All @@ -24,7 +25,7 @@ mod deserialization {
use {
super::{Backend, BackendsConfig},
crate::error::{BackendConfigError, FastlyConfigError},
hyper::Uri,
hyper::{header::HeaderValue, Uri},
std::{convert::TryFrom, sync::Arc},
toml::value::{Table, Value},
};
Expand Down Expand Up @@ -87,10 +88,21 @@ mod deserialization {
Value::String(url) => url.parse::<Uri>().map_err(BackendConfigError::from),
_ => Err(BackendConfigError::InvalidUrlEntry),
})?;
let override_host = if let Some(override_host) = toml.remove("override_host") {
Some(match override_host {
Value::String(override_host) if !override_host.trim().is_empty() => {
HeaderValue::from_str(&override_host).map_err(BackendConfigError::from)
}
Value::String(_) => Err(BackendConfigError::EmptyOverrideHost),
_ => Err(BackendConfigError::InvalidOverrideHostEntry),
}?)
} else {
None
};

check_for_unrecognized_keys(&toml)?;

Ok(Self { uri })
Ok(Self { uri, override_host })
}
}
}
81 changes: 65 additions & 16 deletions lib/src/config/unit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,28 +50,29 @@ fn fastly_toml_files_with_simple_backend_configurations_can_be_read() {
[local_server.backends."shark.server"]
url = "http://localhost:7878/shark-mocks"
override_host = "somehost.com"
[local_server.backends.detective]
url = "http://www.elementary.org/"
"#,
)
.expect("can read toml data containing backend configurations");

let backend = config
.backends()
.get("dog")
.expect("backend configurations can be accessed");
assert_eq!(backend.uri, "http://localhost:7878/dog-mocks");
assert_eq!(backend.override_host, None);

let backend = config
.backends()
.get("shark.server")
.expect("backend configurations can be accessed");
assert_eq!(backend.uri, "http://localhost:7878/shark-mocks");
assert_eq!(
config
.backends()
.get("dog")
.expect("backend configurations can be accessed")
.uri,
"http://localhost:7878/dog-mocks"
);
assert_eq!(
config
.backends()
.get("shark.server")
.expect("backend configurations can be accessed")
.uri,
"http://localhost:7878/shark-mocks"
backend.override_host,
Some("somehost.com".parse().expect("can parse override_host"))
);
}

Expand Down Expand Up @@ -183,15 +184,63 @@ mod testing_config_tests {
#[test]
fn backend_configs_must_provide_a_valid_url() {
use BackendConfigError::InvalidUrl;
static NO_URL: &str = r#"
static BAD_URL_FIELD: &str = r#"
[backends]
"shark" = { url = "http:://[:::1]" }
"#;
match read_toml_config(NO_URL) {
match read_toml_config(BAD_URL_FIELD) {
Err(InvalidBackendDefinition {
err: InvalidUrl(_), ..
}) => {}
res => panic!("unexpected result: {:?}", res),
}
}
/// Check that override_host field is a string.
#[test]
fn backend_configs_must_provide_override_host_as_a_string() {
use BackendConfigError::InvalidOverrideHostEntry;
static BAD_OVERRIDE_HOST_FIELD: &str = r#"
[backends]
"shark" = { url = "http://a.com", override_host = 3 }
"#;
match read_toml_config(BAD_OVERRIDE_HOST_FIELD) {
Err(InvalidBackendDefinition {
err: InvalidOverrideHostEntry,
..
}) => {}
res => panic!("unexpected result: {:?}", res),
}
}
/// Check that override_host field is non empty.
#[test]
fn backend_configs_must_provide_a_non_empty_override_host() {
use BackendConfigError::EmptyOverrideHost;
static EMPTY_OVERRIDE_HOST_FIELD: &str = r#"
[backends]
"shark" = { url = "http://a.com", override_host = "" }
"#;
match read_toml_config(EMPTY_OVERRIDE_HOST_FIELD) {
Err(InvalidBackendDefinition {
err: EmptyOverrideHost,
..
}) => {}
res => panic!("unexpected result: {:?}", res),
}
}
/// Check that override_host field is valid.
#[test]
fn backend_configs_must_provide_a_valid_override_host() {
use BackendConfigError::InvalidOverrideHost;
static BAD_OVERRIDE_HOST_FIELD: &str = r#"
[backends]
"shark" = { url = "http://a.com", override_host = "somehost.com\n" }
"#;
match read_toml_config(BAD_OVERRIDE_HOST_FIELD) {
Err(InvalidBackendDefinition {
err: InvalidOverrideHost(_),
..
}) => {}
res => panic!("unexpected result: {:?}", res),
}
}
}
9 changes: 9 additions & 0 deletions lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,15 @@ pub enum BackendConfigError {
#[error("definition was not provided as a TOML table")]
InvalidEntryType,

#[error("invalid override_host: {0}")]
InvalidOverrideHost(#[from] http::header::InvalidHeaderValue),

#[error("'override_host' field is empty")]
EmptyOverrideHost,

#[error("'override_host' field was not a string")]
InvalidOverrideHostEntry,

#[error("invalid url: {0}")]
InvalidUrl(#[from] http::uri::InvalidUri),

Expand Down
6 changes: 6 additions & 0 deletions lib/src/upstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ pub fn send_request(
req = Request::from_parts(req_parts, req_body);
}

// if requested override the host header
if let Some(override_host) = &backend.override_host {
req.headers_mut()
.insert(hyper::header::HOST, override_host.clone());
}

filter_outgoing_headers(req.headers_mut());

Ok(async move {
Expand Down

0 comments on commit a1091bb

Please sign in to comment.