use crate::config::{Certs, Config, HostName, Log, Path, PreCompressed, Proxy, Settings};
use crate::files::{CompressedFileTypes, CompressedFiles};
use crate::log_middleware::CustomLog;
use actix_rt::spawn;
use actix_service::ServiceFactory;
use actix_web::Scope;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{
App, HttpRequest, HttpResponse, HttpServer,
dev::Server,
guard,
middleware::{Compress, DefaultHeaders, NormalizePath},
web::{Bytes, PayloadConfig, resource, route, scope},
};
use awc::Client;
use aws_lc_rs::digest::{Context, SHA256};
use base64::Engine;
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use blocking::unblock;
use futures_util::StreamExt;
use pem::{encode, parse_many};
use rustls::{
ServerConfig,
crypto::ring::{default_provider, sign::any_supported_type},
server::ResolvesServerCertUsingSni,
sign::CertifiedKey,
};
use rustls_acme::caches::{CompositeCache, DirCache, TestCache};
use rustls_acme::{AcmeConfig, CertCache};
use rustls_pemfile::{certs, private_key};
use async_trait::async_trait;
use std::fs::{create_dir_all, remove_file};
use std::io::ErrorKind;
use std::{
error::Error,
ffi::OsString,
fs::{File, read_dir},
io::BufReader,
path::PathBuf,
sync::Arc,
};
use tracing::{trace, warn};
use url::Url;
#[derive(Debug)]
pub(crate) struct PathGroup {
pub(crate) children: Vec<PathTree>,
pub(crate) path: String,
}
#[derive(Debug)]
pub(crate) enum PathTree {
Group(PathGroup),
Path(Path),
}
impl PathTree {
fn strip_prefix(&mut self, prefix: &str) {
match self {
PathTree::Group(g) => {
g.path = g.path.strip_prefix(prefix).map(|x| x.to_owned()).unwrap();
}
PathTree::Path(p) => {
p.strip_prefix(prefix);
}
}
}
pub fn path(path: Path) -> PathTree {
PathTree::Path(path)
}
pub fn group(path: String, children: Vec<PathTree>) -> PathTree {
PathTree::Group(PathGroup { children, path })
}
}
struct Host {
pub name: Vec<HostName>,
pub path: Vec<PathTree>,
pub proxy: Vec<Proxy>,
pub log: Vec<Log>,
}
enum ListenSSL {
Certs(Vec<Certs>),
Acme(Acme),
None,
}
struct Listen {
sock_addr: String,
ssl: ListenSSL,
}
struct Acme {
mails: Vec<String>,
dir: String,
provider: Option<String>,
test_cache: Option<TestCache>,
domains: Vec<String>,
}
fn certs_for_folder(certs: &mut Vec<Certs>, folder: &str, cert_name: &str, key_name: &str) {
if let Ok(read) = read_dir(folder).map_err(|e| warn!("Fail reading certs folder {} error {}", folder, e)) {
for found in read.flatten() {
let path = found.path();
if path.is_dir() {
let cert = path.join(cert_name).to_str().map(str::to_owned);
let key = path.join(key_name).to_str().map(str::to_owned);
if let (Some(cert), Some(key)) = (&cert, &key) {
certs.push(Certs::new(cert, key));
} else if cert.is_none() && key.is_none() {
warn!(
"fail reading files for cert:'{}' and key:'{}' in path:{:?}",
cert_name, key_name, path
);
}
}
}
}
}
fn sites_in_folder(folders: &mut Vec<(OsString, PathBuf)>, folder: &str) {
if let Ok(read) = read_dir(folder).map_err(|e| warn!("Fail reading auto sites folder {} error {}", folder, e)) {
for found in read.flatten() {
if found.path().is_dir() {
folders.push((found.file_name(), found.path().clone()));
}
}
}
}
pub fn common_prefix(first: &str, second: &str) -> String {
println!("check common {} {}", first, second);
let mut res = String::new();
for (n, c) in first.chars().enumerate() {
if second.chars().nth(n) == Some(c) {
res.push(c);
} else {
break;
}
}
if res == "/" {
"".to_owned()
} else if res.len() == first.len() || res.len() == second.len() {
res
} else if let Some(pos) = res.rfind('/') {
res.truncate(pos + 1);
println!("found common {}", res);
res
} else {
"".to_owned()
}
}
pub(crate) fn pop_stacks(
stack: &mut Vec<(String, Vec<PathTree>)>,
mut tree_to_add: PathTree,
prefix: String,
) -> (Option<String>, Vec<PathTree>) {
loop {
if let Some((parent_prefix, mut parent_group)) = stack.pop() {
let stack_prefix = common_prefix(&parent_prefix, &prefix);
if stack_prefix.is_empty() {
println!("stripping {}", parent_prefix);
tree_to_add.strip_prefix(&parent_prefix);
parent_group.push(tree_to_add);
tree_to_add = PathTree::group(parent_prefix, parent_group);
} else if stack_prefix == parent_prefix {
println!("stripping {}", parent_prefix);
tree_to_add.strip_prefix(&parent_prefix);
parent_group.push(tree_to_add);
break (Some(parent_prefix), parent_group);
} else {
println!("stripping {}", parent_prefix);
tree_to_add.strip_prefix(&parent_prefix);
parent_group.push(tree_to_add);
tree_to_add = PathTree::group(parent_prefix, parent_group);
}
} else {
let mut new_group = Vec::new();
tree_to_add.strip_prefix(&prefix);
new_group.push(tree_to_add);
break (Some(prefix), new_group);
}
}
}
pub(crate) fn read_paths(paths: &Vec<Path>) -> Vec<PathTree> {
let mut paths = paths.clone();
paths.sort_by(|x, y| y.path.cmp(&x.path));
let mut tree = Vec::new();
let mut pre: Option<(Option<String>, Path, Vec<PathTree>)> = None;
let mut stack: Vec<(String, Vec<PathTree>)> = Vec::new();
for path in paths {
pre = if let Some((pp, mut pfa, mut cur_group)) = pre {
let prefix = common_prefix(&pfa.path, &path.path);
if prefix.is_empty() {
let strip_pref = &pp.as_ref().map(|x| x.as_str()).unwrap_or("");
pfa.strip_prefix(&strip_pref);
let to_add = if cur_group.is_empty() {
PathTree::path(pfa)
} else {
cur_group.push(PathTree::path(pfa));
PathTree::group(pp.unwrap(), cur_group)
};
let (_, group) = pop_stacks(&mut stack, to_add, prefix.clone());
if !group.is_empty() {
tree.extend(group);
}
Some((None, path.clone(), Vec::new()))
} else if Some(&prefix) == pp.as_ref() || pp.is_none() {
pfa.strip_prefix(&prefix);
cur_group.push(PathTree::path(pfa));
Some((Some(prefix), path.clone(), cur_group))
} else if prefix.len() > pp.as_ref().map(|x| x.len()).unwrap_or(0) {
stack.push((pp.unwrap(), cur_group));
pfa.strip_prefix(&prefix);
let mut new_group = Vec::new();
new_group.push(PathTree::path(pfa));
Some((Some(prefix), path.clone(), new_group))
} else {
let strip_pref = &pp.as_ref().map(|x| x.as_str()).unwrap_or("");
pfa.strip_prefix(&strip_pref);
let tree_to_add = if cur_group.is_empty() {
PathTree::path(pfa)
} else {
cur_group.push(PathTree::path(pfa));
PathTree::group(pp.unwrap(), cur_group)
};
let (rp, group) = pop_stacks(&mut stack, tree_to_add, prefix);
Some((rp, path, group))
}
} else {
Some((None, path.clone(), Vec::new()))
};
}
let mut pre_pop: Option<PathTree> = None;
while let Some((parent_prefix, mut parent_group)) = stack.pop() {
if let Some(mut to_add) = pre_pop {
to_add.strip_prefix(&parent_prefix);
parent_group.push(to_add);
}
pre_pop = Some(PathTree::group(parent_prefix, parent_group));
}
if let Some(to_add) = pre_pop {
tree.push(to_add);
}
if let Some((p, mut pe, mut data)) = pre {
if data.is_empty() {
tree.push(PathTree::path(pe));
} else {
pe.strip_prefix(p.as_ref().unwrap());
data.push(PathTree::path(pe));
tree.push(PathTree::group(p.unwrap(), data));
}
}
tree
}
pub fn read_config(config: &Config) -> ParsedServerConfig {
let mut hosts = Vec::new();
let mut listens = Vec::new();
let mut settings = Vec::new();
for server in &config.servers {
let mut host_names = Vec::new();
for host in &server.host {
let paths = read_paths(&host.path);
trace!("read paths:{paths:#?}");
let run_host = Host {
name: host.name.clone(),
path: paths,
proxy: host.proxy.clone(),
log: host.log.clone(),
};
hosts.push(run_host);
for name in &host.name {
host_names.push(name.value().clone());
}
}
if let Some(auto) = &server.auto {
let mut folders = Vec::new();
sites_in_folder(&mut folders, auto.base_path());
for (name, path) in folders {
let string_name = name.to_string_lossy().to_string();
let run_host = Host {
name: vec![HostName(string_name.clone())],
path: vec![PathTree::path(Path {
path: "/".to_owned(),
location: path.to_string_lossy().to_string(),
list: auto.list,
headers: auto.headers.clone(),
index: auto.index.clone(),
compressions: PreCompressed::defaults(),
})],
proxy: vec![],
log: vec![],
};
hosts.push(run_host);
host_names.push(string_name);
}
}
for bind in &server.listen {
let mut ssl = Vec::new();
let listen_ssl = if let Some(ssl_config) = bind.ssl() {
ssl.extend_from_slice(&ssl_config.certs);
if let Some(lets) = &ssl_config.letsencrypt {
certs_for_folder(&mut ssl, lets.as_str(), "fullchain.pem", "privkey.pem");
}
if let Some(lets) = &ssl_config.acme_redirect {
certs_for_folder(&mut ssl, lets.as_str(), "fullchain", "privkey");
}
if let Some(acme_config) = &ssl_config.acme {
let mut domains = host_names.clone();
if let Some(extra_domains) = acme_config.extra_domains() {
domains.extend(extra_domains.clone());
}
let acme = Acme {
mails: acme_config.contacts().clone(),
dir: acme_config.store().clone(),
provider: acme_config.provider().clone(),
test_cache: None,
domains: domains,
};
ListenSSL::Acme(acme)
} else {
ListenSSL::Certs(ssl)
}
} else {
ListenSSL::None
};
let listen = Listen {
sock_addr: bind.sock_addr(),
ssl: listen_ssl,
};
listens.push(listen);
}
if let Some(sett) = &server.settings {
settings.push(sett.clone());
}
}
ParsedServerConfig {
hosts,
listens,
settings,
}
}
struct MissingKey();
impl Error for MissingKey {}
impl std::fmt::Display for MissingKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "key not present")
}
}
impl std::fmt::Debug for MissingKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "key not present")
}
}
fn read_cert_and_domains(cert: &Certs) -> Result<(CertifiedKey, Vec<String>), Box<dyn Error>> {
let cert_file = &mut BufReader::new(File::open(&cert.cert)?);
let key_file = &mut BufReader::new(File::open(&cert.key)?);
let cert_chain: Vec<_> = certs(cert_file).filter_map(|x| x.ok()).collect();
let key_read = private_key(key_file)?;
let key = any_supported_type(&key_read.ok_or_else(MissingKey)?)?;
let ck = CertifiedKey::new(cert_chain.clone(), key.clone());
let der = rustls_pki_types::CertificateDer::from(ck.end_entity_cert()?.clone());
let we = webpki::EndEntityCert::try_from(&der)?;
let names = we.valid_dns_names().map(|x| x.to_owned()).collect();
Ok((ck, names))
}
fn path_to_scope<T>(path_config: &Path, target: Scope<T>) -> Scope<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = actix_web::Error,
InitError = (),
>,
{
let path = path_config.path.trim_end_matches('/');
trace!("configuring path '{}' to '{}'", path, path_config.location);
let scope = scope(path);
let mut dh = DefaultHeaders::new();
for header in &path_config.headers {
dh = dh.add((header.name(), header.value()));
}
let scope = scope.wrap(dh);
let mut files = CompressedFiles::new("", &path_config.location);
files = files.use_hidden_files();
if path_config.list {
files = files.show_files_listing();
}
files = files.index_file(path_config.index.as_deref().unwrap_or("index.html"));
files = files.pre_compressed(CompressedFileTypes::from(&path_config.compressions));
let scope = scope.service(files);
target.service(scope)
}
fn group_to_scope<T>(g: &PathGroup, target: Scope<T>) -> Scope<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = actix_web::Error,
InitError = (),
>,
{
let path = g.path.trim_end_matches('/');
trace!("configuring group '{}'", g.path);
let mut scope = scope(&path);
for path_config in &g.children {
match path_config {
PathTree::Group(g) => {
scope = group_to_scope(g, scope);
}
PathTree::Path(path_config) => {
if !path_config.path.is_empty() {
scope = path_to_scope(&path_config, scope);
}
}
}
}
for path_config in &g.children {
match path_config {
PathTree::Path(path_config) => {
if path_config.path.is_empty() {
trace!("setting path to group '{}', to '{}'", g.path, path_config.location);
let mut dh = DefaultHeaders::new();
for header in &path_config.headers {
dh = dh.add((header.name(), header.value()));
}
let scope = scope.wrap(dh);
let mut files = CompressedFiles::new("", &path_config.location);
files = files.use_hidden_files();
if path_config.list {
files = files.show_files_listing();
}
files = files.index_file(path_config.index.as_deref().unwrap_or("index.html"));
files = files.pre_compressed(CompressedFileTypes::from(&path_config.compressions));
let scope = scope.service(files);
return target.service(scope);
}
}
_ => {}
}
}
target.service(scope)
}
pub struct ParsedServerConfig {
hosts: Vec<Host>,
listens: Vec<Listen>,
settings: Vec<Settings>,
}
impl ParsedServerConfig {
pub fn set_ssl_test_cache(&mut self, test_cache: TestCache) {
for ls in &mut self.listens {
if let ListenSSL::Acme(acme) = &mut ls.ssl {
acme.test_cache = Some(test_cache);
break;
}
}
}
}
pub fn server(config: Config) -> std::io::Result<Server> {
let parsed_config = read_config(&config);
server_internal(parsed_config)
}
pub fn server_internal(
ParsedServerConfig {
hosts,
listens,
settings,
}: ParsedServerConfig,
) -> std::io::Result<Server> {
let hosts_ref = Arc::new(hosts);
let mapping = hosts_ref.clone();
let mut server = HttpServer::new(move || {
let app = App::new();
let app = app.wrap(NormalizePath::trim());
let app = app.wrap(Compress::default());
let mut app = app.wrap(DefaultHeaders::new().add(("Server", "Vacuna")));
for host in mapping.iter() {
let mut scope = scope("");
for proxy in &host.proxy {
let client = Client::builder().disable_redirects().no_default_headers().finish();
if let Ok(val) = Url::parse(&proxy.url).map_err(|e| warn!("Ignoring proxy for error parsing url {}", e))
{
let sett = settings.clone();
scope = scope.service(resource(format!("{}{}", proxy.path, "{tail}*")).default_service(
route().to(move |req, body| forward(req, val.clone(), body, client.clone(), sett.clone())),
));
}
}
for path_config in &host.path {
match path_config {
PathTree::Group(g) => {
scope = group_to_scope(g, scope);
}
PathTree::Path(path_config) => {
scope = path_to_scope(path_config, scope);
}
}
}
let mut cur_guard: Option<guard::AnyGuard> = None;
for host_name in &host.name {
cur_guard = if let Some(g) = cur_guard {
Some(g.or(guard::Host(host_name.value())))
} else {
Some(guard::Any(guard::Host(host_name.value())))
};
}
if let Some(g) = cur_guard {
scope = scope.guard(g);
}
let scope = scope.wrap(CustomLog::new(&host.log));
app = app.service(scope);
}
for sett in &settings {
if let Some(limit) = sett.payload_limit {
app = app.app_data(PayloadConfig::new(limit as usize));
}
}
app
});
for bind in listens {
match bind.ssl {
ListenSSL::Certs(certs) => {
trace!("configuring ssl bind '{}'", bind.sock_addr);
let mut certs_resolver = ResolvesServerCertUsingSni::new();
for cert in certs {
if let Ok((ck, domains)) = read_cert_and_domains(&cert) {
for domain in domains {
trace!("binding dns {} for cert {}", domain, cert.cert);
if let Err(e) = certs_resolver.add(&domain, ck.clone()) {
warn!("failing adding cert for domain:'{}' ssl listening '{}'", domain, e);
}
}
}
}
let ssl = ServerConfig::builder_with_provider(Arc::new(default_provider()))
.with_safe_default_protocol_versions()
.expect("got providers")
.with_no_client_auth()
.with_cert_resolver(Arc::new(certs_resolver));
server = server.bind_rustls_0_23(&bind.sock_addr, ssl)?;
}
ListenSSL::Acme(mut acme) => {
let cache = CompositeCache::new(FilesCache::new(acme.dir.clone())?, DirCache::new(acme.dir.clone()));
let mut config = AcmeConfig::new(acme.domains)
.contact(acme.mails.clone())
.cache(cache)
.directory_lets_encrypt(acme.test_cache.is_none());
if let Some(prov) = &acme.provider {
config = config.directory(prov);
}
if let Some(test_cache) = acme.test_cache.take() {
config = config.cache(test_cache);
}
let mut state = config.state();
let rustls_config = Arc::into_inner(state.challenge_rustls_config()).unwrap();
spawn(async move {
loop {
match state.next().await.unwrap() {
Ok(ok) => log::info!("event: {:?}", ok),
Err(err) => log::error!("error: {:?}", err),
}
}
});
server = server.bind_rustls_0_23(&bind.sock_addr, rustls_config)?;
}
ListenSSL::None => {
trace!("configuring bind '{}'", bind.sock_addr);
server = server.bind(&bind.sock_addr)?;
}
}
}
Ok(server.run())
}
async fn forward(
req: HttpRequest,
mut forward_url: Url,
body: Bytes,
client: Client,
settings: Vec<Settings>,
) -> actix_web::Result<HttpResponse> {
forward_url.set_path(&req.match_info()["tail"]);
forward_url.set_query(req.uri().query());
let forwarded_req = client.request_from(forward_url.as_str(), req.head()).no_decompress();
let forwarded_req = if let Some(addr) = req.head().peer_addr {
forwarded_req.insert_header(("X-Forwarded-For", format!("{}", addr.ip())))
} else {
forwarded_req
};
trace!("forwarding {:?}", forward_url);
match forwarded_req.send_body(body).await {
Ok(mut res) => {
let mut client_resp = HttpResponse::build(res.status());
for (header_name, header_value) in res
.headers()
.iter()
.filter(|(h, _)| *h != "connection" && *h != "content-length")
{
client_resp.append_header((header_name.clone(), header_value.clone()));
}
let mut body = res.body();
for sett in &settings {
if let Some(limit) = sett.payload_limit {
body = body.limit(limit as usize);
}
}
Ok(client_resp.body(body.await?))
}
Err(e) => Ok(HttpResponse::InternalServerError().body(format!("{}", e))),
}
}
pub(crate) struct FilesCache {
path: PathBuf,
}
impl FilesCache {
pub(crate) fn new<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<Self> {
Ok(Self {
path: path.as_ref().to_path_buf(),
})
}
fn cached_cert_file_name(domains: &[String], directory_url: impl AsRef<str>) -> String {
let mut ctx = Context::new(&SHA256);
for domain in domains {
ctx.update(domain.as_ref());
ctx.update(&[0])
}
ctx.update(directory_url.as_ref().as_bytes());
let hash = BASE64_URL_SAFE_NO_PAD.encode(ctx.finish());
format!("cached_cert_{hash}")
}
async fn read_if_exist(&self, file: impl AsRef<std::path::Path>) -> Result<Option<Vec<u8>>, std::io::Error> {
let path = file.as_ref().to_path_buf();
match unblock(move || std::fs::read(&path)).await {
Ok(content) => Ok(Some(content)),
Err(err) => match err.kind() {
ErrorKind::NotFound => Ok(None),
_ => Err(err),
},
}
}
async fn write(&self, file: impl AsRef<std::path::Path>, contents: impl AsRef<[u8]>) -> Result<(), std::io::Error> {
let path = file.as_ref().to_path_buf();
let contents = contents.as_ref().to_owned();
unblock(move || std::fs::write(path, contents)).await
}
async fn create_domain_links(&self, domains: &[String], cert_dir: &PathBuf) -> Result<(), std::io::Error> {
for domain in domains {
let domain_link_path = self.path.join(domain);
let cert_dir_name = cert_dir
.file_name()
.ok_or_else(|| std::io::Error::new(ErrorKind::InvalidData, "Invalid cert directory path"))?;
if domain_link_path.exists() {
if domain_link_path.is_symlink() {
let path_to_remove = domain_link_path.clone();
unblock(move || remove_file(&path_to_remove)).await?;
} else {
warn!("Cannot delete '{}' is not a link", domain_link_path.display());
}
}
let link_path = domain_link_path.clone();
let cert_dir_relative = PathBuf::from(cert_dir_name);
unblock(move || create_dir_link(cert_dir_relative, &link_path)).await?;
trace!("Created symbolic link: {} -> {}", domain, cert_dir.display());
}
Ok(())
}
}
#[cfg(unix)]
fn create_dir_link<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(original: P, link: Q) -> std::io::Result<()> {
use std::os::unix::fs::symlink;
symlink(original, link)
}
#[cfg(windows)]
fn create_dir_link<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(original: P, link: Q) -> std::io::Result<()> {
use std::os::windows::fs::symlink_dir;
symlink_dir(original, link)
}
#[cfg(not(any(windows, unix)))]
fn create_dir_link<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(original: P, link: Q) -> std::io::Result<()> {
warn!("Creation of fs link not supported by environment");
Ok(())
}
#[async_trait]
impl CertCache for FilesCache {
type EC = std::io::Error;
async fn load_cert(&self, domains: &[String], directory_url: &str) -> Result<Option<Vec<u8>>, Self::EC> {
let dir = Self::cached_cert_file_name(domains, directory_url);
let cert_dir = self.path.join(dir);
let mut res = Vec::new();
let privkey_path = cert_dir.join("privkey.pem");
if let Some(privkey) = self.read_if_exist(&privkey_path).await? {
res.extend(privkey);
} else {
return Ok(None);
}
let fullchain_path = cert_dir.join("fullchain.pem");
if let Some(fullchain) = self.read_if_exist(&fullchain_path).await? {
res.extend(fullchain);
} else {
return Ok(None);
}
Ok(Some(res))
}
async fn store_cert(&self, domains: &[String], directory_url: &str, cert: &[u8]) -> Result<(), Self::EC> {
let dir = Self::cached_cert_file_name(domains, directory_url);
let cert_dir = self.path.join(dir);
let to_create = cert_dir.clone();
unblock(move || create_dir_all(&to_create)).await?;
let pem_blocks = parse_many(cert).map_err(|e| std::io::Error::new(ErrorKind::InvalidData, e))?;
let mut cert_pems = Vec::new();
let mut key_pem = None;
for pem in &pem_blocks {
match pem.tag() {
"CERTIFICATE" => cert_pems.push(pem.clone()),
"PRIVATE KEY" | "RSA PRIVATE KEY" | "EC PRIVATE KEY" => {
if key_pem.is_none() {
key_pem = Some(pem.clone());
}
}
_ => {} }
}
if !cert_pems.is_empty() {
let mut fullchain_content = Vec::new();
for cert_pem in &cert_pems {
fullchain_content.extend_from_slice(encode(cert_pem).as_bytes());
}
let fullchain_path = cert_dir.join("fullchain.pem");
self.write(&fullchain_path, fullchain_content).await?;
if let Some(first_cert) = cert_pems.first() {
let cert_path = cert_dir.join("cert.pem");
self.write(&cert_path, encode(first_cert).as_bytes()).await?;
}
if cert_pems.len() > 1 {
let mut chain_content = Vec::new();
for cert_pem in cert_pems.iter().skip(1) {
chain_content.extend_from_slice(encode(cert_pem).as_bytes());
}
let chain_path = cert_dir.join("chain.pem");
self.write(&chain_path, chain_content).await?;
}
}
if let Some(key) = key_pem {
let privkey_path = cert_dir.join("privkey.pem");
self.write(&privkey_path, encode(&key).as_bytes()).await?;
}
self.create_domain_links(domains, &cert_dir).await?;
Ok(())
}
}