Compare commits
3 Commits
b44fd575a9
...
0e164f739d
Author | SHA1 | Date | |
---|---|---|---|
|
0e164f739d | ||
|
dcbf45ec85 | ||
|
7a1a2297d4 |
66
README.md
66
README.md
@ -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`
|
||||
|
@ -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::*;
|
||||
|
@ -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>,
|
||||
}
|
||||
|
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::{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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
);
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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! {
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user