Skip to content

Commit 0828632

Browse files
Not being able to interpolate a header value is no longer a fatal error
1 parent d7a132a commit 0828632

File tree

4 files changed

+92
-42
lines changed

4 files changed

+92
-42
lines changed

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,25 @@ cache-timeout = 43200
9595
warning-policy = "warn"
9696

9797
# Extra HTTP headers that must be send to certain web sites
98-
# in order to link check to succeed
98+
# in order to link check to succeed.
9999
#
100100
# This is a dictionary (map), with keys being regexes
101101
# matching a set of web sites, and values being an array of
102102
# the headers.
103-
[http-headers]
103+
[output.linkcheck.http-headers]
104104
# Any hyperlink that contains this regexp will be sent
105105
# the "Accept: text/html" header
106106
'crates\.io' = ["Accept: text/html"]
107107

108-
# mdbook-linkcheck will interpolate environment variables
109-
# into your header via $IDENT.
108+
# mdbook-linkcheck will interpolate environment variables into your header via
109+
# $IDENT.
110110
#
111-
# If this is not what you want
112-
# you must escape the `$` symbol, like `\$TOKEN`. `\` itself can also be escaped
113-
# via `\\`.
111+
# If this is not what you want you must escape the `$` symbol, like `\$TOKEN`.
112+
# `\` itself can also be escaped via `\\`.
113+
#
114+
# Note: If interpolation fails, the header will be skipped and the failure will
115+
# be logged. This can be useful if a particular header isn't always necessary,
116+
# but may be helpful (e.g. when working with rate limiting).
114117
'website\.com' = ["Authorization: Basic $TOKEN"]
115118
```
116119

src/config.rs

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
use crate::hashed_regex::HashedRegex;
22
use anyhow::Error;
33
use http::header::{HeaderName, HeaderValue};
4+
use log::Level;
45
use reqwest::Client;
56
use serde_derive::{Deserialize, Serialize};
67
use std::{
7-
collections::HashMap, convert::TryFrom, str::FromStr, time::Duration,
8+
collections::HashMap,
9+
convert::TryFrom,
10+
fmt::{self, Display, Formatter},
11+
str::FromStr,
12+
time::Duration,
813
};
914

1015
/// The configuration options available with this backend.
@@ -40,11 +45,18 @@ pub struct Config {
4045
pub struct HttpHeader {
4146
pub name: HeaderName,
4247
pub value: String,
48+
}
49+
50+
impl HttpHeader {
51+
pub(crate) fn interpolate(&self) -> Result<HeaderValue, Error> {
52+
interpolate_env(&self.value)
53+
}
54+
}
4355

44-
// This is a separate field because interpolated env vars
45-
// may contain some secrets that should not be revealed
46-
// in logs, config, error messages and the like.
47-
pub(crate) interpolated_value: HeaderValue,
56+
impl Display for HttpHeader {
57+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
58+
write!(f, "{}: {}", self.name, self.value)
59+
}
4860
}
4961

5062
impl Config {
@@ -67,6 +79,44 @@ impl Config {
6779
.insert(http::header::USER_AGENT, self.user_agent.parse().unwrap());
6880
Client::builder().default_headers(headers).build().unwrap()
6981
}
82+
83+
pub(crate) fn interpolate_headers(
84+
&self,
85+
warning_policy: WarningPolicy,
86+
) -> Vec<(HashedRegex, Vec<(HeaderName, HeaderValue)>)> {
87+
let mut all_headers = Vec::new();
88+
let log_level = warning_policy.to_log_level();
89+
90+
for (pattern, headers) in &self.http_headers {
91+
let mut interpolated = Vec::new();
92+
93+
for header in headers {
94+
match header.interpolate() {
95+
Ok(value) => {
96+
interpolated.push((header.name.clone(), value))
97+
},
98+
Err(e) => {
99+
// We don't want failed interpolation (i.e. due to a
100+
// missing env variable) to abort the whole
101+
// linkchecking, so emit a warning and keep going.
102+
//
103+
// If it was important, the user would notice a "broken"
104+
// link and read back through the logs.
105+
log::log!(
106+
log_level,
107+
"Unable to interpolate \"{}\" because {}",
108+
header,
109+
e
110+
);
111+
},
112+
}
113+
}
114+
115+
all_headers.push((pattern.clone(), interpolated));
116+
}
117+
118+
all_headers
119+
}
70120
}
71121

72122
impl Default for Config {
@@ -91,16 +141,14 @@ impl FromStr for HttpHeader {
91141
Some(idx) => {
92142
let name = s[..idx].parse()?;
93143
let value = s[idx + 2..].to_string();
94-
let interpolated_value = interpolate_env(&value)?;
95144
Ok(HttpHeader {
96145
name,
97146
value,
98-
interpolated_value,
99147
})
100148
},
101149

102150
None => Err(Error::msg(format!(
103-
"The `{}` HTTP header must contain `: ` but it doesn't",
151+
"The `{}` HTTP header must be in the form `key: value` but it isn't",
104152
s
105153
))),
106154
}
@@ -209,6 +257,16 @@ pub enum WarningPolicy {
209257
Error,
210258
}
211259

260+
impl WarningPolicy {
261+
pub(crate) fn to_log_level(self) -> Level {
262+
match self {
263+
WarningPolicy::Error => Level::Error,
264+
WarningPolicy::Warn => Level::Warn,
265+
WarningPolicy::Ignore => Level::Debug,
266+
}
267+
}
268+
}
269+
212270
impl Default for WarningPolicy {
213271
fn default() -> WarningPolicy { WarningPolicy::Warn }
214272
}
@@ -269,16 +327,14 @@ https = ["accept: html/text", "authorization: Basic $TOKEN"]
269327

270328
#[test]
271329
fn interpolation() {
272-
std::env::set_var("TOKEN", "QWxhZGRpbjpPcGVuU2VzYW1l");
273-
let should_be = HttpHeader {
330+
std::env::set_var("TOKEN", "abcdefg123456");
331+
let header = HttpHeader {
274332
name: "Authorization".parse().unwrap(),
275333
value: "Basic $TOKEN".into(),
276-
interpolated_value: "Basic QWxhZGRpbjpPcGVuU2VzYW1l"
277-
.parse()
278-
.unwrap(),
279334
};
335+
let should_be: HeaderValue = "Basic abcdefg123456".parse().unwrap();
280336

281-
let got = HttpHeader::try_from("Authorization: Basic $TOKEN").unwrap();
337+
let got = header.interpolate().unwrap();
282338

283339
assert_eq!(got, should_be);
284340
}

src/context.rs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::Config;
1+
use crate::{Config, HashedRegex};
22
use codespan::Files;
3-
use http::HeaderMap;
3+
use http::header::{HeaderMap, HeaderName, HeaderValue};
44
use linkcheck::{
55
validation::{Cache, Options},
66
Link,
@@ -20,6 +20,8 @@ pub struct Context<'a> {
2020
pub(crate) files: &'a Files<String>,
2121
pub(crate) client: Client,
2222
pub(crate) filesystem_options: Options,
23+
pub(crate) interpolated_headers:
24+
Vec<(HashedRegex, Vec<(HeaderName, HeaderValue)>)>,
2325
}
2426

2527
impl<'a> linkcheck::validation::Context for Context<'a> {
@@ -42,26 +44,12 @@ impl<'a> linkcheck::validation::Context for Context<'a> {
4244
let url = url.to_string();
4345
let mut headers = HeaderMap::new();
4446

45-
let extra_headers = self
46-
.cfg
47-
.http_headers
48-
.iter()
49-
.filter_map(|(re, extra)| {
50-
if re.find(&url).is_some() {
51-
Some(extra)
52-
} else {
53-
None
47+
for (pattern, matching_headers) in &self.interpolated_headers {
48+
if pattern.find(&url).is_some() {
49+
for (name, value) in matching_headers {
50+
headers.insert(name.clone(), value.clone());
5451
}
55-
})
56-
.flatten();
57-
58-
for header in extra_headers {
59-
let crate::config::HttpHeader {
60-
name,
61-
interpolated_value,
62-
..
63-
} = header;
64-
headers.insert(name.clone(), interpolated_value.clone());
52+
}
6553
}
6654

6755
headers

src/validate.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,16 @@ fn lc_validate(
4343
.set_default_file("README.md")
4444
.set_custom_validation(ensure_included_in_book(src_dir, file_names));
4545

46+
let interpolated_headers = cfg.interpolate_headers(cfg.warning_policy);
47+
4648
let ctx = Context {
4749
client: cfg.client(),
4850
filesystem_options: options,
4951
cfg,
5052
src_dir,
5153
cache: Mutex::new(cache.clone()),
5254
files,
55+
interpolated_headers,
5356
};
5457
let links = collate_links(links, src_dir, files);
5558

0 commit comments

Comments
 (0)