diff --git a/.gitignore b/.gitignore index 518afe5b12..6f35c59f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ npm/@tailcallhq npm/node_modules + +/keys/*.pem \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 36c7477e29..0b8c37a2be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -399,6 +399,51 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -1081,6 +1126,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1493,6 +1553,31 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1796,6 +1881,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.6.4" @@ -1933,6 +2024,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.4" @@ -1998,6 +2107,32 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" +[[package]] +name = "openssl" +version = "0.10.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +dependencies = [ + "bitflags 2.4.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -2006,9 +2141,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" dependencies = [ "cc", "libc", @@ -2249,6 +2384,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e081b29f63d83a4bc75cfc9f3fe424f9156cf92d8a4f0c9407cce9a1b67327cf" +dependencies = [ + "prost", +] + [[package]] name = "pulldown-cmark" version = "0.9.3" @@ -2603,6 +2770,29 @@ dependencies = [ "untrusted", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.19" @@ -2872,6 +3062,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2919,6 +3115,7 @@ dependencies = [ "http-cache-semantics", "httpmock", "hyper", + "hyper-tls", "indexmap 2.1.0", "inquire", "log", @@ -2943,6 +3140,9 @@ dependencies = [ "stripmargin", "thiserror", "tokio", + "tokio-stream", + "tonic", + "tonic-reflection", "url", ] @@ -3089,6 +3289,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-macros" version = "2.1.0" @@ -3100,6 +3310,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -3110,6 +3330,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.9" @@ -3141,6 +3372,75 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.4", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "rustls", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-reflection" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa37c513df1339d197f4ba21d28c918b9ef1ac1768265f11ecb6b7f1cba1b76" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 6bc1d3144c..1f9b56f460 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,10 @@ log = "0.4.20" env_logger = "0.10.0" stripmargin = "0.1.1" ring = "0.17.5" +tonic = { version = "0.10.2", features = ["tls"] } +tonic-reflection = "0.10.2" +hyper-tls = "0.5.0" +tokio-stream = "0.1.14" [dev-dependencies] criterion = "0.5.1" diff --git a/examples/grpc-sample.graphql b/examples/grpc-sample.graphql new file mode 100644 index 0000000000..fa4a351324 --- /dev/null +++ b/examples/grpc-sample.graphql @@ -0,0 +1,33 @@ +schema + @server(port: 8000, enableGraphiql: "/graphiql", enableQueryValidation: false, enableGrpc: true, hostname: "0.0.0.0") + @upstream { + query: Query +} + +type Query { + posts: [Post] @grpc( + service: "PostService", + method: "GetPosts" + ) +} + +type User { + id: Int! + name: String! + username: String! + email: String! + phone: String + website: String +} + +type Post { + id: Int! + userId: Int! + title: String! + body: String! + user: User @grpc( + service: "UserService", + method: "GetUsers", + request: [{key: "id", value: "{{args.id}}"}] + ) +} \ No newline at end of file diff --git a/keys/config.toml b/keys/config.toml new file mode 100644 index 0000000000..c6082cb029 --- /dev/null +++ b/keys/config.toml @@ -0,0 +1,7 @@ +#TODO: add script to generate TLS cert and key; for development purposes run the following command: +# openssl req -x509 -newkey rsa:4096 -keyout keys/key.pem -out keys/cert.pem -days 365 -nodes -subj "/CN=localhost" +[grpc_server] +host = "127.0.0.1" +port = 50051 +tls_cert_file = "cert.pem" +tls_key_file = "key.pem" diff --git a/src/blueprint/from_config.rs b/src/blueprint/from_config.rs index 199aec6582..3cb6ad6834 100644 --- a/src/blueprint/from_config.rs +++ b/src/blueprint/from_config.rs @@ -1,3 +1,7 @@ +/*! +* Parse configurations into an internal representation. +* transform the application's configuration into a blueprint for the GraphQL schema. +*/ #![allow(clippy::too_many_arguments)] use std::collections::{BTreeMap, BTreeSet, HashMap}; @@ -56,6 +60,8 @@ pub fn config_blueprint<'a>() -> TryFold<'a, Config, Blueprint, String> { .update(super::compress::compress) } +/// parses and validates the upstream configuration, +/// such as the base URL for HTTP connections. fn to_upstream<'a>() -> TryFold<'a, Config, Upstream, String> { TryFoldConfig::::new(|config, up| { let upstream = up.merge_right(config.upstream.clone()); @@ -82,6 +88,8 @@ pub fn apply_batching(mut blueprint: Blueprint) -> Blueprint { blueprint } +/// converts GraphQL directives from the configuration +/// into a valid Directive structure that can be used by the application. fn to_directive(const_directive: ConstDirective) -> Valid { const_directive .arguments @@ -99,6 +107,8 @@ fn to_directive(const_directive: ConstDirective) -> Valid { .into() } +/// transforms the GraphQL schema configuration into a SchemaDefinition, +/// validating the presence of the query root and handling server directives. fn to_schema<'a>() -> TryFoldConfig<'a, SchemaDefinition> { TryFoldConfig::new(|config, _| { validate_query(config) diff --git a/src/blueprint/into_schema.rs b/src/blueprint/into_schema.rs index 5de72854a3..1462fe2f83 100644 --- a/src/blueprint/into_schema.rs +++ b/src/blueprint/into_schema.rs @@ -1,3 +1,8 @@ +/*! + * + * This module is responsible for converting configuration into a GraphQL schema. + * +*/ use std::borrow::Cow; use std::sync::Arc; diff --git a/src/blueprint/server.rs b/src/blueprint/server.rs index ac4dededc0..84ad08e69b 100644 --- a/src/blueprint/server.rs +++ b/src/blueprint/server.rs @@ -1,3 +1,6 @@ +/*! +* relate to server configuration or server-specific aspects of the blueprint. +*/ use std::collections::BTreeMap; use std::net::{AddrParseError, IpAddr}; diff --git a/src/cli/tc.rs b/src/cli/tc.rs index 08e05be351..e696fa28ab 100644 --- a/src/cli/tc.rs +++ b/src/cli/tc.rs @@ -13,6 +13,7 @@ use super::command::{Cli, Command}; use crate::blueprint::Blueprint; use crate::cli::fmt::Fmt; use crate::config::Config; +use crate::grpc::start_grpc_server; use crate::http::start_server; use crate::print_schema; @@ -25,7 +26,12 @@ pub async fn run() -> Result<()> { .filter_level(log_level.unwrap_or(Level::Info).to_level_filter()) .init(); let config = Config::from_file_paths(file_path.iter()).await?; - start_server(config).await?; + if config.server.enable_grpc.is_some() { + // TODO: load config.toml for tls-based auth then start grpc server + start_grpc_server(&config).await?; + } else { + start_server(config).await?; + } Ok(()) } Command::Check { file_path, n_plus_one_queries, schema } => { diff --git a/src/config/config.rs b/src/config/config.rs index 21af404bc1..100a3a32a9 100644 --- a/src/config/config.rs +++ b/src/config/config.rs @@ -218,6 +218,7 @@ pub struct Field { pub modify: Option, pub inline: Option, pub http: Option, + pub grpc: Option, #[serde(rename = "unsafe")] pub unsafe_operation: Option, pub const_field: Option, @@ -225,13 +226,16 @@ pub struct Field { impl Field { pub fn has_resolver(&self) -> bool { - self.http.is_some() || self.unsafe_operation.is_some() || self.const_field.is_some() + self.http.is_some() || self.unsafe_operation.is_some() || self.const_field.is_some() || self.grpc.is_some() } pub fn resolvable_directives(&self) -> Vec<&str> { let mut directives = Vec::with_capacity(3); if self.http.is_some() { directives.push("@http") } + if self.grpc.is_some() { + directives.push("@grpc") + } if self.unsafe_operation.is_some() { directives.push("@unsafe") } @@ -314,6 +318,19 @@ pub struct Http { pub group_by: Vec, } +/// grpc definition in graphql schema config. +#[derive(Serialize, Deserialize, Clone, Debug, Default)] +pub struct Grpc { + /// The name of a grpc service. + pub service: String, + /// The name of a grpc method. + pub method: String, + /// grpc query args. + #[serde(default)] + #[serde(skip_serializing_if = "is_default")] + pub request: KeyValues, +} + #[derive(Serialize, Deserialize, Clone, Debug)] pub struct ConstField { pub data: Value, diff --git a/src/config/from_document.rs b/src/config/from_document.rs index f4c75a98eb..dc1fc0375e 100644 --- a/src/config/from_document.rs +++ b/src/config/from_document.rs @@ -214,6 +214,7 @@ fn to_common_field( modify, inline, http, + grpc: None, unsafe_operation, const_field, } @@ -288,6 +289,15 @@ fn to_http(directives: &[Positioned]) -> Valid]) -> Valid, String> { + for directive in directives { + if directive.node.name.node == "grpc" { + return config::Grpc::from_directive(&directive.node).map(Some); + } + } + Valid::succeed(None) +} + fn to_union(union_type: UnionType, doc: &Option) -> Union { let types = union_type .members diff --git a/src/config/server.rs b/src/config/server.rs index b661521e41..dcf0555598 100644 --- a/src/config/server.rs +++ b/src/config/server.rs @@ -22,6 +22,7 @@ pub struct Server { pub vars: KeyValues, #[serde(skip_serializing_if = "is_default", default)] pub response_headers: KeyValues, + pub enable_grpc: Option, } #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Setters)] diff --git a/src/grpc/context.rs b/src/grpc/context.rs new file mode 100644 index 0000000000..d096174f64 --- /dev/null +++ b/src/grpc/context.rs @@ -0,0 +1,4 @@ +/*! + * gRPC Context + * + */ diff --git a/src/grpc/data_loader.rs b/src/grpc/data_loader.rs new file mode 100644 index 0000000000..eadef92f91 --- /dev/null +++ b/src/grpc/data_loader.rs @@ -0,0 +1,3 @@ +/*! + * gRPC data loader + */ diff --git a/src/grpc/mod.rs b/src/grpc/mod.rs new file mode 100644 index 0000000000..a86c23aca3 --- /dev/null +++ b/src/grpc/mod.rs @@ -0,0 +1,5 @@ +mod context; +mod data_loader; +mod reflection_client; +mod server; +pub use server::start_server as start_grpc_server; diff --git a/src/grpc/reflection_client.rs b/src/grpc/reflection_client.rs new file mode 100644 index 0000000000..45a95db15a --- /dev/null +++ b/src/grpc/reflection_client.rs @@ -0,0 +1,91 @@ +use hyper::http::uri::InvalidUri; +use hyper_tls::HttpsConnector; +use tokio_stream::StreamExt; +use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity}; +use tonic_reflection::pb::{ + server_reflection_client::ServerReflectionClient, server_reflection_request, server_reflection_response, + ListServiceResponse, ServerReflectionRequest, +}; + +/// Fetch service name and method name from a given grpc server dynamically +pub async fn fetch_service_schema( + address: &str, + service_name: &str, + /*tls_config: Option<(String, Identity)>*/ +) -> Result, tonic::Status> { + let https = HttpsConnector::new(); + let channel = match Channel::builder( + address + .parse() + .map_err(|e: InvalidUri| tonic::Status::internal(e.to_string()))?, + ) + .connect_with_connector(https) + .await + { + Ok(it) => it, + Err(err) => return Err(tonic::Status::internal(format!("Transport error: {}", err))), + }; + // let channel = setup_channel(address, tls_config); + + let mut client = ServerReflectionClient::new(channel); + + let request = ServerReflectionRequest { + host: "".into(), + message_request: Some(server_reflection_request::MessageRequest::ListServices("".into())), + ..Default::default() + }; + + let response = client + .server_reflection_info(tonic::Request::new(tokio_stream::once(request))) + .await?; + + let mut response_stream = response.into_inner(); + + let mut services = Vec::new(); + + while let Some(result) = response_stream.next().await { + match result { + Ok(resp) => { + if let Some(msg_resp) = resp.message_response { + match msg_resp { + server_reflection_response::MessageResponse::ListServicesResponse(ListServiceResponse { + service: inner_services, + }) => { + for service in inner_services { + services.push(service.name); + } + } + _ => {} + } + } + } + Err(e) => { + eprintln!("Error fetching service schema: {}", e); + } + } + } + + // Filter the services based on the provided service_name + services = services.into_iter().filter(|name| name == service_name).collect(); + + Ok(services) +} + +//TODO: move this to utils, may change function signature! - util fns +pub async fn setup_channel(address: &str, tls_config: Option<(String, Identity)>) -> Result { + let mut endpoint = Endpoint::from_shared(address.to_string()).map_err(|e| tonic::Status::internal(e.to_string()))?; + + if let Some((cert, identity)) = tls_config { + let cert = Certificate::from_pem(cert); + endpoint = endpoint + .tls_config(ClientTlsConfig::new().identity(identity).ca_certificate(cert)) + .map_err(|e| tonic::Status::internal(e.to_string()))?; + } + + let channel = endpoint + .connect() + .await + .map_err(|err| tonic::Status::internal(format!("Transport error: {}", err)))?; + + Ok(channel) +} diff --git a/src/grpc/server.rs b/src/grpc/server.rs new file mode 100644 index 0000000000..88f3e9525a --- /dev/null +++ b/src/grpc/server.rs @@ -0,0 +1,10 @@ +use hyper::service::{make_service_fn, service_fn}; +use std::sync::Arc; + +use crate::{blueprint::Blueprint, cli::CLIError, config::Config}; + +/// Initialize the gRPC server with TLS configuration if provided +pub async fn start_server(&config: Config) -> Result<()> { + let blueprint = Blueprint::try_from(&config).map_err(CLIError::from)?; + todo!() +} diff --git a/src/lib.rs b/src/lib.rs index 29b19251ff..4e6381bf59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,10 @@ pub mod config; pub mod directive; pub mod document; pub mod endpoint; +pub mod grpc; pub mod has_headers; pub mod http; + #[cfg(feature = "unsafe-js")] pub mod javascript; pub mod json;