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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/main/java/graphql/ParseAndValidate.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import graphql.language.Document;
import graphql.parser.InvalidSyntaxException;
import graphql.parser.Parser;
import graphql.parser.ParserOptions;
import graphql.schema.GraphQLSchema;
import graphql.validation.ValidationError;
import graphql.validation.Validator;
Expand All @@ -23,6 +24,7 @@ public class ParseAndValidate {
*
* @param graphQLSchema the schema to validate against
* @param executionInput the execution input containing the query
*
* @return a result object that indicates how this operation went
*/
public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchema, ExecutionInput executionInput) {
Expand All @@ -38,12 +40,16 @@ public static ParseAndValidateResult parseAndValidate(GraphQLSchema graphQLSchem
* This can be called to parse (but not validate) a graphql query.
*
* @param executionInput the input containing the query
*
* @return a result object that indicates how this operation went
*/
public static ParseAndValidateResult parse(ExecutionInput executionInput) {
try {
//
// we allow the caller to specify new parser options by context
ParserOptions parserOptions = executionInput.getGraphQLContext().get(ParserOptions.class);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParserOptions under the key ParserOptions.class

Parser parser = new Parser();
Document document = parser.parseDocument(executionInput.getQuery());
Document document = parser.parseDocument(executionInput.getQuery(), parserOptions);
return ParseAndValidateResult.newResult().document(document).variables(executionInput.getVariables()).build();
} catch (InvalidSyntaxException e) {
return ParseAndValidateResult.newResult().syntaxException(e).variables(executionInput.getVariables()).build();
Expand All @@ -55,6 +61,7 @@ public static ParseAndValidateResult parse(ExecutionInput executionInput) {
*
* @param graphQLSchema the graphql schema to validate against
* @param parsedDocument the previously parsed document
*
* @return a result object that indicates how this operation went
*/
public static List<ValidationError> validate(GraphQLSchema graphQLSchema, Document parsedDocument) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/graphql/PublicSpi.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* The guarantee is for callers of code with this annotation as well as derivations that inherit / implement this code.
*
* New methods will not be added (without using default methods say) that would nominally breaks SPI implementations
* New methods will not be added (without using default methods say) that would nominally break SPI implementations
* within a major release.
*/
@Retention(RetentionPolicy.RUNTIME)
Expand Down
29 changes: 25 additions & 4 deletions src/main/java/graphql/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public Document parseDocument(Reader reader, ParserOptions parserOptions) throws
return parseDocumentImpl(reader, parserOptions);
}

private Document parseDocumentImpl(Reader reader, ParserOptions parserOptions) throws InvalidSyntaxException, ParseCancelledException {
private Document parseDocumentImpl(Reader reader, ParserOptions parserOptions) throws InvalidSyntaxException {
BiFunction<GraphqlParser, GraphqlAntlrToLanguage, Object[]> nodeFunction = (parser, toLanguage) -> {
GraphqlParser.DocumentContext documentContext = parser.document();
Document doc = toLanguage.createDocument(documentContext);
Expand Down Expand Up @@ -238,21 +238,42 @@ public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int
}

private void setupParserListener(MultiSourceReader multiSourceReader, GraphqlParser parser, GraphqlAntlrToLanguage toLanguage) {
int maxTokens = toLanguage.getParserOptions().getMaxTokens();
ParserOptions parserOptions = toLanguage.getParserOptions();
ParsingListener parsingListener = parserOptions.getParsingListener();
int maxTokens = parserOptions.getMaxTokens();
// prevent a billion laugh attacks by restricting how many tokens we allow
ParseTreeListener listener = new GraphqlBaseListener() {
int count = 0;

@Override
public void visitTerminal(TerminalNode node) {

final Token token = node.getSymbol();
parsingListener.onToken(new ParsingListener.Token() {
@Override
public String getText() {
return token == null ? null : token.getText();
}

@Override
public int getLine() {
return token == null ? -1 : token.getLine();
}

@Override
public int getCharPositionInLine() {
return token == null ? -1 : token.getCharPositionInLine();
}
});

count++;
if (count > maxTokens) {
String msg = String.format("More than %d parse tokens have been presented. To prevent Denial Of Service attacks, parsing has been cancelled.", maxTokens);
SourceLocation sourceLocation = null;
String offendingToken = null;
if (node.getSymbol() != null) {
if (token != null) {
offendingToken = node.getText();
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, node.getSymbol().getLine(), node.getSymbol().getCharPositionInLine());
sourceLocation = AntlrHelper.createSourceLocation(multiSourceReader, token.getLine(), token.getCharPositionInLine());
}

throw new ParseCancelledException(msg, sourceLocation, offendingToken);
Expand Down
22 changes: 18 additions & 4 deletions src/main/java/graphql/parser/ParserOptions.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package graphql.parser;

import graphql.Assert;
import graphql.PublicApi;

import java.util.function.Consumer;

import static graphql.Assert.assertNotNull;

/**
* Options that control how the {@link Parser} behaves.
*/
Expand Down Expand Up @@ -60,17 +61,19 @@ public static ParserOptions getDefaultParserOptions() {
* @see graphql.language.SourceLocation
*/
public static void setDefaultParserOptions(ParserOptions options) {
defaultJvmParserOptions = Assert.assertNotNull(options);
defaultJvmParserOptions = assertNotNull(options);
}

private final boolean captureIgnoredChars;
private final boolean captureSourceLocation;
private final int maxTokens;
private final ParsingListener parsingListener;

private ParserOptions(Builder builder) {
this.captureIgnoredChars = builder.captureIgnoredChars;
this.captureSourceLocation = builder.captureSourceLocation;
this.maxTokens = builder.maxTokens;
this.parsingListener = builder.parsingListener;
}

/**
Expand All @@ -97,8 +100,8 @@ public boolean isCaptureSourceLocation() {
}

/**
* An graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn
* memory representing a document that wont ever execute. To prevent this you can set a maximum number of parse
* A graphql hacking vector is to send nonsensical queries that burn lots of parsing CPU time and burn
* memory representing a document that won't ever execute. To prevent this you can set a maximum number of parse
* tokens that will be accepted before an exception is thrown and the parsing is stopped.
*
* @return the maximum number of raw tokens the parser will accept, after which an exception will be thrown.
Expand All @@ -107,6 +110,10 @@ public int getMaxTokens() {
return maxTokens;
}

public ParsingListener getParsingListener() {
return parsingListener;
}

public ParserOptions transform(Consumer<Builder> builderConsumer) {
Builder builder = new Builder(this);
builderConsumer.accept(builder);
Expand All @@ -122,6 +129,7 @@ public static class Builder {
private boolean captureIgnoredChars = false;
private boolean captureSourceLocation = true;
private int maxTokens = MAX_QUERY_TOKENS;
private ParsingListener parsingListener = ParsingListener.NOOP;

Builder() {
}
Expand All @@ -130,6 +138,7 @@ public static class Builder {
this.captureIgnoredChars = parserOptions.captureIgnoredChars;
this.captureSourceLocation = parserOptions.captureSourceLocation;
this.maxTokens = parserOptions.maxTokens;
this.parsingListener = parserOptions.parsingListener;
}

public Builder captureIgnoredChars(boolean captureIgnoredChars) {
Expand All @@ -147,6 +156,11 @@ public Builder maxTokens(int maxTokens) {
return this;
}

public Builder parsingListener(ParsingListener parsingListener) {
this.parsingListener = assertNotNull(parsingListener);
return this;
}

public ParserOptions build() {
return new ParserOptions(this);
}
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/graphql/parser/ParsingListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package graphql.parser;

import graphql.PublicSpi;

/**
* This listener interface is invoked for each token parsed by the graphql parser code.
*/
@PublicSpi
public interface ParsingListener {

/**
* A NoOp implementation of {@link ParsingListener}
*/
ParsingListener NOOP = t -> {
};


/**
* This represents a token that has been parsed
*/
interface Token {
/**
* @return the text of the parsed token
*/
String getText();

/**
* @return the line the token occurred on
*/
int getLine();

/**
* @return the position within the line the token occurred on
*/
int getCharPositionInLine();
}

/**
* This is called for each token found during parsing
*
* @param token the token found
*/
void onToken(Token token);
}
39 changes: 38 additions & 1 deletion src/test/groovy/graphql/parser/ParserTest.groovy
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package graphql.parser


import graphql.ExecutionInput
import graphql.TestUtil
import graphql.language.Argument
import graphql.language.ArrayValue
Expand Down Expand Up @@ -1104,4 +1104,41 @@ triple3 : """edge cases \\""" "" " \\"" \\" edge cases"""
then:
doc != null
}

def "they can set their own listener into action"() {
def queryText = "query { f(arg : 1) }"

def count = 0
def tokens = []
ParsingListener listener = { count++; tokens.add(it.getText()) }
def parserOptions = ParserOptions.newParserOptions().parsingListener(listener).build()
when:
def doc = new Parser().parseDocument(queryText, parserOptions)

then:
doc != null
count == 9
tokens == ["query" , "{", "f" , "(", "arg", ":", "1", ")", "}"]

when: "integration test to prove it be supplied via EI"

def sdl = """type Query { f(arg : Int) : ID} """
def graphQL = TestUtil.graphQL(sdl).build()


def context = [:]
context.put(ParserOptions.class, parserOptions)
def executionInput = ExecutionInput.newExecutionInput()
.query(queryText)
.graphQLContext(context).build()

count = 0
tokens = []
def er = graphQL.execute(executionInput)
then:
er.errors.size() == 0
count == 9
tokens == ["query" , "{", "f" , "(", "arg", ":", "1", ")", "}"]

}
}