Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,109 changes: 667 additions & 442 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions bin/router/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use hive_router_config::RouterConfigError;
use hive_router_plan_executor::executors::error::TlsCertificatesError;

use crate::{
jwt::jwks_manager::JwksSourceError, pipeline::usage_reporting::UsageReportingError,
Expand Down Expand Up @@ -34,4 +35,6 @@ pub enum RouterInitError {
endpoint_name_two: String,
endpoint: String,
},
#[error(transparent)]
TlsCertificatesError(#[from] TlsCertificatesError),
}
23 changes: 19 additions & 4 deletions bin/router/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub use sonic_rs;
pub use tokio;
pub use tracing;
use tracing::{info, warn, Instrument};
pub mod tls;

static LABORATORY_HTML: &str = include_str!(concat!(env!("OUT_DIR"), "/laboratory.html"));

Expand Down Expand Up @@ -250,10 +251,11 @@ pub async fn router_entrypoint(plugin_registry: PluginRegistry) -> Result<(), Ro
let paths = RouterPaths::new(graphql_path.clone(), websocket_path, callback_path);
paths.detect_conflicts(&prometheus)?;

let graphql_path = graphql_path.to_string();
let long_lived_client_limit_service =
LongLivedClientLimitService::new(&shared_state.router_config);

let maybe_error = web::HttpServer::new(async move || {
let server = web::HttpServer::new(async move || {
let landing_page_path = graphql_path.clone();
let prometheus = prometheus.clone();
let long_lived_client_limit_service = long_lived_client_limit_service.clone();
Expand All @@ -273,9 +275,22 @@ pub async fn router_entrypoint(plugin_registry: PluginRegistry) -> Result<(), Ro
.default_service(web::to(move || {
landing_page_handler(landing_page_path.clone())
}))
})
.bind(&addr)
.map_err(|err| RouterInitError::HttpServerBindError(addr, err))?
});

let tls_config = shared_state_clone
.router_config
.traffic_shaping
.router
.tls
.as_ref();

let maybe_error = if let Some(tls_config) = tls_config {
let rustls_config = tls::build_rustls_config(tls_config)?;
server.bind_rustls(&addr, &rustls_config)
} else {
server.bind(&addr)
}
.map_err(|err| RouterInitError::HttpServerBindError(addr.to_string(), err))?
.run()
.await
.map_err(RouterInitError::HttpServerStartError);
Expand Down
30 changes: 30 additions & 0 deletions bin/router/src/tls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::sync::Arc;

use hive_router_config::traffic_shaping::ServerTLSConfig;
use hive_router_plan_executor::executors::{
error::TlsCertificatesError, map::from_cert_file_config_to_certificate_der,
};
use rustls::{
pki_types::{pem::PemObject, PrivateKeyDer},
server::{NoClientAuth, WebPkiClientVerifier},
RootCertStore, ServerConfig,
};

pub fn build_rustls_config(
tls_config: &ServerTLSConfig,
) -> Result<ServerConfig, TlsCertificatesError> {
let client_auth = if let Some(client_auth_config) = tls_config.client_auth.as_ref() {
let certs = from_cert_file_config_to_certificate_der(&client_auth_config.cert_file)?;
let mut roots = RootCertStore::empty();
roots.add_parsable_certificates(certs);
WebPkiClientVerifier::builder(roots.into()).build()?
} else {
Arc::new(NoClientAuth)
};
let certs = from_cert_file_config_to_certificate_der(&tls_config.cert_file)?;
let key = PrivateKeyDer::from_pem_file(&tls_config.key_file.absolute)
.map_err(|err| TlsCertificatesError::CustomTlsCertificatesError("key_file", err))?;
Ok(ServerConfig::builder()
.with_client_cert_verifier(client_auth)
.with_single_cert(certs, key)?)
}
69 changes: 69 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3076,6 +3076,7 @@ The default configuration that will be applied to all subgraphs, unless overridd
|**dedupe\_enabled**|`boolean`|Enables/disables request deduplication to subgraphs.<br/><br/>When requests exactly matches the hashing mechanism (e.g., subgraph name, URL, headers, query, variables), and are executed at the same time, they will<br/>be deduplicated by sharing the response of other in-flight requests.<br/>Default: `true`<br/>||
|**pool\_idle\_timeout**|`string`|Timeout for idle sockets being kept-alive.<br/>Default: `"50s"`<br/>||
|**request\_timeout**||Optional timeout configuration for requests to subgraphs.<br/><br/>Example with a fixed duration:<br/>```yaml<br/> timeout:<br/> duration: 5s<br/>```<br/><br/>Or with a VRL expression that can return a duration based on the operation kind:<br/>```yaml<br/> timeout:<br/> expression: \|<br/> if (.request.operation.type == "mutation") {<br/> "10s"<br/> } else {<br/> "15s"<br/> }<br/>```<br/>Default: `"30s"`<br/>||
|[**tls**](#traffic_shapingalltls)|`object`, `null`|||

**Additional Properties:** not allowed
**Example**
Expand All @@ -3087,6 +3088,28 @@ request_timeout: 30s

```

<a name="traffic_shapingalltls"></a>
#### traffic\_shaping\.all\.tls: object,null

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**cert\_file**||||
|[**client\_auth**](#traffic_shapingalltlsclient_auth)|`object`, `null`||yes|

**Additional Properties:** not allowed
<a name="traffic_shapingalltlsclient_auth"></a>
##### traffic\_shaping\.all\.tls\.client\_auth: object,null

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**cert\_file**|||yes|
|**key\_file**|`string`|Format: `"path"`<br/>|yes|

**Additional Properties:** not allowed
<a name="traffic_shapingrouter"></a>
### traffic\_shaping\.router: object

Expand All @@ -3100,6 +3123,7 @@ Configuration for the router itself, e.g., for handling incoming requests, or ot
|[**dedupe**](#traffic_shapingrouterdedupe)|`object`|Default: `{"enabled":false,"headers":"all"}`<br/>||
|**max\_long\_lived\_clients**|`integer`|Maximum number of concurrent long-lived clients (WebSocket connections and HTTP streaming responses).<br/>Regular non-streaming requests are not counted toward this limit.<br/>When the limit is reached, new WebSocket and streaming HTTP requests are rejected with 503.<br/>If both WebSockets and Subscriptions are disabled, this setting has no effect.<br/>Default: `128`<br/>Format: `"uint"`<br/>Minimum: `0`<br/>||
|**request\_timeout**|`string`|Optional timeout configuration for incoming requests to the router.<br/>It starts from the moment the request is received by the router,<br/>and includes the entire processing of the request (validation, execution, etc.) until a response is sent back to the client.<br/>If a request takes longer than the specified duration, it will be aborted and a timeout error will be returned to the client.<br/>Default: `"1m"`<br/>||
|[**tls**](#traffic_shapingroutertls)|`object`, `null`||yes|

**Additional Properties:** not allowed
**Example**
Expand Down Expand Up @@ -3132,6 +3156,28 @@ headers: all

```

<a name="traffic_shapingroutertls"></a>
#### traffic\_shaping\.router\.tls: object,null

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**cert\_file**|||yes|
|[**client\_auth**](#traffic_shapingroutertlsclient_auth)|`object`, `null`||yes|
|**key\_file**|`string`|Format: `"path"`<br/>|yes|

**Additional Properties:** not allowed
<a name="traffic_shapingroutertlsclient_auth"></a>
##### traffic\_shaping\.router\.tls\.client\_auth: object,null

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**cert\_file**|||yes|

**Additional Properties:** not allowed
<a name="traffic_shapingsubgraphs"></a>
### traffic\_shaping\.subgraphs: object

Expand All @@ -3154,6 +3200,29 @@ Optional per-subgraph configurations that will override the default configuratio
|**dedupe\_enabled**|`boolean`, `null`|Enables/disables request deduplication to subgraphs.<br/><br/>When requests exactly matches the hashing mechanism (e.g., subgraph name, URL, headers, query, variables), and are executed at the same time, they will<br/>be deduplicated by sharing the response of other in-flight requests.<br/>||
|**pool\_idle\_timeout**|`string`, `null`|Timeout for idle sockets being kept-alive.<br/>||
|**request\_timeout**||Optional timeout configuration for requests to subgraphs.<br/><br/>Example with a fixed duration:<br/>```yaml<br/> timeout:<br/> duration: 5s<br/>```<br/><br/>Or with a VRL expression that can return a duration based on the operation kind:<br/>```yaml<br/> timeout:<br/> expression: \|<br/> if (.request.operation.type == "mutation") {<br/> "10s"<br/> } else {<br/> "15s"<br/> }<br/>```<br/>||
|[**tls**](#traffic_shapingsubgraphsadditionalpropertiestls)|`object`, `null`|||

**Additional Properties:** not allowed
<a name="traffic_shapingsubgraphsadditionalpropertiestls"></a>
##### traffic\_shaping\.subgraphs\.additionalProperties\.tls: object,null

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**cert\_file**||||
|[**client\_auth**](#traffic_shapingsubgraphsadditionalpropertiestlsclient_auth)|`object`, `null`||yes|

**Additional Properties:** not allowed
<a name="traffic_shapingsubgraphsadditionalpropertiestlsclient_auth"></a>
###### traffic\_shaping\.subgraphs\.additionalProperties\.tls\.client\_auth: object,null

**Properties**

|Name|Type|Description|Required|
|----|----|-----------|--------|
|**cert\_file**|||yes|
|**key\_file**|`string`|Format: `"path"`<br/>|yes|

**Additional Properties:** not allowed
<a name="websocket"></a>
Expand Down
6 changes: 4 additions & 2 deletions e2e/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ sonic-rs = { workspace = true }
lazy_static = { workspace = true }
jsonwebtoken = { workspace = true }
insta = { workspace = true }
reqwest = { workspace = true, features = ["json"]}
reqwest = { workspace = true, features = ["json", "rustls-tls"] }
opentelemetry-otlp = { workspace = true, features = ["http-proto"] }
opentelemetry-proto = { workspace = true }
prost = { workspace = true }
Expand All @@ -31,6 +31,7 @@ dashmap = { workspace = true }
axum = { workspace = true }
bytes = { workspace = true }
arc-swap = { workspace = true }
rustls = { workspace = true }

hive-router = { path = "../bin/router", features = ["testing"] }
hive-router-config = { path = "../lib/router-config" }
Expand All @@ -46,4 +47,5 @@ hex = "0.4"
tiny_http = "0.12"
futures-util = { workspace = true }
bollard = "0.20.0"

axum-server = { version = "0.8.0", features = ["tls-rustls"] }
rcgen = "0.14.7"
6 changes: 3 additions & 3 deletions e2e/src/error_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ mod error_handling_e2e_tests {
#[ntex::test]
async fn should_continue_execution_when_a_subgraph_is_down() {
let subgraphs = TestSubgraphs::builder().build().start().await;
let subgraphs_addr = subgraphs.addr();
let subgraphs_url = subgraphs.url();

let router = TestRouter::builder()
// we dont set subgraphs avoiding the port change in the supergrph.
Expand All @@ -18,9 +18,9 @@ mod error_handling_e2e_tests {
path: supergraph.graphql
override_subgraph_urls:
accounts:
url: "http://{subgraphs_addr}/accounts"
url: "{subgraphs_url}/accounts"
reviews:
url: "http://{subgraphs_addr}/reviews"
url: "{subgraphs_url}/reviews"
products:
url: "http://0.0.0.0:1000/products"
"#,
Expand Down
2 changes: 2 additions & 0 deletions e2e/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ mod telemetry;
#[cfg(test)]
mod timeout_per_subgraph;
#[cfg(test)]
mod tls;
#[cfg(test)]
mod websocket;

pub use insta;
Expand Down
8 changes: 4 additions & 4 deletions e2e/src/override_subgraph_urls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ mod override_subgraph_urls_e2e_tests {
/// This way we can verify that the override is applied correctly.
async fn should_override_subgraph_url_based_on_static_value() {
let subgraphs = TestSubgraphs::builder().build().start().await;
let subgraphs_addr = subgraphs.addr();
let subgraphs_url = subgraphs.url();

let router = TestRouter::builder()
.inline_config(format!(
Expand All @@ -22,7 +22,7 @@ mod override_subgraph_urls_e2e_tests {
path: supergraph.graphql
override_subgraph_urls:
accounts:
url: "http://{subgraphs_addr}/accounts"
url: "{subgraphs_url}/accounts"
"#,
))
.build()
Expand Down Expand Up @@ -54,7 +54,7 @@ mod override_subgraph_urls_e2e_tests {
/// Without the header, the request goes to 4200 and fail (thanks to `.default`).
async fn should_override_subgraph_url_based_on_header_value() {
let subgraphs = TestSubgraphs::builder().build().start().await;
let subgraphs_addr = subgraphs.addr();
let subgraphs_url = subgraphs.url();

let router = TestRouter::builder()
.inline_config(format!(
Expand All @@ -67,7 +67,7 @@ mod override_subgraph_urls_e2e_tests {
url:
expression: |
if .request.headers."x-accounts-port" == "4100" {{
"http://{subgraphs_addr}/accounts"
"{subgraphs_url}/accounts"
}} else {{
.default
}}
Expand Down
Loading
Loading