-
Notifications
You must be signed in to change notification settings - Fork 8
Datadome server-side validation #654
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
ChristianPavilonis
wants to merge
2
commits into
split/multi-backend-routing
from
split/datadome-server-side-validation
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| #![allow(dead_code)] | ||
|
|
||
| mod common; | ||
| mod environments; | ||
|
|
||
| use common::runtime::RuntimeEnvironment as _; | ||
| use environments::fastly::FastlyViceroy; | ||
| use std::io::{Read as _, Write as _}; | ||
| use std::net::{TcpListener, TcpStream}; | ||
| use std::sync::atomic::{AtomicBool, Ordering}; | ||
| use std::sync::{Arc, OnceLock}; | ||
| use std::thread; | ||
|
|
||
| /// In-process HTTP server that returns a fixed response body. | ||
| /// | ||
| /// Listens on a fixed port. Accepts connections in a background thread, | ||
| /// drains each request, and responds with `HTTP/1.1 200 OK` and the | ||
| /// configured body. Stopped on [`Drop`] via a shutdown flag + self-connect. | ||
| /// | ||
| /// Does not store the `JoinHandle` so that `MockOrigin` remains `Sync` | ||
| /// (required for placement in a `static OnceLock`). The thread exits | ||
| /// naturally when the process ends. | ||
| struct MockOrigin { | ||
| port: u16, | ||
| shutdown: Arc<AtomicBool>, | ||
| } | ||
|
|
||
| impl MockOrigin { | ||
| /// Start a mock origin server on `port` that always responds with `body`. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if the port cannot be bound. | ||
| fn start(port: u16, body: &'static str) -> Self { | ||
| let listener = TcpListener::bind(format!("127.0.0.1:{port}")) | ||
| .unwrap_or_else(|e| panic!("should bind MockOrigin to port {port}: {e}")); | ||
|
|
||
| let shutdown = Arc::new(AtomicBool::new(false)); | ||
| let shutdown_clone = Arc::clone(&shutdown); | ||
|
|
||
| thread::spawn(move || { | ||
| for stream in listener.incoming() { | ||
| if shutdown_clone.load(Ordering::Relaxed) { | ||
| break; | ||
| } | ||
| if let Ok(stream) = stream { | ||
| serve(stream, body); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| MockOrigin { port, shutdown } | ||
| } | ||
| } | ||
|
|
||
| impl Drop for MockOrigin { | ||
| fn drop(&mut self) { | ||
| self.shutdown.store(true, Ordering::Relaxed); | ||
| // Unblock the accept() call so the thread can observe the shutdown flag. | ||
| let _ = TcpStream::connect(format!("127.0.0.1:{}", self.port)); | ||
| } | ||
| } | ||
|
|
||
| /// Write a minimal HTTP/1.1 200 response with `body` to `stream`. | ||
| /// | ||
| /// Drains the incoming request first so the client does not see a broken pipe. | ||
| fn serve(mut stream: TcpStream, body: &'static str) { | ||
| let mut buf = [0u8; 4096]; | ||
| let _ = stream.read(&mut buf); | ||
| let response = format!( | ||
| "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {len}\r\nConnection: close\r\n\r\n{body}", | ||
| len = body.len(), | ||
| ); | ||
| let _ = stream.write_all(response.as_bytes()); | ||
| } | ||
|
|
||
| /// Shared test state: mock origins + Viceroy process + pre-configured reqwest client. | ||
| /// | ||
| /// Initialised once via [`get_harness`]. All five test functions share this | ||
| /// single instance to avoid the cost of spinning up Viceroy per test. | ||
| struct RoutingHarness { | ||
| _origins: Vec<MockOrigin>, | ||
| _process: common::runtime::RuntimeProcess, | ||
| /// Client with resolve overrides so `http://site-a.test/` connects to Viceroy | ||
| /// while sending the correct `Host` header. | ||
| client: reqwest::blocking::Client, | ||
| } | ||
|
|
||
| static HARNESS: OnceLock<Option<RoutingHarness>> = OnceLock::new(); | ||
|
|
||
| /// Return the shared harness, or `None` if `ROUTING_WASM_PATH` is not set. | ||
| /// | ||
| /// Returns `None` rather than panicking so that tests pass trivially when | ||
| /// invoked outside the routing-specific CI step (e.g. `cargo test --workspace`). | ||
| fn get_harness() -> Option<&'static RoutingHarness> { | ||
| HARNESS | ||
| .get_or_init(|| { | ||
| let wasm_path = std::env::var("ROUTING_WASM_PATH").ok()?; | ||
|
|
||
| let origins = vec![ | ||
| MockOrigin::start(19090, "default"), | ||
| MockOrigin::start(19091, "site-a"), | ||
| MockOrigin::start(19092, "site-b"), | ||
| MockOrigin::start(19093, "api"), | ||
| ]; | ||
|
|
||
| let process = FastlyViceroy | ||
| .spawn(std::path::Path::new(&wasm_path)) | ||
| .expect("should spawn Viceroy with routing WASM"); | ||
|
|
||
| let viceroy_port: u16 = process | ||
| .base_url | ||
| .trim_start_matches("http://127.0.0.1:") | ||
| .parse() | ||
| .expect("should parse Viceroy port from base_url"); | ||
|
|
||
| let viceroy_addr: std::net::SocketAddr = format!("127.0.0.1:{viceroy_port}") | ||
| .parse() | ||
| .expect("should parse Viceroy socket addr"); | ||
|
|
||
| let client = reqwest::blocking::ClientBuilder::new() | ||
| .resolve("site-a.test", viceroy_addr) | ||
| .resolve("www.site-a.test", viceroy_addr) | ||
| .resolve("site-b.test", viceroy_addr) | ||
| .resolve("any.test", viceroy_addr) | ||
| .resolve("unknown.test", viceroy_addr) | ||
| .build() | ||
| .expect("should build reqwest client"); | ||
|
|
||
| Some(RoutingHarness { | ||
| _origins: origins, | ||
| _process: process, | ||
| client, | ||
| }) | ||
| }) | ||
| .as_ref() | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Tests | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| #[test] | ||
| fn domain_routes_to_site_a() { | ||
| let Some(h) = get_harness() else { return }; | ||
|
|
||
| let body = h | ||
| .client | ||
| .get("http://site-a.test/") | ||
| .send() | ||
| .expect("should send request to site-a.test") | ||
| .text() | ||
| .expect("should read response body"); | ||
|
|
||
| assert_eq!(body, "site-a", "should route site-a.test to the site-a backend"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn domain_routes_to_site_b() { | ||
| let Some(h) = get_harness() else { return }; | ||
|
|
||
| let body = h | ||
| .client | ||
| .get("http://site-b.test/") | ||
| .send() | ||
| .expect("should send request to site-b.test") | ||
| .text() | ||
| .expect("should read response body"); | ||
|
|
||
| assert_eq!(body, "site-b", "should route site-b.test to the site-b backend"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn www_prefix_stripped() { | ||
| let Some(h) = get_harness() else { return }; | ||
|
|
||
| let body = h | ||
| .client | ||
| .get("http://www.site-a.test/") | ||
| .send() | ||
| .expect("should send request to www.site-a.test") | ||
| .text() | ||
| .expect("should read response body"); | ||
|
|
||
| assert_eq!( | ||
| body, "site-a", | ||
| "should strip www. prefix and route to the site-a backend" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn path_routes_to_api() { | ||
| let Some(h) = get_harness() else { return }; | ||
|
|
||
| // any.test has no domain entry — path pattern matching fires instead. | ||
| let body = h | ||
| .client | ||
| .get("http://any.test/.api/users") | ||
| .send() | ||
| .expect("should send request to any.test/.api/users") | ||
| .text() | ||
| .expect("should read response body"); | ||
|
|
||
| assert_eq!( | ||
| body, "api", | ||
| "should route /.api/ path prefix to the api backend" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn unknown_host_falls_back_to_default() { | ||
| let Some(h) = get_harness() else { return }; | ||
|
|
||
| let body = h | ||
| .client | ||
| .get("http://unknown.test/") | ||
| .send() | ||
| .expect("should send request to unknown.test") | ||
| .text() | ||
| .expect("should read response body"); | ||
|
|
||
| assert_eq!( | ||
| body, "default", | ||
| "should fall back to publisher.origin_url for unmatched hosts" | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 thinking — Hardcoded ports 19090–19093 collide between concurrent local runs. Lower-priority since these tests skip when
ROUTING_WASM_PATHis unset, but consider ephemeral-port allocation + writing the chosen ports into the WASM build env.