Moved http proxy under the service module

This commit is contained in:
Brian Picciano 2023-07-19 22:36:29 +02:00
parent f7ecafbc17
commit 31782be10d
12 changed files with 205 additions and 191 deletions

View File

@ -3,16 +3,6 @@ origin:
domain:
store_dir_path: /tmp/domani_dev_env/domain
builtins:
foo:
kind: http_proxy
url: http://127.0.0.1:9000
request_headers:
- name: x-foo
value: BAR
- name: host
value: hi
- name: user-agent
value: ""
bar:
kind: git
url: a
@ -21,6 +11,16 @@ domain:
service:
http:
form_method: GET
proxied_domains:
foo:
url: http://127.0.0.1:9000
request_headers:
- name: x-foo
value: BAR
- name: host
value: hi
- name: user-agent
value: ""
passphrase: foobar
dns_records:
- kind: A

View File

@ -68,29 +68,6 @@ domain:
# # domain list, but will not be configurable in the web interface
# public: false
# 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:
# kind: http_proxy
# 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: ""
#
# # If true then the built-in will be included in the web interface's
# # domain list, but will not be configurable in the web interface
# public: false
service:
# Passphrase which must be given by users who are configuring new domains via
@ -123,10 +100,32 @@ service:
# The address to listen for HTTP requests on. This must use port 80 if
# https_addr is set.
#http_addr: "[::]:3030"
#http_addr: "[::]:3080"
# The address to listen for HTTPS requests on. This is optional.
#https_addr: "[::]:443"
#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:
# kind: http_proxy
# 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: ""
```
The YAML config file can be passed to the Domani process via the `--config-path`

View File

@ -248,10 +248,6 @@ impl Manager for ManagerImpl {
) -> Result<util::BoxByteStream, GetFileError> {
let settings = self.domain_store.get(domain)?.settings;
if let origin::Descr::HttpProxy { .. } = settings.origin_descr {
return Err(unexpected::Error::from("origin is proxy, can't serve file").into());
}
let path = settings.process_path(path);
let f = self

View File

@ -78,16 +78,6 @@ async fn main() {
config.service.dns_records.push(primary_cname);
}
for (domain, builtin_domain) in &config.domain.builtins {
if let domani::origin::Descr::HttpProxy { ref url, .. } =
builtin_domain.settings.origin_descr
{
if let Err(e) = domani::origin::proxy::validate_proxy_url(url) {
panic!("invalid config for builtin domain {domain}: {e}");
}
}
}
config
};

View File

@ -2,7 +2,6 @@ mod config;
mod descr;
pub mod git;
pub mod mux;
pub mod proxy;
pub use config::*;
pub use descr::Descr;

View File

@ -14,12 +14,6 @@ pub struct DescrHttpProxyHeader {
pub enum Descr {
#[serde(rename = "git")]
Git { url: String, branch_name: String },
#[serde(rename = "http_proxy")]
HttpProxy {
url: String,
request_headers: Vec<DescrHttpProxyHeader>,
},
}
impl Descr {
@ -38,18 +32,6 @@ impl Descr {
h_update(url);
h_update(branch_name);
}
Descr::HttpProxy {
url,
request_headers,
} => {
h_update("proxy");
h_update(url);
for h in request_headers {
h_update("header");
h_update(&h.name);
h_update(&h.value);
}
}
}
h.finalize().encode_hex::<String>()

View File

@ -57,15 +57,11 @@ impl FSStore {
}
fn deconstruct_descr(descr: &origin::Descr) -> (&str, &str) {
if let origin::Descr::Git {
let origin::Descr::Git {
ref url,
ref branch_name,
} = descr
{
(url, branch_name)
} else {
panic!("non git descr passed in")
}
} = descr;
(url, branch_name)
}
fn create_repo_snapshot(

View File

@ -1,78 +0,0 @@
use crate::error::unexpected::{self, Mappable};
use crate::{domain, origin};
use http::header::{HeaderName, HeaderValue};
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 fn validate_proxy_url(proxy_url: &str) -> unexpected::Result<()> {
let parsed_proxy_url =
http::Uri::from_str(proxy_url).or_unexpected_while("parsing proxy url {proxy_url}")?;
let scheme = parsed_proxy_url.scheme().map_unexpected_while(|| {
format!("expected a scheme of http in the proxy url {proxy_url}")
})?;
if scheme != "http" {
return Err(unexpected::Error::from(
format!("scheme of proxy url {proxy_url} should be 'http'",).as_str(),
));
}
Ok(())
}
pub async fn serve_http_request(
settings: &domain::Settings,
client_ip: net::IpAddr,
mut req: hyper::Request<hyper::Body>,
req_is_https: bool,
) -> unexpected::Result<hyper::Response<hyper::Body>> {
let (url, request_headers) = if let origin::Descr::HttpProxy {
ref url,
ref request_headers,
} = settings.origin_descr
{
(url, request_headers)
} else {
panic!("non-proxy domain settings passed in: {settings:?}")
};
for header in request_headers {
let name: HeaderName = header
.name
.as_str()
.try_into()
.map_unexpected_while(|| format!("parsing header name {}", &header.name))?;
if header.value == "" {
req.headers_mut().remove(name);
continue;
}
let value = HeaderValue::from_str(&header.value).map_unexpected_while(|| {
format!(
"parsing {} as value for header {}",
&header.value, &header.name
)
})?;
req.headers_mut().insert(name, value);
}
if req_is_https {
req.headers_mut()
.insert("x-forwarded-proto", HeaderValue::from_static("https"));
}
match hyper_reverse_proxy::call(client_ip, 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 to {url}: {e:?}").as_str(),
)),
}
}

View File

@ -1,4 +1,5 @@
mod config;
mod proxy;
mod tasks;
mod tpl;
@ -12,7 +13,7 @@ use std::str::FromStr;
use std::{future, net, sync};
use crate::error::unexpected;
use crate::{domain, origin, service, util};
use crate::{domain, service, util};
pub struct Service {
domain_manager: sync::Arc<dyn domain::manager::Manager>,
@ -158,34 +159,7 @@ impl<'svc> Service {
)
}
async fn serve_origin(
&self,
client_ip: net::IpAddr,
domain: domain::Name,
req: Request<Body>,
req_is_https: bool,
) -> Response<Body> {
let settings = match self.domain_manager.get_settings(&domain) {
Ok(domain::manager::GetSettingsResult { settings, .. }) => settings,
Err(domain::manager::GetSettingsError::NotFound) => {
return self.render_error_page(404, "Domain not found");
}
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
return self.internal_error(
format!("failed to fetch settings for domain {domain}: {e}").as_str(),
);
}
};
// if the domain is backed by a proxy then that is handled specially.
if let origin::Descr::HttpProxy { .. } = settings.origin_descr {
return origin::proxy::serve_http_request(&settings, client_ip, req, req_is_https)
.await
.unwrap_or_else(|e| {
self.internal_error(format!("proxying {domain}: {e}").as_str())
});
};
async fn serve_origin(&self, domain: domain::Name, req: Request<Body>) -> Response<Body> {
let mut path_owned;
let path = req.uri().path();
@ -463,11 +437,28 @@ impl<'svc> Service {
}
}
// If a managed domain was given then serve that from its origin, which is possibly a proxy
// If a managed domain was given then serve that from its origin or a proxy.
if let Some(domain) = maybe_host {
return self
.serve_origin(client_ip, domain, req, req_is_https)
.await;
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(),
)
});
}
return self.serve_origin(domain, req).await;
}
// Serve main domani site

View File

@ -1,5 +1,10 @@
use crate::domain;
use crate::error::unexpected::{self, Mappable};
use serde::{Deserialize, Serialize};
use std::{net, str::FromStr};
use serde_with::{serde_as, TryFromInto};
use std::{collections, net, str::FromStr};
fn default_http_addr() -> net::SocketAddr {
net::SocketAddr::from_str("[::]:3030").unwrap()
@ -35,6 +40,105 @@ 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())
.or_unexpected_while("parsing proxy url {proxy_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)]
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)]
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)]
pub struct Config {
#[serde(default = "default_http_addr")]
@ -43,6 +147,9 @@ pub struct Config {
#[serde(default)]
pub form_method: ConfigFormMethod,
#[serde(default)]
pub proxied_domains: collections::HashMap<domain::Name, ConfigProxiedDomain>,
}
impl Default for Config {
@ -51,6 +158,7 @@ impl Default for Config {
http_addr: default_http_addr(),
https_addr: None,
form_method: Default::default(),
proxied_domains: Default::default(),
}
}
}

36
src/service/http/proxy.rs Normal file
View File

@ -0,0 +1,36 @@
use crate::error::unexpected::{self};
use crate::service;
use http::header::HeaderValue;
use std::net;
pub async fn serve_http_request(
config: &service::http::ConfigProxiedDomain,
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() {
if value == "" {
req.headers_mut().remove(name);
continue;
}
req.headers_mut().insert(name, value.clone());
}
if req_is_https {
req.headers_mut()
.insert("x-forwarded-proto", HeaderValue::from_static("https"));
}
let url = config.url.as_ref();
match hyper_reverse_proxy::call(client_ip, 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 to {url}: {e:?}").as_str(),
)),
}
}

View File

@ -56,11 +56,6 @@ impl TryFrom<domain::Settings> for FlatDomainSettings {
res.domain_setting_origin_descr_git_url = Some(url);
res.domain_setting_origin_descr_git_branch_name = Some(branch_name);
}
origin::Descr::HttpProxy { .. } => {
return Err(unexpected::Error::from(
"proxy origins not supported for forms",
));
}
}
res.domain_setting_add_path_prefix = v.add_path_prefix;