script/dom/servoparser/
prefetch.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::cell::{Cell, RefCell};
6use std::ops::Deref;
7
8use base::id::{PipelineId, WebViewId};
9use html5ever::buffer_queue::BufferQueue;
10use html5ever::tokenizer::states::RawKind;
11use html5ever::tokenizer::{
12    Tag, TagKind, Token, TokenSink, TokenSinkResult, Tokenizer as HtmlTokenizer,
13};
14use html5ever::{Attribute, LocalName, local_name};
15use js::jsapi::JSTracer;
16use markup5ever::TokenizerResult;
17use net_traits::policy_container::PolicyContainer;
18use net_traits::request::{
19    CorsSettings, CredentialsMode, Destination, InsecureRequestsPolicy, ParserMetadata, Referrer,
20};
21use net_traits::{CoreResourceMsg, FetchChannels, IpcSend, ReferrerPolicy, ResourceThreads};
22use servo_url::{ImmutableOrigin, ServoUrl};
23
24use crate::dom::bindings::reflector::DomGlobal;
25use crate::dom::bindings::trace::{CustomTraceable, JSTraceable};
26use crate::dom::document::{Document, determine_policy_for_token};
27use crate::dom::html::htmlscriptelement::script_fetch_request;
28use crate::fetch::create_a_potential_cors_request;
29use crate::script_module::ScriptFetchOptions;
30
31#[derive(JSTraceable, MallocSizeOf)]
32#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
33pub(crate) struct Tokenizer {
34    #[ignore_malloc_size_of = "Defined in html5ever"]
35    inner: TraceableTokenizer,
36}
37
38struct TraceableTokenizer(HtmlTokenizer<PrefetchSink>);
39
40impl Deref for TraceableTokenizer {
41    type Target = HtmlTokenizer<PrefetchSink>;
42    fn deref(&self) -> &Self::Target {
43        &self.0
44    }
45}
46
47#[allow(unsafe_code)]
48unsafe impl JSTraceable for TraceableTokenizer {
49    unsafe fn trace(&self, trc: *mut JSTracer) {
50        CustomTraceable::trace(&self.0, trc)
51    }
52}
53
54#[allow(unsafe_code)]
55unsafe impl CustomTraceable for PrefetchSink {
56    unsafe fn trace(&self, trc: *mut JSTracer) {
57        <Self as JSTraceable>::trace(self, trc)
58    }
59}
60
61impl Tokenizer {
62    pub(crate) fn new(document: &Document) -> Self {
63        let global = document.global();
64        let sink = PrefetchSink {
65            origin: document.origin().immutable().clone(),
66            pipeline_id: global.pipeline_id(),
67            webview_id: document.webview_id(),
68            base_url: RefCell::new(None),
69            document_url: document.url(),
70            referrer: global.get_referrer(),
71            referrer_policy: document.get_referrer_policy(),
72            resource_threads: document.loader().resource_threads().clone(),
73            // Initially we set prefetching to false, and only set it
74            // true after the first script tag, since that is what will
75            // block the main parser.
76            prefetching: Cell::new(false),
77            insecure_requests_policy: document.insecure_requests_policy(),
78            has_trustworthy_ancestor_origin: document.has_trustworthy_ancestor_or_current_origin(),
79            policy_container: global.policy_container(),
80        };
81        let options = Default::default();
82        let inner = TraceableTokenizer(HtmlTokenizer::new(sink, options));
83        Tokenizer { inner }
84    }
85
86    pub(crate) fn feed(&self, input: &BufferQueue) {
87        while let TokenizerResult::Script(PrefetchHandle) = self.inner.feed(input) {}
88    }
89}
90
91#[derive(JSTraceable)]
92struct PrefetchSink {
93    #[no_trace]
94    origin: ImmutableOrigin,
95    #[no_trace]
96    pipeline_id: PipelineId,
97    #[no_trace]
98    webview_id: WebViewId,
99    #[no_trace]
100    document_url: ServoUrl,
101    #[no_trace]
102    base_url: RefCell<Option<ServoUrl>>,
103    #[no_trace]
104    referrer: Referrer,
105    #[no_trace]
106    referrer_policy: ReferrerPolicy,
107    #[no_trace]
108    resource_threads: ResourceThreads,
109    prefetching: Cell<bool>,
110    #[no_trace]
111    insecure_requests_policy: InsecureRequestsPolicy,
112    has_trustworthy_ancestor_origin: bool,
113    #[no_trace]
114    policy_container: PolicyContainer,
115}
116
117/// The prefetch tokenizer produces trivial results
118#[derive(Clone, Copy, JSTraceable)]
119struct PrefetchHandle;
120
121impl TokenSink for PrefetchSink {
122    type Handle = PrefetchHandle;
123    fn process_token(&self, token: Token, _line_number: u64) -> TokenSinkResult<PrefetchHandle> {
124        let tag = match token {
125            Token::TagToken(ref tag) => tag,
126            _ => return TokenSinkResult::Continue,
127        };
128        match (tag.kind, &tag.name) {
129            (TagKind::StartTag, &local_name!("script")) if self.prefetching.get() => {
130                if let Some(url) = self.get_url(tag, local_name!("src")) {
131                    debug!("Prefetch script {}", url);
132                    let cors_setting = self.get_cors_settings(tag, local_name!("crossorigin"));
133                    let integrity_metadata = self
134                        .get_attr(tag, local_name!("integrity"))
135                        .map(|attr| String::from(&attr.value))
136                        .unwrap_or_default();
137                    let cryptographic_nonce = self
138                        .get_attr(tag, local_name!("nonce"))
139                        .map(|attr| String::from(&attr.value))
140                        .unwrap_or_default();
141                    let request = script_fetch_request(
142                        self.webview_id,
143                        url,
144                        cors_setting,
145                        self.origin.clone(),
146                        self.pipeline_id,
147                        ScriptFetchOptions {
148                            referrer: self.referrer.clone(),
149                            referrer_policy: self.referrer_policy,
150                            integrity_metadata,
151                            cryptographic_nonce,
152                            credentials_mode: CredentialsMode::CredentialsSameOrigin,
153                            parser_metadata: ParserMetadata::ParserInserted,
154                        },
155                        self.insecure_requests_policy,
156                        self.has_trustworthy_ancestor_origin,
157                        self.policy_container.clone(),
158                    );
159                    let _ = self
160                        .resource_threads
161                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
162                }
163                TokenSinkResult::RawData(RawKind::ScriptData)
164            },
165            (TagKind::StartTag, &local_name!("img")) if self.prefetching.get() => {
166                if let Some(url) = self.get_url(tag, local_name!("src")) {
167                    debug!("Prefetch {} {}", tag.name, url);
168                    let request = create_a_potential_cors_request(
169                        Some(self.webview_id),
170                        url,
171                        Destination::Image,
172                        self.get_cors_settings(tag, local_name!("crossorigin")),
173                        None,
174                        self.referrer.clone(),
175                        self.insecure_requests_policy,
176                        self.has_trustworthy_ancestor_origin,
177                        self.policy_container.clone(),
178                    )
179                    .origin(self.origin.clone())
180                    .pipeline_id(Some(self.pipeline_id))
181                    .referrer_policy(self.get_referrer_policy(tag, local_name!("referrerpolicy")));
182
183                    let _ = self
184                        .resource_threads
185                        .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
186                }
187                TokenSinkResult::Continue
188            },
189            (TagKind::StartTag, &local_name!("link")) if self.prefetching.get() => {
190                if let Some(rel) = self.get_attr(tag, local_name!("rel")) {
191                    if rel.value.eq_ignore_ascii_case("stylesheet") {
192                        if let Some(url) = self.get_url(tag, local_name!("href")) {
193                            debug!("Prefetch {} {}", tag.name, url);
194                            let cors_setting =
195                                self.get_cors_settings(tag, local_name!("crossorigin"));
196                            let referrer_policy =
197                                self.get_referrer_policy(tag, local_name!("referrerpolicy"));
198                            let integrity_metadata = self
199                                .get_attr(tag, local_name!("integrity"))
200                                .map(|attr| String::from(&attr.value))
201                                .unwrap_or_default();
202
203                            // https://html.spec.whatwg.org/multipage/#default-fetch-and-process-the-linked-resource
204                            let request = create_a_potential_cors_request(
205                                Some(self.webview_id),
206                                url,
207                                Destination::Style,
208                                cors_setting,
209                                None,
210                                self.referrer.clone(),
211                                self.insecure_requests_policy,
212                                self.has_trustworthy_ancestor_origin,
213                                self.policy_container.clone(),
214                            )
215                            .origin(self.origin.clone())
216                            .pipeline_id(Some(self.pipeline_id))
217                            .referrer_policy(referrer_policy)
218                            .integrity_metadata(integrity_metadata);
219
220                            let _ = self
221                                .resource_threads
222                                .send(CoreResourceMsg::Fetch(request, FetchChannels::Prefetch));
223                        }
224                    }
225                }
226                TokenSinkResult::Continue
227            },
228            (TagKind::StartTag, &local_name!("script")) => {
229                TokenSinkResult::RawData(RawKind::ScriptData)
230            },
231            (TagKind::EndTag, &local_name!("script")) => {
232                // After the first script tag, the main parser is blocked, so it's worth prefetching.
233                self.prefetching.set(true);
234                TokenSinkResult::Script(PrefetchHandle)
235            },
236            (TagKind::StartTag, &local_name!("base")) => {
237                if let Some(url) = self.get_url(tag, local_name!("href")) {
238                    if self.base_url.borrow().is_none() {
239                        debug!("Setting base {}", url);
240                        *self.base_url.borrow_mut() = Some(url);
241                    }
242                }
243                TokenSinkResult::Continue
244            },
245            _ => TokenSinkResult::Continue,
246        }
247    }
248}
249
250impl PrefetchSink {
251    fn get_attr<'a>(&'a self, tag: &'a Tag, name: LocalName) -> Option<&'a Attribute> {
252        tag.attrs.iter().find(|attr| attr.name.local == name)
253    }
254
255    fn get_url(&self, tag: &Tag, name: LocalName) -> Option<ServoUrl> {
256        let attr = self.get_attr(tag, name)?;
257        let base_url = self.base_url.borrow();
258        let base = base_url.as_ref().unwrap_or(&self.document_url);
259        ServoUrl::parse_with_base(Some(base), &attr.value).ok()
260    }
261
262    fn get_referrer_policy(&self, tag: &Tag, name: LocalName) -> ReferrerPolicy {
263        self.get_attr(tag, name)
264            .map(|attr| determine_policy_for_token(&attr.value))
265            .unwrap_or(self.referrer_policy)
266    }
267
268    fn get_cors_settings(&self, tag: &Tag, name: LocalName) -> Option<CorsSettings> {
269        let crossorigin = self.get_attr(tag, name)?;
270        if crossorigin.value.eq_ignore_ascii_case("anonymous") {
271            Some(CorsSettings::Anonymous)
272        } else if crossorigin.value.eq_ignore_ascii_case("use-credentials") {
273            Some(CorsSettings::UseCredentials)
274        } else {
275            None
276        }
277    }
278}