blitz_traits/
resource_policy.rs1use crate::net::Url;
4use crate::platform::OriginKey;
5use std::net::IpAddr;
6use std::sync::{Arc, Mutex};
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
9pub enum Credentials {
10 Omit,
11 #[default]
12 SameOrigin,
13 Include,
14}
15
16#[derive(Clone, Debug)]
18pub struct ResourcePolicy {
19 pub max_requests: u64,
20 pub warn_requests: u64,
21 pub max_bytes: u64,
22 pub warn_bytes: u64,
23 pub cross_origin_credentials: bool,
25}
26
27impl Default for ResourcePolicy {
28 fn default() -> Self {
29 Self {
30 max_requests: 4096,
31 warn_requests: 3072,
32 max_bytes: 256 * 1024 * 1024,
33 warn_bytes: 192 * 1024 * 1024,
34 cross_origin_credentials: false,
35 }
36 }
37}
38
39#[derive(Debug, Default)]
40struct Usage {
41 requests: u64,
42 bytes: u64,
43}
44
45#[derive(Debug)]
47pub struct ResourceScope {
48 url: Url,
49 origin: OriginKey,
50 policy: Arc<ResourcePolicy>,
51 usage: Arc<Mutex<Usage>>,
52 file_access: bool,
53 private_network: bool,
54}
55
56#[derive(Clone, Debug, Default)]
58pub enum Initiator {
59 #[default]
60 Unknown,
61 Browser(Arc<ResourceScope>),
62 Document(Arc<ResourceScope>),
63}
64
65impl ResourcePolicy {
66 pub fn browser(self: &Arc<Self>, url: Url) -> Initiator {
68 Initiator::Browser(Arc::new(ResourceScope {
69 origin: OriginKey::for_document(&url),
70 file_access: url.scheme() == "file",
71 private_network: true,
72 url,
73 policy: Arc::clone(self),
74 usage: Arc::new(Mutex::new(Usage::default())),
75 }))
76 }
77
78 pub fn document(self: &Arc<Self>, url: Url, navigation: &Initiator) -> Initiator {
81 let scope = navigation.scope();
82 let file_access = matches!(navigation, Initiator::Browser(_))
83 && scope.is_some_and(|scope| scope.file_access && scope.url == url);
84 let private_network = local_host(&url) && scope.is_some_and(|scope| scope.private_network);
85 Initiator::Document(Arc::new(ResourceScope {
86 origin: OriginKey::for_document(&url),
87 url,
88 policy: Arc::clone(self),
89 usage: scope.map_or_else(
90 || Arc::new(Mutex::new(Usage::default())),
91 |scope| Arc::clone(&scope.usage),
92 ),
93 file_access,
94 private_network,
95 }))
96 }
97}
98
99impl Initiator {
100 pub fn scope(&self) -> Option<&Arc<ResourceScope>> {
101 match self {
102 Self::Unknown => None,
103 Self::Browser(scope) | Self::Document(scope) => Some(scope),
104 }
105 }
106
107 pub fn document_url(&self) -> Option<&Url> {
108 self.scope().map(|scope| &scope.url)
109 }
110
111 pub fn origin(&self) -> Option<&OriginKey> {
112 self.scope().map(|scope| &scope.origin)
113 }
114
115 pub fn child(&self, url: Url) -> Self {
118 match self.scope() {
119 Some(scope) => scope.policy.document(url, self),
120 None => Self::Unknown,
121 }
122 }
123
124 pub fn check_url(&self, url: &Url) -> Result<(), String> {
125 let scope = self.scope().ok_or("resource-initiator-unknown")?;
126 if url.scheme() == "file" && !scope.file_access {
127 return Err("resource-file-origin".into());
128 }
129 if matches!(url.scheme(), "http" | "https") && local_host(url) && !scope.private_network {
130 return Err("resource-private-address".into());
131 }
132 Ok(())
133 }
134
135 pub fn check_address(&self, address: IpAddr) -> Result<(), String> {
137 let scope = self.scope().ok_or("resource-initiator-unknown")?;
138 if private_address(address) && !scope.private_network {
139 return Err("resource-private-address".into());
140 }
141 Ok(())
142 }
143
144 pub fn restrict_network(&self) -> bool {
145 self.scope().is_none_or(|scope| !scope.private_network)
146 }
147
148 pub fn allows_credentials(&self, url: &Url, mode: Credentials) -> bool {
149 let Some(scope) = self.scope() else {
150 return false;
151 };
152 mode != Credentials::Omit
153 && (scope.origin.is_persistable() && scope.origin == OriginKey::for_document(url)
154 || mode == Credentials::Include && scope.policy.cross_origin_credentials)
155 }
156
157 pub fn begin(&self, url: &Url) -> Result<(), String> {
158 let scope = self.scope().ok_or("resource-initiator-unknown")?;
159 let mut usage = scope
160 .usage
161 .lock()
162 .unwrap_or_else(|error| error.into_inner());
163 if usage.requests >= scope.policy.max_requests {
164 return Err("resource-request-cap".into());
165 }
166 usage.requests += 1;
167 if usage.requests == scope.policy.warn_requests {
168 eprintln!(
169 "resource-request-warning: {} ({})",
170 usage.requests, scope.url
171 );
172 }
173 drop(usage);
174 self.check_url(url)
175 }
176
177 pub fn charge(&self, bytes: usize) -> Result<(), String> {
180 let scope = self.scope().ok_or("resource-initiator-unknown")?;
181 let mut usage = scope
182 .usage
183 .lock()
184 .unwrap_or_else(|error| error.into_inner());
185 let bytes = bytes as u64;
186 if bytes > scope.policy.max_bytes.saturating_sub(usage.bytes) {
187 return Err("resource-byte-cap".into());
188 }
189 let previous = usage.bytes;
190 usage.bytes += bytes;
191 if previous < scope.policy.warn_bytes && usage.bytes >= scope.policy.warn_bytes {
192 eprintln!("resource-byte-warning: {} ({})", usage.bytes, scope.url);
193 }
194 Ok(())
195 }
196
197 pub fn remaining_bytes(&self) -> usize {
198 self.scope().map_or(0, |scope| {
199 let usage = scope
200 .usage
201 .lock()
202 .unwrap_or_else(|error| error.into_inner());
203 usize::try_from(scope.policy.max_bytes.saturating_sub(usage.bytes))
204 .unwrap_or(usize::MAX)
205 })
206 }
207}
208
209fn local_host(url: &Url) -> bool {
212 match url.host() {
213 Some(url::Host::Ipv4(ip)) => private_address(ip.into()),
214 Some(url::Host::Ipv6(ip)) => private_address(ip.into()),
215 Some(url::Host::Domain(host)) => {
216 let host = host.trim_end_matches('.').to_ascii_lowercase();
217 host == "localhost"
218 || host.ends_with(".localhost")
219 || host == "local"
220 || host.ends_with(".local")
221 }
222 None => false,
223 }
224}
225
226pub fn private_address(address: IpAddr) -> bool {
227 match address {
228 IpAddr::V4(ip) => {
229 let [a, b, _, _] = ip.octets();
230 a == 0
231 || a == 127
232 || a == 10
233 || a == 172 && (16..=31).contains(&b)
234 || a == 192 && b == 168
235 || a == 169 && b == 254
236 || a == 100 && (64..=127).contains(&b)
237 || a >= 224
238 }
239 IpAddr::V6(ip) => {
240 if let Some(v4) = ip.to_ipv4_mapped() {
241 return private_address(v4.into());
242 }
243 let octets = ip.octets();
244 ip.is_unspecified() || ip.is_loopback() || ip.is_multicast()
245 || octets[0] & 0xfe == 0xfc
246 || octets[0] == 0xfe && octets[1] & 0xc0 == 0x80
247 || ip.segments()[0] == 0x2002
249 || ip.segments()[0] == 0x2001 && ip.segments()[1] == 0
250 || ip.segments()[0] == 0x64 && ip.segments()[1] == 0xff9b
251 }
252 }
253}