View Javadoc
1   /*
2    * Copyright 2015 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import java.net.InetSocketAddress;
19  import java.net.URI;
20  import java.nio.charset.Charset;
21  import java.nio.charset.IllegalCharsetNameException;
22  import java.nio.charset.UnsupportedCharsetException;
23  import java.util.ArrayList;
24  import java.util.Iterator;
25  import java.util.List;
26  
27  import io.netty.util.AsciiString;
28  import io.netty.util.CharsetUtil;
29  import io.netty.util.NetUtil;
30  import io.netty.util.internal.ObjectUtil;
31  
32  import static io.netty.util.internal.StringUtil.COMMA;
33  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
34  
35  /**
36   * Utility methods useful in the HTTP context.
37   */
38  public final class HttpUtil {
39  
40      private static final AsciiString CHARSET_EQUALS = AsciiString.of(HttpHeaderValues.CHARSET + "=");
41      private static final AsciiString SEMICOLON = AsciiString.cached(";");
42      private static final String COMMA_STRING = String.valueOf(COMMA);
43  
44      private HttpUtil() { }
45  
46      /**
47       * Determine if a uri is in origin-form according to
48       * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
49       */
50      public static boolean isOriginForm(URI uri) {
51          return isOriginForm(uri.toString());
52      }
53  
54      /**
55       * Determine if a string uri is in origin-form according to
56       * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
57       */
58      public static boolean isOriginForm(String uri) {
59          return uri.startsWith("/");
60      }
61  
62      /**
63       * Determine if a uri is in asterisk-form according to
64       * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
65       */
66      public static boolean isAsteriskForm(URI uri) {
67          return isAsteriskForm(uri.toString());
68      }
69  
70      /**
71       * Determine if a string uri is in asterisk-form according to
72       * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
73       */
74      public static boolean isAsteriskForm(String uri) {
75          return "*".equals(uri);
76      }
77  
78      /**
79       * Returns {@code true} if and only if the connection can remain open and
80       * thus 'kept alive'.  This methods respects the value of the.
81       *
82       * {@code "Connection"} header first and then the return value of
83       * {@link HttpVersion#isKeepAliveDefault()}.
84       */
85      public static boolean isKeepAlive(HttpMessage message) {
86          return !message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true) &&
87                 (message.protocolVersion().isKeepAliveDefault() ||
88                  message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true));
89      }
90  
91      /**
92       * Sets the value of the {@code "Connection"} header depending on the
93       * protocol version of the specified message. This getMethod sets or removes
94       * the {@code "Connection"} header depending on what the default keep alive
95       * mode of the message's protocol version is, as specified by
96       * {@link HttpVersion#isKeepAliveDefault()}.
97       * <ul>
98       * <li>If the connection is kept alive by default:
99       *     <ul>
100      *     <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
101      *     <li>remove otherwise.</li>
102      *     </ul></li>
103      * <li>If the connection is closed by default:
104      *     <ul>
105      *     <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
106      *     <li>remove otherwise.</li>
107      *     </ul></li>
108      * </ul>
109      * @see #setKeepAlive(HttpHeaders, HttpVersion, boolean)
110      */
111     public static void setKeepAlive(HttpMessage message, boolean keepAlive) {
112         setKeepAlive(message.headers(), message.protocolVersion(), keepAlive);
113     }
114 
115     /**
116      * Sets the value of the {@code "Connection"} header depending on the
117      * protocol version of the specified message. This getMethod sets or removes
118      * the {@code "Connection"} header depending on what the default keep alive
119      * mode of the message's protocol version is, as specified by
120      * {@link HttpVersion#isKeepAliveDefault()}.
121      * <ul>
122      * <li>If the connection is kept alive by default:
123      *     <ul>
124      *     <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
125      *     <li>remove otherwise.</li>
126      *     </ul></li>
127      * <li>If the connection is closed by default:
128      *     <ul>
129      *     <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
130      *     <li>remove otherwise.</li>
131      *     </ul></li>
132      * </ul>
133      */
134     public static void setKeepAlive(HttpHeaders h, HttpVersion httpVersion, boolean keepAlive) {
135         if (httpVersion.isKeepAliveDefault()) {
136             if (keepAlive) {
137                 h.remove(HttpHeaderNames.CONNECTION);
138             } else {
139                 h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
140             }
141         } else {
142             if (keepAlive) {
143                 h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
144             } else {
145                 h.remove(HttpHeaderNames.CONNECTION);
146             }
147         }
148     }
149 
150     /**
151      * Returns the length of the content. Please note that this value is
152      * not retrieved from {@link HttpContent#content()} but from the
153      * {@code "Content-Length"} header, and thus they are independent from each
154      * other.
155      *
156      * @return the content length
157      *
158      * @throws NumberFormatException
159      *         if the message does not have the {@code "Content-Length"} header
160      *         or its value is not a number
161      */
162     public static long getContentLength(HttpMessage message) {
163         String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
164         if (value != null) {
165             return Long.parseLong(value);
166         }
167 
168         // We know the content length if it's a Web Socket message even if
169         // Content-Length header is missing.
170         long webSocketContentLength = getWebSocketContentLength(message);
171         if (webSocketContentLength >= 0) {
172             return webSocketContentLength;
173         }
174 
175         // Otherwise we don't.
176         throw new NumberFormatException("header not found: " + HttpHeaderNames.CONTENT_LENGTH);
177     }
178 
179     /**
180      * Returns the length of the content or the specified default value if the message does not have the {@code
181      * "Content-Length" header}. Please note that this value is not retrieved from {@link HttpContent#content()} but
182      * from the {@code "Content-Length"} header, and thus they are independent from each other.
183      *
184      * @param message      the message
185      * @param defaultValue the default value
186      * @return the content length or the specified default value
187      * @throws NumberFormatException if the {@code "Content-Length"} header does not parse as a long
188      */
189     public static long getContentLength(HttpMessage message, long defaultValue) {
190         String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
191         if (value != null) {
192             return Long.parseLong(value);
193         }
194 
195         // We know the content length if it's a Web Socket message even if
196         // Content-Length header is missing.
197         long webSocketContentLength = getWebSocketContentLength(message);
198         if (webSocketContentLength >= 0) {
199             return webSocketContentLength;
200         }
201 
202         // Otherwise we don't.
203         return defaultValue;
204     }
205 
206     /**
207      * Get an {@code int} representation of {@link #getContentLength(HttpMessage, long)}.
208      *
209      * @return the content length or {@code defaultValue} if this message does
210      *         not have the {@code "Content-Length"} header.
211      *
212      * @throws NumberFormatException if the {@code "Content-Length"} header does not parse as an int
213      */
214     public static int getContentLength(HttpMessage message, int defaultValue) {
215         return (int) Math.min(Integer.MAX_VALUE, getContentLength(message, (long) defaultValue));
216     }
217 
218     /**
219      * Returns the content length of the specified web socket message. If the
220      * specified message is not a web socket message, {@code -1} is returned.
221      */
222     static int getWebSocketContentLength(HttpMessage message) {
223         // WebSocket messages have constant content-lengths.
224         HttpHeaders h = message.headers();
225         if (message instanceof HttpRequest) {
226             HttpRequest req = (HttpRequest) message;
227             if (HttpMethod.GET.equals(req.method()) &&
228                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
229                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2)) {
230                 return 8;
231             }
232         } else if (message instanceof HttpResponse) {
233             HttpResponse res = (HttpResponse) message;
234             if (res.status().code() == 101 &&
235                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN) &&
236                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_LOCATION)) {
237                 return 16;
238             }
239         }
240 
241         // Not a web socket message
242         return -1;
243     }
244 
245     /**
246      * Sets the {@code "Content-Length"} header.
247      */
248     public static void setContentLength(HttpMessage message, long length) {
249         message.headers().set(HttpHeaderNames.CONTENT_LENGTH, length);
250     }
251 
252     public static boolean isContentLengthSet(HttpMessage m) {
253         return m.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
254     }
255 
256     /**
257      * Returns {@code true} if and only if the specified message contains an expect header and the only expectation
258      * present is the 100-continue expectation. Note that this method returns {@code false} if the expect header is
259      * not valid for the message (e.g., the message is a response, or the version on the message is HTTP/1.0).
260      *
261      * @param message the message
262      * @return {@code true} if and only if the expectation 100-continue is present and it is the only expectation
263      * present
264      */
265     public static boolean is100ContinueExpected(HttpMessage message) {
266         return isExpectHeaderValid(message)
267           // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive
268           && message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true);
269     }
270 
271     /**
272      * Returns {@code true} if the specified message contains an expect header specifying an expectation that is not
273      * supported. Note that this method returns {@code false} if the expect header is not valid for the message
274      * (e.g., the message is a response, or the version on the message is HTTP/1.0).
275      *
276      * @param message the message
277      * @return {@code true} if and only if an expectation is present that is not supported
278      */
279     static boolean isUnsupportedExpectation(HttpMessage message) {
280         if (!isExpectHeaderValid(message)) {
281             return false;
282         }
283 
284         final String expectValue = message.headers().get(HttpHeaderNames.EXPECT);
285         return expectValue != null && !HttpHeaderValues.CONTINUE.toString().equalsIgnoreCase(expectValue);
286     }
287 
288     private static boolean isExpectHeaderValid(final HttpMessage message) {
289         /*
290          * Expect: 100-continue is for requests only and it works only on HTTP/1.1 or later. Note further that RFC 7231
291          * section 5.1.1 says "A server that receives a 100-continue expectation in an HTTP/1.0 request MUST ignore
292          * that expectation."
293          */
294         return message instanceof HttpRequest &&
295                 message.protocolVersion().compareTo(HttpVersion.HTTP_1_1) >= 0;
296     }
297 
298     /**
299      * Sets or removes the {@code "Expect: 100-continue"} header to / from the
300      * specified message. If {@code expected} is {@code true},
301      * the {@code "Expect: 100-continue"} header is set and all other previous
302      * {@code "Expect"} headers are removed.  Otherwise, all {@code "Expect"}
303      * headers are removed completely.
304      */
305     public static void set100ContinueExpected(HttpMessage message, boolean expected) {
306         if (expected) {
307             message.headers().set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
308         } else {
309             message.headers().remove(HttpHeaderNames.EXPECT);
310         }
311     }
312 
313     /**
314      * Checks to see if the transfer encoding in a specified {@link HttpMessage} is chunked
315      *
316      * @param message The message to check
317      * @return True if transfer encoding is chunked, otherwise false
318      */
319     public static boolean isTransferEncodingChunked(HttpMessage message) {
320         return message.headers().containsValue(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
321     }
322 
323     /**
324      * Set the {@link HttpHeaderNames#TRANSFER_ENCODING} to either include {@link HttpHeaderValues#CHUNKED} if
325      * {@code chunked} is {@code true}, or remove {@link HttpHeaderValues#CHUNKED} if {@code chunked} is {@code false}.
326      *
327      * @param m The message which contains the headers to modify.
328      * @param chunked if {@code true} then include {@link HttpHeaderValues#CHUNKED} in the headers. otherwise remove
329      * {@link HttpHeaderValues#CHUNKED} from the headers.
330      */
331     public static void setTransferEncodingChunked(HttpMessage m, boolean chunked) {
332         if (chunked) {
333             m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
334             m.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
335         } else {
336             List<String> encodings = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING);
337             if (encodings.isEmpty()) {
338                 return;
339             }
340             List<CharSequence> values = new ArrayList<CharSequence>(encodings);
341             Iterator<CharSequence> valuesIt = values.iterator();
342             while (valuesIt.hasNext()) {
343                 CharSequence value = valuesIt.next();
344                 if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(value)) {
345                     valuesIt.remove();
346                 }
347             }
348             if (values.isEmpty()) {
349                 m.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
350             } else {
351                 m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, values);
352             }
353         }
354     }
355 
356     /**
357      * Fetch charset from message's Content-Type header.
358      *
359      * @param message entity to fetch Content-Type header from
360      * @return the charset from message's Content-Type header or {@link CharsetUtil#ISO_8859_1}
361      * if charset is not presented or unparsable
362      */
363     public static Charset getCharset(HttpMessage message) {
364         return getCharset(message, CharsetUtil.ISO_8859_1);
365     }
366 
367     /**
368      * Fetch charset from Content-Type header value.
369      *
370      * @param contentTypeValue Content-Type header value to parse
371      * @return the charset from message's Content-Type header or {@link CharsetUtil#ISO_8859_1}
372      * if charset is not presented or unparsable
373      */
374     public static Charset getCharset(CharSequence contentTypeValue) {
375         if (contentTypeValue != null) {
376             return getCharset(contentTypeValue, CharsetUtil.ISO_8859_1);
377         } else {
378             return CharsetUtil.ISO_8859_1;
379         }
380     }
381 
382     /**
383      * Fetch charset from message's Content-Type header.
384      *
385      * @param message        entity to fetch Content-Type header from
386      * @param defaultCharset result to use in case of empty, incorrect or doesn't contain required part header value
387      * @return the charset from message's Content-Type header or {@code defaultCharset}
388      * if charset is not presented or unparsable
389      */
390     public static Charset getCharset(HttpMessage message, Charset defaultCharset) {
391         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
392         if (contentTypeValue != null) {
393             return getCharset(contentTypeValue, defaultCharset);
394         } else {
395             return defaultCharset;
396         }
397     }
398 
399     /**
400      * Fetch charset from Content-Type header value.
401      *
402      * @param contentTypeValue Content-Type header value to parse
403      * @param defaultCharset   result to use in case of empty, incorrect or doesn't contain required part header value
404      * @return the charset from message's Content-Type header or {@code defaultCharset}
405      * if charset is not presented or unparsable
406      */
407     public static Charset getCharset(CharSequence contentTypeValue, Charset defaultCharset) {
408         if (contentTypeValue != null) {
409             CharSequence charsetRaw = getCharsetAsSequence(contentTypeValue);
410             if (charsetRaw != null) {
411                 if (charsetRaw.length() > 2) { // at least contains 2 quotes(")
412                     if (charsetRaw.charAt(0) == '"' && charsetRaw.charAt(charsetRaw.length() - 1) == '"') {
413                         charsetRaw = charsetRaw.subSequence(1, charsetRaw.length() - 1);
414                     }
415                 }
416                 try {
417                     return Charset.forName(charsetRaw.toString());
418                 } catch (IllegalCharsetNameException ignored) {
419                     // just return the default charset
420                 } catch (UnsupportedCharsetException ignored) {
421                     // just return the default charset
422                 }
423             }
424         }
425         return defaultCharset;
426     }
427 
428     /**
429      * Fetch charset from message's Content-Type header as a char sequence.
430      *
431      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
432      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
433      *
434      * @param message entity to fetch Content-Type header from
435      * @return the {@code CharSequence} with charset from message's Content-Type header
436      * or {@code null} if charset is not presented
437      * @deprecated use {@link #getCharsetAsSequence(HttpMessage)}
438      */
439     @Deprecated
440     public static CharSequence getCharsetAsString(HttpMessage message) {
441         return getCharsetAsSequence(message);
442     }
443 
444     /**
445      * Fetch charset from message's Content-Type header as a char sequence.
446      *
447      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
448      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
449      *
450      * @return the {@code CharSequence} with charset from message's Content-Type header
451      * or {@code null} if charset is not presented
452      */
453     public static CharSequence getCharsetAsSequence(HttpMessage message) {
454         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
455         if (contentTypeValue != null) {
456             return getCharsetAsSequence(contentTypeValue);
457         } else {
458             return null;
459         }
460     }
461 
462     /**
463      * Fetch charset from Content-Type header value as a char sequence.
464      *
465      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
466      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
467      *
468      * @param contentTypeValue Content-Type header value to parse
469      * @return the {@code CharSequence} with charset from message's Content-Type header
470      * or {@code null} if charset is not presented
471      * @throws NullPointerException in case if {@code contentTypeValue == null}
472      */
473     public static CharSequence getCharsetAsSequence(CharSequence contentTypeValue) {
474         ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
475 
476         int indexOfCharset = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, CHARSET_EQUALS, 0);
477         if (indexOfCharset == AsciiString.INDEX_NOT_FOUND) {
478             return null;
479         }
480 
481         int indexOfEncoding = indexOfCharset + CHARSET_EQUALS.length();
482         if (indexOfEncoding < contentTypeValue.length()) {
483             CharSequence charsetCandidate = contentTypeValue.subSequence(indexOfEncoding, contentTypeValue.length());
484             int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(charsetCandidate, SEMICOLON, 0);
485             if (indexOfSemicolon == AsciiString.INDEX_NOT_FOUND) {
486                 return charsetCandidate;
487             }
488 
489             return charsetCandidate.subSequence(0, indexOfSemicolon);
490         }
491 
492         return null;
493     }
494 
495     /**
496      * Fetch MIME type part from message's Content-Type header as a char sequence.
497      *
498      * @param message entity to fetch Content-Type header from
499      * @return the MIME type as a {@code CharSequence} from message's Content-Type header
500      * or {@code null} if content-type header or MIME type part of this header are not presented
501      * <p/>
502      * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
503      * "content-type: text/html" - "text/html" will be returned <br/>
504      * "content-type: " or no header - {@code null} we be returned
505      */
506     public static CharSequence getMimeType(HttpMessage message) {
507         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
508         if (contentTypeValue != null) {
509             return getMimeType(contentTypeValue);
510         } else {
511             return null;
512         }
513     }
514 
515     /**
516      * Fetch MIME type part from Content-Type header value as a char sequence.
517      *
518      * @param contentTypeValue Content-Type header value to parse
519      * @return the MIME type as a {@code CharSequence} from message's Content-Type header
520      * or {@code null} if content-type header or MIME type part of this header are not presented
521      * <p/>
522      * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
523      * "content-type: text/html" - "text/html" will be returned <br/>
524      * "content-type: empty header - {@code null} we be returned
525      * @throws NullPointerException in case if {@code contentTypeValue == null}
526      */
527     public static CharSequence getMimeType(CharSequence contentTypeValue) {
528         ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
529 
530         int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, SEMICOLON, 0);
531         if (indexOfSemicolon != AsciiString.INDEX_NOT_FOUND) {
532             return contentTypeValue.subSequence(0, indexOfSemicolon);
533         } else {
534             return contentTypeValue.length() > 0 ? contentTypeValue : null;
535         }
536     }
537 
538     /**
539      * Formats the host string of an address so it can be used for computing an HTTP component
540      * such as a URL or a Host header
541      *
542      * @param addr the address
543      * @return the formatted String
544      */
545     public static String formatHostnameForHttp(InetSocketAddress addr) {
546         String hostString = NetUtil.getHostname(addr);
547         if (NetUtil.isValidIpV6Address(hostString)) {
548             if (!addr.isUnresolved()) {
549                 hostString = NetUtil.toAddressString(addr.getAddress());
550             } else if (hostString.charAt(0) == '[' && hostString.charAt(hostString.length() - 1) == ']') {
551                 // If IPv6 address already contains brackets, let's return as is.
552                 return hostString;
553             }
554 
555             return '[' + hostString + ']';
556         }
557         return hostString;
558     }
559 
560     /**
561      * Validates, and optionally extracts the content length from headers. This method is not intended for
562      * general use, but is here to be shared between HTTP/1 and HTTP/2 parsing.
563      *
564      * @param contentLengthFields the content-length header fields.
565      * @param isHttp10OrEarlier {@code true} if we are handling HTTP/1.0 or earlier
566      * @param allowDuplicateContentLengths {@code true}  if multiple, identical-value content lengths should be allowed.
567      * @return the normalized content length from the headers or {@code -1} if the fields were empty.
568      * @throws IllegalArgumentException if the content-length fields are not valid
569      */
570     public static long normalizeAndGetContentLength(
571             List<? extends CharSequence> contentLengthFields, boolean isHttp10OrEarlier,
572             boolean allowDuplicateContentLengths) {
573         if (contentLengthFields.isEmpty()) {
574             return -1;
575         }
576 
577         // Guard against multiple Content-Length headers as stated in
578         // https://tools.ietf.org/html/rfc7230#section-3.3.2:
579         //
580         // If a message is received that has multiple Content-Length header
581         //   fields with field-values consisting of the same decimal value, or a
582         //   single Content-Length header field with a field value containing a
583         //   list of identical decimal values (e.g., "Content-Length: 42, 42"),
584         //   indicating that duplicate Content-Length header fields have been
585         //   generated or combined by an upstream message processor, then the
586         //   recipient MUST either reject the message as invalid or replace the
587         //   duplicated field-values with a single valid Content-Length field
588         //   containing that decimal value prior to determining the message body
589         //   length or forwarding the message.
590         String firstField = contentLengthFields.get(0).toString();
591         boolean multipleContentLengths =
592                 contentLengthFields.size() > 1 || firstField.indexOf(COMMA) >= 0;
593 
594         if (multipleContentLengths && !isHttp10OrEarlier) {
595             if (allowDuplicateContentLengths) {
596                 // Find and enforce that all Content-Length values are the same
597                 String firstValue = null;
598                 for (CharSequence field : contentLengthFields) {
599                     String[] tokens = field.toString().split(COMMA_STRING, -1);
600                     for (String token : tokens) {
601                         String trimmed = token.trim();
602                         if (firstValue == null) {
603                             firstValue = trimmed;
604                         } else if (!trimmed.equals(firstValue)) {
605                             throw new IllegalArgumentException(
606                                     "Multiple Content-Length values found: " + contentLengthFields);
607                         }
608                     }
609                 }
610                 // Replace the duplicated field-values with a single valid Content-Length field
611                 firstField = firstValue;
612             } else {
613                 // Reject the message as invalid
614                 throw new IllegalArgumentException(
615                         "Multiple Content-Length values found: " + contentLengthFields);
616             }
617         }
618         // Ensure we not allow sign as part of the content-length:
619         // See https://github.com/squid-cache/squid/security/advisories/GHSA-qf3v-rc95-96j5
620         if (firstField.isEmpty() || !Character.isDigit(firstField.charAt(0))) {
621             // Reject the message as invalid
622             throw new IllegalArgumentException(
623                     "Content-Length value is not a number: " + firstField);
624         }
625         try {
626             final long value = Long.parseLong(firstField);
627             return checkPositiveOrZero(value, "Content-Length value");
628         } catch (NumberFormatException e) {
629             // Reject the message as invalid
630             throw new IllegalArgumentException(
631                     "Content-Length value is not a number: " + firstField, e);
632         }
633     }
634 }