net/protocols/
mod.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::hash_map::Entry;
6use std::future;
7use std::future::Future;
8use std::ops::Bound;
9use std::pin::Pin;
10
11use headers::Range;
12use http::StatusCode;
13use log::error;
14use net_traits::filemanager_thread::RelativePos;
15use net_traits::request::Request;
16use net_traits::response::Response;
17use net_traits::{DiscardFetch, NetworkError};
18use rustc_hash::FxHashMap;
19use servo_url::ServoUrl;
20use url::Position;
21
22use crate::fetch::methods::{DoneChannel, FetchContext, RangeRequestBounds, fetch};
23
24mod blob;
25mod data;
26mod file;
27
28use blob::BlobProtocolHander;
29use data::DataProtocolHander;
30use file::FileProtocolHander;
31
32type FutureResponse = Pin<Box<dyn Future<Output = Response> + Send>>;
33
34// The set of schemes that can't be registered.
35static FORBIDDEN_SCHEMES: [&str; 4] = ["http", "https", "chrome", "about"];
36
37pub trait ProtocolHandler: Send + Sync {
38    /// A list of schema-less URLs that can be resolved against this handler's
39    /// scheme. These URLs will be granted access to the `navigator.servo`
40    /// interface to perform privileged operations that manipulate Servo internals.
41    fn privileged_paths(&self) -> &'static [&'static str] {
42        &[]
43    }
44
45    /// Triggers the load of a resource for this protocol and returns a future
46    /// that will produce a Response. Even if the protocol is not backed by a
47    /// http endpoint, it is recommended to a least provide:
48    /// - A relevant status code.
49    /// - A Content Type.
50    fn load(
51        &self,
52        request: &mut Request,
53        done_chan: &mut DoneChannel,
54        context: &FetchContext,
55    ) -> FutureResponse;
56
57    /// Specify if resources served by that protocol can be retrieved
58    /// with `fetch()` without no-cors mode to allow the caller direct
59    /// access to the resource content.
60    fn is_fetchable(&self) -> bool {
61        false
62    }
63
64    /// Specify if this custom protocol can be used in a [secure context]
65    ///
66    /// Note: this only works for bypassing mixed content checks right now
67    ///
68    /// [secure context]: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
69    fn is_secure(&self) -> bool {
70        false
71    }
72}
73
74#[derive(Default)]
75pub struct ProtocolRegistry {
76    pub(crate) handlers: FxHashMap<String, Box<dyn ProtocolHandler>>, // Maps scheme -> handler
77}
78
79#[derive(Clone, Copy, Debug)]
80pub enum ProtocolRegisterError {
81    ForbiddenScheme,
82    SchemeAlreadyRegistered,
83}
84
85impl ProtocolRegistry {
86    pub fn with_internal_protocols() -> Self {
87        let mut registry = Self::default();
88        // We just created a new registry, and know that we aren't using
89        // any forbidden schemes, so this should never panic.
90        registry
91            .register("data", DataProtocolHander::default())
92            .expect("Infallible");
93        registry
94            .register("blob", BlobProtocolHander::default())
95            .expect("Infallible");
96        registry
97            .register("file", FileProtocolHander::default())
98            .expect("Infallible");
99        registry
100    }
101
102    /// Do not allow users to enter an arbitrary protocol as this can lead to slowdowns.
103    pub fn register(
104        &mut self,
105        scheme: &str,
106        handler: impl ProtocolHandler + 'static,
107    ) -> Result<(), ProtocolRegisterError> {
108        if FORBIDDEN_SCHEMES.contains(&scheme) {
109            error!("Protocol handler for '{scheme}' is not allowed to be registered.");
110            return Err(ProtocolRegisterError::ForbiddenScheme);
111        }
112
113        if let Entry::Vacant(entry) = self.handlers.entry(scheme.into()) {
114            entry.insert(Box::new(handler));
115            Ok(())
116        } else {
117            error!("Protocol handler for '{scheme}' is already registered.");
118            Err(ProtocolRegisterError::SchemeAlreadyRegistered)
119        }
120    }
121
122    pub fn register_page_content_handler(
123        &mut self,
124        scheme: String,
125        url: String,
126    ) -> Result<(), ProtocolRegisterError> {
127        self.register(
128            &scheme.clone(),
129            WebPageContentProtocolHandler { url, scheme },
130        )
131    }
132
133    pub fn get(&self, scheme: &str) -> Option<&dyn ProtocolHandler> {
134        self.handlers.get(scheme).map(|e| e.as_ref())
135    }
136
137    pub fn merge(&mut self, mut other: ProtocolRegistry) {
138        for (scheme, handler) in other.handlers.drain() {
139            if FORBIDDEN_SCHEMES.contains(&scheme.as_str()) {
140                error!("Protocol handler for '{scheme}' is not allowed to be registered.");
141                continue;
142            }
143
144            self.handlers.entry(scheme).or_insert(handler);
145        }
146    }
147
148    pub fn is_fetchable(&self, scheme: &str) -> bool {
149        self.handlers
150            .get(scheme)
151            .is_some_and(|handler| handler.is_fetchable())
152    }
153
154    pub fn is_secure(&self, scheme: &str) -> bool {
155        self.handlers
156            .get(scheme)
157            .is_some_and(|handler| handler.is_secure())
158    }
159
160    pub fn privileged_urls(&self) -> Vec<ServoUrl> {
161        self.handlers
162            .iter()
163            .flat_map(|(scheme, handler)| {
164                let paths = handler.privileged_paths();
165                paths
166                    .iter()
167                    .filter_map(move |path| ServoUrl::parse(&format!("{scheme}:{path}")).ok())
168            })
169            .collect()
170    }
171}
172
173struct WebPageContentProtocolHandler {
174    url: String,
175    scheme: String,
176}
177
178impl ProtocolHandler for WebPageContentProtocolHandler {
179    /// <https://html.spec.whatwg.org/multipage/#protocol-handler-invocation>
180    fn load(
181        &self,
182        request: &mut Request,
183        _done_chan: &mut DoneChannel,
184        context: &FetchContext,
185    ) -> FutureResponse {
186        let mut url = request.current_url();
187        // Step 1. Assert: inputURL's scheme is normalizedScheme.
188        assert!(url.scheme() == self.scheme);
189        // Step 2. Set the username given inputURL and the empty string.
190        //
191        // Ignore errors if no username can be set, which depending on the
192        // URL in the scheme handler might be bogus. For example, with a
193        // `mailto:` handler, it doesn't have a base. See the
194        // documentation at [`url::Url::set_username`]
195        let _ = url.set_username("");
196
197        // Step 3. Set the password given inputURL and the empty string.
198        //
199        // Ignore errors if no password can be set, which depending on the
200        // URL in the scheme handler might be bogus. For example, with a
201        // `mailto:` handler, it doesn't have a base. See the
202        // documentation at [`url::Url::set_password`]
203        let _ = url.set_password(None);
204
205        // Step 4. Let inputURLString be the serialization of inputURL.
206        // Step 5. Let encodedURL be the result of running UTF-8 percent-encode on inputURLString using the component percent-encode set.
207        //
208        // Url is already UTF-8, so encoding isn't required
209        let encoded_url = &url[Position::AfterScheme..][1..];
210        // Step 6. Let handlerURLString be normalizedURLString.
211        // Step 7. Replace the first instance of "%s" in handlerURLString with encodedURL.
212        let handler_url_string = self.url.replacen("%s", encoded_url, 1);
213        // Step 8. Let resultURL be the result of parsing handlerURLString.
214        let Ok(result_url) = ServoUrl::parse(&handler_url_string) else {
215            return Box::pin(future::ready(Response::network_error(
216                NetworkError::Internal("Failed to parse substituted protocol handler url".into()),
217            )));
218        };
219        // Ensure we did a proper substitution with a HTTP result
220        assert!(matches!(result_url.scheme(), "http" | "https"));
221        // Step 9. Navigate an appropriate navigable to resultURL.
222        request.url_list.push(result_url);
223        let request2 = request.clone();
224        let context2 = context.clone();
225        Box::pin(async move { fetch(request2, &mut DiscardFetch, &context2).await })
226    }
227}
228
229/// Test if the URL is potentially trustworthy or the custom protocol is registered as secure
230pub fn is_url_potentially_trustworthy(
231    protocol_registry: &ProtocolRegistry,
232    url: &ServoUrl,
233) -> bool {
234    url.is_potentially_trustworthy() || protocol_registry.is_secure(url.scheme())
235}
236
237pub fn range_not_satisfiable_error(response: &mut Response) {
238    response.status = StatusCode::RANGE_NOT_SATISFIABLE.into();
239}
240
241/// Get the range bounds if the `Range` header is present.
242pub fn get_range_request_bounds(range: Option<Range>, len: u64) -> RangeRequestBounds {
243    if let Some(ref range) = range {
244        let (start, end) = match range.satisfiable_ranges(len).next() {
245            Some((Bound::Included(start), Bound::Unbounded)) => (start, None),
246            Some((Bound::Included(start), Bound::Included(end))) => {
247                // `end` should be less or equal to `start`.
248                (start, Some(i64::max(start as i64, end as i64)))
249            },
250            Some((Bound::Unbounded, Bound::Included(offset))) => {
251                return RangeRequestBounds::Pending(offset);
252            },
253            _ => (0, None),
254        };
255        RangeRequestBounds::Final(RelativePos::from_opts(Some(start as i64), end))
256    } else {
257        RangeRequestBounds::Final(RelativePos::from_opts(Some(0), None))
258    }
259}
260
261pub fn partial_content(response: &mut Response) {
262    response.status = StatusCode::PARTIAL_CONTENT.into();
263}