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 | UnsupportedCharsetException ignored) {
471                     // just return the default charset
472                 }
473             }
474         }
475         return defaultCharset;
476     }
477 
478     /**
479      * Whether the given character is optional whitespace (OWS) as defined by
480      * <a href="https://www.rfc-editor.org/rfc/rfc9110#section-5.6.3">RFC 9110 section 5.6.3</a>.
481      */
482     private static boolean isOws(char c) {
483         return c == ' ' || c == '\t';
484     }
485 
486     /**
487      * Fetch charset from message's Content-Type header as a char sequence.
488      *
489      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
490      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
491      *
492      * @param message entity to fetch Content-Type header from
493      * @return the {@code CharSequence} with charset from message's Content-Type header
494      * or {@code null} if charset is not presented
495      * @deprecated use {@link #getCharsetAsSequence(HttpMessage)}
496      */
497     @Deprecated
498     public static CharSequence getCharsetAsString(HttpMessage message) {
499         return getCharsetAsSequence(message);
500     }
501 
502     /**
503      * Fetch charset from message's Content-Type header as a char sequence.
504      *
505      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
506      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
507      *
508      * @return the {@code CharSequence} with charset from message's Content-Type header
509      * or {@code null} if charset is not presented
510      */
511     public static CharSequence getCharsetAsSequence(HttpMessage message) {
512         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
513         if (contentTypeValue != null) {
514             return getCharsetAsSequence(contentTypeValue);
515         } else {
516             return null;
517         }
518     }
519 
520     /**
521      * Fetch charset from Content-Type header value as a char sequence.
522      *
523      * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
524      * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
525      *
526      * @param contentTypeValue Content-Type header value to parse
527      * @return the {@code CharSequence} with charset from message's Content-Type header
528      * or {@code null} if charset is not presented
529      * @throws NullPointerException in case if {@code contentTypeValue == null}
530      */
531     public static CharSequence getCharsetAsSequence(CharSequence contentTypeValue) {
532         ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
533 
534         int indexOfCharset = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, CHARSET_EQUALS, 0);
535         if (indexOfCharset == AsciiString.INDEX_NOT_FOUND) {
536             return null;
537         }
538 
539         int indexOfEncoding = indexOfCharset + CHARSET_EQUALS.length();
540         if (indexOfEncoding < contentTypeValue.length()) {
541             CharSequence charsetCandidate = contentTypeValue.subSequence(indexOfEncoding, contentTypeValue.length());
542             int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(charsetCandidate, SEMICOLON, 0);
543             if (indexOfSemicolon == AsciiString.INDEX_NOT_FOUND) {
544                 return charsetCandidate;
545             }
546 
547             return charsetCandidate.subSequence(0, indexOfSemicolon);
548         }
549 
550         return null;
551     }
552 
553     /**
554      * Fetch MIME type part from message's Content-Type header as a char sequence.
555      *
556      * @param message entity to fetch Content-Type header from
557      * @return the MIME type as a {@code CharSequence} from message's Content-Type header
558      * or {@code null} if content-type header or MIME type part of this header are not presented
559      * <p/>
560      * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
561      * "content-type: text/html" - "text/html" will be returned <br/>
562      * "content-type: " or no header - {@code null} we be returned
563      */
564     public static CharSequence getMimeType(HttpMessage message) {
565         CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
566         if (contentTypeValue != null) {
567             return getMimeType(contentTypeValue);
568         } else {
569             return null;
570         }
571     }
572 
573     /**
574      * Fetch MIME type part from Content-Type header value as a char sequence.
575      *
576      * @param contentTypeValue Content-Type header value to parse
577      * @return the MIME type as a {@code CharSequence} from message's Content-Type header
578      * or {@code null} if content-type header or MIME type part of this header are not presented
579      * <p/>
580      * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
581      * "content-type: text/html" - "text/html" will be returned <br/>
582      * "content-type: empty header - {@code null} we be returned
583      * @throws NullPointerException in case if {@code contentTypeValue == null}
584      */
585     public static CharSequence getMimeType(CharSequence contentTypeValue) {
586         ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
587 
588         int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, SEMICOLON, 0);
589         if (indexOfSemicolon != AsciiString.INDEX_NOT_FOUND) {
590             // Remove the trailing optional whitespace (OWS) that RFC 9110 allows before the semicolon
591             // starting the next parameter, see https://www.rfc-editor.org/rfc/rfc9110#section-8.3.1
592             int end = indexOfSemicolon;
593             while (end > 0 && isOws(contentTypeValue.charAt(end - 1))) {
594                 end--;
595             }
596             return contentTypeValue.subSequence(0, end);
597         } else {
598             return contentTypeValue.length() > 0 ? contentTypeValue : null;
599         }
600     }
601 
602     /**
603      * Formats the host string of an address so it can be used for computing an HTTP component
604      * such as a URL or a Host header
605      *
606      * @param addr the address
607      * @return the formatted String
608      */
609     public static String formatHostnameForHttp(InetSocketAddress addr) {
610         String hostString = NetUtil.getHostname(addr);
611         if (NetUtil.isValidIpV6Address(hostString)) {
612             if (!addr.isUnresolved()) {
613                 hostString = NetUtil.toAddressString(addr.getAddress());
614             } else if (hostString.charAt(0) == '[' && hostString.charAt(hostString.length() - 1) == ']') {
615                 // If IPv6 address already contains brackets, let's return as is.
616                 return hostString;
617             }
618 
619             return '[' + hostString + ']';
620         }
621         return hostString;
622     }
623 
624     /**
625      * Validates, and optionally extracts the content length from headers. This method is not intended for
626      * general use, but is here to be shared between HTTP/1 and HTTP/2 parsing.
627      *
628      * @param contentLengthFields the content-length header fields.
629      * @param isHttp10OrEarlier unused
630      * @param allowDuplicateContentLengths {@code true}  if multiple, identical-value content lengths should be allowed.
631      * @return the normalized content length from the headers or {@code -1} if the fields were empty.
632      * @throws IllegalArgumentException if the content-length fields are not valid
633      */
634     public static long normalizeAndGetContentLength(
635             List<? extends CharSequence> contentLengthFields, @SuppressWarnings("unused") boolean isHttp10OrEarlier,
636             boolean allowDuplicateContentLengths) {
637         if (contentLengthFields.isEmpty()) {
638             return -1;
639         }
640 
641         // Guard against multiple Content-Length headers as stated in
642         // https://tools.ietf.org/html/rfc7230#section-3.3.2:
643         //
644         // If a message is received that has multiple Content-Length header
645         //   fields with field-values consisting of the same decimal value, or a
646         //   single Content-Length header field with a field value containing a
647         //   list of identical decimal values (e.g., "Content-Length: 42, 42"),
648         //   indicating that duplicate Content-Length header fields have been
649         //   generated or combined by an upstream message processor, then the
650         //   recipient MUST either reject the message as invalid or replace the
651         //   duplicated field-values with a single valid Content-Length field
652         //   containing that decimal value prior to determining the message body
653         //   length or forwarding the message.
654         String firstField = contentLengthFields.get(0).toString();
655         boolean multipleContentLengths =
656                 contentLengthFields.size() > 1 || firstField.indexOf(COMMA) >= 0;
657 
658         if (multipleContentLengths) {
659             if (allowDuplicateContentLengths) {
660                 // Find and enforce that all Content-Length values are the same
661                 String firstValue = null;
662                 for (CharSequence field : contentLengthFields) {
663                     String[] tokens = field.toString().split(COMMA_STRING, -1);
664                     for (String token : tokens) {
665                         String trimmed = token.trim();
666                         if (firstValue == null) {
667                             firstValue = trimmed;
668                         } else if (!trimmed.equals(firstValue)) {
669                             throw new IllegalArgumentException(
670                                     "Multiple Content-Length values found: " + contentLengthFields);
671                         }
672                     }
673                 }
674                 // Replace the duplicated field-values with a single valid Content-Length field
675                 firstField = firstValue;
676             } else {
677                 // Reject the message as invalid
678                 throw new IllegalArgumentException(
679                         "Multiple Content-Length values found: " + contentLengthFields);
680             }
681         }
682         // Ensure we not allow sign as part of the content-length:
683         // See https://github.com/squid-cache/squid/security/advisories/GHSA-qf3v-rc95-96j5
684         if (firstField.isEmpty() || !Character.isDigit(firstField.charAt(0))) {
685             // Reject the message as invalid
686             throw new IllegalArgumentException(
687                     "Content-Length value is not a number: " + firstField);
688         }
689         try {
690             final long value = Long.parseLong(firstField);
691             return checkPositiveOrZero(value, "Content-Length value");
692         } catch (NumberFormatException e) {
693             // Reject the message as invalid
694             throw new IllegalArgumentException(
695                     "Content-Length value is not a number: " + firstField, e);
696         }
697     }
698 
699     /**
700      * Validate a <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">token</a> contains only allowed
701      * characters.
702      * <p>
703      * The <a href="https://tools.ietf.org/html/rfc2616#section-2.2">token</a> format is used for variety of HTTP
704      * components, like  <a href="https://tools.ietf.org/html/rfc6265#section-4.1.1">cookie-name</a>,
705      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">field-name</a> of a
706      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2">header-field</a>, or
707      * <a href="https://tools.ietf.org/html/rfc7231#section-4">request method</a>.
708      *
709      * @param token the token to validate.
710      * @return the index of the first invalid token character found, or {@code -1} if there are none.
711      */
712     static int validateToken(CharSequence token) {
713         if (token instanceof AsciiString) {
714             return validateAsciiStringToken((AsciiString) token);
715         }
716         return validateCharSequenceToken(token);
717     }
718 
719     /**
720      * Validate that an {@link AsciiString} contain onlu valid
721      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">token</a> characters.
722      *
723      * @param token the ascii string to validate.
724      */
725     private static int validateAsciiStringToken(AsciiString token) {
726         byte[] array = token.array();
727         for (int i = token.arrayOffset(), len = token.arrayOffset() + token.length(); i < len; i++) {
728             if (!isValidTokenChar(array[i])) {
729                 return i - token.arrayOffset();
730             }
731         }
732         return -1;
733     }
734 
735     /**
736      * Validate that a {@link CharSequence} contain onlu valid
737      * <a href="https://tools.ietf.org/html/rfc7230#section-3.2.6">token</a> characters.
738      *
739      * @param token the character sequence to validate.
740      */
741     private static int validateCharSequenceToken(CharSequence token) {
742         for (int i = 0, len = token.length(); i < len; i++) {
743             int value = token.charAt(i);
744             // 1. Check for truncation (anything above 255)
745             // 2. Check against the BitSet (isValidTokenChar handles 128-255 via bit < 0)
746             if (value > 0xFF || !isValidTokenChar((byte) value)) {
747                 return i;
748             }
749         }
750         return -1;
751     }
752 
753     // HEADER
754     // header-field   = field-name ":" OWS field-value OWS
755     //
756     // field-name     = token
757     // token          = 1*tchar
758     //
759     // tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
760     //                    / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
761     //                    / DIGIT / ALPHA
762     //                    ; any VCHAR, except delimiters.
763     //  Delimiters are chosen
764     //   from the set of US-ASCII visual characters not allowed in a token
765     //   (DQUOTE and "(),/:;<=>?@[\]{}")
766     //
767     // COOKIE
768     // cookie-pair       = cookie-name "=" cookie-value
769     // cookie-name       = token
770     // token          = 1*<any CHAR except CTLs or separators>
771     // CTL = <any US-ASCII control character
772     //       (octets 0 - 31) and DEL (127)>
773     // separators     = "(" | ")" | "<" | ">" | "@"
774     //                      | "," | ";" | ":" | "\" | <">
775     //                      | "/" | "[" | "]" | "?" | "="
776     //                      | "{" | "}" | SP | HT
777     //
778     // field-name's token is equivalent to cookie-name's token, we can reuse the tchar mask for both:
779 
780     //    private static final class BitSet128 {
781     //        private long high;
782     //        private long low;
783     //
784     //        BitSet128 range(char fromInc, char toInc) {
785     //            for (int bit = fromInc; bit <= toInc; bit++) {
786     //                if (bit < 64) {
787     //                    low |= 1L << bit;
788     //                } else {
789     //                    high |= 1L << bit - 64;
790     //                }
791     //            }
792     //            return this;
793     //        }
794     //
795     //        BitSet128 bits(char... bits) {
796     //            for (char bit : bits) {
797     //                if (bit < 64) {
798     //                    low |= 1L << bit;
799     //                } else {
800     //                    high |= 1L << bit - 64;
801     //                }
802     //            }
803     //            return this;
804     //        }
805     //
806     //        long high() {
807     //            return high;
808     //        }
809     //
810     //        long low() {
811     //            return low;
812     //        }
813     //
814     //        static boolean contains(byte bit, long high, long low) {
815     //            if (bit < 0) {
816     //                return false;
817     //            }
818     //            if (bit < 64) {
819     //                return 0 != (low & 1L << bit);
820     //            }
821     //            return 0 != (high & 1L << bit - 64);
822     //        }
823     //    }
824 
825     // BitSet128 tokenChars = new BitSet128()
826     //        .range('0', '9').range('a', 'z').range('A', 'Z') // Alphanumeric.
827     //        .bits('-', '.', '_', '~') // Unreserved characters.
828     //        .bits('!', '#', '$', '%', '&', '\'', '*', '+', '^', '`', '|'); // Token special characters.
829 
830     // This constants calculated by the above code
831     private static final long TOKEN_CHARS_HIGH = 0x57ffffffc7fffffeL;
832     private static final long TOKEN_CHARS_LOW = 0x3ff6cfa00000000L;
833 
834     static boolean isValidTokenChar(byte octet) {
835         if (octet < 0) {
836             return false;
837         }
838         if (octet < 64) {
839             return 0 != (TOKEN_CHARS_LOW & 1L << octet);
840         }
841         return 0 != (TOKEN_CHARS_HIGH & 1L << octet - 64);
842     }
843 }