diff --git a/src/handlers/icons.rs b/src/handlers/icons.rs new file mode 100644 index 00000000..92b49043 --- /dev/null +++ b/src/handlers/icons.rs @@ -0,0 +1,63 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::Response, +}; +use std::sync::Arc; +use worker::Env; + +use crate::error::AppError; + +#[worker::send] +pub async fn get_icon( + State(_env): State>, + Path(path): Path, +) -> Result { + let domain = path + .strip_suffix("/icon.png") + .unwrap_or(&path) + .to_string(); + + let target_url = format!("https://vault.bitwarden.com/icons/{}/icon.png", domain); + + let request = worker::Request::new(&target_url, worker::Method::Get) + .map_err(|_| AppError::Internal)?; + + let mut upstream_response = worker::Fetch::Request(request) + .send() + .await + .map_err(|e| { + log::error!("Failed to fetch icon from Bitwarden: {:?}", e); + AppError::Internal + })?; + + let status = upstream_response.status_code(); + + let body_bytes = upstream_response + .bytes() + .await + .map_err(|e| { + log::error!("Failed to read icon response body: {:?}", e); + AppError::Internal + })?; + + let mut response = Response::new(axum::body::Body::from(body_bytes)); + *response.status_mut() = StatusCode::from_u16(status) + .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); + + if let Ok(Some(content_type)) = upstream_response.headers().get("content-type") { + if let Ok(header_value) = axum::http::HeaderValue::from_str(&content_type) { + response.headers_mut().insert( + axum::http::header::CONTENT_TYPE, + header_value, + ); + } + } + + response.headers_mut().insert( + axum::http::header::CACHE_CONTROL, + axum::http::HeaderValue::from_static("public, max-age=604800, immutable"), + ); + + Ok(response) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 151de1d2..5a32140d 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -9,3 +9,4 @@ pub mod two_factor; pub mod devices; pub mod sends; pub mod usage; +pub mod icons; diff --git a/src/router.rs b/src/router.rs index fcd56a56..89e9d892 100644 --- a/src/router.rs +++ b/src/router.rs @@ -7,13 +7,14 @@ use axum::extract::DefaultBodyLimit; use std::sync::Arc; use worker::Env; -use crate::handlers::{accounts, ciphers, config, identity, sync, folders, import, two_factor, devices, sends, usage}; +use crate::handlers::{accounts, ciphers, config, identity, sync, folders, import, two_factor, devices, sends, usage, icons}; pub fn api_router(env: Env) -> Router { let app_state = Arc::new(env); Router::new() .route("/demo.html", get(|| async { Html(include_str!("../static/demo.html")) })) + .route("/icons/{*path}", get(icons::get_icon)) // Identity/Auth routes .route("/identity/accounts/prelogin", post(accounts::prelogin)) .route("/api/accounts/prelogin", post(accounts::prelogin)) diff --git a/wrangler.jsonc b/wrangler.jsonc index 54183c1a..095bafbf 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -30,6 +30,6 @@ "assets": { "directory": "./static/web-vault", "not_found_handling": "single-page-application", - "run_worker_first": ["/api/*", "/identity/*", "/sends/*", "/demo.html"] + "run_worker_first": ["/api/*", "/identity/*", "/sends/*", "/demo.html", "/icons/*"] } }