harness_web/
config-validate.rs1use std::net::IpAddr;
10use std::time::Duration;
11
12use ipnet::IpNet;
13use reqwest::header::HeaderValue;
14
15use super::{HostAddressException, MAX_REDIRECTS_CEILING, MaxRedirects, UserAgent, canonical_host};
16
17#[derive(Debug, thiserror::Error)]
25#[error(transparent)]
26pub struct ConfigError(#[from] ConfigErrorRepr);
27
28#[derive(Debug, thiserror::Error)]
30pub(super) enum ConfigErrorRepr {
31 #[error("user agent is not a valid http header value")]
33 UserAgent(#[source] reqwest::header::InvalidHeaderValue),
34
35 #[error("{field} must be greater than zero")]
37 ZeroLimit {
38 field: &'static str,
40 },
41
42 #[error("{field} ({value}) exceeds the maximum of {ceiling}")]
44 OverCeiling {
45 field: &'static str,
47 value: usize,
49 ceiling: usize,
51 },
52
53 #[error("{field} must be a positive duration")]
55 ZeroTimeout {
56 field: &'static str,
58 },
59
60 #[error("{field} ({value:?}) exceeds the maximum of {ceiling:?}")]
62 TimeoutOverCeiling {
63 field: &'static str,
65 value: Duration,
67 ceiling: Duration,
69 },
70
71 #[error("invalid deny cidr {cidr}")]
73 Cidr {
74 cidr: String,
76 #[source]
78 source: ipnet::AddrParseError,
79 },
80
81 #[error("invalid exact host {host:?}")]
83 Host {
84 host: String,
86 },
87
88 #[error("http client construction failed")]
90 ClientBuild(#[source] reqwest::Error),
91}
92
93impl ConfigError {
94 pub(crate) fn client_build(source: reqwest::Error) -> ConfigError {
96 ConfigError(ConfigErrorRepr::ClientBuild(source))
97 }
98}
99
100pub(super) fn validate_user_agent(ua: String) -> Result<UserAgent, ConfigErrorRepr> {
102 HeaderValue::from_str(&ua).map_err(ConfigErrorRepr::UserAgent)?;
103 Ok(UserAgent(ua))
104}
105
106pub(super) fn validate_limit(
108 field: &'static str,
109 value: usize,
110 ceiling: usize,
111) -> Result<usize, ConfigErrorRepr> {
112 if value == 0 {
113 return Err(ConfigErrorRepr::ZeroLimit { field });
114 }
115 if value > ceiling {
116 return Err(ConfigErrorRepr::OverCeiling {
117 field,
118 value,
119 ceiling,
120 });
121 }
122 Ok(value)
123}
124
125pub(super) fn validate_redirects(value: usize) -> Result<MaxRedirects, ConfigErrorRepr> {
127 if value > MAX_REDIRECTS_CEILING {
128 return Err(ConfigErrorRepr::OverCeiling {
129 field: "max_redirects",
130 value,
131 ceiling: MAX_REDIRECTS_CEILING,
132 });
133 }
134 Ok(MaxRedirects(value))
135}
136
137pub(super) fn validate_timeout(
139 field: &'static str,
140 value: Duration,
141 ceiling: Duration,
142) -> Result<Duration, ConfigErrorRepr> {
143 if value.is_zero() {
144 return Err(ConfigErrorRepr::ZeroTimeout { field });
145 }
146 if value > ceiling {
147 return Err(ConfigErrorRepr::TimeoutOverCeiling {
148 field,
149 value,
150 ceiling,
151 });
152 }
153 Ok(value)
154}
155
156pub(super) fn validate_deny_cidrs(cidrs: Vec<String>) -> Result<Vec<IpNet>, ConfigErrorRepr> {
158 let mut nets = Vec::with_capacity(cidrs.len());
159 for cidr in cidrs {
160 let net = cidr
161 .parse::<IpNet>()
162 .map_err(|source| ConfigErrorRepr::Cidr {
163 cidr: cidr.clone(),
164 source,
165 })?;
166 if !nets.contains(&net) {
167 nets.push(net);
168 }
169 }
170 Ok(nets)
171}
172
173fn validate_host(raw: &str) -> Result<String, ConfigErrorRepr> {
180 let host = canonical_host(raw);
181 if host.is_empty() {
182 return Err(ConfigErrorRepr::Host {
183 host: raw.to_string(),
184 });
185 }
186 if host.parse::<IpAddr>().is_ok() {
188 return Ok(host);
189 }
190 match url::Host::parse(&host) {
195 Ok(url::Host::Domain(domain)) => Ok(domain),
196 _ => Err(ConfigErrorRepr::Host {
197 host: raw.to_string(),
198 }),
199 }
200}
201
202pub(super) fn validate_allow_hosts(
204 hosts: Vec<(String, IpAddr)>,
205) -> Result<Vec<HostAddressException>, ConfigErrorRepr> {
206 let mut out: Vec<HostAddressException> = Vec::with_capacity(hosts.len());
207 for (raw, addr) in hosts {
208 let host = validate_host(&raw)?;
209 let entry = HostAddressException { host, addr };
210 if !out.contains(&entry) {
211 out.push(entry);
212 }
213 }
214 Ok(out)
215}