#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use gosub_sonar::net::test_support::{CorsRouteOptions, RouteConfig, TestServer};
use gosub_sonar::{
FetchRequest, FetchResult, Fetcher, FetcherConfig, FetcherContext, Initiator, NetEvent,
NetObserver, ReferrerPolicy, RequestCredentials, RequestDestination, RequestId, RequestMode,
RequestReference, ResourceKind,
};
use http::{HeaderMap, HeaderValue, Method};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use url::Url;
struct PolicyLog;
impl NetObserver for PolicyLog {
fn on_event(&self, ev: NetEvent) {
match ev {
NetEvent::Blocked { reason, url } => println!(" blocked: {reason} ({url})"),
NetEvent::CorsPreflight { url } => println!(" preflight sent to {url}"),
NetEvent::Redirected { status, to, .. } => println!(" {status} -> {to}"),
_ => {}
}
}
}
struct Ctx;
impl FetcherContext for Ctx {
fn observer_for(
&self,
_: RequestReference,
_: RequestId,
_: ResourceKind,
_: Initiator,
) -> Arc<dyn NetObserver + Send + Sync> {
Arc::new(PolicyLog)
}
fn on_ref_active(&self, _: RequestReference) {}
fn on_ref_done(&self, _: RequestReference) {}
}
fn from_page(page: &Url, method: Method, url: Url) -> FetchRequest {
FetchRequest::builder(method, url)
.with_origin(page.origin())
.with_referrer(page.clone())
.with_destination(RequestDestination::Image)
.with_mode(RequestMode::NoCors)
.build()
}
async fn show(label: &str, fetcher: &Fetcher, req: FetchRequest) {
println!("{label}");
match fetcher.fetch(req).await {
FetchResult::Buffered { meta, body } => {
println!(
" {} {:?} (tainting: {:?})\n",
meta.status,
String::from_utf8_lossy(&body),
meta.tainting
)
}
FetchResult::Stream { meta, .. } => println!(" {} (stream)\n", meta.status),
FetchResult::Error(e) => println!(" {e}\n"),
}
}
#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let srv = TestServer::new()
.route("/referer", RouteConfig::echo_request_header("Referer"))
.route("/site", RouteConfig::echo_request_header("Sec-Fetch-Site"))
.route("/dest", RouteConfig::echo_request_header("Sec-Fetch-Dest"))
.route("/origin", RouteConfig::echo_request_header("Origin"))
.route(
"/api",
RouteConfig::cors(CorsRouteOptions {
allow_headers: Some("x-custom".into()),
..CorsRouteOptions::default()
}),
)
.start()
.await;
let fetcher = Arc::new(Fetcher::new(FetcherConfig::default(), Arc::new(Ctx))?);
let shutdown = CancellationToken::new();
let run = fetcher.clone();
let cancel = shutdown.clone();
tokio::spawn(async move { run.run(cancel).await });
let same = srv.url("/page.html");
let cross = Url::parse(&format!(
"http://localhost:{}/page.html",
srv.socket_addr().port()
))?;
println!("== same-origin page {same}\n");
show(
"Referer: full URL within the same origin",
&fetcher,
from_page(&same, Method::GET, srv.url("/referer")),
)
.await;
show(
"Sec-Fetch-Site",
&fetcher,
from_page(&same, Method::GET, srv.url("/site")),
)
.await;
show(
"Sec-Fetch-Dest (from FetchRequest::destination)",
&fetcher,
from_page(&same, Method::GET, srv.url("/dest")),
)
.await;
println!("== cross-origin page {cross}\n");
show(
"Referer: strict-origin-when-cross-origin sends only the origin",
&fetcher,
from_page(&cross, Method::GET, srv.url("/referer")),
)
.await;
show(
"Referer with ReferrerPolicy::NoReferrer",
&fetcher,
FetchRequest::builder(Method::GET, srv.url("/referer"))
.with_origin(cross.origin())
.with_referrer(cross.clone())
.with_referrer_policy(ReferrerPolicy::NoReferrer)
.build(),
)
.await;
show(
"Sec-Fetch-Site",
&fetcher,
from_page(&cross, Method::GET, srv.url("/site")),
)
.await;
show(
"Origin header on a POST",
&fetcher,
from_page(&cross, Method::POST, srv.url("/origin")),
)
.await;
show(
"mode: cors to a route without CORS headers -> refused, response never reaches the page",
&fetcher,
FetchRequest::builder(Method::GET, srv.url("/site"))
.with_origin(cross.origin())
.with_mode(RequestMode::Cors)
.build(),
)
.await;
let mut headers = HeaderMap::new();
headers.insert("x-custom", HeaderValue::from_static("1"));
show(
"mode: cors with a custom header to a CORS route -> preflight, then the request",
&fetcher,
FetchRequest::builder(Method::GET, srv.url("/api"))
.with_origin(cross.origin())
.with_mode(RequestMode::Cors)
.with_credentials(RequestCredentials::Omit)
.with_headers(headers)
.build(),
)
.await;
println!("== secure page https://secure.example/\n");
let secure = Url::parse("https://secure.example/page.html")?;
show(
"mixed content: http:// image from an https:// page -> blocked before connecting",
&fetcher,
from_page(
&secure,
Method::GET,
Url::parse("http://example.com/logo.png")?,
),
)
.await;
shutdown.cancel();
Ok(())
}