Skip to content

Commit a70943e

Browse files
committed
kk
1 parent e6a52a6 commit a70943e

4 files changed

Lines changed: 93 additions & 54 deletions

File tree

‎crates/bin/docs_rs_web/src/config.rs‎

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use anyhow::Result;
1+
use anyhow::{Context as _, Result, ensure};
22
use docs_rs_config::AppConfig;
33
use docs_rs_env_vars::maybe_env;
44
use docs_rs_types::{ByteSize, Duration};
5+
use url::Url;
56

67
#[derive(Debug, bon::Builder)]
78
#[builder(on(_, overwritable))]
@@ -39,10 +40,9 @@ pub struct Config {
3940
#[builder(default)]
4041
pub(crate) csp_report_only: bool,
4142

42-
/// Datadog graph embed URLs generated for a 400 × 200 viewport.
43-
/// Unset URLs hide the corresponding graphs on the build queue page.
44-
pub(crate) build_queue_length_graph_url: Option<String>,
45-
pub(crate) build_queue_wait_time_graph_url: Option<String>,
43+
/// Graph embed URLs, displayed in order. An empty list hides the metrics column.
44+
#[builder(default)]
45+
pub(crate) build_queue_graph_urls: Vec<Url>,
4646

4747
// Cache-Control header, for versioned URLs.
4848
// If both are absent, don't generate the header. If only one is present,
@@ -60,18 +60,19 @@ use config_builder::State;
6060

6161
impl<S: State> ConfigBuilder<S> {
6262
pub(crate) fn load_environment(self) -> Result<ConfigBuilder<S>> {
63+
let graph_urls = maybe_env::<String>("DOCSRS_BUILD_QUEUE_GRAPH_URLS")?
64+
.map(|value| parse_graph_urls(&value))
65+
.transpose()?
66+
.unwrap_or_default();
6367
Ok(self
68+
.build_queue_graph_urls(graph_urls)
6469
.maybe_cratesio_token(maybe_env("DOCSRS_CRATESIO_TOKEN")?)
6570
.maybe_max_parse_memory(maybe_env("DOCSRS_MAX_PARSE_MEMORY")?)
6671
.maybe_render_threads(maybe_env("DOCSRS_RENDER_THREADS")?)
6772
.maybe_request_timeout(maybe_env("DOCSRS_REQUEST_TIMEOUT")?)
6873
.maybe_report_request_timeouts(maybe_env("DOCSRS_REPORT_REQUEST_TIMEOUTS")?)
6974
.maybe_random_crate_search_view_size(maybe_env("DOCSRS_RANDOM_CRATE_SEARCH_VIEW_SIZE")?)
7075
.maybe_csp_report_only(maybe_env("DOCSRS_CSP_REPORT_ONLY")?)
71-
.maybe_build_queue_length_graph_url(maybe_env("DOCSRS_BUILD_QUEUE_LENGTH_GRAPH_URL")?)
72-
.maybe_build_queue_wait_time_graph_url(maybe_env(
73-
"DOCSRS_BUILD_QUEUE_WAIT_TIME_GRAPH_URL",
74-
)?)
7576
.maybe_cache_control_stale_while_revalidate(maybe_env(
7677
"CACHE_CONTROL_STALE_WHILE_REVALIDATE",
7778
)?)
@@ -101,3 +102,36 @@ impl AppConfig for Config {
101102
Ok(Self::builder().test_config()?.build())
102103
}
103104
}
105+
106+
fn parse_graph_urls(value: &str) -> Result<Vec<Url>> {
107+
value
108+
.split_whitespace()
109+
.map(|value| {
110+
let url = Url::parse(value).context("invalid URL in DOCSRS_BUILD_QUEUE_GRAPH_URLS")?;
111+
ensure!(
112+
matches!(url.scheme(), "http" | "https") && url.host_str().is_some(),
113+
"DOCSRS_BUILD_QUEUE_GRAPH_URLS requires HTTP(S) URLs"
114+
);
115+
Ok(url)
116+
})
117+
.collect()
118+
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
124+
#[test]
125+
fn graph_urls_are_validated_and_preserve_order() {
126+
let urls = parse_graph_urls(
127+
"https://app.datadoghq.com/graph/embed?token=a&legend=true\nhttps://example.com/graph?token=b",
128+
)
129+
.unwrap();
130+
assert_eq!(urls.len(), 2);
131+
assert_eq!(urls[0].query(), Some("token=a&legend=true"));
132+
assert_eq!(urls[1].host_str(), Some("example.com"));
133+
assert!(parse_graph_urls(" ").unwrap().is_empty());
134+
assert!(parse_graph_urls("not-a-url").is_err());
135+
assert!(parse_graph_urls("javascript:alert(1)").is_err());
136+
}
137+
}

‎crates/bin/docs_rs_web/src/handlers/releases.rs‎

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -768,8 +768,7 @@ struct BuildQueuePage {
768768
in_progress_builds: Vec<InProgressBuild>,
769769
expand_rebuild_queue: bool,
770770
show_length_warning: bool,
771-
queue_length_graph_url: Option<String>,
772-
queue_wait_time_graph_url: Option<String>,
771+
graph_urls: Vec<url::Url>,
773772
}
774773

775774
impl_axum_webpage! { BuildQueuePage }
@@ -852,10 +851,8 @@ pub(crate) async fn build_queue_handler(
852851
}
853852
});
854853

855-
if config.build_queue_length_graph_url.is_some()
856-
|| config.build_queue_wait_time_graph_url.is_some()
857-
{
858-
csp.allow_datadog_frame();
854+
for url in &config.build_queue_graph_urls {
855+
csp.allow_frame(url);
859856
}
860857

861858
Ok(BuildQueuePage {
@@ -865,8 +862,7 @@ pub(crate) async fn build_queue_handler(
865862
in_progress_builds,
866863
expand_rebuild_queue: params.expand.is_some(),
867864
show_length_warning,
868-
queue_length_graph_url: config.build_queue_length_graph_url.clone(),
869-
queue_wait_time_graph_url: config.build_queue_wait_time_graph_url.clone(),
865+
graph_urls: config.build_queue_graph_urls.clone(),
870866
})
871867
}
872868

@@ -895,35 +891,39 @@ mod tests {
895891
use std::str::FromStr;
896892
use test_case::test_case;
897893

898-
#[test_case(false, false)]
899-
#[test_case(true, false)]
900-
#[test_case(false, true)]
901-
#[test_case(true, true)]
902-
fn test_build_queue_optional_graphs(length: bool, wait_time: bool) {
903-
let url = "https://app.datadoghq.com/graph/embed?token=test&legend=true";
894+
#[test_case(0)]
895+
#[test_case(1)]
896+
#[test_case(3)]
897+
fn test_build_queue_optional_graphs(count: usize) {
898+
let urls: Vec<url::Url> = (0..count)
899+
.map(|index| {
900+
format!("https://app.datadoghq.com/graph/embed?token={index}&legend=true")
901+
.parse()
902+
.unwrap()
903+
})
904+
.collect();
904905
let mut page = BuildQueuePage {
905906
description: "test",
906907
queue: Vec::new(),
907908
rebuild_queue: Vec::new(),
908909
in_progress_builds: Vec::new(),
909910
expand_rebuild_queue: false,
910911
show_length_warning: false,
911-
queue_length_graph_url: length.then(|| url.to_owned()),
912-
queue_wait_time_graph_url: wait_time.then(|| url.to_owned()),
912+
graph_urls: urls.clone(),
913913
};
914914
let html =
915915
kuchikiki::parse_html().one(page.render_with_csp_nonce("test-nonce".into()).unwrap());
916916
let frames: Vec<_> = html
917917
.select(".build-queue-metrics iframe")
918918
.unwrap()
919919
.collect();
920-
assert_eq!(frames.len(), usize::from(length) + usize::from(wait_time));
921-
for frame in frames {
922-
assert_eq!(frame.attributes.borrow().get("src"), Some(url));
920+
assert_eq!(frames.len(), count);
921+
for (frame, url) in frames.iter().zip(&urls) {
922+
assert_eq!(frame.attributes.borrow().get("src"), Some(url.as_str()));
923923
}
924924
assert_eq!(
925925
html.select(".build-queue-layout").unwrap().count(),
926-
usize::from(length || wait_time)
926+
usize::from(count > 0)
927927
);
928928
}
929929

‎crates/bin/docs_rs_web/src/middleware/csp.rs‎

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ use axum::{
66
};
77
use base64::{Engine, engine::general_purpose::STANDARD as b64};
88
use std::{
9+
collections::BTreeSet,
910
fmt::Write,
1011
sync::{
11-
Arc,
12+
Arc, Mutex,
1213
atomic::{AtomicBool, Ordering},
1314
},
1415
};
1516

1617
pub(crate) struct Csp {
1718
nonce: String,
1819
suppress: AtomicBool,
19-
allow_datadog_frame: AtomicBool,
20+
frame_origins: Mutex<BTreeSet<String>>,
2021
}
2122

2223
impl Csp {
@@ -29,7 +30,7 @@ impl Csp {
2930
Self {
3031
nonce: b64.encode(random),
3132
suppress: AtomicBool::new(false),
32-
allow_datadog_frame: AtomicBool::new(false),
33+
frame_origins: Mutex::new(BTreeSet::new()),
3334
}
3435
}
3536

@@ -41,8 +42,13 @@ impl Csp {
4142
&self.nonce
4243
}
4344

44-
pub(crate) fn allow_datadog_frame(&self) {
45-
self.allow_datadog_frame.store(true, Ordering::Relaxed);
45+
pub(crate) fn allow_frame(&self, url: &url::Url) {
46+
if matches!(url.scheme(), "http" | "https") {
47+
self.frame_origins
48+
.lock()
49+
.unwrap()
50+
.insert(url.origin().ascii_serialization());
51+
}
4652
}
4753

4854
fn render(&self, content_type: ContentType) -> Option<String> {
@@ -74,8 +80,12 @@ impl Csp {
7480
}
7581

7682
fn render_html(&self, result: &mut String) {
77-
if self.allow_datadog_frame.load(Ordering::Relaxed) {
78-
result.push_str("; frame-src https://app.datadoghq.com");
83+
let origins = self.frame_origins.lock().unwrap();
84+
if !origins.is_empty() {
85+
result.push_str("; frame-src");
86+
for origin in origins.iter() {
87+
write!(result, " {origin}").unwrap();
88+
}
7989
}
8090

8191
// Allow loading any CSS file from the current origin.
@@ -200,15 +210,20 @@ mod tests {
200210
}
201211

202212
#[test]
203-
fn test_csp_datadog_frame() {
213+
fn test_csp_frame_origins() {
204214
let csp = Csp::new();
205215
assert!(!csp.render(ContentType::Html).unwrap().contains("frame-src"));
206216

207-
csp.allow_datadog_frame();
217+
let url = "https://app.datadoghq.com/graph/embed?token=test"
218+
.parse()
219+
.unwrap();
220+
csp.allow_frame(&url);
221+
csp.allow_frame(&url);
222+
csp.allow_frame(&"https://example.com:8443/graph".parse().unwrap());
208223
assert!(
209224
csp.render(ContentType::Html)
210225
.unwrap()
211-
.contains("; frame-src https://app.datadoghq.com;")
226+
.contains("; frame-src https://app.datadoghq.com https://example.com:8443;")
212227
);
213228
assert!(!csp.render(ContentType::Svg).unwrap().contains("frame-src"));
214229
assert!(

‎crates/bin/docs_rs_web/templates/releases/build_queue.html‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</div>
2828
{%- endif %}
2929

30-
<div class="{% if queue_length_graph_url.is_some() || queue_wait_time_graph_url.is_some() %}build-queue-layout{% endif %}">
30+
<div class="{% if !graph_urls.is_empty() %}build-queue-layout{% endif %}">
3131
<div class="recent-releases-container">
3232
<div class="release">
3333
<strong>Currently being built</strong>
@@ -128,28 +128,18 @@
128128
</ol>
129129
{%- endif -%}
130130
</div>
131-
{% if queue_length_graph_url.is_some() || queue_wait_time_graph_url.is_some() %}
131+
{% if !graph_urls.is_empty() %}
132132
<aside class="build-queue-metrics" aria-label="Build metrics">
133-
{% if let Some(url) = queue_length_graph_url %}
133+
{% for url in graph_urls %}
134134
<iframe
135135
class="build-metrics-graph"
136136
src="{{ url }}"
137-
title="Queue length graph"
137+
title="Build metrics graph {{ loop.index }}"
138138
width="400"
139139
height="200"
140140
frameborder="0"
141141
></iframe>
142-
{% endif %}
143-
{% if let Some(url) = queue_wait_time_graph_url %}
144-
<iframe
145-
class="build-metrics-graph"
146-
src="{{ url }}"
147-
title="Queue waiting time graph"
148-
width="400"
149-
height="200"
150-
frameborder="0"
151-
></iframe>
152-
{% endif %}
142+
{% endfor %}
153143
</aside>
154144
{% endif %}
155145
</div>

0 commit comments

Comments
 (0)