/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2013 The ZAP Development team
*
* 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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.parosproxy.paros.core.scanner;
import org.apache.commons.lang.StringEscapeUtils;
/**
*
* @author andy
*/
public class VariantJSONQuery extends VariantAbstractRPCQuery {
public static final String JSON_RPC_CONTENT_TYPE = "application/json";
public static final int NAME_SEPARATOR = ':';
public static final int VALUE_SEPARATOR = ',';
public static final int BEGIN_ARRAY = '[';
public static final int QUOTATION_MARK = '"';
public static final int BEGIN_OBJECT = '{';
public static final int END_OBJECT = '}';
public static final int END_ARRAY = ']';
private SimpleStringReader sr;
/**
*
* @param contentType
* @return
*/
@Override
public boolean isValidContentType(String contentType) {
return contentType.startsWith(JSON_RPC_CONTENT_TYPE);
}
/**
*
* @param content
*/
@Override
public void parseContent(String content) {
sr = new SimpleStringReader(content);
parseObject();
}
/**
*
* @param value
* @param toQuote
* @return
*/
@Override
public String getEscapedValue(String value, boolean toQuote) {
String result = StringEscapeUtils.escapeJava(value);
return (toQuote) ? VariantJSONQuery.QUOTATION_MARK + result + VariantJSONQuery.QUOTATION_MARK : result;
}
@Override
public String getUnescapedValue(String value) {
return StringEscapeUtils.unescapeJava(value);
}
// --------------------------------------------------------------------
private static final int STATE_READ_START_OBJECT = 0;
private static final int STATE_READ_FIELD = 1;
private static final int STATE_READ_VALUE = 2;
private static final int STATE_READ_POST_VALUE = 3;
private void parseObject() {
int state = STATE_READ_START_OBJECT;
boolean objectRead = false;
boolean done = false;
String field = null;
int beginToken;
int endToken;
int chr;
while (!done) {
switch (state) {
case STATE_READ_START_OBJECT:
chr = sr.skipWhitespaceRead();
if (chr == BEGIN_OBJECT) {
objectRead = true;
chr = sr.skipWhitespaceRead();
if (chr == END_OBJECT) { // empty object
return;
}
sr.unreadLastCharacter();
state = STATE_READ_FIELD;
} else if (chr == BEGIN_ARRAY) {
sr.unreadLastCharacter();
state = STATE_READ_VALUE;
} else {
throw new IllegalArgumentException("Input is invalid JSON; does not start with '{' or '[', c=" + chr);
}
break;
case STATE_READ_FIELD:
chr = sr.skipWhitespaceRead();
if (chr == QUOTATION_MARK) {
beginToken = sr.getPosition();
while ((chr = sr.read()) != QUOTATION_MARK) {
if (chr == -1)
throw new IllegalArgumentException("EOF reached while reading JSON field name");
}
endToken = sr.getPosition();
// Now we have the string object name
// we can do something here for value filtering...
field = getToken(beginToken, endToken);
chr = sr.skipWhitespaceRead();
if (chr != NAME_SEPARATOR) {
throw new IllegalArgumentException("Expected ':' between string field and value at position " + sr.getPosition());
}
sr.skipWhitespaceRead();
sr.unreadLastCharacter();
state = STATE_READ_VALUE;
} else {
throw new IllegalArgumentException("Expected quote at position " + sr.getPosition());
}
break;
case STATE_READ_VALUE:
if (field == null) {
// field is null when you have an untyped Object[], so we place
// the JsonArray on the @items field.
field = "@items";
}
parseValue(field);
state = STATE_READ_POST_VALUE;
break;
case STATE_READ_POST_VALUE:
chr = sr.skipWhitespaceRead();
if (chr == -1 && objectRead) {
throw new IllegalArgumentException("EOF reached before closing '}'");
}
if (chr == END_OBJECT || chr == -1) {
done = true;
} else if (chr == VALUE_SEPARATOR) {
state = STATE_READ_FIELD;
} else {
throw new IllegalArgumentException("Object not ended with '}' or ']' at position " + sr.getPosition());
}
break;
}
}
}
/**
*
* @param sr
*/
private void parseValue(String fieldName) {
int chr = sr.read();
// Check if the value is a string
if (chr == QUOTATION_MARK) {
int beginToken = sr.getPosition();
while ((chr = sr.read()) != QUOTATION_MARK) {
if (chr == -1) {
throw new IllegalArgumentException("EOF reached while reading JSON field name");
}
}
// Now we have the string object value
// Put everything inside the parameter array
addParameter(fieldName, beginToken, sr.getPosition()-1, false, true);
// check if the value is a number
} else if (Character.isDigit(chr) || chr == '-') {
sr.unreadLastCharacter();
int beginToken = sr.getPosition();
do {
chr = sr.read();
if (chr == -1) {
throw new IllegalArgumentException("Reached EOF while reading number");
}
} while (Character.isDigit(chr) ||
(chr == '.') || (chr == 'e') || (chr == 'E') ||
(chr == '+') || (chr == '-'));
sr.unreadLastCharacter();
// Now we have the int object value
// Put everything inside the parameter array
addParameter(fieldName, beginToken, sr.getPosition()-1, true, false);
} else if (chr == BEGIN_OBJECT) {
sr.unreadLastCharacter();
parseObject();
} else if (chr == BEGIN_ARRAY) {
parseArray(fieldName);
} else if (chr == END_ARRAY) { // [] empty array
sr.unreadLastCharacter();
} else if (chr == 't' || chr == 'T') {
sr.unreadLastCharacter();
parseToken("true");
} else if (chr == 'f' || chr == 'F') {
sr.unreadLastCharacter();
parseToken("false");
} else if (chr == 'n' || chr == 'N') {
sr.unreadLastCharacter();
parseToken("null");
} else if (chr == -1) {
throw new IllegalArgumentException("EOF reached prematurely");
} else {
throw new IllegalArgumentException("Unknown value type at position " + sr.getPosition());
}
}
/**
* Read a JSON array
*/
private void parseArray(String fieldName) {
int chr;
int idx = 0;
while (true) {
sr.skipWhitespaceRead();
sr.unreadLastCharacter();
parseValue(fieldName + "[" + (idx++) + "]");
chr = sr.skipWhitespaceRead();
if (chr == END_ARRAY) {
break;
}
if (chr != VALUE_SEPARATOR) {
throw new IllegalArgumentException("Expected ',' or ']' inside array at position " + sr.getPosition());
}
}
}
/**
* Return the specified token from the reader. If it is not found, throw an
* IOException indicating that. Converting to chr to (char) chr is acceptable
* because the 'tokens' allowed in a JSON input stream (true, false, null)
* are all ASCII.
*/
private void parseToken(String token) {
int len = token.length();
for (int i = 0; i < len; i++) {
int chr = sr.read();
if (chr == -1) {
throw new IllegalArgumentException("EOF reached while reading token: " + token);
}
chr = Character.toLowerCase((char)chr);
int loTokenChar = token.charAt(i);
if (loTokenChar != chr) {
throw new IllegalArgumentException("Expected token: " + token + " at position " + sr.getPosition());
}
}
}
protected class SimpleStringReader {
private static final String WS = " \t\r\n";
private String str;
private int length;
private int next = 0;
/**
* Creates a new string reader.
*
* @param s String providing the character stream.
*/
public SimpleStringReader(String s) {
this.str = s;
this.length = s.length();
}
/**
* Read until non-whitespace character and then return it. This saves
* extra read/pushback.
*
* @return int repesenting the next non-whitespace character in the
* stream.
*/
public int skipWhitespaceRead() {
int c = read();
while (WS.indexOf(c) != -1) {
c = read();
}
return c;
}
/**
* Reads a single character.
*
* @return The character read, or -1 if the end of the stream has been
* reached
*/
public int read() {
if (next >= length) {
return -1;
}
return str.charAt(next++);
}
public void unreadLastCharacter() {
next--;
}
/**
*
* @return
*/
public int getPosition() {
return next;
}
}
}