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        request_id: RequestId,
108        metadata: Result<FetchMetadata, NetworkError>,
109    );
110    fn process_response_chunk(&mut self, request_id: RequestId, chunk: Vec<u8>);
111    fn process_response_eof(
112        self,
113        request_id: RequestId,
114        response: Result<(), NetworkError>,
115        timing: ResourceFetchTiming,
116    );
117    fn process_csp_violations(&mut self, request_id: RequestId, violations: Vec<Violation>);
118}
119
120/// An off-thread sink for async network event tasks. All such events are forwarded to
121/// a target thread, where they are invoked on the provided context object.
122pub(crate) struct NetworkListener<Listener: FetchResponseListener> {
123    pub(crate) context: Arc<Mutex<Option<Listener>>>,
124    pub(crate) task_source: SendableTaskSource,
125}
126
127impl<Listener: FetchResponseListener> NetworkListener<Listener> {
128    pub(crate) fn new(context: Listener, task_source: SendableTaskSource) -> Self {
129        Self {
130            context: Arc::new(Mutex::new(Some(context))),
131            task_source,
132        }
133    }
134
135    pub(crate) fn notify(&mut self, message: FetchResponseMsg) {
136        let context = self.context.clone();
137        self.task_source
138            .queue(task!(network_listener_response: move || {
139                let mut context = context.lock().unwrap();
140                let Some(fetch_listener) = &mut *context else {
141                    return;
142                };
143
144                if !fetch_listener.should_invoke() {
145                    return;
146                }
147
148                match message {
149                    FetchResponseMsg::ProcessRequestBody(request_id) => {
150                        fetch_listener.process_request_body(request_id)
151                    },
152                    FetchResponseMsg::ProcessRequestEOF(request_id) => {
153                        fetch_listener.process_request_eof(request_id)
154                    },
155                    FetchResponseMsg::ProcessResponse(request_id, meta) => {
156                        fetch_listener.process_response(request_id, meta)
157                    },
158                    FetchResponseMsg::ProcessResponseChunk(request_id, data) => {
159                        fetch_listener.process_response_chunk(request_id, data.0)
160                    },
161                    FetchResponseMsg::ProcessResponseEOF(request_id, result, timing) => {
162                        if let Some(fetch_listener) = context.take() {
163                            fetch_listener.process_response_eof(request_id, result, timing);
164                        };
165                    },
166                    FetchResponseMsg::ProcessCspViolations(request_id, violations) => {
167                        fetch_listener.process_csp_violations(request_id, violations)
168                    },
169                }
170            }));
171    }
172
173    pub(crate) fn into_callback(mut self) -> BoxedFetchCallback {
174        Box::new(move |response_msg| self.notify(response_msg))
175    }
176}