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