From 5099f792609a76550625c2edaab9f143e9d23a68 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 17 Jul 2023 16:54:03 +0200 Subject: [PATCH] Add ability to add request headers to proxied requests --- .dev-config.yml | 9 ++++++++- README.md | 13 ++++++++++++- src/main.rs | 4 +++- src/origin/descr.rs | 21 +++++++++++++++++++-- src/origin/proxy.rs | 46 ++++++++++++++++++++++++--------------------- 5 files changed, 67 insertions(+), 26 deletions(-) diff --git a/.dev-config.yml b/.dev-config.yml index d779cf3..e1b0464 100644 --- a/.dev-config.yml +++ b/.dev-config.yml @@ -5,7 +5,14 @@ domain: builtins: foo: kind: proxy - url: http://ok + url: http://127.0.0.1:9000 + request_http_headers: + - name: x-foo + value: BAR + - name: host + value: hi + - name: user-agent + value: "" bar: kind: git url: a diff --git a/README.md b/README.md index fcfd90a..d7a0c48 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,24 @@ domain: # public: false # An example built-in domain backed by a reverse-proxy to some other - # web-service. Proxies are currently limited in the following ways: + # 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: proxy # url: "http://some.other.service.com" + # + # # Extra headers to add to requests being proxied + # request_http_headers: + # - name: Host + # value: "yet.another.service.com" + # - name: X-HEADER-TO-DELETE + # value: "" + # # public: false service: diff --git a/src/main.rs b/src/main.rs index 04d0c45..45d82de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,9 @@ async fn main() { } for (domain, builtin_domain) in &config.domain.builtins { - if let domani::origin::Descr::Proxy { ref url } = builtin_domain.settings.origin_descr { + if let domani::origin::Descr::Proxy { 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}"); } diff --git a/src/origin/descr.rs b/src/origin/descr.rs index 5cbd81b..294dfa9 100644 --- a/src/origin/descr.rs +++ b/src/origin/descr.rs @@ -2,6 +2,12 @@ use hex::ToHex; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DescrProxyHttpHeader { + pub name: String, + pub value: String, +} + #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(tag = "kind")] /// A unique description of an origin, from where a domain might be served. @@ -10,7 +16,10 @@ pub enum Descr { Git { url: String, branch_name: String }, #[serde(rename = "proxy")] - Proxy { url: String }, + Proxy { + url: String, + request_http_headers: Vec, + }, } impl Descr { @@ -29,9 +38,17 @@ impl Descr { h_update(url); h_update(branch_name); } - Descr::Proxy { url } => { + Descr::Proxy { + url, + request_http_headers, + } => { h_update("proxy"); h_update(url); + for h in request_http_headers { + h_update("header"); + h_update(&h.name); + h_update(&h.value); + } } } diff --git a/src/origin/proxy.rs b/src/origin/proxy.rs index 6923e36..9f6ac8c 100644 --- a/src/origin/proxy.rs +++ b/src/origin/proxy.rs @@ -1,6 +1,6 @@ use crate::error::unexpected::{self, Mappable}; use crate::{domain, origin}; -use http::header::HeaderValue; +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 @@ -30,33 +30,37 @@ pub async fn serve_http_request( mut req: hyper::Request, req_is_https: bool, ) -> unexpected::Result> { - let proxy_url = if let origin::Descr::Proxy { ref url } = settings.origin_descr { - url + let (proxy_url, request_http_headers) = if let origin::Descr::Proxy { + ref url, + ref request_http_headers, + } = settings.origin_descr + { + (url, request_http_headers) } else { panic!("non-proxy domain settings passed in: {settings:?}") }; - let parsed_proxy_url = - http::Uri::from_str(proxy_url).or_unexpected_while("parsing proxy url {proxy_url}")?; + for header in request_http_headers { + let name: HeaderName = header + .name + .as_str() + .try_into() + .map_unexpected_while(|| format!("parsing header name {}", &header.name))?; - // 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(format!( - "getting host from proxy url {proxy_url}, there is no host" - ))?; + if header.value == "" { + req.headers_mut().remove(name); + continue; + } - let host_and_port; - let mut host = authority.host(); + let value = HeaderValue::from_str(&header.value).map_unexpected_while(|| { + format!( + "parsing {} as value for header {}", + &header.value, &header.name + ) + })?; - if let Some(port) = authority.port() { - host_and_port = format!("{host}:{port}"); - host = host_and_port.as_str(); - }; - - HeaderValue::from_str(host).or_unexpected()? - }; - - req.headers_mut().insert("host", host); + req.headers_mut().insert(name, value); + } if req_is_https { req.headers_mut()