From 5b36237428939ff8bae3741297e916d6988fbf99 Mon Sep 17 00:00:00 2001 From: Jeremy Lempereur Date: Tue, 21 Sep 2021 19:23:28 +0200 Subject: [PATCH] Switch to warp + CORS configuration (#182) resolves #168 Switched the server framework to Warp, so we can benefit from the performance improvements and the available middlewares. Added CORS configuration options (allow_credentials, allow_headers, expose_headers, allow_methods, allow_origins) --- Cargo.lock | 382 ++++++++++++++++- crates/apollo-router/Cargo.toml | 20 +- crates/apollo-router/src/configuration/mod.rs | 32 ++ ...e_configuration_api_does_not_change-2.snap | 4 + ...ation__tests__supergraph_config_serde.snap | 4 + crates/apollo-router/src/lib.rs | 6 +- .../src/warp_http_server_factory.rs | 401 ++++++++++++++++++ 7 files changed, 828 insertions(+), 21 deletions(-) create mode 100644 crates/apollo-router/src/warp_http_server_factory.rs diff --git a/Cargo.lock b/Cargo.lock index b96f5d0e09..c7a2481229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -57,7 +72,6 @@ dependencies = [ "futures", "hotwatch", "httpmock", - "hyper", "insta", "log", "maplit", @@ -81,6 +95,7 @@ dependencies = [ "typed-builder", "url", "uuid", + "warp", ] [[package]] @@ -150,6 +165,20 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443ccbb270374a2b1055fc72da40e1f237809cd6bb0e97e66d264cd138473a6" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.4.1" @@ -389,6 +418,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.0.2" @@ -412,6 +450,27 @@ dependencies = [ "cmake", ] +[[package]] +name = "brotli" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71cb90ade945043d3d53597b2fc359bb063db8ade2bcffe7997351d0756e9d50" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "0.2.16" @@ -424,6 +483,16 @@ dependencies = [ "serde", ] +[[package]] +name = "buf_redux" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" +dependencies = [ + "memchr", + "safemem", +] + [[package]] name = "bumpalo" version = "3.7.0" @@ -606,6 +675,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpufeatures" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -828,6 +906,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + [[package]] name = "directories" version = "3.0.2" @@ -1187,6 +1274,27 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -1195,7 +1303,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] @@ -1315,6 +1423,31 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "headers" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "mime", + "sha-1", + "time", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.3.3" @@ -1519,6 +1652,15 @@ dependencies = [ "libc", ] +[[package]] +name = "input_buffer" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97967975f448f1a7ddb12b0bc41069d09ed6a1c161a92687e057325db35d413" +dependencies = [ + "bytes", +] + [[package]] name = "insta" version = "1.8.0" @@ -1788,6 +1930,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -1896,6 +2048,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multipart" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050aeedc89243f5347c3e237e3e13dc76fbe4ae3742a57b94dc14f69acf76d4" +dependencies = [ + "buf_redux", + "httparse", + "log", + "mime", + "mime_guess", + "quick-error", + "rand 0.7.3", + "safemem", + "tempfile", + "twoway", +] + [[package]] name = "native-tls" version = "0.2.7" @@ -2015,6 +2185,12 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.34" @@ -2060,7 +2236,7 @@ dependencies = [ "lazy_static", "percent-encoding", "pin-project", - "rand", + "rand 0.8.4", "serde", "thiserror", "tokio", @@ -2466,6 +2642,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.9" @@ -2475,6 +2657,19 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc 0.2.0", +] + [[package]] name = "rand" version = "0.8.4" @@ -2482,9 +2677,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -2494,7 +2699,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -2503,7 +2717,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom", + "getrandom 0.2.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2512,7 +2735,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" dependencies = [ - "rand_core", + "rand_core 0.6.3", ] [[package]] @@ -2555,7 +2778,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ - "getrandom", + "getrandom 0.2.3", "redox_syscall", ] @@ -2706,6 +2929,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -2725,6 +2954,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "scoped-tls" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2883,6 +3118,19 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "sharded-slab" version = "0.1.3" @@ -3046,7 +3294,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand", + "rand 0.8.4", "redox_syscall", "remove_dir_all", "winapi 0.3.9", @@ -3142,6 +3390,16 @@ dependencies = [ "threadpool", ] +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3249,6 +3507,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a5f475f1b9d077ea1017ecbc60890fda8e54942d680ca0b1d2b47cfa2d861b" +dependencies = [ + "futures-util", + "log", + "pin-project", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.6.7" @@ -3317,7 +3588,7 @@ dependencies = [ "futures-util", "indexmap", "pin-project", - "rand", + "rand 0.8.4", "slab", "tokio", "tokio-stream", @@ -3450,6 +3721,34 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "tungstenite" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ada8297e8d70872fa9a551d93250a9f407beb9f37ef86494eb20012a2ff7c24" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "input_buffer", + "log", + "rand 0.8.4", + "sha-1", + "url", + "utf-8", +] + +[[package]] +name = "twoway" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" +dependencies = [ + "memchr", +] + [[package]] name = "typed-builder" version = "0.9.1" @@ -3461,12 +3760,27 @@ dependencies = [ "syn", ] +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + [[package]] name = "ucd-trie" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.5" @@ -3522,13 +3836,19 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom", + "getrandom 0.2.3", "serde", ] @@ -3587,6 +3907,42 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d47745e9a0c38636dbd454729b147d16bd1ed08ae67b3ab281c4506771054" +dependencies = [ + "async-compression", + "bytes", + "futures", + "headers", + "http", + "hyper", + "log", + "mime", + "mime_guess", + "multipart", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/crates/apollo-router/Cargo.toml b/crates/apollo-router/Cargo.toml index 62c5c9a736..574ed56978 100644 --- a/crates/apollo-router/Cargo.toml +++ b/crates/apollo-router/Cargo.toml @@ -11,7 +11,12 @@ path = "src/main.rs" [features] default = ["otlp-tonic"] -otlp-tonic = ["opentelemetry-otlp/tonic", "opentelemetry-otlp/tonic-build", "opentelemetry-otlp/prost", "tonic"] +otlp-tonic = [ + "opentelemetry-otlp/tonic", + "opentelemetry-otlp/tonic-build", + "opentelemetry-otlp/prost", + "tonic", +] otlp-grpcio = ["opentelemetry-otlp/grpc-sys", "opentelemetry-otlp/openssl"] otlp-http = ["opentelemetry-otlp/http-proto"] tls = ["opentelemetry-otlp/tls", "tonic/transport", "tonic/tls"] @@ -24,14 +29,18 @@ derivative = "2.2.0" derive_more = "0.99.16" directories = "3.0.2" displaydoc = "0.2" -futures = { version = "0.3.17", features = ["thread-pool"]} +futures = { version = "0.3.17", features = ["thread-pool"] } hotwatch = "0.4.5" -hyper = { version = "0.14.13", features = ["full"] } log = "0.4.14" once_cell = "1.8.0" opentelemetry = { version = "0.16.0", features = ["rt-tokio", "serialize"] } -opentelemetry-jaeger = { version = "0.15.0", features = ["collector_client", "rt-tokio"] } -opentelemetry-otlp = { version = "0.9.0", default-features = false, features = ["serialize"], optional = true } +opentelemetry-jaeger = { version = "0.15.0", features = [ + "collector_client", + "rt-tokio", +] } +opentelemetry-otlp = { version = "0.9.0", default-features = false, features = [ + "serialize", +], optional = true } parking_lot = "0.11.2" reqwest = { version = "0.11.4", features = ["json", "stream"] } serde = { version = "1.0.130", features = ["derive", "rc"] } @@ -46,6 +55,7 @@ tracing-opentelemetry = "0.15.0" tracing-subscriber = "0.2.21" typed-builder = "0.9.1" url = { version = "2.2.2", features = ["serde"] } +warp = { version = "0.3.1", features = ["compression"] } [dev-dependencies] apollo-router-core = { path = "../apollo-router-core", features = ["mockall"] } diff --git a/crates/apollo-router/src/configuration/mod.rs b/crates/apollo-router/src/configuration/mod.rs index a596ebeba3..d9d83504e6 100644 --- a/crates/apollo-router/src/configuration/mod.rs +++ b/crates/apollo-router/src/configuration/mod.rs @@ -92,6 +92,23 @@ pub struct Server { #[derive(Debug, Deserialize, Serialize, TypedBuilder)] #[serde(deny_unknown_fields)] pub struct Cors { + /// Set to true to add the `Access-Control-Allow-Credentials` header. + #[serde(default)] + #[builder(default)] + pub allow_credentials: Option, + + /// The headers to allow. + /// Defaults to the required request header for Apollo Studio + #[serde(default = "default_cors_headers")] + #[builder(default_code = "default_cors_headers()")] + pub allow_headers: Vec, + + #[serde(default)] + #[builder(default)] + /// Which response headers should be made available to scripts running in the browser, + /// in response to a cross-origin request. + pub expose_headers: Option>, + /// The origin(s) to allow requests from. /// Use `https://studio.apollographql.com/` to allow Apollo Studio to function. pub origins: Vec, @@ -102,6 +119,10 @@ pub struct Cors { pub methods: Vec, } +fn default_cors_headers() -> Vec { + vec!["Content-Type".into()] +} + fn default_cors_methods() -> Vec { vec!["GET".into(), "POST".into(), "OPTIONS".into()] } @@ -112,6 +133,17 @@ impl Default for Server { } } +impl Cors { + pub fn into_warp_middleware(&self) -> warp::cors::Builder { + warp::cors() + .allow_credentials(self.allow_credentials.unwrap_or_default()) + .allow_headers(self.allow_headers.iter().map(std::string::String::as_str)) + .expose_headers(self.allow_headers.iter().map(std::string::String::as_str)) + .allow_methods(self.methods.iter().map(std::string::String::as_str)) + .allow_origins(self.origins.iter().map(std::string::String::as_str)) + } +} + #[derive(Debug, Deserialize, Serialize)] #[serde(deny_unknown_fields, rename_all = "snake_case")] #[allow(clippy::large_enum_variant)] diff --git a/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__ensure_configuration_api_does_not_change-2.snap b/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__ensure_configuration_api_does_not_change-2.snap index d2cd358d85..396ec8b4bb 100644 --- a/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__ensure_configuration_api_does_not_change-2.snap +++ b/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__ensure_configuration_api_does_not_change-2.snap @@ -6,6 +6,10 @@ expression: config server: listen: "1.2.3.4:5" cors: + allow_credentials: ~ + allow_headers: + - Content-Type + expose_headers: ~ origins: - foo - bar diff --git a/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__supergraph_config_serde.snap b/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__supergraph_config_serde.snap index 0cd880d926..c8517a0403 100644 --- a/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__supergraph_config_serde.snap +++ b/crates/apollo-router/src/configuration/snapshots/apollo_router__configuration__tests__supergraph_config_serde.snap @@ -6,6 +6,10 @@ expression: config server: listen: "127.0.0.1:4001" cors: + allow_credentials: ~ + allow_headers: + - Content-Type + expose_headers: ~ origins: - studio.apollographql.com methods: diff --git a/crates/apollo-router/src/lib.rs b/crates/apollo-router/src/lib.rs index c7fb4b1d52..194629c238 100644 --- a/crates/apollo-router/src/lib.rs +++ b/crates/apollo-router/src/lib.rs @@ -6,12 +6,12 @@ mod graph_factory; mod http_server_factory; pub mod http_service_registry; pub mod http_subgraph; -mod hyper_http_server_factory; mod state_machine; +mod warp_http_server_factory; use crate::graph_factory::FederatedGraphFactory; -use crate::hyper_http_server_factory::HyperHttpServerFactory; use crate::state_machine::StateMachine; +use crate::warp_http_server_factory::WarpHttpServerFactory; use crate::Event::{NoMoreConfiguration, NoMoreSchema}; use configuration::{Configuration, OpenTelemetry}; use derivative::Derivative; @@ -502,7 +502,7 @@ impl FederatedServer { /// pub fn serve(self) -> FederatedServerHandle { let (state_listener, state_receiver) = mpsc::channel::(1); - let server_factory = HyperHttpServerFactory::new(); + let server_factory = WarpHttpServerFactory::new(); let state_machine = StateMachine::new( server_factory, Some(state_listener), diff --git a/crates/apollo-router/src/warp_http_server_factory.rs b/crates/apollo-router/src/warp_http_server_factory.rs new file mode 100644 index 0000000000..90692b0865 --- /dev/null +++ b/crates/apollo-router/src/warp_http_server_factory.rs @@ -0,0 +1,401 @@ +use crate::configuration::Configuration; +use crate::http_server_factory::{HttpServerFactory, HttpServerHandle}; +use crate::FederatedServerError; +use apollo_router_core::{FetchError, GraphQLFetcher, GraphQLRequest}; +use bytes::Bytes; +use futures::channel::oneshot; +use futures::prelude::*; +use parking_lot::RwLock; +use std::sync::Arc; +use warp::hyper::Response; +use warp::{ + http::{header::HeaderValue, StatusCode, Uri}, + hyper::Body, + Filter, +}; +use warp::{Rejection, Reply}; + +/// A basic http server using warp. +/// Uses streaming as primary method of response. +/// Redirects to studio for GET requests. +#[derive(Debug)] +pub(crate) struct WarpHttpServerFactory; + +impl WarpHttpServerFactory { + pub(crate) fn new() -> Self { + Self + } +} + +impl HttpServerFactory for WarpHttpServerFactory { + fn create( + &self, + graph: Arc>, + configuration: Arc>, + ) -> HttpServerHandle + where + F: GraphQLFetcher + 'static, + { + let (shutdown_sender, shutdown_receiver) = oneshot::channel(); + let listen_address = configuration.read().server.listen; + + let cors = configuration + .read() + .server + .cors + .as_ref() + .map(|cors_configuration| cors_configuration.into_warp_middleware()) + .unwrap_or_else(warp::cors); + + let routes = redirect_to_studio() + .or(perform_graphql_request(graph, configuration)) + .with(cors); + + let (actual_listen_address, server) = + warp::serve(routes).bind_with_graceful_shutdown(listen_address, async { + shutdown_receiver.await.ok(); + }); + + // Spawn the server into a runtime + let server_future = tokio::task::spawn(server) + .map_err(|_| FederatedServerError::HttpServerLifecycleError) + .boxed(); + + HttpServerHandle { + shutdown_sender, + server_future, + listen_address: actual_listen_address, + } + } +} + +fn redirect_to_studio() -> impl Filter,), Error = Rejection> + Clone { + warp::get() + .and(warp::path::end().or(warp::path("graphql"))) + .and(warp::header::value("Host")) + .map(|_, host: HeaderValue| { + host.to_str() + .map(|h| -> Box { + Box::new(warp::redirect::temporary( + format!( + "https://studio.apollographql.com/sandbox?endpoint=http://{}", + h + ) + .parse::() + .unwrap(), + )) + }) + .unwrap_or_else(|_| { + Box::new(warp::reply::with_status( + "Invalid request Host header", + StatusCode::BAD_REQUEST, + )) + }) + }) + .boxed() +} + +fn perform_graphql_request( + graph: Arc>, + configuration: Arc>, +) -> impl Filter,), Error = Rejection> + Clone +where + F: GraphQLFetcher + 'static, +{ + warp::post() + .and(warp::path::end().or(warp::path("graphql"))) + .and(warp::body::json()) + .map(move |_, graphql_request: GraphQLRequest| { + let default_tracing_dispatcher = { + let lock = configuration.read(); + lock.subscriber + .clone() + .map(tracing::Dispatch::new) + .unwrap_or_default() + }; + let stream = tracing::dispatcher::with_default(&default_tracing_dispatcher, || { + graph.read().stream(graphql_request) + }); + Response::new(Body::wrap_stream( + stream + .enumerate() + .map(|(index, res)| match serde_json::to_string(&res) { + Ok(bytes) => Ok(Bytes::from(bytes)), + Err(err) => { + // We didn't manage to serialise the response! + // Do our best to send some sort of error back. + serde_json::to_string( + &FetchError::MalformedResponse { + reason: err.to_string(), + } + .to_response(index == 0), + ) + .map(Bytes::from) + } + }) + .boxed(), + )) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::configuration::Cors; + use apollo_router_core::{ + FetchError, GraphQLFetcher, GraphQLRequest, GraphQLResponse, GraphQLResponseStream, + }; + use mockall::{mock, predicate::*}; + use reqwest::header::{ + ACCEPT, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, + LOCATION, ORIGIN, + }; + use reqwest::redirect::Policy; + use reqwest::{Client, Method, StatusCode}; + use serde_json::json; + use std::net::SocketAddr; + use std::str::FromStr; + + macro_rules! assert_header { + ($response:expr, $header:expr, $expected:expr $(, $msg:expr)?) => { + assert_eq!( + $response + .headers() + .get_all($header) + .iter() + .map(|v|v.to_str().unwrap().to_string()) + .collect::>(), + $expected + $(, $msg)* + ); + }; + } + + /// `assert_header_contains` works like `assert_headers`, + /// except it doesn't care for the order of the items + macro_rules! assert_header_contains { + ($response:expr, $header:expr, $expected:expr $(, $msg:expr)?) => { + let header_values = $response + .headers() + .get_all($header) + .iter() + .map(|v|v.to_str().unwrap().to_string()) + .collect::>(); + + for e in $expected { + assert!( + header_values + .iter() + .find(|header_value| header_value.contains(&e.to_string())) + .is_some(), + $($msg)* + ); + } + + }; + } + + mock! { + #[derive(Debug)] + MyGraphQLFetcher{} + impl GraphQLFetcher for MyGraphQLFetcher { // specification of the trait to mock + fn stream(&self, request: GraphQLRequest) -> GraphQLResponseStream; + } + } + + fn init(listen_address: &str) -> (Arc>, HttpServerHandle, Client) { + let _ = env_logger::builder().is_test(true).try_init(); + let fetcher = MockMyGraphQLFetcher::new(); + let server_factory = WarpHttpServerFactory::new(); + let fetcher = Arc::new(RwLock::new(fetcher)); + let server = server_factory.create( + fetcher.to_owned(), + Arc::new(RwLock::new( + Configuration::builder() + .server( + crate::configuration::Server::builder() + .listen(SocketAddr::from_str(listen_address).unwrap()) + .cors(Some( + Cors::builder() + .origins(vec!["http://studio".to_string()]) + .build(), + )) + .build(), + ) + .subgraphs(Default::default()) + .build(), + )), + ); + let client = reqwest::Client::builder() + .redirect(Policy::none()) + .build() + .unwrap(); + (fetcher, server, client) + } + + #[tokio::test] + async fn redirect_to_studio() -> Result<(), FederatedServerError> { + let (_fetcher, server, client) = init("127.0.0.1:0"); + + for url in vec![ + format!("http://{}/", server.listen_address), + format!("http://{}/graphql", server.listen_address), + ] { + let response = client + .get(url) + .header(ACCEPT, "text/html") + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT); + assert_header!( + &response, + LOCATION, + vec![format!( + "https://studio.apollographql.com/sandbox?endpoint=http://{}", + server.listen_address + ) + .to_string()], + "Incorrect redirect url" + ); + } + + server.shutdown().await + } + + #[tokio::test] + async fn malformed_request() -> Result<(), FederatedServerError> { + let (_fetcher, server, client) = init("127.0.0.1:0"); + + let response = client + .post(format!("http://{}/graphql", server.listen_address)) + .body("Garbage") + .send() + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + server.shutdown().await + } + + #[tokio::test] + async fn response() -> Result<(), FederatedServerError> { + let expected_response = GraphQLResponse::builder() + .data(json!({"response": "yay"})) + .build(); + let example_response = expected_response.clone(); + let (fetcher, server, client) = init("127.0.0.1:0"); + { + fetcher + .write() + .expect_stream() + .times(1) + .return_once(move |_| futures::stream::iter(vec![example_response]).boxed()); + } + let response = client + .post(format!("http://{}/graphql", server.listen_address)) + .body( + json!( + { + "query": "query", + }) + .to_string(), + ) + .send() + .await + .unwrap() + .error_for_status() + .expect("unexpected response"); + + assert_eq!( + response.json::().await.unwrap(), + expected_response, + ); + + server.shutdown().await + } + + #[tokio::test] + async fn response_failure() -> Result<(), FederatedServerError> { + let (fetcher, server, client) = init("127.0.0.1:0"); + { + fetcher.write().expect_stream().times(1).return_once(|_| { + futures::stream::iter(vec![FetchError::SubrequestHttpError { + service: "Mock service".to_string(), + reason: "Mock error".to_string(), + } + .to_response(true)]) + .boxed() + }); + } + let response = client + .post(format!("http://{}/graphql", server.listen_address)) + .body( + json!( + { + "query": "query", + }) + .to_string(), + ) + .send() + .await + .ok() + .unwrap() + .json::() + .await + .unwrap(); + + assert_eq!( + response, + FetchError::SubrequestHttpError { + service: "Mock service".to_string(), + reason: "Mock error".to_string(), + } + .to_response(true) + ); + server.shutdown().await + } + + #[tokio::test] + async fn cors_preflight() -> Result<(), FederatedServerError> { + let (_fetcher, server, client) = init("127.0.0.1:0"); + + for url in vec![ + format!("http://{}/", server.listen_address), + format!("http://{}/graphql", server.listen_address), + ] { + let response = client + .request(Method::OPTIONS, &url) + .header(ACCEPT, "text/html") + .header(ORIGIN, "http://studio") + .header(ACCESS_CONTROL_REQUEST_METHOD, "POST") + .header(ACCESS_CONTROL_REQUEST_HEADERS, "Content-type") + .send() + .await + .unwrap(); + + assert_header!( + &response, + ACCESS_CONTROL_ALLOW_ORIGIN, + vec!["http://studio"], + "Incorrect access control allow origin header" + ); + assert_header_contains!( + &response, + ACCESS_CONTROL_ALLOW_HEADERS, + vec!["content-type"], + "Incorrect access control allow header header" + ); + assert_header_contains!( + &response, + ACCESS_CONTROL_ALLOW_METHODS, + vec!["GET", "POST", "OPTIONS"], + "Incorrect access control allow methods header" + ); + + assert_eq!(response.status(), StatusCode::OK); + } + + server.shutdown().await + } +}