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