1 /*
2  *  Licensed to the Apache Software Foundation (ASF) under one or more
3  *  contributor license agreements.  See the NOTICE file distributed with
4  *  this work for additional information regarding copyright ownership.
5  *  The ASF licenses this file to You under the Apache License, Version 2.0
6  *  (the "License"); you may not use this file except in compliance with
7  *  the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  *  Unless required by applicable law or agreed to in writing, software
12  *  distributed under the License is distributed on an "AS IS" BASIS,
13  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  *  See the License for the specific language governing permissions and
15  *  limitations under the License.
16  */

17 package org.apache.tomcat.util.http;
18
19 import java.io.IOException;
20 import java.nio.charset.Charset;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.Enumeration;
25 import java.util.LinkedHashMap;
26 import java.util.Map;
27
28 import org.apache.juli.logging.Log;
29 import org.apache.juli.logging.LogFactory;
30 import org.apache.tomcat.util.buf.ByteChunk;
31 import org.apache.tomcat.util.buf.MessageBytes;
32 import org.apache.tomcat.util.buf.StringUtils;
33 import org.apache.tomcat.util.buf.UDecoder;
34 import org.apache.tomcat.util.log.UserDataHelper;
35 import org.apache.tomcat.util.res.StringManager;
36
37 /**
38  *
39  * @author Costin Manolache
40  */

41 public final class Parameters {
42
43     private static final Log log = LogFactory.getLog(Parameters.class);
44
45     private static final UserDataHelper userDataLog = new UserDataHelper(log);
46
47     private static final UserDataHelper maxParamCountLog = new UserDataHelper(log);
48
49     private static final StringManager sm =
50         StringManager.getManager("org.apache.tomcat.util.http");
51
52     private final Map<String,ArrayList<String>> paramHashValues =
53             new LinkedHashMap<>();
54     private boolean didQueryParameters=false;
55
56     private MessageBytes queryMB;
57
58     private UDecoder urlDec;
59     private final MessageBytes decodedQuery = MessageBytes.newInstance();
60
61     private Charset charset = StandardCharsets.ISO_8859_1;
62     private Charset queryStringCharset = StandardCharsets.UTF_8;
63
64     private int limit = -1;
65     private int parameterCount = 0;
66
67     /**
68      * Set to the reason for the failure (the first failure if there is more
69      * than one) if there were failures during parameter parsing.
70      */

71     private FailReason parseFailedReason = null;
72
73     public Parameters() {
74         // NO-OP
75     }
76
77     public void setQuery( MessageBytes queryMB ) {
78         this.queryMB=queryMB;
79     }
80
81     public void setLimit(int limit) {
82         this.limit = limit;
83     }
84
85     public Charset getCharset() {
86         return charset;
87     }
88
89     public void setCharset(Charset charset) {
90         if (charset == null) {
91             charset = DEFAULT_BODY_CHARSET;
92         }
93         this.charset = charset;
94         if(log.isDebugEnabled()) {
95             log.debug("Set encoding to " + charset.name());
96         }
97     }
98
99     public void setQueryStringCharset(Charset queryStringCharset) {
100         if (queryStringCharset == null) {
101             queryStringCharset = DEFAULT_URI_CHARSET;
102         }
103         this.queryStringCharset = queryStringCharset;
104
105         if(log.isDebugEnabled()) {
106             log.debug("Set query string encoding to " + queryStringCharset.name());
107         }
108     }
109
110
111     public boolean isParseFailed() {
112         return parseFailedReason != null;
113     }
114
115
116     public FailReason getParseFailedReason() {
117         return parseFailedReason;
118     }
119
120
121     public void setParseFailedReason(FailReason failReason) {
122         if (this.parseFailedReason == null) {
123             this.parseFailedReason = failReason;
124         }
125     }
126
127
128     public void recycle() {
129         parameterCount = 0;
130         paramHashValues.clear();
131         didQueryParameters = false;
132         charset = DEFAULT_BODY_CHARSET;
133         decodedQuery.recycle();
134         parseFailedReason = null;
135     }
136
137
138     // -------------------- Data access --------------------
139     // Access to the current name/values, no side effect ( processing ).
140     // You must explicitly call handleQueryParameters and the post methods.
141
142     public String[] getParameterValues(String name) {
143         handleQueryParameters();
144         // no "facade"
145         ArrayList<String> values = paramHashValues.get(name);
146         if (values == null) {
147             return null;
148         }
149         return values.toArray(new String[values.size()]);
150     }
151
152     public Enumeration<String> getParameterNames() {
153         handleQueryParameters();
154         return Collections.enumeration(paramHashValues.keySet());
155     }
156
157     public String getParameter(String name ) {
158         handleQueryParameters();
159         ArrayList<String> values = paramHashValues.get(name);
160         if (values != null) {
161             if(values.size() == 0) {
162                 return "";
163             }
164             return values.get(0);
165         } else {
166             return null;
167         }
168     }
169     // -------------------- Processing --------------------
170     /** Process the query string into parameters
171      */

172     public void handleQueryParameters() {
173         if (didQueryParameters) {
174             return;
175         }
176
177         didQueryParameters = true;
178
179         if (queryMB == null || queryMB.isNull()) {
180             return;
181         }
182
183         if(log.isDebugEnabled()) {
184             log.debug("Decoding query " + decodedQuery + " " + queryStringCharset.name());
185         }
186
187         try {
188             decodedQuery.duplicate(queryMB);
189         } catch (IOException e) {
190             // Can't happen, as decodedQuery can't overflow
191             e.printStackTrace();
192         }
193         processParameters(decodedQuery, queryStringCharset);
194     }
195
196
197     public void addParameter( String key, String value )
198             throws IllegalStateException {
199
200         if( key==null ) {
201             return;
202         }
203
204         parameterCount ++;
205         if (limit > -1 && parameterCount > limit) {
206             // Processing this parameter will push us over the limit. ISE is
207             // what Request.parseParts() uses for requests that are too big
208             setParseFailedReason(FailReason.TOO_MANY_PARAMETERS);
209             throw new IllegalStateException(sm.getString(
210                     "parameters.maxCountFail", Integer.valueOf(limit)));
211         }
212
213         ArrayList<String> values = paramHashValues.get(key);
214         if (values == null) {
215             values = new ArrayList<>(1);
216             paramHashValues.put(key, values);
217         }
218         values.add(value);
219     }
220
221     public void setURLDecoder( UDecoder u ) {
222         urlDec=u;
223     }
224
225     // -------------------- Parameter parsing --------------------
226     // we are called from a single thread - we can do it the hard way
227     // if needed
228     private final ByteChunk tmpName=new ByteChunk();
229     private final ByteChunk tmpValue=new ByteChunk();
230     private final ByteChunk origName=new ByteChunk();
231     private final ByteChunk origValue=new ByteChunk();
232     private static final Charset DEFAULT_BODY_CHARSET = StandardCharsets.ISO_8859_1;
233     private static final Charset DEFAULT_URI_CHARSET = StandardCharsets.UTF_8;
234
235
236     public void processParameters( byte bytes[], int start, int len ) {
237         processParameters(bytes, start, len, charset);
238     }
239
240     private void processParameters(byte bytes[], int start, int len, Charset charset) {
241
242         if(log.isDebugEnabled()) {
243             log.debug(sm.getString("parameters.bytes",
244                     new String(bytes, start, len, DEFAULT_BODY_CHARSET)));
245         }
246
247         int decodeFailCount = 0;
248
249         int pos = start;
250         int end = start + len;
251
252         while(pos < end) {
253             int nameStart = pos;
254             int nameEnd = -1;
255             int valueStart = -1;
256             int valueEnd = -1;
257
258             boolean parsingName = true;
259             boolean decodeName = false;
260             boolean decodeValue = false;
261             boolean parameterComplete = false;
262
263             do {
264                 switch(bytes[pos]) {
265                     case '=':
266                         if (parsingName) {
267                             // Name finished. Value starts from next character
268                             nameEnd = pos;
269                             parsingName = false;
270                             valueStart = ++pos;
271                         } else {
272                             // Equals character in value
273                             pos++;
274                         }
275                         break;
276                     case '&':
277                         if (parsingName) {
278                             // Name finished. No value.
279                             nameEnd = pos;
280                         } else {
281                             // Value finished
282                             valueEnd  = pos;
283                         }
284                         parameterComplete = true;
285                         pos++;
286                         break;
287                     case '%':
288                     case '+':
289                         // Decoding required
290                         if (parsingName) {
291                             decodeName = true;
292                         } else {
293                             decodeValue = true;
294                         }
295                         pos ++;
296                         break;
297                     default:
298                         pos ++;
299                         break;
300                 }
301             } while (!parameterComplete && pos < end);
302
303             if (pos == end) {
304                 if (nameEnd == -1) {
305                     nameEnd = pos;
306                 } else if (valueStart > -1 && valueEnd == -1){
307                     valueEnd = pos;
308                 }
309             }
310
311             if (log.isDebugEnabled() && valueStart == -1) {
312                 log.debug(sm.getString("parameters.noequal",
313                         Integer.valueOf(nameStart), Integer.valueOf(nameEnd),
314                         new String(bytes, nameStart, nameEnd-nameStart, DEFAULT_BODY_CHARSET)));
315             }
316
317             if (nameEnd <= nameStart ) {
318                 if (valueStart == -1) {
319                     // &&
320                     if (log.isDebugEnabled()) {
321                         log.debug(sm.getString("parameters.emptyChunk"));
322                     }
323                     // Do not flag as error
324                     continue;
325                 }
326                 // &=foo&
327                 UserDataHelper.Mode logMode = userDataLog.getNextMode();
328                 if (logMode != null) {
329                     String extract;
330                     if (valueEnd > nameStart) {
331                         extract = new String(bytes, nameStart, valueEnd - nameStart,
332                                 DEFAULT_BODY_CHARSET);
333                     } else {
334                         extract = "";
335                     }
336                     String message = sm.getString("parameters.invalidChunk",
337                             Integer.valueOf(nameStart),
338                             Integer.valueOf(valueEnd), extract);
339                     switch (logMode) {
340                         case INFO_THEN_DEBUG:
341                             message += sm.getString("parameters.fallToDebug");
342                             //$FALL-THROUGH$
343                         case INFO:
344                             log.info(message);
345                             break;
346                         case DEBUG:
347                             log.debug(message);
348                     }
349                 }
350                 setParseFailedReason(FailReason.NO_NAME);
351                 continue;
352                 // invalid chunk - it's better to ignore
353             }
354
355             tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
356             if (valueStart >= 0) {
357                 tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
358             } else {
359                 tmpValue.setBytes(bytes, 0, 0);
360             }
361
362             // Take copies as if anything goes wrong originals will be
363             // corrupted. This means original values can be logged.
364             // For performance - only done for debug
365             if (log.isDebugEnabled()) {
366                 try {
367                     origName.append(bytes, nameStart, nameEnd - nameStart);
368                     if (valueStart >= 0) {
369                         origValue.append(bytes, valueStart, valueEnd - valueStart);
370                     } else {
371                         origValue.append(bytes, 0, 0);
372                     }
373                 } catch (IOException ioe) {
374                     // Should never happen...
375                     log.error(sm.getString("parameters.copyFail"), ioe);
376                 }
377             }
378
379             try {
380                 String name;
381                 String value;
382
383                 if (decodeName) {
384                     urlDecode(tmpName);
385                 }
386                 tmpName.setCharset(charset);
387                 name = tmpName.toString();
388
389                 if (valueStart >= 0) {
390                     if (decodeValue) {
391                         urlDecode(tmpValue);
392                     }
393                     tmpValue.setCharset(charset);
394                     value = tmpValue.toString();
395                 } else {
396                     value = "";
397                 }
398
399                 try {
400                     addParameter(name, value);
401                 } catch (IllegalStateException ise) {
402                     // Hitting limit stops processing further params but does
403                     // not cause request to fail.
404                     UserDataHelper.Mode logMode = maxParamCountLog.getNextMode();
405                     if (logMode != null) {
406                         String message = ise.getMessage();
407                         switch (logMode) {
408                             case INFO_THEN_DEBUG:
409                                 message += sm.getString(
410                                         "parameters.maxCountFail.fallToDebug");
411                                 //$FALL-THROUGH$
412                             case INFO:
413                                 log.info(message);
414                                 break;
415                             case DEBUG:
416                                 log.debug(message);
417                         }
418                     }
419                     break;
420                 }
421             } catch (IOException e) {
422                 setParseFailedReason(FailReason.URL_DECODING);
423                 decodeFailCount++;
424                 if (decodeFailCount == 1 || log.isDebugEnabled()) {
425                     if (log.isDebugEnabled()) {
426                         log.debug(sm.getString("parameters.decodeFail.debug",
427                                 origName.toString(), origValue.toString()), e);
428                     } else if (log.isInfoEnabled()) {
429                         UserDataHelper.Mode logMode = userDataLog.getNextMode();
430                         if (logMode != null) {
431                             String message = sm.getString(
432                                     "parameters.decodeFail.info",
433                                     tmpName.toString(), tmpValue.toString());
434                             switch (logMode) {
435                                 case INFO_THEN_DEBUG:
436                                     message += sm.getString("parameters.fallToDebug");
437                                     //$FALL-THROUGH$
438                                 case INFO:
439                                     log.info(message);
440                                     break;
441                                 case DEBUG:
442                                     log.debug(message);
443                             }
444                         }
445                     }
446                 }
447             }
448
449             tmpName.recycle();
450             tmpValue.recycle();
451             // Only recycle copies if we used them
452             if (log.isDebugEnabled()) {
453                 origName.recycle();
454                 origValue.recycle();
455             }
456         }
457
458         if (decodeFailCount > 1 && !log.isDebugEnabled()) {
459             UserDataHelper.Mode logMode = userDataLog.getNextMode();
460             if (logMode != null) {
461                 String message = sm.getString(
462                         "parameters.multipleDecodingFail",
463                         Integer.valueOf(decodeFailCount));
464                 switch (logMode) {
465                     case INFO_THEN_DEBUG:
466                         message += sm.getString("parameters.fallToDebug");
467                         //$FALL-THROUGH$
468                     case INFO:
469                         log.info(message);
470                         break;
471                     case DEBUG:
472                         log.debug(message);
473                 }
474             }
475         }
476     }
477
478     private void urlDecode(ByteChunk bc)
479         throws IOException {
480         if( urlDec==null ) {
481             urlDec=new UDecoder();
482         }
483         urlDec.convert(bc, true);
484     }
485
486     public void processParameters(MessageBytes data, Charset charset) {
487         if( data==null || data.isNull() || data.getLength() <= 0 ) {
488             return;
489         }
490
491         if( data.getType() != MessageBytes.T_BYTES ) {
492             data.toBytes();
493         }
494         ByteChunk bc=data.getByteChunk();
495         processParameters(bc.getBytes(), bc.getOffset(), bc.getLength(), charset);
496     }
497
498     /**
499      * Debug purpose
500      */

501     @Override
502     public String toString() {
503         StringBuilder sb = new StringBuilder();
504         for (Map.Entry<String, ArrayList<String>> e : paramHashValues.entrySet()) {
505             sb.append(e.getKey()).append('=');
506             StringUtils.join(e.getValue(), ',', sb);
507             sb.append('\n');
508         }
509         return sb.toString();
510     }
511
512
513     public enum FailReason {
514         CLIENT_DISCONNECT,
515         MULTIPART_CONFIG_INVALID,
516         INVALID_CONTENT_TYPE,
517         IO_ERROR,
518         NO_NAME,
519         POST_TOO_LARGE,
520         REQUEST_BODY_INCOMPLETE,
521         TOO_MANY_PARAMETERS,
522         UNKNOWN,
523         URL_DECODING
524     }
525 }
526