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