Skip to content

Commit 6c92d75

Browse files
authored
Allow for HTTP2.0 configuration (#62)
I had previously missed that without additional configuration, all downstream and upstream connections default to HTTP1.0 only. This PR fixes that, introducing new configuration with default-on support for H2 (where applicable)
1 parent af9333b commit 6c92d75

File tree

8 files changed

+178
-37
lines changed

8 files changed

+178
-37
lines changed

source/river/assets/test-config.kdl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ services {
2828
// as we'd like, at least one is required
2929
listeners {
3030
"0.0.0.0:8080"
31-
"0.0.0.0:4443" cert-path="./assets/test.crt" key-path="./assets/test.key"
31+
"0.0.0.0:4443" cert-path="./assets/test.crt" key-path="./assets/test.key" offer-h2=true
3232
}
3333

3434
// Connectors are the "upstream" interfaces that we connect with. We can name as many
@@ -44,7 +44,7 @@ services {
4444
discovery "Static"
4545
health-check "None"
4646
}
47-
"91.107.223.4:443" tls-sni="onevariable.com"
47+
"91.107.223.4:443" tls-sni="onevariable.com" proto="h2-or-h1"
4848
}
4949

5050
// Path control are optional modifiers for requests and responses
@@ -79,7 +79,7 @@ services {
7979
// at least one.
8080
listeners {
8181
"0.0.0.0:9000"
82-
"0.0.0.0:9443" cert-path="./assets/test.crt" key-path="./assets/test.key"
82+
"0.0.0.0:9443" cert-path="./assets/test.crt" key-path="./assets/test.key" offer-h2=true
8383
}
8484
// File servers have additional configuration items
8585
file-server {

source/river/src/config/internal.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ pub enum ListenerKind {
158158
Tcp {
159159
addr: String,
160160
tls: Option<TlsConfig>,
161+
offer_h2: bool,
161162
},
162163
Uds(PathBuf),
163164
}

source/river/src/config/kdl/mod.rs

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66

77
use kdl::{KdlDocument, KdlEntry, KdlNode};
88
use miette::{bail, Diagnostic, SourceSpan};
9-
use pingora::upstreams::peer::HttpPeer;
9+
use pingora::{protocols::ALPN, upstreams::peer::HttpPeer};
1010

1111
use crate::{
1212
config::internal::{
@@ -392,21 +392,48 @@ fn extract_connector(
392392
return Err(Bad::docspan("Not a valid socket address", doc, node.span()).into());
393393
};
394394

395-
let args = utils::str_str_args(doc, args)?;
396-
let (tls, sni) = match args.as_slice() {
397-
[] => (false, String::new()),
398-
[("tls-sni", sni)] => (true, sni.to_string()),
399-
_ => {
395+
// TODO: consistent enforcement of only-known args?
396+
let args = utils::str_str_args(doc, args)?
397+
.into_iter()
398+
.collect::<HashMap<&str, &str>>();
399+
400+
let proto = match args.get("proto").copied() {
401+
None => None,
402+
Some("h1-only") => Some(ALPN::H1),
403+
Some("h2-only") => Some(ALPN::H2),
404+
Some("h1-or-h2") => {
405+
tracing::warn!("accepting 'h1-or-h2' as meaning 'h2-or-h1'");
406+
Some(ALPN::H2H1)
407+
}
408+
Some("h2-or-h1") => Some(ALPN::H2H1),
409+
Some(other) => {
400410
return Err(Bad::docspan(
401-
"This should have zero args or just 'tls-sni'",
411+
format!(
412+
"'proto' should be one of 'h1-only', 'h2-only', or 'h2-or-h1', found '{other}'"
413+
),
402414
doc,
403415
node.span(),
404416
)
405417
.into());
406418
}
407419
};
420+
let tls_sni = args.get("tls-sni");
421+
422+
let (tls, sni, alpn) = match (proto, tls_sni) {
423+
(None, None) | (Some(ALPN::H1), None) => (false, String::new(), ALPN::H1),
424+
(None, Some(sni)) => (true, sni.to_string(), ALPN::H2H1),
425+
(Some(_), None) => {
426+
return Err(
427+
Bad::docspan("'tls-sni' is required for HTTP2 support", doc, node.span()).into(),
428+
);
429+
}
430+
(Some(p), Some(sni)) => (true, sni.to_string(), p),
431+
};
408432

409-
Ok(HttpPeer::new(sadd, tls, sni))
433+
let mut peer = HttpPeer::new(sadd, tls, sni);
434+
peer.options.alpn = alpn;
435+
436+
Ok(peer)
410437
}
411438

412439
// services { Service { listeners { ... } } }
@@ -416,39 +443,57 @@ fn extract_listener(
416443
name: &str,
417444
args: &[KdlEntry],
418445
) -> miette::Result<ListenerConfig> {
419-
let mut args = utils::str_str_args(doc, args)?;
420-
args.sort_by_key(|a| a.0);
446+
let args = utils::str_value_args(doc, args)?
447+
.into_iter()
448+
.collect::<HashMap<&str, &KdlEntry>>();
421449

422450
// Is this a bindable name?
423451
if name.parse::<SocketAddr>().is_ok() {
424452
// Cool: do we have reasonable args for this?
425-
match args.as_slice() {
426-
// No argument - it's a regular TCP listener
427-
[] => Ok(ListenerConfig {
453+
let cert_path = utils::map_ensure_str(doc, args.get("cert-path").copied())?;
454+
let key_path = utils::map_ensure_str(doc, args.get("key-path").copied())?;
455+
let offer_h2 = utils::map_ensure_bool(doc, args.get("offer-h2").copied())?;
456+
457+
match (cert_path, key_path, offer_h2) {
458+
// No config? No problem!
459+
(None, None, None) => Ok(ListenerConfig {
428460
source: ListenerKind::Tcp {
429461
addr: name.to_string(),
430462
tls: None,
463+
offer_h2: false,
431464
},
432465
}),
433-
// exactly these two args: it's a TLS listener
434-
[("cert-path", cpath), ("key-path", kpath)] => Ok(ListenerConfig {
466+
// We must have both of cert-path and key-path if both are present
467+
// ignore "offer-h2" if this is incorrect
468+
(None, Some(_), _) | (Some(_), None, _) => {
469+
return Err(Bad::docspan(
470+
"'cert-path' and 'key-path' must either BOTH be present, or NEITHER should be present",
471+
doc,
472+
node.span(),
473+
)
474+
.into());
475+
}
476+
// We can't offer H2 if we don't have TLS (at least for now, unless we
477+
// expose H2C settings in pingora)
478+
(None, None, Some(_)) => {
479+
return Err(Bad::docspan(
480+
"'offer-h2' requires TLS, specify 'cert-path' and 'key-path'",
481+
doc,
482+
node.span(),
483+
)
484+
.into());
485+
}
486+
(Some(cpath), Some(kpath), offer_h2) => Ok(ListenerConfig {
435487
source: ListenerKind::Tcp {
436488
addr: name.to_string(),
437489
tls: Some(TlsConfig {
438490
cert_path: cpath.into(),
439491
key_path: kpath.into(),
440492
}),
493+
// Default to enabling H2 if unspecified
494+
offer_h2: offer_h2.unwrap_or(true),
441495
},
442496
}),
443-
// Otherwise, I dunno what this is
444-
_ => {
445-
return Err(Bad::docspan(
446-
"listeners must have no args or both cert-path and key-path",
447-
doc,
448-
node.span(),
449-
)
450-
.into())
451-
}
452497
}
453498
} else if let Ok(pb) = name.parse::<PathBuf>() {
454499
// TODO: Should we check that this path exists? Otherwise it seems to always match

source/river/src/config/kdl/test.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ fn load_test() {
3131
source: crate::config::internal::ListenerKind::Tcp {
3232
addr: "0.0.0.0:8080".into(),
3333
tls: None,
34+
offer_h2: false,
3435
},
3536
},
3637
ListenerConfig {
@@ -40,6 +41,7 @@ fn load_test() {
4041
cert_path: "./assets/test.crt".into(),
4142
key_path: "./assets/test.key".into(),
4243
}),
44+
offer_h2: true,
4345
},
4446
},
4547
],
@@ -85,6 +87,7 @@ fn load_test() {
8587
source: crate::config::internal::ListenerKind::Tcp {
8688
addr: "0.0.0.0:8000".into(),
8789
tls: None,
90+
offer_h2: false,
8891
},
8992
}],
9093
upstreams: vec![HttpPeer::new("91.107.223.4:80", false, String::new())],
@@ -102,6 +105,7 @@ fn load_test() {
102105
source: crate::config::internal::ListenerKind::Tcp {
103106
addr: "0.0.0.0:9000".into(),
104107
tls: None,
108+
offer_h2: false,
105109
},
106110
},
107111
ListenerConfig {
@@ -111,6 +115,7 @@ fn load_test() {
111115
cert_path: "./assets/test.crt".into(),
112116
key_path: "./assets/test.key".into(),
113117
}),
118+
offer_h2: true,
114119
},
115120
},
116121
],
@@ -218,7 +223,8 @@ fn one_service() {
218223
val.basic_proxies[0].listeners[0].source,
219224
ListenerKind::Tcp {
220225
addr: "127.0.0.1:80".into(),
221-
tls: None
226+
tls: None,
227+
offer_h2: false,
222228
}
223229
);
224230
assert_eq!(

source/river/src/config/kdl/utils.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Various ad-hoc KDL document parsers used
22
3-
use super::OptExtParse;
3+
use super::{Bad, OptExtParse};
44
use kdl::{KdlDocument, KdlEntry, KdlNode};
55
use std::collections::HashMap;
66

@@ -94,6 +94,58 @@ pub(crate) fn str_str_args<'a>(
9494
Ok(out)
9595
}
9696

97+
/// Useful for collecting all arguments as str:Value key pairs
98+
///
99+
/// KdlEntry is returned instead of KdlValue to allow for retaining the
100+
/// span for error messages
101+
pub(crate) fn str_value_args<'a>(
102+
doc: &KdlDocument,
103+
args: &'a [KdlEntry],
104+
) -> miette::Result<Vec<(&'a str, &'a KdlEntry)>> {
105+
let mut out = vec![];
106+
for arg in args {
107+
let name =
108+
arg.name()
109+
.map(|a| a.value())
110+
.or_bail("arguments should be named", doc, arg.span())?;
111+
112+
out.push((name, arg));
113+
}
114+
Ok(out)
115+
}
116+
117+
/// If the argument exists, ensure it is a str
118+
///
119+
/// Useful with [`str_value_args()`].
120+
pub(crate) fn map_ensure_str<'a>(
121+
doc: &'_ KdlDocument,
122+
val: Option<&'a KdlEntry>,
123+
) -> miette::Result<Option<&'a str>> {
124+
let Some(v) = val else {
125+
return Ok(None);
126+
};
127+
match v.value().as_string() {
128+
Some(vas) => Ok(Some(vas)),
129+
None => Err(Bad::docspan("Expected string argument", doc, v.span()).into()),
130+
}
131+
}
132+
133+
/// If the argument exists, ensure it is a bool
134+
///
135+
/// Useful with [`str_value_args()`].
136+
pub(crate) fn map_ensure_bool(
137+
doc: &KdlDocument,
138+
val: Option<&KdlEntry>,
139+
) -> miette::Result<Option<bool>> {
140+
let Some(v) = val else {
141+
return Ok(None);
142+
};
143+
match v.value().as_bool() {
144+
Some(vas) => Ok(Some(vas)),
145+
None => Err(Bad::docspan("Expected bool argument", doc, v.span()).into()),
146+
}
147+
}
148+
97149
/// Extract a single un-named string argument, like `discovery "Static"`
98150
pub(crate) fn extract_one_str_arg<T, F: FnOnce(&str) -> Option<T>>(
99151
doc: &KdlDocument,

source/river/src/config/toml.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ impl From<ListenerKind> for super::internal::ListenerKind {
189189
ListenerKind::Tcp { addr, tls } => super::internal::ListenerKind::Tcp {
190190
addr,
191191
tls: tls.map(Into::into),
192+
offer_h2: false,
192193
},
193194
ListenerKind::Uds(a) => super::internal::ListenerKind::Uds(a),
194195
}
@@ -318,6 +319,7 @@ pub mod test {
318319
source: internal::ListenerKind::Tcp {
319320
addr: "0.0.0.0:8080".into(),
320321
tls: None,
322+
offer_h2: false,
321323
},
322324
},
323325
internal::ListenerConfig {
@@ -327,6 +329,7 @@ pub mod test {
327329
cert_path: "./assets/test.crt".into(),
328330
key_path: "./assets/test.key".into(),
329331
}),
332+
offer_h2: false,
330333
},
331334
},
332335
],
@@ -367,6 +370,7 @@ pub mod test {
367370
source: internal::ListenerKind::Tcp {
368371
addr: "0.0.0.0:8000".into(),
369372
tls: None,
373+
offer_h2: false,
370374
},
371375
}],
372376
upstreams: vec![HttpPeer::new("91.107.223.4:80", false, String::new())],

source/river/src/main.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod proxy;
55
use crate::{files::river_file_server, proxy::river_proxy_service};
66
use config::internal::{ListenerConfig, ListenerKind};
77
use pingora::{server::Server, services::Service};
8+
use pingora_core::listeners::TlsSettings;
89

910
fn main() {
1011
// Set up tracing, including catching `log` crate logs from pingora crates
@@ -57,17 +58,31 @@ pub fn populate_listners<T>(
5758
ListenerKind::Tcp {
5859
addr,
5960
tls: Some(tls_cfg),
61+
offer_h2,
6062
} => {
6163
let cert_path = tls_cfg
6264
.cert_path
6365
.to_str()
6466
.expect("cert path should be utf8");
6567
let key_path = tls_cfg.key_path.to_str().expect("key path should be utf8");
66-
service
67-
.add_tls(&addr, cert_path, key_path)
68+
69+
// TODO: Make conditional!
70+
let mut settings = TlsSettings::intermediate(cert_path, key_path)
6871
.expect("adding TLS listener shouldn't fail");
72+
if offer_h2 {
73+
settings.enable_h2();
74+
}
75+
76+
service.add_tls_with_settings(&addr, None, settings);
6977
}
70-
ListenerKind::Tcp { addr, tls: None } => {
78+
ListenerKind::Tcp {
79+
addr,
80+
tls: None,
81+
offer_h2,
82+
} => {
83+
if offer_h2 {
84+
panic!("Unsupported configuration: {addr:?} configured without TLS, but H2 enabled which requires TLS");
85+
}
7186
service.add_tcp(&addr);
7287
}
7388
ListenerKind::Uds(path) => {

0 commit comments

Comments
 (0)