use kynos::{
Router,
http::{Method, Request, Response, StatusCode, header},
middleware::cors::Cors,
response::status::NoContent,
router::service::Service,
};
#[derive(kynos::Schema, serde::Serialize)]
struct Widget {
name: String,
}
#[kynos::get("/widgets")]
async fn list_widgets() -> kynos::extract::body::json::Json<Vec<Widget>> {
kynos::extract::body::json::Json(vec![Widget {
name: "sprocket".to_owned(),
}])
}
#[kynos::delete("/widgets")]
async fn delete_widget() -> NoContent {
NoContent
}
#[tokio::main]
async fn main() -> kynos::Result<()> {
let permitted: Vec<String> = std::env::var("CORS_ORIGINS")
.unwrap_or_else(|_| "https://app.example.com".to_owned())
.split(',')
.map(|origin| origin.trim().to_owned())
.collect();
let service = Router::<()>::new()
.mount(kynos::routes![list_widgets, delete_widget])
.intercept(
Cors::new()
.allow_origins(permitted)
.allow_origins_matching(|origin| origin.ends_with(".internal.example.com"))
.allow_headers(["x-trace-id"])
.expose_headers(["x-request-id"])
.max_age(std::time::Duration::from_secs(600)),
)
.build(())?;
preflight(&service).await;
real_request(&service).await;
a_predicate_origin(&service).await;
refused_origin(&service).await;
a_configuration_that_cannot_be_honoured();
Ok(())
}
async fn preflight(service: &Service<()>) {
let response = send(
service,
Method::OPTIONS,
&[
("origin", "https://app.example.com"),
("access-control-request-method", "DELETE"),
("access-control-request-headers", "x-trace-id"),
],
)
.await;
println!("preflight -> {}", response.status());
assert_eq!(response.status(), StatusCode::NO_CONTENT);
show(&response, &header::ACCESS_CONTROL_ALLOW_METHODS);
show(&response, &header::ACCESS_CONTROL_ALLOW_HEADERS);
show(&response, &header::ACCESS_CONTROL_MAX_AGE);
show(&response, &header::VARY);
}
async fn real_request(service: &Service<()>) {
let response = send(
service,
Method::GET,
&[("origin", "https://app.example.com")],
)
.await;
println!("\nreal request -> {}", response.status());
show(&response, &header::ACCESS_CONTROL_ALLOW_ORIGIN);
show(&response, &header::ACCESS_CONTROL_EXPOSE_HEADERS);
show(&response, &header::VARY);
}
async fn a_predicate_origin(service: &Service<()>) {
let response = send(
service,
Method::GET,
&[("origin", "https://team-b.internal.example.com")],
)
.await;
println!("\npredicate origin -> {}", response.status());
show(&response, &header::ACCESS_CONTROL_ALLOW_ORIGIN);
}
async fn refused_origin(service: &Service<()>) {
let response = send(
service,
Method::GET,
&[("origin", "https://evil.example.com")],
)
.await;
println!("\nrefused origin -> {}", response.status());
assert!(
response
.headers()
.get(header::ACCESS_CONTROL_ALLOW_ORIGIN)
.is_none()
);
println!(" access-control-allow-origin: <absent>");
}
fn a_configuration_that_cannot_be_honoured() {
let refused = Router::<()>::new()
.mount(kynos::routes![list_widgets])
.intercept(Cors::new().allow_any_origin().allow_credentials())
.build(());
println!(
"\nany origin + credentials -> {}",
match &refused {
Ok(_) => "accepted".to_owned(),
Err(error) => error.to_string(),
}
);
assert!(refused.is_err());
}
async fn send(service: &Service<()>, method: Method, fields: &[(&str, &str)]) -> Response {
let mut request = Request::new(kynos::http::body::Body::empty());
*request.method_mut() = method;
*request.uri_mut() = "/widgets".parse().expect("a usable path");
for (name, value) in fields {
request.headers_mut().insert(
header::HeaderName::from_bytes(name.as_bytes()).expect("a usable field name"),
kynos::http::HeaderValue::from_str(value).expect("a usable field value"),
);
}
service.call(request).await
}
fn show(response: &Response, name: &header::HeaderName) {
if let Some(value) = response.headers().get(name) {
println!(" {name}: {}", value.to_str().unwrap_or("<unprintable>"));
}
}