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