aboutsummaryrefslogtreecommitdiffstats
path: root/components/net
diff options
context:
space:
mode:
Diffstat (limited to 'components/net')
-rw-r--r--components/net/connector.rs114
-rw-r--r--components/net/fetch/methods.rs58
-rw-r--r--components/net/http_loader.rs20
-rw-r--r--components/net/resource_thread.rs30
-rw-r--r--components/net/tests/fetch.rs95
-rw-r--r--components/net/tests/main.rs9
-rw-r--r--components/net/websocket_loader.rs15
7 files changed, 311 insertions, 30 deletions
diff --git a/components/net/connector.rs b/components/net/connector.rs
index 058a27c47d6..dc44002a85a 100644
--- a/components/net/connector.rs
+++ b/components/net/connector.rs
@@ -8,8 +8,13 @@ use hyper::client::HttpConnector as HyperHttpConnector;
use hyper::rt::Future;
use hyper::{Body, Client};
use hyper_openssl::HttpsConnector;
-use openssl::ssl::{SslConnector, SslConnectorBuilder, SslMethod, SslOptions};
-use openssl::x509;
+use openssl::ex_data::Index;
+use openssl::ssl::{
+ Ssl, SslConnector, SslConnectorBuilder, SslContext, SslMethod, SslOptions, SslVerifyMode,
+};
+use openssl::x509::{self, X509StoreContext};
+use std::collections::hash_map::{Entry, HashMap};
+use std::sync::{Arc, Mutex};
use tokio::prelude::future::Executor;
pub const BUF_SIZE: usize = 32768;
@@ -30,6 +35,38 @@ const SIGNATURE_ALGORITHMS: &'static str = concat!(
"RSA+SHA512:RSA+SHA384:RSA+SHA256"
);
+#[derive(Clone)]
+pub struct ConnectionCerts {
+ certs: Arc<Mutex<HashMap<String, (Vec<u8>, u32)>>>,
+}
+
+impl ConnectionCerts {
+ pub fn new() -> Self {
+ Self {
+ certs: Arc::new(Mutex::new(HashMap::new())),
+ }
+ }
+
+ fn store(&self, host: String, cert_bytes: Vec<u8>) {
+ let mut certs = self.certs.lock().unwrap();
+ let entry = certs.entry(host).or_insert((cert_bytes, 0));
+ entry.1 += 1;
+ }
+
+ pub(crate) fn remove(&self, host: String) -> Option<Vec<u8>> {
+ match self.certs.lock().unwrap().entry(host) {
+ Entry::Vacant(_) => return None,
+ Entry::Occupied(mut e) => {
+ e.get_mut().1 -= 1;
+ if e.get().1 == 0 {
+ return Some((e.remove_entry().1).0);
+ }
+ Some(e.get().0.clone())
+ },
+ }
+ }
+}
+
pub struct HttpConnector {
inner: HyperHttpConnector,
}
@@ -60,7 +97,34 @@ impl Connect for HttpConnector {
pub type Connector = HttpsConnector<HttpConnector>;
pub type TlsConfig = SslConnectorBuilder;
-pub fn create_tls_config(certs: &str, alpn: &[u8]) -> TlsConfig {
+#[derive(Clone)]
+pub struct ExtraCerts(Arc<Mutex<Vec<Vec<u8>>>>);
+
+impl ExtraCerts {
+ pub fn new() -> Self {
+ Self(Arc::new(Mutex::new(vec![])))
+ }
+
+ pub fn add(&self, bytes: Vec<u8>) {
+ self.0.lock().unwrap().push(bytes);
+ }
+}
+
+struct Host(String);
+
+lazy_static! {
+ static ref EXTRA_INDEX: Index<SslContext, ExtraCerts> = SslContext::new_ex_index().unwrap();
+ static ref CONNECTION_INDEX: Index<SslContext, ConnectionCerts> =
+ SslContext::new_ex_index().unwrap();
+ static ref HOST_INDEX: Index<Ssl, Host> = Ssl::new_ex_index().unwrap();
+}
+
+pub fn create_tls_config(
+ certs: &str,
+ alpn: &[u8],
+ extra_certs: ExtraCerts,
+ connection_certs: ConnectionCerts,
+) -> TlsConfig {
// certs include multiple certificates. We could add all of them at once,
// but if any of them were already added, openssl would fail to insert all
// of them.
@@ -104,6 +168,44 @@ pub fn create_tls_config(certs: &str, alpn: &[u8]) -> TlsConfig {
SslOptions::NO_COMPRESSION,
);
+ cfg.set_ex_data(*EXTRA_INDEX, extra_certs);
+ cfg.set_ex_data(*CONNECTION_INDEX, connection_certs);
+ cfg.set_verify_callback(SslVerifyMode::PEER, |verified, x509_store_context| {
+ if verified {
+ return true;
+ }
+
+ let ssl_idx = X509StoreContext::ssl_idx().unwrap();
+ let ssl = x509_store_context.ex_data(ssl_idx).unwrap();
+
+ // Obtain the cert bytes for this connection.
+ let cert = match x509_store_context.current_cert() {
+ Some(cert) => cert,
+ None => return false,
+ };
+ let pem = match cert.to_pem() {
+ Ok(pem) => pem,
+ Err(_) => return false,
+ };
+
+ let ssl_context = ssl.ssl_context();
+
+ // Ensure there's an entry stored in the set of known connection certs for this connection.
+ if let Some(host) = ssl.ex_data(*HOST_INDEX) {
+ let connection_certs = ssl_context.ex_data(*CONNECTION_INDEX).unwrap();
+ connection_certs.store((*host).0.clone(), pem.clone());
+ }
+
+ // Fall back to the dynamic set of allowed certs.
+ let extra_certs = ssl_context.ex_data(*EXTRA_INDEX).unwrap();
+ for cert in &*extra_certs.0.lock().unwrap() {
+ if pem == *cert {
+ return true;
+ }
+ }
+ false
+ });
+
cfg
}
@@ -111,7 +213,11 @@ pub fn create_http_client<E>(tls_config: TlsConfig, executor: E) -> Client<Conne
where
E: Executor<Box<dyn Future<Error = (), Item = ()> + Send + 'static>> + Sync + Send + 'static,
{
- let connector = HttpsConnector::with_connector(HttpConnector::new(), tls_config).unwrap();
+ let mut connector = HttpsConnector::with_connector(HttpConnector::new(), tls_config).unwrap();
+ connector.set_callback(|configuration, destination| {
+ configuration.set_ex_data(*HOST_INDEX, Host(destination.host().to_owned()));
+ Ok(())
+ });
Client::builder()
.http1_title_case_headers(true)
diff --git a/components/net/fetch/methods.rs b/components/net/fetch/methods.rs
index 457b462bc0b..c25ab10cbe6 100644
--- a/components/net/fetch/methods.rs
+++ b/components/net/fetch/methods.rs
@@ -15,17 +15,19 @@ use headers::{AccessControlExposeHeaders, ContentType, HeaderMapExt, Range};
use http::header::{self, HeaderMap, HeaderName};
use hyper::Method;
use hyper::StatusCode;
-use ipc_channel::ipc::IpcReceiver;
+use ipc_channel::ipc::{self, IpcReceiver};
use mime::{self, Mime};
use net_traits::blob_url_store::{parse_blob_url, BlobURLStoreError};
use net_traits::filemanager_thread::{FileTokenCheck, RelativePos};
use net_traits::request::{
is_cors_safelisted_method, is_cors_safelisted_request_header, Origin, ResponseTainting, Window,
};
-use net_traits::request::{CredentialsMode, Destination, Referrer, Request, RequestMode};
+use net_traits::request::{
+ BodyChunkRequest, CredentialsMode, Destination, Referrer, Request, RequestMode,
+};
use net_traits::response::{Response, ResponseBody, ResponseType};
use net_traits::{FetchTaskTarget, NetworkError, ReferrerPolicy, ResourceFetchTiming};
-use net_traits::{ResourceAttribute, ResourceTimeValue};
+use net_traits::{ResourceAttribute, ResourceTimeValue, ResourceTimingType};
use servo_arc::Arc as ServoArc;
use servo_url::ServoUrl;
use std::borrow::Cow;
@@ -282,7 +284,10 @@ pub fn main_fetch(
false
};
- if (same_origin && !cors_flag) || current_url.scheme() == "data" {
+ if (same_origin && !cors_flag) ||
+ current_url.scheme() == "data" ||
+ current_url.scheme() == "chrome"
+ {
// Substep 1.
request.response_tainting = ResponseTainting::Basic;
@@ -606,6 +611,17 @@ fn range_not_satisfiable_error(response: &mut Response) {
response.raw_status = Some((StatusCode::RANGE_NOT_SATISFIABLE.as_u16(), reason.into()));
}
+fn create_blank_reply(url: ServoUrl, timing_type: ResourceTimingType) -> Response {
+ let mut response = Response::new(url, ResourceFetchTiming::new(timing_type));
+ response
+ .headers
+ .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
+ *response.body.lock().unwrap() = ResponseBody::Done(vec![]);
+ response.status = Some((StatusCode::OK, "OK".to_string()));
+ response.raw_status = Some((StatusCode::OK.as_u16(), b"OK".to_vec()));
+ response
+}
+
/// [Scheme fetch](https://fetch.spec.whatwg.org#scheme-fetch)
fn scheme_fetch(
request: &mut Request,
@@ -617,15 +633,31 @@ fn scheme_fetch(
let url = request.current_url();
match url.scheme() {
- "about" if url.path() == "blank" => {
- let mut response = Response::new(url, ResourceFetchTiming::new(request.timing_type()));
- response
- .headers
- .typed_insert(ContentType::from(mime::TEXT_HTML_UTF_8));
- *response.body.lock().unwrap() = ResponseBody::Done(vec![]);
- response.status = Some((StatusCode::OK, "OK".to_string()));
- response.raw_status = Some((StatusCode::OK.as_u16(), b"OK".to_vec()));
- response
+ "about" if url.path() == "blank" => create_blank_reply(url, request.timing_type()),
+
+ "chrome" if url.path() == "allowcert" => {
+ let data = request.body.as_mut().and_then(|body| {
+ let stream = body.take_stream();
+ let (body_chan, body_port) = ipc::channel().unwrap();
+ let _ = stream.send(BodyChunkRequest::Connect(body_chan));
+ let _ = stream.send(BodyChunkRequest::Chunk);
+ body_port.recv().ok()
+ });
+ let data = data.as_ref().and_then(|b| {
+ let idx = b.iter().position(|b| *b == b'&')?;
+ Some(b.split_at(idx))
+ });
+
+ if let Some((secret, bytes)) = data {
+ let secret = str::from_utf8(secret).ok().and_then(|s| s.parse().ok());
+ if secret == Some(*net_traits::PRIVILEGED_SECRET) {
+ if let Ok(bytes) = base64::decode(&bytes[1..]) {
+ context.state.extra_certs.add(bytes);
+ }
+ }
+ }
+
+ create_blank_reply(url, request.timing_type())
},
"http" | "https" => http_fetch(
diff --git a/components/net/http_loader.rs b/components/net/http_loader.rs
index 44b36c1bb4d..d69f418006c 100644
--- a/components/net/http_loader.rs
+++ b/components/net/http_loader.rs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-use crate::connector::{create_http_client, Connector, TlsConfig};
+use crate::connector::{create_http_client, ConnectionCerts, Connector, ExtraCerts, TlsConfig};
use crate::cookie;
use crate::cookie_storage::CookieStorage;
use crate::decoder::Decoder;
@@ -89,6 +89,8 @@ pub struct HttpState {
pub auth_cache: RwLock<AuthCache>,
pub history_states: RwLock<HashMap<HistoryStateId, Vec<u8>>>,
pub client: Client<Connector, Body>,
+ pub extra_certs: ExtraCerts,
+ pub connection_certs: ConnectionCerts,
}
impl HttpState {
@@ -104,6 +106,8 @@ impl HttpState {
tls_config,
HANDLE.lock().unwrap().as_ref().unwrap().executor(),
),
+ extra_certs: ExtraCerts::new(),
+ connection_certs: ConnectionCerts::new(),
}
}
}
@@ -527,11 +531,19 @@ fn obtain_response(
let method = method.clone();
let send_start = precise_time_ms();
+ let host = request.uri().host().unwrap_or("").to_owned();
+ let host_clone = request.uri().host().unwrap_or("").to_owned();
+ let connection_certs = context.state.connection_certs.clone();
+ let connection_certs_clone = context.state.connection_certs.clone();
+
let headers = headers.clone();
Box::new(
client
.request(request)
.and_then(move |res| {
+ // We no longer need to track the cert for this connection.
+ connection_certs.remove(host);
+
let send_end = precise_time_ms();
// TODO(#21271) response_start: immediately after receiving first byte of response
@@ -564,7 +576,9 @@ fn obtain_response(
};
Ok((Decoder::detect(res), msg))
})
- .map_err(move |e| NetworkError::from_hyper_error(&e)),
+ .map_err(move |e| {
+ NetworkError::from_hyper_error(&e, connection_certs_clone.remove(host_clone))
+ }),
)
}
@@ -1557,7 +1571,7 @@ fn http_network_fetch(
&url,
&request.method,
&request.headers,
- request.body.as_mut().and_then(|body| body.take_stream()),
+ request.body.as_mut().map(|body| body.take_stream()),
&request.pipeline_id,
request_id.as_ref().map(Deref::deref),
is_xhr,
diff --git a/components/net/resource_thread.rs b/components/net/resource_thread.rs
index 28a397e78e6..94184c3a046 100644
--- a/components/net/resource_thread.rs
+++ b/components/net/resource_thread.rs
@@ -4,7 +4,9 @@
//! A thread that takes a URL and streams back the binary data.
-use crate::connector::{create_http_client, create_tls_config, ALPN_H2_H1};
+use crate::connector::{
+ create_http_client, create_tls_config, ConnectionCerts, ExtraCerts, ALPN_H2_H1,
+};
use crate::cookie;
use crate::cookie_storage::CookieStorage;
use crate::fetch::cors_cache::CorsCache;
@@ -143,6 +145,9 @@ fn create_http_states(
None => resources::read_string(Resource::SSLCertificates),
};
+ let extra_certs = ExtraCerts::new();
+ let connection_certs = ConnectionCerts::new();
+
let http_state = HttpState {
hsts_list: RwLock::new(hsts_list),
cookie_jar: RwLock::new(cookie_jar),
@@ -151,11 +156,21 @@ fn create_http_states(
http_cache: RwLock::new(http_cache),
http_cache_state: Mutex::new(HashMap::new()),
client: create_http_client(
- create_tls_config(&certs, ALPN_H2_H1),
+ create_tls_config(
+ &certs,
+ ALPN_H2_H1,
+ extra_certs.clone(),
+ connection_certs.clone(),
+ ),
HANDLE.lock().unwrap().as_ref().unwrap().executor(),
),
+ extra_certs,
+ connection_certs,
};
+ let extra_certs = ExtraCerts::new();
+ let connection_certs = ConnectionCerts::new();
+
let private_http_state = HttpState {
hsts_list: RwLock::new(HstsList::from_servo_preload()),
cookie_jar: RwLock::new(CookieStorage::new(150)),
@@ -164,9 +179,16 @@ fn create_http_states(
http_cache: RwLock::new(HttpCache::new()),
http_cache_state: Mutex::new(HashMap::new()),
client: create_http_client(
- create_tls_config(&certs, ALPN_H2_H1),
+ create_tls_config(
+ &certs,
+ ALPN_H2_H1,
+ extra_certs.clone(),
+ connection_certs.clone(),
+ ),
HANDLE.lock().unwrap().as_ref().unwrap().executor(),
),
+ extra_certs,
+ connection_certs,
};
(Arc::new(http_state), Arc::new(private_http_state))
@@ -705,6 +727,8 @@ impl CoreResourceManager {
action_receiver,
http_state.clone(),
self.certificate_path.clone(),
+ http_state.extra_certs.clone(),
+ http_state.connection_certs.clone(),
);
}
}
diff --git a/components/net/tests/fetch.rs b/components/net/tests/fetch.rs
index 38797cf027b..dc4c0f032c0 100644
--- a/components/net/tests/fetch.rs
+++ b/components/net/tests/fetch.rs
@@ -22,7 +22,7 @@ use hyper::body::Body;
use hyper::{Request as HyperRequest, Response as HyperResponse};
use mime::{self, Mime};
use msg::constellation_msg::TEST_PIPELINE_ID;
-use net::connector::{create_tls_config, ALPN_H2_H1};
+use net::connector::{create_tls_config, ConnectionCerts, ExtraCerts, ALPN_H2_H1};
use net::fetch::cors_cache::CorsCache;
use net::fetch::methods::{self, CancellationListener, FetchContext};
use net::filemanager_thread::FileManager;
@@ -682,7 +682,12 @@ fn test_fetch_with_hsts() {
let (server, url) = make_ssl_server(handler, cert_path.clone(), key_path.clone());
let certs = fs::read_to_string(cert_path).expect("Couldn't find certificate file");
- let tls_config = create_tls_config(&certs, ALPN_H2_H1);
+ let tls_config = create_tls_config(
+ &certs,
+ ALPN_H2_H1,
+ ExtraCerts::new(),
+ ConnectionCerts::new(),
+ );
let mut context = FetchContext {
state: Arc::new(HttpState::new(tls_config)),
@@ -735,7 +740,12 @@ fn test_load_adds_host_to_hsts_list_when_url_is_https() {
url.as_mut_url().set_scheme("https").unwrap();
let certs = fs::read_to_string(cert_path).expect("Couldn't find certificate file");
- let tls_config = create_tls_config(&certs, ALPN_H2_H1);
+ let tls_config = create_tls_config(
+ &certs,
+ ALPN_H2_H1,
+ ExtraCerts::new(),
+ ConnectionCerts::new(),
+ );
let mut context = FetchContext {
state: Arc::new(HttpState::new(tls_config)),
@@ -777,6 +787,85 @@ fn test_load_adds_host_to_hsts_list_when_url_is_https() {
}
#[test]
+fn test_fetch_self_signed() {
+ let handler = move |_: HyperRequest<Body>, response: &mut HyperResponse<Body>| {
+ *response.body_mut() = b"Yay!".to_vec().into();
+ };
+ let client_cert_path = Path::new("../../resources/certs").canonicalize().unwrap();
+ let cert_path = Path::new("../../resources/self_signed_certificate_for_testing.crt")
+ .canonicalize()
+ .unwrap();
+ let key_path = Path::new("../../resources/privatekey_for_testing.key")
+ .canonicalize()
+ .unwrap();
+ let (_server, mut url) = make_ssl_server(handler, cert_path.clone(), key_path.clone());
+ url.as_mut_url().set_scheme("https").unwrap();
+
+ let cert_data = fs::read_to_string(cert_path.clone()).expect("Couldn't find certificate file");
+ let client_cert_data =
+ fs::read_to_string(client_cert_path.clone()).expect("Couldn't find certificate file");
+ let extra_certs = ExtraCerts::new();
+ let tls_config = create_tls_config(
+ &client_cert_data,
+ ALPN_H2_H1,
+ extra_certs.clone(),
+ ConnectionCerts::new(),
+ );
+
+ let mut context = FetchContext {
+ state: Arc::new(HttpState::new(tls_config)),
+ user_agent: DEFAULT_USER_AGENT.into(),
+ devtools_chan: None,
+ filemanager: FileManager::new(create_embedder_proxy(), Weak::new()),
+ file_token: FileTokenCheck::NotRequired,
+ cancellation_listener: Arc::new(Mutex::new(CancellationListener::new(None))),
+ timing: ServoArc::new(Mutex::new(ResourceFetchTiming::new(
+ ResourceTimingType::Navigation,
+ ))),
+ };
+
+ let mut request = RequestBuilder::new(url.clone())
+ .method(Method::GET)
+ .body(None)
+ .destination(Destination::Document)
+ .origin(url.clone().origin())
+ .pipeline_id(Some(TEST_PIPELINE_ID))
+ .build();
+
+ let response = fetch_with_context(&mut request, &mut context);
+
+ assert!(matches!(
+ response.get_network_error(),
+ Some(NetworkError::SslValidation(..))
+ ));
+
+ extra_certs.add(cert_data.as_bytes().into());
+
+ // FIXME: something weird happens inside the SSL server after the first
+ // connection encounters a verification error, and it no longer
+ // accepts new connections that should work fine. We are forced
+ // to start a new server and connect to that to verfiy that
+ // the self-signed cert is now accepted.
+
+ let (server, mut url) = make_ssl_server(handler, cert_path.clone(), key_path.clone());
+ url.as_mut_url().set_scheme("https").unwrap();
+
+ let mut request = RequestBuilder::new(url.clone())
+ .method(Method::GET)
+ .body(None)
+ .destination(Destination::Document)
+ .origin(url.clone().origin())
+ .pipeline_id(Some(TEST_PIPELINE_ID))
+ .build();
+
+ let response = fetch_with_context(&mut request, &mut context);
+
+ assert!(response.status.unwrap().0.is_success());
+
+ let _ = server.close();
+}
+
+#[test]
fn test_fetch_with_sri_network_error() {
static MESSAGE: &'static [u8] = b"alert('Hello, Network Error');";
let handler = move |_: HyperRequest<Body>, response: &mut HyperResponse<Body>| {
diff --git a/components/net/tests/main.rs b/components/net/tests/main.rs
index c48ac873729..7583fdf0fc5 100644
--- a/components/net/tests/main.rs
+++ b/components/net/tests/main.rs
@@ -29,7 +29,7 @@ use hyper::server::conn::Http;
use hyper::server::Server as HyperServer;
use hyper::service::service_fn_ok;
use hyper::{Body, Request as HyperRequest, Response as HyperResponse};
-use net::connector::{create_tls_config, ALPN_H2_H1};
+use net::connector::{create_tls_config, ConnectionCerts, ExtraCerts, ALPN_H2_H1};
use net::fetch::cors_cache::CorsCache;
use net::fetch::methods::{self, CancellationListener, FetchContext};
use net::filemanager_thread::FileManager;
@@ -91,7 +91,12 @@ fn new_fetch_context(
pool_handle: Option<Weak<CoreResourceThreadPool>>,
) -> FetchContext {
let certs = resources::read_string(Resource::SSLCertificates);
- let tls_config = create_tls_config(&certs, ALPN_H2_H1);
+ let tls_config = create_tls_config(
+ &certs,
+ ALPN_H2_H1,
+ ExtraCerts::new(),
+ ConnectionCerts::new(),
+ );
let sender = fc.unwrap_or_else(|| create_embedder_proxy());
FetchContext {
diff --git a/components/net/websocket_loader.rs b/components/net/websocket_loader.rs
index 69d3c430fcb..bece51173bb 100644
--- a/components/net/websocket_loader.rs
+++ b/components/net/websocket_loader.rs
@@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
-use crate::connector::{create_tls_config, ALPN_H1};
+use crate::connector::{create_tls_config, ConnectionCerts, ExtraCerts, ALPN_H1};
use crate::cookie::Cookie;
use crate::fetch::methods::should_be_blocked_due_to_bad_port;
use crate::hosts::replace_host;
@@ -38,6 +38,8 @@ struct Client<'a> {
event_sender: &'a IpcSender<WebSocketNetworkEvent>,
protocol_in_use: Option<String>,
certificate_path: Option<String>,
+ extra_certs: ExtraCerts,
+ connection_certs: ConnectionCerts,
}
impl<'a> Factory for Client<'a> {
@@ -167,7 +169,12 @@ impl<'a> Handler for Client<'a> {
WebSocketErrorKind::Protocol,
format!("Unable to parse domain from {}. Needed for SSL.", url),
))?;
- let tls_config = create_tls_config(&certs, ALPN_H1);
+ let tls_config = create_tls_config(
+ &certs,
+ ALPN_H1,
+ self.extra_certs.clone(),
+ self.connection_certs.clone(),
+ );
tls_config
.build()
.connect(domain, stream)
@@ -181,6 +188,8 @@ pub fn init(
dom_action_receiver: IpcReceiver<WebSocketDomAction>,
http_state: Arc<HttpState>,
certificate_path: Option<String>,
+ extra_certs: ExtraCerts,
+ connection_certs: ConnectionCerts,
) {
thread::Builder::new()
.name(format!("WebSocket connection to {}", req_builder.url))
@@ -229,6 +238,8 @@ pub fn init(
event_sender: &resource_event_sender,
protocol_in_use: None,
certificate_path,
+ extra_certs,
+ connection_certs,
};
let mut ws = WebSocket::new(client).unwrap();