Skip to content

Commit 522af03

Browse files
committed
fix per-route services binding to the same port
1 parent b65987f commit 522af03

5 files changed

Lines changed: 189 additions & 79 deletions

File tree

‎src/core/handlers/filesystem.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ pub fn write_file(file_path: SafePath, content: &[u8]) -> Result<()> {
2020
Ok(())
2121
}
2222

23+
pub fn read_file(file_path: &SafePath) -> Result<Vec<u8>> {
24+
let file = fs::read(file_path.as_str())
25+
.map_err(|e| Error::FileSystem(format!("Failed to read {}: {}", file_path, e)))?;
26+
27+
Ok(file)
28+
}
29+
2330
pub fn check_file_exists(file_path: &SafePath) -> bool {
2431
// foxguard: ignore[rs/no-path-traversal]
2532
// validated with SafePath

‎src/core/handlers/proxy.rs‎

Lines changed: 159 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
use crate::{
22
Error, Result,
3-
core::models::proxy::{ProxyConfig, ProxyRoute},
3+
core::{
4+
handlers::filesystem::read_file,
5+
models::{
6+
certs::{TlsCerts, TlsMaterial},
7+
proxy::{ProxyConfig, ProxyRoute, ProxyRouteMap},
8+
routes::Host,
9+
},
10+
},
11+
error,
412
};
513
use async_trait::async_trait;
614
use http::{Response, header};
715
use pingora::{
16+
ErrorType,
817
apps::http_app::ServeHttp,
9-
http::StatusCode,
10-
prelude::{HttpPeer, Result as PingoraResult},
18+
http::{ResponseHeader, StatusCode},
19+
listeners::{TlsAccept, tls::TlsSettings},
20+
prelude::{Error as PingoraError, HttpPeer, Result as PingoraResult},
1121
protocols::http::ServerSession,
1222
proxy::{ProxyHttp, Session, http_proxy_service},
1323
server::{Server, configuration::ServerConf},
1424
services::{Service, listening::Service as ListeningService},
25+
tls::{self, ssl},
1526
};
16-
use std::sync::Arc;
17-
18-
static BODY: &[u8] = "<html><body>301 Moved Permanently</body></html>".as_bytes();
27+
use std::{collections::HashMap, sync::Arc};
1928

2029
pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec<ProxyRoute>) -> Result<()> {
2130
let mut server = Server::new(None).map_err(|e| Error::Proxy(e.to_string()))?;
@@ -26,89 +35,92 @@ pub fn run_proxy(proxy_config: ProxyConfig, routes: Vec<ProxyRoute>) -> Result<(
2635
let http_addr = format!("{}:{}", proxy_config.input_address, *proxy_config.http_port);
2736
let https_addr = format!("{}:{}", proxy_config.input_address, *proxy_config.https_port);
2837

29-
// tls proxies
30-
let tls_services =
31-
tls_routes_services(server_conf.clone(), https_addr.clone(), routes.clone())?;
38+
let mut routes_map = HashMap::new();
39+
let mut tls_certs = HashMap::new();
40+
41+
for route in routes {
42+
if *route.tls {
43+
let cert_bytes = read_file(&route.cert_path)?;
44+
let cert = tls::x509::X509::from_pem(&cert_bytes)
45+
.map_err(|e| Error::Certificate(format!("Failed to parse certificate: {}", e)))?;
46+
47+
let key_bytes = read_file(&route.key_path)?;
48+
let key = tls::pkey::PKey::private_key_from_pem(&key_bytes)
49+
.map_err(|e| Error::Certificate(format!("Failed to parse private key: {}", e)))?;
50+
51+
tls_certs.insert(
52+
route.host.clone(),
53+
TlsMaterial {
54+
cert,
55+
key,
56+
},
57+
);
58+
}
3259

33-
for service in tls_services {
34-
server.add_service(service);
60+
routes_map.insert(route.host.clone(), route);
3561
}
3662

37-
// redirect to tls proxies
38-
let redirect_service = http_redirect_service(http_addr.clone());
63+
let routes_map = Arc::new(routes_map);
64+
let tls_certs = Arc::new(tls_certs);
3965

40-
server.add_service(redirect_service);
41-
// plain proxies
42-
let plain_services = plain_routes_services(server_conf, http_addr.clone(), routes.clone())?;
66+
// plain proxies with redirect
67+
let plain_service =
68+
plain_routes_service(server_conf.clone(), http_addr.clone(), routes_map.clone())?;
69+
server.add_service(plain_service);
4370

44-
for service in plain_services {
45-
server.add_service(service);
46-
}
71+
// tls proxies
72+
let tls_service =
73+
tls_routes_service(server_conf, https_addr.clone(), routes_map.clone(), tls_certs)?;
74+
server.add_service(tls_service);
4775

4876
server.run_forever();
4977

5078
Err(Error::Proxy("proxy stopped".to_string()))
5179
}
5280

53-
pub fn tls_routes_services(
81+
pub fn tls_routes_service(
5482
server_conf: Arc<ServerConf>,
5583
listen_addr: String,
56-
routes: Vec<ProxyRoute>,
57-
) -> Result<Vec<impl Service>> {
58-
let mut services = Vec::new();
59-
60-
let tls_routes = routes.into_iter().filter(|route| *route.tls == true).collect::<Vec<_>>();
84+
routes_map: ProxyRouteMap,
85+
tls_certs: TlsCerts,
86+
) -> Result<impl Service> {
87+
let proxy_app = ProxyToUpstream::new(routes_map.clone(), false);
6188

62-
for route in tls_routes {
63-
let proxy_app = ProxyToUpstream::new(route.clone());
64-
65-
let mut service = http_proxy_service(&server_conf, proxy_app);
66-
service
67-
.add_tls(&listen_addr, &route.cert_path, &route.key_path)
68-
.map_err(|e| Error::Proxy(e.to_string()));
89+
let mut service = http_proxy_service(&server_conf, proxy_app);
90+
service.add_tcp(&listen_addr);
6991

70-
services.push(service);
71-
}
92+
let sni_resolver = SniResolver::new(tls_certs);
93+
let callback = Box::new(sni_resolver);
94+
let tls_settings =
95+
TlsSettings::with_callbacks(callback).map_err(|e| Error::Proxy(e.to_string()))?;
96+
service.add_tls_with_settings(&listen_addr, None, tls_settings);
7297

73-
Ok(services)
98+
Ok(service)
7499
}
75100

76-
pub fn plain_routes_services(
101+
pub fn plain_routes_service(
77102
server_conf: Arc<ServerConf>,
78103
listen_addr: String,
79-
routes: Vec<ProxyRoute>,
80-
) -> Result<Vec<impl Service>> {
81-
let mut services = Vec::new();
82-
83-
let plain_routes = routes.into_iter().filter(|route| *route.tls == false).collect::<Vec<_>>();
84-
85-
for route in plain_routes {
86-
let proxy_app = ProxyToUpstream::new(route.clone());
87-
88-
let mut service = http_proxy_service(&server_conf, proxy_app);
89-
service.add_tcp(&listen_addr);
90-
91-
services.push(service);
92-
}
93-
94-
Ok(services)
95-
}
104+
routes_map: ProxyRouteMap,
105+
) -> Result<impl Service> {
106+
let proxy_app = ProxyToUpstream::new(routes_map, true);
96107

97-
fn http_redirect_service(listen_addr: String) -> impl Service {
98-
let mut service = ListeningService::new("HTTPS Redirect".to_string(), RedirectToHttps {});
108+
let mut service = http_proxy_service(&server_conf, proxy_app);
99109
service.add_tcp(&listen_addr);
100110

101-
service
111+
Ok(service)
102112
}
103113

104114
pub struct ProxyToUpstream {
105-
route: ProxyRoute,
115+
routes_map: ProxyRouteMap,
116+
upgrade_to_https: bool,
106117
}
107118

108119
impl ProxyToUpstream {
109-
pub fn new(route: ProxyRoute) -> Self {
120+
pub fn new(routes_map: ProxyRouteMap, upgrade_to_https: bool) -> Self {
110121
Self {
111-
route,
122+
routes_map,
123+
upgrade_to_https,
112124
}
113125
}
114126
}
@@ -118,36 +130,106 @@ impl ProxyHttp for ProxyToUpstream {
118130
type CTX = ();
119131
fn new_ctx(&self) {}
120132

133+
// ProxyToUpstream is shared with both HTTP and HTTPS
134+
// here we do a quick check to upgrade to HTTPS if its a TLS route
135+
async fn request_filter(&self, session: &mut Session, _ctx: &mut ()) -> PingoraResult<bool> {
136+
if self.upgrade_to_https {
137+
let host = host_from_session(session)
138+
// 400 bad request no host
139+
.ok_or_else(|| PingoraError::new(ErrorType::HTTPStatus(400)))?;
140+
141+
match self.routes_map.get(host) {
142+
// 421 Misdirected Request doesnt match any hosts
143+
None => {
144+
error!("no route for host: {}", host);
145+
let header = ResponseHeader::build(421, None)?;
146+
session.write_response_header(Box::new(header), true).await?;
147+
return Ok(true);
148+
},
149+
// 301 Moved Permanently redirect to https
150+
Some(route) if *route.tls => {
151+
let mut header = ResponseHeader::build(301, None)?;
152+
header.insert_header(header::LOCATION, format!("https://{}", route.host))?;
153+
header.insert_header(header::CONTENT_LENGTH, 0)?;
154+
155+
session.write_response_header(Box::new(header), true).await?;
156+
157+
return Ok(true);
158+
},
159+
// All good let it pass.
160+
Some(_) => return Ok(false),
161+
}
162+
}
163+
164+
Ok(false)
165+
}
166+
121167
async fn upstream_peer(
122168
&self,
123169
session: &mut Session,
124170
_ctx: &mut (),
125171
) -> PingoraResult<Box<HttpPeer>> {
126-
// foxguard: ignore[rs/no-unwrap-in-lib]
127-
let host_header = session.get_header(header::HOST).unwrap().to_str().unwrap();
172+
let host = host_from_session(session)
173+
// 400 bad request no host
174+
.ok_or_else(|| PingoraError::new(ErrorType::HTTPStatus(400)))?;
128175

129-
let proxy_to =
130-
HttpPeer::new(self.route.upstream.as_str(), false, self.route.host.to_string());
176+
let route = self
177+
.routes_map
178+
.get(host)
179+
// 421 Misdirected Request doesnt match any hosts
180+
.ok_or_else(|| PingoraError::new(ErrorType::HTTPStatus(421)))?;
181+
182+
let proxy_to = HttpPeer::new(route.upstream.as_str(), false, route.host.to_string());
131183
let peer = Box::new(proxy_to);
132184
Ok(peer)
133185
}
134186
}
135187

136-
pub struct RedirectToHttps;
188+
// note to self we are not stripping port from the host header
189+
// so Host: example.com:443 will be rejected with a 421
190+
// this is a strict design decision not a bug
191+
fn host_from_session(session: &Session) -> Option<&str> {
192+
session.get_header(header::HOST).and_then(|h| h.to_str().ok())
193+
}
194+
195+
struct SniResolver {
196+
certs: TlsCerts,
197+
}
198+
199+
impl SniResolver {
200+
fn new(tls_certs: TlsCerts) -> Self {
201+
Self {
202+
certs: tls_certs,
203+
}
204+
}
205+
}
137206

138207
#[async_trait]
139-
impl ServeHttp for RedirectToHttps {
140-
async fn response(&self, http_stream: &mut ServerSession) -> Response<Vec<u8>> {
141-
// foxguard: ignore[rs/no-unwrap-in-lib]
142-
let host_header = http_stream.get_header(header::HOST).unwrap().to_str().unwrap();
143-
144-
Response::builder()
145-
.status(StatusCode::MOVED_PERMANENTLY)
146-
.header(header::CONTENT_TYPE, "text/html")
147-
.header(header::CONTENT_LENGTH, BODY.len())
148-
.header(header::LOCATION, format!("https://{host_header}"))
149-
.body(BODY.to_owned())
150-
// foxguard: ignore[rs/no-unwrap-in-lib]
151-
.unwrap()
208+
impl TlsAccept for SniResolver {
209+
async fn certificate_callback(&self, ssl: &mut ssl::SslRef) -> () {
210+
let sni_provided = ssl.servername(ssl::NameType::HOST_NAME).map(str::to_owned);
211+
212+
let Some(sni_provided) = sni_provided else {
213+
error!("No SNI provided");
214+
return;
215+
};
216+
217+
let Some(TlsMaterial {
218+
cert,
219+
key,
220+
}) = self.certs.get(sni_provided.as_str())
221+
else {
222+
error!("No certificate found for SNI: {}", sni_provided);
223+
return;
224+
};
225+
226+
if let Err(e) = tls::ext::ssl_use_certificate(ssl, cert) {
227+
error!("Failed to use certificate for SNI {}: {}", sni_provided, e);
228+
return;
229+
}
230+
if let Err(e) = tls::ext::ssl_use_private_key(ssl, key) {
231+
error!("Failed to use private key for SNI {}: {}", sni_provided, e);
232+
return;
233+
}
152234
}
153235
}

‎src/core/models/certs.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
use crate::core::models::routes::Host;
2+
use pingora::tls::{
3+
pkey::{PKey, Private},
4+
x509::X509,
5+
};
26
use serde::Deserialize;
7+
use std::collections::HashMap;
38
use std::fmt;
49
use std::ops::Deref;
10+
use std::sync::Arc;
511

612
#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
713
#[serde(rename_all = "snake_case")]
@@ -20,6 +26,13 @@ pub struct Certificate {
2026
pub cert_type: CertificateType,
2127
}
2228

29+
pub struct TlsMaterial {
30+
pub cert: X509,
31+
pub key: PKey<Private>,
32+
}
33+
34+
pub type TlsCerts = Arc<HashMap<Host, TlsMaterial>>;
35+
2336
// --- EMAIL ---
2437
#[derive(Debug, Clone, Deserialize)]
2538
pub struct Email(String);

‎src/core/models/proxy.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use crate::core::models::{
44
routes::{Host, Upstream},
55
};
66
use http::{Response, StatusCode, header};
7-
use std::{fmt, ops::Deref};
7+
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
8+
9+
pub type ProxyRouteMap = Arc<HashMap<Host, ProxyRoute>>;
810

911
#[derive(Debug, Clone)]
1012
pub struct ProxyRoute {

‎src/core/models/routes.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub struct Route {
1111
}
1212

1313
// --- HOST ---
14-
#[derive(Debug, Clone, Deserialize)]
14+
#[derive(Debug, Clone, Deserialize, Hash, Eq, PartialEq)]
1515
pub struct Host(String);
1616

1717
impl Deref for Host {
@@ -22,6 +22,12 @@ impl Deref for Host {
2222
}
2323
}
2424

25+
impl std::borrow::Borrow<str> for Host {
26+
fn borrow(&self) -> &str {
27+
&self.0
28+
}
29+
}
30+
2531
impl fmt::Display for Host {
2632
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2733
write!(f, "{}", self.0)

0 commit comments

Comments
 (0)