use super::*;
pub(crate) fn capable(app: &App, headers: &HeaderMap) -> bool {
headers
.get(CAPABILITY_HEADER)
.and_then(|v| v.to_str().ok())
.is_some_and(|c| app.capabilities.verify(c.trim()))
}
pub(crate) const WINDOW_HEADER: &str = "x-snyvi-window";
pub(crate) fn window_secret_ok(app: &App, headers: &HeaderMap) -> bool {
headers
.get(WINDOW_HEADER)
.and_then(|v| v.to_str().ok())
.is_some_and(|s| constant_eq(s.trim(), &app.window))
}
pub(crate) fn windowed(app: &App, headers: &HeaderMap) -> bool {
capable(app, headers) || window_secret_ok(app, headers)
}
pub(crate) fn not_windowed() -> Response {
(
StatusCode::UNAUTHORIZED,
Json(json!({ "error": "not the window, and no window secret" })),
)
.into_response()
}
pub(crate) fn refuse_reader(app: &App, headers: &HeaderMap) -> Option<Response> {
(!from_this_page(headers) && !authorized(app, headers)).then(|| {
(
StatusCode::FORBIDDEN,
Json(json!({ "error": "not from this page, and no token" })),
)
.into_response()
})
}
pub(crate) async fn mint_capability(State(app): S, headers: HeaderMap) -> Response {
if !window_secret_ok(&app, &headers) {
return not_windowed();
}
match app.capabilities.mint() {
Ok(capability) => Json(json!({ "capability": capability })).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": e.to_string() })),
)
.into_response(),
}
}
pub(crate) fn from_this_page(headers: &HeaderMap) -> bool {
if let Some(site) = headers.get("sec-fetch-site").and_then(|v| v.to_str().ok()) {
if site != "same-origin" {
return false;
}
}
let Some(origin) = headers.get(header::ORIGIN).and_then(|v| v.to_str().ok()) else {
return false;
};
let port = config::port();
["127.0.0.1", "localhost", "[::1]"]
.iter()
.any(|h| origin == format!("http://{h}:{port}"))
}
pub(crate) fn same_origin_read(headers: &HeaderMap) -> bool {
if headers.contains_key(header::ORIGIN) {
return false;
}
if let Some(site) = headers.get("sec-fetch-site").and_then(|v| v.to_str().ok()) {
if site != "same-origin" {
return false;
}
}
let Some(host) = headers.get(header::HOST).and_then(|v| v.to_str().ok()) else {
return false;
};
let port = config::port();
["127.0.0.1", "localhost", "[::1]"]
.iter()
.any(|h| host == format!("{h}:{port}"))
}
pub(crate) const NOT_THIS_HOST: &str = "not this host";
pub(crate) const NOT_THIS_ORIGIN: &str = "not this origin";
pub(crate) fn not_this_host(
headers: &HeaderMap,
method: &axum::http::Method,
) -> Option<&'static str> {
let port = config::port();
let ours = |given: &str, scheme: &str| {
let given = given.trim().to_ascii_lowercase();
["127.0.0.1", "localhost", "[::1]"]
.iter()
.any(|h| given == format!("{scheme}{h}:{port}"))
};
match headers.get(header::HOST).and_then(|v| v.to_str().ok()) {
Some(h) if ours(h, "") => {}
_ => return Some(NOT_THIS_HOST),
}
if let Some(o) = headers.get(header::ORIGIN).and_then(|v| v.to_str().ok()) {
if !ours(o, "http://") {
return Some(NOT_THIS_ORIGIN);
}
}
if *method != axum::http::Method::GET && *method != axum::http::Method::HEAD {
if let Some(site) = headers.get("sec-fetch-site").and_then(|v| v.to_str().ok()) {
if site != "same-origin" && site != "none" {
return Some(NOT_THIS_ORIGIN);
}
}
}
None
}
pub(crate) async fn host_gate(
req: axum::extract::Request,
next: axum::middleware::Next,
) -> Response {
if let Some(why) = not_this_host(req.headers(), req.method()) {
return (StatusCode::FORBIDDEN, Json(json!({ "error": why }))).into_response();
}
next.run(req).await
}
pub(crate) const CAPABILITY_HEADER: &str = "x-snyvi-capability";
pub(crate) fn desk_refusal(
caps: &crate::capability::Capabilities,
headers: &HeaderMap,
q: &std::collections::HashMap<String, String>,
) -> Option<&'static str> {
if q.contains_key(crate::desktop::CAPABILITY_KEY) || q.contains_key("capability") {
return Some("the capability is not a query parameter");
}
if !from_this_page(headers) && !same_origin_read(headers) {
return Some("not from this page");
}
let given = headers
.get(CAPABILITY_HEADER)
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
(!caps.verify(given)).then_some("no capability")
}
pub(crate) fn refuse_desk(
app: &App,
headers: &HeaderMap,
q: &std::collections::HashMap<String, String>,
) -> Option<Response> {
desk_refusal(&app.capabilities, headers, q)
.map(|why| (StatusCode::FORBIDDEN, Json(json!({ "error": why }))).into_response())
}
pub(crate) fn authorized(app: &App, headers: &HeaderMap) -> bool {
let bearer = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(str::trim);
let alt = headers
.get("x-snyvi-token")
.and_then(|v| v.to_str().ok())
.map(str::trim);
bearer
.or(alt)
.map(|t| constant_eq(t, &app.token.read().unwrap()))
.unwrap_or(false)
}