Compare commits

..

3 Commits

Author SHA1 Message Date
Brian Picciano
0e164f739d Small fixes after some testing 2023-08-03 16:28:28 +02:00
Brian Picciano
dcbf45ec85 Allow for disabling https for proxied domains 2023-08-03 14:58:49 +02:00
Brian Picciano
7a1a2297d4 Move proxy config into domain (bigger change than it sounds like) 2023-08-03 14:14:51 +02:00
13 changed files with 409 additions and 499 deletions

View File

@ -79,6 +79,36 @@ domain:
# domain list, but will not be configurable in the web interface
#public: false
#proxied:
# An example built-in domain backed by an gemini and HTTP reverse-proxies to
# other backends.
#
# HTTP requests will be proxied to http_url, and gemini requests will be
# proxied to gemini_url. Either can be null.
#
# HTTP requests to the backing service will automatically have
# X-Forwarded-For and (if HTTPS) X-Forwarded-Proto headers added to them.
#
# Proxies are currently limited in the following ways:
# * http_url must be to an http endpoint (not https)
# * dns.resolver_addr is ignored and the system-wide dns is used
#
#example.com:
#http_url: "http://some.other.service.com"
#gemini_url: "gemini://some.other.service.com"
# Extra headers to add to proxied requests
#http_request_headers:
# - name: Host
# value: "yet.another.service.com"
# - name: X-HEADER-TO-DELETE
# value: ""
# Set to true to prevent the domain from being served over https.
#https_disabled: false
service:
# Passphrase which must be given by users who are configuring new domains via
@ -91,7 +121,7 @@ service:
#
# A CNAME record with the interface_domain of this server is automatically
# included, if it's not null itself.
dns_records:
#dns_records:
#- kind: A
# addr: 127.0.0.1
@ -119,45 +149,11 @@ service:
# enabled. You can enable HTTPS by setting this to "[::]:443".
#https_addr: null
#proxied_domains:
# An example built-in domain backed by an HTTP reverse-proxy to some
# other web-service. Requests to the backing service will automatically
# have X-Forwarded-For and (if HTTPS) X-Forwarded-Proto headers added to
# them.
#
# Proxies are currently limited in the following ways:
# * url must be to an http endpoint (not https)
# * dns.resolver_addr is ignored and the system-wide dns is used
#
#proxy.example.com:
# url: "http://some.other.service.com"
#
# # Extra headers to add to proxied requests
# request_headers:
# - name: Host
# value: "yet.another.service.com"
# - name: X-HEADER-TO-DELETE
# value: ""
#gemini:
# The address to listen for gemini requests on. Set this to null to disable
# gemini support.
#gemini_addr: "[::]:3965"
#proxied_domains:
# An example built-in domain backed by a reverse-proxy to some other
# gemini server. Requests to this domain will have connections
# transparently proxied to the backing server.
#
# Proxies are currently limited in the following ways:
# * url must be to a gemini endpoint
# * dns.resolver_addr is ignored and the system-wide dns is used
#
#proxy.example.com:
# url: "gemini://some.other.service.com"
```
The YAML config file can be passed to the Domani process via the `--config-path`

View File

@ -1,13 +1,13 @@
pub mod acme;
pub mod checker;
mod config;
pub mod gemini;
pub mod manager;
mod name;
mod settings;
pub mod store;
mod tls;
mod config;
mod name;
mod settings;
pub use config::*;
pub use name::*;
pub use settings::*;

View File

@ -1,8 +1,9 @@
use std::{collections, net, path, str::FromStr};
use serde::{Deserialize, Serialize};
mod proxied_domain;
use crate::domain;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, TryFromInto};
use std::{collections, net, path, str::FromStr};
fn default_resolver_addr() -> net::SocketAddr {
net::SocketAddr::from_str("1.1.1.1:53").unwrap()
@ -27,7 +28,7 @@ pub struct ConfigACME {
pub contact_email: String,
}
#[derive(Deserialize, Serialize)]
#[derive(Clone, Deserialize, Serialize)]
pub struct ConfigBuiltinDomain {
#[serde(flatten)]
pub settings: domain::Settings,
@ -36,12 +37,33 @@ pub struct ConfigBuiltinDomain {
pub public: bool,
}
#[serde_as]
#[derive(Clone, Deserialize, Serialize)]
pub struct ConfigProxiedDomain {
#[serde_as(as = "Option<TryFromInto<String>>")]
pub gemini_url: Option<proxied_domain::GeminiUrl>,
#[serde_as(as = "Option<TryFromInto<String>>")]
pub http_url: Option<proxied_domain::HttpUrl>,
#[serde(default)]
#[serde_as(as = "TryFromInto<Vec<proxied_domain::HttpRequestHeader>>")]
pub http_request_headers: proxied_domain::HttpRequestHeaders,
#[serde(default)]
pub https_disabled: bool,
}
#[derive(Deserialize, Serialize)]
pub struct Config {
pub store_dir_path: path::PathBuf,
#[serde(default)]
pub dns: ConfigDNS,
pub acme: Option<ConfigACME>,
#[serde(default)]
pub builtins: collections::HashMap<domain::Name, ConfigBuiltinDomain>,
#[serde(default)]
pub proxied: collections::HashMap<domain::Name, ConfigProxiedDomain>,
}

View File

@ -0,0 +1,131 @@
use crate::error::unexpected::{self, Mappable};
use serde::{Deserialize, Serialize};
fn addr_from_url(
url: &str,
expected_scheme: &str,
default_port: u16,
) -> unexpected::Result<String> {
let parsed: http::Uri = url
.parse()
.map_unexpected_while(|| format!("could not parse as url"))?;
let scheme = parsed
.scheme()
.map_unexpected_while(|| format!("scheme is missing"))?;
if scheme != expected_scheme {
return Err(unexpected::Error::from(
format!("scheme should be {expected_scheme}").as_str(),
));
}
if let Some(path_and_query) = parsed.path_and_query() {
let path_and_query = path_and_query.as_str();
if path_and_query != "" && path_and_query != "/" {
return Err(unexpected::Error::from(
format!("path must be empty").as_str(),
));
}
}
match parsed.authority() {
None => Err(unexpected::Error::from(format!("host is missing").as_str())),
Some(authority) => {
let port = authority.port().map(|p| p.as_u16()).unwrap_or(default_port);
Ok(format!("{}:{port}", authority.host()))
}
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct GeminiUrl {
pub original_url: String,
pub addr: String,
}
impl TryFrom<String> for GeminiUrl {
type Error = unexpected::Error;
fn try_from(url: String) -> Result<Self, Self::Error> {
let addr = addr_from_url(&url, "gemini", 1965)?;
Ok(Self {
original_url: url,
addr,
})
}
}
impl From<GeminiUrl> for String {
fn from(u: GeminiUrl) -> Self {
u.original_url
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct HttpUrl {
pub original_url: String,
pub addr: String,
}
impl TryFrom<String> for HttpUrl {
type Error = unexpected::Error;
fn try_from(url: String) -> Result<Self, Self::Error> {
let addr = addr_from_url(&url, "http", 80)?;
Ok(Self {
original_url: url,
addr,
})
}
}
impl From<HttpUrl> for String {
fn from(u: HttpUrl) -> Self {
u.original_url
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct HttpRequestHeader {
name: String,
value: String,
}
#[derive(Clone)]
pub struct HttpRequestHeaders(pub http::header::HeaderMap);
impl Default for HttpRequestHeaders {
fn default() -> Self {
Self(http::header::HeaderMap::default())
}
}
impl TryFrom<HttpRequestHeaders> for Vec<HttpRequestHeader> {
type Error = http::header::ToStrError;
fn try_from(h: HttpRequestHeaders) -> Result<Self, Self::Error> {
let mut v = vec![];
for (name, value) in &h.0 {
v.push(HttpRequestHeader {
name: name.to_string(),
value: value.to_str()?.to_string(),
})
}
Ok(v)
}
}
impl TryFrom<Vec<HttpRequestHeader>> for HttpRequestHeaders {
type Error = unexpected::Error;
fn try_from(v: Vec<HttpRequestHeader>) -> Result<Self, Self::Error> {
use http::header::{HeaderMap, HeaderName, HeaderValue};
let mut h = HeaderMap::new();
for pair in v {
let name: HeaderName = pair.name.parse().or_unexpected()?;
let value: HeaderValue = pair.value.parse().or_unexpected()?;
h.insert(name, value);
}
Ok(Self(h))
}
}

View File

@ -1,11 +1,15 @@
use crate::domain::{self, acme, checker, gemini, store, tls};
use crate::domain::{self, acme, checker, config, gemini, store, tls};
use crate::error::unexpected::{self, Mappable};
use crate::{origin, task_stack, util};
use std::sync;
use std::{collections, sync};
use tokio_util::sync::CancellationToken;
pub type GetSettingsResult = domain::store::GetResult;
pub enum GetSettingsResult {
Stored(domain::Settings),
Builtin(domain::config::ConfigBuiltinDomain),
Proxied(domain::config::ConfigProxiedDomain),
}
#[derive(thiserror::Error, Debug)]
pub enum GetSettingsError {
@ -37,11 +41,11 @@ pub enum GetFileError {
Unexpected(#[from] unexpected::Error),
}
impl From<store::GetError> for GetFileError {
fn from(e: store::GetError) -> Self {
impl From<GetSettingsError> for GetFileError {
fn from(e: GetSettingsError) -> Self {
match e {
store::GetError::NotFound => Self::DomainNotFound,
store::GetError::Unexpected(e) => Self::Unexpected(e),
GetSettingsError::NotFound => Self::DomainNotFound,
GetSettingsError::Unexpected(e) => Self::Unexpected(e),
}
}
}
@ -58,31 +62,10 @@ impl From<origin::GetFileError> for GetFileError {
}
}
#[derive(thiserror::Error, Debug)]
pub enum SyncError {
#[error("not found")]
NotFound,
#[error("already in progress")]
AlreadyInProgress,
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
}
impl From<store::GetError> for SyncError {
fn from(e: store::GetError) -> SyncError {
match e {
store::GetError::NotFound => SyncError::NotFound,
store::GetError::Unexpected(e) => SyncError::Unexpected(e),
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum SyncWithSettingsError {
#[error("cannot call SyncWithSettings on builtin domain")]
BuiltinDomain,
#[error("domain's settings cannot be modified")]
NotModifiable,
#[error("invalid url")]
InvalidURL,
@ -128,18 +111,12 @@ impl From<checker::CheckDomainError> for SyncWithSettingsError {
}
}
impl From<store::SetError> for SyncWithSettingsError {
fn from(e: store::SetError) -> SyncWithSettingsError {
match e {
store::SetError::BuiltinDomain => SyncWithSettingsError::BuiltinDomain,
store::SetError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
}
}
}
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
pub type StoredDomain = domain::store::StoredDomain;
pub struct ManagedDomain {
pub domain: domain::Name,
pub public: bool,
}
#[mockall::automock]
pub trait Manager: Sync + Send {
@ -167,7 +144,7 @@ pub trait Manager: Sync + Send {
domain: &domain::Name,
) -> unexpected::Result<Option<String>>;
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error>;
fn all_domains(&self) -> Result<Vec<ManagedDomain>, unexpected::Error>;
}
pub struct ManagerImpl {
@ -176,6 +153,8 @@ pub struct ManagerImpl {
domain_checker: checker::DNSChecker,
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
gemini_store: Option<Box<dyn gemini::Store + Send + Sync>>,
builtins: collections::HashMap<domain::Name, config::ConfigBuiltinDomain>,
proxied: collections::HashMap<domain::Name, config::ConfigProxiedDomain>,
}
impl ManagerImpl {
@ -191,6 +170,8 @@ impl ManagerImpl {
domain_checker: checker::DNSChecker,
acme_manager: Option<AcmeManager>,
gemini_store: Option<GeminiStore>,
builtins: collections::HashMap<domain::Name, config::ConfigBuiltinDomain>,
proxied: collections::HashMap<domain::Name, config::ConfigProxiedDomain>,
) -> sync::Arc<Self> {
let manager = sync::Arc::new(ManagerImpl {
origin_store: Box::from(origin_store),
@ -199,12 +180,14 @@ impl ManagerImpl {
acme_manager: acme_manager
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
gemini_store: gemini_store.map(|m| Box::new(m) as Box<dyn gemini::Store + Send + Sync>),
builtins,
proxied,
});
task_stack.push_spawn(|canceller| {
let manager = manager.clone();
async move {
manager.sync_all_domains(canceller).await;
manager.sync_all_domains_job(canceller).await;
Ok(())
}
});
@ -212,14 +195,18 @@ impl ManagerImpl {
manager
}
async fn sync_domain_certs_and_origin(
fn sync_domain_origin(
&self,
domain: &domain::Name,
settings: &domain::Settings,
) -> Result<(), SyncWithSettingsError> {
self.origin_store.sync(&settings.origin_descr)?;
origin_descr: &origin::Descr,
) -> Result<(), origin::SyncError> {
log::info!("Syncing origin {:?} for domain {domain}", origin_descr,);
self.origin_store.sync(origin_descr)
}
fn sync_domain_gemini_cert(&self, domain: &domain::Name) -> unexpected::Result<()> {
if let Some(ref gemini_store) = self.gemini_store {
log::info!("Syncing gemini certificate for domain {domain}");
if let Some(_) = gemini_store.get_certificate(domain).or_unexpected()? {
return Ok(());
}
@ -231,49 +218,70 @@ impl ManagerImpl {
gemini_store.set_certificate(domain, pkey, cert)?;
}
Ok(())
}
async fn sync_domain_https_cert(&self, domain: &domain::Name) -> unexpected::Result<()> {
if let Some(ref acme_manager) = self.acme_manager {
log::info!("Syncing HTTPS certificate for domain {domain}");
acme_manager.sync_domain(domain.clone()).await?;
}
Ok(())
}
async fn sync_all_domains_once(&self) {
let domains = match self.domain_store.all_domains() {
Ok(domains) => domains,
Err(err) => {
log::error!("Error fetching all domains: {err}");
return;
}
}
.into_iter();
async fn sync_all_domains(&self) -> unexpected::Result<()> {
let domains = self
.all_domains()
.or_unexpected_while("fetching all domains")?
.into_iter();
for StoredDomain { domain, .. } in domains {
log::info!("Syncing domain {}", &domain);
for ManagedDomain { domain, .. } in domains {
let (settings, https_cert, gemini_cert) = match self
.get_settings(&domain)
.map_unexpected_while(|| format!("fetching settings for {domain}"))?
{
GetSettingsResult::Stored(settings) => (Some(settings), true, true),
GetSettingsResult::Builtin(config) => (Some(config.settings), true, true),
let get_res = match self.domain_store.get(&domain) {
Ok(get_res) => get_res,
Err(err) => {
log::error!("Failed to fetch settings for domain {domain}: {err}");
return;
}
// A proxied domain never needs gemini certs, since gemini requests will be
// transparently proxied to the backing server anyway.
GetSettingsResult::Proxied(config) => (None, !config.https_disabled, false),
};
let settings = get_res.settings;
if let Some(settings) = settings {
self.sync_domain_origin(&domain, &settings.origin_descr)
.map_unexpected_while(|| {
format!(
"syncing origin {:?} for domain {domain}",
&settings.origin_descr,
)
})?;
}
if let Err(err) = self.sync_domain_certs_and_origin(&domain, &settings).await {
log::error!("Failed to sync settings for {domain}, settings:{settings:?}: {err}",)
if gemini_cert {
self.sync_domain_gemini_cert(&domain)
.map_unexpected_while(|| format!("syncing gemini cert for domain {domain}"))?;
}
if https_cert {
self.sync_domain_https_cert(&domain)
.await
.map_unexpected_while(|| format!("syncing https cert for domain {domain}",))?;
}
}
Ok(())
}
async fn sync_all_domains(&self, canceller: CancellationToken) {
async fn sync_all_domains_job(&self, canceller: CancellationToken) {
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(20 * 60));
loop {
tokio::select! {
_ = canceller.cancelled() => return,
_ = interval.tick() => self.sync_all_domains_once().await,
_ = interval.tick() => if let Err(err) = self.sync_all_domains().await {
log::error!("Failed to sync all domains: {err}")
},
}
}
}
@ -281,7 +289,15 @@ impl ManagerImpl {
impl Manager for ManagerImpl {
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError> {
Ok(self.domain_store.get(domain)?)
if let Some(config) = self.builtins.get(domain) {
return Ok(GetSettingsResult::Builtin(config.clone()));
}
if let Some(config) = self.proxied.get(domain) {
return Ok(GetSettingsResult::Proxied(config.clone()));
}
Ok(GetSettingsResult::Stored(self.domain_store.get(domain)?))
}
fn get_file<'store>(
@ -289,7 +305,15 @@ impl Manager for ManagerImpl {
domain: &domain::Name,
path: &str,
) -> Result<util::BoxByteStream, GetFileError> {
let settings = self.domain_store.get(domain)?.settings;
let settings = match self.get_settings(domain)? {
GetSettingsResult::Stored(settings) => settings,
GetSettingsResult::Builtin(config) => config.settings,
GetSettingsResult::Proxied(_) => {
return Err(
unexpected::Error::from("can't call get_file on proxied domain").into(),
);
}
};
let path = settings.process_path(path);
@ -306,14 +330,27 @@ impl Manager for ManagerImpl {
settings: domain::Settings,
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> {
Box::pin(async move {
if self.builtins.contains_key(&domain) || self.proxied.contains_key(&domain) {
return Err(SyncWithSettingsError::NotModifiable);
}
let hash = settings
.hash()
.or_unexpected_while("calculating config hash")?;
self.domain_checker.check_domain(&domain, &hash).await?;
self.sync_domain_certs_and_origin(&domain, &settings)
.await?;
self.sync_domain_origin(&domain, &settings.origin_descr)?;
self.sync_domain_gemini_cert(&domain)
.or_unexpected_while("syncing domain gemini cert")?;
self.sync_domain_https_cert(&domain)
.await
.or_unexpected_while("syncing domain https cert")?;
self.domain_store.set(&domain, &settings)?;
Ok(())
})
}
@ -336,8 +373,34 @@ impl Manager for ManagerImpl {
self.domain_checker.get_challenge_token(domain)
}
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
self.domain_store.all_domains()
fn all_domains(&self) -> Result<Vec<ManagedDomain>, unexpected::Error> {
let mut res: Vec<ManagedDomain> = self
.domain_store
.all_domains()?
.into_iter()
.map(|domain| ManagedDomain {
domain,
public: true,
})
.collect();
self.builtins
.iter()
.map(|(domain, config)| ManagedDomain {
domain: domain.clone(),
public: config.public,
})
.collect_into(&mut res);
self.proxied
.iter()
.map(|(domain, _)| ManagedDomain {
domain: domain.clone(),
public: false,
})
.collect_into(&mut res);
Ok(res)
}
}

View File

@ -1,21 +1,8 @@
use std::{collections, fs, io, path};
use std::{fs, io, path};
use crate::domain;
use crate::error::unexpected::{self, Intoable, Mappable};
#[derive(Debug, PartialEq)]
/// Extra information about a domain which is related to how its stored.
pub struct Metadata {
pub builtin: bool,
pub public: bool,
}
#[derive(Debug, PartialEq)]
pub struct GetResult {
pub settings: domain::Settings,
pub metadata: Metadata,
}
#[derive(thiserror::Error, Debug)]
pub enum GetError {
#[error("not found")]
@ -25,25 +12,11 @@ pub enum GetError {
Unexpected(#[from] unexpected::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum SetError {
#[error("cannot call set on builtin domain")]
BuiltinDomain,
#[error(transparent)]
Unexpected(#[from] unexpected::Error),
}
pub struct StoredDomain {
pub domain: domain::Name,
pub metadata: Metadata,
}
#[mockall::automock]
pub trait Store {
fn get(&self, domain: &domain::Name) -> Result<GetResult, GetError>;
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>;
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error>;
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError>;
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> unexpected::Result<()>;
fn all_domains(&self) -> unexpected::Result<Vec<domain::Name>>;
}
pub struct FSStore {
@ -68,7 +41,7 @@ impl FSStore {
}
impl Store for FSStore {
fn get(&self, domain: &domain::Name) -> Result<GetResult, GetError> {
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError> {
let path = self.settings_file_path(domain);
let settings_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() {
io::ErrorKind::NotFound => GetError::NotFound,
@ -80,16 +53,10 @@ impl Store for FSStore {
let settings = serde_json::from_reader(settings_file)
.map_unexpected_while(|| format!("json parsing {}", path.display()))?;
Ok(GetResult {
settings,
metadata: Metadata {
public: true,
builtin: false,
},
})
Ok(settings)
}
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> {
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> unexpected::Result<()> {
let dir_path = self.settings_dir_path(domain);
fs::create_dir_all(dir_path.as_path())
.map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?;
@ -104,85 +71,25 @@ impl Store for FSStore {
Ok(())
}
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
fn all_domains(&self) -> unexpected::Result<Vec<domain::Name>> {
fs::read_dir(&self.dir_path)
.or_unexpected()?
.map(
|dir_entry_res: io::Result<fs::DirEntry>| -> Result<StoredDomain, unexpected::Error> {
|dir_entry_res: io::Result<fs::DirEntry>| -> unexpected::Result<domain::Name> {
let domain = dir_entry_res.or_unexpected()?.file_name();
let domain = domain.to_str().ok_or(unexpected::Error::from(
"couldn't convert os string to &str",
))?;
Ok(StoredDomain{
domain: domain.parse().map_unexpected_while(|| format!("parsing {domain} as domain name"))?,
metadata: Metadata{
public: true,
builtin: false,
},
})
Ok(domain
.parse()
.map_unexpected_while(|| format!("parsing {domain} as domain name"))?)
},
)
.try_collect()
}
}
pub struct StoreWithBuiltin<S: Store> {
inner: S,
domains: collections::HashMap<domain::Name, domain::config::ConfigBuiltinDomain>,
}
impl<S: Store> StoreWithBuiltin<S> {
pub fn new(
inner: S,
builtin_domains: collections::HashMap<domain::Name, domain::config::ConfigBuiltinDomain>,
) -> StoreWithBuiltin<S> {
StoreWithBuiltin {
inner,
domains: builtin_domains,
}
}
}
impl<S: Store> Store for StoreWithBuiltin<S> {
fn get(&self, domain: &domain::Name) -> Result<GetResult, GetError> {
if let Some(domain) = self.domains.get(domain) {
return Ok(GetResult {
settings: domain.settings.clone(),
metadata: Metadata {
public: domain.public,
builtin: true,
},
});
}
self.inner.get(domain)
}
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> {
if self.domains.get(domain).is_some() {
return Err(SetError::BuiltinDomain);
}
self.inner.set(domain, settings)
}
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
let inner_domains = self.inner.all_domains()?;
let mut domains: Vec<StoredDomain> = self
.domains
.iter()
.map(|(domain, v)| StoredDomain {
domain: domain.clone(),
metadata: Metadata {
public: v.public,
builtin: true,
},
})
.collect();
domains.extend(inner_domains);
Ok(domains)
}
}
#[cfg(test)]
mod tests {
use super::{Store, *};
@ -211,20 +118,11 @@ mod tests {
assert!(matches!(
store.get(&domain),
Err::<GetResult, GetError>(GetError::NotFound)
Err::<domain::Settings, GetError>(GetError::NotFound)
));
store.set(&domain, &settings).expect("set");
assert_eq!(
GetResult {
settings,
metadata: Metadata {
public: true,
builtin: false,
},
},
store.get(&domain).expect("settings retrieved")
);
assert_eq!(settings, store.get(&domain).expect("settings retrieved"));
let new_settings = domain::Settings {
origin_descr: Descr::Git {
@ -236,13 +134,7 @@ mod tests {
store.set(&domain, &new_settings).expect("set");
assert_eq!(
GetResult {
settings: new_settings,
metadata: Metadata {
public: true,
builtin: false,
},
},
new_settings,
store.get(&domain).expect("settings retrieved")
);
}

View File

@ -102,9 +102,6 @@ async fn main() {
domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains"))
.expect("domain config store initialization failed");
let domain_store =
domani::domain::store::StoreWithBuiltin::new(domain_store, config.domain.builtins);
let domain_acme_manager = if config.service.http.https_addr.is_some() {
let acme_config = config
.domain
@ -146,6 +143,8 @@ async fn main() {
domain_checker,
domain_acme_manager,
domain_gemini_store,
config.domain.builtins.clone(),
config.domain.proxied.clone(),
);
let _ = domani::service::http::Service::new(
@ -153,6 +152,7 @@ async fn main() {
domain_manager.clone(),
domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()),
config.service.clone(),
config.domain.proxied.clone(),
);
if gemini_enabled {
@ -160,7 +160,8 @@ async fn main() {
&mut task_stack,
domain_manager.clone(),
domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()),
config.service,
config.service.gemini.clone(),
config.domain.proxied.clone(),
);
}

View File

@ -27,6 +27,8 @@ impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
#[derive(Deserialize, Serialize, Clone)]
pub struct Config {
pub passphrase: String,
#[serde(default)]
pub dns_records: Vec<ConfigDNSRecord>,
#[serde(default = "default_interface_domain")]

View File

@ -6,13 +6,14 @@ pub use config::*;
use crate::error::unexpected::{self, Mappable};
use crate::{domain, service, task_stack, util};
use std::sync;
use std::{collections, sync};
use tokio_util::sync::CancellationToken;
pub struct Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
config: service::Config,
config: Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
}
#[derive(thiserror::Error, Debug)]
@ -29,7 +30,8 @@ impl Service {
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: CertResolver,
config: service::Config,
config: Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
) -> sync::Arc<Service>
where
CertResolver: rustls::server::ResolvesServerCert + 'static,
@ -38,6 +40,7 @@ impl Service {
domain_manager,
cert_resolver: sync::Arc::from(cert_resolver),
config,
proxied,
});
task_stack.push_spawn(|canceller| listen(service.clone(), canceller));
service
@ -111,19 +114,13 @@ impl Service {
.await?)
}
async fn proxy_conn<IO>(
&self,
proxied_domain: &ConfigProxiedDomain,
mut conn: IO,
) -> unexpected::Result<()>
async fn proxy_conn<IO>(&self, proxy_addr: &str, mut conn: IO) -> unexpected::Result<()>
where
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
{
let mut proxy_conn = tokio::net::TcpStream::connect(&proxied_domain.url.addr)
let mut proxy_conn = tokio::net::TcpStream::connect(proxy_addr)
.await
.map_unexpected_while(|| {
format!("failed to connect to proxy {}", proxied_domain.url.url,)
})?;
.map_unexpected_while(|| format!("failed to connect to proxy {proxy_addr}"))?;
_ = tokio::io::copy_bidirectional(&mut conn, &mut proxy_conn).await;
@ -160,10 +157,13 @@ impl Service {
})?;
// If the domain should be proxied, then proxy it
if let Some(proxied_domain) = self.config.gemini.proxied_domains.get(&domain) {
let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner());
self.proxy_conn(proxied_domain, prefixed_conn).await?;
return Ok(());
if let Some(config) = self.proxied.get(&domain) {
if let Some(ref gemini_url) = config.gemini_url {
let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner());
self.proxy_conn(gemini_url.addr.as_str(), prefixed_conn)
.await?;
return Ok(());
}
}
let conn = start.into_stream(tls_config).await.or_unexpected()?;
@ -185,7 +185,6 @@ async fn listen(
) -> unexpected::Result<()> {
let addr = &service
.config
.gemini
.gemini_addr
.expect("listen called with gemini_addr not set");
@ -200,7 +199,7 @@ async fn listen(
let listener = tokio::net::TcpListener::bind(addr)
.await
.or_unexpected_while("binding tcp socket")?;
.expect("failed to bind tcp socket");
loop {
let (conn, addr) = tokio::select! {

View File

@ -1,79 +1,20 @@
use crate::domain;
use crate::error::unexpected::{self, Mappable};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, TryFromInto};
use std::{collections, net, str::FromStr};
use std::net;
fn default_gemini_addr() -> Option<net::SocketAddr> {
Some(net::SocketAddr::from_str("[::]:3965").unwrap())
}
#[derive(Deserialize, Serialize, Clone)]
pub struct ConfigProxiedDomainUrl {
pub url: String,
pub addr: String,
}
impl From<ConfigProxiedDomainUrl> for String {
fn from(url: ConfigProxiedDomainUrl) -> Self {
url.url
}
}
impl TryFrom<String> for ConfigProxiedDomainUrl {
type Error = unexpected::Error;
fn try_from(url: String) -> Result<Self, Self::Error> {
// use http's implementation, should be the same
let parsed = http::Uri::from_str(url.as_str())
.map_unexpected_while(|| format!("parsing proxy url {url}"))?;
let scheme = parsed.scheme().map_unexpected_while(|| {
format!("expected a scheme of gemini in the proxy url {url}")
})?;
if scheme != "gemini" {
return Err(unexpected::Error::from(
format!("scheme of proxy url {url} should be 'gemini'",).as_str(),
));
}
match parsed.authority() {
None => Err(unexpected::Error::from(
format!("proxy url {url} should have a host",).as_str(),
)),
Some(authority) => {
let port = authority.port().map(|p| p.as_u16()).unwrap_or(1965);
Ok(ConfigProxiedDomainUrl {
url: url,
addr: format!("{}:{port}", authority.host()),
})
}
}
}
}
#[serde_as]
#[derive(Deserialize, Serialize, Clone)]
pub struct ConfigProxiedDomain {
#[serde_as(as = "TryFromInto<String>")]
pub url: ConfigProxiedDomainUrl,
Some("[::]:3965".parse().unwrap())
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Config {
#[serde(default = "default_gemini_addr")]
pub gemini_addr: Option<net::SocketAddr>,
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
}
impl Default for Config {
fn default() -> Self {
Self {
gemini_addr: default_gemini_addr(),
proxied_domains: Default::default(),
}
}
}

View File

@ -10,7 +10,7 @@ use http::request::Parts;
use hyper::{Body, Method, Request, Response};
use serde::{Deserialize, Serialize};
use std::{future, net, sync};
use std::{collections, future, net, sync};
use crate::error::unexpected;
use crate::{domain, service, task_stack};
@ -20,6 +20,7 @@ pub struct Service {
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
handlebars: handlebars::Handlebars<'static>,
config: service::Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
}
#[derive(Serialize)]
@ -57,6 +58,7 @@ impl Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
cert_resolver: CertResolver,
config: service::Config,
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
) -> sync::Arc<Service>
where
CertResolver: rustls::server::ResolvesServerCert + 'static,
@ -68,6 +70,7 @@ impl Service {
cert_resolver: sync::Arc::from(cert_resolver),
handlebars: tpl::get(),
config,
proxied,
});
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
@ -209,8 +212,6 @@ impl Service {
}
fn domain(&self, args: DomainArgs) -> Response<Body> {
use domain::store::Metadata;
#[derive(Serialize)]
struct Data {
domain: domain::Name,
@ -218,42 +219,11 @@ impl Service {
}
let settings = match self.domain_manager.get_settings(&args.domain) {
Ok(domain::manager::GetSettingsResult {
metadata:
Metadata {
public: false,
builtin: _,
},
..
}) => None,
Ok(domain::manager::GetSettingsResult {
settings,
metadata:
Metadata {
public: true,
builtin: false,
},
}) => Some(settings),
Ok(domain::manager::GetSettingsResult {
metadata:
Metadata {
public: true,
builtin: true,
},
..
}) => {
return self.render_error_page(
403,
format!(
"Settings for domain {} cannot be viewed or modified",
args.domain
)
.as_str(),
)
Ok(domain::manager::GetSettingsResult::Stored(settings)) => Some(settings),
Ok(domain::manager::GetSettingsResult::Builtin(config)) => {
config.public.then(|| config.settings)
}
Ok(domain::manager::GetSettingsResult::Proxied(_)) => None,
Err(domain::manager::GetSettingsError::NotFound) => None,
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
return self.internal_error(
@ -355,7 +325,7 @@ impl Service {
let error_msg = match sync_result {
Ok(_) => None,
Err(domain::manager::SyncWithSettingsError::BuiltinDomain) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()),
Err(domain::manager::SyncWithSettingsError::NotModifiable) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()),
Err(domain::manager::SyncWithSettingsError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
Err(domain::manager::SyncWithSettingsError::InvalidBranchName) => Some("The git repository does not have a branch of the given name, please double check that you input the correct name.".to_string()),
Err(domain::manager::SyncWithSettingsError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".to_string()),
@ -386,7 +356,7 @@ impl Service {
let mut domains: Vec<String> = domains
.into_iter()
.filter(|d| d.metadata.public)
.filter(|d| d.public)
.map(|d| d.domain.as_str().to_string())
.collect();
@ -487,23 +457,25 @@ impl Service {
}
}
if let Some(proxied_domain_config) = self.config.http.proxied_domains.get(&domain) {
return service::http::proxy::serve_http_request(
proxied_domain_config,
client_ip,
req,
req_is_https,
)
.await
.unwrap_or_else(|e| {
self.internal_error(
format!(
"serving {domain} via proxy {}: {e}",
proxied_domain_config.url.as_ref()
)
.as_str(),
if let Some(config) = self.proxied.get(&domain) {
if let Some(ref http_url) = config.http_url {
return service::http::proxy::serve_http_request(
http_url.original_url.as_str(),
&config.http_request_headers.0,
client_ip,
req,
req_is_https,
)
});
.await
.unwrap_or_else(|e| {
self.internal_error(
format!("serving {domain} via proxy {}: {e}", http_url.original_url)
.as_str(),
)
});
} else {
return self.render_error_page(404, "Domain not found");
}
}
if Some(&domain) == self.config.interface_domain.as_ref() {

View File

@ -1,13 +1,8 @@
use crate::domain;
use crate::error::unexpected::{self, Mappable};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, TryFromInto};
use std::{collections, net, str::FromStr};
use std::net;
fn default_http_addr() -> net::SocketAddr {
net::SocketAddr::from_str("[::]:3080").unwrap()
"[::]:3080".parse().unwrap()
}
#[derive(Deserialize, Serialize, Clone)]
@ -40,105 +35,6 @@ impl AsRef<hyper::Method> for ConfigFormMethod {
}
}
#[derive(Clone)]
pub struct ConfigProxiedDomainUrl(String);
impl AsRef<str> for ConfigProxiedDomainUrl {
fn as_ref(&self) -> &str {
return &self.0;
}
}
impl From<ConfigProxiedDomainUrl> for String {
fn from(url: ConfigProxiedDomainUrl) -> Self {
url.0
}
}
impl TryFrom<String> for ConfigProxiedDomainUrl {
type Error = unexpected::Error;
fn try_from(url: String) -> Result<Self, Self::Error> {
let parsed = http::Uri::from_str(url.as_str())
.map_unexpected_while(|| format!("parsing proxy url {url}"))?;
let scheme = parsed
.scheme()
.map_unexpected_while(|| format!("expected a scheme of http in the proxy url {url}"))?;
if scheme != "http" {
return Err(unexpected::Error::from(
format!("scheme of proxy url {url} should be 'http'",).as_str(),
));
}
Ok(ConfigProxiedDomainUrl(url))
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct ConfigProxiedDomainRequestHeader {
pub name: String,
pub value: String,
}
#[derive(Clone)]
pub struct ConfigProxiedDomainRequestHeaders(http::header::HeaderMap);
impl AsRef<http::header::HeaderMap> for ConfigProxiedDomainRequestHeaders {
fn as_ref(&self) -> &http::header::HeaderMap {
&self.0
}
}
impl Default for ConfigProxiedDomainRequestHeaders {
fn default() -> Self {
ConfigProxiedDomainRequestHeaders(http::header::HeaderMap::default())
}
}
impl TryFrom<ConfigProxiedDomainRequestHeaders> for Vec<ConfigProxiedDomainRequestHeader> {
type Error = http::header::ToStrError;
fn try_from(h: ConfigProxiedDomainRequestHeaders) -> Result<Self, Self::Error> {
let mut v = vec![];
for (name, value) in &h.0 {
v.push(ConfigProxiedDomainRequestHeader {
name: name.to_string(),
value: value.to_str()?.to_string(),
})
}
Ok(v)
}
}
impl TryFrom<Vec<ConfigProxiedDomainRequestHeader>> for ConfigProxiedDomainRequestHeaders {
type Error = unexpected::Error;
fn try_from(v: Vec<ConfigProxiedDomainRequestHeader>) -> Result<Self, Self::Error> {
use http::header::{HeaderMap, HeaderName, HeaderValue};
let mut h = HeaderMap::new();
for pair in v {
let name: HeaderName = pair.name.parse().or_unexpected()?;
let value: HeaderValue = pair.value.parse().or_unexpected()?;
h.insert(name, value);
}
Ok(ConfigProxiedDomainRequestHeaders(h))
}
}
#[serde_as]
#[derive(Deserialize, Serialize, Clone)]
pub struct ConfigProxiedDomain {
#[serde_as(as = "TryFromInto<String>")]
pub url: ConfigProxiedDomainUrl,
#[serde(default)]
#[serde_as(as = "TryFromInto<Vec<ConfigProxiedDomainRequestHeader>>")]
pub request_headers: ConfigProxiedDomainRequestHeaders,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Config {
#[serde(default = "default_http_addr")]
@ -147,9 +43,6 @@ pub struct Config {
#[serde(default)]
pub form_method: ConfigFormMethod,
#[serde(default)]
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
}
impl Default for Config {
@ -158,7 +51,6 @@ impl Default for Config {
http_addr: default_http_addr(),
https_addr: None,
form_method: Default::default(),
proxied_domains: Default::default(),
}
}
}

View File

@ -1,15 +1,14 @@
use crate::error::unexpected::{self};
use crate::service;
use http::header::HeaderValue;
use crate::error::unexpected;
use std::net;
pub async fn serve_http_request(
config: &service::http::ConfigProxiedDomain,
proxy_addr: &str,
headers: &http::header::HeaderMap,
client_ip: net::IpAddr,
mut req: hyper::Request<hyper::Body>,
req_is_https: bool,
) -> unexpected::Result<hyper::Response<hyper::Body>> {
for (name, value) in config.request_headers.as_ref() {
for (name, value) in headers {
if value == "" {
req.headers_mut().remove(name);
continue;
@ -19,18 +18,18 @@ pub async fn serve_http_request(
}
if req_is_https {
req.headers_mut()
.insert("x-forwarded-proto", HeaderValue::from_static("https"));
req.headers_mut().insert(
"x-forwarded-proto",
http::header::HeaderValue::from_static("https"),
);
}
let url = config.url.as_ref();
match hyper_reverse_proxy::call(client_ip, url, req).await {
match hyper_reverse_proxy::call(client_ip, proxy_addr, req).await {
Ok(res) => Ok(res),
// ProxyError doesn't actually implement Error :facepalm: so we have to format the error
// manually
Err(e) => Err(unexpected::Error::from(
format!("error while proxying to {url}: {e:?}").as_str(),
format!("error while proxying to {proxy_addr}: {e:?}").as_str(),
)),
}
}