585 lines
19 KiB
Java
585 lines
19 KiB
Java
/*
|
|
* 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.io.UnsupportedEncodingException;
|
|
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.B2CConverter;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @return The current encoding
|
|
*
|
|
* @deprecated This method will be removed in Tomcat 9.0.x
|
|
*/
|
|
@Deprecated
|
|
public String getEncoding() {
|
|
return charset.name();
|
|
}
|
|
|
|
public Charset getCharset() {
|
|
return charset;
|
|
}
|
|
|
|
/**
|
|
* @param s The new encoding
|
|
*
|
|
* @deprecated This method will be removed in Tomcat 9.0.x
|
|
*/
|
|
@Deprecated
|
|
public void setEncoding(String s) {
|
|
setCharset(getCharset(s, DEFAULT_BODY_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());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param s The new query string encoding
|
|
*
|
|
* @deprecated This method will be removed in Tomcat 9
|
|
*/
|
|
@Deprecated
|
|
public void setQueryStringEncoding(String s) {
|
|
setQueryStringCharset(getCharset(s, DEFAULT_URI_CHARSET));
|
|
}
|
|
|
|
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();
|
|
/**
|
|
* @deprecated This will be removed in Tomcat 9.0.x
|
|
*/
|
|
@Deprecated
|
|
public static final String DEFAULT_ENCODING = "ISO-8859-1";
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* @param data Parameter data
|
|
* @param encoding Encoding to use for encoded bytes
|
|
*
|
|
* @deprecated This method will be removed in Tomcat 9.0.x
|
|
*/
|
|
@Deprecated
|
|
public void processParameters(MessageBytes data, String encoding) {
|
|
processParameters(data, getCharset(encoding, DEFAULT_BODY_CHARSET));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private Charset getCharset(String encoding, Charset defaultCharset) {
|
|
if (encoding == null) {
|
|
return defaultCharset;
|
|
}
|
|
try {
|
|
return B2CConverter.getCharset(encoding);
|
|
} catch (UnsupportedEncodingException e) {
|
|
return defaultCharset;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
}
|