2020-06-18 02:30:42 +02:00
|
|
|
#![deny(warnings)]
|
2020-08-01 12:31:05 +02:00
|
|
|
#![warn(unused_extern_crates)]
|
|
|
|
#![deny(clippy::unwrap_used)]
|
|
|
|
#![deny(clippy::expect_used)]
|
|
|
|
#![deny(clippy::panic)]
|
|
|
|
#![deny(clippy::unreachable)]
|
|
|
|
#![deny(clippy::await_holding_lock)]
|
|
|
|
#![deny(clippy::needless_pass_by_value)]
|
|
|
|
#![deny(clippy::trivially_copy_pass_by_ref)]
|
|
|
|
|
2020-02-13 00:43:01 +01:00
|
|
|
#[macro_use]
|
|
|
|
extern crate log;
|
|
|
|
|
2020-07-28 08:55:58 +02:00
|
|
|
use users::{get_current_gid, get_current_uid, get_effective_gid, get_effective_uid};
|
|
|
|
|
|
|
|
use std::fs::metadata;
|
|
|
|
use std::os::unix::fs::MetadataExt;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
2020-02-13 00:43:01 +01:00
|
|
|
use bytes::{BufMut, BytesMut};
|
|
|
|
use futures::SinkExt;
|
|
|
|
use futures::StreamExt;
|
2020-02-15 01:27:25 +01:00
|
|
|
use libc::umask;
|
2020-02-13 00:43:01 +01:00
|
|
|
use std::error::Error;
|
|
|
|
use std::io;
|
|
|
|
use std::sync::Arc;
|
|
|
|
use tokio::net::{UnixListener, UnixStream};
|
|
|
|
use tokio_util::codec::Framed;
|
|
|
|
use tokio_util::codec::{Decoder, Encoder};
|
|
|
|
|
|
|
|
use kanidm_client::KanidmClientBuilder;
|
|
|
|
|
|
|
|
use kanidm_unix_common::cache::CacheLayer;
|
2020-02-29 05:02:14 +01:00
|
|
|
use kanidm_unix_common::unix_config::KanidmUnixdConfig;
|
2020-02-13 00:43:01 +01:00
|
|
|
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse};
|
|
|
|
|
|
|
|
//=== the codec
|
|
|
|
|
|
|
|
struct ClientCodec;
|
|
|
|
|
|
|
|
impl Decoder for ClientCodec {
|
|
|
|
type Item = ClientRequest;
|
|
|
|
type Error = io::Error;
|
|
|
|
|
|
|
|
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
|
|
|
|
match serde_cbor::from_slice::<ClientRequest>(&src) {
|
|
|
|
Ok(msg) => {
|
|
|
|
// Clear the buffer for the next message.
|
|
|
|
src.clear();
|
|
|
|
Ok(Some(msg))
|
|
|
|
}
|
|
|
|
_ => Ok(None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-11 02:32:56 +02:00
|
|
|
impl Encoder<ClientResponse> for ClientCodec {
|
2020-02-13 00:43:01 +01:00
|
|
|
type Error = io::Error;
|
|
|
|
|
|
|
|
fn encode(&mut self, msg: ClientResponse, dst: &mut BytesMut) -> Result<(), Self::Error> {
|
2020-02-29 05:02:14 +01:00
|
|
|
debug!("Attempting to send response -> {:?} ...", msg);
|
2020-02-13 00:43:01 +01:00
|
|
|
let data = serde_cbor::to_vec(&msg).map_err(|e| {
|
|
|
|
error!("socket encoding error -> {:?}", e);
|
|
|
|
io::Error::new(io::ErrorKind::Other, "CBOR encode error")
|
|
|
|
})?;
|
|
|
|
dst.put(data.as_slice());
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ClientCodec {
|
|
|
|
fn new() -> Self {
|
|
|
|
ClientCodec
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn rm_if_exist(p: &str) {
|
|
|
|
let _ = std::fs::remove_file(p).map_err(|e| {
|
2020-07-28 08:55:58 +02:00
|
|
|
warn!("attempting to remove {:?} -> {:?}", p, e);
|
2020-02-13 00:43:01 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn handle_client(
|
|
|
|
sock: UnixStream,
|
|
|
|
cachelayer: Arc<CacheLayer>,
|
|
|
|
) -> Result<(), Box<dyn Error>> {
|
|
|
|
debug!("Accepted connection");
|
|
|
|
|
|
|
|
let mut reqs = Framed::new(sock, ClientCodec::new());
|
|
|
|
|
|
|
|
while let Some(Ok(req)) = reqs.next().await {
|
2020-02-15 01:27:25 +01:00
|
|
|
let resp = match req {
|
2020-02-13 00:43:01 +01:00
|
|
|
ClientRequest::SshKey(account_id) => {
|
2020-02-15 01:27:25 +01:00
|
|
|
debug!("sshkey req");
|
|
|
|
cachelayer
|
|
|
|
.get_sshkeys(account_id.as_str())
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::SshKeys)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
2020-02-13 00:43:01 +01:00
|
|
|
error!("unable to load keys, returning empty set.");
|
|
|
|
ClientResponse::SshKeys(vec![])
|
2020-02-15 01:27:25 +01:00
|
|
|
})
|
2020-02-13 00:43:01 +01:00
|
|
|
}
|
2020-02-15 01:27:25 +01:00
|
|
|
ClientRequest::NssAccounts => {
|
|
|
|
debug!("nssaccounts req");
|
|
|
|
cachelayer
|
|
|
|
.get_nssaccounts()
|
2020-08-04 04:58:11 +02:00
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::NssAccounts)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
error!("unable to enum accounts");
|
|
|
|
ClientResponse::NssAccounts(Vec::new())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
ClientRequest::NssAccountByUid(gid) => {
|
|
|
|
debug!("nssaccountbyuid req");
|
|
|
|
cachelayer
|
|
|
|
.get_nssaccount_gid(gid)
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::NssAccount)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
error!("unable to load account, returning empty.");
|
|
|
|
ClientResponse::NssAccount(None)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
ClientRequest::NssAccountByName(account_id) => {
|
|
|
|
debug!("nssaccountbyname req");
|
|
|
|
cachelayer
|
|
|
|
.get_nssaccount_name(account_id.as_str())
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::NssAccount)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
error!("unable to load account, returning empty.");
|
|
|
|
ClientResponse::NssAccount(None)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
ClientRequest::NssGroups => {
|
|
|
|
debug!("nssgroups req");
|
|
|
|
cachelayer
|
|
|
|
.get_nssgroups()
|
2020-08-04 04:58:11 +02:00
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::NssGroups)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
error!("unable to enum groups");
|
|
|
|
ClientResponse::NssGroups(Vec::new())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
ClientRequest::NssGroupByGid(gid) => {
|
|
|
|
debug!("nssgroupbygid req");
|
|
|
|
cachelayer
|
|
|
|
.get_nssgroup_gid(gid)
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::NssGroup)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
error!("unable to load group, returning empty.");
|
|
|
|
ClientResponse::NssGroup(None)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
ClientRequest::NssGroupByName(grp_id) => {
|
|
|
|
debug!("nssgroupbyname req");
|
|
|
|
cachelayer
|
|
|
|
.get_nssgroup_name(grp_id.as_str())
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::NssGroup)
|
2020-02-15 01:27:25 +01:00
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
error!("unable to load group, returning empty.");
|
|
|
|
ClientResponse::NssGroup(None)
|
|
|
|
})
|
|
|
|
}
|
2020-02-29 05:02:14 +01:00
|
|
|
ClientRequest::PamAuthenticate(account_id, cred) => {
|
|
|
|
debug!("pam authenticate");
|
|
|
|
cachelayer
|
|
|
|
.pam_account_authenticate(account_id.as_str(), cred.as_str())
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::PamStatus)
|
2020-02-29 05:02:14 +01:00
|
|
|
.unwrap_or(ClientResponse::Error)
|
|
|
|
}
|
|
|
|
ClientRequest::PamAccountAllowed(account_id) => {
|
|
|
|
debug!("pam account allowed");
|
|
|
|
cachelayer
|
|
|
|
.pam_account_allowed(account_id.as_str())
|
|
|
|
.await
|
2020-06-18 02:30:42 +02:00
|
|
|
.map(ClientResponse::PamStatus)
|
2020-02-29 05:02:14 +01:00
|
|
|
.unwrap_or(ClientResponse::Error)
|
|
|
|
}
|
2020-02-15 01:27:25 +01:00
|
|
|
ClientRequest::InvalidateCache => {
|
|
|
|
debug!("invalidate cache");
|
|
|
|
cachelayer
|
|
|
|
.invalidate()
|
2020-08-04 04:58:11 +02:00
|
|
|
.await
|
2020-02-15 01:27:25 +01:00
|
|
|
.map(|_| ClientResponse::Ok)
|
|
|
|
.unwrap_or(ClientResponse::Error)
|
|
|
|
}
|
|
|
|
ClientRequest::ClearCache => {
|
|
|
|
debug!("clear cache");
|
|
|
|
cachelayer
|
|
|
|
.clear_cache()
|
2020-08-04 04:58:11 +02:00
|
|
|
.await
|
2020-02-15 01:27:25 +01:00
|
|
|
.map(|_| ClientResponse::Ok)
|
|
|
|
.unwrap_or(ClientResponse::Error)
|
|
|
|
}
|
|
|
|
ClientRequest::Status => {
|
|
|
|
debug!("status check");
|
|
|
|
if cachelayer.test_connection().await {
|
|
|
|
ClientResponse::Ok
|
|
|
|
} else {
|
|
|
|
ClientResponse::Error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
reqs.send(resp).await?;
|
|
|
|
reqs.flush().await?;
|
|
|
|
debug!("flushed response!");
|
2020-02-13 00:43:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Disconnect them
|
|
|
|
debug!("Disconnecting client ...");
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-01-10 04:41:56 +01:00
|
|
|
#[tokio::main]
|
2020-02-13 00:43:01 +01:00
|
|
|
async fn main() {
|
2020-07-28 08:55:58 +02:00
|
|
|
let cuid = get_current_uid();
|
|
|
|
let ceuid = get_effective_uid();
|
|
|
|
let cgid = get_current_gid();
|
|
|
|
let cegid = get_effective_gid();
|
|
|
|
|
|
|
|
if cuid == 0 || ceuid == 0 || cgid == 0 || cegid == 0 {
|
|
|
|
eprintln!("Refusing to run - this process must not operate as root.");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
|
2020-02-16 00:29:48 +01:00
|
|
|
// ::std::env::set_var("RUST_LOG", "kanidm=debug,kanidm_client=debug");
|
2020-02-13 00:43:01 +01:00
|
|
|
env_logger::init();
|
|
|
|
|
2020-07-28 08:55:58 +02:00
|
|
|
let cfg_path = Path::new("/etc/kanidm/config");
|
2020-09-08 04:46:10 +02:00
|
|
|
let cfg_path_str = match cfg_path.to_str() {
|
|
|
|
Some(cps) => cps,
|
|
|
|
None => {
|
|
|
|
error!("Unable to turn cfg_path to str");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
2020-07-28 08:55:58 +02:00
|
|
|
if cfg_path.exists() {
|
|
|
|
let cfg_meta = match metadata(&cfg_path) {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(e) => {
|
2020-09-08 04:46:10 +02:00
|
|
|
error!("Unable to read metadata for {} - {:?}", cfg_path_str, e);
|
2020-07-28 08:55:58 +02:00
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if !cfg_meta.permissions().readonly() {
|
|
|
|
warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...",
|
2020-09-08 04:46:10 +02:00
|
|
|
cfg_path_str
|
|
|
|
);
|
2020-07-28 08:55:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if cfg_meta.uid() == cuid || cfg_meta.uid() == ceuid {
|
|
|
|
warn!("WARNING: {} owned by the current uid, which may allow file permission changes. This could be a security risk ...",
|
2020-09-08 04:46:10 +02:00
|
|
|
cfg_path_str
|
2020-07-28 08:55:58 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-01 07:31:39 +02:00
|
|
|
let unixd_path = Path::new("/etc/kanidm/unixd");
|
2020-09-08 04:46:10 +02:00
|
|
|
let unixd_path_str = match unixd_path.to_str() {
|
|
|
|
Some(cps) => cps,
|
|
|
|
None => {
|
|
|
|
error!("Unable to turn unixd_path to str");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
2020-07-28 08:55:58 +02:00
|
|
|
if unixd_path.exists() {
|
|
|
|
let unixd_meta = match metadata(&unixd_path) {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(e) => {
|
2020-09-08 04:46:10 +02:00
|
|
|
error!("Unable to read metadata for {} - {:?}", unixd_path_str, e);
|
2020-07-28 08:55:58 +02:00
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
if !unixd_meta.permissions().readonly() {
|
|
|
|
warn!("permissions on {} may not be secure. Should be readonly to running uid. This could be a security risk ...",
|
2020-09-08 04:46:10 +02:00
|
|
|
unixd_path_str);
|
2020-07-28 08:55:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if unixd_meta.uid() == cuid || unixd_meta.uid() == ceuid {
|
|
|
|
warn!("WARNING: {} owned by the current uid, which may allow file permission changes. This could be a security risk ...",
|
2020-09-08 04:46:10 +02:00
|
|
|
unixd_path_str
|
2020-07-28 08:55:58 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-13 00:43:01 +01:00
|
|
|
// setup
|
2020-07-28 08:55:58 +02:00
|
|
|
let cb = match KanidmClientBuilder::new().read_options_from_optional_config(cfg_path) {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(_) => {
|
2020-09-08 04:46:10 +02:00
|
|
|
error!("Failed to parse {}", cfg_path_str);
|
2020-07-28 08:55:58 +02:00
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
2020-02-13 00:43:01 +01:00
|
|
|
|
2020-07-28 08:55:58 +02:00
|
|
|
let cfg = match KanidmUnixdConfig::new().read_options_from_optional_config(unixd_path) {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(_) => {
|
2020-09-08 04:46:10 +02:00
|
|
|
error!("Failed to parse {}", unixd_path_str);
|
2020-07-28 08:55:58 +02:00
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
2020-02-29 05:02:14 +01:00
|
|
|
|
|
|
|
rm_if_exist(cfg.sock_path.as_str());
|
|
|
|
|
|
|
|
let cb = cb.connect_timeout(cfg.conn_timeout);
|
2020-02-13 00:43:01 +01:00
|
|
|
|
2020-09-08 04:46:10 +02:00
|
|
|
let rsclient = match cb.build_async() {
|
|
|
|
Ok(rsc) => rsc,
|
|
|
|
Err(_e) => {
|
|
|
|
error!("Failed to build async client");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
2020-02-13 00:43:01 +01:00
|
|
|
|
2020-07-28 08:55:58 +02:00
|
|
|
// Check the pb path will be okay.
|
|
|
|
if cfg.db_path != "" {
|
|
|
|
let db_path = PathBuf::from(cfg.db_path.as_str());
|
|
|
|
// We only need to check the parent folder path permissions as the db itself may not
|
|
|
|
// exist yet.
|
|
|
|
if let Some(db_parent_path) = db_path.parent() {
|
|
|
|
if !db_parent_path.exists() {
|
|
|
|
error!(
|
|
|
|
"Refusing to run, DB folder {} does not exist",
|
2020-09-08 04:46:10 +02:00
|
|
|
db_parent_path
|
|
|
|
.to_str()
|
|
|
|
.unwrap_or_else(|| "<db_parent_path invalid>")
|
2020-07-28 08:55:58 +02:00
|
|
|
);
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
let db_par_path_buf = db_parent_path.to_path_buf();
|
|
|
|
|
|
|
|
let i_meta = match metadata(&db_par_path_buf) {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(e) => {
|
|
|
|
error!(
|
|
|
|
"Unable to read metadata for {} - {:?}",
|
2020-09-08 04:46:10 +02:00
|
|
|
db_par_path_buf
|
|
|
|
.to_str()
|
|
|
|
.unwrap_or_else(|| "<db_par_path_buf invalid>"),
|
2020-07-28 08:55:58 +02:00
|
|
|
e
|
|
|
|
);
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if !i_meta.is_dir() {
|
|
|
|
error!(
|
|
|
|
"Refusing to run - DB folder {} may not be a directory",
|
2020-09-08 04:46:10 +02:00
|
|
|
db_par_path_buf
|
|
|
|
.to_str()
|
|
|
|
.unwrap_or_else(|| "<db_par_path_buf invalid>")
|
2020-07-28 08:55:58 +02:00
|
|
|
);
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
if i_meta.permissions().readonly() {
|
2020-09-08 04:46:10 +02:00
|
|
|
warn!("WARNING: DB folder permissions on {} indicate it may not be RW. This could cause the server start up to fail!", db_par_path_buf.to_str()
|
|
|
|
.unwrap_or_else(|| "<db_par_path_buf invalid>")
|
|
|
|
);
|
2020-07-28 08:55:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if i_meta.mode() & 0o007 != 0 {
|
2020-09-08 04:46:10 +02:00
|
|
|
warn!("WARNING: DB folder {} has 'everyone' permission bits in the mode. This could be a security risk ...", db_par_path_buf.to_str()
|
|
|
|
.unwrap_or_else(|| "<db_par_path_buf invalid>")
|
|
|
|
);
|
2020-07-28 08:55:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-08 04:46:10 +02:00
|
|
|
let cl_inner = match CacheLayer::new(
|
|
|
|
cfg.db_path.as_str(), // The sqlite db path
|
|
|
|
cfg.cache_timeout,
|
|
|
|
rsclient,
|
|
|
|
cfg.pam_allowed_login_groups.clone(),
|
|
|
|
cfg.default_shell.clone(),
|
|
|
|
cfg.home_prefix.clone(),
|
|
|
|
cfg.home_attr,
|
|
|
|
cfg.uid_attr_map,
|
|
|
|
cfg.gid_attr_map,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
{
|
|
|
|
Ok(c) => c,
|
|
|
|
Err(_e) => {
|
|
|
|
error!("Failed to build cache layer.");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let cachelayer = Arc::new(cl_inner);
|
2020-02-13 00:43:01 +01:00
|
|
|
|
2020-02-15 01:27:25 +01:00
|
|
|
// Set the umask while we open the path
|
|
|
|
let before = unsafe { umask(0) };
|
2021-01-10 04:41:56 +01:00
|
|
|
let listener = match UnixListener::bind(cfg.sock_path.as_str()) {
|
2020-09-08 04:46:10 +02:00
|
|
|
Ok(l) => l,
|
|
|
|
Err(_e) => {
|
|
|
|
error!("Failed to bind unix socket.");
|
|
|
|
std::process::exit(1);
|
|
|
|
}
|
|
|
|
};
|
2020-02-15 01:27:25 +01:00
|
|
|
// Undo it.
|
|
|
|
let _ = unsafe { umask(before) };
|
2020-02-13 00:43:01 +01:00
|
|
|
|
2020-12-28 00:41:16 +01:00
|
|
|
// TODO: Setup a task that handles pre-fetching here.
|
|
|
|
|
2020-02-13 00:43:01 +01:00
|
|
|
let server = async move {
|
2021-01-10 04:41:56 +01:00
|
|
|
loop {
|
|
|
|
match listener.accept().await {
|
|
|
|
Ok((socket, _addr)) => {
|
2020-02-13 00:43:01 +01:00
|
|
|
let cachelayer_ref = cachelayer.clone();
|
2020-05-08 02:44:31 +02:00
|
|
|
tokio::spawn(async move {
|
|
|
|
if let Err(e) = handle_client(socket, cachelayer_ref.clone()).await {
|
|
|
|
error!("an error occured; error = {:?}", e);
|
|
|
|
}
|
|
|
|
});
|
2020-02-13 00:43:01 +01:00
|
|
|
}
|
|
|
|
Err(err) => {
|
|
|
|
error!("Accept error -> {:?}", err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
info!("Server started ...");
|
|
|
|
|
|
|
|
server.await;
|
|
|
|
}
|