Skip to content

Commit

Permalink
subdomain verification
Browse files Browse the repository at this point in the history
  • Loading branch information
paulgb committed Sep 24, 2024
1 parent 2e93a0f commit 0e53e8f
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 25 deletions.
22 changes: 22 additions & 0 deletions plane/plane-tests/tests/common/localhost_resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use hyper::client::connect::dns::Name;
use reqwest::dns::{Resolve, Resolving};
use std::{future::ready, net::SocketAddr, sync::Arc};

/// A reqwest-compatible DNS resolver that resolves all requests to localhost.
struct LocalhostResolver;

impl Resolve for LocalhostResolver {
fn resolve(&self, _name: Name) -> Resolving {
let addrs = vec![SocketAddr::from(([127, 0, 0, 1], 0))];
let addrs: Box<dyn Iterator<Item = SocketAddr> + Send> = Box::new(addrs.into_iter());
Box::pin(ready(Ok(addrs)))
}
}

#[allow(unused)]
pub fn localhost_client() -> reqwest::Client {
reqwest::Client::builder()
.dns_resolver(Arc::new(LocalhostResolver))
.build()
.unwrap()
}
1 change: 1 addition & 0 deletions plane/plane-tests/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use tokio::time::timeout;
pub mod async_drop;
pub mod auth_mock;
pub mod docker;
pub mod localhost_resolver;
pub mod proxy_mock;
pub mod resources;
pub mod simple_axum_server;
Expand Down
4 changes: 4 additions & 0 deletions plane/plane-tests/tests/common/proxy_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ impl MockProxy {
self.addr
}

pub fn port(&self) -> u16 {
self.addr.port()
}

pub async fn recv_route_info_request(&mut self) -> RouteInfoRequest {
self.route_info_request_receiver
.recv()
Expand Down
111 changes: 96 additions & 15 deletions plane/plane-tests/tests/proxy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::{net::SocketAddr, str::FromStr};

use common::{
localhost_resolver::localhost_client,
proxy_mock::MockProxy,
simple_axum_server::{RequestInfo, SimpleAxumServer},
test_env::TestEnvironment,
Expand All @@ -9,10 +8,11 @@ use plane::{
log_types::BackendAddr,
names::{BackendName, Name},
protocol::{RouteInfo, RouteInfoResponse},
types::{BearerToken, ClusterName, SecretToken},
types::{BearerToken, ClusterName, SecretToken, Subdomain},
};
use plane_test_macro::plane_test;
use reqwest::StatusCode;
use std::{net::SocketAddr, str::FromStr};
use tokio::net::TcpListener;

mod common;
Expand Down Expand Up @@ -57,8 +57,11 @@ async fn proxy_bad_bearer_token(env: TestEnvironment) {
#[plane_test]
async fn proxy_backend_unreachable(env: TestEnvironment) {
let mut proxy = MockProxy::new().await;
let url = format!("http://{}/abc123/", proxy.addr());
let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") });
let port = proxy.port();
let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap();
let url = format!("http://plane.test:{port}/abc123/");
let client = localhost_client();
let handle = tokio::spawn(client.get(url).send());

let route_info_request = proxy.recv_route_info_request().await;
assert_eq!(
Expand All @@ -73,15 +76,15 @@ async fn proxy_backend_unreachable(env: TestEnvironment) {
backend_id: BackendName::new_random(),
address: BackendAddr(SocketAddr::from(([123, 234, 123, 234], 12345))),
secret_token: SecretToken::from("secret".to_string()),
cluster: ClusterName::from_str("test").unwrap(),
cluster,
user: None,
user_data: None,
subdomain: None,
}),
})
.await;

let response = handle.await.unwrap();
let response = handle.await.unwrap().unwrap();

assert_eq!(response.status(), StatusCode::BAD_GATEWAY);
}
Expand All @@ -95,8 +98,11 @@ async fn proxy_backend_timeout(env: TestEnvironment) {
let addr = listener.local_addr().unwrap();

let mut proxy = MockProxy::new().await;
let url = format!("http://{}/abc123/", proxy.addr());
let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") });
let port = proxy.port();
let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap();
let url = format!("http://plane.test:{port}/abc123/");
let client = localhost_client();
let handle = tokio::spawn(client.get(url).send());

let route_info_request = proxy.recv_route_info_request().await;
assert_eq!(
Expand All @@ -111,15 +117,15 @@ async fn proxy_backend_timeout(env: TestEnvironment) {
backend_id: BackendName::new_random(),
address: BackendAddr(addr),
secret_token: SecretToken::from("secret".to_string()),
cluster: ClusterName::from_str("test").unwrap(),
cluster,
user: None,
user_data: None,
subdomain: None,
}),
})
.await;

let response = handle.await.unwrap();
let response = handle.await.unwrap().unwrap();

assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
}
Expand All @@ -129,8 +135,11 @@ async fn proxy_backend_accepts(env: TestEnvironment) {
let server = SimpleAxumServer::new().await;

let mut proxy = MockProxy::new().await;
let url = format!("http://{}/abc123/", proxy.addr());
let handle = tokio::spawn(async { reqwest::get(url).await.expect("Failed to send request") });
let port = proxy.port();
let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap();
let url = format!("http://plane.test:{port}/abc123/");
let client = localhost_client();
let handle = tokio::spawn(client.get(url).send());

let route_info_request = proxy.recv_route_info_request().await;
assert_eq!(
Expand All @@ -145,17 +154,89 @@ async fn proxy_backend_accepts(env: TestEnvironment) {
backend_id: BackendName::new_random(),
address: BackendAddr(server.addr()),
secret_token: SecretToken::from("secret".to_string()),
cluster: ClusterName::from_str("test").unwrap(),
cluster,
user: None,
user_data: None,
subdomain: None,
}),
})
.await;

let response = handle.await.unwrap();
let response = handle.await.unwrap().unwrap();
assert_eq!(response.status(), StatusCode::OK);
let request_info: RequestInfo = response.json().await.unwrap();
assert_eq!(request_info.path, "/");
assert_eq!(request_info.method, "GET");
}

#[plane_test]
async fn proxy_expected_subdomain_not_present(env: TestEnvironment) {
let server = SimpleAxumServer::new().await;

let mut proxy = MockProxy::new().await;
let port = proxy.port();
let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap();
let url = format!("http://plane.test:{port}/abc123/");
let client = localhost_client();
let handle = tokio::spawn(client.get(url).send());

let route_info_request = proxy.recv_route_info_request().await;
assert_eq!(
route_info_request.token,
BearerToken::from("abc123".to_string())
);

proxy
.send_route_info_response(RouteInfoResponse {
token: BearerToken::from("abc123".to_string()),
route_info: Some(RouteInfo {
backend_id: BackendName::new_random(),
address: BackendAddr(server.addr()),
secret_token: SecretToken::from("secret".to_string()),
cluster,
user: None,
user_data: None,
subdomain: Some(Subdomain::from_str("missing-subdomain").unwrap()),
}),
})
.await;

let response = handle.await.unwrap().unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}

#[plane_test]
async fn proxy_expected_subdomain_is_present(env: TestEnvironment) {
let server = SimpleAxumServer::new().await;

let mut proxy = MockProxy::new().await;
let port = proxy.port();
let cluster = ClusterName::from_str(&format!("plane.test:{}", port)).unwrap();
let url = format!("http://mysubdomain.plane.test:{port}/abc123/");
let client = localhost_client();
let handle = tokio::spawn(client.get(url).send());

let route_info_request = proxy.recv_route_info_request().await;
assert_eq!(
route_info_request.token,
BearerToken::from("abc123".to_string())
);

proxy
.send_route_info_response(RouteInfoResponse {
token: BearerToken::from("abc123".to_string()),
route_info: Some(RouteInfo {
backend_id: BackendName::new_random(),
address: BackendAddr(server.addr()),
secret_token: SecretToken::from("secret".to_string()),
cluster,
user: None,
user_data: None,
subdomain: Some(Subdomain::from_str("mysubdomain").unwrap()),
}),
})
.await;

let response = handle.await.unwrap().unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
45 changes: 35 additions & 10 deletions plane/src/proxy/proxy_server.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use super::{
connection_monitor::ConnectionMonitorHandle, request::get_and_maybe_remove_bearer_token,
connection_monitor::ConnectionMonitorHandle,
request::{get_and_maybe_remove_bearer_token, subdomain_from_host},
route_map::RouteMap,
};
use dynamic_proxy::{
body::{simple_empty_body, SimpleBody},
hyper::{body::Incoming, service::Service, Request, Response, StatusCode, Uri},
hyper::{body::Incoming, header, service::Service, Request, Response, StatusCode, Uri},
proxy::ProxyClient,
request::MutableRequest,
};
Expand Down Expand Up @@ -61,10 +62,7 @@ impl Service<Request<Incoming>> for ProxyState {
let bearer_token = get_and_maybe_remove_bearer_token(&mut uri_parts);

let Some(bearer_token) = bearer_token else {
return Box::pin(ready(Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(simple_empty_body())
.unwrap())));
return Box::pin(ready(status_code_to_response(StatusCode::UNAUTHORIZED)));
};

request.parts.uri = Uri::from_parts(uri_parts).unwrap();

Check warning on line 68 in plane/src/proxy/proxy_server.rs

View workflow job for this annotation

GitHub Actions / clippy

used `unwrap()` on a `Result` value

warning: used `unwrap()` on a `Result` value --> plane/src/proxy/proxy_server.rs:68:29 | 68 | request.parts.uri = Uri::from_parts(uri_parts).unwrap(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: if this value is an `Err`, it will panic = help: consider using `expect()` to provide a better panic message = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used note: the lint level is defined here --> plane/src/lib.rs:1:9 | 1 | #![warn(clippy::unwrap_used)] | ^^^^^^^^^^^^^^^^^^^
Expand All @@ -76,12 +74,30 @@ impl Service<Request<Incoming>> for ProxyState {
let route_info = inner.route_map.lookup(&bearer_token).await;

let Some(route_info) = route_info else {
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.body(simple_empty_body())
.unwrap());
return status_code_to_response(StatusCode::UNAUTHORIZED);
};

// Check cluster and subdomain.
let Some(host) = request
.parts
.headers
.get(header::HOST)
.and_then(|h| h.to_str().ok())
else {
return status_code_to_response(StatusCode::BAD_REQUEST);
};

let Ok(request_subdomain) = subdomain_from_host(host, &route_info.cluster) else {
// The host header does not match the expected cluster.
return status_code_to_response(StatusCode::FORBIDDEN);
};

if let Some(subdomain) = route_info.subdomain {
if request_subdomain != Some(&subdomain) {
return status_code_to_response(StatusCode::FORBIDDEN);
}
}

request.set_upstream_address(route_info.address.0);
let request = request.into_request_with_simple_body();

Expand Down Expand Up @@ -109,3 +125,12 @@ impl Service<Request<Incoming>> for ProxyState {
})
}
}

fn status_code_to_response(
status_code: StatusCode,
) -> Result<Response<SimpleBody>, Box<dyn std::error::Error + Send + Sync>> {
Ok(Response::builder()
.status(status_code)
.body(simple_empty_body())
.unwrap())

Check warning on line 135 in plane/src/proxy/proxy_server.rs

View workflow job for this annotation

GitHub Actions / clippy

used `unwrap()` on a `Result` value

warning: used `unwrap()` on a `Result` value --> plane/src/proxy/proxy_server.rs:132:8 | 132 | Ok(Response::builder() | ________^ 133 | | .status(status_code) 134 | | .body(simple_empty_body()) 135 | | .unwrap()) | |_________________^ | = note: if this value is an `Err`, it will panic = help: consider using `expect()` to provide a better panic message = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unwrap_used
}

0 comments on commit 0e53e8f

Please sign in to comment.