mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(media-proxy): allow the desktop app origin and preflight (#3325)
This commit is contained in:
1 parent
0febad324c
commit
d0b4816a5f
7 files changed
+218
-6
No files matched your search
@@ -91,10 +91,12 @@ An operator can limit which web origins read media through CORS. `FLUXER_MEDIA_P
|
|||||||
| `report` | Every response is the same as under `off`, and each read `enforce` would refuse is logged |
|
| `report` | Every response is the same as under `off`, and each read `enforce` would refuse is logged |
|
||||||
| `enforce` | A read from an origin outside the allowlist returns 403 |
|
| `enforce` | A read from an origin outside the allowlist returns 403 |
|
||||||
|
|
||||||
Under `enforce`, an `Origin` is allowed when it equals the serialised form of an entry in `FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS` byte for byte. Anything else is refused, including `null`, a repeated `Origin` header, and a value that is not ASCII. A refused read returns 403 on every read path, and the Media Proxy reads no object and fetches no external URL for it. [Media Proxy settings](/operator/configuration/#media-proxy-settings) defines how an entry is parsed and serialised.
|
Under `enforce`, an `Origin` is allowed when it equals the serialised form of an entry in `FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS` byte for byte. The desktop app origin `fluxer-app://app` is always allowed and needs no entry. Anything else is refused, including `null`, a repeated `Origin` header, and a value that is not ASCII. A refused read returns 403 on every read path, and the Media Proxy reads no object and fetches no external URL for it. [Media Proxy settings](/operator/configuration/#media-proxy-settings) defines how an entry is parsed and serialised.
|
||||||
|
|
||||||
A read with no `Origin` is served without `Access-Control-Allow-Origin`. A browser sends no `Origin` for an `<img>` or a `<video>` without `crossorigin`, so those elements still load. An allowed read gets its own origin back in `Access-Control-Allow-Origin` on a media representation and on a 416. No error has CORS headers.
|
A read with no `Origin` is served without `Access-Control-Allow-Origin`. A browser sends no `Origin` for an `<img>` or a `<video>` without `crossorigin`, so those elements still load. An allowed read gets its own origin back in `Access-Control-Allow-Origin` on a media representation and on a 416. No error has CORS headers.
|
||||||
|
|
||||||
|
A preflight `OPTIONS` request from an allowed origin gets a 204 that allows `GET` and `HEAD` with the `Range` header. A preflight from a refused origin gets the same 403 as a read.
|
||||||
|
|
||||||
Under `enforce`, every response to a `GET` or `HEAD` on a read path has `Vary: Accept-Encoding, Origin`, the 403 for a refused `Origin` included.
|
Under `enforce`, every response to a `GET` or `HEAD` on a read path has `Vary: Accept-Encoding, Origin`, the 403 for a refused `Origin` included.
|
||||||
|
|
||||||
`report` changes no response. The Media Proxy logs a warning with the reason `cors_origin_would_deny` for each read `enforce` would refuse.
|
`report` changes no response. The Media Proxy logs a warning with the reason `cors_origin_would_deny` for each read `enforce` would refuse.
|
||||||
|
|||||||
@@ -1441,7 +1441,7 @@ Default `off`. The [cross-origin read policy](/media-proxy/overview/#cross-origi
|
|||||||
|
|
||||||
#### `FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS`
|
#### `FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS`
|
||||||
|
|
||||||
Default empty. Comma-separated origins that may read media through CORS, such as `https://web.fluxer.app`. Spaces around an entry and empty entries are ignored. Each entry is parsed as a URL and must be `http` or `https` with no credentials, query, fragment, or path beyond `/`. Its host must be an IPv4 address, a bracketed IPv6 address, or a domain name whose labels are ASCII letters, digits, and hyphens after parsing. So a wildcard such as `https://*.fluxer.app`, a brace pattern, an underscore, a trailing dot on a domain name, or any non-ASCII character fails. Write an internationalised domain name in its punycode form. Any failing entry makes the process exit. An entry is compared in the form a browser sends. `https://Web.Fluxer.App:443/` matches `https://web.fluxer.app`. Required when `FLUXER_MEDIA_PROXY_CORS_MODE` is `report` or `enforce`. When the policy is `report` or `enforce`, the Media Proxy logs the parsed list once at startup. When `FLUXER_MEDIA_PROXY_MODE` is `static` or `relay`, the Media Proxy ignores it.
|
Default empty. Comma-separated origins that may read media through CORS, such as `https://web.fluxer.app`. Spaces around an entry and empty entries are ignored. Each entry is parsed as a URL and must be `http` or `https` with no credentials, query, fragment, or path beyond `/`. Its host must be an IPv4 address, a bracketed IPv6 address, or a domain name whose labels are ASCII letters, digits, and hyphens after parsing. So a wildcard such as `https://*.fluxer.app`, a brace pattern, an underscore, a trailing dot on a domain name, or any non-ASCII character fails. Write an internationalised domain name in its punycode form. The one exception is the desktop app origin `fluxer-app://app`. It is accepted as an entry and is always allowed without one. Any failing entry makes the process exit. An entry is compared in the form a browser sends. `https://Web.Fluxer.App:443/` matches `https://web.fluxer.app`. Required when `FLUXER_MEDIA_PROXY_CORS_MODE` is `report` or `enforce`. When the policy is `report` or `enforce`, the Media Proxy logs the parsed list once at startup. When `FLUXER_MEDIA_PROXY_MODE` is `static` or `relay`, the Media Proxy ignores it.
|
||||||
|
|
||||||
Compose forwards it from `.env`, and defaults it to the public origin, built from `FLUXER_PUBLIC_ORIGIN`, or from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN`, and `FLUXER_PUBLIC_PORT`. That origin is the only entry, so the web app the instance serves reads its own media. The hosted web client and the desktop app run on `https://web.fluxer.app`. To let them read this instance's media through CORS, set the list in `.env` to the public origin and `https://web.fluxer.app`, such as `https://chat.example.com,https://web.fluxer.app`. A value set in `.env` replaces the Compose value, so it must name the public origin too.
|
Compose forwards it from `.env`, and defaults it to the public origin, built from `FLUXER_PUBLIC_ORIGIN`, or from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN`, and `FLUXER_PUBLIC_PORT`. That origin is the only entry, so the web app the instance serves reads its own media. The hosted web client and the desktop app run on `https://web.fluxer.app`. To let them read this instance's media through CORS, set the list in `.env` to the public origin and `https://web.fluxer.app`, such as `https://chat.example.com,https://web.fluxer.app`. A value set in `.env` replaces the Compose value, so it must name the public origin too.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use super::{BucketStyle, DeploymentMode, PolicyMode, StorageBackend};
|
use super::{BucketStyle, DeploymentMode, PolicyMode, StorageBackend};
|
||||||
use crate::secret::SecretBytes;
|
use crate::{constants::DESKTOP_APP_ORIGIN, secret::SecretBytes};
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
use http::HeaderValue;
|
use http::HeaderValue;
|
||||||
|
|
||||||
@@ -121,6 +121,9 @@ fn parse_origin(entry: &str) -> Option<HeaderValue> {
|
|||||||
if !entry.is_ascii() {
|
if !entry.is_ascii() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
if entry == DESKTOP_APP_ORIGIN {
|
||||||
|
return Some(HeaderValue::from_static(DESKTOP_APP_ORIGIN));
|
||||||
|
}
|
||||||
let url = url::Url::parse(entry).ok()?;
|
let url = url::Url::parse(entry).ok()?;
|
||||||
let bare = matches!(url.scheme(), "http" | "https")
|
let bare = matches!(url.scheme(), "http" | "https")
|
||||||
&& url.host().is_some_and(is_origin_host)
|
&& url.host().is_some_and(is_origin_host)
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ fn allowed_origins_are_normalised_to_their_browser_serialisation() {
|
|||||||
("https://web.fluxer.app/", vec!["https://web.fluxer.app"]),
|
("https://web.fluxer.app/", vec!["https://web.fluxer.app"]),
|
||||||
("https://web.fluxer.app:443", vec!["https://web.fluxer.app"]),
|
("https://web.fluxer.app:443", vec!["https://web.fluxer.app"]),
|
||||||
("http://localhost:8088", vec!["http://localhost:8088"]),
|
("http://localhost:8088", vec!["http://localhost:8088"]),
|
||||||
|
("fluxer-app://app", vec!["fluxer-app://app"]),
|
||||||
|
(
|
||||||
|
"https://web.fluxer.app,fluxer-app://app,fluxer-app://app",
|
||||||
|
vec!["https://web.fluxer.app", "fluxer-app://app"],
|
||||||
|
),
|
||||||
("http://127.0.0.1:8088", vec!["http://127.0.0.1:8088"]),
|
("http://127.0.0.1:8088", vec!["http://127.0.0.1:8088"]),
|
||||||
("http://[::1]:8088", vec!["http://[::1]:8088"]),
|
("http://[::1]:8088", vec!["http://[::1]:8088"]),
|
||||||
(
|
(
|
||||||
@@ -94,7 +99,11 @@ fn rejects_an_allowed_origin_that_is_not_a_bare_http_origin() {
|
|||||||
"web.fluxer.app",
|
"web.fluxer.app",
|
||||||
"ftp://web.fluxer.app",
|
"ftp://web.fluxer.app",
|
||||||
"file:///tmp/x",
|
"file:///tmp/x",
|
||||||
"fluxer-app://app",
|
"fluxer-app://app/",
|
||||||
|
"fluxer-app://evil",
|
||||||
|
"fluxer-app://app:443",
|
||||||
|
"FLUXER-APP://APP",
|
||||||
|
"other-app://app",
|
||||||
"https://[email protected]",
|
"https://[email protected]",
|
||||||
"https://user:[email protected]",
|
"https://user:[email protected]",
|
||||||
"https://web.fluxer.app/app",
|
"https://web.fluxer.app/app",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
pub const MAX_MEDIA_PROXY_BYTES: usize = 500 * 1024 * 1024;
|
pub const MAX_MEDIA_PROXY_BYTES: usize = 500 * 1024 * 1024;
|
||||||
|
pub const DESKTOP_APP_ORIGIN: &str = "fluxer-app://app";
|
||||||
|
|
||||||
pub const OUTBOUND_USER_AGENT: &str =
|
pub const OUTBOUND_USER_AGENT: &str =
|
||||||
"Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)";
|
"Mozilla/5.0 (compatible; Fluxerbot/1.0; +https://fluxer.app)";
|
||||||
|
|||||||
@@ -1,12 +1,22 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::{request_log, server::response::error::text_with_source};
|
use crate::{
|
||||||
|
constants::DESKTOP_APP_ORIGIN, http_headers, request_log,
|
||||||
|
server::response::error::text_with_source,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
body::Body,
|
||||||
http::{HeaderMap, HeaderValue, Method, StatusCode, header},
|
http::{HeaderMap, HeaderValue, Method, StatusCode, header},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
|
|
||||||
const VARY_ON_ORIGIN: &str = "Accept-Encoding, Origin";
|
const VARY_ON_ORIGIN: &str = "Accept-Encoding, Origin";
|
||||||
|
const VARY_ON_PREFLIGHT: &str =
|
||||||
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers";
|
||||||
|
const PREFLIGHT_ALLOW_METHODS: &str = "GET, HEAD, OPTIONS";
|
||||||
|
const PREFLIGHT_ALLOW_HEADERS: &str = "Range";
|
||||||
|
const PREFLIGHT_MAX_AGE_SECONDS: &str = "600";
|
||||||
|
const ANY_ORIGIN: &str = "*";
|
||||||
const ORIGIN_LOG_BYTES_MAX: usize = 256;
|
const ORIGIN_LOG_BYTES_MAX: usize = 256;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
@@ -24,6 +34,9 @@ pub(in crate::server) fn check_origin(allowed: &[HeaderValue], request: &HeaderM
|
|||||||
if origins.next().is_some() {
|
if origins.next().is_some() {
|
||||||
return OriginCheck::Refused;
|
return OriginCheck::Refused;
|
||||||
}
|
}
|
||||||
|
if origin.as_bytes() == DESKTOP_APP_ORIGIN.as_bytes() {
|
||||||
|
return OriginCheck::Allowed(HeaderValue::from_static(DESKTOP_APP_ORIGIN));
|
||||||
|
}
|
||||||
allowed
|
allowed
|
||||||
.iter()
|
.iter()
|
||||||
.find(|candidate| candidate.as_bytes() == origin.as_bytes())
|
.find(|candidate| candidate.as_bytes() == origin.as_bytes())
|
||||||
@@ -45,6 +58,38 @@ pub(in crate::server) fn refused_response(request: &HeaderMap) -> Response {
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(in crate::server) fn is_preflight(method: &Method, request: &HeaderMap) -> bool {
|
||||||
|
method == Method::OPTIONS
|
||||||
|
&& request.contains_key(header::ORIGIN)
|
||||||
|
&& request.contains_key(header::ACCESS_CONTROL_REQUEST_METHOD)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::server) fn preflight_response(check: &OriginCheck) -> Response {
|
||||||
|
let mut response = Response::new(Body::empty());
|
||||||
|
*response.status_mut() = StatusCode::NO_CONTENT;
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
http_headers::add_security_headers(headers);
|
||||||
|
let allow_origin = match check {
|
||||||
|
OriginCheck::Allowed(origin) => origin.clone(),
|
||||||
|
OriginCheck::Absent | OriginCheck::Refused => HeaderValue::from_static(ANY_ORIGIN),
|
||||||
|
};
|
||||||
|
headers.insert(header::ACCESS_CONTROL_ALLOW_ORIGIN, allow_origin);
|
||||||
|
headers.insert(
|
||||||
|
header::ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
|
HeaderValue::from_static(PREFLIGHT_ALLOW_METHODS),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
header::ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
|
HeaderValue::from_static(PREFLIGHT_ALLOW_HEADERS),
|
||||||
|
);
|
||||||
|
headers.insert(
|
||||||
|
header::ACCESS_CONTROL_MAX_AGE,
|
||||||
|
HeaderValue::from_static(PREFLIGHT_MAX_AGE_SECONDS),
|
||||||
|
);
|
||||||
|
headers.insert(header::VARY, HeaderValue::from_static(VARY_ON_PREFLIGHT));
|
||||||
|
response
|
||||||
|
}
|
||||||
|
|
||||||
pub(in crate::server) fn log_would_refuse(method: &Method, path: &str, request: &HeaderMap) {
|
pub(in crate::server) fn log_would_refuse(method: &Method, path: &str, request: &HeaderMap) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
reason = "cors_origin_would_deny",
|
reason = "cors_origin_would_deny",
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ pub(in crate::server) async fn catch_all(
|
|||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let method = request.method().clone();
|
let method = request.method().clone();
|
||||||
|
if app.cfg.mode != DeploymentMode::Relay && cors::is_preflight(&method, request.headers()) {
|
||||||
|
return preflight(&app, request.uri().path(), request.headers());
|
||||||
|
}
|
||||||
if method != Method::GET && method != Method::HEAD {
|
if method != Method::GET && method != Method::HEAD {
|
||||||
return text(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed");
|
return text(StatusCode::METHOD_NOT_ALLOWED, "Method Not Allowed");
|
||||||
}
|
}
|
||||||
@@ -89,6 +92,21 @@ pub(in crate::server) async fn catch_all(
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn preflight(app: &AppState, path: &str, headers: &HeaderMap) -> Response {
|
||||||
|
if app.cfg.cors.mode == PolicyMode::Off {
|
||||||
|
return cors::preflight_response(&OriginCheck::Absent);
|
||||||
|
}
|
||||||
|
let check = cors::check_origin(&app.cfg.cors.allowed_origins, headers);
|
||||||
|
if check != OriginCheck::Refused {
|
||||||
|
return cors::preflight_response(&check);
|
||||||
|
}
|
||||||
|
if app.cfg.cors.mode == PolicyMode::Enforce {
|
||||||
|
return cors::refused_response(headers);
|
||||||
|
}
|
||||||
|
cors::log_would_refuse(&Method::OPTIONS, path, headers);
|
||||||
|
cors::preflight_response(&check)
|
||||||
|
}
|
||||||
|
|
||||||
async fn serve_public_read(app: &Arc<AppState>, read: PublicRead<'_>) -> Response {
|
async fn serve_public_read(app: &Arc<AppState>, read: PublicRead<'_>) -> Response {
|
||||||
let PublicRead {
|
let PublicRead {
|
||||||
method,
|
method,
|
||||||
@@ -189,7 +207,7 @@ fn without_signature_parameters(params: &HashMap<String, String>) -> HashMap<Str
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::Config;
|
use crate::{config::Config, constants::DESKTOP_APP_ORIGIN};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::to_bytes,
|
body::to_bytes,
|
||||||
http::{HeaderValue, header},
|
http::{HeaderValue, header},
|
||||||
@@ -852,6 +870,140 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn preflight_request(path: &str, origin: &str, requested: Option<&str>) -> Request<Body> {
|
||||||
|
let mut builder = Request::builder()
|
||||||
|
.method(Method::OPTIONS)
|
||||||
|
.uri(path)
|
||||||
|
.header(header::ORIGIN, origin);
|
||||||
|
if let Some(requested) = requested {
|
||||||
|
builder = builder.header(header::ACCESS_CONTROL_REQUEST_METHOD, requested);
|
||||||
|
}
|
||||||
|
builder.body(Body::empty()).expect("preflight request")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn enforce_serves_the_desktop_app_origin_without_configuring_it() {
|
||||||
|
let tmp = tempfile::tempdir().expect("storage root");
|
||||||
|
let root = tmp.path().canonicalize().expect("canonical storage root");
|
||||||
|
let root = root.as_path();
|
||||||
|
write_stored_representations(root);
|
||||||
|
let app = dispatch_app("mp", root, ENFORCE);
|
||||||
|
for method in [Method::GET, Method::HEAD] {
|
||||||
|
let response = dispatch_request(
|
||||||
|
&app,
|
||||||
|
read_request(
|
||||||
|
method.clone(),
|
||||||
|
THEME_PATH,
|
||||||
|
&[DESKTOP_APP_ORIGIN.as_bytes()],
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(StatusCode::OK, response.status(), "{method}");
|
||||||
|
assert_eq!(
|
||||||
|
Some(DESKTOP_APP_ORIGIN.to_owned()),
|
||||||
|
header_value(&response, header::ACCESS_CONTROL_ALLOW_ORIGIN),
|
||||||
|
"{method}"
|
||||||
|
);
|
||||||
|
assert_eq!(vec![ORIGIN_VARY.to_owned()], vary_values(&response));
|
||||||
|
}
|
||||||
|
for lookalike in [
|
||||||
|
"fluxer-app://app/",
|
||||||
|
"fluxer-app://evil",
|
||||||
|
"fluxer-app://app.evil.example",
|
||||||
|
"fluxer-app://app:443",
|
||||||
|
"FLUXER-APP://APP",
|
||||||
|
"fluxer-app:app",
|
||||||
|
] {
|
||||||
|
let response = dispatch_request(
|
||||||
|
&app,
|
||||||
|
read_request(Method::GET, THEME_PATH, &[lookalike.as_bytes()], None),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(StatusCode::FORBIDDEN, response.status(), "{lookalike}");
|
||||||
|
assert_eq!(
|
||||||
|
None,
|
||||||
|
header_value(&response, header::ACCESS_CONTROL_ALLOW_ORIGIN),
|
||||||
|
"{lookalike}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn preflight_answers_allowed_origins_and_refuses_the_rest() {
|
||||||
|
let tmp = tempfile::tempdir().expect("storage root");
|
||||||
|
let root = tmp.path().canonicalize().expect("canonical storage root");
|
||||||
|
let root = root.as_path();
|
||||||
|
let enforce = dispatch_app("mp", root, ENFORCE);
|
||||||
|
for origin in [WEB, DESKTOP_APP_ORIGIN] {
|
||||||
|
for requested in ["GET", "HEAD"] {
|
||||||
|
let response = dispatch_request(
|
||||||
|
&enforce,
|
||||||
|
preflight_request(THEME_PATH, origin, Some(requested)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let label = format!("{origin} {requested}");
|
||||||
|
assert_eq!(StatusCode::NO_CONTENT, response.status(), "{label}");
|
||||||
|
assert_eq!(
|
||||||
|
Some(origin.to_owned()),
|
||||||
|
header_value(&response, header::ACCESS_CONTROL_ALLOW_ORIGIN),
|
||||||
|
"{label}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Some("GET, HEAD, OPTIONS".to_owned()),
|
||||||
|
header_value(&response, header::ACCESS_CONTROL_ALLOW_METHODS),
|
||||||
|
"{label}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Some("Range".to_owned()),
|
||||||
|
header_value(&response, header::ACCESS_CONTROL_ALLOW_HEADERS),
|
||||||
|
"{label}"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
"Origin, Access-Control-Request-Method, Access-Control-Request-Headers"
|
||||||
|
.to_owned()
|
||||||
|
],
|
||||||
|
vary_values(&response),
|
||||||
|
"{label}"
|
||||||
|
);
|
||||||
|
assert!(body_of(response).await.is_empty(), "{label}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let refused =
|
||||||
|
dispatch_request(&enforce, preflight_request(THEME_PATH, EVIL, Some("GET"))).await;
|
||||||
|
assert_eq!(StatusCode::FORBIDDEN, refused.status());
|
||||||
|
assert_eq!(
|
||||||
|
None,
|
||||||
|
header_value(&refused, header::ACCESS_CONTROL_ALLOW_ORIGIN)
|
||||||
|
);
|
||||||
|
|
||||||
|
let not_a_preflight =
|
||||||
|
dispatch_request(&enforce, preflight_request(THEME_PATH, WEB, None)).await;
|
||||||
|
assert_eq!(StatusCode::METHOD_NOT_ALLOWED, not_a_preflight.status());
|
||||||
|
|
||||||
|
for (mode, extra) in [("mp", &[][..]), ("mp", REPORT), ("static", &[][..])] {
|
||||||
|
let app = dispatch_app(mode, root, extra);
|
||||||
|
let response =
|
||||||
|
dispatch_request(&app, preflight_request(THEME_PATH, EVIL, Some("GET"))).await;
|
||||||
|
assert_eq!(StatusCode::NO_CONTENT, response.status(), "{mode}");
|
||||||
|
assert_eq!(
|
||||||
|
Some("*".to_owned()),
|
||||||
|
header_value(&response, header::ACCESS_CONTROL_ALLOW_ORIGIN),
|
||||||
|
"{mode}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let relay = dispatch_app("relay", root, &[]);
|
||||||
|
assert_eq!(
|
||||||
|
StatusCode::METHOD_NOT_ALLOWED,
|
||||||
|
dispatch_request(&relay, preflight_request(THEME_PATH, WEB, Some("GET")))
|
||||||
|
.await
|
||||||
|
.status()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn enforce_applies_to_upload_mode_reads() {
|
async fn enforce_applies_to_upload_mode_reads() {
|
||||||
let tmp = tempfile::tempdir().expect("storage root");
|
let tmp = tempfile::tempdir().expect("storage root");
|
||||||
|
|||||||
Reference in new issue
Block a user