refactor(svc): tidy the rust services and build tooling (#2735)

This commit is contained in:
Hampus
2026-09-13 17:38:32 +02:00
committed by GitHub
parent 33737e0f79
commit 6af33c7188
41 changed files with 863 additions and 1304 deletions
+1
View File
@@ -10,6 +10,7 @@ anyhow = "1.0.104"
axum = { version = "0.8.9", features = ["macros"] }
base64 = "0.22"
fluxer_common = { path = "../fluxer_common" }
futures-util = { version = "0.3.32", default-features = false, features = ["std"] }
hex = "0.4"
rand = "0.10"
reqwest = { version = "0.13.4", default-features = false, features = ["json", "rustls", "stream", "gzip", "brotli", "deflate"] }
+13 -20
View File
@@ -132,7 +132,7 @@ pub fn inject_bootstrap(
let media = media_endpoint.trim_end_matches('/');
let nonced = html.replace("{{CSP_NONCE_PLACEHOLDER}}", nonce);
let nonced = apply_static_preconnect(&nonced, static_cdn);
let nonced = apply_static_preconnect(nonced, static_cdn);
let nonced = nonced.replace("{{STATIC_CDN_ENDPOINT}}", static_cdn);
let nonced = apply_media_preconnect(&nonced, media, static_cdn);
@@ -143,20 +143,14 @@ pub fn inject_bootstrap(
return nonced.replace("{{FLUXER_BOOTSTRAP}}", script_tag);
}
if let Some(pos) = nonced.find("<head>") {
let insert_at = pos + "<head>".len();
let mut result = String::with_capacity(nonced.len() + script_tag.len() + 3);
result.push_str(&nonced[..insert_at]);
result.push_str("\n\t\t");
result.push_str(script_tag);
result.push_str(&nonced[insert_at..]);
return result;
}
if let Some(pos) = nonced.find("<head ")
&& let Some(close) = nonced[pos..].find('>')
{
let insert_at = pos + close + 1;
let insert_at = nonced
.find("<head>")
.map(|pos| pos + "<head>".len())
.or_else(|| {
let pos = nonced.find("<head ")?;
nonced[pos..].find('>').map(|close| pos + close + 1)
});
if let Some(insert_at) = insert_at {
let mut result = String::with_capacity(nonced.len() + script_tag.len() + 3);
result.push_str(&nonced[..insert_at]);
result.push_str("\n\t\t");
@@ -168,15 +162,14 @@ pub fn inject_bootstrap(
nonced
}
fn apply_static_preconnect(html: &str, static_cdn: &str) -> String {
fn apply_static_preconnect(mut html: String, static_cdn: &str) -> String {
if !static_cdn.is_empty() {
return html.to_owned();
return html;
}
let mut stripped = html.to_owned();
for tag in STATIC_PRECONNECT_TAGS {
stripped = stripped.replace(&format!("{tag}\n"), "").replace(tag, "");
html = html.replace(&format!("{tag}\n"), "").replace(tag, "");
}
stripped
html
}
fn apply_media_preconnect(html: &str, media: &str, static_cdn: &str) -> String {
+17 -36
View File
@@ -309,30 +309,20 @@ impl fmt::Display for CspReportUri {
}
}
fn warn_invalid(error: InvalidAppProxyEnvironmentError) {
fn warn_invalid(error: &InvalidAppProxyEnvironmentError) {
tracing::warn!(%error, "ignoring invalid app proxy environment value");
}
fn parse_optional_http_url(name: &'static str, value: Option<String>) -> Option<HttpUrl> {
let value = value?;
match HttpUrl::parse(name, &value) {
Ok(url) => Some(url),
Err(error) => {
warn_invalid(error);
None
}
}
HttpUrl::parse(name, &value).inspect_err(warn_invalid).ok()
}
fn parse_optional_http_endpoint(name: &'static str, value: Option<String>) -> Option<HttpEndpoint> {
let value = value?;
match HttpEndpoint::parse(name, &value) {
Ok(endpoint) => Some(endpoint),
Err(error) => {
warn_invalid(error);
None
}
}
HttpEndpoint::parse(name, &value)
.inspect_err(warn_invalid)
.ok()
}
fn parse_env_or_warn<T: std::str::FromStr>(name: &str, raw: &str, default: T) -> T {
@@ -434,25 +424,19 @@ fn read_csp_sources(name: &'static str) -> Vec<CspSource> {
.split([',', ' ', '\t', '\n'])
.map(str::trim)
.filter(|source| !source.is_empty())
.filter_map(|source| match CspSource::parse(name, source) {
Ok(source) => Some(source),
Err(error) => {
warn_invalid(error);
None
}
.filter_map(|source| {
CspSource::parse(name, source)
.inspect_err(warn_invalid)
.ok()
})
.collect()
}
fn read_csp_report_uri(name: &'static str) -> Option<CspReportUri> {
let value = cfg::non_empty_env(name)?;
match CspReportUri::parse(name, &value) {
Ok(report_uri) => Some(report_uri),
Err(error) => {
warn_invalid(error);
None
}
}
CspReportUri::parse(name, &value)
.inspect_err(warn_invalid)
.ok()
}
impl AppProxyConfig {
@@ -474,13 +458,10 @@ impl AppProxyConfig {
);
let s3_uploads_bucket = cfg::read_env("FLUXER_S3_BUCKET_UPLOADS", "fluxer-uploads");
let s3_uploads_endpoint = s3_public_endpoint.as_ref().and_then(|endpoint| {
match endpoint.with_host_prefix("FLUXER_S3_BUCKET_UPLOADS", s3_uploads_bucket.trim()) {
Ok(endpoint) => Some(endpoint),
Err(error) => {
warn_invalid(error);
None
}
}
endpoint
.with_host_prefix("FLUXER_S3_BUCKET_UPLOADS", s3_uploads_bucket.trim())
.inspect_err(warn_invalid)
.ok()
});
Self {
@@ -499,7 +480,7 @@ impl AppProxyConfig {
"FLUXER_STATIC_CDN_ENDPOINT",
cfg::non_empty_env("FLUXER_STATIC_CDN_ENDPOINT"),
),
s3_public_endpoint: s3_public_endpoint.clone(),
s3_public_endpoint,
s3_uploads_endpoint,
discovery_upstream_url: resolve_discovery_upstream_url_from_env(),
discovery_refresh_interval_ms: parse_env_or_warn(
+5 -9
View File
@@ -287,15 +287,11 @@ fn extend_runtime_s3_sources(target: &mut Vec<String>, runtime_sources: &Runtime
}
fn extend_from(target: &mut Vec<String>, extra: &[CspSource], defaults: &[&str]) {
for source in defaults {
if target.iter().any(|existing| existing == source) {
continue;
}
target.push((*source).to_owned());
}
for source in extra {
let source = source.as_str();
for source in defaults
.iter()
.copied()
.chain(extra.iter().map(CspSource::as_str))
{
if target.iter().any(|existing| existing == source) {
continue;
}
@@ -2,47 +2,33 @@
use axum::{
Json,
http::{HeaderValue, header},
http::header,
response::{IntoResponse, Response},
};
use serde_json::json;
pub async fn assetlinks() -> Response {
let body = json!([
{
let body = ["com.fluxer", "com.fluxer.canary"].map(|package_name| {
json!({
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.fluxer",
"package_name": package_name,
"sha256_cert_fingerprints": [
"91:E4:98:E1:B8:A6:C8:BA:99:41:5E:DB:29:78:29:6B:6C:58:BA:A5:E2:D2:A6:49:CE:C6:2D:A7:A8:29:C7:BC"
]
}
},
{
"relation": [
"delegate_permission/common.handle_all_urls",
"delegate_permission/common.get_login_creds"
],
"target": {
"namespace": "android_app",
"package_name": "com.fluxer.canary",
"sha256_cert_fingerprints": [
"91:E4:98:E1:B8:A6:C8:BA:99:41:5E:DB:29:78:29:6B:6C:58:BA:A5:E2:D2:A6:49:CE:C6:2D:A7:A8:29:C7:BC"
]
}
}
]);
})
});
let mut response = Json(body).into_response();
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=1800"),
);
response
(
[(header::CACHE_CONTROL, "public, max-age=1800")],
Json(body),
)
.into_response()
}
#[cfg(test)]
@@ -2,7 +2,7 @@
use axum::{
Json,
http::{HeaderValue, header},
http::header,
response::{IntoResponse, Response},
};
use serde_json::json;
@@ -19,10 +19,9 @@ pub async fn apple_app_site_association() -> Response {
}
});
let mut response = Json(body).into_response();
response.headers_mut().insert(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=1800"),
);
response
(
[(header::CACHE_CONTROL, "public, max-age=1800")],
Json(body),
)
.into_response()
}
+43 -25
View File
@@ -9,9 +9,7 @@ use axum::{
};
use std::path::{Path as FsPath, PathBuf};
use std::time::Duration;
use tokio::io::{AsyncWriteExt, DuplexStream};
use tokio::sync::{OwnedSemaphorePermit, TryAcquireError};
use tokio_util::io::ReaderStream;
use super::file_stream::stream_file;
use super::spa_static::{CORS_ALLOW_ANY_VALUE, asset_cache_control, guess_mime, is_font_mime};
@@ -19,7 +17,6 @@ use super::spa_static::{CORS_ALLOW_ANY_VALUE, asset_cache_control, guess_mime, i
const ASSET_REQUEST_TIMEOUT: Duration = Duration::from_secs(15);
const PRECOMPRESSED_VARIANTS: &[(&str, &str)] = &[("br", "br"), ("gzip", "gz")];
const MAX_ASSET_SIZE_BYTES: u64 = 100 * 1024 * 1024;
const UPSTREAM_ASSET_PUMP_BUFFER_BYTES: usize = 64 * 1024;
const UPSTREAM_FAILURE_CACHE_CONTROL: &str = "no-store";
const UPSTREAM_FAILURE_STRIPPED_HEADERS: &[&str] = &[
"cdn-cache-control",
@@ -148,38 +145,59 @@ pub async fn proxy_assets(
response_headers.insert(header::CONTENT_SECURITY_POLICY, state.csp.asset_header());
response_headers.remove("content-security-policy-report-only");
let body = Body::from_stream(upstream_asset_body(upstream_response, upstream_slot));
let body = upstream_asset_body(upstream_response, upstream_slot);
let mut response = Response::new(body);
*response.status_mut() = status;
*response.headers_mut() = response_headers;
response
}
struct UpstreamAssetReadState {
response: reqwest::Response,
_permit: OwnedSemaphorePermit,
remaining_bytes: u64,
}
fn upstream_asset_body(
mut upstream_response: reqwest::Response,
upstream_response: reqwest::Response,
upstream_slot: OwnedSemaphorePermit,
) -> ReaderStream<DuplexStream> {
let (writer, reader) = tokio::io::duplex(UPSTREAM_ASSET_PUMP_BUFFER_BYTES);
tokio::spawn(async move {
let _upstream_slot = upstream_slot;
let mut writer = writer;
loop {
match upstream_response.chunk().await {
Ok(Some(chunk)) => {
if writer.write_all(&chunk).await.is_err() {
return;
}
}
Ok(None) => break,
Err(err) => {
) -> Body {
let state = UpstreamAssetReadState {
response: upstream_response,
_permit: upstream_slot,
remaining_bytes: MAX_ASSET_SIZE_BYTES,
};
Body::from_stream(futures_util::stream::try_unfold(
state,
|mut state| async move {
let Some(chunk) = state
.response
.chunk()
.await
.inspect_err(|err| {
tracing::warn!(%err, "upstream asset body ended early");
return;
}
})
.map_err(axum::Error::new)?
else {
return Ok(None);
};
let chunk_bytes = chunk.len() as u64;
if chunk_bytes > state.remaining_bytes {
tracing::warn!(
maximum_bytes = MAX_ASSET_SIZE_BYTES,
remaining_bytes = state.remaining_bytes,
chunk_bytes,
"upstream asset body exceeds size cap"
);
return Err(axum::Error::new(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("upstream asset body exceeds {MAX_ASSET_SIZE_BYTES} bytes"),
)));
}
}
let _ = writer.shutdown().await;
});
ReaderStream::new(reader)
state.remaining_bytes -= chunk_bytes;
Ok(Some((chunk, state)))
},
))
}
pub(super) async fn serve_local_asset(
+7 -13
View File
@@ -13,7 +13,7 @@ use axum::{
Router,
extract::Request,
http::{HeaderName, HeaderValue, header},
middleware::{Next, from_fn, from_fn_with_state},
middleware::{Next, from_fn},
response::{IntoResponse, Response},
routing::get,
};
@@ -56,10 +56,7 @@ pub fn build_router(state: AppState) -> Router {
.fallback(get(spa_index::spa_catch_all))
.layer(from_fn(request_id_middleware))
.layer(from_fn(cache_headers_middleware))
.layer(from_fn_with_state(
state.clone(),
security_headers_middleware,
))
.layer(from_fn(security_headers_middleware))
.layer(
CompressionLayer::new()
.compress_when(DefaultPredicate::new().and(NotForContentType::const_new("font/"))),
@@ -68,14 +65,13 @@ pub fn build_router(state: AppState) -> Router {
.with_state(state)
}
async fn security_headers_middleware(
axum::extract::State(_state): axum::extract::State<AppState>,
request: Request,
next: Next,
) -> Response {
async fn security_headers_middleware(request: Request, next: Next) -> Response {
let mut response = next.run(request).await;
let headers = response.headers_mut();
set_security_headers(response.headers_mut());
response
}
fn set_security_headers(headers: &mut axum::http::HeaderMap) {
set_static_header(
headers,
header::STRICT_TRANSPORT_SECURITY,
@@ -89,8 +85,6 @@ async fn security_headers_middleware(
HeaderName::from_static("permissions-policy"),
PERMISSIONS_POLICY_VALUE,
);
response
}
async fn cache_headers_middleware(request: Request, next: Next) -> Response {
+1 -18
View File
@@ -410,19 +410,7 @@ fn build_spa_response(
} else {
headers.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
}
headers.insert(
header::STRICT_TRANSPORT_SECURITY,
HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"),
);
headers.insert(
header::X_CONTENT_TYPE_OPTIONS,
HeaderValue::from_static("nosniff"),
);
headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
headers.insert(
header::REFERRER_POLICY,
HeaderValue::from_static("strict-origin-when-cross-origin"),
);
super::set_security_headers(headers);
headers.insert(
axum::http::HeaderName::from_static("accept-ch"),
HeaderValue::from_static(ACCEPT_CH_VALUE),
@@ -431,11 +419,6 @@ fn build_spa_response(
axum::http::HeaderName::from_static("critical-ch"),
HeaderValue::from_static(CRITICAL_CH_VALUE),
);
headers.insert(
axum::http::HeaderName::from_static("permissions-policy"),
HeaderValue::from_static(super::PERMISSIONS_POLICY_VALUE),
);
#[cfg(feature = "time-freeze")]
{
if let Some(tf) = time_freeze_header