Move proxy config into domain (bigger change than it sounds like)
This commit is contained in:
parent
b44fd575a9
commit
7a1a2297d4
@ -1,13 +1,13 @@
|
|||||||
pub mod acme;
|
pub mod acme;
|
||||||
pub mod checker;
|
pub mod checker;
|
||||||
mod config;
|
|
||||||
pub mod gemini;
|
pub mod gemini;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
mod name;
|
|
||||||
mod settings;
|
|
||||||
pub mod store;
|
pub mod store;
|
||||||
mod tls;
|
mod tls;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod name;
|
||||||
|
mod settings;
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use name::*;
|
pub use name::*;
|
||||||
pub use settings::*;
|
pub use settings::*;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use std::{collections, net, path, str::FromStr};
|
mod proxied_domain;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::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 {
|
fn default_resolver_addr() -> net::SocketAddr {
|
||||||
net::SocketAddr::from_str("1.1.1.1:53").unwrap()
|
net::SocketAddr::from_str("1.1.1.1:53").unwrap()
|
||||||
@ -27,7 +28,7 @@ pub struct ConfigACME {
|
|||||||
pub contact_email: String,
|
pub contact_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct ConfigBuiltinDomain {
|
pub struct ConfigBuiltinDomain {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub settings: domain::Settings,
|
pub settings: domain::Settings,
|
||||||
@ -36,12 +37,32 @@ pub struct ConfigBuiltinDomain {
|
|||||||
pub public: bool,
|
pub public: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
|
pub struct ConfigProxiedDomain {
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde_as(as = "Option<TryFromInto<String>>")]
|
||||||
|
pub gemini_url: Option<proxied_domain::GeminiUrl>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub store_dir_path: path::PathBuf,
|
pub store_dir_path: path::PathBuf,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dns: ConfigDNS,
|
pub dns: ConfigDNS,
|
||||||
pub acme: Option<ConfigACME>,
|
pub acme: Option<ConfigACME>,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub builtins: collections::HashMap<domain::Name, ConfigBuiltinDomain>,
|
pub builtins: collections::HashMap<domain::Name, ConfigBuiltinDomain>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub proxied: collections::HashMap<domain::Name, ConfigProxiedDomain>,
|
||||||
}
|
}
|
||||||
|
131
src/domain/config/proxied_domain.rs
Normal file
131
src/domain/config/proxied_domain.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
@ -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::error::unexpected::{self, Mappable};
|
||||||
use crate::{origin, task_stack, util};
|
use crate::{origin, task_stack, util};
|
||||||
|
|
||||||
use std::sync;
|
use std::{collections, sync};
|
||||||
use tokio_util::sync::CancellationToken;
|
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)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum GetSettingsError {
|
pub enum GetSettingsError {
|
||||||
@ -37,11 +41,11 @@ pub enum GetFileError {
|
|||||||
Unexpected(#[from] unexpected::Error),
|
Unexpected(#[from] unexpected::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<store::GetError> for GetFileError {
|
impl From<GetSettingsError> for GetFileError {
|
||||||
fn from(e: store::GetError) -> Self {
|
fn from(e: GetSettingsError) -> Self {
|
||||||
match e {
|
match e {
|
||||||
store::GetError::NotFound => Self::DomainNotFound,
|
GetSettingsError::NotFound => Self::DomainNotFound,
|
||||||
store::GetError::Unexpected(e) => Self::Unexpected(e),
|
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)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SyncWithSettingsError {
|
pub enum SyncWithSettingsError {
|
||||||
#[error("cannot call SyncWithSettings on builtin domain")]
|
#[error("domain's settings cannot be modified")]
|
||||||
BuiltinDomain,
|
NotModifiable,
|
||||||
|
|
||||||
#[error("invalid url")]
|
#[error("invalid url")]
|
||||||
InvalidURL,
|
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 GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
||||||
|
|
||||||
pub type StoredDomain = domain::store::StoredDomain;
|
pub struct ManagedDomain {
|
||||||
|
pub domain: domain::Name,
|
||||||
|
pub public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait Manager: Sync + Send {
|
pub trait Manager: Sync + Send {
|
||||||
@ -167,7 +144,7 @@ pub trait Manager: Sync + Send {
|
|||||||
domain: &domain::Name,
|
domain: &domain::Name,
|
||||||
) -> unexpected::Result<Option<String>>;
|
) -> 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 {
|
pub struct ManagerImpl {
|
||||||
@ -176,6 +153,8 @@ pub struct ManagerImpl {
|
|||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
|
acme_manager: Option<Box<dyn acme::manager::Manager + Send + Sync>>,
|
||||||
gemini_store: Option<Box<dyn gemini::Store + 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 {
|
impl ManagerImpl {
|
||||||
@ -191,6 +170,8 @@ impl ManagerImpl {
|
|||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
acme_manager: Option<AcmeManager>,
|
acme_manager: Option<AcmeManager>,
|
||||||
gemini_store: Option<GeminiStore>,
|
gemini_store: Option<GeminiStore>,
|
||||||
|
builtins: collections::HashMap<domain::Name, config::ConfigBuiltinDomain>,
|
||||||
|
proxied: collections::HashMap<domain::Name, config::ConfigProxiedDomain>,
|
||||||
) -> sync::Arc<Self> {
|
) -> sync::Arc<Self> {
|
||||||
let manager = sync::Arc::new(ManagerImpl {
|
let manager = sync::Arc::new(ManagerImpl {
|
||||||
origin_store: Box::from(origin_store),
|
origin_store: Box::from(origin_store),
|
||||||
@ -199,6 +180,8 @@ impl ManagerImpl {
|
|||||||
acme_manager: acme_manager
|
acme_manager: acme_manager
|
||||||
.map(|m| Box::new(m) as Box<dyn acme::manager::Manager + Send + Sync>),
|
.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>),
|
gemini_store: gemini_store.map(|m| Box::new(m) as Box<dyn gemini::Store + Send + Sync>),
|
||||||
|
builtins,
|
||||||
|
proxied,
|
||||||
});
|
});
|
||||||
|
|
||||||
task_stack.push_spawn(|canceller| {
|
task_stack.push_spawn(|canceller| {
|
||||||
@ -212,14 +195,18 @@ impl ManagerImpl {
|
|||||||
manager
|
manager
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_domain_certs_and_origin(
|
fn sync_domain_origin(
|
||||||
&self,
|
&self,
|
||||||
domain: &domain::Name,
|
domain: &domain::Name,
|
||||||
settings: &domain::Settings,
|
origin_descr: &origin::Descr,
|
||||||
) -> Result<(), SyncWithSettingsError> {
|
) -> Result<(), origin::SyncError> {
|
||||||
self.origin_store.sync(&settings.origin_descr)?;
|
log::info!("Syncing origin {:?} for domain {domain}", origin_descr,);
|
||||||
|
self.origin_store.sync(origin_descr)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_domain_certs(&self, domain: &domain::Name) -> unexpected::Result<()> {
|
||||||
if let Some(ref gemini_store) = self.gemini_store {
|
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()? {
|
if let Some(_) = gemini_store.get_certificate(domain).or_unexpected()? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@ -233,39 +220,45 @@ impl ManagerImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref acme_manager) = self.acme_manager {
|
if let Some(ref acme_manager) = self.acme_manager {
|
||||||
|
log::info!("Syncing HTTPS certificate for domain {domain}");
|
||||||
acme_manager.sync_domain(domain.clone()).await?;
|
acme_manager.sync_domain(domain.clone()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_all_domains_once(&self) {
|
async fn sync_all_domains_once(&self) -> unexpected::Result<()> {
|
||||||
let domains = match self.domain_store.all_domains() {
|
let domains = self
|
||||||
Ok(domains) => domains,
|
.all_domains()
|
||||||
Err(err) => {
|
.or_unexpected_while("fetching all domains")?
|
||||||
log::error!("Error fetching all domains: {err}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into_iter();
|
.into_iter();
|
||||||
|
|
||||||
for StoredDomain { domain, .. } in domains {
|
for ManagedDomain { domain, .. } in domains {
|
||||||
log::info!("Syncing domain {}", &domain);
|
let settings = match self
|
||||||
|
.get_settings(&domain)
|
||||||
let get_res = match self.domain_store.get(&domain) {
|
.map_unexpected_while(|| format!("fetching settings for {domain}"))?
|
||||||
Ok(get_res) => get_res,
|
{
|
||||||
Err(err) => {
|
GetSettingsResult::Stored(settings) => Some(settings),
|
||||||
log::error!("Failed to fetch settings for domain {domain}: {err}");
|
GetSettingsResult::Builtin(config) => Some(config.settings),
|
||||||
return;
|
GetSettingsResult::Proxied(_) => None,
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
self.sync_domain_certs(&domain)
|
||||||
log::error!("Failed to sync settings for {domain}, settings:{settings:?}: {err}",)
|
.await
|
||||||
}
|
.map_unexpected_while(|| format!("syncing certs for domain {domain}",))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_all_domains(&self, canceller: CancellationToken) {
|
async fn sync_all_domains(&self, canceller: CancellationToken) {
|
||||||
@ -273,7 +266,9 @@ impl ManagerImpl {
|
|||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = canceller.cancelled() => return,
|
_ = canceller.cancelled() => return,
|
||||||
_ = interval.tick() => self.sync_all_domains_once().await,
|
_ = interval.tick() => if let Err(err) = self.sync_all_domains_once().await {
|
||||||
|
log::error!("Failed to sync all domains: {err}")
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,7 +276,15 @@ impl ManagerImpl {
|
|||||||
|
|
||||||
impl Manager for ManagerImpl {
|
impl Manager for ManagerImpl {
|
||||||
fn get_settings(&self, domain: &domain::Name) -> Result<GetSettingsResult, GetSettingsError> {
|
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>(
|
fn get_file<'store>(
|
||||||
@ -289,7 +292,15 @@ impl Manager for ManagerImpl {
|
|||||||
domain: &domain::Name,
|
domain: &domain::Name,
|
||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<util::BoxByteStream, GetFileError> {
|
) -> 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);
|
let path = settings.process_path(path);
|
||||||
|
|
||||||
@ -306,14 +317,21 @@ impl Manager for ManagerImpl {
|
|||||||
settings: domain::Settings,
|
settings: domain::Settings,
|
||||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> {
|
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
if self.builtins.contains_key(&domain) || self.proxied.contains_key(&domain) {
|
||||||
|
return Err(SyncWithSettingsError::NotModifiable);
|
||||||
|
}
|
||||||
|
|
||||||
let hash = settings
|
let hash = settings
|
||||||
.hash()
|
.hash()
|
||||||
.or_unexpected_while("calculating config hash")?;
|
.or_unexpected_while("calculating config hash")?;
|
||||||
|
|
||||||
self.domain_checker.check_domain(&domain, &hash).await?;
|
self.domain_checker.check_domain(&domain, &hash).await?;
|
||||||
self.sync_domain_certs_and_origin(&domain, &settings)
|
self.sync_domain_origin(&domain, &settings.origin_descr)?;
|
||||||
.await?;
|
self.sync_domain_certs(&domain)
|
||||||
|
.await
|
||||||
|
.or_unexpected_while("syncing domain certs")?;
|
||||||
self.domain_store.set(&domain, &settings)?;
|
self.domain_store.set(&domain, &settings)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -336,8 +354,34 @@ impl Manager for ManagerImpl {
|
|||||||
self.domain_checker.get_challenge_token(domain)
|
self.domain_checker.get_challenge_token(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error> {
|
fn all_domains(&self) -> Result<Vec<ManagedDomain>, unexpected::Error> {
|
||||||
self.domain_store.all_domains()
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,8 @@
|
|||||||
use std::{collections, fs, io, path};
|
use std::{fs, io, path};
|
||||||
|
|
||||||
use crate::domain;
|
use crate::domain;
|
||||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
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)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum GetError {
|
pub enum GetError {
|
||||||
#[error("not found")]
|
#[error("not found")]
|
||||||
@ -25,25 +12,11 @@ pub enum GetError {
|
|||||||
Unexpected(#[from] unexpected::Error),
|
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]
|
#[mockall::automock]
|
||||||
pub trait Store {
|
pub trait Store {
|
||||||
fn get(&self, domain: &domain::Name) -> Result<GetResult, GetError>;
|
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError>;
|
||||||
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>;
|
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> unexpected::Result<()>;
|
||||||
fn all_domains(&self) -> Result<Vec<StoredDomain>, unexpected::Error>;
|
fn all_domains(&self) -> unexpected::Result<Vec<domain::Name>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FSStore {
|
pub struct FSStore {
|
||||||
@ -68,7 +41,7 @@ impl FSStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Store for 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 path = self.settings_file_path(domain);
|
||||||
let settings_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() {
|
let settings_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() {
|
||||||
io::ErrorKind::NotFound => GetError::NotFound,
|
io::ErrorKind::NotFound => GetError::NotFound,
|
||||||
@ -80,16 +53,10 @@ impl Store for FSStore {
|
|||||||
let settings = serde_json::from_reader(settings_file)
|
let settings = serde_json::from_reader(settings_file)
|
||||||
.map_unexpected_while(|| format!("json parsing {}", path.display()))?;
|
.map_unexpected_while(|| format!("json parsing {}", path.display()))?;
|
||||||
|
|
||||||
Ok(GetResult {
|
Ok(settings)
|
||||||
settings,
|
|
||||||
metadata: Metadata {
|
|
||||||
public: true,
|
|
||||||
builtin: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
let dir_path = self.settings_dir_path(domain);
|
||||||
fs::create_dir_all(dir_path.as_path())
|
fs::create_dir_all(dir_path.as_path())
|
||||||
.map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?;
|
.map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?;
|
||||||
@ -104,85 +71,25 @@ impl Store for FSStore {
|
|||||||
Ok(())
|
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)
|
fs::read_dir(&self.dir_path)
|
||||||
.or_unexpected()?
|
.or_unexpected()?
|
||||||
.map(
|
.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 = dir_entry_res.or_unexpected()?.file_name();
|
||||||
let domain = domain.to_str().ok_or(unexpected::Error::from(
|
let domain = domain.to_str().ok_or(unexpected::Error::from(
|
||||||
"couldn't convert os string to &str",
|
"couldn't convert os string to &str",
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
Ok(StoredDomain{
|
Ok(domain
|
||||||
domain: domain.parse().map_unexpected_while(|| format!("parsing {domain} as domain name"))?,
|
.parse()
|
||||||
metadata: Metadata{
|
.map_unexpected_while(|| format!("parsing {domain} as domain name"))?)
|
||||||
public: true,
|
|
||||||
builtin: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.try_collect()
|
.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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Store, *};
|
use super::{Store, *};
|
||||||
@ -211,20 +118,11 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.get(&domain),
|
store.get(&domain),
|
||||||
Err::<GetResult, GetError>(GetError::NotFound)
|
Err::<domain::Settings, GetError>(GetError::NotFound)
|
||||||
));
|
));
|
||||||
|
|
||||||
store.set(&domain, &settings).expect("set");
|
store.set(&domain, &settings).expect("set");
|
||||||
assert_eq!(
|
assert_eq!(settings, store.get(&domain).expect("settings retrieved"));
|
||||||
GetResult {
|
|
||||||
settings,
|
|
||||||
metadata: Metadata {
|
|
||||||
public: true,
|
|
||||||
builtin: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
store.get(&domain).expect("settings retrieved")
|
|
||||||
);
|
|
||||||
|
|
||||||
let new_settings = domain::Settings {
|
let new_settings = domain::Settings {
|
||||||
origin_descr: Descr::Git {
|
origin_descr: Descr::Git {
|
||||||
@ -236,13 +134,7 @@ mod tests {
|
|||||||
|
|
||||||
store.set(&domain, &new_settings).expect("set");
|
store.set(&domain, &new_settings).expect("set");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GetResult {
|
new_settings,
|
||||||
settings: new_settings,
|
|
||||||
metadata: Metadata {
|
|
||||||
public: true,
|
|
||||||
builtin: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
store.get(&domain).expect("settings retrieved")
|
store.get(&domain).expect("settings retrieved")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -102,9 +102,6 @@ async fn main() {
|
|||||||
domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains"))
|
domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains"))
|
||||||
.expect("domain config store initialization failed");
|
.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 domain_acme_manager = if config.service.http.https_addr.is_some() {
|
||||||
let acme_config = config
|
let acme_config = config
|
||||||
.domain
|
.domain
|
||||||
@ -146,6 +143,8 @@ async fn main() {
|
|||||||
domain_checker,
|
domain_checker,
|
||||||
domain_acme_manager,
|
domain_acme_manager,
|
||||||
domain_gemini_store,
|
domain_gemini_store,
|
||||||
|
config.domain.builtins.clone(),
|
||||||
|
config.domain.proxied.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = domani::service::http::Service::new(
|
let _ = domani::service::http::Service::new(
|
||||||
@ -153,6 +152,7 @@ async fn main() {
|
|||||||
domain_manager.clone(),
|
domain_manager.clone(),
|
||||||
domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()),
|
domani::domain::manager::HttpsCertResolver::from(domain_manager.clone()),
|
||||||
config.service.clone(),
|
config.service.clone(),
|
||||||
|
config.domain.proxied.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if gemini_enabled {
|
if gemini_enabled {
|
||||||
@ -160,7 +160,8 @@ async fn main() {
|
|||||||
&mut task_stack,
|
&mut task_stack,
|
||||||
domain_manager.clone(),
|
domain_manager.clone(),
|
||||||
domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()),
|
domani::domain::manager::GeminiCertResolver::from(domain_manager.clone()),
|
||||||
config.service,
|
config.service.gemini.clone(),
|
||||||
|
config.domain.proxied.clone(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,13 +6,14 @@ pub use config::*;
|
|||||||
use crate::error::unexpected::{self, Mappable};
|
use crate::error::unexpected::{self, Mappable};
|
||||||
use crate::{domain, service, task_stack, util};
|
use crate::{domain, service, task_stack, util};
|
||||||
|
|
||||||
use std::sync;
|
use std::{collections, sync};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
||||||
config: service::Config,
|
config: Config,
|
||||||
|
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@ -29,7 +30,8 @@ impl Service {
|
|||||||
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
task_stack: &mut task_stack::TaskStack<unexpected::Error>,
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
cert_resolver: CertResolver,
|
cert_resolver: CertResolver,
|
||||||
config: service::Config,
|
config: Config,
|
||||||
|
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
|
||||||
) -> sync::Arc<Service>
|
) -> sync::Arc<Service>
|
||||||
where
|
where
|
||||||
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
||||||
@ -38,6 +40,7 @@ impl Service {
|
|||||||
domain_manager,
|
domain_manager,
|
||||||
cert_resolver: sync::Arc::from(cert_resolver),
|
cert_resolver: sync::Arc::from(cert_resolver),
|
||||||
config,
|
config,
|
||||||
|
proxied,
|
||||||
});
|
});
|
||||||
task_stack.push_spawn(|canceller| listen(service.clone(), canceller));
|
task_stack.push_spawn(|canceller| listen(service.clone(), canceller));
|
||||||
service
|
service
|
||||||
@ -111,19 +114,13 @@ impl Service {
|
|||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn proxy_conn<IO>(
|
async fn proxy_conn<IO>(&self, proxy_addr: &str, mut conn: IO) -> unexpected::Result<()>
|
||||||
&self,
|
|
||||||
proxied_domain: &ConfigProxiedDomain,
|
|
||||||
mut conn: IO,
|
|
||||||
) -> unexpected::Result<()>
|
|
||||||
where
|
where
|
||||||
IO: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
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
|
.await
|
||||||
.map_unexpected_while(|| {
|
.map_unexpected_while(|| format!("failed to connect to proxy {proxy_addr}"))?;
|
||||||
format!("failed to connect to proxy {}", proxied_domain.url.url,)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
_ = tokio::io::copy_bidirectional(&mut conn, &mut proxy_conn).await;
|
_ = tokio::io::copy_bidirectional(&mut conn, &mut proxy_conn).await;
|
||||||
|
|
||||||
@ -160,11 +157,14 @@ impl Service {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
// If the domain should be proxied, then proxy it
|
// If the domain should be proxied, then proxy it
|
||||||
if let Some(proxied_domain) = self.config.gemini.proxied_domains.get(&domain) {
|
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());
|
let prefixed_conn = proxy::teed_io_to_prefixed(start.into_inner());
|
||||||
self.proxy_conn(proxied_domain, prefixed_conn).await?;
|
self.proxy_conn(gemini_url.addr.as_str(), prefixed_conn)
|
||||||
|
.await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let conn = start.into_stream(tls_config).await.or_unexpected()?;
|
let conn = start.into_stream(tls_config).await.or_unexpected()?;
|
||||||
self.serve_conn(&domain, conn).await
|
self.serve_conn(&domain, conn).await
|
||||||
@ -185,7 +185,6 @@ async fn listen(
|
|||||||
) -> unexpected::Result<()> {
|
) -> unexpected::Result<()> {
|
||||||
let addr = &service
|
let addr = &service
|
||||||
.config
|
.config
|
||||||
.gemini
|
|
||||||
.gemini_addr
|
.gemini_addr
|
||||||
.expect("listen called with gemini_addr not set");
|
.expect("listen called with gemini_addr not set");
|
||||||
|
|
||||||
|
@ -1,79 +1,20 @@
|
|||||||
use crate::domain;
|
|
||||||
use crate::error::unexpected::{self, Mappable};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, TryFromInto};
|
use std::net;
|
||||||
|
|
||||||
use std::{collections, net, str::FromStr};
|
|
||||||
|
|
||||||
fn default_gemini_addr() -> Option<net::SocketAddr> {
|
fn default_gemini_addr() -> Option<net::SocketAddr> {
|
||||||
Some(net::SocketAddr::from_str("[::]:3965").unwrap())
|
Some("[::]:3965".parse().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,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_gemini_addr")]
|
#[serde(default = "default_gemini_addr")]
|
||||||
pub gemini_addr: Option<net::SocketAddr>,
|
pub gemini_addr: Option<net::SocketAddr>,
|
||||||
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
gemini_addr: default_gemini_addr(),
|
gemini_addr: default_gemini_addr(),
|
||||||
proxied_domains: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ use http::request::Parts;
|
|||||||
use hyper::{Body, Method, Request, Response};
|
use hyper::{Body, Method, Request, Response};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::{future, net, sync};
|
use std::{collections, future, net, sync};
|
||||||
|
|
||||||
use crate::error::unexpected;
|
use crate::error::unexpected;
|
||||||
use crate::{domain, service, task_stack};
|
use crate::{domain, service, task_stack};
|
||||||
@ -20,6 +20,7 @@ pub struct Service {
|
|||||||
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
cert_resolver: sync::Arc<dyn rustls::server::ResolvesServerCert>,
|
||||||
handlebars: handlebars::Handlebars<'static>,
|
handlebars: handlebars::Handlebars<'static>,
|
||||||
config: service::Config,
|
config: service::Config,
|
||||||
|
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
@ -57,6 +58,7 @@ impl Service {
|
|||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
cert_resolver: CertResolver,
|
cert_resolver: CertResolver,
|
||||||
config: service::Config,
|
config: service::Config,
|
||||||
|
proxied: collections::HashMap<domain::Name, domain::ConfigProxiedDomain>,
|
||||||
) -> sync::Arc<Service>
|
) -> sync::Arc<Service>
|
||||||
where
|
where
|
||||||
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
CertResolver: rustls::server::ResolvesServerCert + 'static,
|
||||||
@ -68,6 +70,7 @@ impl Service {
|
|||||||
cert_resolver: sync::Arc::from(cert_resolver),
|
cert_resolver: sync::Arc::from(cert_resolver),
|
||||||
handlebars: tpl::get(),
|
handlebars: tpl::get(),
|
||||||
config,
|
config,
|
||||||
|
proxied,
|
||||||
});
|
});
|
||||||
|
|
||||||
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
|
task_stack.push_spawn(|canceller| tasks::listen_http(service.clone(), canceller));
|
||||||
@ -209,8 +212,6 @@ impl Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn domain(&self, args: DomainArgs) -> Response<Body> {
|
fn domain(&self, args: DomainArgs) -> Response<Body> {
|
||||||
use domain::store::Metadata;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Data {
|
struct Data {
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
@ -218,42 +219,11 @@ impl Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let settings = match self.domain_manager.get_settings(&args.domain) {
|
let settings = match self.domain_manager.get_settings(&args.domain) {
|
||||||
Ok(domain::manager::GetSettingsResult {
|
Ok(domain::manager::GetSettingsResult::Stored(settings)) => Some(settings),
|
||||||
metadata:
|
Ok(domain::manager::GetSettingsResult::Builtin(config)) => {
|
||||||
Metadata {
|
config.public.then(|| config.settings)
|
||||||
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::Proxied(_)) => None,
|
||||||
Err(domain::manager::GetSettingsError::NotFound) => None,
|
Err(domain::manager::GetSettingsError::NotFound) => None,
|
||||||
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
|
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
|
||||||
return self.internal_error(
|
return self.internal_error(
|
||||||
@ -355,7 +325,7 @@ impl Service {
|
|||||||
|
|
||||||
let error_msg = match sync_result {
|
let error_msg = match sync_result {
|
||||||
Ok(_) => None,
|
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::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::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()),
|
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
|
let mut domains: Vec<String> = domains
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|d| d.metadata.public)
|
.filter(|d| d.public)
|
||||||
.map(|d| d.domain.as_str().to_string())
|
.map(|d| d.domain.as_str().to_string())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -487,9 +457,11 @@ impl Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(proxied_domain_config) = self.config.http.proxied_domains.get(&domain) {
|
if let Some(config) = self.proxied.get(&domain) {
|
||||||
|
if let Some(ref http_url) = config.http_url {
|
||||||
return service::http::proxy::serve_http_request(
|
return service::http::proxy::serve_http_request(
|
||||||
proxied_domain_config,
|
http_url.original_url.as_str(),
|
||||||
|
&config.http_request_headers.0,
|
||||||
client_ip,
|
client_ip,
|
||||||
req,
|
req,
|
||||||
req_is_https,
|
req_is_https,
|
||||||
@ -497,14 +469,12 @@ impl Service {
|
|||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| {
|
||||||
self.internal_error(
|
self.internal_error(
|
||||||
format!(
|
format!("serving {domain} via proxy {}: {e}", http_url.original_url)
|
||||||
"serving {domain} via proxy {}: {e}",
|
|
||||||
proxied_domain_config.url.as_ref()
|
|
||||||
)
|
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if Some(&domain) == self.config.interface_domain.as_ref() {
|
if Some(&domain) == self.config.interface_domain.as_ref() {
|
||||||
return self.serve_interface(req).await;
|
return self.serve_interface(req).await;
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
use crate::domain;
|
|
||||||
use crate::error::unexpected::{self, Mappable};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::{serde_as, TryFromInto};
|
use std::net;
|
||||||
|
|
||||||
use std::{collections, net, str::FromStr};
|
|
||||||
|
|
||||||
fn default_http_addr() -> net::SocketAddr {
|
fn default_http_addr() -> net::SocketAddr {
|
||||||
net::SocketAddr::from_str("[::]:3080").unwrap()
|
"[::]:3080".parse().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[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)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_http_addr")]
|
#[serde(default = "default_http_addr")]
|
||||||
@ -147,9 +43,6 @@ pub struct Config {
|
|||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub form_method: ConfigFormMethod,
|
pub form_method: ConfigFormMethod,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -158,7 +51,6 @@ impl Default for Config {
|
|||||||
http_addr: default_http_addr(),
|
http_addr: default_http_addr(),
|
||||||
https_addr: None,
|
https_addr: None,
|
||||||
form_method: Default::default(),
|
form_method: Default::default(),
|
||||||
proxied_domains: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
use crate::error::unexpected::{self};
|
use crate::error::unexpected;
|
||||||
use crate::service;
|
|
||||||
use http::header::HeaderValue;
|
|
||||||
use std::net;
|
use std::net;
|
||||||
|
|
||||||
pub async fn serve_http_request(
|
pub async fn serve_http_request(
|
||||||
config: &service::http::ConfigProxiedDomain,
|
proxy_addr: &str,
|
||||||
|
headers: &http::header::HeaderMap,
|
||||||
client_ip: net::IpAddr,
|
client_ip: net::IpAddr,
|
||||||
mut req: hyper::Request<hyper::Body>,
|
mut req: hyper::Request<hyper::Body>,
|
||||||
req_is_https: bool,
|
req_is_https: bool,
|
||||||
) -> unexpected::Result<hyper::Response<hyper::Body>> {
|
) -> unexpected::Result<hyper::Response<hyper::Body>> {
|
||||||
for (name, value) in config.request_headers.as_ref() {
|
for (name, value) in headers {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
req.headers_mut().remove(name);
|
req.headers_mut().remove(name);
|
||||||
continue;
|
continue;
|
||||||
@ -19,18 +18,18 @@ pub async fn serve_http_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req_is_https {
|
if req_is_https {
|
||||||
req.headers_mut()
|
req.headers_mut().insert(
|
||||||
.insert("x-forwarded-proto", HeaderValue::from_static("https"));
|
"x-forwarded-proto",
|
||||||
|
http::header::HeaderValue::from_static("https"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = config.url.as_ref();
|
match hyper_reverse_proxy::call(client_ip, proxy_addr, req).await {
|
||||||
|
|
||||||
match hyper_reverse_proxy::call(client_ip, url, req).await {
|
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
// ProxyError doesn't actually implement Error :facepalm: so we have to format the error
|
// ProxyError doesn't actually implement Error :facepalm: so we have to format the error
|
||||||
// manually
|
// manually
|
||||||
Err(e) => Err(unexpected::Error::from(
|
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(),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user