Skip to main content

script/fetch/
network_listener.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::sync::{Arc, Mutex};
6
7use bytes::Bytes;
8use content_security_policy::Violation;
9use js::context::JSContext;
10use net_traits::request::RequestId;
11use net_traits::{
12    BoxedFetchCallback, FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming,
13    ResourceTimingType,
14};
15use servo_url::ServoUrl;
16
17use crate::dom::bindings::inheritance::Castable;
18use crate::dom::globalscope::GlobalScope;
19use crate::dom::performance::performanceentry::PerformanceEntry;
20use crate::dom::performance::performanceresourcetiming::{
21    InitiatorType, PerformanceResourceTiming,
22};
23use crate::tasks::task_source::SendableTaskSource;
24
25pub(crate) trait ResourceTimingListener {
26    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl);
27}
28
29pub(crate) fn submit_timing<T: ResourceTimingListener>(
30    cx: &mut JSContext,
31    global: &GlobalScope,
32    listener: &T,
33    result: &Result<(), NetworkError>,
34    resource_timing: &ResourceFetchTiming,
35) {
36    // https://www.w3.org/TR/resource-timing/#resources-included-in-the-performanceresourcetiming-interface
37    // If a resource fetch is aborted because it failed a fetch precondition
38    // (e.g. mixed content, CORS restriction, CSP policy, etc), then this resource
39    // will not be included as a PerformanceResourceTiming object in
40    // the Performance Timeline.
41    if let Err(error) = &result &&
42        error.is_permanent_failure()
43    {
44        return;
45    }
46
47    // Resource timings should only be submitted for the initial preload request,
48    // not for the request that consumes the preload: https://github.com/whatwg/html/issues/12047
49    if resource_timing.preloaded {
50        return;
51    }
52    // TODO Resources for which the fetch was initiated, but was later aborted
53    // (e.g. due to a network error) MAY be included as PerformanceResourceTiming
54    // objects in the Performance Timeline and MUST contain initialized attribute
55    // values for processed substeps of the processing model.
56    if resource_timing.timing_type != ResourceTimingType::Resource &&
57        resource_timing.timing_type != ResourceTimingType::Error
58    {
59        warn!(
60            "Submitting non-resource ({:?}) timing as resource",
61            resource_timing.timing_type
62        );
63        return;
64    }
65
66    let (initiator_type, url) = listener.resource_timing_information();
67    if initiator_type == InitiatorType::Other {
68        warn!("Ignoring InitiatorType::Other resource {:?}", url);
69        return;
70    }
71
72    submit_timing_data(cx, global, url, initiator_type, resource_timing);
73}
74
75pub(crate) fn submit_timing_data(
76    cx: &mut JSContext,
77    global: &GlobalScope,
78    url: ServoUrl,
79    initiator_type: InitiatorType,
80    resource_timing: &ResourceFetchTiming,
81) {
82    let performance_entry =
83        PerformanceResourceTiming::new(cx, global, url, initiator_type, resource_timing);
84    global
85        .performance(cx)
86        .queue_entry(performance_entry.upcast::<PerformanceEntry>());
87}
88
89pub(crate) trait FetchResponseListener: Send + 'static {
90    /// A gating mechanism that runs before invoking the listener methods on the target
91    /// thread. If the `should_invoke` method returns false, the listener does not receive
92    /// the notification.
93    fn should_invoke(&self) -> bool {
94        true
95    }
96
97    fn process_request_body(&mut self, global: &GlobalScope, request_id: RequestId);
98    fn process_response(
99        &mut self,
100        cx: &mut JSContext,
101        global: &GlobalScope,
102        request_id: RequestId,
103        metadata: Result<FetchMetadata, NetworkError>,
104    );
105    fn process_response_chunk(
106        &mut self,
107        cx: &mut JSContext,
108        global: &GlobalScope,
109        request_id: RequestId,
110        chunk: Bytes,
111    );
112    fn process_response_eof(
113        self,
114        cx: &mut JSContext,
115        global: &GlobalScope,
116        request_id: RequestId,
117        response: Result<(), NetworkError>,
118        timing: ResourceFetchTiming,
119    );
120    fn process_csp_violations(
121        &mut self,
122        cx: &mut js::context::JSContext,
123        global: &GlobalScope,
124        request_id: RequestId,
125        violations: Vec<Violation>,
126    );
127
128    fn process_content_length(&mut self, _request_id: RequestId, _size: usize) {}
129}
130
131/// An off-thread sink for async network event tasks. All such events are forwarded to
132/// a target thread, where they are invoked on the provided context object.
133pub(crate) struct NetworkListener<Listener: FetchResponseListener> {
134    pub(crate) context: Arc<Mutex<Option<Listener>>>,
135    pub(crate) task_source: SendableTaskSource,
136}
137
138impl<Listener: FetchResponseListener> NetworkListener<Listener> {
139    pub(crate) fn new(context: Listener, task_source: SendableTaskSource) -> Self {
140        Self {
141            context: Arc::new(Mutex::new(Some(context))),
142            task_source,
143        }
144    }
145
146    pub(crate) fn notify(&mut self, message: FetchResponseMsg) {
147        let context = self.context.clone();
148
149        self.task_source
150            .queue(task!(network_listener_response: move |cx| {
151                let mut context = context.lock().unwrap();
152                let Some(fetch_listener) = &mut *context else {
153                    return;
154                };
155
156                // Note: This relies on the task queue to properly enter the realm of the global
157                // scope of a task before running it.
158                let Some(global) = GlobalScope::current() else {
159                    return;
160                };
161
162                let is_eof = matches!(message, FetchResponseMsg::ProcessResponseEOF(..));
163                let request_id = message.request_id();
164
165                if fetch_listener.should_invoke() {
166                    match message {
167                        FetchResponseMsg::ProcessRequestBody(request_id) => {
168                            fetch_listener.process_request_body(&global, request_id)
169                        },
170                        FetchResponseMsg::ProcessResponse(request_id, meta) => {
171                            fetch_listener.process_response(cx, &global, request_id, meta)
172                        },
173                        FetchResponseMsg::ProcessResponseChunk(request_id, data) => {
174                            fetch_listener.process_response_chunk(cx, &global, request_id, data)
175                        },
176                        FetchResponseMsg::ProcessResponseEOF(request_id, result, timing) => {
177                            if let Some(fetch_listener) = context.take() {
178                                fetch_listener.process_response_eof(cx, &global, request_id, result, timing);
179                            };
180                        },
181                        FetchResponseMsg::ProcessCspViolations(request_id, violations) => {
182                            fetch_listener.process_csp_violations(cx, &global, request_id, violations)
183                        },
184                        FetchResponseMsg::ProcessContentLength(request_id, size) => { fetch_listener.process_content_length(request_id, size) },
185                    }
186                }
187
188                if is_eof {
189                    global
190                        .fetch_group_mut()
191                        .mark_fetch_request_as_done(&request_id);
192                }
193            }));
194    }
195
196    pub(crate) fn into_callback(mut self) -> BoxedFetchCallback {
197        Box::new(move |response_msg| self.notify(response_msg))
198    }
199}