risingwave_meta_dashboard/
proxy.rs1use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17
18use anyhow::anyhow;
19use axum::Router;
20use axum::http::{HeaderMap, StatusCode, Uri, header};
21use axum::response::{IntoResponse, Response};
22use bytes::Bytes;
23use thiserror_ext::AsReport as _;
24use url::Url;
25
26#[derive(Clone)]
27pub struct CachedResponse {
28 code: StatusCode,
29 body: Bytes,
30 headers: HeaderMap,
31 uri: Url,
32}
33
34impl IntoResponse for CachedResponse {
35 fn into_response(self) -> Response {
36 let guess = mime_guess::from_path(self.uri.path());
37 let mut headers = HeaderMap::new();
38 if let Some(x) = self.headers.get(header::ETAG) {
39 headers.insert(header::ETAG, x.clone());
40 }
41 if let Some(x) = self.headers.get(header::CACHE_CONTROL) {
42 headers.insert(header::CACHE_CONTROL, x.clone());
43 }
44 if let Some(x) = self.headers.get(header::EXPIRES) {
45 headers.insert(header::EXPIRES, x.clone());
46 }
47 if let Some(x) = guess.first() {
48 if x.type_() == "image" && x.subtype() == "svg" {
49 headers.insert(header::CONTENT_TYPE, "image/svg+xml".parse().unwrap());
50 } else {
51 headers.insert(
52 header::CONTENT_TYPE,
53 format!("{}/{}", x.type_(), x.subtype()).parse().unwrap(),
54 );
55 }
56 }
57 (self.code, headers, self.body).into_response()
58 }
59}
60
61async fn proxy(
62 uri: Uri,
63 cache: Arc<Mutex<HashMap<String, CachedResponse>>>,
64) -> anyhow::Result<Response> {
65 let mut path = uri.path().to_owned();
66 if path.ends_with('/') {
67 path += "index.html";
68 }
69
70 if let Some(resp) = cache.lock().unwrap().get(&path) {
71 return Ok(resp.clone().into_response());
72 }
73
74 let url_str = format!(
75 "https://raw.githubusercontent.com/risingwavelabs/risingwave/dashboard-artifact{}",
76 path
77 );
78 let url = Url::parse(&url_str)?;
79 if url.to_string() != url_str {
80 return Err(anyhow!("normalized URL isn't the same as the original one"));
81 }
82
83 tracing::info!("dashboard service: proxying {}", url);
84
85 let content = reqwest::get(url.clone()).await?;
86
87 let resp = CachedResponse {
88 code: content.status(),
89 headers: content.headers().clone(),
90 body: content.bytes().await?,
91 uri: url,
92 };
93
94 cache.lock().unwrap().insert(path, resp.clone());
95
96 Ok(resp.into_response())
97}
98
99pub(crate) fn router() -> Router {
101 let cache = Arc::new(Mutex::new(HashMap::new()));
102
103 let handler = |uri| async move {
104 proxy(uri, cache.clone()).await.unwrap_or_else(|err| {
105 (
106 StatusCode::INTERNAL_SERVER_ERROR,
107 err.context("Unhandled internal error").to_report_string(),
108 )
109 .into_response()
110 })
111 };
112
113 Router::new().fallback(handler)
114}