Compare commits
8 Commits
03428cef02
...
a917f32f04
Author | SHA1 | Date | |
---|---|---|---|
|
a917f32f04 | ||
|
9beeffcdcf | ||
|
4a2ac7460f | ||
|
a86020eedf | ||
|
c336486f5a | ||
|
5dd2e756cc | ||
|
5a4ff4ca65 | ||
|
4483185e75 |
@ -3,9 +3,11 @@ origin:
|
|||||||
domain:
|
domain:
|
||||||
store_dir_path: /tmp/domani_dev_env/domain
|
store_dir_path: /tmp/domani_dev_env/domain
|
||||||
service:
|
service:
|
||||||
|
http:
|
||||||
|
form_method: GET
|
||||||
passphrase: foobar
|
passphrase: foobar
|
||||||
dns_records:
|
dns_records:
|
||||||
- type: A
|
- kind: A
|
||||||
addr: 127.0.0.1
|
addr: 127.0.0.1
|
||||||
- type: AAAA
|
- kind: AAAA
|
||||||
addr: ::1
|
addr: ::1
|
||||||
|
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -466,6 +466,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"hyper-reverse-proxy",
|
||||||
"log",
|
"log",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"mockall",
|
"mockall",
|
||||||
@ -1538,6 +1539,17 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-reverse-proxy"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc1af9b1b483fb9f33bd1cda26b35eacf902f0d116fcf0d56075ea5e5923b935"
|
||||||
|
dependencies = [
|
||||||
|
"hyper",
|
||||||
|
"lazy_static",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.24.1"
|
version = "0.24.1"
|
||||||
|
@ -44,3 +44,4 @@ env_logger = "0.10.0"
|
|||||||
serde_yaml = "0.9.22"
|
serde_yaml = "0.9.22"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
reqwest = "0.11.18"
|
reqwest = "0.11.18"
|
||||||
|
hyper-reverse-proxy = "0.5.1"
|
||||||
|
19
README.md
19
README.md
@ -53,6 +53,18 @@ domain:
|
|||||||
# renewed.
|
# renewed.
|
||||||
#contact_email: REQUIRED if service.http.https_addr is set
|
#contact_email: REQUIRED if service.http.https_addr is set
|
||||||
|
|
||||||
|
# builtins are domains whose configuration is built into domani. These domains
|
||||||
|
# are not able to be configured via the web interface, and will be hidden from
|
||||||
|
# it unless the `public` key is set to true.
|
||||||
|
#builtins:
|
||||||
|
|
||||||
|
# An example built-in domain backed by a git repo.
|
||||||
|
#example.com:
|
||||||
|
# kind: git
|
||||||
|
# url: "https://somewhere.com/some/repo.git"
|
||||||
|
# branch: main
|
||||||
|
# public: false
|
||||||
|
|
||||||
service:
|
service:
|
||||||
|
|
||||||
# Passphrase which must be given by users who are configuring new domains via
|
# Passphrase which must be given by users who are configuring new domains via
|
||||||
@ -66,14 +78,14 @@ service:
|
|||||||
# A CNAME record with the primary_domain of this server is automatically
|
# A CNAME record with the primary_domain of this server is automatically
|
||||||
# included.
|
# included.
|
||||||
dns_records:
|
dns_records:
|
||||||
#- type: A
|
#- kind: A
|
||||||
# addr: 127.0.0.1
|
# addr: 127.0.0.1
|
||||||
|
|
||||||
#- type: AAAA
|
#- kind: AAAA
|
||||||
# addr: ::1
|
# addr: ::1
|
||||||
|
|
||||||
# NOTE that the name given here must resolve to the Domani server.
|
# NOTE that the name given here must resolve to the Domani server.
|
||||||
#- type: CNAME
|
#- kind: CNAME
|
||||||
# name: domain.com
|
# name: domain.com
|
||||||
|
|
||||||
# The domain name which will be used to serve the web interface of Domani. If
|
# The domain name which will be used to serve the web interface of Domani. If
|
||||||
@ -126,5 +138,6 @@ Within the shell which opens you can do `cargo run` to start a local instance.
|
|||||||
* Support for more backends than just git repositories, including:
|
* Support for more backends than just git repositories, including:
|
||||||
* IPFS/IPNS
|
* IPFS/IPNS
|
||||||
* Alternative URLs (reverse proxy)
|
* Alternative URLs (reverse proxy)
|
||||||
|
* Small static files (e.g. for well-knowns)
|
||||||
* Google Drive
|
* Google Drive
|
||||||
* Dropbox
|
* Dropbox
|
||||||
|
@ -18,11 +18,11 @@ use sha2::{Digest, Sha256};
|
|||||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||||
/// Defines how a domain will behave when it is accessed. These are configured by the owner of the
|
/// Defines how a domain will behave when it is accessed. These are configured by the owner of the
|
||||||
/// domain during setup.
|
/// domain during setup.
|
||||||
pub struct Domain {
|
pub struct Settings {
|
||||||
pub origin_descr: origin::Descr,
|
pub origin_descr: origin::Descr,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Domain {
|
impl Settings {
|
||||||
pub fn hash(&self) -> Result<String, unexpected::Error> {
|
pub fn hash(&self) -> Result<String, unexpected::Error> {
|
||||||
let mut h = Sha256::new();
|
let mut h = Sha256::new();
|
||||||
serde_json::to_writer(&mut h, self).or_unexpected()?;
|
serde_json::to_writer(&mut h, self).or_unexpected()?;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
use std::{net, path, str::FromStr};
|
use std::{collections, net, path, str::FromStr};
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::domain;
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
@ -25,10 +27,20 @@ pub struct ConfigACME {
|
|||||||
pub contact_email: String,
|
pub contact_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct BuiltinDomain {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub settings: domain::Settings,
|
||||||
|
|
||||||
|
pub public: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
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)]
|
||||||
|
pub builtins: collections::HashMap<domain::Name, BuiltinDomain>,
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ use std::sync;
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum GetConfigError {
|
pub enum GetSettingsError {
|
||||||
#[error("not found")]
|
#[error("not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
@ -15,11 +15,11 @@ pub enum GetConfigError {
|
|||||||
Unexpected(#[from] unexpected::Error),
|
Unexpected(#[from] unexpected::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<store::GetError> for GetConfigError {
|
impl From<store::GetError> for GetSettingsError {
|
||||||
fn from(e: store::GetError) -> GetConfigError {
|
fn from(e: store::GetError) -> GetSettingsError {
|
||||||
match e {
|
match e {
|
||||||
store::GetError::NotFound => GetConfigError::NotFound,
|
store::GetError::NotFound => GetSettingsError::NotFound,
|
||||||
store::GetError::Unexpected(e) => GetConfigError::Unexpected(e),
|
store::GetError::Unexpected(e) => GetSettingsError::Unexpected(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,6 +32,9 @@ pub enum GetFileError {
|
|||||||
#[error("file not found")]
|
#[error("file not found")]
|
||||||
FileNotFound,
|
FileNotFound,
|
||||||
|
|
||||||
|
#[error("origin is of kind proxy")]
|
||||||
|
OriginIsProxy { url: String },
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unexpected(#[from] unexpected::Error),
|
Unexpected(#[from] unexpected::Error),
|
||||||
}
|
}
|
||||||
@ -79,7 +82,10 @@ impl From<store::GetError> for SyncError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SyncWithConfigError {
|
pub enum SyncWithSettingsError {
|
||||||
|
#[error("cannot call SyncWithSettings on builtin domain")]
|
||||||
|
BuiltinDomain,
|
||||||
|
|
||||||
#[error("invalid url")]
|
#[error("invalid url")]
|
||||||
InvalidURL,
|
InvalidURL,
|
||||||
|
|
||||||
@ -99,35 +105,36 @@ pub enum SyncWithConfigError {
|
|||||||
Unexpected(#[from] unexpected::Error),
|
Unexpected(#[from] unexpected::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<origin::SyncError> for SyncWithConfigError {
|
impl From<origin::SyncError> for SyncWithSettingsError {
|
||||||
fn from(e: origin::SyncError) -> SyncWithConfigError {
|
fn from(e: origin::SyncError) -> SyncWithSettingsError {
|
||||||
match e {
|
match e {
|
||||||
origin::SyncError::InvalidURL => SyncWithConfigError::InvalidURL,
|
origin::SyncError::InvalidURL => SyncWithSettingsError::InvalidURL,
|
||||||
origin::SyncError::InvalidBranchName => SyncWithConfigError::InvalidBranchName,
|
origin::SyncError::InvalidBranchName => SyncWithSettingsError::InvalidBranchName,
|
||||||
origin::SyncError::AlreadyInProgress => SyncWithConfigError::AlreadyInProgress,
|
origin::SyncError::AlreadyInProgress => SyncWithSettingsError::AlreadyInProgress,
|
||||||
origin::SyncError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
origin::SyncError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<checker::CheckDomainError> for SyncWithConfigError {
|
impl From<checker::CheckDomainError> for SyncWithSettingsError {
|
||||||
fn from(e: checker::CheckDomainError) -> SyncWithConfigError {
|
fn from(e: checker::CheckDomainError) -> SyncWithSettingsError {
|
||||||
match e {
|
match e {
|
||||||
checker::CheckDomainError::ServiceDNSRecordsNotSet => {
|
checker::CheckDomainError::ServiceDNSRecordsNotSet => {
|
||||||
SyncWithConfigError::ServiceDNSRecordsNotSet
|
SyncWithSettingsError::ServiceDNSRecordsNotSet
|
||||||
}
|
}
|
||||||
checker::CheckDomainError::ChallengeTokenNotSet => {
|
checker::CheckDomainError::ChallengeTokenNotSet => {
|
||||||
SyncWithConfigError::ChallengeTokenNotSet
|
SyncWithSettingsError::ChallengeTokenNotSet
|
||||||
}
|
}
|
||||||
checker::CheckDomainError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
checker::CheckDomainError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<store::SetError> for SyncWithConfigError {
|
impl From<store::SetError> for SyncWithSettingsError {
|
||||||
fn from(e: store::SetError) -> SyncWithConfigError {
|
fn from(e: store::SetError) -> SyncWithSettingsError {
|
||||||
match e {
|
match e {
|
||||||
store::SetError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
store::SetError::BuiltinDomain => SyncWithSettingsError::BuiltinDomain,
|
||||||
|
store::SetError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +143,7 @@ pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyEr
|
|||||||
|
|
||||||
//#[mockall::automock]
|
//#[mockall::automock]
|
||||||
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
||||||
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError>;
|
fn get_settings(&self, domain: &domain::Name) -> Result<domain::Settings, GetSettingsError>;
|
||||||
|
|
||||||
fn get_file<'store>(
|
fn get_file<'store>(
|
||||||
&'store self,
|
&'store self,
|
||||||
@ -149,11 +156,11 @@ pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
|||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>;
|
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>;
|
||||||
|
|
||||||
fn sync_with_config<'mgr>(
|
fn sync_with_settings<'mgr>(
|
||||||
&'mgr self,
|
&'mgr self,
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
config: domain::Domain,
|
settings: domain::Settings,
|
||||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithConfigError>>;
|
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>>;
|
||||||
|
|
||||||
fn get_acme_http01_challenge_key(
|
fn get_acme_http01_challenge_key(
|
||||||
&self,
|
&self,
|
||||||
@ -231,7 +238,7 @@ impl ManagerImpl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Manager for ManagerImpl {
|
impl Manager for ManagerImpl {
|
||||||
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError> {
|
fn get_settings(&self, domain: &domain::Name) -> Result<domain::Settings, GetSettingsError> {
|
||||||
Ok(self.domain_store.get(domain)?)
|
Ok(self.domain_store.get(domain)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +248,11 @@ impl Manager for ManagerImpl {
|
|||||||
path: &str,
|
path: &str,
|
||||||
) -> Result<util::BoxByteStream, GetFileError> {
|
) -> Result<util::BoxByteStream, GetFileError> {
|
||||||
let config = self.domain_store.get(domain)?;
|
let config = self.domain_store.get(domain)?;
|
||||||
|
|
||||||
|
if let origin::Descr::Proxy { url } = config.origin_descr {
|
||||||
|
return Err(GetFileError::OriginIsProxy { url });
|
||||||
|
}
|
||||||
|
|
||||||
let f = self.origin_store.get_file(&config.origin_descr, path)?;
|
let f = self.origin_store.get_file(&config.origin_descr, path)?;
|
||||||
Ok(f)
|
Ok(f)
|
||||||
}
|
}
|
||||||
@ -258,23 +270,21 @@ impl Manager for ManagerImpl {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_with_config<'mgr>(
|
fn sync_with_settings<'mgr>(
|
||||||
&'mgr self,
|
&'mgr self,
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
config: domain::Domain,
|
settings: domain::Settings,
|
||||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithConfigError>> {
|
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let config_hash = config
|
let hash = settings
|
||||||
.hash()
|
.hash()
|
||||||
.or_unexpected_while("calculating config hash")?;
|
.or_unexpected_while("calculating config hash")?;
|
||||||
|
|
||||||
self.domain_checker
|
self.domain_checker.check_domain(&domain, &hash).await?;
|
||||||
.check_domain(&domain, &config_hash)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.origin_store.sync(&config.origin_descr)?;
|
self.origin_store.sync(&settings.origin_descr)?;
|
||||||
|
|
||||||
self.domain_store.set(&domain, &config)?;
|
self.domain_store.set(&domain, &settings)?;
|
||||||
|
|
||||||
self.sync_cert(domain).await?;
|
self.sync_cert(domain).await?;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{cmp, fmt};
|
use std::{cmp, fmt, hash};
|
||||||
|
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use trust_dns_client::rr as trust_dns_rr;
|
use trust_dns_client::rr as trust_dns_rr;
|
||||||
@ -7,7 +7,7 @@ use trust_dns_client::rr as trust_dns_rr;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
/// Validated representation of a domain name
|
/// Validated representation of a domain name
|
||||||
pub struct Name {
|
pub struct Name {
|
||||||
inner: trust_dns_rr::Name,
|
rr: trust_dns_rr::Name,
|
||||||
utf8_str: String,
|
utf8_str: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ impl Name {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_rr(&self) -> &trust_dns_rr::Name {
|
pub fn as_rr(&self) -> &trust_dns_rr::Name {
|
||||||
&self.inner
|
&self.rr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,13 +36,21 @@ impl FromStr for Name {
|
|||||||
|
|
||||||
n.set_fqdn(true);
|
n.set_fqdn(true);
|
||||||
|
|
||||||
Ok(Name { inner: n, utf8_str })
|
Ok(Name { rr: n, utf8_str })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl cmp::PartialEq for Name {
|
impl cmp::PartialEq for Name {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.inner == other.inner
|
self.rr == other.rr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cmp::Eq for Name {}
|
||||||
|
|
||||||
|
impl hash::Hash for Name {
|
||||||
|
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||||
|
self.rr.hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
use std::path;
|
use std::{collections, fs, io, path, str::FromStr};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{fs, io};
|
|
||||||
|
|
||||||
use crate::domain;
|
use crate::domain;
|
||||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
use crate::error::unexpected::{self, Intoable, Mappable};
|
||||||
@ -16,14 +14,17 @@ pub enum GetError {
|
|||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SetError {
|
pub enum SetError {
|
||||||
|
#[error("cannot call set on builtin domain")]
|
||||||
|
BuiltinDomain,
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unexpected(#[from] unexpected::Error),
|
Unexpected(#[from] unexpected::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait Store {
|
pub trait Store {
|
||||||
fn get(&self, domain: &domain::Name) -> Result<domain::Domain, GetError>;
|
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError>;
|
||||||
fn set(&self, domain: &domain::Name, config: &domain::Domain) -> Result<(), SetError>;
|
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>;
|
||||||
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,40 +40,40 @@ impl FSStore {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_dir_path(&self, domain: &domain::Name) -> path::PathBuf {
|
fn settings_dir_path(&self, domain: &domain::Name) -> path::PathBuf {
|
||||||
self.dir_path.join(domain.as_str())
|
self.dir_path.join(domain.as_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_file_path(&self, domain: &domain::Name) -> path::PathBuf {
|
fn settings_file_path(&self, domain: &domain::Name) -> path::PathBuf {
|
||||||
self.config_dir_path(domain).join("config.json")
|
self.settings_dir_path(domain).join("settings.json")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store for FSStore {
|
impl Store for FSStore {
|
||||||
fn get(&self, domain: &domain::Name) -> Result<domain::Domain, GetError> {
|
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError> {
|
||||||
let path = self.config_file_path(domain);
|
let path = self.settings_file_path(domain);
|
||||||
let config_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,
|
||||||
_ => e
|
_ => e
|
||||||
.into_unexpected_while(format!("opening {}", path.display()))
|
.into_unexpected_while(format!("opening {}", path.display()))
|
||||||
.into(),
|
.into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let config = serde_json::from_reader(config_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(config)
|
Ok(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set(&self, domain: &domain::Name, config: &domain::Domain) -> Result<(), SetError> {
|
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> {
|
||||||
let dir_path = self.config_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()))?;
|
||||||
|
|
||||||
let file_path = self.config_file_path(domain);
|
let file_path = self.settings_file_path(domain);
|
||||||
let config_file = fs::File::create(file_path.as_path())
|
let settings_file = fs::File::create(file_path.as_path())
|
||||||
.map_unexpected_while(|| format!("creating file {}", file_path.display()))?;
|
.map_unexpected_while(|| format!("creating file {}", file_path.display()))?;
|
||||||
|
|
||||||
serde_json::to_writer(config_file, config)
|
serde_json::to_writer(settings_file, settings)
|
||||||
.map_unexpected_while(|| format!("writing config to {}", file_path.display()))?;
|
.map_unexpected_while(|| format!("writing config to {}", file_path.display()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -96,6 +97,51 @@ impl Store for FSStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct StoreWithBuiltin<S: Store> {
|
||||||
|
inner: S,
|
||||||
|
domains: collections::HashMap<domain::Name, domain::config::BuiltinDomain>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Store> StoreWithBuiltin<S> {
|
||||||
|
pub fn new(
|
||||||
|
inner: S,
|
||||||
|
builtin_domains: collections::HashMap<domain::Name, domain::config::BuiltinDomain>,
|
||||||
|
) -> StoreWithBuiltin<S> {
|
||||||
|
StoreWithBuiltin {
|
||||||
|
inner,
|
||||||
|
domains: builtin_domains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: Store> Store for StoreWithBuiltin<S> {
|
||||||
|
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError> {
|
||||||
|
if let Some(domain) = self.domains.get(domain) {
|
||||||
|
return Ok(domain.settings.clone());
|
||||||
|
}
|
||||||
|
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<domain::Name>, unexpected::Error> {
|
||||||
|
let inner_domains = self.inner.all_domains()?;
|
||||||
|
let mut domains: Vec<domain::Name> = self
|
||||||
|
.domains
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, v)| v.public)
|
||||||
|
.map(|(k, _)| k.clone())
|
||||||
|
.collect();
|
||||||
|
domains.extend(inner_domains);
|
||||||
|
Ok(domains)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{Store, *};
|
use super::{Store, *};
|
||||||
@ -108,13 +154,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn basic() {
|
fn basic() {
|
||||||
let tmp_dir = TempDir::new("domain_config_store").unwrap();
|
let tmp_dir = TempDir::new("domain_store").unwrap();
|
||||||
|
|
||||||
let store = FSStore::new(tmp_dir.path()).expect("store created");
|
let store = FSStore::new(tmp_dir.path()).expect("store created");
|
||||||
|
|
||||||
let domain = domain::Name::from_str("foo.com").expect("domain parsed");
|
let domain = domain::Name::from_str("foo.com").expect("domain parsed");
|
||||||
|
|
||||||
let config = domain::Domain {
|
let settings = domain::Settings {
|
||||||
origin_descr: Descr::Git {
|
origin_descr: Descr::Git {
|
||||||
url: "bar".to_string(),
|
url: "bar".to_string(),
|
||||||
branch_name: "baz".to_string(),
|
branch_name: "baz".to_string(),
|
||||||
@ -123,20 +169,23 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.get(&domain),
|
store.get(&domain),
|
||||||
Err::<domain::Domain, GetError>(GetError::NotFound)
|
Err::<domain::Settings, GetError>(GetError::NotFound)
|
||||||
));
|
));
|
||||||
|
|
||||||
store.set(&domain, &config).expect("config set");
|
store.set(&domain, &settings).expect("set");
|
||||||
assert_eq!(config, store.get(&domain).expect("config retrieved"));
|
assert_eq!(settings, store.get(&domain).expect("settings retrieved"));
|
||||||
|
|
||||||
let new_config = domain::Domain {
|
let new_settings = domain::Settings {
|
||||||
origin_descr: Descr::Git {
|
origin_descr: Descr::Git {
|
||||||
url: "BAR".to_string(),
|
url: "BAR".to_string(),
|
||||||
branch_name: "BAZ".to_string(),
|
branch_name: "BAZ".to_string(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
store.set(&domain, &new_config).expect("config set");
|
store.set(&domain, &new_settings).expect("set");
|
||||||
assert_eq!(new_config, store.get(&domain).expect("config retrieved"));
|
assert_eq!(
|
||||||
|
new_settings,
|
||||||
|
store.get(&domain).expect("settings retrieved")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,10 +89,13 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("domain checker initialization failed");
|
.expect("domain checker initialization failed");
|
||||||
|
|
||||||
let domain_config_store =
|
let domain_store =
|
||||||
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
|
||||||
@ -121,7 +124,7 @@ async fn main() {
|
|||||||
let domain_manager = domani::domain::manager::ManagerImpl::new(
|
let domain_manager = domani::domain::manager::ManagerImpl::new(
|
||||||
&mut task_stack,
|
&mut task_stack,
|
||||||
origin_store,
|
origin_store,
|
||||||
domain_config_store,
|
domain_store,
|
||||||
domain_checker,
|
domain_checker,
|
||||||
domain_acme_manager,
|
domain_acme_manager,
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,7 @@ mod config;
|
|||||||
mod descr;
|
mod descr;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod mux;
|
pub mod mux;
|
||||||
|
pub mod proxy;
|
||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
pub use descr::Descr;
|
pub use descr::Descr;
|
||||||
|
@ -3,9 +3,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
/// A unique description of an origin, from where a domain might be served.
|
/// A unique description of an origin, from where a domain might be served.
|
||||||
pub enum Descr {
|
pub enum Descr {
|
||||||
Git { url: String, branch_name: String },
|
Git { url: String, branch_name: String },
|
||||||
|
Proxy { url: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Descr {
|
impl Descr {
|
||||||
@ -24,6 +26,10 @@ impl Descr {
|
|||||||
h_update(url);
|
h_update(url);
|
||||||
h_update(branch_name);
|
h_update(branch_name);
|
||||||
}
|
}
|
||||||
|
Descr::Proxy { url } => {
|
||||||
|
h_update("proxy");
|
||||||
|
h_update(url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h.finalize().encode_hex::<String>()
|
h.finalize().encode_hex::<String>()
|
||||||
|
@ -56,15 +56,24 @@ impl FSStore {
|
|||||||
format!("origin/{branch_name}")
|
format!("origin/{branch_name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deconstruct_descr(descr: &origin::Descr) -> (&str, &str) {
|
||||||
|
if let origin::Descr::Git {
|
||||||
|
ref url,
|
||||||
|
ref branch_name,
|
||||||
|
} = descr
|
||||||
|
{
|
||||||
|
(url, branch_name)
|
||||||
|
} else {
|
||||||
|
panic!("non git descr passed in")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn create_repo_snapshot(
|
fn create_repo_snapshot(
|
||||||
&self,
|
&self,
|
||||||
repo: gix::Repository,
|
repo: gix::Repository,
|
||||||
descr: &origin::Descr,
|
descr: &origin::Descr,
|
||||||
) -> Result<RepoSnapshot, CreateRepoSnapshotError> {
|
) -> Result<RepoSnapshot, CreateRepoSnapshotError> {
|
||||||
let origin::Descr::Git {
|
let (_, branch_name) = Self::deconstruct_descr(descr);
|
||||||
ref branch_name, ..
|
|
||||||
} = descr;
|
|
||||||
|
|
||||||
let branch_ref = self.branch_ref(branch_name);
|
let branch_ref = self.branch_ref(branch_name);
|
||||||
|
|
||||||
let commit_object_id = repo
|
let commit_object_id = repo
|
||||||
@ -148,10 +157,7 @@ impl FSStore {
|
|||||||
fs::create_dir_all(repo_path)
|
fs::create_dir_all(repo_path)
|
||||||
.map_unexpected_while(|| format!("creating {}", repo_path.display()))?;
|
.map_unexpected_while(|| format!("creating {}", repo_path.display()))?;
|
||||||
|
|
||||||
let origin::Descr::Git {
|
let (url, branch_name) = Self::deconstruct_descr(descr);
|
||||||
ref url,
|
|
||||||
ref branch_name,
|
|
||||||
} = descr;
|
|
||||||
|
|
||||||
let (repo, _) = gix::prepare_clone_bare(url.clone(), repo_path)
|
let (repo, _) = gix::prepare_clone_bare(url.clone(), repo_path)
|
||||||
.map_err(|e| match e {
|
.map_err(|e| match e {
|
||||||
|
50
src/origin/proxy.rs
Normal file
50
src/origin/proxy.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
use crate::error::unexpected::{self, Mappable};
|
||||||
|
use std::{net, str::FromStr};
|
||||||
|
|
||||||
|
// proxy is a special case because it is so tied to the underlying protocol that a request is
|
||||||
|
// being served on, it can't be abstracted out into a simple "get_file" operation like other
|
||||||
|
// origins.
|
||||||
|
|
||||||
|
pub async fn serve_http_request(
|
||||||
|
client_ip: net::IpAddr,
|
||||||
|
proxy_url: &str,
|
||||||
|
mut req: hyper::Request<hyper::Body>,
|
||||||
|
) -> unexpected::Result<hyper::Response<hyper::Body>> {
|
||||||
|
let parsed_proxy_url =
|
||||||
|
http::Uri::from_str(proxy_url).or_unexpected_while("parsing proxy url")?;
|
||||||
|
|
||||||
|
let scheme = parsed_proxy_url
|
||||||
|
.scheme()
|
||||||
|
.or_unexpected_while("expected a scheme of http in the proxy url")?;
|
||||||
|
if scheme != "http" {
|
||||||
|
return Err(unexpected::Error::from("proxy url scheme should be 'http"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out what the host header should be, based on the host[:port] of the proxy_url
|
||||||
|
let host = {
|
||||||
|
let authority = parsed_proxy_url
|
||||||
|
.authority()
|
||||||
|
.or_unexpected_while("getting host from proxy url, there is no host")?;
|
||||||
|
|
||||||
|
let host_and_port;
|
||||||
|
let mut host = authority.host();
|
||||||
|
|
||||||
|
if let Some(port) = authority.port() {
|
||||||
|
host_and_port = format!("{host}:{port}");
|
||||||
|
host = host_and_port.as_str();
|
||||||
|
};
|
||||||
|
|
||||||
|
http::header::HeaderValue::from_str(host).or_unexpected()?
|
||||||
|
};
|
||||||
|
|
||||||
|
req.headers_mut().insert("host", host);
|
||||||
|
|
||||||
|
match hyper_reverse_proxy::call(client_ip, proxy_url, 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: {e:?}").as_str(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +1,5 @@
|
|||||||
|
mod config;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use crate::domain;
|
pub use config::*;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{net, str::FromStr};
|
|
||||||
|
|
||||||
fn default_primary_domain() -> domain::Name {
|
|
||||||
domain::Name::from_str("localhost").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum ConfigDNSRecord {
|
|
||||||
A { addr: net::Ipv4Addr },
|
|
||||||
AAAA { addr: net::Ipv6Addr },
|
|
||||||
CNAME { name: domain::Name },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
|
||||||
fn from(r: ConfigDNSRecord) -> Self {
|
|
||||||
match r {
|
|
||||||
ConfigDNSRecord::A { addr } => Self::A(addr),
|
|
||||||
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
|
|
||||||
ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
pub passphrase: String,
|
|
||||||
pub dns_records: Vec<ConfigDNSRecord>,
|
|
||||||
#[serde(default = "default_primary_domain")]
|
|
||||||
pub primary_domain: domain::Name,
|
|
||||||
#[serde(default)]
|
|
||||||
pub http: self::http::Config,
|
|
||||||
}
|
|
||||||
|
35
src/service/config.rs
Normal file
35
src/service/config.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use crate::{domain, service};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{net, str::FromStr};
|
||||||
|
|
||||||
|
fn default_primary_domain() -> domain::Name {
|
||||||
|
domain::Name::from_str("localhost").unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
|
pub enum ConfigDNSRecord {
|
||||||
|
A { addr: net::Ipv4Addr },
|
||||||
|
AAAA { addr: net::Ipv6Addr },
|
||||||
|
CNAME { name: domain::Name },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
||||||
|
fn from(r: ConfigDNSRecord) -> Self {
|
||||||
|
match r {
|
||||||
|
ConfigDNSRecord::A { addr } => Self::A(addr),
|
||||||
|
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
|
||||||
|
ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub passphrase: String,
|
||||||
|
pub dns_records: Vec<ConfigDNSRecord>,
|
||||||
|
#[serde(default = "default_primary_domain")]
|
||||||
|
pub primary_domain: domain::Name,
|
||||||
|
#[serde(default)]
|
||||||
|
pub http: service::http::Config,
|
||||||
|
}
|
@ -4,14 +4,15 @@ mod tpl;
|
|||||||
|
|
||||||
pub use config::*;
|
pub use config::*;
|
||||||
|
|
||||||
|
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::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{future, sync};
|
use std::{future, net, sync};
|
||||||
|
|
||||||
use crate::error::unexpected;
|
use crate::error::unexpected;
|
||||||
use crate::{domain, service, util};
|
use crate::{domain, origin, service, util};
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
@ -48,6 +49,7 @@ pub fn new(
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct BasePresenter<'a, T> {
|
struct BasePresenter<'a, T> {
|
||||||
page_name: &'a str,
|
page_name: &'a str,
|
||||||
|
form_method: &'a str,
|
||||||
data: T,
|
data: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,12 +61,18 @@ struct DomainGetArgs {
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct DomainInitArgs {
|
struct DomainInitArgs {
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
flat_domain_settings: service::util::FlatDomainSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct DomainSyncArgs {
|
struct DomainSyncArgs {
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
flat_domain_settings: service::util::FlatDomainSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'svc> Service {
|
impl<'svc> Service {
|
||||||
@ -121,6 +129,7 @@ impl<'svc> Service {
|
|||||||
"/base.html",
|
"/base.html",
|
||||||
BasePresenter {
|
BasePresenter {
|
||||||
page_name: "/error.html",
|
page_name: "/error.html",
|
||||||
|
form_method: self.config.http.form_method.as_str(),
|
||||||
data: &Response { error_msg: e },
|
data: &Response { error_msg: e },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -143,13 +152,20 @@ impl<'svc> Service {
|
|||||||
"/base.html",
|
"/base.html",
|
||||||
BasePresenter {
|
BasePresenter {
|
||||||
page_name: name,
|
page_name: name,
|
||||||
|
form_method: self.config.http.form_method.as_str(),
|
||||||
data,
|
data,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serve_origin(&self, domain: domain::Name, path: &str) -> Response<Body> {
|
async fn serve_origin(
|
||||||
|
&self,
|
||||||
|
client_ip: net::IpAddr,
|
||||||
|
domain: domain::Name,
|
||||||
|
req: Request<Body>,
|
||||||
|
) -> Response<Body> {
|
||||||
let mut path_owned;
|
let mut path_owned;
|
||||||
|
let path = req.uri().path();
|
||||||
|
|
||||||
let path = match path.ends_with('/') {
|
let path = match path.ends_with('/') {
|
||||||
true => {
|
true => {
|
||||||
@ -168,20 +184,46 @@ impl<'svc> Service {
|
|||||||
Err(domain::manager::GetFileError::FileNotFound) => {
|
Err(domain::manager::GetFileError::FileNotFound) => {
|
||||||
self.render_error_page(404, "File not found")
|
self.render_error_page(404, "File not found")
|
||||||
}
|
}
|
||||||
|
Err(domain::manager::GetFileError::OriginIsProxy { url }) => {
|
||||||
|
origin::proxy::serve_http_request(client_ip, &url, req)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
self.internal_error(format!("proxying {domain} to {url}: {e}").as_str())
|
||||||
|
})
|
||||||
|
}
|
||||||
Err(domain::manager::GetFileError::Unexpected(e)) => {
|
Err(domain::manager::GetFileError::Unexpected(e)) => {
|
||||||
self.internal_error(format!("failed to fetch file {path}: {e}").as_str())
|
self.internal_error(format!("failed to fetch file {path}: {e}").as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn with_query_req<'a, F, In, Out>(&self, req: &'a Request<Body>, f: F) -> Response<Body>
|
async fn with_query_req<'a, F, In, Out>(
|
||||||
|
&self,
|
||||||
|
req: &'a Parts,
|
||||||
|
body: Body,
|
||||||
|
f: F,
|
||||||
|
) -> Response<Body>
|
||||||
where
|
where
|
||||||
In: Deserialize<'a>,
|
In: for<'d> Deserialize<'d>,
|
||||||
F: FnOnce(In) -> Out,
|
F: FnOnce(In) -> Out,
|
||||||
Out: future::Future<Output = Response<Body>>,
|
Out: future::Future<Output = Response<Body>>,
|
||||||
{
|
{
|
||||||
let query = req.uri().query().unwrap_or("");
|
let res = match self.config.http.form_method {
|
||||||
match serde_urlencoded::from_str::<In>(query) {
|
ConfigFormMethod::GET => {
|
||||||
|
serde_urlencoded::from_str::<In>(req.uri.query().unwrap_or(""))
|
||||||
|
}
|
||||||
|
ConfigFormMethod::POST => {
|
||||||
|
let body = match hyper::body::to_bytes(body).await {
|
||||||
|
Ok(bytes) => bytes.to_vec(),
|
||||||
|
Err(e) => {
|
||||||
|
return self.internal_error(format!("failed to read body: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
serde_urlencoded::from_bytes::<In>(body.as_ref())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match res {
|
||||||
Ok(args) => f(args).await,
|
Ok(args) => f(args).await,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.render_error_page(400, format!("failed to parse query args: {}", err).as_str())
|
self.render_error_page(400, format!("failed to parse query args: {}", err).as_str())
|
||||||
@ -191,43 +233,35 @@ impl<'svc> Service {
|
|||||||
|
|
||||||
fn domain_get(&self, args: DomainGetArgs) -> Response<Body> {
|
fn domain_get(&self, args: DomainGetArgs) -> Response<Body> {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Response {
|
struct Data {
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
config: Option<domain::Domain>,
|
settings: Option<domain::Settings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = match self.domain_manager.get_config(&args.domain) {
|
let settings = match self.domain_manager.get_settings(&args.domain) {
|
||||||
Ok(config) => Some(config),
|
Ok(settings) => Some(settings),
|
||||||
Err(domain::manager::GetConfigError::NotFound) => None,
|
Err(domain::manager::GetSettingsError::NotFound) => None,
|
||||||
Err(domain::manager::GetConfigError::Unexpected(e)) => {
|
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
|
||||||
return self.internal_error(
|
return self.internal_error(
|
||||||
format!(
|
format!("retrieving settings for domain {}: {}", &args.domain, e).as_str(),
|
||||||
"retrieving configuration for domain {}: {}",
|
|
||||||
&args.domain, e
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
self.render_page(
|
self.render_page(
|
||||||
"/domain.html",
|
"/domain.html",
|
||||||
Response {
|
Data {
|
||||||
domain: args.domain,
|
domain: args.domain,
|
||||||
config,
|
settings,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn domain_init(
|
fn domain_init(&self, args: DomainInitArgs) -> Response<Body> {
|
||||||
&self,
|
|
||||||
args: DomainInitArgs,
|
|
||||||
domain_config: service::util::FlatConfig,
|
|
||||||
) -> Response<Body> {
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Response<'a> {
|
struct Data<'a> {
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
flat_config: service::util::FlatConfig,
|
flat_domain_settings: service::util::FlatDomainSettings,
|
||||||
dns_records: &'a [service::ConfigDNSRecord],
|
dns_records: &'a [service::ConfigDNSRecord],
|
||||||
challenge_token: String,
|
challenge_token: String,
|
||||||
|
|
||||||
@ -235,19 +269,19 @@ impl<'svc> Service {
|
|||||||
dns_records_have_cname: bool,
|
dns_records_have_cname: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: domain::Domain = match domain_config.try_into() {
|
let settings: domain::Settings = match args.flat_domain_settings.try_into() {
|
||||||
Ok(Some(config)) => config,
|
Ok(settings) => settings,
|
||||||
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
return self
|
||||||
|
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let config_hash = match config.hash() {
|
let settings_hash = match settings.hash() {
|
||||||
Ok(hash) => hash,
|
Ok(hash) => hash,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return self.internal_error(
|
return self.internal_error(
|
||||||
format!("failed to hash domain config {config:?}: {e}").as_str(),
|
format!("failed to hash domain settings {settings:?}: {e}").as_str(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -258,13 +292,21 @@ impl<'svc> Service {
|
|||||||
_ => false,
|
_ => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let flat_domain_settings = match settings.try_into() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
return self
|
||||||
|
.internal_error(format!("failed to flatten domains settings: {e}").as_str())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
self.render_page(
|
self.render_page(
|
||||||
"/domain_init.html",
|
"/domain_init.html",
|
||||||
Response {
|
Data {
|
||||||
domain: args.domain,
|
domain: args.domain,
|
||||||
flat_config: config.into(),
|
flat_domain_settings,
|
||||||
dns_records: &self.config.dns_records,
|
dns_records: &self.config.dns_records,
|
||||||
challenge_token: config_hash,
|
challenge_token: settings_hash,
|
||||||
|
|
||||||
domain_is_zone_apex,
|
domain_is_zone_apex,
|
||||||
dns_records_have_cname,
|
dns_records_have_cname,
|
||||||
@ -272,50 +314,48 @@ impl<'svc> Service {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn domain_sync(
|
async fn domain_sync(&self, args: DomainSyncArgs) -> Response<Body> {
|
||||||
&self,
|
|
||||||
args: DomainSyncArgs,
|
|
||||||
domain_config: service::util::FlatConfig,
|
|
||||||
) -> Response<Body> {
|
|
||||||
if args.passphrase != self.config.passphrase.as_str() {
|
if args.passphrase != self.config.passphrase.as_str() {
|
||||||
return self.render_error_page(401, "Incorrect passphrase");
|
return self.render_error_page(401, "Incorrect passphrase");
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: domain::Domain = match domain_config.try_into() {
|
let settings: domain::Settings = match args.flat_domain_settings.try_into() {
|
||||||
Ok(Some(config)) => config,
|
Ok(settings) => settings,
|
||||||
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
return self
|
||||||
|
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let sync_result = self
|
let sync_result = self
|
||||||
.domain_manager
|
.domain_manager
|
||||||
.sync_with_config(args.domain.clone(), config)
|
.sync_with_settings(args.domain.clone(), settings)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct Response {
|
struct Data {
|
||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
error_msg: Option<String>,
|
error_msg: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let error_msg = match sync_result {
|
let error_msg = match sync_result {
|
||||||
Ok(_) => None,
|
Ok(_) => None,
|
||||||
Err(domain::manager::SyncWithConfigError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
|
Err(domain::manager::SyncWithSettingsError::BuiltinDomain) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()),
|
||||||
Err(domain::manager::SyncWithConfigError::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::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
|
||||||
Err(domain::manager::SyncWithConfigError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".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::SyncWithConfigError::ServiceDNSRecordsNotSet) => Some("None of the expected service DNS records were set on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".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::SyncWithConfigError::ChallengeTokenNotSet) => Some("The TXT record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
Err(domain::manager::SyncWithSettingsError::ServiceDNSRecordsNotSet) => Some("None of the expected service DNS records were set on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||||
Err(domain::manager::SyncWithConfigError::Unexpected(e)) => Some(format!("An unexpected error occurred: {e}")),
|
Err(domain::manager::SyncWithSettingsError::ChallengeTokenNotSet) => Some("The TXT record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||||
|
Err(domain::manager::SyncWithSettingsError::Unexpected(e)) => Some(format!("An unexpected error occurred: {e}")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = Response {
|
self.render_page(
|
||||||
|
"/domain_sync.html",
|
||||||
|
Data {
|
||||||
domain: args.domain,
|
domain: args.domain,
|
||||||
error_msg,
|
error_msg,
|
||||||
};
|
},
|
||||||
|
)
|
||||||
self.render_page("/domain_sync.html", response)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn domains(&self) -> Response<Body> {
|
fn domains(&self) -> Response<Body> {
|
||||||
@ -339,7 +379,7 @@ impl<'svc> Service {
|
|||||||
self.render_page("/domains.html", Response { domains })
|
self.render_page("/domains.html", Response { domains })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_request(&self, req: Request<Body>) -> Response<Body> {
|
async fn handle_request(&self, client_ip: net::IpAddr, req: Request<Body>) -> Response<Body> {
|
||||||
let maybe_host = match (
|
let maybe_host = match (
|
||||||
req.headers()
|
req.headers()
|
||||||
.get("Host")
|
.get("Host")
|
||||||
@ -353,13 +393,13 @@ impl<'svc> Service {
|
|||||||
}
|
}
|
||||||
.and_then(|h| domain::Name::from_str(h).ok());
|
.and_then(|h| domain::Name::from_str(h).ok());
|
||||||
|
|
||||||
let method = req.method();
|
{
|
||||||
let path = req.uri().path();
|
let path = req.uri().path();
|
||||||
|
|
||||||
// Serving acme challenges always takes priority. We serve them from the same store no
|
// Serving acme challenges always takes priority. We serve them from the same store no
|
||||||
// matter the domain, presumably they are cryptographically random enough that it doesn't
|
// matter the domain, presumably they are cryptographically random enough that it doesn't
|
||||||
// matter.
|
// matter.
|
||||||
if method == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
|
if req.method() == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
|
||||||
let token = path.trim_start_matches("/.well-known/acme-challenge/");
|
let token = path.trim_start_matches("/.well-known/acme-challenge/");
|
||||||
|
|
||||||
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
|
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
|
||||||
@ -368,7 +408,7 @@ impl<'svc> Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serving domani challenges similarly takes priority.
|
// Serving domani challenges similarly takes priority.
|
||||||
if method == Method::GET && path == "/.well-known/domani-challenge" {
|
if req.method() == Method::GET && path == "/.well-known/domani-challenge" {
|
||||||
if let Some(ref domain) = maybe_host {
|
if let Some(ref domain) = maybe_host {
|
||||||
match self
|
match self
|
||||||
.domain_manager
|
.domain_manager
|
||||||
@ -384,41 +424,42 @@ impl<'svc> Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If a managed domain was given then serve that from its origin
|
// If a managed domain was given then serve that from its origin
|
||||||
if let Some(domain) = maybe_host {
|
if let Some(domain) = maybe_host {
|
||||||
return self.serve_origin(domain, req.uri().path());
|
return self.serve_origin(client_ip, domain, req).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve main domani site
|
// Serve main domani site
|
||||||
|
let (req, body) = req.into_parts();
|
||||||
|
let path = req.uri.path();
|
||||||
|
|
||||||
if method == Method::GET && path.starts_with("/static/") {
|
if req.method == Method::GET && path.starts_with("/static/") {
|
||||||
return self.render(200, path, ());
|
return self.render(200, path, ());
|
||||||
}
|
}
|
||||||
|
|
||||||
match (method, path) {
|
let config_form_method = self.config.http.form_method.as_ref();
|
||||||
|
|
||||||
|
match (&req.method, path) {
|
||||||
(&Method::GET, "/") | (&Method::GET, "/index.html") => {
|
(&Method::GET, "/") | (&Method::GET, "/index.html") => {
|
||||||
self.render_page("/index.html", ())
|
self.render_page("/index.html", ())
|
||||||
}
|
}
|
||||||
(&Method::GET, "/domain.html") => {
|
(form_method, "/domain.html") if form_method == config_form_method => {
|
||||||
self.with_query_req(&req, |args: DomainGetArgs| async { self.domain_get(args) })
|
self.with_query_req(&req, body, |args: DomainGetArgs| async {
|
||||||
.await
|
self.domain_get(args)
|
||||||
}
|
|
||||||
(&Method::GET, "/domain_init.html") => {
|
|
||||||
self.with_query_req(&req, |args: DomainInitArgs| async {
|
|
||||||
self.with_query_req(&req, |config: service::util::FlatConfig| async {
|
|
||||||
self.domain_init(args, config)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
(&Method::GET, "/domain_sync.html") => {
|
(form_method, "/domain_init.html") if form_method == config_form_method => {
|
||||||
self.with_query_req(&req, |args: DomainSyncArgs| async {
|
self.with_query_req(&req, body, |args: DomainInitArgs| async {
|
||||||
self.with_query_req(&req, |config: service::util::FlatConfig| async {
|
self.domain_init(args)
|
||||||
self.domain_sync(args, config).await
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
}
|
||||||
|
(form_method, "/domain_sync.html") if form_method == config_form_method => {
|
||||||
|
self.with_query_req(&req, body, |args: DomainSyncArgs| async {
|
||||||
|
self.domain_sync(args).await
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,44 @@ fn default_http_addr() -> net::SocketAddr {
|
|||||||
net::SocketAddr::from_str("[::]:3030").unwrap()
|
net::SocketAddr::from_str("[::]:3030").unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub enum ConfigFormMethod {
|
||||||
|
GET,
|
||||||
|
POST,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigFormMethod {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::GET => "GET",
|
||||||
|
Self::POST => "POST",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConfigFormMethod {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::POST
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<hyper::Method> for ConfigFormMethod {
|
||||||
|
fn as_ref(&self) -> &hyper::Method {
|
||||||
|
match self {
|
||||||
|
Self::GET => &hyper::Method::GET,
|
||||||
|
Self::POST => &hyper::Method::POST,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(default = "default_http_addr")]
|
#[serde(default = "default_http_addr")]
|
||||||
pub http_addr: net::SocketAddr,
|
pub http_addr: net::SocketAddr,
|
||||||
pub https_addr: Option<net::SocketAddr>,
|
pub https_addr: Option<net::SocketAddr>,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub form_method: ConfigFormMethod,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
@ -17,6 +50,7 @@ impl Default for Config {
|
|||||||
Self {
|
Self {
|
||||||
http_addr: default_http_addr(),
|
http_addr: default_http_addr(),
|
||||||
https_addr: None,
|
https_addr: None,
|
||||||
|
form_method: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ use crate::service;
|
|||||||
use std::{convert, future, sync};
|
use std::{convert, future, sync};
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use hyper::server::conn::AddrStream;
|
||||||
|
use tokio_rustls::server::TlsStream;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
pub async fn listen_http(
|
pub async fn listen_http(
|
||||||
@ -13,13 +15,14 @@ pub async fn listen_http(
|
|||||||
let addr = service.config.http.http_addr.clone();
|
let addr = service.config.http.http_addr.clone();
|
||||||
let primary_domain = service.config.primary_domain.clone();
|
let primary_domain = service.config.primary_domain.clone();
|
||||||
|
|
||||||
let make_service = hyper::service::make_service_fn(move |_| {
|
let make_service = hyper::service::make_service_fn(move |conn: &AddrStream| {
|
||||||
let service = service.clone();
|
let service = service.clone();
|
||||||
|
let client_ip = conn.remote_addr().ip();
|
||||||
|
|
||||||
// Create a `Service` for responding to the request.
|
// Create a `Service` for responding to the request.
|
||||||
let hyper_service = hyper::service::service_fn(move |req| {
|
let hyper_service = hyper::service::service_fn(move |req| {
|
||||||
let service = service.clone();
|
let service = service.clone();
|
||||||
async move { Ok::<_, convert::Infallible>(service.handle_request(req).await) }
|
async move { Ok::<_, convert::Infallible>(service.handle_request(client_ip, req).await) }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return the service to hyper.
|
// Return the service to hyper.
|
||||||
@ -48,13 +51,14 @@ pub async fn listen_https(
|
|||||||
let addr = service.config.http.https_addr.unwrap().clone();
|
let addr = service.config.http.https_addr.unwrap().clone();
|
||||||
let primary_domain = service.config.primary_domain.clone();
|
let primary_domain = service.config.primary_domain.clone();
|
||||||
|
|
||||||
let make_service = hyper::service::make_service_fn(move |_| {
|
let make_service = hyper::service::make_service_fn(move |conn: &TlsStream<AddrStream>| {
|
||||||
let service = service.clone();
|
let service = service.clone();
|
||||||
|
let client_ip = conn.get_ref().0.remote_addr().ip();
|
||||||
|
|
||||||
// Create a `Service` for responding to the request.
|
// Create a `Service` for responding to the request.
|
||||||
let hyper_service = hyper::service::service_fn(move |req| {
|
let hyper_service = hyper::service::service_fn(move |req| {
|
||||||
let service = service.clone();
|
let service = service.clone();
|
||||||
async move { Ok::<_, convert::Infallible>(service.handle_request(req).await) }
|
async move { Ok::<_, convert::Infallible>(service.handle_request(client_ip, req).await) }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return the service to hyper.
|
// Return the service to hyper.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Configure New Domain
|
Configure New Domain
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{{# if data.config }}
|
{{# if data.settings }}
|
||||||
|
|
||||||
<p>Your domain <code>{{ data.domain }}</code> is already configured with
|
<p>Your domain <code>{{ data.domain }}</code> is already configured with
|
||||||
Domani. You can see the existing configuration below. If you modify any values
|
Domani. You can see the existing configuration below. If you modify any values
|
||||||
@ -20,7 +20,7 @@ automatically updated too!</p>
|
|||||||
<p><em>In the future Domani will support more backends than just git
|
<p><em>In the future Domani will support more backends than just git
|
||||||
repos.</em></p>
|
repos.</em></p>
|
||||||
|
|
||||||
<form method="GET" action="/domain_init.html">
|
<form method="{{ form_method }}" action="/domain_init.html">
|
||||||
<input name="domain" type="hidden" value="{{ data.domain }}" />
|
<input name="domain" type="hidden" value="{{ data.domain }}" />
|
||||||
<input name="config_origin_descr_kind" type="hidden" value="git" />
|
<input name="config_origin_descr_kind" type="hidden" value="git" />
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ automatically updated too!</p>
|
|||||||
<input name="config_origin_descr_git_url"
|
<input name="config_origin_descr_git_url"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="https://example.com/some_repo.git"
|
placeholder="https://example.com/some_repo.git"
|
||||||
value="{{ data.config.origin_descr.Git.url }}"
|
value="{{ data.settings.origin_descr.Git.url }}"
|
||||||
required />
|
required />
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
@ -43,7 +43,7 @@ automatically updated too!</p>
|
|||||||
<input name="config_origin_descr_git_branch_name"
|
<input name="config_origin_descr_git_branch_name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="main / master / etc..."
|
placeholder="main / master / etc..."
|
||||||
value="{{ data.config.origin_descr.Git.branch_name }}"
|
value="{{ data.settings.origin_descr.Git.branch_name }}"
|
||||||
required />
|
required />
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
<p>This step requires a passphrase that has been given to you by the
|
<p>This step requires a passphrase that has been given to you by the
|
||||||
administrator of the Domani server:</p>
|
administrator of the Domani server:</p>
|
||||||
|
|
||||||
<form method="GET" action="/domain_sync.html" id="syncForm">
|
<form method="{{ form_method }}" action="/domain_sync.html" id="syncForm">
|
||||||
<input name="domain" type="hidden" value="{{ data.domain }}" />
|
<input name="domain" type="hidden" value="{{ data.domain }}" />
|
||||||
{{ #each data.flat_config }}
|
{{ #each data.flat_domain_settings }}
|
||||||
<input name="{{ @key }}" type="hidden" value="{{ this }}" />
|
<input name="{{ @key }}" type="hidden" value="{{ this }}" />
|
||||||
{{ /each }}
|
{{ /each }}
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ query for your domain name. It can be <strong>one or more of</strong>:</p>
|
|||||||
|
|
||||||
{{ #each data.dns_records }}
|
{{ #each data.dns_records }}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ this.type }}</td>
|
<td>{{ this.kind }}</td>
|
||||||
<td>{{ lookup ../data "domain" }}</td>
|
<td>{{ lookup ../data "domain" }}</td>
|
||||||
{{ #if this.name }}
|
{{ #if this.name }}
|
||||||
<td>{{ this.name }}</td>
|
<td>{{ this.name }}</td>
|
||||||
|
@ -13,7 +13,7 @@ server, and you're done!</p>
|
|||||||
<p>Input your domain name below to set it up, or to reconfigure it has already
|
<p>Input your domain name below to set it up, or to reconfigure it has already
|
||||||
been set up.</p>
|
been set up.</p>
|
||||||
|
|
||||||
<form method="get" action="/domain.html">
|
<form method="{{ form_method }}" action="/domain.html">
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<label>
|
<label>
|
||||||
|
@ -1,49 +1,58 @@
|
|||||||
use std::convert::{From, TryFrom};
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{domain, origin};
|
use crate::{domain, error::unexpected, origin};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
pub struct FlatConfig {
|
pub struct FlatDomainSettings {
|
||||||
config_origin_descr_kind: Option<String>,
|
domain_setting_origin_descr_kind: Option<String>,
|
||||||
config_origin_descr_git_url: Option<String>,
|
|
||||||
config_origin_descr_git_branch_name: Option<String>,
|
domain_setting_origin_descr_git_url: Option<String>,
|
||||||
|
domain_setting_origin_descr_git_branch_name: Option<String>,
|
||||||
|
|
||||||
|
domain_setting_origin_descr_proxy_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<FlatConfig> for Option<domain::Domain> {
|
impl TryFrom<FlatDomainSettings> for domain::Settings {
|
||||||
type Error = String;
|
type Error = String;
|
||||||
|
|
||||||
fn try_from(v: FlatConfig) -> Result<Self, Self::Error> {
|
fn try_from(v: FlatDomainSettings) -> Result<Self, Self::Error> {
|
||||||
match v
|
let origin_descr = match v
|
||||||
.config_origin_descr_kind
|
.domain_setting_origin_descr_kind
|
||||||
.unwrap_or("".to_string())
|
.unwrap_or("".to_string())
|
||||||
.as_str()
|
.as_str()
|
||||||
{
|
{
|
||||||
"" => Ok(None),
|
"git" => Ok(origin::Descr::Git {
|
||||||
"git" => Ok(Some(domain::Domain {
|
|
||||||
origin_descr: origin::Descr::Git {
|
|
||||||
url: v
|
url: v
|
||||||
.config_origin_descr_git_url
|
.domain_setting_origin_descr_git_url
|
||||||
.ok_or("config_origin_descr_git_url missing")?,
|
.ok_or("missing domain_setting_origin_descr_git_url")?,
|
||||||
branch_name: v
|
branch_name: v
|
||||||
.config_origin_descr_git_branch_name
|
.domain_setting_origin_descr_git_branch_name
|
||||||
.ok_or("config_origin_descr_git_branch_name missing")?,
|
.ok_or("missing domain_setting_origin_descr_git_branch_name")?,
|
||||||
},
|
}),
|
||||||
})),
|
"" => Err("missing domain_setting_origin_descr_kind".to_string()),
|
||||||
_ => Err("invalid config_origin_descr_kind".to_string()),
|
_ => Err("invalid domain_setting_origin_descr_kind".to_string()),
|
||||||
}
|
}?;
|
||||||
|
|
||||||
|
Ok(Self { origin_descr })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<domain::Domain> for FlatConfig {
|
impl TryFrom<domain::Settings> for FlatDomainSettings {
|
||||||
fn from(v: domain::Domain) -> Self {
|
type Error = unexpected::Error;
|
||||||
|
|
||||||
|
fn try_from(v: domain::Settings) -> Result<Self, Self::Error> {
|
||||||
match v.origin_descr {
|
match v.origin_descr {
|
||||||
origin::Descr::Git { url, branch_name } => FlatConfig {
|
origin::Descr::Git { url, branch_name } => Ok(FlatDomainSettings {
|
||||||
config_origin_descr_kind: Some("git".to_string()),
|
domain_setting_origin_descr_kind: Some("git".to_string()),
|
||||||
config_origin_descr_git_url: Some(url),
|
domain_setting_origin_descr_git_url: Some(url),
|
||||||
config_origin_descr_git_branch_name: Some(branch_name),
|
domain_setting_origin_descr_git_branch_name: Some(branch_name),
|
||||||
},
|
..Default::default()
|
||||||
|
}),
|
||||||
|
origin::Descr::Proxy { .. } => Err(unexpected::Error::from(
|
||||||
|
"proxy origins not supported for forms",
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user