/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.tomcat.util.http;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.buf.StringUtils;
import org.apache.tomcat.util.buf.UDecoder;
import org.apache.tomcat.util.log.UserDataHelper;
import org.apache.tomcat.util.res.StringManager;
/**
*
* @author Costin Manolache
*/
public final class Parameters {
private static final Log log = LogFactory.getLog(Parameters.class);
private static final UserDataHelper userDataLog = new UserDataHelper(log);
private static final UserDataHelper maxParamCountLog = new UserDataHelper(log);
private static final StringManager sm =
StringManager.getManager("org.apache.tomcat.util.http");
private final Map<String,ArrayList<String>> paramHashValues =
new LinkedHashMap<>();
private boolean didQueryParameters=false;
private MessageBytes queryMB;
private UDecoder urlDec;
private final MessageBytes decodedQuery = MessageBytes.newInstance();
private Charset charset = StandardCharsets.ISO_8859_1;
private Charset queryStringCharset = StandardCharsets.UTF_8;
private int limit = -1;
private int parameterCount = 0;
/**
* Set to the reason for the failure (the first failure if there is more
* than one) if there were failures during parameter parsing.
*/
private FailReason parseFailedReason = null;
public Parameters() {
// NO-OP
}
public void setQuery( MessageBytes queryMB ) {
this.queryMB=queryMB;
}
public void setLimit(int limit) {
this.limit = limit;
}
public Charset getCharset() {
return charset;
}
public void setCharset(Charset charset) {
if (charset == null) {
charset = DEFAULT_BODY_CHARSET;
}
this.charset = charset;
if(log.isDebugEnabled()) {
log.debug("Set encoding to " + charset.name());
}
}
public void setQueryStringCharset(Charset queryStringCharset) {
if (queryStringCharset == null) {
queryStringCharset = DEFAULT_URI_CHARSET;
}
this.queryStringCharset = queryStringCharset;
if(log.isDebugEnabled()) {
log.debug("Set query string encoding to " + queryStringCharset.name());
}
}
public boolean isParseFailed() {
return parseFailedReason != null;
}
public FailReason getParseFailedReason() {
return parseFailedReason;
}
public void setParseFailedReason(FailReason failReason) {
if (this.parseFailedReason == null) {
this.parseFailedReason = failReason;
}
}
public void recycle() {
parameterCount = 0;
paramHashValues.clear();
didQueryParameters = false;
charset = DEFAULT_BODY_CHARSET;
decodedQuery.recycle();
parseFailedReason = null;
}
// -------------------- Data access --------------------
// Access to the current name/values, no side effect ( processing ).
// You must explicitly call handleQueryParameters and the post methods.
public String[] getParameterValues(String name) {
handleQueryParameters();
// no "facade"
ArrayList<String> values = paramHashValues.get(name);
if (values == null) {
return null;
}
return values.toArray(new String[values.size()]);
}
public Enumeration<String> getParameterNames() {
handleQueryParameters();
return Collections.enumeration(paramHashValues.keySet());
}
public String getParameter(String name ) {
handleQueryParameters();
ArrayList<String> values = paramHashValues.get(name);
if (values != null) {
if(values.size() == 0) {
return "";
}
return values.get(0);
} else {
return null;
}
}
// -------------------- Processing --------------------
/** Process the query string into parameters
*/
public void handleQueryParameters() {
if (didQueryParameters) {
return;
}
didQueryParameters = true;
if (queryMB == null || queryMB.isNull()) {
return;
}
if(log.isDebugEnabled()) {
log.debug("Decoding query " + decodedQuery + " " + queryStringCharset.name());
}
try {
decodedQuery.duplicate(queryMB);
} catch (IOException e) {
// Can't happen, as decodedQuery can't overflow
e.printStackTrace();
}
processParameters(decodedQuery, queryStringCharset);
}
public void addParameter( String key, String value )
throws IllegalStateException {
if( key==null ) {
return;
}
parameterCount ++;
if (limit > -1 && parameterCount > limit) {
// Processing this parameter will push us over the limit. ISE is
// what Request.parseParts() uses for requests that are too big
setParseFailedReason(FailReason.TOO_MANY_PARAMETERS);
throw new IllegalStateException(sm.getString(
"parameters.maxCountFail", Integer.valueOf(limit)));
}
ArrayList<String> values = paramHashValues.get(key);
if (values == null) {
values = new ArrayList<>(1);
paramHashValues.put(key, values);
}
values.add(value);
}
public void setURLDecoder( UDecoder u ) {
urlDec=u;
}
// -------------------- Parameter parsing --------------------
// we are called from a single thread - we can do it the hard way
// if needed
private final ByteChunk tmpName=new ByteChunk();
private final ByteChunk tmpValue=new ByteChunk();
private final ByteChunk origName=new ByteChunk();
private final ByteChunk origValue=new ByteChunk();
private static final Charset DEFAULT_BODY_CHARSET = StandardCharsets.ISO_8859_1;
private static final Charset DEFAULT_URI_CHARSET = StandardCharsets.UTF_8;
public void processParameters( byte bytes[], int start, int len ) {
processParameters(bytes, start, len, charset);
}
private void processParameters(byte bytes[], int start, int len, Charset charset) {
if(log.isDebugEnabled()) {
log.debug(sm.getString("parameters.bytes",
new String(bytes, start, len, DEFAULT_BODY_CHARSET)));
}
int decodeFailCount = 0;
int pos = start;
int end = start + len;
while(pos < end) {
int nameStart = pos;
int nameEnd = -1;
int valueStart = -1;
int valueEnd = -1;
boolean parsingName = true;
boolean decodeName = false;
boolean decodeValue = false;
boolean parameterComplete = false;
do {
switch(bytes[pos]) {
case '=':
if (parsingName) {
// Name finished. Value starts from next character
nameEnd = pos;
parsingName = false;
valueStart = ++pos;
} else {
// Equals character in value
pos++;
}
break;
case '&':
if (parsingName) {
// Name finished. No value.
nameEnd = pos;
} else {
// Value finished
valueEnd = pos;
}
parameterComplete = true;
pos++;
break;
case '%':
case '+':
// Decoding required
if (parsingName) {
decodeName = true;
} else {
decodeValue = true;
}
pos ++;
break;
default:
pos ++;
break;
}
} while (!parameterComplete && pos < end);
if (pos == end) {
if (nameEnd == -1) {
nameEnd = pos;
} else if (valueStart > -1 && valueEnd == -1){
valueEnd = pos;
}
}
if (log.isDebugEnabled() && valueStart == -1) {
log.debug(sm.getString("parameters.noequal",
Integer.valueOf(nameStart), Integer.valueOf(nameEnd),
new String(bytes, nameStart, nameEnd-nameStart, DEFAULT_BODY_CHARSET)));
}
if (nameEnd <= nameStart ) {
if (valueStart == -1) {
// &&
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.emptyChunk"));
}
// Do not flag as error
continue;
}
// &=foo&
UserDataHelper.Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String extract;
if (valueEnd > nameStart) {
extract = new String(bytes, nameStart, valueEnd - nameStart,
DEFAULT_BODY_CHARSET);
} else {
extract = "";
}
String message = sm.getString("parameters.invalidChunk",
Integer.valueOf(nameStart),
Integer.valueOf(valueEnd), extract);
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString("parameters.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
setParseFailedReason(FailReason.NO_NAME);
continue;
// invalid chunk - it's better to ignore
}
tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
} else {
tmpValue.setBytes(bytes, 0, 0);
}
// Take copies as if anything goes wrong originals will be
// corrupted. This means original values can be logged.
// For performance - only done for debug
if (log.isDebugEnabled()) {
try {
origName.append(bytes, nameStart, nameEnd - nameStart);
if (valueStart >= 0) {
origValue.append(bytes, valueStart, valueEnd - valueStart);
} else {
origValue.append(bytes, 0, 0);
}
} catch (IOException ioe) {
// Should never happen...
log.error(sm.getString("parameters.copyFail"), ioe);
}
}
try {
String name;
String value;
if (decodeName) {
urlDecode(tmpName);
}
tmpName.setCharset(charset);
name = tmpName.toString();
if (valueStart >= 0) {
if (decodeValue) {
urlDecode(tmpValue);
}
tmpValue.setCharset(charset);
value = tmpValue.toString();
} else {
value = "";
}
try {
addParameter(name, value);
} catch (IllegalStateException ise) {
// Hitting limit stops processing further params but does
// not cause request to fail.
UserDataHelper.Mode logMode = maxParamCountLog.getNextMode();
if (logMode != null) {
String message = ise.getMessage();
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString(
"parameters.maxCountFail.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
break;
}
} catch (IOException e) {
setParseFailedReason(FailReason.URL_DECODING);
decodeFailCount++;
if (decodeFailCount == 1 || log.isDebugEnabled()) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("parameters.decodeFail.debug",
origName.toString(), origValue.toString()), e);
} else if (log.isInfoEnabled()) {
UserDataHelper.Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String message = sm.getString(
"parameters.decodeFail.info",
tmpName.toString(), tmpValue.toString());
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString("parameters.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
}
}
}
tmpName.recycle();
tmpValue.recycle();
// Only recycle copies if we used them
if (log.isDebugEnabled()) {
origName.recycle();
origValue.recycle();
}
}
if (decodeFailCount > 1 && !log.isDebugEnabled()) {
UserDataHelper.Mode logMode = userDataLog.getNextMode();
if (logMode != null) {
String message = sm.getString(
"parameters.multipleDecodingFail",
Integer.valueOf(decodeFailCount));
switch (logMode) {
case INFO_THEN_DEBUG:
message += sm.getString("parameters.fallToDebug");
//$FALL-THROUGH$
case INFO:
log.info(message);
break;
case DEBUG:
log.debug(message);
}
}
}
}
private void urlDecode(ByteChunk bc)
throws IOException {
if( urlDec==null ) {
urlDec=new UDecoder();
}
urlDec.convert(bc, true);
}
public void processParameters(MessageBytes data, Charset charset) {
if( data==null || data.isNull() || data.getLength() <= 0 ) {
return;
}
if( data.getType() != MessageBytes.T_BYTES ) {
data.toBytes();
}
ByteChunk bc=data.getByteChunk();
processParameters(bc.getBytes(), bc.getOffset(), bc.getLength(), charset);
}
/**
* Debug purpose
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, ArrayList<String>> e : paramHashValues.entrySet()) {
sb.append(e.getKey()).append('=');
StringUtils.join(e.getValue(), ',', sb);
sb.append('\n');
}
return sb.toString();
}
public enum FailReason {
CLIENT_DISCONNECT,
MULTIPART_CONFIG_INVALID,
INVALID_CONTENT_TYPE,
IO_ERROR,
NO_NAME,
POST_TOO_LARGE,
REQUEST_BODY_INCOMPLETE,
TOO_MANY_PARAMETERS,
UNKNOWN,
URL_DECODING
}
}