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 }