feat(server): custom HTTP headers for reverse-proxy gates (#1095) (#1156)

This commit is contained in:
cucadmuh
2026-06-22 16:25:28 +03:00
committed by GitHub
parent 2c9b2eeb46
commit 15cecb5d7d
83 changed files with 2452 additions and 327 deletions
+18 -3
View File
@@ -63,13 +63,15 @@ pub fn build_fanart_url(mbid: &str, api_key: &str, client_key: Option<&str>) ->
/// `Ok(None)` when the artist carries no MBID tag.
pub async fn fetch_artist_tag_mbid(
client: &Client,
registry: Option<&psysonic_core::server_http::ServerHttpRegistry>,
server_ref: Option<&str>,
rest_base: &str,
username: &str,
password: &str,
artist_id: &str,
) -> Result<Option<String>, String> {
let url = build_artist_info2_url(rest_base, username, password, artist_id);
let body = http_get_text(client, &url).await?;
let body = http_get_text_scoped(client, registry, server_ref, &url).await?;
let v: serde_json::Value = serde_json::from_str(&body).map_err(|e| e.to_string())?;
let mbid = v
.get("subsonic-response")
@@ -218,8 +220,21 @@ fn classify_mb_releases(v: &serde_json::Value) -> MbResolution {
}
/// Single GET → response text; any non-2xx is an error.
async fn http_get_text(client: &Client, url: &str) -> Result<String, String> {
let resp = client.get(url).send().await.map_err(|e| e.to_string())?;
async fn http_get_text_scoped(
client: &Client,
registry: Option<&psysonic_core::server_http::ServerHttpRegistry>,
server_ref: Option<&str>,
url: &str,
) -> Result<String, String> {
let resp = psysonic_core::server_http::apply_optional_registry_headers(
registry,
server_ref,
url,
client.get(url),
)
.send()
.await
.map_err(|e| e.to_string())?;
let status = resp.status();
if !status.is_success() {
return Err(format!("HTTP {status}"));
+7 -1
View File
@@ -242,12 +242,18 @@ pub(super) async fn try_external_fanart(
let _permit = fanart_sem.clone().acquire_owned().await.ok()?;
let http_registry = app
.try_state::<Arc<psysonic_core::server_http::ServerHttpRegistry>>()
.map(|s| Arc::clone(&*s));
// §23: resolve the tag MBID Rust-side via getArtistInfo2 — unless the cache
// already carries one (skip the Navidrome round-trip).
let (mbid, mbid_source) = match cached.as_ref().and_then(|r| r.mbid.clone()) {
Some(m) => (m, cached.as_ref().and_then(|r| r.mbid_source.clone())),
None => match external::fetch_artist_tag_mbid(
client,
http_registry.as_deref(),
Some(server_id),
&args.rest_base_url,
&args.username,
&args.password,
@@ -344,7 +350,7 @@ pub(super) async fn try_external_fanart(
}
};
let bytes = match fetch::fetch_cover_bytes(client, &img_url).await {
let bytes = match fetch::fetch_cover_bytes(client, &img_url, None, None).await {
Ok(b) => b,
Err(e) => {
eprintln!("[fanart] download failed: {e}"); // transient — don't cache
+23 -4
View File
@@ -1,4 +1,5 @@
use reqwest::Client;
use psysonic_core::server_http::ServerHttpRegistry;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use url::Url;
@@ -83,8 +84,21 @@ enum FetchAttempt {
Permanent(String),
}
async fn fetch_cover_once(client: &Client, url: &str) -> FetchAttempt {
let resp = match client.get(url).send().await {
async fn fetch_cover_once(
client: &Client,
url: &str,
registry: Option<&ServerHttpRegistry>,
server_ref: Option<&str>,
) -> FetchAttempt {
let mut req = client.get(url);
if let Some(reg) = registry {
if let Some(sid) = server_ref.filter(|s| !s.is_empty()) {
req = reg.apply_for_http_url(sid, url, req);
} else if let Some(ctx) = reg.get_for_server_url(url) {
req = psysonic_core::server_http::apply_server_headers_for_http_url(req, &ctx, url);
}
}
let resp = match req.send().await {
Ok(r) => r,
// Connection reset / timeout / DNS — transient under server load.
Err(e) => return FetchAttempt::Transient(e.to_string()),
@@ -104,10 +118,15 @@ async fn fetch_cover_once(client: &Client, url: &str) -> FetchAttempt {
}
}
pub async fn fetch_cover_bytes(client: &Client, url: &str) -> Result<Vec<u8>, String> {
pub async fn fetch_cover_bytes(
client: &Client,
url: &str,
registry: Option<&ServerHttpRegistry>,
server_ref: Option<&str>,
) -> Result<Vec<u8>, String> {
let mut last_err = String::from("cover fetch failed");
for attempt in 0..COVER_FETCH_ATTEMPTS {
match fetch_cover_once(client, url).await {
match fetch_cover_once(client, url, registry, server_ref).await {
FetchAttempt::Ok(bytes) => return Ok(bytes),
FetchAttempt::Permanent(e) => return Err(e),
FetchAttempt::Transient(e) => {
+12 -2
View File
@@ -354,7 +354,10 @@ impl CoverCacheState {
let source = if let Some(img) = load_image_from_disk(&dir) {
CoverSource::Image(img)
} else {
match download_cover_payload(&dir, &client, &http_sem, args).await {
let http_registry = app
.try_state::<Arc<psysonic_core::server_http::ServerHttpRegistry>>()
.map(|s| Arc::clone(&*s));
match download_cover_payload(&dir, &client, &http_sem, args, http_registry).await {
Ok(bytes) => CoverSource::Bytes(bytes),
Err(err) => {
log_cover_fetch_failure(app, args, &err);
@@ -532,6 +535,7 @@ async fn download_cover_payload(
client: &Client,
http_sem: &Semaphore,
args: &CoverCacheEnsureArgs,
registry: Option<Arc<psysonic_core::server_http::ServerHttpRegistry>>,
) -> Result<Vec<u8>, String> {
let _permit = http_sem
.acquire()
@@ -549,7 +553,13 @@ async fn download_cover_payload(
&args.cover_art_id,
fetch_size,
);
fetch::fetch_cover_bytes(client, &url).await
fetch::fetch_cover_bytes(
client,
&url,
registry.as_deref(),
Some(args.server_index_key.as_str()),
)
.await
}
fn spawn_derive_remaining_tiers(