diff --git a/.circleci/config.yml b/.circleci/config.yml index 2762dbc746..67f8183606 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -87,6 +87,10 @@ parameters: nightly: type: boolean default: false + # quick_nightly will skip testing and only build the release artifacts. + quick_nightly: + type: boolean + default: false # These are common environment variables that we want to set on on all jobs. # While these could conceivably be set on the CircleCI project settings' @@ -626,6 +630,7 @@ jobs: RELEASE_BIN: router APPLE_TEAM_ID: "YQK948L752" APPLE_USERNAME: "opensource@apollographql.com" + REPO_URL: << pipeline.project.git_url >> steps: - checkout - setup_environment: @@ -724,7 +729,9 @@ jobs: and: - equal: [ *amd_linux_build_executor, << parameters.platform >> ] - equal: [ true, << parameters.nightly >> ] - - equal: [ "https://github.com/apollographql/router", << pipeline.project.git_url >> ] + - matches: + pattern: "^https:\\/\\/github\\.com\\/apollographql\\/router.*$" + value: << pipeline.project.git_url >> steps: - setup_remote_docker: version: 20.10.11 @@ -732,10 +739,22 @@ jobs: - run: name: Docker build command: | + # Source of the new image will be ser to the repo URL. + # This will have the effect of setting org.opencontainers.image.source and org.opencontainers.image.author to the originating pipeline + # Therefore the docker image will have the same permissions as the originating project. + # See: https://docs.github.com/en/packages/learn-github-packages/connecting-a-repository-to-a-package#connecting-a-repository-to-a-container-image-using-the-command-line + BASE_VERSION=$(cargo metadata --format-version=1 --no-deps | jq --raw-output '.packages[0].version') ARTIFACT_URL="https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0/artifacts/router-v${BASE_VERSION}-x86_64-unknown-linux-gnu.tar.gz" VERSION="v$(echo "${BASE_VERSION}" | tr "+" "-")" ROUTER_TAG=ghcr.io/apollographql/nightly/router + + echo "REPO_URL: ${REPO_URL}" + echo "BASE_VERSION: ${BASE_VERSION}" + echo "ARTIFACT_URL: ${ARTIFACT_URL}" + echo "VERSION: ${VERSION}" + echo "ROUTER_TAG: ${ROUTER_TAG}" + # Create a multi-arch builder which works properly under qemu docker run --rm --privileged multiarch/qemu-user-static --reset -p yes docker context create buildx-build @@ -745,10 +764,10 @@ jobs: echo ${GITHUB_OCI_TOKEN} | docker login ghcr.io -u apollo-bot2 --password-stdin # TODO: Can't figure out how to build multi-arch image from ARTIFACT_URL right now. Figure out later... # Build and push debug image - docker buildx build --load --platform linux/amd64 --build-arg ARTIFACT_URL="${ARTIFACT_URL}" --build-arg DEBUG_IMAGE="true" --build-arg ROUTER_RELEASE=${VERSION} -f dockerfiles/Dockerfile.router -t ${ROUTER_TAG}:${VERSION}-debug . + docker buildx build --load --platform linux/amd64 --build-arg CIRCLE_TOKEN="${CIRCLE_TOKEN}" --build-arg REPO_URL="${REPO_URL}" --build-arg ARTIFACT_URL="${ARTIFACT_URL}" --build-arg DEBUG_IMAGE="true" --build-arg ROUTER_RELEASE=${VERSION} -f dockerfiles/Dockerfile.router -t ${ROUTER_TAG}:${VERSION}-debug . docker push ${ROUTER_TAG}:${VERSION}-debug # Build and push release image - docker buildx build --load --platform linux/amd64 --build-arg ARTIFACT_URL="${ARTIFACT_URL}" --build-arg ROUTER_RELEASE=${VERSION} -f dockerfiles/Dockerfile.router -t ${ROUTER_TAG}:${VERSION} . + docker buildx build --load --platform linux/amd64 --build-arg CIRCLE_TOKEN="${CIRCLE_TOKEN}" --build-arg REPO_URL="${REPO_URL}" --build-arg ARTIFACT_URL="${ARTIFACT_URL}" --build-arg ROUTER_RELEASE=${VERSION} -f dockerfiles/Dockerfile.router -t ${ROUTER_TAG}:${VERSION} . docker push ${ROUTER_TAG}:${VERSION} # save containers for analysis mkdir built-containers @@ -920,7 +939,10 @@ jobs: workflows: ci_checks: when: - not: << pipeline.parameters.nightly >> + not: + or: + - << pipeline.parameters.nightly >> + - << pipeline.parameters.quick_nightly >> jobs: - lint: matrix: @@ -954,6 +976,18 @@ workflows: platform: [ macos_test, windows_test, amd_linux_test, arm_linux_test ] + quick-nightly: + when: << pipeline.parameters.quick_nightly >> + jobs: + - build_release: + nightly: true + context: + - router + - orb-publishing + matrix: + parameters: + platform: + [ macos_build, windows_build, amd_linux_build, arm_linux_build ] nightly: when: << pipeline.parameters.nightly >> jobs: @@ -993,7 +1027,9 @@ workflows: - test - test_updated nightly: true - context: router + context: + - router + - orb-publishing matrix: parameters: platform: @@ -1020,7 +1056,10 @@ workflows: release: when: - not: << pipeline.parameters.nightly >> + not: + or: + - << pipeline.parameters.nightly >> + - << pipeline.parameters.quick_nightly >> jobs: - pre_verify_release: matrix: @@ -1110,7 +1149,10 @@ workflows: security-scans: when: - not: << pipeline.parameters.nightly >> + not: + or: + - << pipeline.parameters.nightly >> + - << pipeline.parameters.quick_nightly >> jobs: - secops/gitleaks: context: diff --git a/CHANGELOG.md b/CHANGELOG.md index c2db241d92..8fc01da455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,30 @@ All notable changes to Router will be documented in this file. This project adheres to [Semantic Versioning v2.0.0](https://semver.org/spec/v2.0.0.html). +# [1.52.1] - 2024-08-27 + +> [!IMPORTANT] +> If you have enabled [Distributed query plan caching](https://www.apollographql.com/docs/router/configuration/distributed-caching/#distributed-query-plan-caching), this release changes the hashing algorithm used for the cache keys. On account of this, you should anticipate additional cache regeneration cost when updating between these versions while the new hashing algorithm comes into service. + +## 🔒 Security + +### CVE-2024-43783: Payload limits may exceed configured maximum + +Correct a denial-of-service vulnerability which, under certain non-default configurations below, made it possible to exceed the configured request payload maximums set with the [`limits.http_max_request_bytes`](https://www.apollographql.com/docs/router/configuration/overview/#http_max_request_bytes) option. + +This affects the following non-default Router configurations: + +1. Those configured to send request bodies to [External Coprocessors](https://www.apollographql.com/docs/router/customizations/coprocessor) where the `coprocessor.router.request.body` configuration option is set to `true`; or +2. Those which declare custom native Rust plugins using the `plugins` configuration where those plugins access the request body in the `RouterService` layer. + +Rhai plugins are **not** impacted. See the associated Github Advisory, [GHSA-x6xq-whh3-gg32](https://github.com/apollographql/router/security/advisories/GHSA-x6xq-whh3-gg32), for more information. + +### CVE-2024-43414: Update query planner to resolve uncontrolled recursion + +Update the version of `@apollo/query-planner` used by Router to v2.8.5 which corrects an uncontrolled recursion weakness (classified as [CWE-674](https://cwe.mitre.org/data/definitions/674.html)) during query planning for complex queries on particularly complex graphs. + +This weakness impacts all versions of Router prior to this release. See the associated Github Advisory, [GHSA-fmj9-77q8-g6c4](https://github.com/apollographql/federation/security/advisories/GHSA-fmj9-77q8-g6c4), for more information. + # [1.52.0] - 2024-07-30 ## 🚀 Features diff --git a/Cargo.lock b/Cargo.lock index 604247d2a0..c74849f9a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,7 +424,7 @@ dependencies = [ [[package]] name = "apollo-federation" -version = "1.52.0" +version = "1.52.1" dependencies = [ "apollo-compiler", "derive_more", @@ -471,7 +471,7 @@ dependencies = [ [[package]] name = "apollo-router" -version = "1.52.0" +version = "1.52.1" dependencies = [ "access-json", "ahash", @@ -578,6 +578,7 @@ dependencies = [ "rhai", "rmp", "router-bridge", + "rowan", "rstack", "rust-embed", "rustls", @@ -639,7 +640,7 @@ dependencies = [ [[package]] name = "apollo-router-benchmarks" -version = "1.52.0" +version = "1.52.1" dependencies = [ "apollo-parser", "apollo-router", @@ -655,7 +656,7 @@ dependencies = [ [[package]] name = "apollo-router-scaffold" -version = "1.52.0" +version = "1.52.1" dependencies = [ "anyhow", "cargo-scaffold", @@ -6095,9 +6096,9 @@ dependencies = [ [[package]] name = "router-bridge" -version = "0.5.27+v2.8.1" +version = "0.5.31+v2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "288fa40fc4e0a76fb911410e05d4525e8bf7558622bd02403f89f871c4d0785b" +checksum = "672901b1ec6fd110ac41d61ca5e1754319d0edf39546a089a114ab865d42ae97" dependencies = [ "anyhow", "async-channel 1.9.0", diff --git a/apollo-federation/Cargo.toml b/apollo-federation/Cargo.toml index a43735f123..bb3bb7a84d 100644 --- a/apollo-federation/Cargo.toml +++ b/apollo-federation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-federation" -version = "1.52.0" +version = "1.52.1" authors = ["The Apollo GraphQL Contributors"] edition = "2021" description = "Apollo Federation" diff --git a/apollo-router-benchmarks/Cargo.toml b/apollo-router-benchmarks/Cargo.toml index cc1e73902a..67369c060a 100644 --- a/apollo-router-benchmarks/Cargo.toml +++ b/apollo-router-benchmarks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-benchmarks" -version = "1.52.0" +version = "1.52.1" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/Cargo.toml b/apollo-router-scaffold/Cargo.toml index 7bd39a628b..0aea0a37a9 100644 --- a/apollo-router-scaffold/Cargo.toml +++ b/apollo-router-scaffold/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router-scaffold" -version = "1.52.0" +version = "1.52.1" authors = ["Apollo Graph, Inc. "] edition = "2021" license = "Elastic-2.0" diff --git a/apollo-router-scaffold/templates/base/Cargo.template.toml b/apollo-router-scaffold/templates/base/Cargo.template.toml index 21f679e602..f135df3eb3 100644 --- a/apollo-router-scaffold/templates/base/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/Cargo.template.toml @@ -22,7 +22,7 @@ apollo-router = { path ="{{integration_test}}apollo-router" } apollo-router = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} # Note if you update these dependencies then also update xtask/Cargo.toml -apollo-router = "1.52.0" +apollo-router = "1.52.1" {{/if}} {{/if}} async-trait = "0.1.52" diff --git a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml index 5194c11c10..6d9c8e98f6 100644 --- a/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml +++ b/apollo-router-scaffold/templates/base/xtask/Cargo.template.toml @@ -13,7 +13,7 @@ apollo-router-scaffold = { path ="{{integration_test}}apollo-router-scaffold" } {{#if branch}} apollo-router-scaffold = { git="https://github.com/apollographql/router.git", branch="{{branch}}" } {{else}} -apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.52.0" } +apollo-router-scaffold = { git = "https://github.com/apollographql/router.git", tag = "v1.52.1" } {{/if}} {{/if}} anyhow = "1.0.58" diff --git a/apollo-router/Cargo.toml b/apollo-router/Cargo.toml index 26618a16a6..c3844426cb 100644 --- a/apollo-router/Cargo.toml +++ b/apollo-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-router" -version = "1.52.0" +version = "1.52.1" authors = ["Apollo Graph, Inc. "] repository = "https://github.com/apollographql/router/" documentation = "https://docs.rs/apollo-router" @@ -68,7 +68,7 @@ askama = "0.12.1" access-json = "0.1.0" anyhow = "1.0.86" apollo-compiler.workspace = true -apollo-federation = { path = "../apollo-federation", version = "=1.52.0" } +apollo-federation = { path = "../apollo-federation", version = "=1.52.1" } arc-swap = "1.6.0" async-channel = "1.9.0" async-compression = { version = "0.4.6", features = [ @@ -161,6 +161,8 @@ opentelemetry-aws = "0.8.0" # opentelemetry-datadog = { version = "0.8.0", features = ["reqwest-client"] } rmp = "0.8" # END TEMP DATADOG +# Pin rowan until update to rust 1.77 +rowan = "=0.15.15" opentelemetry-http = "0.9.0" opentelemetry-jaeger = { version = "0.19.0", features = [ "collector_client", @@ -195,7 +197,7 @@ regex = "1.10.5" reqwest.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.27+v2.8.1" +router-bridge = "=0.5.31+v2.8.5" rust-embed = { version = "8.4.0", features = ["include-exclude"] } rustls = "0.21.12" diff --git a/apollo-router/src/axum_factory/tests.rs b/apollo-router/src/axum_factory/tests.rs index dde3852032..3d8f39b5d5 100644 --- a/apollo-router/src/axum_factory/tests.rs +++ b/apollo-router/src/axum_factory/tests.rs @@ -44,8 +44,6 @@ use test_log::test; use tokio::io::AsyncRead; use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; -#[cfg(unix)] -use tokio::io::BufReader; use tokio::sync::mpsc; use tokio_util::io::StreamReader; use tower::service_fn; @@ -1270,14 +1268,11 @@ async fn it_answers_to_custom_endpoint() -> Result<(), ApolloRouterError> { Ok::<_, BoxError>( http::Response::builder() .status(StatusCode::OK) - .body( - format!( - "{} + {}", - req.router_request.method(), - req.router_request.uri().path() - ) - .into(), - ) + .body(format!( + "{} + {}", + req.router_request.method(), + req.router_request.uri().path() + )) .unwrap() .into(), ) @@ -1380,14 +1375,11 @@ async fn it_refuses_to_bind_two_extra_endpoints_on_the_same_path() { Ok::<_, BoxError>( http::Response::builder() .status(StatusCode::OK) - .body( - format!( - "{} + {}", - req.router_request.method(), - req.router_request.uri().path() - ) - .into(), - ) + .body(format!( + "{} + {}", + req.router_request.method(), + req.router_request.uri().path() + )) .unwrap() .into(), ) @@ -2076,7 +2068,7 @@ async fn listening_to_unix_socket() { .await; assert_eq!( - serde_json::from_slice::(&output).unwrap(), + serde_json::from_str::(&output).unwrap(), expected_response, ); @@ -2084,12 +2076,12 @@ async fn listening_to_unix_socket() { let output = send_to_unix_socket( server.graphql_listen_address().as_ref().unwrap(), Method::GET, - r#"query=query%7Bme%7Bname%7D%7D"#, + r#"/?query=query%7Bme%7Bname%7D%7D"#, ) .await; assert_eq!( - serde_json::from_slice::(&output).unwrap(), + serde_json::from_str::(&output).unwrap(), expected_response, ); @@ -2097,67 +2089,31 @@ async fn listening_to_unix_socket() { } #[cfg(unix)] -async fn send_to_unix_socket(addr: &ListenAddr, method: Method, body: &str) -> Vec { - use tokio::io::AsyncBufReadExt; - use tokio::io::Interest; +async fn send_to_unix_socket(addr: &ListenAddr, method: Method, body: &str) -> String { use tokio::net::UnixStream; - - let content = match method { - Method::GET => { - format!( - "{} /?{} HTTP/1.1\r -Host: localhost:4100\r -Content-Length: {}\r -Content-Type: application/json\r -Accept: application/json\r - -\n", - method.as_str(), - body, - body.len(), - ) - } - Method::POST => { - format!( - "{} / HTTP/1.1\r -Host: localhost:4100\r -Content-Length: {}\r -Content-Type: application/json\r -Accept: application/json\r - -{}\n", - method.as_str(), - body.len(), - body - ) - } - _ => { - unimplemented!() - } - }; - let mut stream = UnixStream::connect(addr.to_string()).await.unwrap(); - stream.ready(Interest::WRITABLE).await.unwrap(); - stream.write_all(content.as_bytes()).await.unwrap(); - stream.flush().await.unwrap(); - let stream = BufReader::new(stream); - let mut lines = stream.lines(); - let header_first_line = lines - .next_line() - .await - .unwrap() - .expect("no header received"); - // skip the rest of the headers - let mut headers = String::new(); - let mut stream = lines.into_inner(); - loop { - if stream.read_line(&mut headers).await.unwrap() == 2 { - break; + let stream = UnixStream::connect(addr.to_string()).await.unwrap(); + let (mut sender, conn) = hyper::client::conn::handshake(stream).await.unwrap(); + tokio::task::spawn(async move { + if let Err(err) = conn.await { + println!("Connection failed: {:?}", err); } + }); + + let http_body = hyper::Body::from(body.to_string()); + let mut request = http::Request::builder() + .method(method.clone()) + .header("Host", "localhost:4100") + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .body(http_body) + .unwrap(); + if method == Method::GET { + *request.uri_mut() = body.parse().unwrap(); } - // get rest of the buffer as body - let body = stream.buffer().to_vec(); - assert!(header_first_line.contains(" 200 "), ""); - body + + let response = sender.send_request(request).await.unwrap(); + let body = response.collect().await.unwrap().to_bytes(); + String::from_utf8(body.to_vec()).unwrap() } #[tokio::test] diff --git a/apollo-router/src/configuration/mod.rs b/apollo-router/src/configuration/mod.rs index e3f5ee071e..cebc0add67 100644 --- a/apollo-router/src/configuration/mod.rs +++ b/apollo-router/src/configuration/mod.rs @@ -47,6 +47,7 @@ use crate::configuration::schema::Mode; use crate::graphql; use crate::notification::Notify; use crate::plugin::plugins; +use crate::plugins::limits; use crate::plugins::subscription::SubscriptionConfig; use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN; use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME; @@ -154,7 +155,7 @@ pub struct Configuration { /// Configuration for operation limits, parser limits, HTTP limits, etc. #[serde(default)] - pub(crate) limits: Limits, + pub(crate) limits: limits::Config, /// Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions. /// You probably don’t want this in production! @@ -251,18 +252,25 @@ impl<'de> serde::Deserialize<'de> for Configuration { tls: Tls, apq: Apq, persisted_queries: PersistedQueries, - limits: Limits, + limits: limits::Config, experimental_chaos: Chaos, batching: Batching, experimental_type_conditioned_fetching: bool, experimental_apollo_metrics_generation_mode: ApolloMetricsGenerationMode, experimental_query_planner_mode: QueryPlannerMode, } - let ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?; + let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?; let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins) .map_err(|e| serde::de::Error::custom(e.to_string()))?; + // Allow the limits plugin to use the configuration from the configuration struct. + // This means that the limits plugin will get the regular configuration via plugin init. + ad_hoc.apollo_plugins.plugins.insert( + "limits".to_string(), + serde_json::to_value(&ad_hoc.limits).unwrap(), + ); + // Use a struct literal instead of a builder to ensure this is exhaustive Configuration { health_check: ad_hoc.health_check, @@ -319,7 +327,7 @@ impl Configuration { tls: Option, apq: Option, persisted_query: Option, - operation_limits: Option, + operation_limits: Option, chaos: Option, uplink: Option, experimental_type_conditioned_fetching: Option, @@ -439,7 +447,7 @@ impl Configuration { notify: Option>, apq: Option, persisted_query: Option, - operation_limits: Option, + operation_limits: Option, chaos: Option, uplink: Option, batching: Option, @@ -856,106 +864,6 @@ impl Supergraph { } } -/// Configuration for operation limits, parser limits, HTTP limits, etc. -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] -#[serde(deny_unknown_fields, default)] -pub(crate) struct Limits { - /// If set, requests with operations deeper than this maximum - /// are rejected with a HTTP 400 Bad Request response and GraphQL error with - /// `"extensions": {"code": "MAX_DEPTH_LIMIT"}` - /// - /// Counts depth of an operation, looking at its selection sets, - /// including fields in fragments and inline fragments. The following - /// example has a depth of 3. - /// - /// ```graphql - /// query getProduct { - /// book { # 1 - /// ...bookDetails - /// } - /// } - /// - /// fragment bookDetails on Book { - /// details { # 2 - /// ... on ProductDetailsBook { - /// country # 3 - /// } - /// } - /// } - /// ``` - pub(crate) max_depth: Option, - - /// If set, requests with operations higher than this maximum - /// are rejected with a HTTP 400 Bad Request response and GraphQL error with - /// `"extensions": {"code": "MAX_DEPTH_LIMIT"}` - /// - /// Height is based on simple merging of fields using the same name or alias, - /// but only within the same selection set. - /// For example `name` here is only counted once and the query has height 3, not 4: - /// - /// ```graphql - /// query { - /// name { first } - /// name { last } - /// } - /// ``` - /// - /// This may change in a future version of Apollo Router to do - /// [full field merging across fragments][merging] instead. - /// - /// [merging]: https://spec.graphql.org/October2021/#sec-Field-Selection-Merging] - pub(crate) max_height: Option, - - /// If set, requests with operations with more root fields than this maximum - /// are rejected with a HTTP 400 Bad Request response and GraphQL error with - /// `"extensions": {"code": "MAX_ROOT_FIELDS_LIMIT"}` - /// - /// This limit counts only the top level fields in a selection set, - /// including fragments and inline fragments. - pub(crate) max_root_fields: Option, - - /// If set, requests with operations with more aliases than this maximum - /// are rejected with a HTTP 400 Bad Request response and GraphQL error with - /// `"extensions": {"code": "MAX_ALIASES_LIMIT"}` - pub(crate) max_aliases: Option, - - /// If set to true (which is the default is dev mode), - /// requests that exceed a `max_*` limit are *not* rejected. - /// Instead they are executed normally, and a warning is logged. - pub(crate) warn_only: bool, - - /// Limit recursion in the GraphQL parser to protect against stack overflow. - /// default: 500 - pub(crate) parser_max_recursion: usize, - - /// Limit the number of tokens the GraphQL parser processes before aborting. - pub(crate) parser_max_tokens: usize, - - /// Limit the size of incoming HTTP requests read from the network, - /// to protect against running out of memory. Default: 2000000 (2 MB) - pub(crate) http_max_request_bytes: usize, -} - -impl Default for Limits { - fn default() -> Self { - Self { - // These limits are opt-in - max_depth: None, - max_height: None, - max_root_fields: None, - max_aliases: None, - warn_only: false, - http_max_request_bytes: 2_000_000, - parser_max_tokens: 15_000, - - // This is `apollo-parser`’s default, which protects against stack overflow - // but is still very high for "reasonable" queries. - // https://github.com/apollographql/apollo-rs/blob/apollo-parser%400.7.3/crates/apollo-parser/src/parser/mod.rs#L93-L104 - parser_max_recursion: 500, - } - } -} - /// Router level (APQ) configuration #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)] #[serde(deny_unknown_fields)] diff --git a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap index 1f66e536ba..c47abd0744 100644 --- a/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap +++ b/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__schema_generation.snap @@ -1222,8 +1222,8 @@ expression: "&schema" "nullable": true }, "subgraph": { - "$ref": "#/definitions/Config3", - "description": "#/definitions/Config3", + "$ref": "#/definitions/Config4", + "description": "#/definitions/Config4", "nullable": true } }, @@ -1321,8 +1321,8 @@ expression: "&schema" "description": "Telemetry configuration", "properties": { "apollo": { - "$ref": "#/definitions/Config8", - "description": "#/definitions/Config8" + "$ref": "#/definitions/Config9", + "description": "#/definitions/Config9" }, "exporters": { "$ref": "#/definitions/Exporters", @@ -1336,11 +1336,100 @@ expression: "&schema" "type": "object" }, "Config": { - "description": "This is a broken plugin for testing purposes only.", + "additionalProperties": false, + "description": "Configuration for operation limits, parser limits, HTTP limits, etc.", + "properties": { + "http_max_request_bytes": { + "default": 2000000, + "description": "Limit the size of incoming HTTP requests read from the network, to protect against running out of memory. Default: 2000000 (2 MB)", + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "max_aliases": { + "default": null, + "description": "If set, requests with operations with more aliases than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_ALIASES_LIMIT\"}`", + "format": "uint32", + "minimum": 0.0, + "nullable": true, + "type": "integer" + }, + "max_depth": { + "default": null, + "description": "If set, requests with operations deeper than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_DEPTH_LIMIT\"}`\n\nCounts depth of an operation, looking at its selection sets,˛ including fields in fragments and inline fragments. The following example has a depth of 3.\n\n```graphql query getProduct { book { # 1 ...bookDetails } }\n\nfragment bookDetails on Book { details { # 2 ... on ProductDetailsBook { country # 3 } } } ```", + "format": "uint32", + "minimum": 0.0, + "nullable": true, + "type": "integer" + }, + "max_height": { + "default": null, + "description": "If set, requests with operations higher than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_DEPTH_LIMIT\"}`\n\nHeight is based on simple merging of fields using the same name or alias, but only within the same selection set. For example `name` here is only counted once and the query has height 3, not 4:\n\n```graphql query { name { first } name { last } } ```\n\nThis may change in a future version of Apollo Router to do [full field merging across fragments][merging] instead.\n\n[merging]: https://spec.graphql.org/October2021/#sec-Field-Selection-Merging]", + "format": "uint32", + "minimum": 0.0, + "nullable": true, + "type": "integer" + }, + "max_root_fields": { + "default": null, + "description": "If set, requests with operations with more root fields than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_ROOT_FIELDS_LIMIT\"}`\n\nThis limit counts only the top level fields in a selection set, including fragments and inline fragments.", + "format": "uint32", + "minimum": 0.0, + "nullable": true, + "type": "integer" + }, + "parser_max_recursion": { + "default": 500, + "description": "Limit recursion in the GraphQL parser to protect against stack overflow. default: 500", + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "parser_max_tokens": { + "default": 15000, + "description": "Limit the number of tokens the GraphQL parser processes before aborting.", + "format": "uint", + "minimum": 0.0, + "type": "integer" + }, + "warn_only": { + "default": false, + "description": "If set to true (which is the default is dev mode), requests that exceed a `max_*` limit are *not* rejected. Instead they are executed normally, and a warning is logged.", + "type": "boolean" + } + }, + "type": "object" + }, + "Config10": { + "additionalProperties": false, "properties": { + "batch_processor": { + "$ref": "#/definitions/BatchProcessorConfig", + "description": "#/definitions/BatchProcessorConfig" + }, "enabled": { - "description": "Enable the broken plugin.", + "description": "Enable otlp", "type": "boolean" + }, + "endpoint": { + "$ref": "#/definitions/UriEndpoint", + "description": "#/definitions/UriEndpoint" + }, + "grpc": { + "$ref": "#/definitions/GrpcExporter", + "description": "#/definitions/GrpcExporter" + }, + "http": { + "$ref": "#/definitions/HttpExporter", + "description": "#/definitions/HttpExporter" + }, + "protocol": { + "$ref": "#/definitions/Protocol", + "description": "#/definitions/Protocol" + }, + "temporality": { + "$ref": "#/definitions/Temporality", + "description": "#/definitions/Temporality" } }, "required": [ @@ -1348,7 +1437,7 @@ expression: "&schema" ], "type": "object" }, - "Config10": { + "Config11": { "additionalProperties": false, "description": "Prometheus configuration", "properties": { @@ -1369,7 +1458,7 @@ expression: "&schema" }, "type": "object" }, - "Config11": { + "Config12": { "anyOf": [ { "additionalProperties": false, @@ -1415,7 +1504,7 @@ expression: "&schema" } ] }, - "Config12": { + "Config13": { "additionalProperties": false, "properties": { "batch_processor": { @@ -1436,7 +1525,7 @@ expression: "&schema" ], "type": "object" }, - "Config13": { + "Config14": { "additionalProperties": false, "properties": { "batch_processor": { @@ -1493,7 +1582,7 @@ expression: "&schema" ], "type": "object" }, - "Config14": { + "Config15": { "additionalProperties": false, "description": "Configuration for the experimental traffic shaping plugin", "properties": { @@ -1525,6 +1614,19 @@ expression: "&schema" "type": "object" }, "Config2": { + "description": "This is a broken plugin for testing purposes only.", + "properties": { + "enabled": { + "description": "Enable the broken plugin.", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "Config3": { "description": "Restricted plugin (for testing purposes only)", "properties": { "enabled": { @@ -1537,7 +1639,7 @@ expression: "&schema" ], "type": "object" }, - "Config3": { + "Config4": { "additionalProperties": false, "description": "Configure subgraph authentication", "properties": { @@ -1557,7 +1659,7 @@ expression: "&schema" }, "type": "object" }, - "Config4": { + "Config5": { "additionalProperties": false, "description": "Configuration for header propagation", "properties": { @@ -1577,7 +1679,7 @@ expression: "&schema" }, "type": "object" }, - "Config5": { + "Config6": { "additionalProperties": false, "description": "Configuration for exposing errors that originate from subgraphs", "properties": { @@ -1597,7 +1699,7 @@ expression: "&schema" }, "type": "object" }, - "Config6": { + "Config7": { "additionalProperties": false, "description": "Configuration for entity caching", "properties": { @@ -1625,11 +1727,11 @@ expression: "&schema" ], "type": "object" }, - "Config7": { + "Config8": { "description": "Configuration for the progressive override plugin", "type": "object" }, - "Config8": { + "Config9": { "additionalProperties": false, "properties": { "batch_processor": { @@ -1705,43 +1807,6 @@ expression: "&schema" }, "type": "object" }, - "Config9": { - "additionalProperties": false, - "properties": { - "batch_processor": { - "$ref": "#/definitions/BatchProcessorConfig", - "description": "#/definitions/BatchProcessorConfig" - }, - "enabled": { - "description": "Enable otlp", - "type": "boolean" - }, - "endpoint": { - "$ref": "#/definitions/UriEndpoint", - "description": "#/definitions/UriEndpoint" - }, - "grpc": { - "$ref": "#/definitions/GrpcExporter", - "description": "#/definitions/GrpcExporter" - }, - "http": { - "$ref": "#/definitions/HttpExporter", - "description": "#/definitions/HttpExporter" - }, - "protocol": { - "$ref": "#/definitions/Protocol", - "description": "#/definitions/Protocol" - }, - "temporality": { - "$ref": "#/definitions/Temporality", - "description": "#/definitions/Temporality" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, "ContextForward": { "additionalProperties": false, "description": "Configuration to forward context values in metric attributes/labels", @@ -3601,71 +3666,6 @@ expression: "&schema" ], "type": "object" }, - "Limits": { - "additionalProperties": false, - "description": "Configuration for operation limits, parser limits, HTTP limits, etc.", - "properties": { - "http_max_request_bytes": { - "default": 2000000, - "description": "Limit the size of incoming HTTP requests read from the network, to protect against running out of memory. Default: 2000000 (2 MB)", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "max_aliases": { - "default": null, - "description": "If set, requests with operations with more aliases than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_ALIASES_LIMIT\"}`", - "format": "uint32", - "minimum": 0.0, - "nullable": true, - "type": "integer" - }, - "max_depth": { - "default": null, - "description": "If set, requests with operations deeper than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_DEPTH_LIMIT\"}`\n\nCounts depth of an operation, looking at its selection sets, including fields in fragments and inline fragments. The following example has a depth of 3.\n\n```graphql query getProduct { book { # 1 ...bookDetails } }\n\nfragment bookDetails on Book { details { # 2 ... on ProductDetailsBook { country # 3 } } } ```", - "format": "uint32", - "minimum": 0.0, - "nullable": true, - "type": "integer" - }, - "max_height": { - "default": null, - "description": "If set, requests with operations higher than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_DEPTH_LIMIT\"}`\n\nHeight is based on simple merging of fields using the same name or alias, but only within the same selection set. For example `name` here is only counted once and the query has height 3, not 4:\n\n```graphql query { name { first } name { last } } ```\n\nThis may change in a future version of Apollo Router to do [full field merging across fragments][merging] instead.\n\n[merging]: https://spec.graphql.org/October2021/#sec-Field-Selection-Merging]", - "format": "uint32", - "minimum": 0.0, - "nullable": true, - "type": "integer" - }, - "max_root_fields": { - "default": null, - "description": "If set, requests with operations with more root fields than this maximum are rejected with a HTTP 400 Bad Request response and GraphQL error with `\"extensions\": {\"code\": \"MAX_ROOT_FIELDS_LIMIT\"}`\n\nThis limit counts only the top level fields in a selection set, including fragments and inline fragments.", - "format": "uint32", - "minimum": 0.0, - "nullable": true, - "type": "integer" - }, - "parser_max_recursion": { - "default": 500, - "description": "Limit recursion in the GraphQL parser to protect against stack overflow. default: 500", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "parser_max_tokens": { - "default": 15000, - "description": "Limit the number of tokens the GraphQL parser processes before aborting.", - "format": "uint", - "minimum": 0.0, - "type": "integer" - }, - "warn_only": { - "default": false, - "description": "If set to true (which is the default is dev mode), requests that exceed a `max_*` limit are *not* rejected. Instead they are executed normally, and a warning is logged.", - "type": "boolean" - } - }, - "type": "object" - }, "ListLength": { "oneOf": [ { @@ -3845,12 +3845,12 @@ expression: "&schema" "description": "#/definitions/MetricsCommon" }, "otlp": { - "$ref": "#/definitions/Config9", - "description": "#/definitions/Config9" - }, - "prometheus": { "$ref": "#/definitions/Config10", "description": "#/definitions/Config10" + }, + "prometheus": { + "$ref": "#/definitions/Config11", + "description": "#/definitions/Config11" } }, "type": "object" @@ -4119,8 +4119,8 @@ expression: "&schema" "additionalProperties": false, "properties": { "experimental.broken": { - "$ref": "#/definitions/Config", - "description": "#/definitions/Config" + "$ref": "#/definitions/Config2", + "description": "#/definitions/Config2" }, "experimental.expose_query_plan": { "$ref": "#/definitions/ExposeQueryPlanConfig", @@ -4131,8 +4131,8 @@ expression: "&schema" "description": "#/definitions/RecordConfig" }, "experimental.restricted": { - "$ref": "#/definitions/Config2", - "description": "#/definitions/Config2" + "$ref": "#/definitions/Config3", + "description": "#/definitions/Config3" }, "test.always_fails_to_start": { "$ref": "#/definitions/Conf", @@ -7150,28 +7150,28 @@ expression: "&schema" "description": "#/definitions/TracingCommon" }, "datadog": { - "$ref": "#/definitions/Config13", - "description": "#/definitions/Config13" + "$ref": "#/definitions/Config14", + "description": "#/definitions/Config14" }, "experimental_response_trace_id": { "$ref": "#/definitions/ExposeTraceId", "description": "#/definitions/ExposeTraceId" }, "jaeger": { - "$ref": "#/definitions/Config11", - "description": "#/definitions/Config11" + "$ref": "#/definitions/Config12", + "description": "#/definitions/Config12" }, "otlp": { - "$ref": "#/definitions/Config9", - "description": "#/definitions/Config9" + "$ref": "#/definitions/Config10", + "description": "#/definitions/Config10" }, "propagation": { "$ref": "#/definitions/Propagation", "description": "#/definitions/Propagation" }, "zipkin": { - "$ref": "#/definitions/Config12", - "description": "#/definitions/Config12" + "$ref": "#/definitions/Config13", + "description": "#/definitions/Config13" } }, "type": "object" @@ -8259,8 +8259,8 @@ expression: "&schema" "description": "#/definitions/ForbidMutationsConfig" }, "headers": { - "$ref": "#/definitions/Config4", - "description": "#/definitions/Config4" + "$ref": "#/definitions/Config5", + "description": "#/definitions/Config5" }, "health_check": { "$ref": "#/definitions/HealthCheck", @@ -8271,12 +8271,12 @@ expression: "&schema" "description": "#/definitions/Homepage" }, "include_subgraph_errors": { - "$ref": "#/definitions/Config5", - "description": "#/definitions/Config5" + "$ref": "#/definitions/Config6", + "description": "#/definitions/Config6" }, "limits": { - "$ref": "#/definitions/Limits", - "description": "#/definitions/Limits" + "$ref": "#/definitions/Config", + "description": "#/definitions/Config" }, "override_subgraph_url": { "$ref": "#/definitions/Conf5", @@ -8295,16 +8295,16 @@ expression: "&schema" "description": "#/definitions/DemandControlConfig" }, "preview_entity_cache": { - "$ref": "#/definitions/Config6", - "description": "#/definitions/Config6" + "$ref": "#/definitions/Config7", + "description": "#/definitions/Config7" }, "preview_file_uploads": { "$ref": "#/definitions/FileUploadsConfig", "description": "#/definitions/FileUploadsConfig" }, "progressive_override": { - "$ref": "#/definitions/Config7", - "description": "#/definitions/Config7" + "$ref": "#/definitions/Config8", + "description": "#/definitions/Config8" }, "rhai": { "$ref": "#/definitions/Conf6", @@ -8331,8 +8331,8 @@ expression: "&schema" "description": "#/definitions/Tls" }, "traffic_shaping": { - "$ref": "#/definitions/Config14", - "description": "#/definitions/Config14" + "$ref": "#/definitions/Config15", + "description": "#/definitions/Config15" } }, "title": "Configuration", diff --git a/apollo-router/src/plugins/limits/fixtures/content_length_limit.router.yaml b/apollo-router/src/plugins/limits/fixtures/content_length_limit.router.yaml new file mode 100644 index 0000000000..e95015cbdd --- /dev/null +++ b/apollo-router/src/plugins/limits/fixtures/content_length_limit.router.yaml @@ -0,0 +1,2 @@ +limits: + http_max_request_bytes: 10 \ No newline at end of file diff --git a/apollo-router/src/plugins/limits/layer.rs b/apollo-router/src/plugins/limits/layer.rs new file mode 100644 index 0000000000..680a95c41b --- /dev/null +++ b/apollo-router/src/plugins/limits/layer.rs @@ -0,0 +1,376 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::AtomicUsize; +use std::sync::Arc; +use std::task::Poll; + +use displaydoc::Display; +use futures::FutureExt; +use pin_project_lite::pin_project; +use tokio::sync::AcquireError; +use tokio::sync::OwnedSemaphorePermit; +use tower::Layer; +use tower_service::Service; + +#[derive(thiserror::Error, Debug, Display)] +pub(super) enum BodyLimitError { + /// Request body payload too large + PayloadTooLarge, +} + +struct BodyLimitControlInner { + limit: AtomicUsize, + current: AtomicUsize, +} + +/// This structure allows the body limit to be updated dynamically. +/// It also allows the error message to be updated +#[derive(Clone)] +pub(crate) struct BodyLimitControl { + inner: Arc, +} + +impl BodyLimitControl { + pub(crate) fn new(limit: usize) -> Self { + Self { + inner: Arc::new(BodyLimitControlInner { + limit: AtomicUsize::new(limit), + current: AtomicUsize::new(0), + }), + } + } + + /// To disable the limit check just set this to usize::MAX + pub(crate) fn update_limit(&self, limit: usize) { + self.inner + .limit + .store(limit, std::sync::atomic::Ordering::SeqCst); + } + + /// Returns the current limit, this may have been updated dynamically. + /// Usually it is the minimum of the content-length header and the configured limit. + pub(crate) fn limit(&self) -> usize { + self.inner.limit.load(std::sync::atomic::Ordering::SeqCst) + } + + /// Returns how much is remaining before the limit is hit + pub(crate) fn remaining(&self) -> usize { + self.inner.limit.load(std::sync::atomic::Ordering::SeqCst) + - self.inner.current.load(std::sync::atomic::Ordering::SeqCst) + } + + /// Increment the current counted bytes by an amount + pub(crate) fn increment(&self, amount: usize) -> usize { + self.inner + .current + .fetch_add(amount, std::sync::atomic::Ordering::SeqCst) + } +} + +/// This layer differs from the tower version in that it will always generate an error eagerly rather than +/// allowing the downstream service to catch and handle the error. +/// This way we can guarantee that the correct error will be returned to the client. +/// +/// The layer that precedes this one is responsible for handling the error and returning the correct response. +/// It will ALWAYS be able to downcast the error to the correct type. +/// +pub(crate) struct RequestBodyLimitLayer { + _phantom: std::marker::PhantomData, + control: BodyLimitControl, +} +impl RequestBodyLimitLayer { + pub(crate) fn new(control: BodyLimitControl) -> Self { + Self { + _phantom: Default::default(), + control, + } + } +} + +impl Layer for RequestBodyLimitLayer +where + S: Service>>, + Body: http_body::Body, +{ + type Service = RequestBodyLimit; + + fn layer(&self, inner: S) -> Self::Service { + RequestBodyLimit::new(inner, self.control.clone()) + } +} + +pub(crate) struct RequestBodyLimit { + _phantom: std::marker::PhantomData, + inner: S, + control: BodyLimitControl, +} + +impl RequestBodyLimit +where + S: Service>>, + Body: http_body::Body, +{ + fn new(inner: S, control: BodyLimitControl) -> Self { + Self { + _phantom: Default::default(), + inner, + control, + } + } +} + +impl Service> for RequestBodyLimit +where + S: Service< + http::Request>, + Response = http::Response, + >, + ReqBody: http_body::Body, + RespBody: http_body::Body, + S::Error: From, +{ + type Response = S::Response; + type Error = S::Error; + type Future = ResponseFuture; + + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: http::Request) -> Self::Future { + let content_length = req + .headers() + .get(http::header::CONTENT_LENGTH) + .and_then(|value| value.to_str().ok()?.parse::().ok()); + + let _body_limit = match content_length { + Some(len) if len > self.control.limit() => return ResponseFuture::Reject, + Some(len) => self.control.limit().min(len), + None => self.control.limit(), + }; + + // TODO: We can only do this once this layer is moved to the beginning of the router pipeline. + // Otherwise the context length will be checked against the decompressed size of the body. + // self.control.update_limit(_body_limit); + + // This mutex allows us to signal the body stream to stop processing if the limit is hit. + let abort = Arc::new(tokio::sync::Semaphore::new(1)); + + // This will be dropped if the body stream hits the limit signalling an immediate response. + let owned_permit = abort + .clone() + .try_acquire_owned() + .expect("abort lock is new, qed"); + + let f = + self.inner.call(req.map(|body| { + super::limited::Limited::new(body, self.control.clone(), owned_permit) + })); + + ResponseFuture::Continue { + inner: f, + abort: abort.acquire_owned().boxed(), + } + } +} + +pin_project! { + #[project = ResponseFutureProj] + pub (crate) enum ResponseFuture { + Reject, + Continue { + #[pin] + inner: F, + + #[pin] + abort: futures::future::BoxFuture<'static, Result>, + } + } +} + +impl Future for ResponseFuture +where + Inner: Future, Error>>, + Body: http_body::Body, + Error: From, +{ + type Output = Result, Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let project = self.project(); + match project { + // Content-length header exceeded, eager reject + ResponseFutureProj::Reject => Poll::Ready(Err(BodyLimitError::PayloadTooLarge.into())), + // Continue processing the request + ResponseFutureProj::Continue { inner, abort, .. } => { + match inner.poll(cx) { + Poll::Ready(r) => Poll::Ready(r), + Poll::Pending => { + // Check to see if the stream limit has been hit + match abort.poll(cx) { + Poll::Ready(_) => { + Poll::Ready(Err(BodyLimitError::PayloadTooLarge.into())) + } + Poll::Pending => Poll::Pending, + } + } + } + } + } + } +} + +#[cfg(test)] +mod test { + use futures::stream::StreamExt; + use http::StatusCode; + use tower::BoxError; + use tower::ServiceBuilder; + use tower_service::Service; + + use crate::plugins::limits::layer::BodyLimitControl; + use crate::plugins::limits::layer::RequestBodyLimitLayer; + use crate::services; + + #[tokio::test] + async fn test_body_content_length_limit_exceeded() { + let control = BodyLimitControl::new(10); + let mut service = ServiceBuilder::new() + .layer(RequestBodyLimitLayer::new(control.clone())) + .service_fn(|r: http::Request<_>| async move { + services::http::body_stream::BodyStream::new(r.into_body()) + .collect::>() + .await; + panic!("should have rejected request"); + }); + let resp: Result, BoxError> = service + .call(http::Request::new("This is a test".to_string())) + .await; + assert!(resp.is_err()); + } + + #[tokio::test] + async fn test_body_content_length_limit_ok() { + let control = BodyLimitControl::new(10); + let mut service = ServiceBuilder::new() + .layer(RequestBodyLimitLayer::new(control.clone())) + .service_fn(|r: http::Request<_>| async move { + services::http::body_stream::BodyStream::new(r.into_body()) + .collect::>() + .await; + Ok(http::Response::builder() + .status(StatusCode::OK) + .body("This is a test".to_string()) + .unwrap()) + }); + let resp: Result<_, BoxError> = service.call(http::Request::new("OK".to_string())).await; + + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.into_body(), "This is a test"); + } + + #[tokio::test] + async fn test_header_content_length_limit_exceeded() { + let control = BodyLimitControl::new(10); + let mut service = ServiceBuilder::new() + .layer(RequestBodyLimitLayer::new(control.clone())) + .service_fn(|r: http::Request<_>| async move { + services::http::body_stream::BodyStream::new(r.into_body()) + .collect::>() + .await; + panic!("should have rejected request"); + }); + let resp: Result, BoxError> = service + .call( + http::Request::builder() + .header("Content-Length", "100") + .body("This is a test".to_string()) + .unwrap(), + ) + .await; + assert!(resp.is_err()); + } + + #[tokio::test] + async fn test_header_content_length_limit_ok() { + let control = BodyLimitControl::new(10); + let mut service = ServiceBuilder::new() + .layer(RequestBodyLimitLayer::new(control.clone())) + .service_fn(|r: http::Request<_>| async move { + services::http::body_stream::BodyStream::new(r.into_body()) + .collect::>() + .await; + Ok(http::Response::builder() + .status(StatusCode::OK) + .body("This is a test".to_string()) + .unwrap()) + }); + let resp: Result<_, BoxError> = service + .call( + http::Request::builder() + .header("Content-Length", "5") + .body("OK".to_string()) + .unwrap(), + ) + .await; + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.into_body(), "This is a test"); + } + + #[tokio::test] + async fn test_limits_dynamic_update() { + let control = BodyLimitControl::new(10); + let mut service = ServiceBuilder::new() + .layer(RequestBodyLimitLayer::new(control.clone())) + .service_fn(move |r: http::Request<_>| { + let control = control.clone(); + async move { + services::http::body_stream::BodyStream::new(r.into_body()) + .collect::>() + .await; + control.update_limit(100); + Ok(http::Response::builder() + .status(StatusCode::OK) + .body("This is a test".to_string()) + .unwrap()) + } + }); + let resp: Result<_, BoxError> = service + .call(http::Request::new("This is a test".to_string())) + .await; + assert!(resp.is_err()); + } + + #[tokio::test] + async fn test_body_length_exceeds_content_length() { + let control = BodyLimitControl::new(10); + let mut service = ServiceBuilder::new() + .layer(RequestBodyLimitLayer::new(control.clone())) + .service_fn(|r: http::Request<_>| async move { + services::http::body_stream::BodyStream::new(r.into_body()) + .collect::>() + .await; + Ok(http::Response::builder() + .status(StatusCode::OK) + .body("This is a test".to_string()) + .unwrap()) + }); + let resp: Result<_, BoxError> = service + .call( + http::Request::builder() + .header("Content-Length", "5") + .body("Exceeded".to_string()) + .unwrap(), + ) + .await; + assert!(resp.is_ok()); + //TODO this needs to to fail once the limit layer is moved before decompression. + let resp = resp.unwrap(); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!(resp.into_body(), "This is a test"); + } +} diff --git a/apollo-router/src/plugins/limits/limited.rs b/apollo-router/src/plugins/limits/limited.rs new file mode 100644 index 0000000000..54a632d93e --- /dev/null +++ b/apollo-router/src/plugins/limits/limited.rs @@ -0,0 +1,212 @@ +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; + +use bytes::Buf; +use http::HeaderMap; +use http_body::SizeHint; +use pin_project_lite::pin_project; +use tokio::sync::OwnedSemaphorePermit; + +use crate::plugins::limits::layer::BodyLimitControl; + +pin_project! { + /// An implementation of http_body::Body that limits the number of bytes read from the inner body. + /// Unlike the `RequestBodyLimit` middleware, this will always return Pending if the inner body has exceeded the limit. + /// Upon reaching the limit the guard will be dropped allowing the RequestBodyLimitLayer to return. + pub(crate) struct Limited { + #[pin] + inner: Body, + #[pin] + permit: ForgetfulPermit, + control: BodyLimitControl, + } +} + +impl Limited +where + Body: http_body::Body, +{ + pub(super) fn new( + inner: Body, + control: BodyLimitControl, + permit: OwnedSemaphorePermit, + ) -> Self { + Self { + inner, + control, + permit: permit.into(), + } + } +} + +struct ForgetfulPermit(Option); + +impl ForgetfulPermit { + fn release(&mut self) { + self.0.take(); + } +} + +impl Drop for ForgetfulPermit { + fn drop(&mut self) { + // If the limit was not hit we must not release the guard otherwise a response of 413 will be returned. + // This may be because the inner body was not fully read. + // Instead we must forget the permit. + if let Some(permit) = self.0.take() { + permit.forget(); + } + } +} + +impl From for ForgetfulPermit { + fn from(permit: OwnedSemaphorePermit) -> Self { + Self(Some(permit)) + } +} + +impl http_body::Body for Limited +where + Body: http_body::Body, +{ + type Data = Body::Data; + type Error = Body::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut this = self.project(); + let res = match this.inner.poll_data(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(None) => None, + Poll::Ready(Some(Ok(data))) => { + if data.remaining() > this.control.remaining() { + // This is the difference between http_body::Limited and our implementation. + // Dropping this mutex allows the containing layer to immediately return an error response + // This prevents the need to deal with wrapped errors. + this.control.update_limit(0); + this.permit.release(); + return Poll::Pending; + } else { + this.control.increment(data.remaining()); + Some(Ok(data)) + } + } + Poll::Ready(Some(Err(err))) => Some(Err(err)), + }; + + Poll::Ready(res) + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + let this = self.project(); + let res = match this.inner.poll_trailers(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(data)) => Ok(data), + Poll::Ready(Err(err)) => Err(err), + }; + + Poll::Ready(res) + } + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + fn size_hint(&self) -> SizeHint { + match u64::try_from(self.control.remaining()) { + Ok(n) => { + let mut hint = self.inner.size_hint(); + if hint.lower() >= n { + hint.set_exact(n) + } else if let Some(max) = hint.upper() { + hint.set_upper(n.min(max)) + } else { + hint.set_upper(n) + } + hint + } + Err(_) => self.inner.size_hint(), + } + } +} + +#[cfg(test)] +mod test { + use std::pin::Pin; + use std::sync::Arc; + + use http_body::Body; + use tower::BoxError; + + use crate::plugins::limits::layer::BodyLimitControl; + + #[test] + fn test_completes() { + let control = BodyLimitControl::new(100); + let semaphore = Arc::new(tokio::sync::Semaphore::new(1)); + let lock = semaphore.clone().try_acquire_owned().unwrap(); + let mut limited = super::Limited::new("test".to_string(), control, lock); + + assert_eq!( + Pin::new(&mut limited).poll_data(&mut std::task::Context::from_waker( + &futures::task::noop_waker() + )), + std::task::Poll::Ready(Some(Ok("test".to_string().into_bytes().into()))) + ); + assert!(semaphore.try_acquire().is_err()); + + // We need to assert that if the stream is dropped the semaphore isn't released. + // It's only explicitly hitting the limit that releases the semaphore. + drop(limited); + assert!(semaphore.try_acquire().is_err()); + } + + #[test] + fn test_limit_hit() { + let control = BodyLimitControl::new(1); + let semaphore = Arc::new(tokio::sync::Semaphore::new(1)); + let lock = semaphore.clone().try_acquire_owned().unwrap(); + let mut limited = super::Limited::new("test".to_string(), control, lock); + + assert_eq!( + Pin::new(&mut limited).poll_data(&mut std::task::Context::from_waker( + &futures::task::noop_waker() + )), + std::task::Poll::Pending + ); + assert!(semaphore.try_acquire().is_ok()) + } + + #[test] + fn test_limit_hit_after_multiple() { + let control = BodyLimitControl::new(5); + let semaphore = Arc::new(tokio::sync::Semaphore::new(1)); + let lock = semaphore.clone().try_acquire_owned().unwrap(); + + let mut limited = super::Limited::new( + hyper::Body::wrap_stream(futures::stream::iter(vec![ + Ok::<&str, BoxError>("hello"), + Ok("world"), + ])), + control, + lock, + ); + assert!(matches!( + Pin::new(&mut limited).poll_data(&mut std::task::Context::from_waker( + &futures::task::noop_waker() + )), + std::task::Poll::Ready(Some(Ok(_))) + )); + assert!(semaphore.try_acquire().is_err()); + assert!(matches!( + Pin::new(&mut limited).poll_data(&mut std::task::Context::from_waker( + &futures::task::noop_waker() + )), + std::task::Poll::Pending + )); + assert!(semaphore.try_acquire().is_ok()); + } +} diff --git a/apollo-router/src/plugins/limits/mod.rs b/apollo-router/src/plugins/limits/mod.rs new file mode 100644 index 0000000000..ea743c6d2b --- /dev/null +++ b/apollo-router/src/plugins/limits/mod.rs @@ -0,0 +1,434 @@ +mod layer; +mod limited; + +use std::error::Error; + +use async_trait::async_trait; +use http::StatusCode; +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use tower::BoxError; +use tower::ServiceBuilder; +use tower::ServiceExt; + +use crate::graphql; +use crate::layers::ServiceBuilderExt; +use crate::plugin::Plugin; +use crate::plugin::PluginInit; +use crate::plugins::limits::layer::BodyLimitControl; +use crate::plugins::limits::layer::BodyLimitError; +use crate::plugins::limits::layer::RequestBodyLimitLayer; +use crate::services::router; +use crate::services::router::BoxService; +use crate::Context; + +/// Configuration for operation limits, parser limits, HTTP limits, etc. +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, default)] +pub(crate) struct Config { + /// If set, requests with operations deeper than this maximum + /// are rejected with a HTTP 400 Bad Request response and GraphQL error with + /// `"extensions": {"code": "MAX_DEPTH_LIMIT"}` + /// + /// Counts depth of an operation, looking at its selection sets,˛ + /// including fields in fragments and inline fragments. The following + /// example has a depth of 3. + /// + /// ```graphql + /// query getProduct { + /// book { # 1 + /// ...bookDetails + /// } + /// } + /// + /// fragment bookDetails on Book { + /// details { # 2 + /// ... on ProductDetailsBook { + /// country # 3 + /// } + /// } + /// } + /// ``` + pub(crate) max_depth: Option, + + /// If set, requests with operations higher than this maximum + /// are rejected with a HTTP 400 Bad Request response and GraphQL error with + /// `"extensions": {"code": "MAX_DEPTH_LIMIT"}` + /// + /// Height is based on simple merging of fields using the same name or alias, + /// but only within the same selection set. + /// For example `name` here is only counted once and the query has height 3, not 4: + /// + /// ```graphql + /// query { + /// name { first } + /// name { last } + /// } + /// ``` + /// + /// This may change in a future version of Apollo Router to do + /// [full field merging across fragments][merging] instead. + /// + /// [merging]: https://spec.graphql.org/October2021/#sec-Field-Selection-Merging] + pub(crate) max_height: Option, + + /// If set, requests with operations with more root fields than this maximum + /// are rejected with a HTTP 400 Bad Request response and GraphQL error with + /// `"extensions": {"code": "MAX_ROOT_FIELDS_LIMIT"}` + /// + /// This limit counts only the top level fields in a selection set, + /// including fragments and inline fragments. + pub(crate) max_root_fields: Option, + + /// If set, requests with operations with more aliases than this maximum + /// are rejected with a HTTP 400 Bad Request response and GraphQL error with + /// `"extensions": {"code": "MAX_ALIASES_LIMIT"}` + pub(crate) max_aliases: Option, + + /// If set to true (which is the default is dev mode), + /// requests that exceed a `max_*` limit are *not* rejected. + /// Instead they are executed normally, and a warning is logged. + pub(crate) warn_only: bool, + + /// Limit recursion in the GraphQL parser to protect against stack overflow. + /// default: 500 + pub(crate) parser_max_recursion: usize, + + /// Limit the number of tokens the GraphQL parser processes before aborting. + pub(crate) parser_max_tokens: usize, + + /// Limit the size of incoming HTTP requests read from the network, + /// to protect against running out of memory. Default: 2000000 (2 MB) + pub(crate) http_max_request_bytes: usize, +} + +impl Default for Config { + fn default() -> Self { + Self { + // These limits are opt-in + max_depth: None, + max_height: None, + max_root_fields: None, + max_aliases: None, + warn_only: false, + http_max_request_bytes: 2_000_000, + parser_max_tokens: 15_000, + + // This is `apollo-parser`’s default, which protects against stack overflow + // but is still very high for "reasonable" queries. + // https://github.com/apollographql/apollo-rs/blob/apollo-parser%400.7.3/crates/apollo-parser/src/parser/mod.rs#L93-L104 + parser_max_recursion: 500, + } + } +} + +struct LimitsPlugin { + config: Config, +} + +#[async_trait] +impl Plugin for LimitsPlugin { + type Config = Config; + + async fn new(init: PluginInit) -> Result + where + Self: Sized, + { + Ok(LimitsPlugin { + config: init.config, + }) + } + + fn router_service(&self, service: BoxService) -> BoxService { + let control = BodyLimitControl::new(self.config.http_max_request_bytes); + let control_for_context = control.clone(); + ServiceBuilder::new() + .map_request(move |r: router::Request| { + let control_for_context = control_for_context.clone(); + r.context + .extensions() + .with_lock(|mut lock| lock.insert(control_for_context)); + r + }) + .map_future_with_request_data( + |r: &router::Request| r.context.clone(), + |ctx, f| async { Self::map_error_to_graphql(f.await, ctx) }, + ) + // Here we need to convert to and from the underlying http request types so that we can use existing middleware. + .map_request(Into::into) + .map_response(Into::into) + .layer(RequestBodyLimitLayer::new(control)) + .map_request(Into::into) + .map_response(Into::into) + .service(service) + .boxed() + } +} + +impl LimitsPlugin { + fn map_error_to_graphql( + resp: Result, + ctx: Context, + ) -> Result { + // There are two ways we can get a payload too large error: + // 1. The request body is too large and detected via content length header + // 2. The request body is and it failed at some other point in the pipeline. + // We expect that other pipeline errors will have wrapped the source error rather than throwing it away. + match resp { + Ok(r) => { + if r.response.status() == StatusCode::PAYLOAD_TOO_LARGE { + Self::increment_legacy_metric(); + Ok(BodyLimitError::PayloadTooLarge.into_response(ctx)) + } else { + Ok(r) + } + } + Err(e) => { + // Getting the root cause is a bit fiddly + let mut root_cause: &dyn Error = e.as_ref(); + while let Some(cause) = root_cause.source() { + root_cause = cause; + } + + match root_cause.downcast_ref::() { + None => Err(e), + Some(_) => { + Self::increment_legacy_metric(); + Ok(BodyLimitError::PayloadTooLarge.into_response(ctx)) + } + } + } + } + } + + fn increment_legacy_metric() { + // Remove this eventually + // This is already handled by the telemetry plugin via the http.server.request metric. + u64_counter!( + "apollo_router_http_requests_total", + "Total number of HTTP requests made.", + 1, + status = StatusCode::PAYLOAD_TOO_LARGE.as_u16() as i64, + error = BodyLimitError::PayloadTooLarge.to_string() + ); + } +} + +impl BodyLimitError { + fn into_response(self, ctx: Context) -> router::Response { + match self { + BodyLimitError::PayloadTooLarge => router::Response::error_builder() + .error( + graphql::Error::builder() + .message(self.to_string()) + .extension_code("INVALID_GRAPHQL_REQUEST") + .extension("details", self.to_string()) + .build(), + ) + .status_code(StatusCode::PAYLOAD_TOO_LARGE) + .context(ctx) + .build() + .unwrap(), + } + } +} + +register_plugin!("apollo", "limits", LimitsPlugin); + +#[cfg(test)] +mod test { + use http::StatusCode; + use tower::BoxError; + + use crate::plugins::limits::layer::BodyLimitControl; + use crate::plugins::limits::LimitsPlugin; + use crate::plugins::test::PluginTestHarness; + use crate::services::router; + use crate::services::router::body::get_body_bytes; + + #[tokio::test] + async fn test_body_content_length_limit_exceeded() { + let plugin = plugin().await; + let resp = plugin + .call_router( + router::Request::fake_builder() + .body("This is a test") + .build() + .unwrap(), + |r| async { + let body = r.router_request.into_body(); + let _ = get_body_bytes(body).await?; + panic!("should have failed to read stream") + }, + ) + .await; + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + String::from_utf8( + get_body_bytes(resp.response.into_body()) + .await + .unwrap() + .to_vec() + ) + .unwrap(), + "{\"errors\":[{\"message\":\"Request body payload too large\",\"extensions\":{\"details\":\"Request body payload too large\",\"code\":\"INVALID_GRAPHQL_REQUEST\"}}]}" + ); + } + + #[tokio::test] + async fn test_body_content_length_limit_ok() { + let plugin = plugin().await; + let resp = plugin + .call_router( + router::Request::fake_builder().body("").build().unwrap(), + |r| async { + let body = r.router_request.into_body(); + let body = get_body_bytes(body).await; + assert!(body.is_ok()); + Ok(router::Response::fake_builder().build().unwrap()) + }, + ) + .await; + + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.response.status(), StatusCode::OK); + assert_eq!( + String::from_utf8( + get_body_bytes(resp.response.into_body()) + .await + .unwrap() + .to_vec() + ) + .unwrap(), + "{}" + ); + } + + #[tokio::test] + async fn test_header_content_length_limit_exceeded() { + let plugin = plugin().await; + let resp = plugin + .call_router( + router::Request::fake_builder() + .header("Content-Length", "100") + .body("") + .build() + .unwrap(), + |_| async { panic!("should have rejected request") }, + ) + .await; + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.response.status(), StatusCode::PAYLOAD_TOO_LARGE); + assert_eq!( + String::from_utf8( + get_body_bytes(resp.response.into_body()) + .await + .unwrap() + .to_vec() + ) + .unwrap(), + "{\"errors\":[{\"message\":\"Request body payload too large\",\"extensions\":{\"details\":\"Request body payload too large\",\"code\":\"INVALID_GRAPHQL_REQUEST\"}}]}" + ); + } + + #[tokio::test] + async fn test_header_content_length_limit_ok() { + let plugin = plugin().await; + let resp = plugin + .call_router( + router::Request::fake_builder() + .header("Content-Length", "5") + .body("") + .build() + .unwrap(), + |_| async { Ok(router::Response::fake_builder().build().unwrap()) }, + ) + .await; + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.response.status(), StatusCode::OK); + assert_eq!( + String::from_utf8( + get_body_bytes(resp.response.into_body()) + .await + .unwrap() + .to_vec() + ) + .unwrap(), + "{}" + ); + } + + #[tokio::test] + async fn test_non_limit_error_passthrough() { + // We should not be translating errors that are not limit errors into graphql errors + let plugin = plugin().await; + let resp = plugin + .call_router( + router::Request::fake_builder().body("").build().unwrap(), + |_| async { Err(BoxError::from("error")) }, + ) + .await; + assert!(resp.is_err()); + } + + #[tokio::test] + async fn test_limits_dynamic_update() { + let plugin = plugin().await; + let resp = plugin + .call_router( + router::Request::fake_builder() + .body("This is a test") + .build() + .unwrap(), + |r| async move { + // Before we go for the body, we'll update the limit + r.context.extensions().with_lock(|lock| { + let control: &BodyLimitControl = + lock.get().expect("mut have body limit control"); + assert_eq!(control.remaining(), 10); + assert_eq!(control.limit(), 10); + control.update_limit(100); + }); + let body = r.router_request.into_body(); + let _ = get_body_bytes(body).await?; + + // Now let's check progress + r.context.extensions().with_lock(|lock| { + let control: &BodyLimitControl = + lock.get().expect("mut have body limit control"); + assert_eq!(control.remaining(), 86); + }); + Ok(router::Response::fake_builder().build().unwrap()) + }, + ) + .await; + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert_eq!(resp.response.status(), StatusCode::OK); + assert_eq!( + String::from_utf8( + get_body_bytes(resp.response.into_body()) + .await + .unwrap() + .to_vec() + ) + .unwrap(), + "{}" + ); + } + + async fn plugin() -> PluginTestHarness { + let plugin: PluginTestHarness = PluginTestHarness::new( + Some(include_str!("fixtures/content_length_limit.router.yaml")), + None, + ) + .await; + plugin + } +} diff --git a/apollo-router/src/plugins/mod.rs b/apollo-router/src/plugins/mod.rs index 1b8c60cec5..beac8037b9 100644 --- a/apollo-router/src/plugins/mod.rs +++ b/apollo-router/src/plugins/mod.rs @@ -31,6 +31,7 @@ pub(crate) mod file_uploads; mod forbid_mutations; mod headers; mod include_subgraph_errors; +pub(crate) mod limits; pub(crate) mod override_url; pub(crate) mod progressive_override; mod record_replay; diff --git a/apollo-router/src/plugins/telemetry/config_new/events.rs b/apollo-router/src/plugins/telemetry/config_new/events.rs index c68dea7f0d..7f957ea7e6 100644 --- a/apollo-router/src/plugins/telemetry/config_new/events.rs +++ b/apollo-router/src/plugins/telemetry/config_new/events.rs @@ -756,14 +756,14 @@ mod tests { .header("x-log-request", HeaderValue::from_static("log")) .build() .unwrap(), - |_r| { - router::Response::fake_builder() + |_r|async { + Ok(router::Response::fake_builder() .header("custom-header", "val1") .header(CONTENT_LENGTH, "25") .header("x-log-request", HeaderValue::from_static("log")) .data(serde_json_bytes::json!({"data": "res"})) .build() - .expect("expecting valid response") + .expect("expecting valid response")) }, ) .await @@ -790,17 +790,17 @@ mod tests { .header("custom-header", "val1") .build() .unwrap(), - |_r| { + |_r| async { let context_with_error = Context::new(); let _ = context_with_error .insert(CONTAINS_GRAPHQL_ERROR, true) .unwrap(); - router::Response::fake_builder() + Ok(router::Response::fake_builder() .header("custom-header", "val1") .context(context_with_error) .data(serde_json_bytes::json!({"errors": [{"message": "res"}]})) .build() - .expect("expecting valid response") + .expect("expecting valid response")) }, ) .await @@ -827,14 +827,14 @@ mod tests { .header("custom-header", "val1") .build() .unwrap(), - |_r| { - router::Response::fake_builder() + |_r| async { + Ok(router::Response::fake_builder() .header("custom-header", "val1") .header(CONTENT_LENGTH, "25") .header("x-log-response", HeaderValue::from_static("log")) .data(serde_json_bytes::json!({"data": "res"})) .build() - .expect("expecting valid response") + .expect("expecting valid response")) }, ) .await diff --git a/apollo-router/src/plugins/telemetry/logging/mod.rs b/apollo-router/src/plugins/telemetry/logging/mod.rs index eb8175cb19..83cf680c9b 100644 --- a/apollo-router/src/plugins/telemetry/logging/mod.rs +++ b/apollo-router/src/plugins/telemetry/logging/mod.rs @@ -22,13 +22,13 @@ mod test { .body("query { foo }") .build() .expect("expecting valid request"), - |_r| { + |_r| async { tracing::info!("response"); - router::Response::fake_builder() + Ok(router::Response::fake_builder() .header("custom-header", "val1") .data(serde_json::json!({"data": "res"})) .build() - .expect("expecting valid response") + .expect("expecting valid response")) }, ) .await diff --git a/apollo-router/src/plugins/test.rs b/apollo-router/src/plugins/test.rs index c31ae9acc7..ea155f0cd8 100644 --- a/apollo-router/src/plugins/test.rs +++ b/apollo-router/src/plugins/test.rs @@ -1,4 +1,5 @@ use std::any::TypeId; +use std::future::Future; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -124,14 +125,17 @@ impl PluginTestHarness { } #[allow(dead_code)] - pub(crate) async fn call_router( + pub(crate) async fn call_router( &self, request: router::Request, - response_fn: fn(router::Request) -> router::Response, - ) -> Result { + response_fn: fn(router::Request) -> F, + ) -> Result + where + F: Future> + Send + 'static, + { let service: router::BoxService = router::BoxService::new( ServiceBuilder::new() - .service_fn(move |req: router::Request| async move { Ok((response_fn)(req)) }), + .service_fn(move |req: router::Request| async move { (response_fn)(req).await }), ); self.plugin.router_service(service).call(request).await diff --git a/apollo-router/src/router_factory.rs b/apollo-router/src/router_factory.rs index ca1dd4cb7b..fe6dca8fb8 100644 --- a/apollo-router/src/router_factory.rs +++ b/apollo-router/src/router_factory.rs @@ -682,6 +682,7 @@ pub(crate) async fn create_plugins( } } } + add_mandatory_apollo_plugin!("limits"); add_mandatory_apollo_plugin!("traffic_shaping"); add_optional_apollo_plugin!("forbid_mutations"); add_optional_apollo_plugin!("subscription"); diff --git a/apollo-router/src/services/http.rs b/apollo-router/src/services/http.rs index 980c20ce70..1579ea155f 100644 --- a/apollo-router/src/services/http.rs +++ b/apollo-router/src/services/http.rs @@ -9,6 +9,7 @@ use super::router::body::RouterBody; use super::Plugins; use crate::Context; +pub(crate) mod body_stream; pub(crate) mod service; #[cfg(test)] mod tests; diff --git a/apollo-router/src/services/http/body_stream.rs b/apollo-router/src/services/http/body_stream.rs new file mode 100644 index 0000000000..e75ed22c50 --- /dev/null +++ b/apollo-router/src/services/http/body_stream.rs @@ -0,0 +1,34 @@ +use std::task::Poll; + +use futures::Stream; +use pin_project_lite::pin_project; + +pin_project! { + /// Allows conversion between an http_body::Body and a futures stream. + pub(crate) struct BodyStream { + #[pin] + inner: B + } +} + +impl BodyStream { + /// Create a new `BodyStream`. + pub(crate) fn new(body: B) -> Self { + Self { inner: body } + } +} + +impl Stream for BodyStream +where + B: http_body::Body, + B::Error: Into, +{ + type Item = Result; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.project().inner.poll_data(cx) + } +} diff --git a/apollo-router/src/services/router.rs b/apollo-router/src/services/router.rs index d7d1a08120..cfd6e69c21 100644 --- a/apollo-router/src/services/router.rs +++ b/apollo-router/src/services/router.rs @@ -1,5 +1,8 @@ #![allow(missing_docs)] // FIXME +use std::any::Any; +use std::mem; + use bytes::Bytes; use futures::future::Either; use futures::Stream; @@ -24,6 +27,7 @@ use super::supergraph; use crate::graphql; use crate::http_ext::header_map; use crate::json_ext::Path; +use crate::services; use crate::services::TryIntoHeaderName; use crate::services::TryIntoHeaderValue; use crate::Context; @@ -53,15 +57,6 @@ pub struct Request { pub context: Context, } -impl From> for Request { - fn from(router_request: http::Request) -> Self { - Self { - router_request, - context: Context::new(), - } - } -} - impl From<(http::Request, Context)> for Request { fn from((router_request, context): (http::Request, Context)) -> Self { Self { @@ -187,15 +182,6 @@ pub struct Response { pub context: Context, } -impl From> for Response { - fn from(response: http::Response) -> Self { - Self { - response, - context: Context::new(), - } - } -} - #[buildstructor::buildstructor] impl Response { pub async fn next_response(&mut self) -> Option> { @@ -398,3 +384,128 @@ pub(crate) struct ClientRequestAccepts { pub(crate) json: bool, pub(crate) wildcard: bool, } + +impl From> for Response +where + T: http_body::Body + Send + 'static, + ::Error: Into, +{ + fn from(response: http::Response) -> Self { + let context: Context = response.extensions().get().cloned().unwrap_or_default(); + + Self { + response: response.map(convert_to_body), + context, + } + } +} + +impl From for http::Response { + fn from(mut response: Response) -> Self { + response.response.extensions_mut().insert(response.context); + response.response + } +} + +impl From> for Request +where + T: http_body::Body + Send + 'static, + + ::Error: Into, +{ + fn from(request: http::Request) -> Self { + let context: Context = request.extensions().get().cloned().unwrap_or_default(); + + Self { + router_request: request.map(convert_to_body), + context, + } + } +} + +impl From for http::Request { + fn from(mut request: Request) -> Self { + request + .router_request + .extensions_mut() + .insert(request.context); + request.router_request + } +} + +/// This function is used to convert a `http_body::Body` into a `Body`. +/// It does a downcast check to see if the body is already a `Body` and if it is then it just returns it. +/// There is zero overhead if the body is already a `Body`. +/// Note that ALL graphql responses are already a stream as they may be part of a deferred or stream response, +/// therefore if a body has to be wrapped the cost is minimal. +fn convert_to_body(mut b: T) -> Body +where + T: http_body::Body + Send + 'static, + + ::Error: Into, +{ + let val_any = &mut b as &mut dyn Any; + match val_any.downcast_mut::() { + Some(body) => mem::take(body), + None => Body::wrap_stream(services::http::body_stream::BodyStream::new( + b.map_err(Into::into), + )), + } +} + +#[cfg(test)] +mod test { + use std::pin::Pin; + use std::task::Context; + use std::task::Poll; + + use http::HeaderMap; + use tower::BoxError; + + use crate::services::router::body::get_body_bytes; + use crate::services::router::convert_to_body; + + struct MockBody { + data: Option<&'static str>, + } + impl http_body::Body for MockBody { + type Data = bytes::Bytes; + type Error = BoxError; + + fn poll_data( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if let Some(data) = self.data.take() { + Poll::Ready(Some(Ok(bytes::Bytes::from(data)))) + } else { + Poll::Ready(None) + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + Poll::Ready(Ok(None)) + } + } + + #[tokio::test] + async fn test_convert_from_http_body() { + let body = convert_to_body(MockBody { data: Some("test") }); + assert_eq!( + &String::from_utf8(get_body_bytes(body).await.unwrap().to_vec()).unwrap(), + "test" + ); + } + + #[tokio::test] + async fn test_convert_from_hyper_body() { + let body = convert_to_body(hyper::Body::from("test")); + assert_eq!( + &String::from_utf8(get_body_bytes(body).await.unwrap().to_vec()).unwrap(), + "test" + ); + } +} diff --git a/apollo-router/src/services/router/service.rs b/apollo-router/src/services/router/service.rs index 24cab0a387..f5013e5380 100644 --- a/apollo-router/src/services/router/service.rs +++ b/apollo-router/src/services/router/service.rs @@ -15,6 +15,7 @@ use futures::future::BoxFuture; use futures::stream; use futures::stream::once; use futures::stream::StreamExt; +use futures::TryFutureExt; use http::header::CONTENT_TYPE; use http::header::VARY; use http::request::Parts; @@ -33,6 +34,7 @@ use tower::ServiceExt; use tower_service::Service; use tracing::Instrument; +use super::Body; use super::ClientRequestAccepts; use crate::axum_factory::CanceledRequest; use crate::batching::Batch; @@ -94,7 +96,6 @@ pub(crate) struct RouterService { apq_layer: APQLayer, persisted_query_layer: Arc, query_analysis_layer: QueryAnalysisLayer, - http_max_request_bytes: usize, batching: Batching, } @@ -104,7 +105,6 @@ impl RouterService { apq_layer: APQLayer, persisted_query_layer: Arc, query_analysis_layer: QueryAnalysisLayer, - http_max_request_bytes: usize, batching: Batching, ) -> Self { RouterService { @@ -112,7 +112,6 @@ impl RouterService { apq_layer, persisted_query_layer, query_analysis_layer, - http_max_request_bytes, batching, } } @@ -408,9 +407,14 @@ impl RouterService { } async fn call_inner(&self, req: RouterRequest) -> Result { - let context = req.context.clone(); + let context = req.context; + let (parts, body) = req.router_request.into_parts(); + let requests = self.get_graphql_requests(&parts, body).await?; - let (supergraph_requests, is_batch) = match self.translate_request(req).await { + let (supergraph_requests, is_batch) = match futures::future::ready(requests) + .and_then(|r| self.translate_request(&context, parts, r)) + .await + { Ok(requests) => requests, Err(err) => { u64_counter!( @@ -647,67 +651,11 @@ impl RouterService { async fn translate_request( &self, - req: RouterRequest, + context: &Context, + parts: Parts, + graphql_requests: (Vec, bool), ) -> Result<(Vec, bool), TranslateError> { - let RouterRequest { - router_request, - context, - } = req; - - let (parts, body) = router_request.into_parts(); - - let graphql_requests: Result<(Vec, bool), TranslateError> = if parts - .method - == Method::GET - { - self.translate_query_request(&parts).await - } else { - // FIXME: use a try block when available: https://github.com/rust-lang/rust/issues/31436 - let content_length = (|| { - parts - .headers - .get(http::header::CONTENT_LENGTH)? - .to_str() - .ok()? - .parse() - .ok() - })(); - if content_length.unwrap_or(0) > self.http_max_request_bytes { - Err(TranslateError { - status: StatusCode::PAYLOAD_TOO_LARGE, - error: "payload too large for the `http_max_request_bytes` configuration", - extension_code: "INVALID_GRAPHQL_REQUEST", - extension_details: "payload too large".to_string(), - }) - } else { - let body = http_body::Limited::new(body, self.http_max_request_bytes); - get_body_bytes(body) - .instrument(tracing::debug_span!("receive_body")) - .await - .map_err(|e| { - if e.is::() { - TranslateError { - status: StatusCode::PAYLOAD_TOO_LARGE, - error: "payload too large for the `http_max_request_bytes` configuration", - extension_code: "INVALID_GRAPHQL_REQUEST", - extension_details: "payload too large".to_string(), - } - } else { - TranslateError { - status: StatusCode::BAD_REQUEST, - error: "failed to get the request body", - extension_code: "INVALID_GRAPHQL_REQUEST", - extension_details: format!("failed to get the request body: {e}"), - } - } - }) - .and_then(|bytes| { - self.translate_bytes_request(&bytes) - }) - } - }; - - let (ok_results, is_batch) = graphql_requests?; + let (ok_results, is_batch) = graphql_requests; let mut results = Vec::with_capacity(ok_results.len()); let batch_size = ok_results.len(); @@ -759,7 +707,7 @@ impl RouterService { *new.body_mut() = graphql_request; // XXX Lose some private entries, is that ok? let new_context = Context::new(); - new_context.extend(&context); + new_context.extend(context); let client_request_accepts_opt = context .extensions() .with_lock(|lock| lock.get::().cloned()); @@ -811,13 +759,30 @@ impl RouterService { 0, SupergraphRequest { supergraph_request: sg, - context, + context: context.clone(), }, ); Ok((results, is_batch)) } + async fn get_graphql_requests( + &self, + parts: &Parts, + body: Body, + ) -> Result, bool), TranslateError>, BoxError> { + let graphql_requests: Result<(Vec, bool), TranslateError> = + if parts.method == Method::GET { + self.translate_query_request(parts).await + } else { + let bytes = get_body_bytes(body) + .instrument(tracing::debug_span!("receive_body")) + .await?; + self.translate_bytes_request(&bytes) + }; + Ok(graphql_requests) + } + fn count_errors(errors: &[graphql::Error]) { let mut map = HashMap::new(); for error in errors { @@ -865,7 +830,6 @@ pub(crate) struct RouterCreator { apq_layer: APQLayer, pub(crate) persisted_query_layer: Arc, query_analysis_layer: QueryAnalysisLayer, - http_max_request_bytes: usize, batching: Batching, } @@ -915,7 +879,6 @@ impl RouterCreator { static_page, apq_layer, query_analysis_layer, - http_max_request_bytes: configuration.limits.http_max_request_bytes, persisted_query_layer, batching: configuration.batching.clone(), }) @@ -934,7 +897,6 @@ impl RouterCreator { self.apq_layer.clone(), self.persisted_query_layer.clone(), self.query_analysis_layer.clone(), - self.http_max_request_bytes, self.batching.clone(), )); diff --git a/apollo-router/src/services/router/tests.rs b/apollo-router/src/services/router/tests.rs index 77ea25ae4f..94caafd006 100644 --- a/apollo-router/src/services/router/tests.rs +++ b/apollo-router/src/services/router/tests.rs @@ -186,7 +186,7 @@ async fn it_fails_on_no_query() { #[tokio::test] async fn test_http_max_request_bytes() { - /// Size of the JSON serialization of the request created by `fn canned_new` + /// Size of the JSON serialization of the request created by `fn canned_new` /// in `apollo-router/src/services/supergraph.rs` const CANNED_REQUEST_LEN: usize = 391; diff --git a/apollo-router/tests/integration/coprocessor.rs b/apollo-router/tests/integration/coprocessor.rs index 02924763f5..213947eae4 100644 --- a/apollo-router/tests/integration/coprocessor.rs +++ b/apollo-router/tests/integration/coprocessor.rs @@ -1,5 +1,11 @@ use insta::assert_yaml_snapshot; +use serde_json::json; use tower::BoxError; +use wiremock::matchers::body_partial_json; +use wiremock::matchers::method; +use wiremock::matchers::path; +use wiremock::Mock; +use wiremock::ResponseTemplate; use crate::integration::common::graph_os_enabled; use crate::integration::IntegrationTest; @@ -24,3 +30,55 @@ async fn test_error_not_propagated_to_client() -> Result<(), BoxError> { router.graceful_shutdown().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_coprocessor_limit_payload() -> Result<(), BoxError> { + if !graph_os_enabled() { + return Ok(()); + } + + let mock_server = wiremock::MockServer::start().await; + let coprocessor_address = mock_server.uri(); + + // Expect a small query + Mock::given(method("POST")) + .and(path("/")) + .and(body_partial_json(json!({"version":1,"stage":"RouterRequest","control":"continue","body":"{\"query\":\"query {topProducts{name}}\",\"variables\":{}}","method":"POST"}))) + .respond_with( + ResponseTemplate::new(200).set_body_json(json!({"version":1,"stage":"RouterRequest","control":"continue","body":"{\"query\":\"query {topProducts{name}}\",\"variables\":{}}","method":"POST"})), + ) + .expect(1) + .mount(&mock_server) + .await; + + // Do not expect a large query + Mock::given(method("POST")) + .and(path("/")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({"version":1,"stage":"RouterRequest","control":"continue","body":"{\"query\":\"query {topProducts{name}}\",\"variables\":{}}","method":"POST"}))) + .expect(0) + .mount(&mock_server) + .await; + + let mut router = IntegrationTest::builder() + .config( + include_str!("fixtures/coprocessor_body_limit.router.yaml") + .replace("", &coprocessor_address), + ) + .build() + .await; + + router.start().await; + router.assert_started().await; + + // This query is small and should make it to the coprocessor + let (_trace_id, response) = router.execute_default_query().await; + assert_eq!(response.status(), 200); + + // This query is huge and will be rejected because it is too large before hitting the coprocessor + let (_trace_id, response) = router.execute_huge_query().await; + assert_eq!(response.status(), 413); + assert_yaml_snapshot!(response.text().await?); + + router.graceful_shutdown().await; + Ok(()) +} diff --git a/apollo-router/tests/integration/fixtures/coprocessor_body_limit.router.yaml b/apollo-router/tests/integration/fixtures/coprocessor_body_limit.router.yaml new file mode 100644 index 0000000000..c59346cea4 --- /dev/null +++ b/apollo-router/tests/integration/fixtures/coprocessor_body_limit.router.yaml @@ -0,0 +1,8 @@ +# This coprocessor doesn't point to anything +coprocessor: + url: "" + router: + request: + body: true +limits: + http_max_request_bytes: 100 \ No newline at end of file diff --git a/apollo-router/tests/integration/fixtures/request_bytes_limit.router.yaml b/apollo-router/tests/integration/fixtures/request_bytes_limit.router.yaml new file mode 100644 index 0000000000..035f1d4c17 --- /dev/null +++ b/apollo-router/tests/integration/fixtures/request_bytes_limit.router.yaml @@ -0,0 +1,7 @@ +limits: + http_max_request_bytes: 60 +coprocessor: + url: http://localhost:4005 + router: + request: + body: true \ No newline at end of file diff --git a/apollo-router/tests/integration/fixtures/request_bytes_limit_with_coprocessor.router.yaml b/apollo-router/tests/integration/fixtures/request_bytes_limit_with_coprocessor.router.yaml new file mode 100644 index 0000000000..035f1d4c17 --- /dev/null +++ b/apollo-router/tests/integration/fixtures/request_bytes_limit_with_coprocessor.router.yaml @@ -0,0 +1,7 @@ +limits: + http_max_request_bytes: 60 +coprocessor: + url: http://localhost:4005 + router: + request: + body: true \ No newline at end of file diff --git a/apollo-router/tests/integration/operation_limits.rs b/apollo-router/tests/integration/operation_limits.rs index 6476ca1869..79ad7d9f89 100644 --- a/apollo-router/tests/integration/operation_limits.rs +++ b/apollo-router/tests/integration/operation_limits.rs @@ -7,8 +7,11 @@ use apollo_router::services::execution; use apollo_router::services::supergraph; use apollo_router::TestHarness; use serde_json::json; +use tower::BoxError; use tower::ServiceExt; +use crate::integration::IntegrationTest; + #[tokio::test(flavor = "multi_thread")] async fn test_response_errors() { let (mut service, execution_count) = build_test_harness(json!({ @@ -296,3 +299,33 @@ fn expect_errors(response: graphql::Response, expected_error_codes: &[&str]) { assert!(response.data.is_none()) } } + +#[tokio::test(flavor = "multi_thread")] +async fn test_request_bytes_limit_with_coprocessor() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(include_str!( + "fixtures/request_bytes_limit_with_coprocessor.router.yaml" + )) + .build() + .await; + router.start().await; + router.assert_started().await; + let (_, resp) = router.execute_huge_query().await; + assert_eq!(resp.status(), 413); + router.graceful_shutdown().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_request_bytes_limit() -> Result<(), BoxError> { + let mut router = IntegrationTest::builder() + .config(include_str!("fixtures/request_bytes_limit.router.yaml")) + .build() + .await; + router.start().await; + router.assert_started().await; + let (_, resp) = router.execute_huge_query().await; + assert_eq!(resp.status(), 413); + router.graceful_shutdown().await; + Ok(()) +} diff --git a/apollo-router/tests/integration/redis.rs b/apollo-router/tests/integration/redis.rs index b7bfe2ea52..110b850857 100644 --- a/apollo-router/tests/integration/redis.rs +++ b/apollo-router/tests/integration/redis.rs @@ -26,7 +26,7 @@ async fn query_planner_cache() -> Result<(), BoxError> { // 2. run `docker compose up -d` and connect to the redis container by running `docker-compose exec redis /bin/bash`. // 3. Run the `redis-cli` command from the shell and start the redis `monitor` command. // 4. Run this test and yank the updated cache key from the redis logs. - let known_cache_key = "plan:0:v2.8.1:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3106dfc3339d8c3f3020434024bff0f566a8be5995199954db5a7525a7d7e67a"; + let known_cache_key = "plan:0:v2.8.5:16385ebef77959fcdc520ad507eb1f7f7df28f1d54a0569e3adabcb4cd00d7ce:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3106dfc3339d8c3f3020434024bff0f566a8be5995199954db5a7525a7d7e67a"; let config = RedisConfig::from_url("redis://127.0.0.1:6379").unwrap(); let client = RedisClient::new(config, None, None, None); @@ -909,7 +909,7 @@ async fn connection_failure_blocks_startup() { async fn query_planner_redis_update_query_fragments() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_query_fragments.router.yaml"), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9054d19854e1d9e282ac7645c612bc70b8a7143d43b73d44dade4a5ec43938b4", + "plan:0:v2.8.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:9054d19854e1d9e282ac7645c612bc70b8a7143d43b73d44dade4a5ec43938b4", ) .await; } @@ -928,7 +928,7 @@ async fn query_planner_redis_update_planner_mode() { async fn query_planner_redis_update_introspection() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_introspection.router.yaml"), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:04b3051125b5994fba6b0a22b2d8b4246cadc145be030c491a3431655d2ba07a", + "plan:0:v2.8.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:04b3051125b5994fba6b0a22b2d8b4246cadc145be030c491a3431655d2ba07a", ) .await; } @@ -937,7 +937,7 @@ async fn query_planner_redis_update_introspection() { async fn query_planner_redis_update_defer() { test_redis_query_plan_config_update( include_str!("fixtures/query_planner_redis_config_update_defer.router.yaml"), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3b7241b0db2cd878b79c0810121953ba544543f3cb2692aaf1a59184470747b0", + "plan:0:v2.8.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:3b7241b0db2cd878b79c0810121953ba544543f3cb2692aaf1a59184470747b0", ) .await; } @@ -948,7 +948,7 @@ async fn query_planner_redis_update_type_conditional_fetching() { include_str!( "fixtures/query_planner_redis_config_update_type_conditional_fetching.router.yaml" ), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:0ca695a8c4c448b65fa04229c663f44150af53b184ebdcbb0ad6862290efed76", + "plan:0:v2.8.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:0ca695a8c4c448b65fa04229c663f44150af53b184ebdcbb0ad6862290efed76", ) .await; } @@ -959,7 +959,7 @@ async fn query_planner_redis_update_reuse_query_fragments() { include_str!( "fixtures/query_planner_redis_config_update_reuse_query_fragments.router.yaml" ), - "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:f7c04319556397ec4b550aa5aaa96c73689cee09026b661b6a9fc20b49e6fa77", + "plan:0:v2.8.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:f7c04319556397ec4b550aa5aaa96c73689cee09026b661b6a9fc20b49e6fa77", ) .await; } @@ -982,7 +982,7 @@ async fn test_redis_query_plan_config_update(updated_config: &str, new_cache_key router.assert_started().await; router.clear_redis_cache().await; - let starting_key = "plan:0:v2.8.1:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4a5827854a6d2efc85045f0d5bede402e15958390f1073d2e77df56188338e5a"; + let starting_key = "plan:0:v2.8.5:a9e605fa09adc5a4b824e690b4de6f160d47d84ede5956b58a7d300cca1f7204:3973e022e93220f9212c18d0d0c543ae7c309e46640da93a4a0314de999f5112:4a5827854a6d2efc85045f0d5bede402e15958390f1073d2e77df56188338e5a"; router.execute_default_query().await; router.assert_redis_cache_contains(starting_key, None).await; router.update_config(updated_config).await; diff --git a/apollo-router/tests/integration/snapshots/integration_tests__integration__coprocessor__coprocessor_limit_payload.snap b/apollo-router/tests/integration/snapshots/integration_tests__integration__coprocessor__coprocessor_limit_payload.snap new file mode 100644 index 0000000000..a0424fb17f --- /dev/null +++ b/apollo-router/tests/integration/snapshots/integration_tests__integration__coprocessor__coprocessor_limit_payload.snap @@ -0,0 +1,5 @@ +--- +source: apollo-router/tests/integration/coprocessor.rs +expression: response.text().await? +--- +"{\"errors\":[{\"message\":\"Request body payload too large\",\"extensions\":{\"details\":\"Request body payload too large\",\"code\":\"INVALID_GRAPHQL_REQUEST\"}}]}" diff --git a/apollo-router/tests/integration/telemetry/metrics.rs b/apollo-router/tests/integration/telemetry/metrics.rs index 74465528bf..f6d6e56417 100644 --- a/apollo-router/tests/integration/telemetry/metrics.rs +++ b/apollo-router/tests/integration/telemetry/metrics.rs @@ -177,7 +177,7 @@ async fn test_bad_queries() { router.execute_huge_query().await; router .assert_metrics_contains( - r#"apollo_router_http_requests_total{error="payload too large for the `http_max_request_bytes` configuration",status="413",otel_scope_name="apollo/router"} 1"#, + r#"apollo_router_http_requests_total{error="Request body payload too large",status="413",otel_scope_name="apollo/router"} 1"#, None, ) .await; diff --git a/dockerfiles/Dockerfile.router b/dockerfiles/Dockerfile.router index d18c20ebe3..eba93272a4 100644 --- a/dockerfiles/Dockerfile.router +++ b/dockerfiles/Dockerfile.router @@ -1,6 +1,7 @@ FROM debian:bookworm-slim AS downloader ARG ROUTER_RELEASE=latest ARG ARTIFACT_URL= +ARG CIRCLE_TOKEN= # Install curl RUN \ @@ -12,15 +13,16 @@ WORKDIR /dist # Run the Router downloader which puts Router into current working directory RUN if [ -z "${ARTIFACT_URL}"]; then \ - curl -sSL https://router.apollo.dev/download/nix/${ROUTER_RELEASE}/ | sh; \ + curl -sSL "https://router.apollo.dev/download/nix/${ROUTER_RELEASE}"/ | sh; \ else \ cd /; \ - curl -sSL -o - ${ARTIFACT_URL} | tar -xzf -; \ + curl -sSL -H "Circle-Token: ${CIRCLE_TOKEN}" -o - "${ARTIFACT_URL}" | tar -xzf -; \ cd -; \ fi FROM debian:bookworm-slim AS distro ARG DEBUG_IMAGE=false +ARG REPO_URL=https://github.com/apollographql/router # Add a user to run the router as RUN useradd -m router @@ -52,8 +54,8 @@ RUN mkdir config schema # Copy configuration for docker image COPY dockerfiles/router.yaml config -LABEL org.opencontainers.image.authors="Apollo Graph, Inc. https://github.com/apollographql/router" -LABEL org.opencontainers.image.source="https://github.com/apollographql/router" +LABEL org.opencontainers.image.authors="Apollo Graph, Inc. ${REPO_URL}" +LABEL org.opencontainers.image.source="${REPO_URL}" ENV APOLLO_ROUTER_CONFIG_PATH="/dist/config/router.yaml" diff --git a/dockerfiles/tracing/docker-compose.datadog.yml b/dockerfiles/tracing/docker-compose.datadog.yml index 68cb7cdbbd..8c5397d82c 100644 --- a/dockerfiles/tracing/docker-compose.datadog.yml +++ b/dockerfiles/tracing/docker-compose.datadog.yml @@ -3,7 +3,7 @@ services: apollo-router: container_name: apollo-router - image: ghcr.io/apollographql/router:v1.52.0 + image: ghcr.io/apollographql/router:v1.52.1 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/datadog.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.jaeger.yml b/dockerfiles/tracing/docker-compose.jaeger.yml index 254b9e98f8..e3f00a1416 100644 --- a/dockerfiles/tracing/docker-compose.jaeger.yml +++ b/dockerfiles/tracing/docker-compose.jaeger.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router #build: ./router - image: ghcr.io/apollographql/router:v1.52.0 + image: ghcr.io/apollographql/router:v1.52.1 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/jaeger.router.yaml:/etc/config/configuration.yaml diff --git a/dockerfiles/tracing/docker-compose.zipkin.yml b/dockerfiles/tracing/docker-compose.zipkin.yml index b94e872653..df90c46e89 100644 --- a/dockerfiles/tracing/docker-compose.zipkin.yml +++ b/dockerfiles/tracing/docker-compose.zipkin.yml @@ -4,7 +4,7 @@ services: apollo-router: container_name: apollo-router build: ./router - image: ghcr.io/apollographql/router:v1.52.0 + image: ghcr.io/apollographql/router:v1.52.1 volumes: - ./supergraph.graphql:/etc/config/supergraph.graphql - ./router/zipkin.router.yaml:/etc/config/configuration.yaml diff --git a/docs/source/configuration/authn-jwt.mdx b/docs/source/configuration/authn-jwt.mdx index 696fa394d2..bb16070e10 100644 --- a/docs/source/configuration/authn-jwt.mdx +++ b/docs/source/configuration/authn-jwt.mdx @@ -4,6 +4,12 @@ subtitle: Restrict access to credentialed users and systems description: Protect sensitive data by enabling JWT authentication in the Apollo GraphOS Router. Restrict access to credentialed users and systems. --- + + Authentication is crucial to prevent illegitimate access and protect sensitive data in your graph. The GraphOS Router supports request authentication and key rotation via the [JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519) (**JWT**) and [JSON Web Key](https://www.rfc-editor.org/rfc/rfc7517) (**JWK**) standards. This support is compatible with popular identity providers (**IdPs**) like Okta and Auth0. diff --git a/docs/source/configuration/authn-subgraph.mdx b/docs/source/configuration/authn-subgraph.mdx index d138e981d7..3d06ef578f 100644 --- a/docs/source/configuration/authn-subgraph.mdx +++ b/docs/source/configuration/authn-subgraph.mdx @@ -5,6 +5,12 @@ description: Secure communication to AWS subgraphs via the Apollo GraphOS Router minVersion: 1.27.0 --- + + The GraphOS Router and Apollo Router Core support subgraph request authentication and key rotation via [AWS Signature Version 4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html) (SigV4). This allows you to secure communication to AWS subgraphs by making sure a subgraph request was made by the router, and the payload hasn't been tampered with. diff --git a/docs/source/configuration/csrf.mdx b/docs/source/configuration/csrf.mdx index b94dd53da5..ef1abea0b5 100644 --- a/docs/source/configuration/csrf.mdx +++ b/docs/source/configuration/csrf.mdx @@ -5,6 +5,12 @@ description: Prevent cross-site request forgery (CSRF) attacks in the Apollo Gra minVersion: 0.9.0 --- + + ## About CSRF [Cross-site request forgery (CSRF)](https://owasp.org/www-community/attacks/csrf) attacks use side effects of ["simple"](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests) requests to execute GraphQL operations from requests sent by sites that shouldn't be allowed to communicate with your server, based on the [CORS policy](./cors/) of your GraphOS Router or Apollo Router Core. diff --git a/docs/source/configuration/distributed-caching.mdx b/docs/source/configuration/distributed-caching.mdx index 2890cad126..1c97c9b47a 100644 --- a/docs/source/configuration/distributed-caching.mdx +++ b/docs/source/configuration/distributed-caching.mdx @@ -6,6 +6,12 @@ description: Distributed caching for GraphOS Router with GraphOS Enterprise. Con import RedisTLS from '../../shared/redis-tls.mdx' + + If you have multiple GraphOS Router instances, those instances can share a Redis-backed cache for their query plans and automatic persisted queries (APQ). This means that if _any_ of your router instances caches a particular value, _all_ of your instances can look up that value to significantly improve responsiveness. For more details on query plans and APQ, see the article on [in-memory caching](./in-memory-caching). @@ -162,4 +168,4 @@ When active, the `required_to_start` option will prevent the router from startin ### Reset TTL -When this option is active, accessing a cache entry in Redis will reset its expiration. +When this option is active, accessing a cache entry in Redis will reset its expiration. \ No newline at end of file diff --git a/docs/source/configuration/entity-caching.mdx b/docs/source/configuration/entity-caching.mdx index 655d61dbcf..125eb28b56 100644 --- a/docs/source/configuration/entity-caching.mdx +++ b/docs/source/configuration/entity-caching.mdx @@ -5,6 +5,12 @@ description: Subgraph entity caching for GraphOS Router with GraphOS Enterprise. minVersion: 1.40.0 --- + + diff --git a/docs/source/configuration/in-memory-caching.mdx b/docs/source/configuration/in-memory-caching.mdx index 2acf126c59..6c2e129ad5 100644 --- a/docs/source/configuration/in-memory-caching.mdx +++ b/docs/source/configuration/in-memory-caching.mdx @@ -4,6 +4,12 @@ subtitle: Configure router caching for query plans and automatic persisted queri description: Configure in-memory caching for improved performance in Apollo GraphOS Router or Apollo Router Core. Configure query plans and automatic persisted queries caching. --- + + Both GraphOS Router and Apollo Router Core use an in-memory LRU cache to store the following data: - [Generated query plans](#caching-query-plans) @@ -38,18 +44,6 @@ supergraph: limit: 512 # This is the default value. ``` -On schema reloads, the cache will be reset, and queries will need to go through query planning again. To avoid latencies right after the reload, you can configure the router to pregenerate query plans for the most used queries before switching to the new schema: - -```yaml title="router.yaml" -supergraph: - query_planning: - # Pre-plan the 100 most used operations when the supergraph changes. (Default is "0", disabled.) - warmed_up_queries: 100 - cache: - in_memory: - limit: 512 -``` - ### Cache warm-up When loading a new schema, a query plan might change for some queries, so cached query plans cannot be reused. diff --git a/docs/source/configuration/operation-limits.mdx b/docs/source/configuration/operation-limits.mdx index da05ae4e54..48551fa971 100644 --- a/docs/source/configuration/operation-limits.mdx +++ b/docs/source/configuration/operation-limits.mdx @@ -4,6 +4,12 @@ subtitle: Set constraints on depth, height, aliases, and root fields description: Ensure your GraphQL operations are secure with GraphOS Router's operation limits. Set constraints on depth, height, aliases, and root fields. --- + + You can define **operation limits** in your router's configuration to reject potentially malicious requests. An operation that exceeds _any_ specified limit is rejected (unless you run your router in [`warn_only` mode](#warn_only-mode)). diff --git a/docs/source/configuration/persisted-queries.mdx b/docs/source/configuration/persisted-queries.mdx index 677582a01c..bb2ba65d5e 100644 --- a/docs/source/configuration/persisted-queries.mdx +++ b/docs/source/configuration/persisted-queries.mdx @@ -5,6 +5,12 @@ description: Secure your federated GraphQL API by creating an allowlist of trust minVersion: 1.25.0 --- + + diff --git a/docs/source/configuration/telemetry/instrumentation/selectors.mdx b/docs/source/configuration/telemetry/instrumentation/selectors.mdx index 59eaca160a..0234bb886d 100644 --- a/docs/source/configuration/telemetry/instrumentation/selectors.mdx +++ b/docs/source/configuration/telemetry/instrumentation/selectors.mdx @@ -5,7 +5,7 @@ description: Extract and select data from the Apollo GraphOS Router's pipeline s --- import RouterServices from '../../../../shared/router-lifecycle-services.mdx'; -A _selector_ is used to extract data from the GraphOS Router's request lifecycle (pipeline) services and attach them to telemetry, specifically [spans](./spans), [instruments](./instruments), [conditions](./conditions) and [events](./events). +A _selector_ is used to extract data from the GraphOS Router's request lifecycle (pipeline) services and attach them to telemetry. Specifically [spans](./spans), [instruments](./instruments), [conditions](./conditions) and [events](./events). An example of a selector, `request_header`, of the router service on a custom span attribute: @@ -40,7 +40,7 @@ The router service is the initial entrypoint for all requests. It is HTTP centri | `response_context` | Yes | | The name of a response context key | | `baggage` | Yes | | The name of a baggage item | | `env` | Yes | | The name of an environment variable | -| `on_graphql_error` | No | `true`\|`false` | Boolean set to true if the response payload contains a graphql error | +| `on_graphql_error` | No | `true`\|`false` | Boolean set to true if the response payload contains a GraphQL error | | `static` | No | | A static string value | | `error` | No | `reason` | a string value containing error reason when it's a critical error | @@ -52,20 +52,20 @@ The supergraph service is executed after query parsing but before query executio |--------------------|-------------|-------------------------------------------------------|-----------------------------------------------------------------------------------| | `operation_name` | Yes | `string`\|`hash` | The operation name from the query | | `operation_kind` | No | `string` | The operation kind from the query | -| `query` | Yes | `string`\|`aliases`\|`depth`\|`height`\|`root_fields` | The graphql query | -| `query_variable` | Yes | | The name of a graphql query variable | +| `query` | Yes | `string`\|`aliases`\|`depth`\|`height`\|`root_fields` | The GraphQL query | +| `query_variable` | Yes | | The name of a GraphQL query variable | | `request_header` | Yes | | The name of a request header | | `response_header` | Yes | | The name of a response header | | `is_primary_response` | No | `true`\|`false` | Boolean returning true if it's the primary response and not events like subscription events or deferred responses | -| `response_data` | Yes | | Json Path into the supergraph response body data (it might impact performances) | -| `response_errors` | Yes | | Json Path into the supergraph response body errors (it might impact performances) | +| `response_data` | Yes | | JSON Path into the supergraph response body data (it might impact performance) | +| `response_errors` | Yes | | JSON Path into the supergraph response body errors (it might impact performance) | | `request_context` | Yes | | The name of a request context key | | `response_context` | Yes | | The name of a response context key | -| `on_graphql_error` | No | `true`\|`false` | Boolean set to true if the response payload contains a graphql error | +| `on_graphql_error` | No | `true`\|`false` | Boolean set to true if the response payload contains a GraphQL error | | `baggage` | Yes | | The name of a baggage item | | `env` | Yes | | The name of an environment variable | | `static` | No | | A static string value | -| `error` | No | `reason` | a string value containing error reason when it's a critical error | +| `error` | No | `reason` | A string value containing error reason when it's a critical error | ### Subgraph @@ -75,15 +75,15 @@ The subgraph service executes multiple times during query execution, with each e |-----------------------------|-------------|------------------|--------------------------------------------------------------------------------| | `subgraph_operation_name` | Yes | `string`\|`hash` | The operation name from the subgraph query | | `subgraph_operation_kind` | No | `string` | The operation kind from the subgraph query | -| `subgraph_query` | Yes | `string` | The graphql query to the subgraph | +| `subgraph_query` | Yes | `string` | The GraphQL query to the subgraph | | `subgraph_name` | No | `true`\|`false` | The subgraph name | | `subgraph_query_variable` | Yes | | The name of a subgraph query variable | -| `subgraph_response_data` | Yes | | Json Path into the subgraph response body data (it might impact performance) | -| `subgraph_response_errors` | Yes | | Json Path into the subgraph response body errors (it might impact performance) | +| `subgraph_response_data` | Yes | | JSON Path into the subgraph response body data (it might impact performance) | +| `subgraph_response_errors` | Yes | | JSON Path into the subgraph response body errors (it might impact performance) | | `subgraph_request_header` | Yes | | The name of a subgraph request header | | `subgraph_response_header` | Yes | | The name of a subgraph response header | | `subgraph_response_status` | Yes | `code`\|`reason` | The status of a subgraph response | -| `subgraph_on_graphql_error` | No | `true`\|`false` | Boolean set to true if the subgraph response payload contains a graphql error | +| `subgraph_on_graphql_error` | No | `true`\|`false` | Boolean set to true if the subgraph response payload contains a GraphQL error | | `supergraph_operation_name` | Yes | `string`\|`hash` | The operation name from the supergraph query | | `supergraph_operation_kind` | Yes | `string` | The operation kind from the supergraph query | | `supergraph_query` | Yes | `string` | The graphql query to the supergraph | diff --git a/docs/source/configuration/telemetry/instrumentation/spans.mdx b/docs/source/configuration/telemetry/instrumentation/spans.mdx index 120bbf58d1..73dddd58e0 100644 --- a/docs/source/configuration/telemetry/instrumentation/spans.mdx +++ b/docs/source/configuration/telemetry/instrumentation/spans.mdx @@ -37,11 +37,11 @@ Spans may have attributes attached to them from the router pipeline. These attri Attributes may be drawn from [standard attributes](./standard-attributes) or [selectors](./selectors). - + Granular customization of attributes on spans requires a GraphOS [Dedicated or Enterprise plan](https://www.apollographql.com/pricing#observability). - + The attributes that are available depend on the service of the pipeline. diff --git a/docs/source/configuration/telemetry/overview.mdx b/docs/source/configuration/telemetry/overview.mdx index 620e8480f3..076630210a 100644 --- a/docs/source/configuration/telemetry/overview.mdx +++ b/docs/source/configuration/telemetry/overview.mdx @@ -6,6 +6,12 @@ description: Observe and monitor the health and performance of GraphQL operation import TelemetryPerformanceNote from '../../../shared/telemetry-performance.mdx'; + + In this overview, learn about: - How GraphOS Router and Apollo Router Core telemetry enable supergraph observability and debuggability - What data is captured in the router's logs, metrics, and traces diff --git a/docs/source/executing-operations/demand-control.mdx b/docs/source/executing-operations/demand-control.mdx index 8b441cb381..1aa0957b81 100644 --- a/docs/source/executing-operations/demand-control.mdx +++ b/docs/source/executing-operations/demand-control.mdx @@ -5,6 +5,12 @@ description: Protect your graph from malicious or demanding clients with Apollo minVersion: 1.48.0 --- + + diff --git a/docs/source/executing-operations/subscription-support.mdx b/docs/source/executing-operations/subscription-support.mdx index 6593044ca3..73bc205ea5 100644 --- a/docs/source/executing-operations/subscription-support.mdx +++ b/docs/source/executing-operations/subscription-support.mdx @@ -5,6 +5,12 @@ description: Configure your router to support GraphQL subscriptions, enabling cl minVersion: 1.22.0 --- + + **For self-hosted routers, subscription support is an [Enterprise feature](../enterprise-features/).** diff --git a/docs/source/federation-version-support.mdx b/docs/source/federation-version-support.mdx index 195e9b0018..331b9eb7d2 100644 --- a/docs/source/federation-version-support.mdx +++ b/docs/source/federation-version-support.mdx @@ -37,7 +37,23 @@ The table below shows which version of federation each router release is compile - v1.49.1 and later (see latest releases) + v1.53.0 and later (see latest releases) + + + 2.9.0 + + + + + v1.52.1 + + + 2.8.5 + + + + + v1.49.0 - v1.52.0 2.8.1 @@ -53,7 +69,7 @@ The table below shows which version of federation each router release is compile - v1.46.0 - v1.47.0 + v1.46.0 - v1.47.0 2.7.5 @@ -337,28 +353,32 @@ The table below shows which version of federation each router release is compile - v0.9.0 – v0.15.1 + + v0.9.0 – v0.15.1 2.0.2 - v0.1.0-preview.5 – v0.1.0-preview.7 + + v0.1.0-preview.5 – v0.1.0-preview.7 2.0.1 - v0.1.0-preview.4 + + v0.1.0-preview.4 2.0.0 - v0.1.0-preview.3 + + v0.1.0-preview.3 2.0.0-preview.7 diff --git a/docs/source/index.mdx b/docs/source/index.mdx index 7dbcd70870..3ad0bb4e8f 100644 --- a/docs/source/index.mdx +++ b/docs/source/index.mdx @@ -6,6 +6,12 @@ description: Distribute operations efficiently across microservices in your fede import { Link } from 'gatsby'; + + > 💡 Not self-hosting your router? Get started with [Cloud Router](/graphos/quickstart/cloud/). GraphOS Router is a scalable runtime for supergraphs that's fully integrated with GraphOS and based on the Apollo Router Core. diff --git a/docs/source/migrating-from-gateway.mdx b/docs/source/migrating-from-gateway.mdx index 2e91a39d67..4ae7ae2bed 100644 --- a/docs/source/migrating-from-gateway.mdx +++ b/docs/source/migrating-from-gateway.mdx @@ -4,6 +4,12 @@ subtitle: Migrating to the router from @apollo/gateway description: Learn how to migrate a federated graph from @apollo/gateway to the Apollo GraphOS Router or Apollo Router Core without any changes to your subgraphs. --- + + Learn how to migrate a federated supergraph using the `@apollo/gateway` library to the GraphOS Router or Apollo Router Core and gain significant performance improvements while making zero changes to your subgraphs. ## What's different? @@ -133,6 +139,12 @@ Examples for each are provided in their respective documentation, and in the [Ro For migrating to the router deployed on Kubernetes, see some tips for [configuring your router deployment](./containerization/kubernetes/#configure-for-migration-from-gateway). +## Responses and errors + +Apollo Gateway runs its core GraphQL server using Apollo Server and a user configured HTTP server. GraphOS Router is built as an all-in-one product with the GraphQL and HTTP server bundled together. With this change there may be some slight difference to the logic of HTTP status codes or GraphQL error extension codes. + +Refer the full list of [router error codes](./errors) for any changes to your gateway implementation. If you need to customize the responses, refer to [router customizations](./customizations/overview). + ## Reporting migration issues If you encounter a migration issue that isn't resolved by this article, please search for existing [GitHub discussions](https://github.com/apollographql/router/discussions/) and start a new discussion if you don't find what you're looking for. diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 928b68ed73..d332194093 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -20,7 +20,7 @@ reqwest = { workspace = true, features = ["json", "blocking"] } serde_json.workspace = true tokio.workspace = true # note: this dependency should _always_ be pinned, prefix the version with an `=` -router-bridge = "=0.5.27+v2.8.1" +router-bridge = "=0.5.31+v2.8.5" [dev-dependencies] anyhow = "1" diff --git a/helm/chart/router/Chart.yaml b/helm/chart/router/Chart.yaml index 02d1f131bd..4d7abff27b 100644 --- a/helm/chart/router/Chart.yaml +++ b/helm/chart/router/Chart.yaml @@ -20,10 +20,10 @@ type: application # so it matches the shape of our release process and release automation. # By proxy of that decision, this version uses SemVer 2.0.0, though the prefix # of "v" is not included. -version: 1.52.0 +version: 1.52.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v1.52.0" +appVersion: "v1.52.1" diff --git a/helm/chart/router/README.md b/helm/chart/router/README.md index a1940b4b28..706e25fe89 100644 --- a/helm/chart/router/README.md +++ b/helm/chart/router/README.md @@ -2,7 +2,7 @@ [router](https://github.com/apollographql/router) Rust Graph Routing runtime for Apollo Federation -![Version: 1.52.0](https://img.shields.io/badge/Version-1.52.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.52.0](https://img.shields.io/badge/AppVersion-v1.52.0-informational?style=flat-square) +![Version: 1.52.1](https://img.shields.io/badge/Version-1.52.1-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.52.1](https://img.shields.io/badge/AppVersion-v1.52.1-informational?style=flat-square) ## Prerequisites @@ -11,7 +11,7 @@ ## Get Repo Info ```console -helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.52.0 +helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.52.1 ``` ## Install Chart @@ -19,7 +19,7 @@ helm pull oci://ghcr.io/apollographql/helm-charts/router --version 1.52.0 **Important:** only helm3 is supported ```console -helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.52.0 --values my-values.yaml +helm upgrade --install [RELEASE_NAME] oci://ghcr.io/apollographql/helm-charts/router --version 1.52.1 --values my-values.yaml ``` _See [configuration](#configuration) below._ @@ -95,3 +95,5 @@ helm show values oci://ghcr.io/apollographql/helm-charts/router | topologySpreadConstraints | list | `[]` | Sets the [topology spread constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/) for Deployment pods | | virtualservice.enabled | bool | `false` | | +---------------------------------------------- +Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/licenses.html b/licenses.html index f4c1083d5b..32b2ee310b 100644 --- a/licenses.html +++ b/licenses.html @@ -44,13 +44,13 @@

Third Party Licenses

Overview of licenses:

    -
  • Apache License 2.0 (490)
  • -
  • MIT License (165)
  • +
  • Apache License 2.0 (488)
  • +
  • MIT License (163)
  • BSD 3-Clause "New" or "Revised" License (11)
  • ISC License (8)
  • -
  • Elastic License 2.0 (6)
  • BSD 2-Clause "Simplified" License (5)
  • Mozilla Public License 2.0 (5)
  • +
  • Elastic License 2.0 (3)
  • Creative Commons Zero v1.0 Universal (2)
  • OpenSSL License (1)
  • Unicode License Agreement - Data Files and Software (2016) (1)
  • @@ -65,7 +65,6 @@

    Used by:

  • aws-config
  • aws-credential-types
  • aws-runtime
  • -
  • aws-sigv4
  • aws-smithy-async
  • aws-smithy-http
  • aws-smithy-json
  • @@ -8643,7 +8642,6 @@

    Used by:

  • cc
  • cfg-if
  • cfg-if
  • -
  • ci_info
  • cmake
  • concurrent-queue
  • const-random
  • @@ -11511,18 +11509,6 @@

    Used by:

    additional terms or conditions. -
  • -

    Apache License 2.0

    -

    Used by:

    - -
    ../../LICENSE-APACHE
    -
  • Apache License 2.0

    Used by:

    @@ -12170,10 +12156,16 @@

    Used by:

    Apache License 2.0

    Used by:

      +
    • apollo-compiler
    • +
    • apollo-encoder
    • +
    • apollo-parser
    • +
    • apollo-smith
    • +
    • apollo-smith
    • async-graphql-actix-web
    • async-graphql-derive
    • async-graphql-parser
    • async-graphql-value
    • +
    • buildstructor
    • deno-proc-macro-rules
    • deno-proc-macro-rules-macros
    • dunce
    • @@ -12293,27 +12285,6 @@

      Used by:

      http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - -
    • -

      Apache License 2.0

      -

      Used by:

      - -
      Copyright [2022] [Bryn Cooke]
      -
      -Licensed under the Apache License, Version 2.0 (the "License");
      -you may not use this file except in compliance with the License.
      -You may obtain a copy of the License at
      -
      -    http://www.apache.org/licenses/LICENSE-2.0
      -
       Unless required by applicable law or agreed to in writing, software
       distributed under the License is distributed on an "AS IS" BASIS,
       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      @@ -12977,9 +12948,6 @@ 

      Elastic License 2.0

      Used by:

      Copyright 2021 Apollo Graph, Inc.
       
      @@ -14091,66 +14059,6 @@ 

      Used by:

      shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -
      -
    • -
    • -

      MIT License

      -

      Used by:

      - -
      Copyright (c) 2019 Carl Lerche
      -
      -Permission is hereby granted, free of charge, to any
      -person obtaining a copy of this software and associated
      -documentation files (the "Software"), to deal in the
      -Software without restriction, including without
      -limitation the rights to use, copy, modify, merge,
      -publish, distribute, sublicense, and/or sell copies of
      -the Software, and to permit persons to whom the Software
      -is furnished to do so, subject to the following
      -conditions:
      -
      -The above copyright notice and this permission notice
      -shall be included in all copies or substantial portions
      -of the Software.
      -
      -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
      -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
      -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
      -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
      -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
      -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
      -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
      -DEALINGS IN THE SOFTWARE.
      -
      -Copyright (c) 2018 David Tolnay
      -
      -Permission is hereby granted, free of charge, to any
      -person obtaining a copy of this software and associated
      -documentation files (the "Software"), to deal in the
      -Software without restriction, including without
      -limitation the rights to use, copy, modify, merge,
      -publish, distribute, sublicense, and/or sell copies of
      -the Software, and to permit persons to whom the Software
      -is furnished to do so, subject to the following
      -conditions:
      -
      -The above copyright notice and this permission notice
      -shall be included in all copies or substantial portions
      -of the Software.
      -
       THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
       ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
       TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
      @@ -15157,6 +15065,8 @@ 

      Used by:

      MIT License

      Used by:

        +
      • async-stream
      • +
      • async-stream-impl
      • base64-simd
      • convert_case
      • cookie-factory
      • @@ -16090,8 +16000,6 @@

        Used by:

        MIT License

        Used by:

          -
        • aho-corasick
        • -
        • byteorder
        • globset
        • memchr
        • regex-automata
        • @@ -16500,6 +16408,7 @@

          Used by:

          Mozilla Public License 2.0

          Used by:

          Mozilla Public License Version 2.0
          @@ -16882,7 +16791,6 @@ 

          Used by:

          Mozilla Public License 2.0

          Used by:

          Mozilla Public License Version 2.0
          diff --git a/scripts/install.sh b/scripts/install.sh
          index de9ed49dae..a6e078d154 100755
          --- a/scripts/install.sh
          +++ b/scripts/install.sh
          @@ -11,7 +11,7 @@ BINARY_DOWNLOAD_PREFIX="https://github.com/apollographql/router/releases/downloa
           
           # Router version defined in apollo-router's Cargo.toml
           # Note: Change this line manually during the release steps.
          -PACKAGE_VERSION="v1.52.0"
          +PACKAGE_VERSION="v1.52.1"
           
           download_binary() {
               downloader --check