aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock78
-rw-r--r--Cargo.toml6
-rwxr-xr-xgenkeys.sh46
-rw-r--r--src/rpc_client.rs11
-rw-r--r--src/rpc_server.rs2
-rw-r--r--src/tls_util.rs152
6 files changed, 190 insertions, 105 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4ee91a4a..0d4521c8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,20 +116,6 @@ dependencies = [
]
[[package]]
-name = "core-foundation"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "core-foundation-sys"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
name = "crc32fast"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -162,14 +148,6 @@ dependencies = [
]
[[package]]
-name = "ct-logs"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
name = "digest"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -455,12 +433,10 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustls-native-certs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-rustls 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"webpki 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -640,11 +616,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
-name = "openssl-probe"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-
-[[package]]
name = "parking_lot"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -842,17 +813,6 @@ dependencies = [
]
[[package]]
-name = "rustls-native-certs"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
- "rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
- "security-framework 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
name = "rustversion"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -868,15 +828,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
-name = "schannel"
-version = "0.1.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -891,27 +842,6 @@ dependencies = [
]
[[package]]
-name = "security-framework"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
- "core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)",
- "security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
-name = "security-framework-sys"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-dependencies = [
- "core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)",
-]
-
-[[package]]
name = "serde"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1321,12 +1251,9 @@ dependencies = [
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
-"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
-"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
"checksum crossbeam-epoch 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-"checksum ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113"
"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
"checksum err-derive 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "82f46c91bbed409ee74495549acbfcc7fae856e712e1df15afe75d0775eedc6c"
"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed"
@@ -1376,7 +1303,6 @@ dependencies = [
"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096"
"checksum num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "46203554f085ff89c235cd12f7075f3233af9b11ed7c9e16dfe2560d03313ce6"
"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
-"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
"checksum parking_lot 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "92e98c49ab0b7ce5b222f2cc9193fc4efe11c6d0bd4f648e374684a6857b1cfc"
"checksum parking_lot_core 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7582838484df45743c8434fbff785e8edf260c28748353d44bc0da32e0ceabf1"
"checksum pin-project 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7804a463a8d9572f13453c516a5faea534a2403d7ced2f0c7e100eeff072772c"
@@ -1400,14 +1326,10 @@ dependencies = [
"checksum rmp 0.8.9 (registry+https://github.com/rust-lang/crates.io-index)" = "0f10b46df14cf1ee1ac7baa4d2fbc2c52c0622a4b82fa8740e37bc452ac0184f"
"checksum rmp-serde 0.14.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4c1ee98f14fe8b8e9c5ea13d25da7b2a1796169202c57a09d7288de90d56222b"
"checksum rustls 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0d4a31f5d68413404705d6982529b0e11a9aacd4839d1d6222ee3b8cb4015e1"
-"checksum rustls-native-certs 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a75ffeb84a6bd9d014713119542ce415db3a3e4748f0bfce1e1416cd224a23a5"
"checksum rustversion 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b3bba175698996010c4f6dce5e7f173b6eb781fce25d2cfc45e27091ce0b79f6"
"checksum ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76"
-"checksum schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19"
"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
"checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
-"checksum security-framework 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "572dfa3a0785509e7a44b5b4bebcf94d41ba34e9ed9eb9df722545c3b3c4144a"
-"checksum security-framework-sys 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "8ddb15a5fec93b7021b8a9e96009c5d8d51c15673569f7c0f6b7204e5b7b404f"
"checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399"
"checksum serde_bytes 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "325a073952621257820e7a3469f55ba4726d8b28657e7e36653d1c36dc2c84ae"
"checksum serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c"
diff --git a/Cargo.toml b/Cargo.toml
index 1ca20c98..2e98c45a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,11 +29,11 @@ sha2 = "0.8"
async-trait = "0.1.30"
reduce = "0.1.2"
serde_json = "1.0"
+
rustls = "0.17"
-tokio-rustls = "0.13"
+tokio-rustls = { version = "0.13", features = ["dangerous_configuration"] }
+hyper-rustls = { version = "0.20", default-features = false }
webpki = "0.21"
-hyper-rustls = "0.20"
-#hyper-rustls = { version = "0.4", default-features = false }
[profile.dev]
lto = "off"
diff --git a/genkeys.sh b/genkeys.sh
index bff69da9..ae493248 100755
--- a/genkeys.sh
+++ b/genkeys.sh
@@ -10,27 +10,39 @@ cd pki
if [ ! -f garage-ca.key ]; then
echo "Generating Garage CA keys..."
openssl genrsa -out garage-ca.key 4096
- openssl req -x509 -new -key garage-ca.key -subj "/C=FR/O=Garage" -days 3650 -out garage-ca.crt
+ openssl req -x509 -new -nodes -key garage-ca.key -sha256 -days 3650 -out garage-ca.crt -subj "/C=FR/O=Garage"
fi
-if [ ! -f garage.key ]; then
- echo "Generating Garage agent keys..."
- openssl genrsa -out garage.key 4096
- openssl req -new -sha256 -key garage.key -subj "/C=FR/O=Garage/CN=*" -out garage.csr
+
+if [ ! -f garage.crt ]; then
+ echo "Generating Garage agent keys..."
+ if [ ! -f garage.key ]; then
+ openssl genrsa -out garage.key 4096
+ fi
+ openssl req -new -sha256 -key garage.key -subj "/C=FR/O=Garage/CN=garage" \
+ -out garage.csr
openssl req -in garage.csr -noout -text
openssl x509 -req -in garage.csr \
- -CA garage-ca.crt -CAkey garage-ca.key -CAcreateserial \
- -out garage.crt -days 365 -sha256
- rm garage.csr
-fi
+ -extensions v3_req \
+ -extfile <(cat <<EOF
+[req]
+distinguished_name = req_distinguished_name
+req_extensions = v3_req
+prompt = no
+
+[req_distinguished_name]
+C = FR
+O = Garage
+CN = garage
-if [ ! -f garage-client.key ]; then
- echo "Generating Garage client key..."
- openssl genrsa -out garage-client.key 4096
- openssl req -new -sha256 -key garage-client.key -subj "/C=FR/O=Garage" -out garage-client.csr
- openssl req -in garage-client.csr -noout -text
- openssl x509 -req -in garage-client.csr \
+[v3_req]
+keyUsage = keyEncipherment, dataEncipherment
+extendedKeyUsage = serverAuth, clientAuth
+subjectAltName = @alt_names
+[alt_names]
+DNS.1 = garage
+EOF
+) \
-CA garage-ca.crt -CAkey garage-ca.key -CAcreateserial \
- -out garage-client.crt -days 365 -sha256
- rm garage-client.csr
+ -out garage.crt -days 365
fi
diff --git a/src/rpc_client.rs b/src/rpc_client.rs
index 247f114e..255eb958 100644
--- a/src/rpc_client.rs
+++ b/src/rpc_client.rs
@@ -8,7 +8,6 @@ use futures::stream::StreamExt;
use futures_util::future::FutureExt;
use hyper::client::{Client, HttpConnector};
use hyper::{Body, Method, Request, StatusCode};
-use hyper_rustls::HttpsConnector;
use crate::data::*;
use crate::error::Error;
@@ -93,7 +92,7 @@ pub async fn rpc_call(
pub enum RpcClient {
HTTP(Client<HttpConnector, hyper::Body>),
- HTTPS(Client<HttpsConnector<HttpConnector>, hyper::Body>),
+ HTTPS(Client<tls_util::HttpsConnectorFixedDnsname<HttpConnector>, hyper::Body>),
}
impl RpcClient {
@@ -109,12 +108,11 @@ impl RpcClient {
config.root_store.add(crt)?;
}
- config.set_single_client_cert([&ca_certs[..], &node_certs[..]].concat(), node_key)?;
+ config.set_single_client_cert([&node_certs[..], &ca_certs[..]].concat(), node_key)?;
+ // config.dangerous().set_certificate_verifier(Arc::new(tls_util::NoHostnameCertVerifier));
- let mut http_connector = HttpConnector::new();
- http_connector.enforce_http(false);
let connector =
- HttpsConnector::<HttpConnector>::from((http_connector, Arc::new(config)));
+ tls_util::HttpsConnectorFixedDnsname::<HttpConnector>::new(config, "garage");
Ok(RpcClient::HTTPS(Client::builder().build(connector)))
} else {
@@ -161,3 +159,4 @@ impl RpcClient {
}
}
}
+
diff --git a/src/rpc_server.rs b/src/rpc_server.rs
index f42d54ac..17da6f86 100644
--- a/src/rpc_server.rs
+++ b/src/rpc_server.rs
@@ -120,7 +120,7 @@ pub async fn run_rpc_server(
let mut config =
rustls::ServerConfig::new(rustls::AllowAnyAuthenticatedClient::new(ca_store));
- config.set_single_cert([&ca_certs[..], &node_certs[..]].concat(), node_key)?;
+ config.set_single_cert([&node_certs[..], &ca_certs[..]].concat(), node_key)?;
let tls_acceptor = Arc::new(TlsAcceptor::from(Arc::new(config)));
let mut listener = TcpListener::bind(&bind_addr).await?;
diff --git a/src/tls_util.rs b/src/tls_util.rs
index a9e16c53..5a17d380 100644
--- a/src/tls_util.rs
+++ b/src/tls_util.rs
@@ -1,6 +1,20 @@
use std::{fs, io};
+use core::task::{Poll, Context};
+use std::pin::Pin;
+use std::sync::Arc;
+use core::future::Future;
+use futures_util::future::*;
+use tokio::io::{AsyncRead, AsyncWrite};
use rustls::internal::pemfile;
+use rustls::*;
+use hyper::client::HttpConnector;
+use hyper::client::connect::Connection;
+use hyper::service::Service;
+use hyper::Uri;
+use hyper_rustls::MaybeHttpsStream;
+use tokio_rustls::TlsConnector;
+use webpki::DNSNameRef;
use crate::error::Error;
@@ -44,3 +58,141 @@ pub fn load_private_key(filename: &str) -> Result<rustls::PrivateKey, Error> {
}
Ok(keys[0].clone())
}
+
+
+// ---- AWFUL COPYPASTA FROM rustls/verifier.rs
+// ---- USED TO ALLOW TO VERIFY SERVER CERTIFICATE VALIDITY IN CHAIN
+// ---- BUT DISREGARD HOSTNAME PARAMETER
+
+pub struct NoHostnameCertVerifier;
+
+type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm];
+static SUPPORTED_SIG_ALGS: SignatureAlgorithms = &[
+ &webpki::ECDSA_P256_SHA256,
+ &webpki::ECDSA_P256_SHA384,
+ &webpki::ECDSA_P384_SHA256,
+ &webpki::ECDSA_P384_SHA384,
+ &webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
+ &webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
+ &webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
+ &webpki::RSA_PKCS1_2048_8192_SHA256,
+ &webpki::RSA_PKCS1_2048_8192_SHA384,
+ &webpki::RSA_PKCS1_2048_8192_SHA512,
+ &webpki::RSA_PKCS1_3072_8192_SHA384
+];
+
+impl rustls::ServerCertVerifier for NoHostnameCertVerifier {
+ fn verify_server_cert(&self,
+ roots: &RootCertStore,
+ presented_certs: &[Certificate],
+ _dns_name: webpki::DNSNameRef,
+ _ocsp_response: &[u8]) -> Result<rustls::ServerCertVerified, TLSError> {
+
+ if presented_certs.is_empty() {
+ return Err(TLSError::NoCertificatesPresented);
+ }
+
+ let cert = webpki::EndEntityCert::from(&presented_certs[0].0)
+ .map_err(TLSError::WebPKIError)?;
+
+ let chain = presented_certs.iter()
+ .skip(1)
+ .map(|cert| cert.0.as_ref())
+ .collect::<Vec<_>>();
+
+ let trustroots: Vec<webpki::TrustAnchor> = roots.roots
+ .iter()
+ .map(|x| x.to_trust_anchor())
+ .collect();
+
+ let now = webpki::Time::try_from(std::time::SystemTime::now())
+ .map_err( |_ | TLSError::FailedToGetCurrentTime)?;
+
+ cert.verify_is_valid_tls_server_cert(SUPPORTED_SIG_ALGS,
+ &webpki::TLSServerTrustAnchors(&trustroots), &chain, now)
+ .map_err(TLSError::WebPKIError)?;
+
+ Ok(rustls::ServerCertVerified::assertion())
+ }
+}
+
+
+// ---- AWFUL COPYPASTA FROM HYPER-RUSTLS connector.rs
+// ---- ALWAYS USE `garage` AS HOSTNAME FOR TLS VERIFICATION
+
+#[derive(Clone)]
+pub struct HttpsConnectorFixedDnsname<T> {
+ http: T,
+ tls_config: Arc<rustls::ClientConfig>,
+ fixed_dnsname: &'static str,
+}
+
+type BoxError = Box<dyn std::error::Error + Send + Sync>;
+
+impl HttpsConnectorFixedDnsname<HttpConnector> {
+ pub fn new(mut tls_config: rustls::ClientConfig, fixed_dnsname: &'static str) -> Self {
+ let mut http = HttpConnector::new();
+ http.enforce_http(false);
+ tls_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
+ Self {
+ http,
+ tls_config: Arc::new(tls_config),
+ fixed_dnsname,
+ }
+ }
+}
+
+impl<T> Service<Uri> for HttpsConnectorFixedDnsname<T>
+ where
+ T: Service<Uri>,
+ T::Response: Connection + AsyncRead + AsyncWrite + Send + Unpin + 'static,
+ T::Future: Send + 'static,
+ T::Error: Into<BoxError>,
+{
+ type Response = MaybeHttpsStream<T::Response>;
+ type Error = BoxError;
+
+ #[allow(clippy::type_complexity)]
+ type Future =
+ Pin<Box<dyn Future<Output = Result<MaybeHttpsStream<T::Response>, BoxError>> + Send>>;
+
+ fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+ match self.http.poll_ready(cx) {
+ Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
+ Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())),
+ Poll::Pending => Poll::Pending,
+ }
+ }
+
+ fn call(&mut self, dst: Uri) -> Self::Future {
+ let is_https = dst.scheme_str() == Some("https");
+
+ if !is_https {
+ let connecting_future = self.http.call(dst);
+
+ let f = async move {
+ let tcp = connecting_future.await.map_err(Into::into)?;
+
+ Ok(MaybeHttpsStream::Http(tcp))
+ };
+ f.boxed()
+ } else {
+ let cfg = self.tls_config.clone();
+ let connecting_future = self.http.call(dst);
+
+ let dnsname = DNSNameRef::try_from_ascii_str(self.fixed_dnsname)
+ .expect("Invalid fixed dnsname");
+
+ let f = async move {
+ let tcp = connecting_future.await.map_err(Into::into)?;
+ let connector = TlsConnector::from(cfg);
+ let tls = connector
+ .connect(dnsname, tcp)
+ .await
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+ Ok(MaybeHttpsStream::Https(tls))
+ };
+ f.boxed()
+ }
+ }
+}