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://datatracker.ietf.org/doc/html/rfc9112#section-3.2.1">RFC 9112, 3.2.1</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://datatracker.ietf.org/doc/html/rfc9112#section-3.2.1">RFC 9112, 3.2.1</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://datatracker.ietf.org/doc/html/rfc9112#section-3.2.4">RFC 9112, 3.2.4</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://datatracker.ietf.org/doc/html/rfc9112#section-3.2.4">RFC 9112, 3.2.4</a>.
73       */
74      public static boolean isAsteriskForm(String uri) {
75          return "*".equals(uri);
76      }
77  
78      static void validateRequestLineTokens(HttpMethod method, String uri) {
79          // The HttpVersion class does its own validation, and it's not possible for subclasses to circumvent it.
80          // The HttpMethod class does its own validation, but subclasses might circumvent it.
81          if (method.getClass() != HttpMethod.class) {
82              if (!isEncodingSafeStartLineToken(method.asciiName())) {
83                  throw new IllegalArgumentException(
84                          "The HTTP method name contain illegal characters: " + method.asciiName());
85              }
86          }
87  
88          if (!isEncodingSafeStartLineToken(uri)) {
89              throw new IllegalArgumentException("The URI contain illegal characters: " + uri);
90          }
91      }
92  
93      /**
94       * Validate that the given request line token is safe for verbatim encoding to the network.
95       * This does not fully check that the token – HTTP method, version, or URI – is valid and formatted correctly.
96       * Only that the token does not contain characters that would break or
97       * desynchronize HTTP message parsing of the start line wherein the token would be included.
98       * <p>
99       * See <a href="https://datatracker.ietf.org/doc/html/rfc9112#name-request-line">RFC 9112, 3.</a>
100      *
101      * @param token The token to check.
102      * @return {@code true} if the token is safe to encode verbatim into the HTTP message output stream,
103      * otherwise {@code false}.
104      */
105     public static boolean isEncodingSafeStartLineToken(CharSequence token) {
106         int lenBytes = token.length();
107         for (int i = 0; i < lenBytes; i++) {
108             char ch = token.charAt(i);
109             // this is to help AOT compiled code which cannot profile the switch
110             if (ch <= ' ') {
111                 switch (ch) {
112                     case '\n':
113                     case '\r':
114                     case ' ':
115                         return false;
116                 }
117             }
118         }
119         return true;
120     }
121 
122     /**
123      * Returns {@code true} if and only if the connection can remain open and
124      * thus 'kept alive'. This method respects the value of the
125      * {@code "Connection"} header first and then the return value of
126      * {@link HttpVersion#isKeepAliveDefault()}.
127      */
128     public static boolean isKeepAlive(HttpMessage message) {
129         return !message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true) &&
130                (message.protocolVersion().isKeepAliveDefault() ||
131                 message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true));
132     }
133 
134     /**
135      * Sets the value of the {@code "Connection"} header depending on the
136      * protocol version of the specified message. This getMethod sets or removes
137      * the {@code "Connection"} header depending on what the default keep alive
138      * mode of the message's protocol version is, as specified by
139      * {@link HttpVersion#isKeepAliveDefault()}.
140      * <ul>
141      * <li>If the connection is kept alive by default:
142      *     <ul>
143      *     <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
144      *     <li>remove otherwise.</li>
145      *     </ul></li>
146      * <li>If the connection is closed by default:
147      *     <ul>
148      *     <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
149      *     <li>remove otherwise.</li>
150      *     </ul></li>
151      * </ul>
152      * @see #setKeepAlive(HttpHeaders, HttpVersion, boolean)
153      */
154     public static void setKeepAlive(HttpMessage message, boolean keepAlive) {
155         setKeepAlive(message.headers(), message.protocolVersion(), keepAlive);
156     }
157 
158     /**
159      * Sets the value of the {@code "Connection"} header depending on the
160      * protocol version of the specified message. This getMethod sets or removes
161      * the {@code "Connection"} header depending on what the default keep alive
162      * mode of the message's protocol version is, as specified by
163      * {@link HttpVersion#isKeepAliveDefault()}.
164      * <ul>
165      * <li>If the connection is kept alive by default:
166      *     <ul>
167      *     <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
168      *     <li>remove otherwise.</li>
169      *     </ul></li>
170      * <li>If the connection is closed by default:
171      *     <ul>
172      *     <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
173      *     <li>remove otherwise.</li>
174      *     </ul></li>
175      * </ul>
176      */
177     public static void setKeepAlive(HttpHeaders h, HttpVersion httpVersion, boolean keepAlive) {
178         if (httpVersion.isKeepAliveDefault()) {
179             if (keepAlive) {
180                 h.remove(HttpHeaderNames.CONNECTION);
181             } else {
182                 h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
183             }
184         } else {
185             if (keepAlive) {
186                 h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
187             } else {
188                 h.remove(HttpHeaderNames.CONNECTION);
189             }
190         }
191     }
192 
193     /**
194      * Returns the length of the content. Please note that this value is
195      * not retrieved from {@link HttpContent#content()} but from the
196      * {@code "Content-Length"} header, and thus they are independent from each
197      * other.
198      *
199      * @return the content length
200      *
201      * @throws NumberFormatException
202      *         if the message does not have the {@code "Content-Length"} header
203      *         or its value is not a number
204      */
205     public static long getContentLength(HttpMessage message) {
206         String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
207         if (value != null) {
208             return Long.parseLong(value);
209         }
210 
211         // We know the content length if it's a Web Socket message even if
212         // Content-Length header is missing.
213         long webSocketContentLength = getWebSocketContentLength(message);
214         if (webSocketContentLength >= 0) {
215             return webSocketContentLength;
216         }
217 
218         // Otherwise we don't.
219         throw new NumberFormatException("header not found: " + HttpHeaderNames.CONTENT_LENGTH);
220     }
221 
222     /**
223      * Returns the length of the content or the specified default value if the message does not have the {@code
224      * "Content-Length" header}. Please note that this value is not retrieved from {@link HttpContent#content()} but
225      * from the {@code "Content-Length"} header, and thus they are independent from each other.
226      *
227      * @param message      the message
228      * @param defaultValue the default value
229      * @return the content length or the specified default value
230      * @throws NumberFormatException if the {@code "Content-Length"} header does not parse as a long
231      */
232     public static long getContentLength(HttpMessage message, long defaultValue) {
233         String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
234         if (value != null) {
235             return Long.parseLong(value);
236         }
237 
238         // We know the content length if it's a Web Socket message even if
239         // Content-Length header is missing.
240         long webSocketContentLength = getWebSocketContentLength(message);
241         if (webSocketContentLength >= 0) {
242             return webSocketContentLength;
243         }
244 
245         // Otherwise we don't.
246         return defaultValue;
247     }
248 
249     /**
250      * Get an {@code int} representation of {@link #getContentLength(HttpMessage, long)}.
251      *
252      * @return the content length or {@code defaultValue} if this message does
253      *         not have the {@code "Content-Length"} header.
254      *
255      * @throws NumberFormatException if the {@code "Content-Length"} header does not parse as an int
256      */
257     public static int getContentLength(HttpMessage message, int defaultValue) {
258         return (int) Math.min(Integer.MAX_VALUE, getContentLength(message, (long) defaultValue));
259     }
260 
261     /**
262      * Returns the content length of the specified web socket message. If the
263      * specified message is not a web socket message, {@code -1} is returned.
264      */
265     static int getWebSocketContentLength(HttpMessage message) {
266         // WebSocket messages have constant content-lengths.
267         HttpHeaders h = message.headers();
268         if (message instanceof HttpRequest) {
269             HttpRequest req = (HttpRequest) message;
270             if (HttpMethod.GET.equals(req.method()) &&
271                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
272                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2)) {
273                 return 8;
274             }
275         } else if (message instanceof HttpResponse) {
276             HttpResponse res = (HttpResponse) message;
277             if (res.status().code() == 101 &&
278                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN) &&
279                     h.contains(HttpHeaderNames.SEC_WEBSOCKET_LOCATION)) {
280                 return 16;
281             }
282         }
283 
284         // Not a web socket message
285         return -1;
286     }
287 
288     /**
289      * Sets the {@code "Content-Length"} header.
290      */
291     public static void setContentLength(HttpMessage message, long length) {
292         message.headers().set(HttpHeaderNames.CONTENT_LENGTH, length);
293     }
294 
295     public static boolean isContentLengthSet(HttpMessage m) {
296         return m.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
297     }
298 
299     /**
300      * Returns {@code true} if and only if the specified message contains an expect header and the only expectation
301      * present is the 100-continue expectation. Note that this method returns {@code false} if the expect header is
302      * not valid for the message (e.g., the message is a response, or the version on the message is HTTP/1.0).
303      *
304      * @param message the message
305      * @return {@code true} if and only if the expectation 100-continue is present and it is the only expectation
306      * present
307      */
308     public static boolean is100ContinueExpected(HttpMessage message) {
309         return isExpectHeaderValid(message)
310           // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive
311           && message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true);
312     }
313 
314     /**
315      * Returns {@code true} if the specified message contains an expect header specifying an expectation that is not
316      * supported. Note that this method returns {@code false} if the expect header is not valid for the message
317      * (e.g., the message is a response, or the version on the message is HTTP/1.0).
318      *
319      * @param message the message
320      * @return {@code true} if and only if an expectation is present that is not supported
321      */
322     static boolean isUnsupportedExpectation(HttpMessage message) {
323         if (!isExpectHeaderValid(message)) {
324             return false;
325         }
326 
327         final String expectValue = message.headers().get(HttpHeaderNames.EXPECT);
328         return expectValue != null && !HttpHeaderValues.CONTINUE.toString().equalsIgnoreCase(expectValue);
329     }
330 
331     private static boolean isExpectHeaderValid(final HttpMessage message) {
332         /*
333          * Expect: 100-continue is for requests only and it works only on HTTP/1.1 or later. Note further that RFC 7231
334          * section 5.1.1 says "A server that receives a 100-continue expectation in an HTTP/1.0 request MUST ignore
335          * that expectation."
336          */
337         return message instanceof HttpRequest &&
338                 message.protocolVersion().compareTo(HttpVersion.HTTP_1_1) >= 0;
339     }
340 
341     /**
342      * Sets or removes the {@code "Expect: 100-continue"} header to / from the
343      * specified message. If {@code expected} is {@code true},
344      * the {@code "Expect: 100-continue"} header is set and all other previous
345      * {@code "Expect"} headers are removed.  Otherwise, all {@code "Expect"}
346      * headers are removed completely.
347      */
348     public static void set100ContinueExpected(HttpMessage message, boolean expected) {
349         if (expected) {
350             message.headers().set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
351         } else {
352             message.headers().remove(HttpHeaderNames.EXPECT);
353         }
354     }
355 
356     /**
357      * Checks to see if the transfer encoding in a specified {@link HttpMessage} is chunked
358      *
359      * @param message The message to check
360      * @return True if transfer encoding is chunked, otherwise false
361      */
362     public static boolean isTransferEncodingChunked(HttpMessage message) {
363         return message.headers().containsValue(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
364     }
365 
366     /**
367      * Set the {@link HttpHeaderNames#TRANSFER_ENCODING} to either include {@link HttpHeaderValues#CHUNKED} if
368      * {@code chunked} is {@code true}, or remove {@link HttpHeaderValues#CHUNKED} if {@code chunked} is {@code false}.
369      *
370      * @param m The message which contains the headers to modify.
371      * @param chunked if {@code true} then include {@link HttpHeaderValues#CHUNKED} in the headers. otherwise remove
372      * {@link HttpHeaderValues#CHUNKED} from the headers.
373      */
374     public static void setTransferEncodingChunked(HttpMessage m, boolean chunked) {
375         if (chunked) {
376             m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
377             m.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
378         } else {
379             List<String> encodings = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING);
380             if (encodings.isEmpty()) {
381                 return;
382             }
383             List<CharSequence> values = new ArrayList<CharSequence>(encodings);
384             Iterator<CharSequence> valuesIt = values.iterator();
385             while (valuesIt.hasNext()) {
386                 CharSequence value = valuesIt.next();
387                 if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(value)) {
388                     valuesIt.remove();
389                 }
390             }
391             if (values.isEmpty()) {
392                 m.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
393             } else {
394                 m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, values);
395             }
396         }
397     }
398 
399     /**
400      * Fetch charset from message's Content-Type header.
401      *
402      * @param message entity to fetch Content-Type header from
403      * @return the charset from message's Content-Type header or {@link CharsetUtil#ISO_8859_1}
404      * if charset is not presented or unparsable
405      */
406     public static Charset getCharset(HttpMessage message) {
407         return getCharset(message, CharsetUtil.ISO_8859_1);
408     }
409 
410     /**
411      * Fetch charset from Content-Type header value.
412      *
413      * @param contentTypeValue Content-Type header value to parse
414      * @return the charset from message's Content-Type header or {@link CharsetUtil#ISO_8859_1}
415      * if charset is not presented or unparsable
416      */
417     public static Charset getCharset(CharSequence contentTypeValue) {
418         if (contentTypeValue != null) {
419             return getCharset(contentTypeValue, CharsetUtil.ISO_8859_1);
420         } else {
421             return CharsetUtil.ISO_8859_1;
422         }
423     }
424 
425     /**
426      * Fetch charset from message's Content-Type header.
427      *
428      * @param message        entity to fetch Content-Type header from
429      * @param defaultCharset result to use in case of empty, incorrect or doesn't contain required part header value
430      * @return the charset from message's Content-Type header or {@code defaultCharset}
431      * if charset is not presented or unparsable
432      */
433     public static Charset getCharset(HttpMessage message, Charset defaultCharset) {
434         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
435         if (contentTypeValue != null) {
436             return getCharset(contentTypeValue, defaultCharset);
437         } else {
438             return defaultCharset;
439         }
440     }
441 
442     /**
443      * Fetch charset from Content-Type header value.
444      *
445      * @param contentTypeValue Content-Type header value to parse
446      * @param defaultCharset   result to use in case of empty, incorrect or doesn't contain required part header value
447      * @return the charset from message's Content-Type header or {@code defaultCharset}
448      * if charset is not presented or unparsable
449      */
450     public static Charset getCharset(CharSequence contentTypeValue, Charset defaultCharset) {
451         if (contentTypeValue != null) {
452             CharSequence charsetRaw = getCharsetAsSequence(contentTypeValue);
453             if (charsetRaw != null) {
454                 // Remove only the trailing optional whitespace (OWS) that RFC 9110 allows before
455                 // the semicolon starting the next parameter, see
456                 // https://www.rfc-editor.org/rfc/rfc9110#section-5.6.6
457                 // Leading whitespace after '=' is not grammar-legal and is deliberately kept, so
458                 // such values keep failing the charset lookup below.
459                 int end = charsetRaw.length();
460                 while (end > 0 && isOws(charsetRaw.charAt(end - 1))) {
461                     end--;
462                 }
463                 if (end < charsetRaw.length()) {
464                     charsetRaw = charsetRaw.subSequence(0, end);
465                 }
466                 if (charsetRaw.length() > 2) { // at least contains 2 quotes(")
467                     if (charsetRaw.charAt(0) == '"' && charsetRaw.charAt(charsetRaw.length() - 1) == '"') {
468                         charsetRaw = charsetRaw.subSequence(1, charsetRaw.length() - 1);
469                     }
470                 }
471                 try {
472                     return Charset.forName(charsetRaw.toString());
473                 } catch (IllegalCharsetNameException | UnsupportedCharsetException ignored) {
474                     // just return the default charset
475                 }
476             }
477         }
478         return defaultCharset;
479     }
480 
481     /**
482      * Whether the given character is optional whitespace (OWS) as defined by
483      * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3">RFC 9110 section 5.6.3</a>.
484      */
485     private static boolean isOws(char c) {
486         return c == ' ' || c == '\t';
487     }
488 
489     /**
490      * Fetch charset from message's Content-Type header as a char sequence.
491      *
492      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
493      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
494      *
495      * @param message entity to fetch Content-Type header from
496      * @return the {@code CharSequence} with charset from message's Content-Type header
497      * or {@code null} if charset is not presented
498      * @deprecated use {@link #getCharsetAsSequence(HttpMessage)}
499      */
500     @Deprecated
501     public static CharSequence getCharsetAsString(HttpMessage message) {
502         return getCharsetAsSequence(message);
503     }
504 
505     /**
506      * Fetch charset from message's Content-Type header as a char sequence.
507      *
508      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
509      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
510      *
511      * @return the {@code CharSequence} with charset from message's Content-Type header
512      * or {@code null} if charset is not presented
513      */
514     public static CharSequence getCharsetAsSequence(HttpMessage message) {
515         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
516         if (contentTypeValue != null) {
517             return getCharsetAsSequence(contentTypeValue);
518         } else {
519             return null;
520         }
521     }
522 
523     /**
524      * Fetch charset from Content-Type header value as a char sequence.
525      *
526      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
527      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
528      *
529      * @param contentTypeValue Content-Type header value to parse
530      * @return the {@code CharSequence} with charset from message's Content-Type header
531      * or {@code null} if charset is not presented
532      * @throws NullPointerException in case if {@code contentTypeValue == null}
533      */
534     public static CharSequence getCharsetAsSequence(CharSequence contentTypeValue) {
535         ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
536 
537         int indexOfCharset = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, CHARSET_EQUALS, 0);
538         if (indexOfCharset == AsciiString.INDEX_NOT_FOUND) {
539             return null;
540         }
541 
542         int indexOfEncoding = indexOfCharset + CHARSET_EQUALS.length();
543         if (indexOfEncoding < contentTypeValue.length()) {
544             CharSequence charsetCandidate = contentTypeValue.subSequence(indexOfEncoding, contentTypeValue.length());
545             int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(charsetCandidate, SEMICOLON, 0);
546             if (indexOfSemicolon == AsciiString.INDEX_NOT_FOUND) {
547                 return charsetCandidate;
548             }
549 
550             return charsetCandidate.subSequence(0, indexOfSemicolon);
551         }
552 
553         return null;
554     }
555 
556     /**
557      * Fetch MIME type part from message's Content-Type header as a char sequence.
558      *
559      * @param message entity to fetch Content-Type header from
560      * @return the MIME type as a {@code CharSequence} from message's Content-Type header
561      * or {@code null} if content-type header or MIME type part of this header are not presented
562      * <p/>
563      * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
564      * "content-type: text/html" - "text/html" will be returned <br/>
565      * "content-type: " or no header - {@code null} we be returned
566      */
567     public static CharSequence getMimeType(HttpMessage message) {
568         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
569         if (contentTypeValue != null) {
570             return getMimeType(contentTypeValue);
571         } else {
572             return null;
573         }
574     }
575 
576     /**
577      * Fetch MIME type part from Content-Type header value as a char sequence.
578      *
579      * @param contentTypeValue Content-Type header value to parse
580      * @return the MIME type as a {@code CharSequence} from message's Content-Type header
581      * or {@code null} if content-type header or MIME type part of this header are not presented
582      * <p/>
583      * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
584      * "content-type: text/html" - "text/html" will be returned <br/>
585      * "content-type: empty header - {@code null} we be returned
586      * @throws NullPointerException in case if {@code contentTypeValue == null}
587      */
588     public static CharSequence getMimeType(CharSequence contentTypeValue) {
589         ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
590 
591         int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, SEMICOLON, 0);
592         if (indexOfSemicolon != AsciiString.INDEX_NOT_FOUND) {
593             // Remove the trailing optional whitespace (OWS) that RFC 9110 allows before the semicolon
594             // starting the next parameter, see https://www.rfc-editor.org/rfc/rfc9110#section-8.3.1
595             int end = indexOfSemicolon;
596             while (end > 0 && isOws(contentTypeValue.charAt(end - 1))) {
597                 end--;
598             }
599             return contentTypeValue.subSequence(0, end);
600         } else {
601             return contentTypeValue.length() > 0 ? contentTypeValue : null;
602         }
603     }
604 
605     /**
606      * Formats the host string of an address so it can be used for computing an HTTP component
607      * such as a URL or a Host header
608      *
609      * @param addr the address
610      * @return the formatted String
611      */
612     public static String formatHostnameForHttp(InetSocketAddress addr) {
613         String hostString = NetUtil.getHostname(addr);
614         if (NetUtil.isValidIpV6Address(hostString)) {
615             if (!addr.isUnresolved()) {
616                 hostString = NetUtil.toAddressString(addr.getAddress());
617             } else if (hostString.charAt(0) == '[' && hostString.charAt(hostString.length() - 1) == ']') {
618                 // If IPv6 address already contains brackets, let's return as is.
619                 return hostString;
620             }
621 
622             return '[' + hostString + ']';
623         }
624         return hostString;
625     }
626 
627     /**
628      * Validates, and optionally extracts the content length from headers. This method is not intended for
629      * general use, but is here to be shared between HTTP/1 and HTTP/2 parsing.
630      *
631      * @param contentLengthFields the content-length header fields.
632      * @param isHttp10OrEarlier unused
633      * @param allowDuplicateContentLengths {@code true}  if multiple, identical-value content lengths should be allowed.
634      * @return the normalized content length from the headers or {@code -1} if the fields were empty.
635      * @throws IllegalArgumentException if the content-length fields are not valid
636      */
637     public static long normalizeAndGetContentLength(
638             List<? extends CharSequence> contentLengthFields, @SuppressWarnings("unused") boolean isHttp10OrEarlier,
639             boolean allowDuplicateContentLengths) {
640         if (contentLengthFields.isEmpty()) {
641             return -1;
642         }
643 
644         // Guard against multiple Content-Length headers as stated in
645         // https://tools.ietf.org/html/rfc7230#section-3.3.2:
646         //
647         // If a message is received that has multiple Content-Length header
648         //   fields with field-values consisting of the same decimal value, or a
649         //   single Content-Length header field with a field value containing a
650         //   list of identical decimal values (e.g., "Content-Length: 42, 42"),
651         //   indicating that duplicate Content-Length header fields have been
652         //   generated or combined by an upstream message processor, then the
653         //   recipient MUST either reject the message as invalid or replace the
654         //   duplicated field-values with a single valid Content-Length field
655         //   containing that decimal value prior to determining the message body
656         //   length or forwarding the message.
657         String firstField = contentLengthFields.get(0).toString();
658         boolean multipleContentLengths =
659                 contentLengthFields.size() > 1 || firstField.indexOf(COMMA) >= 0;
660 
661         if (multipleContentLengths) {
662             if (allowDuplicateContentLengths) {
663                 // Find and enforce that all Content-Length values are the same
664                 String firstValue = null;
665                 for (CharSequence field : contentLengthFields) {
666                     String[] tokens = field.toString().split(COMMA_STRING, -1);
667                     for (String token : tokens) {
668                         String trimmed = token.trim();
669                         if (firstValue == null) {
670                             firstValue = trimmed;
671                         } else if (!trimmed.equals(firstValue)) {
672                             throw new IllegalArgumentException(
673                                     "Multiple Content-Length values found: " + contentLengthFields);
674                         }
675                     }
676                 }
677                 // Replace the duplicated field-values with a single valid Content-Length field
678                 firstField = firstValue;
679             } else {
680                 // Reject the message as invalid
681                 throw new IllegalArgumentException(
682                         "Multiple Content-Length values found: " + contentLengthFields);
683             }
684         }
685         // Ensure we not allow sign as part of the content-length:
686         // See https://github.com/squid-cache/squid/security/advisories/GHSA-qf3v-rc95-96j5
687         if (firstField.isEmpty() || !Character.isDigit(firstField.charAt(0))) {
688             // Reject the message as invalid
689             throw new IllegalArgumentException(
690                     "Content-Length value is not a number: " + firstField);
691         }
692         try {
693             final long value = Long.parseLong(firstField);
694             return checkPositiveOrZero(value, "Content-Length value");
695         } catch (NumberFormatException e) {
696             // Reject the message as invalid
697             throw new IllegalArgumentException(
698                     "Content-Length value is not a number: " + firstField, e);
699         }
700     }
701 
702     /**
703      * Validate a <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">token</a> contains only allowed
704      * characters.
705      * <p>
706      * The <a href="https://tools.ietf.org/html/rfc2616#section-2.2">token</a> format is used for variety of HTTP
707      * components, like  <a href="https://tools.ietf.org/html/rfc6265#section-4.1.1">cookie-name</a>,
708      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">field-name</a> of a
709      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2">header-field</a>, or
710      * <a href="https://tools.ietf.org/html/rfc7231#section-4">request method</a>.
711      *
712      * @param token the token to validate.
713      * @return the index of the first invalid token character found, or {@code -1} if there are none.
714      */
715     static int validateToken(CharSequence token) {
716         if (token instanceof AsciiString) {
717             return validateAsciiStringToken((AsciiString) token);
718         }
719         return validateCharSequenceToken(token);
720     }
721 
722     /**
723      * Validate that an {@link AsciiString} contain onlu valid
724      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">token</a> characters.
725      *
726      * @param token the ascii string to validate.
727      */
728     private static int validateAsciiStringToken(AsciiString token) {
729         byte[] array = token.array();
730         for (int i = token.arrayOffset(), len = token.arrayOffset() + token.length(); i < len; i++) {
731             if (!isValidTokenChar(array[i])) {
732                 return i - token.arrayOffset();
733             }
734         }
735         return -1;
736     }
737 
738     /**
739      * Validate that a {@link CharSequence} contain onlu valid
740      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">token</a> characters.
741      *
742      * @param token the character sequence to validate.
743      */
744     private static int validateCharSequenceToken(CharSequence token) {
745         for (int i = 0, len = token.length(); i < len; i++) {
746             int value = token.charAt(i);
747             // 1. Check for truncation (anything above 255)
748             // 2. Check against the BitSet (isValidTokenChar handles 128-255 via bit < 0)
749             if (value > 0xFF || !isValidTokenChar((byte) value)) {
750                 return i;
751             }
752         }
753         return -1;
754     }
755 
756     // HEADER
757     // header-field   = field-name ":" OWS field-value OWS
758     //
759     // field-name     = token
760     // token          = 1*tchar
761     //
762     // tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
763     //                    / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
764     //                    / DIGIT / ALPHA
765     //                    ; any VCHAR, except delimiters.
766     //  Delimiters are chosen
767     //   from the set of US-ASCII visual characters not allowed in a token
768     //   (DQUOTE and "(),/:;<=>?@[\]{}")
769     //
770     // COOKIE
771     // cookie-pair       = cookie-name "=" cookie-value
772     // cookie-name       = token
773     // token          = 1*<any CHAR except CTLs or separators>
774     // CTL = <any US-ASCII control character
775     //       (octets 0 - 31) and DEL (127)>
776     // separators     = "(" | ")" | "<" | ">" | "@"
777     //                      | "," | ";" | ":" | "\" | <">
778     //                      | "/" | "[" | "]" | "?" | "="
779     //                      | "{" | "}" | SP | HT
780     //
781     // field-name's token is equivalent to cookie-name's token, we can reuse the tchar mask for both:
782 
783     //    private static final class BitSet128 {
784     //        private long high;
785     //        private long low;
786     //
787     //        BitSet128 range(char fromInc, char toInc) {
788     //            for (int bit = fromInc; bit <= toInc; bit++) {
789     //                if (bit < 64) {
790     //                    low |= 1L << bit;
791     //                } else {
792     //                    high |= 1L << bit - 64;
793     //                }
794     //            }
795     //            return this;
796     //        }
797     //
798     //        BitSet128 bits(char... bits) {
799     //            for (char bit : bits) {
800     //                if (bit < 64) {
801     //                    low |= 1L << bit;
802     //                } else {
803     //                    high |= 1L << bit - 64;
804     //                }
805     //            }
806     //            return this;
807     //        }
808     //
809     //        long high() {
810     //            return high;
811     //        }
812     //
813     //        long low() {
814     //            return low;
815     //        }
816     //
817     //        static boolean contains(byte bit, long high, long low) {
818     //            if (bit < 0) {
819     //                return false;
820     //            }
821     //            if (bit < 64) {
822     //                return 0 != (low & 1L << bit);
823     //            }
824     //            return 0 != (high & 1L << bit - 64);
825     //        }
826     //    }
827 
828     // BitSet128 tokenChars = new BitSet128()
829     //        .range('0', '9').range('a', 'z').range('A', 'Z') // Alphanumeric.
830     //        .bits('-', '.', '_', '~') // Unreserved characters.
831     //        .bits('!', '#', '$', '%', '&', '\'', '*', '+', '^', '`', '|'); // Token special characters.
832 
833     // This constants calculated by the above code
834     private static final long TOKEN_CHARS_HIGH = 0x57ffffffc7fffffeL;
835     private static final long TOKEN_CHARS_LOW = 0x3ff6cfa00000000L;
836 
837     static boolean isValidTokenChar(byte octet) {
838         if (octet < 0) {
839             return false;
840         }
841         if (octet < 64) {
842             return 0 != (TOKEN_CHARS_LOW & 1L << octet);
843         }
844         return 0 != (TOKEN_CHARS_HIGH & 1L << octet - 64);
845     }
846 }