Add secret service.http.form_method field for debugging

This commit is contained in:
Brian Picciano 2023-07-16 13:55:06 +02:00
parent 4483185e75
commit 5a4ff4ca65
9 changed files with 139 additions and 80 deletions

View File

@ -3,6 +3,8 @@ 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:
- kind: A - kind: A

View File

@ -41,5 +41,6 @@ pub struct Config {
#[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>, pub builtins: collections::HashMap<domain::Name, BuiltinDomain>,
} }

View File

@ -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 = "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: self::http::Config,
}

35
src/service/config.rs Normal file
View 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,
}

View File

@ -4,6 +4,7 @@ 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};
@ -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)]
domain_config: service::util::FlatConfig,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct DomainSyncArgs { struct DomainSyncArgs {
domain: domain::Name, domain: domain::Name,
passphrase: String, passphrase: String,
#[serde(flatten)]
domain_config: service::util::FlatConfig,
} }
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,6 +152,7 @@ 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,
}, },
) )
@ -174,14 +184,33 @@ impl<'svc> Service {
} }
} }
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())
@ -219,11 +248,7 @@ impl<'svc> Service {
) )
} }
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 Response<'a> {
domain: domain::Name, domain: domain::Name,
@ -235,7 +260,7 @@ impl<'svc> Service {
dns_records_have_cname: bool, dns_records_have_cname: bool,
} }
let config: domain::Domain = match domain_config.try_into() { let config: domain::Domain = match args.domain_config.try_into() {
Ok(Some(config)) => config, Ok(Some(config)) => config,
Ok(None) => return self.render_error_page(400, "domain config is required"), Ok(None) => return self.render_error_page(400, "domain config is required"),
Err(e) => { Err(e) => {
@ -272,16 +297,12 @@ 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 config: domain::Domain = match args.domain_config.try_into() {
Ok(Some(config)) => config, Ok(Some(config)) => config,
Ok(None) => return self.render_error_page(400, "domain config is required"), Ok(None) => return self.render_error_page(400, "domain config is required"),
Err(e) => { Err(e) => {
@ -341,12 +362,14 @@ impl<'svc> Service {
} }
async fn handle_request(&self, req: Request<Body>) -> Response<Body> { async fn handle_request(&self, req: Request<Body>) -> Response<Body> {
let (req, body) = req.into_parts();
let maybe_host = match ( let maybe_host = match (
req.headers() req.headers
.get("Host") .get("Host")
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.map(strip_port), .map(strip_port),
req.uri().host().map(strip_port), req.uri.host().map(strip_port),
) { ) {
(Some(h), _) if h != self.config.primary_domain.as_str() => Some(h), (Some(h), _) if h != self.config.primary_domain.as_str() => Some(h),
(_, Some(h)) if h != self.config.primary_domain.as_str() => Some(h), (_, Some(h)) if h != self.config.primary_domain.as_str() => Some(h),
@ -354,13 +377,12 @@ 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) {
@ -369,7 +391,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
@ -388,38 +410,36 @@ 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(domain, req.uri.path());
} }
// Serve main domani site // Serve main domani site
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
} }

View File

@ -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(),
} }
} }
} }

View File

@ -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" />

View File

@ -3,7 +3,7 @@
<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_config }}
<input name="{{ @key }}" type="hidden" value="{{ this }}" /> <input name="{{ @key }}" type="hidden" value="{{ this }}" />
@ -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>

View File

@ -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>