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