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:
store_dir_path: /tmp/domani_dev_env/domain
service:
http:
form_method: GET
passphrase: foobar
dns_records:
- kind: A

View File

@ -41,5 +41,6 @@ pub struct Config {
#[serde(default)]
pub dns: ConfigDNS,
pub acme: Option<ConfigACME>,
#[serde(default)]
pub builtins: collections::HashMap<domain::Name, BuiltinDomain>,
}

View File

@ -1,38 +1,5 @@
mod config;
pub mod http;
mod util;
use crate::domain;
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,
}
pub use 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::*;
use http::request::Parts;
use hyper::{Body, Method, Request, Response};
use serde::{Deserialize, Serialize};
@ -48,6 +49,7 @@ pub fn new(
#[derive(Serialize)]
struct BasePresenter<'a, T> {
page_name: &'a str,
form_method: &'a str,
data: T,
}
@ -59,12 +61,18 @@ struct DomainGetArgs {
#[derive(Deserialize)]
struct DomainInitArgs {
domain: domain::Name,
#[serde(flatten)]
domain_config: service::util::FlatConfig,
}
#[derive(Deserialize)]
struct DomainSyncArgs {
domain: domain::Name,
passphrase: String,
#[serde(flatten)]
domain_config: service::util::FlatConfig,
}
impl<'svc> Service {
@ -121,6 +129,7 @@ impl<'svc> Service {
"/base.html",
BasePresenter {
page_name: "/error.html",
form_method: self.config.http.form_method.as_str(),
data: &Response { error_msg: e },
},
)
@ -143,6 +152,7 @@ impl<'svc> Service {
"/base.html",
BasePresenter {
page_name: name,
form_method: self.config.http.form_method.as_str(),
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
In: Deserialize<'a>,
In: for<'d> Deserialize<'d>,
F: FnOnce(In) -> Out,
Out: future::Future<Output = Response<Body>>,
{
let query = req.uri().query().unwrap_or("");
match serde_urlencoded::from_str::<In>(query) {
let res = match self.config.http.form_method {
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,
Err(err) => {
self.render_error_page(400, format!("failed to parse query args: {}", err).as_str())
@ -219,11 +248,7 @@ impl<'svc> Service {
)
}
fn domain_init(
&self,
args: DomainInitArgs,
domain_config: service::util::FlatConfig,
) -> Response<Body> {
fn domain_init(&self, args: DomainInitArgs) -> Response<Body> {
#[derive(Serialize)]
struct Response<'a> {
domain: domain::Name,
@ -235,7 +260,7 @@ impl<'svc> Service {
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(None) => return self.render_error_page(400, "domain config is required"),
Err(e) => {
@ -272,16 +297,12 @@ impl<'svc> Service {
)
}
async fn domain_sync(
&self,
args: DomainSyncArgs,
domain_config: service::util::FlatConfig,
) -> Response<Body> {
async fn domain_sync(&self, args: DomainSyncArgs) -> Response<Body> {
if args.passphrase != self.config.passphrase.as_str() {
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(None) => return self.render_error_page(400, "domain config is required"),
Err(e) => {
@ -341,12 +362,14 @@ impl<'svc> Service {
}
async fn handle_request(&self, req: Request<Body>) -> Response<Body> {
let (req, body) = req.into_parts();
let maybe_host = match (
req.headers()
req.headers
.get("Host")
.and_then(|v| v.to_str().ok())
.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),
@ -354,13 +377,12 @@ impl<'svc> Service {
}
.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
// matter the domain, presumably they are cryptographically random enough that it doesn't
// 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/");
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.
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 {
match self
.domain_manager
@ -388,38 +410,36 @@ impl<'svc> Service {
// If a managed domain was given then serve that from its origin
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
if method == Method::GET && path.starts_with("/static/") {
if req.method == Method::GET && path.starts_with("/static/") {
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") => {
self.render_page("/index.html", ())
}
(&Method::GET, "/domain.html") => {
self.with_query_req(&req, |args: DomainGetArgs| async { self.domain_get(args) })
.await
}
(&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
(form_method, "/domain.html") if form_method == config_form_method => {
self.with_query_req(&req, body, |args: DomainGetArgs| async {
self.domain_get(args)
})
.await
}
(&Method::GET, "/domain_sync.html") => {
self.with_query_req(&req, |args: DomainSyncArgs| async {
self.with_query_req(&req, |config: service::util::FlatConfig| async {
self.domain_sync(args, config).await
})
.await
(form_method, "/domain_init.html") if form_method == config_form_method => {
self.with_query_req(&req, body, |args: DomainInitArgs| async {
self.domain_init(args)
})
.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
}

View File

@ -5,11 +5,44 @@ fn default_http_addr() -> net::SocketAddr {
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)]
pub struct Config {
#[serde(default = "default_http_addr")]
pub http_addr: net::SocketAddr,
pub https_addr: Option<net::SocketAddr>,
#[serde(default)]
pub form_method: ConfigFormMethod,
}
impl Default for Config {
@ -17,6 +50,7 @@ impl Default for Config {
Self {
http_addr: default_http_addr(),
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
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="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
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 }}" />
{{ #each data.flat_config }}
<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 }}
<tr>
<td>{{ this.type }}</td>
<td>{{ this.kind }}</td>
<td>{{ lookup ../data "domain" }}</td>
{{ #if this.name }}
<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
been set up.</p>
<form method="get" action="/domain.html">
<form method="{{ form_method }}" action="/domain.html">
<fieldset>
<label>