script/
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 content_security_policy::Violation;
8use net_traits::request::RequestId;
9use net_traits::{
10    BoxedFetchCallback, FetchMetadata, FetchResponseMsg, NetworkError, ResourceFetchTiming,
11    ResourceTimingType,
12};
13use servo_url::ServoUrl;
14
15use crate::dom::bindings::inheritance::Castable;
16use crate::dom::bindings::root::DomRoot;
17use crate::dom::globalscope::GlobalScope;
18use crate::dom::performance::performanceentry::PerformanceEntry;
19use crate::dom::performance::performanceresourcetiming::{
20    InitiatorType, PerformanceResourceTiming,
21};
22use crate::script_runtime::CanGc;
23use crate::task_source::SendableTaskSource;
24
25pub(crate) trait ResourceTimingListener {
26    fn resource_timing_information(&self) -> (InitiatorType, ServoUrl);
27    fn resource_timing_global(&self) -> DomRoot<GlobalScope>;
28}
29
30pub(crate) fn submit_timing<T: ResourceTimingListener>(
31    listener: &T,
32    result: &Result<(), NetworkError>,
33    resource_timing: &ResourceFetchTiming,
34    can_gc: CanGc,
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        if error.is_permanent_failure() {
43            return;
44        }
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(
73        &listener.resource_timing_global(),
74        url,
75        initiator_type,
76        resource_timing,
77        can_gc,
78    );
79}
80
81pub(crate) fn submit_timing_data(
82    global: &GlobalScope,
83    url: ServoUrl,
84    initiator_type: InitiatorType,
85    resource_timing: &ResourceFetchTiming,
86    can_gc: CanGc,
87) {
88    let performance_entry =
89        PerformanceResourceTiming::new(global, url, initiator_type, None, resource_timing, can_gc);
90    global
91        .performance()
92        .queue_entry(performance_entry.upcast::<PerformanceEntry>());
93}
94
95pub(crate) trait FetchResponseListener: Send + 'static {
96    /// A gating mechanism that runs before invoking the listener methods on the target
97    /// thread. If the `should_invoke` method returns false, the listener does not receive
98    /// the notification.
99    fn should_invoke(&self) -> bool {
100        true
101    }
102
103    fn process_request_body(&mut self, request_id: RequestId);
104    fn process_request_eof(&mut self, request_id: RequestId);
105    fn process_response(
106        &mut self,
107        cx: &mut js::context::JSContext,
108        request_id: RequestId,
109        metadata: Result<FetchMetadata, NetworkError>,
110    );
111    fn process_response_chunk(&mut self, request_id: RequestId, chunk: Vec<u8>);
112    fn process_response_eof(
113        self,
114        cx: &mut js::context::JSContext,
115        request_id: RequestId,
116        response: Result<(), NetworkError>,
117        timing: ResourceFetchTiming,
118    );
119    fn process_csp_violations(&mut self, request_id: RequestId, violations: Vec<Violation>);
120}
121
122/// An off-thread sink for async network event tasks. All such events are forwarded to
123/// a target thread, where they are invoked on the provided context object.
124pub(crate) struct NetworkListener<Listener: FetchResponseListener> {
125    pub(crate) context: Arc<Mutex<Option<Listener>>>,
126    pub(crate) task_source: SendableTaskSource,
127}
128
129impl<Listener: FetchResponseListener> NetworkListener<Listener> {
130    pub(crate) fn new(context: Listener, task_source: SendableTaskSource) -> Self {
131        Self {
132            context: Arc::new(Mutex::new(Some(context))),
133            task_source,
134        }
135    }
136
137    pub(crate) fn notify(&mut self, message: FetchResponseMsg) {
138        let context = self.context.clone();
139        self.task_source
140            .queue(task!(network_listener_response: move |cx| {
141                let mut context = context.lock().unwrap();
142                let Some(fetch_listener) = &mut *context else {
143                    return;
144                };
145
146                if !fetch_listener.should_invoke() {
147                    return;
148                }
149
150                match message {
151                    FetchResponseMsg::ProcessRequestBody(request_id) => {
152                        fetch_listener.process_request_body(request_id)
153                    },
154                    FetchResponseMsg::ProcessRequestEOF(request_id) => {
155                        fetch_listener.process_request_eof(request_id)
156                    },
157                    FetchResponseMsg::ProcessResponse(request_id, meta) => {
158                        fetch_listener.process_response(cx, request_id, meta)
159                    },
160                    FetchResponseMsg::ProcessResponseChunk(request_id, data) => {
161                        fetch_listener.process_response_chunk(request_id, data.0)
162                    },
163                    FetchResponseMsg::ProcessResponseEOF(request_id, result, timing) => {
164                        if let Some(fetch_listener) = context.take() {
165                            fetch_listener.process_response_eof(cx, request_id, result, timing);
166                        };
167                    },
168                    FetchResponseMsg::ProcessCspViolations(request_id, violations) => {
169                        fetch_listener.process_csp_violations(request_id, violations)
170                    },
171                }
172            }));
173    }
174
175    pub(crate) fn into_callback(mut self) -> BoxedFetchCallback {
176        Box::new(move |response_msg| self.notify(response_msg))
177    }
178}