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