diff --git a/Cargo.toml b/Cargo.toml index 087a8463..42753f52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "1.5.1" +version = "1.5.2" authors = ["sunli "] edition = "2018" description = "The GraphQL server library implemented by rust" @@ -18,7 +18,7 @@ default = ["bson", "chrono", "uuid", "url", "validators"] validators = ["regex", "once_cell"] [dependencies] -async-graphql-derive = { path = "async-graphql-derive", version = "1.5.1" } +async-graphql-derive = { path = "async-graphql-derive", version = "1.5.2" } graphql-parser = "0.2.3" anyhow = "1.0.26" thiserror = "1.0.11" diff --git a/README.md b/README.md index 2ab06905..8a7aec64 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,9 @@ Open `http://localhost:8000` in browser - [X] FIELD - [X] FRAGMENT_SPREAD - [X] INLINE_FRAGMENT - - [X] Schema + - [X] Introspection + - [X] Query + - [X] Disable introspection - [X] Multipart Request (https://github.com/jaydenseric/graphql-multipart-request-spec) - [X] Actix-web - [X] Cursor Connections @@ -118,6 +120,8 @@ Open `http://localhost:8000` in browser - [X] List - [X] ListMaxLength - [X] ListMinLength + - [ ] Limit query complexity + - [ ] Limit query depth - [X] Subscription - [X] Filter - [X] WebSocket transport diff --git a/async-graphql-actix-web/Cargo.toml b/async-graphql-actix-web/Cargo.toml index ec37824b..10821cb0 100644 --- a/async-graphql-actix-web/Cargo.toml +++ b/async-graphql-actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "0.6.1" +version = "0.6.2" authors = ["sunli "] edition = "2018" description = "async-graphql for actix-web" @@ -13,7 +13,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "..", version = "1.5.1" } +async-graphql = { path = "..", version = "1.5.2" } actix-web = "2.0.0" actix-multipart = "0.2.0" actix-web-actors = "2.0.0" diff --git a/async-graphql-derive/Cargo.toml b/async-graphql-derive/Cargo.toml index 97c18c3f..712e3ee6 100644 --- a/async-graphql-derive/Cargo.toml +++ b/async-graphql-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "1.5.1" +version = "1.5.2" authors = ["sunli "] edition = "2018" description = "Macros for async-graphql" diff --git a/src/error.rs b/src/error.rs index 1d6561d8..27f4d557 100644 --- a/src/error.rs +++ b/src/error.rs @@ -148,6 +148,12 @@ pub enum QueryError { /// Actual type actual: String, }, + + #[error("Too complex.")] + TooComplex, + + #[error("Too deep.")] + TooDeep, } /// Creates a wrapper with an error location diff --git a/src/query.rs b/src/query.rs index 0c94dd2c..a2e23da2 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,8 +1,8 @@ use crate::context::Data; use crate::registry::{CacheControl, Registry}; use crate::types::QueryRoot; -use crate::validation::check_rules; -use crate::{ContextBase, OutputValueType, Result}; +use crate::validation::{check_rules, CheckResult}; +use crate::{ContextBase, OutputValueType, Result, Schema}; use crate::{ObjectType, QueryError, QueryParseError, Variables}; use bytes::Bytes; use graphql_parser::parse_query; @@ -17,17 +17,15 @@ enum Root<'a, Query, Mutation> { } /// Query builder -pub struct QueryBuilder<'a, Query, Mutation> { - pub(crate) query: &'a QueryRoot, - pub(crate) mutation: &'a Mutation, - pub(crate) registry: &'a Registry, +pub struct QueryBuilder<'a, Query, Mutation, Subscription> { + pub(crate) schema: &'a Schema, pub(crate) source: &'a str, pub(crate) operation_name: Option<&'a str>, pub(crate) variables: Option, pub(crate) data: &'a Data, } -impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { +impl<'a, Query, Mutation, Subscription> QueryBuilder<'a, Query, Mutation, Subscription> { /// Specify the operation name. pub fn operator_name(self, name: &'a str) -> Self { QueryBuilder { @@ -47,7 +45,23 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { /// Prepare query pub fn prepare(self) -> Result> { let document = parse_query(self.source).map_err(|err| QueryParseError(err.to_string()))?; - let cache_control = check_rules(self.registry, &document)?; + let CheckResult { + cache_control, + complexity, + depth, + } = check_rules(&self.schema.registry, &document)?; + + if let Some(limit_complexity) = self.schema.complexity { + if complexity > limit_complexity { + return Err(QueryError::TooComplex.into()); + } + } + + if let Some(limit_depth) = self.schema.depth { + if depth > limit_depth { + return Err(QueryError::TooDeep.into()); + } + } let mut fragments = HashMap::new(); let mut selection_set = None; @@ -59,14 +73,14 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { Definition::Operation(operation_definition) => match operation_definition { OperationDefinition::SelectionSet(s) => { selection_set = Some(s); - root = Some(Root::Query(self.query)); + root = Some(Root::Query(&self.schema.query)); } OperationDefinition::Query(query) if query.name.is_none() || query.name.as_deref() == self.operation_name => { selection_set = Some(query.selection_set); variable_definitions = Some(query.variable_definitions); - root = Some(Root::Query(self.query)); + root = Some(Root::Query(&self.schema.query)); } OperationDefinition::Mutation(mutation) if mutation.name.is_none() @@ -74,7 +88,7 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { { selection_set = Some(mutation.selection_set); variable_definitions = Some(mutation.variable_definitions); - root = Some(Root::Mutation(self.mutation)); + root = Some(Root::Mutation(&self.schema.mutation)); } OperationDefinition::Subscription(subscription) if subscription.name.is_none() @@ -91,7 +105,7 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { } Ok(PreparedQuery { - registry: self.registry, + registry: &self.schema.registry, variables: self.variables.unwrap_or_default(), data: self.data, fragments, diff --git a/src/schema.rs b/src/schema.rs index 495188d1..8089aec7 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -9,11 +9,13 @@ use std::collections::HashMap; /// GraphQL schema pub struct Schema { - query: QueryRoot, - mutation: Mutation, + pub(crate) query: QueryRoot, + pub(crate) mutation: Mutation, pub(crate) subscription: Subscription, pub(crate) registry: Registry, pub(crate) data: Data, + pub(crate) complexity: Option, + pub(crate) depth: Option, } impl @@ -99,14 +101,37 @@ impl } Self { - query: QueryRoot { inner: query }, + query: QueryRoot { + inner: query, + disable_introspection: false, + }, mutation, subscription, registry, data: Default::default(), + complexity: None, + depth: None, } } + /// Disable introspection query + pub fn disable_introspection(mut self) -> Self { + self.query.disable_introspection = true; + self + } + + /// Set limit complexity, Default no limit. + pub fn limit_complexity(mut self, complexity: usize) -> Self { + self.complexity = Some(complexity); + self + } + + /// Set limit complexity, Default no limit. + pub fn limit_depth(mut self, depth: usize) -> Self { + self.depth = Some(depth); + self + } + /// Add a global data that can be accessed in the `Context`. pub fn data(mut self, data: D) -> Self { self.data.insert(data); @@ -114,11 +139,9 @@ impl } /// Start a query and return `QueryBuilder`. - pub fn query<'a>(&'a self, source: &'a str) -> QueryBuilder<'a, Query, Mutation> { + pub fn query<'a>(&'a self, source: &'a str) -> QueryBuilder<'a, Query, Mutation, Subscription> { QueryBuilder { - query: &self.query, - mutation: &self.mutation, - registry: &self.registry, + schema: self, source, operation_name: None, variables: None, diff --git a/src/types/query_root.rs b/src/types/query_root.rs index beb30f90..1c27a27e 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -1,7 +1,7 @@ use crate::model::{__Schema, __Type}; use crate::{ do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, - OutputValueType, Result, Type, Value, + OutputValueType, QueryError, Result, Type, Value, }; use graphql_parser::query::Field; use std::borrow::Cow; @@ -9,6 +9,7 @@ use std::collections::HashMap; pub struct QueryRoot { pub inner: T, + pub disable_introspection: bool, } impl Type for QueryRoot { @@ -67,6 +68,14 @@ impl Type for QueryRoot { impl ObjectType for QueryRoot { async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result { if field.name.as_str() == "__schema" { + if self.disable_introspection { + return Err(QueryError::FieldNotFound { + field_name: field.name.clone(), + object: Self::type_name().to_string(), + } + .into()); + } + let ctx_obj = ctx.with_item(&field.selection_set); return OutputValueType::resolve( &__Schema { diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 115bb9e5..3ff52e00 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -1,6 +1,7 @@ mod rules; mod utils; mod visitor; +mod visitors; use crate::error::RuleErrors; use crate::registry::Registry; @@ -8,9 +9,18 @@ use crate::{CacheControl, Result}; use graphql_parser::query::Document; use visitor::{visit, VisitorContext, VisitorNil}; -pub fn check_rules(registry: &Registry, doc: &Document) -> Result { +pub struct CheckResult { + pub cache_control: CacheControl, + pub complexity: usize, + pub depth: usize, +} + +pub fn check_rules(registry: &Registry, doc: &Document) -> Result { let mut ctx = VisitorContext::new(registry, doc); let mut cache_control = CacheControl::default(); + let mut complexity = 0; + let mut depth = 0; + let mut visitor = VisitorNil .with(rules::ArgumentsOfCorrectType::default()) .with(rules::DefaultValuesOfCorrectType) @@ -37,14 +47,21 @@ pub fn check_rules(registry: &Registry, doc: &Document) -> Result .with(rules::KnownDirectives::default()) .with(rules::OverlappingFieldsCanBeMerged) .with(rules::UploadFile) - .with(rules::CacheControlCalculate { + .with(visitors::CacheControlCalculate { cache_control: &mut cache_control, - }); + }) + .with(visitors::ComplexityCalculate { + complexity: &mut complexity, + }) + .with(visitors::DepthCalculate::new(&mut depth)); visit(&mut visitor, &mut ctx, doc); if !ctx.errors.is_empty() { - Err(RuleErrors { errors: ctx.errors }.into()) - } else { - Ok(cache_control) + return Err(RuleErrors { errors: ctx.errors }.into()); } + Ok(CheckResult { + cache_control, + complexity, + depth: depth as usize, + }) } diff --git a/src/validation/rules/mod.rs b/src/validation/rules/mod.rs index 386b63c8..a54c00ba 100644 --- a/src/validation/rules/mod.rs +++ b/src/validation/rules/mod.rs @@ -1,5 +1,4 @@ mod arguments_of_correct_type; -mod cache_control; mod default_values_of_correct_type; mod fields_on_correct_type; mod fragments_on_composite_types; @@ -26,7 +25,6 @@ mod variables_are_input_types; mod variables_in_allowed_position; pub use arguments_of_correct_type::ArgumentsOfCorrectType; -pub use cache_control::CacheControlCalculate; pub use default_values_of_correct_type::DefaultValuesOfCorrectType; pub use fields_on_correct_type::FieldsOnCorrectType; pub use fragments_on_composite_types::FragmentsOnCompositeTypes; diff --git a/src/validation/visitor.rs b/src/validation/visitor.rs index 2bed7694..20ec26ad 100644 --- a/src/validation/visitor.rs +++ b/src/validation/visitor.rs @@ -463,11 +463,13 @@ fn visit_selection_set<'a, V: Visitor<'a>>( ctx: &mut VisitorContext<'a>, selection_set: &'a SelectionSet, ) { - v.enter_selection_set(ctx, selection_set); - for selection in &selection_set.items { - visit_selection(v, ctx, selection); + if !selection_set.items.is_empty() { + v.enter_selection_set(ctx, selection_set); + for selection in &selection_set.items { + visit_selection(v, ctx, selection); + } + v.exit_selection_set(ctx, selection_set); } - v.exit_selection_set(ctx, selection_set); } fn visit_selection<'a, V: Visitor<'a>>( @@ -578,6 +580,9 @@ fn visit_fragment_spread<'a, V: Visitor<'a>>( ) { v.enter_fragment_spread(ctx, fragment_spread); visit_directives(v, ctx, &fragment_spread.directives); + if let Some(fragment) = ctx.fragment(fragment_spread.fragment_name.as_str()) { + visit_selection_set(v, ctx, &fragment.selection_set); + } v.exit_fragment_spread(ctx, fragment_spread); } diff --git a/src/validation/rules/cache_control.rs b/src/validation/visitors/cache_control.rs similarity index 100% rename from src/validation/rules/cache_control.rs rename to src/validation/visitors/cache_control.rs diff --git a/src/validation/visitors/complexity.rs b/src/validation/visitors/complexity.rs new file mode 100644 index 00000000..3508c22f --- /dev/null +++ b/src/validation/visitors/complexity.rs @@ -0,0 +1,12 @@ +use crate::validation::visitor::{Visitor, VisitorContext}; +use graphql_parser::query::Field; + +pub struct ComplexityCalculate<'a> { + pub complexity: &'a mut usize, +} + +impl<'ctx, 'a> Visitor<'ctx> for ComplexityCalculate<'a> { + fn enter_field(&mut self, _ctx: &mut VisitorContext<'_>, _field: &Field) { + *self.complexity += 1; + } +} diff --git a/src/validation/visitors/depth.rs b/src/validation/visitors/depth.rs new file mode 100644 index 00000000..9f9c06ba --- /dev/null +++ b/src/validation/visitors/depth.rs @@ -0,0 +1,68 @@ +use crate::validation::visitor::{Visitor, VisitorContext}; +use graphql_parser::query::{FragmentSpread, InlineFragment, SelectionSet}; + +pub struct DepthCalculate<'a> { + max_depth: &'a mut i32, + current_depth: i32, +} + +impl<'a> DepthCalculate<'a> { + pub fn new(max_depth: &'a mut i32) -> Self { + *max_depth = -1; + Self { + max_depth, + current_depth: -1, + } + } +} + +impl<'ctx, 'a> Visitor<'ctx> for DepthCalculate<'a> { + fn enter_selection_set( + &mut self, + _ctx: &mut VisitorContext<'ctx>, + _selection_set: &'ctx SelectionSet, + ) { + self.current_depth += 1; + *self.max_depth = (*self.max_depth).max(self.current_depth); + } + + fn exit_selection_set( + &mut self, + _ctx: &mut VisitorContext<'ctx>, + _selection_set: &'ctx SelectionSet, + ) { + self.current_depth -= 1; + } + + fn enter_fragment_spread( + &mut self, + _ctx: &mut VisitorContext<'ctx>, + _fragment_spread: &'ctx FragmentSpread, + ) { + self.current_depth -= 1; + } + + fn exit_fragment_spread( + &mut self, + _ctx: &mut VisitorContext<'ctx>, + _fragment_spread: &'ctx FragmentSpread, + ) { + self.current_depth += 1; + } + + fn enter_inline_fragment( + &mut self, + _ctx: &mut VisitorContext<'ctx>, + _inline_fragment: &'ctx InlineFragment, + ) { + self.current_depth -= 1; + } + + fn exit_inline_fragment( + &mut self, + _ctx: &mut VisitorContext<'ctx>, + _inline_fragment: &'ctx InlineFragment, + ) { + self.current_depth += 1; + } +} diff --git a/src/validation/visitors/mod.rs b/src/validation/visitors/mod.rs new file mode 100644 index 00000000..03765d98 --- /dev/null +++ b/src/validation/visitors/mod.rs @@ -0,0 +1,7 @@ +mod cache_control; +mod complexity; +mod depth; + +pub use cache_control::CacheControlCalculate; +pub use complexity::ComplexityCalculate; +pub use depth::DepthCalculate;