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 import io.netty.util.internal.UnstableApi;
32
33 import static io.netty.util.internal.StringUtil.COMMA;
34 import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
35
36 /**
37 * Utility methods useful in the HTTP context.
38 */
39 public final class HttpUtil {
40
41 private static final AsciiString CHARSET_EQUALS = AsciiString.of(HttpHeaderValues.CHARSET + "=");
42 private static final AsciiString SEMICOLON = AsciiString.cached(";");
43 private static final String COMMA_STRING = String.valueOf(COMMA);
44
45 private HttpUtil() { }
46
47 /**
48 * Determine if a uri is in origin-form according to
49 * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
50 */
51 public static boolean isOriginForm(URI uri) {
52 return isOriginForm(uri.toString());
53 }
54
55 /**
56 * Determine if a string uri is in origin-form according to
57 * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
58 */
59 public static boolean isOriginForm(String uri) {
60 return uri.startsWith("/");
61 }
62
63 /**
64 * Determine if a uri is in asterisk-form according to
65 * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
66 */
67 public static boolean isAsteriskForm(URI uri) {
68 return isAsteriskForm(uri.toString());
69 }
70
71 /**
72 * Determine if a string uri is in asterisk-form according to
73 * <a href="https://tools.ietf.org/html/rfc7230#section-5.3">rfc7230, 5.3</a>.
74 */
75 public static boolean isAsteriskForm(String uri) {
76 return "*".equals(uri);
77 }
78
79 /**
80 * Returns {@code true} if and only if the connection can remain open and
81 * thus 'kept alive'. This methods respects the value of the.
82 *
83 * {@code "Connection"} header first and then the return value of
84 * {@link HttpVersion#isKeepAliveDefault()}.
85 */
86 public static boolean isKeepAlive(HttpMessage message) {
87 return !message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE, true) &&
88 (message.protocolVersion().isKeepAliveDefault() ||
89 message.headers().containsValue(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE, true));
90 }
91
92 /**
93 * Sets the value of the {@code "Connection"} header depending on the
94 * protocol version of the specified message. This getMethod sets or removes
95 * the {@code "Connection"} header depending on what the default keep alive
96 * mode of the message's protocol version is, as specified by
97 * {@link HttpVersion#isKeepAliveDefault()}.
98 * <ul>
99 * <li>If the connection is kept alive by default:
100 * <ul>
101 * <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
102 * <li>remove otherwise.</li>
103 * </ul></li>
104 * <li>If the connection is closed by default:
105 * <ul>
106 * <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
107 * <li>remove otherwise.</li>
108 * </ul></li>
109 * </ul>
110 * @see #setKeepAlive(HttpHeaders, HttpVersion, boolean)
111 */
112 public static void setKeepAlive(HttpMessage message, boolean keepAlive) {
113 setKeepAlive(message.headers(), message.protocolVersion(), keepAlive);
114 }
115
116 /**
117 * Sets the value of the {@code "Connection"} header depending on the
118 * protocol version of the specified message. This getMethod sets or removes
119 * the {@code "Connection"} header depending on what the default keep alive
120 * mode of the message's protocol version is, as specified by
121 * {@link HttpVersion#isKeepAliveDefault()}.
122 * <ul>
123 * <li>If the connection is kept alive by default:
124 * <ul>
125 * <li>set to {@code "close"} if {@code keepAlive} is {@code false}.</li>
126 * <li>remove otherwise.</li>
127 * </ul></li>
128 * <li>If the connection is closed by default:
129 * <ul>
130 * <li>set to {@code "keep-alive"} if {@code keepAlive} is {@code true}.</li>
131 * <li>remove otherwise.</li>
132 * </ul></li>
133 * </ul>
134 */
135 public static void setKeepAlive(HttpHeaders h, HttpVersion httpVersion, boolean keepAlive) {
136 if (httpVersion.isKeepAliveDefault()) {
137 if (keepAlive) {
138 h.remove(HttpHeaderNames.CONNECTION);
139 } else {
140 h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
141 }
142 } else {
143 if (keepAlive) {
144 h.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
145 } else {
146 h.remove(HttpHeaderNames.CONNECTION);
147 }
148 }
149 }
150
151 /**
152 * Returns the length of the content. Please note that this value is
153 * not retrieved from {@link HttpContent#content()} but from the
154 * {@code "Content-Length"} header, and thus they are independent from each
155 * other.
156 *
157 * @return the content length
158 *
159 * @throws NumberFormatException
160 * if the message does not have the {@code "Content-Length"} header
161 * or its value is not a number
162 */
163 public static long getContentLength(HttpMessage message) {
164 String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
165 if (value != null) {
166 return Long.parseLong(value);
167 }
168
169 // We know the content length if it's a Web Socket message even if
170 // Content-Length header is missing.
171 long webSocketContentLength = getWebSocketContentLength(message);
172 if (webSocketContentLength >= 0) {
173 return webSocketContentLength;
174 }
175
176 // Otherwise we don't.
177 throw new NumberFormatException("header not found: " + HttpHeaderNames.CONTENT_LENGTH);
178 }
179
180 /**
181 * Returns the length of the content or the specified default value if the message does not have the {@code
182 * "Content-Length" header}. Please note that this value is not retrieved from {@link HttpContent#content()} but
183 * from the {@code "Content-Length"} header, and thus they are independent from each other.
184 *
185 * @param message the message
186 * @param defaultValue the default value
187 * @return the content length or the specified default value
188 * @throws NumberFormatException if the {@code "Content-Length"} header does not parse as a long
189 */
190 public static long getContentLength(HttpMessage message, long defaultValue) {
191 String value = message.headers().get(HttpHeaderNames.CONTENT_LENGTH);
192 if (value != null) {
193 return Long.parseLong(value);
194 }
195
196 // We know the content length if it's a Web Socket message even if
197 // Content-Length header is missing.
198 long webSocketContentLength = getWebSocketContentLength(message);
199 if (webSocketContentLength >= 0) {
200 return webSocketContentLength;
201 }
202
203 // Otherwise we don't.
204 return defaultValue;
205 }
206
207 /**
208 * Get an {@code int} representation of {@link #getContentLength(HttpMessage, long)}.
209 *
210 * @return the content length or {@code defaultValue} if this message does
211 * not have the {@code "Content-Length"} header.
212 *
213 * @throws NumberFormatException if the {@code "Content-Length"} header does not parse as an int
214 */
215 public static int getContentLength(HttpMessage message, int defaultValue) {
216 return (int) Math.min(Integer.MAX_VALUE, getContentLength(message, (long) defaultValue));
217 }
218
219 /**
220 * Returns the content length of the specified web socket message. If the
221 * specified message is not a web socket message, {@code -1} is returned.
222 */
223 static int getWebSocketContentLength(HttpMessage message) {
224 // WebSocket messages have constant content-lengths.
225 HttpHeaders h = message.headers();
226 if (message instanceof HttpRequest) {
227 HttpRequest req = (HttpRequest) message;
228 if (HttpMethod.GET.equals(req.method()) &&
229 h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY1) &&
230 h.contains(HttpHeaderNames.SEC_WEBSOCKET_KEY2)) {
231 return 8;
232 }
233 } else if (message instanceof HttpResponse) {
234 HttpResponse res = (HttpResponse) message;
235 if (res.status().code() == 101 &&
236 h.contains(HttpHeaderNames.SEC_WEBSOCKET_ORIGIN) &&
237 h.contains(HttpHeaderNames.SEC_WEBSOCKET_LOCATION)) {
238 return 16;
239 }
240 }
241
242 // Not a web socket message
243 return -1;
244 }
245
246 /**
247 * Sets the {@code "Content-Length"} header.
248 */
249 public static void setContentLength(HttpMessage message, long length) {
250 message.headers().set(HttpHeaderNames.CONTENT_LENGTH, length);
251 }
252
253 public static boolean isContentLengthSet(HttpMessage m) {
254 return m.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
255 }
256
257 /**
258 * Returns {@code true} if and only if the specified message contains an expect header and the only expectation
259 * present is the 100-continue expectation. Note that this method returns {@code false} if the expect header is
260 * not valid for the message (e.g., the message is a response, or the version on the message is HTTP/1.0).
261 *
262 * @param message the message
263 * @return {@code true} if and only if the expectation 100-continue is present and it is the only expectation
264 * present
265 */
266 public static boolean is100ContinueExpected(HttpMessage message) {
267 return isExpectHeaderValid(message)
268 // unquoted tokens in the expect header are case-insensitive, thus 100-continue is case insensitive
269 && message.headers().contains(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE, true);
270 }
271
272 /**
273 * Returns {@code true} if the specified message contains an expect header specifying an expectation that is not
274 * supported. Note that this method returns {@code false} if the expect header is not valid for the message
275 * (e.g., the message is a response, or the version on the message is HTTP/1.0).
276 *
277 * @param message the message
278 * @return {@code true} if and only if an expectation is present that is not supported
279 */
280 static boolean isUnsupportedExpectation(HttpMessage message) {
281 if (!isExpectHeaderValid(message)) {
282 return false;
283 }
284
285 final String expectValue = message.headers().get(HttpHeaderNames.EXPECT);
286 return expectValue != null && !HttpHeaderValues.CONTINUE.toString().equalsIgnoreCase(expectValue);
287 }
288
289 private static boolean isExpectHeaderValid(final HttpMessage message) {
290 /*
291 * Expect: 100-continue is for requests only and it works only on HTTP/1.1 or later. Note further that RFC 7231
292 * section 5.1.1 says "A server that receives a 100-continue expectation in an HTTP/1.0 request MUST ignore
293 * that expectation."
294 */
295 return message instanceof HttpRequest &&
296 message.protocolVersion().compareTo(HttpVersion.HTTP_1_1) >= 0;
297 }
298
299 /**
300 * Sets or removes the {@code "Expect: 100-continue"} header to / from the
301 * specified message. If {@code expected} is {@code true},
302 * the {@code "Expect: 100-continue"} header is set and all other previous
303 * {@code "Expect"} headers are removed. Otherwise, all {@code "Expect"}
304 * headers are removed completely.
305 */
306 public static void set100ContinueExpected(HttpMessage message, boolean expected) {
307 if (expected) {
308 message.headers().set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
309 } else {
310 message.headers().remove(HttpHeaderNames.EXPECT);
311 }
312 }
313
314 /**
315 * Checks to see if the transfer encoding in a specified {@link HttpMessage} is chunked
316 *
317 * @param message The message to check
318 * @return True if transfer encoding is chunked, otherwise false
319 */
320 public static boolean isTransferEncodingChunked(HttpMessage message) {
321 return message.headers().containsValue(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED, true);
322 }
323
324 /**
325 * Set the {@link HttpHeaderNames#TRANSFER_ENCODING} to either include {@link HttpHeaderValues#CHUNKED} if
326 * {@code chunked} is {@code true}, or remove {@link HttpHeaderValues#CHUNKED} if {@code chunked} is {@code false}.
327 *
328 * @param m The message which contains the headers to modify.
329 * @param chunked if {@code true} then include {@link HttpHeaderValues#CHUNKED} in the headers. otherwise remove
330 * {@link HttpHeaderValues#CHUNKED} from the headers.
331 */
332 public static void setTransferEncodingChunked(HttpMessage m, boolean chunked) {
333 if (chunked) {
334 m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
335 m.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
336 } else {
337 List<String> encodings = m.headers().getAll(HttpHeaderNames.TRANSFER_ENCODING);
338 if (encodings.isEmpty()) {
339 return;
340 }
341 List<CharSequence> values = new ArrayList<CharSequence>(encodings);
342 Iterator<CharSequence> valuesIt = values.iterator();
343 while (valuesIt.hasNext()) {
344 CharSequence value = valuesIt.next();
345 if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(value)) {
346 valuesIt.remove();
347 }
348 }
349 if (values.isEmpty()) {
350 m.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
351 } else {
352 m.headers().set(HttpHeaderNames.TRANSFER_ENCODING, values);
353 }
354 }
355 }
356
357 /**
358 * Fetch charset from message's Content-Type header.
359 *
360 * @param message entity to fetch Content-Type header from
361 * @return the charset from message's Content-Type header or {@link CharsetUtil#ISO_8859_1}
362 * if charset is not presented or unparsable
363 */
364 public static Charset getCharset(HttpMessage message) {
365 return getCharset(message, CharsetUtil.ISO_8859_1);
366 }
367
368 /**
369 * Fetch charset from Content-Type header value.
370 *
371 * @param contentTypeValue Content-Type header value to parse
372 * @return the charset from message's Content-Type header or {@link CharsetUtil#ISO_8859_1}
373 * if charset is not presented or unparsable
374 */
375 public static Charset getCharset(CharSequence contentTypeValue) {
376 if (contentTypeValue != null) {
377 return getCharset(contentTypeValue, CharsetUtil.ISO_8859_1);
378 } else {
379 return CharsetUtil.ISO_8859_1;
380 }
381 }
382
383 /**
384 * Fetch charset from message's Content-Type header.
385 *
386 * @param message entity to fetch Content-Type header from
387 * @param defaultCharset result to use in case of empty, incorrect or doesn't contain required part header value
388 * @return the charset from message's Content-Type header or {@code defaultCharset}
389 * if charset is not presented or unparsable
390 */
391 public static Charset getCharset(HttpMessage message, Charset defaultCharset) {
392 CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
393 if (contentTypeValue != null) {
394 return getCharset(contentTypeValue, defaultCharset);
395 } else {
396 return defaultCharset;
397 }
398 }
399
400 /**
401 * Fetch charset from Content-Type header value.
402 *
403 * @param contentTypeValue Content-Type header value to parse
404 * @param defaultCharset result to use in case of empty, incorrect or doesn't contain required part header value
405 * @return the charset from message's Content-Type header or {@code defaultCharset}
406 * if charset is not presented or unparsable
407 */
408 public static Charset getCharset(CharSequence contentTypeValue, Charset defaultCharset) {
409 if (contentTypeValue != null) {
410 CharSequence charsetRaw = getCharsetAsSequence(contentTypeValue);
411 if (charsetRaw != null) {
412 if (charsetRaw.length() > 2) { // at least contains 2 quotes(")
413 if (charsetRaw.charAt(0) == '"' && charsetRaw.charAt(charsetRaw.length() - 1) == '"') {
414 charsetRaw = charsetRaw.subSequence(1, charsetRaw.length() - 1);
415 }
416 }
417 try {
418 return Charset.forName(charsetRaw.toString());
419 } catch (IllegalCharsetNameException ignored) {
420 // just return the default charset
421 } catch (UnsupportedCharsetException ignored) {
422 // just return the default charset
423 }
424 }
425 }
426 return defaultCharset;
427 }
428
429 /**
430 * Fetch charset from message's Content-Type header as a char sequence.
431 *
432 * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
433 * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
434 *
435 * @param message entity to fetch Content-Type header from
436 * @return the {@code CharSequence} with charset from message's Content-Type header
437 * or {@code null} if charset is not presented
438 * @deprecated use {@link #getCharsetAsSequence(HttpMessage)}
439 */
440 @Deprecated
441 public static CharSequence getCharsetAsString(HttpMessage message) {
442 return getCharsetAsSequence(message);
443 }
444
445 /**
446 * Fetch charset from message's Content-Type header as a char sequence.
447 *
448 * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
449 * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
450 *
451 * @return the {@code CharSequence} with charset from message's Content-Type header
452 * or {@code null} if charset is not presented
453 */
454 public static CharSequence getCharsetAsSequence(HttpMessage message) {
455 CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
456 if (contentTypeValue != null) {
457 return getCharsetAsSequence(contentTypeValue);
458 } else {
459 return null;
460 }
461 }
462
463 /**
464 * Fetch charset from Content-Type header value as a char sequence.
465 *
466 * A lot of sites/possibly clients have charset="CHARSET", for example charset="utf-8". Or "utf8" instead of "utf-8"
467 * This is not according to standard, but this method provide an ability to catch desired mistakes manually in code
468 *
469 * @param contentTypeValue Content-Type header value to parse
470 * @return the {@code CharSequence} with charset from message's Content-Type header
471 * or {@code null} if charset is not presented
472 * @throws NullPointerException in case if {@code contentTypeValue == null}
473 */
474 public static CharSequence getCharsetAsSequence(CharSequence contentTypeValue) {
475 ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
476
477 int indexOfCharset = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, CHARSET_EQUALS, 0);
478 if (indexOfCharset == AsciiString.INDEX_NOT_FOUND) {
479 return null;
480 }
481
482 int indexOfEncoding = indexOfCharset + CHARSET_EQUALS.length();
483 if (indexOfEncoding < contentTypeValue.length()) {
484 CharSequence charsetCandidate = contentTypeValue.subSequence(indexOfEncoding, contentTypeValue.length());
485 int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(charsetCandidate, SEMICOLON, 0);
486 if (indexOfSemicolon == AsciiString.INDEX_NOT_FOUND) {
487 return charsetCandidate;
488 }
489
490 return charsetCandidate.subSequence(0, indexOfSemicolon);
491 }
492
493 return null;
494 }
495
496 /**
497 * Fetch MIME type part from message's Content-Type header as a char sequence.
498 *
499 * @param message entity to fetch Content-Type header from
500 * @return the MIME type as a {@code CharSequence} from message's Content-Type header
501 * or {@code null} if content-type header or MIME type part of this header are not presented
502 * <p/>
503 * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
504 * "content-type: text/html" - "text/html" will be returned <br/>
505 * "content-type: " or no header - {@code null} we be returned
506 */
507 public static CharSequence getMimeType(HttpMessage message) {
508 CharSequence contentTypeValue = message.headers().get(HttpHeaderNames.CONTENT_TYPE);
509 if (contentTypeValue != null) {
510 return getMimeType(contentTypeValue);
511 } else {
512 return null;
513 }
514 }
515
516 /**
517 * Fetch MIME type part from Content-Type header value as a char sequence.
518 *
519 * @param contentTypeValue Content-Type header value to parse
520 * @return the MIME type as a {@code CharSequence} from message's Content-Type header
521 * or {@code null} if content-type header or MIME type part of this header are not presented
522 * <p/>
523 * "content-type: text/html; charset=utf-8" - "text/html" will be returned <br/>
524 * "content-type: text/html" - "text/html" will be returned <br/>
525 * "content-type: empty header - {@code null} we be returned
526 * @throws NullPointerException in case if {@code contentTypeValue == null}
527 */
528 public static CharSequence getMimeType(CharSequence contentTypeValue) {
529 ObjectUtil.checkNotNull(contentTypeValue, "contentTypeValue");
530
531 int indexOfSemicolon = AsciiString.indexOfIgnoreCaseAscii(contentTypeValue, SEMICOLON, 0);
532 if (indexOfSemicolon != AsciiString.INDEX_NOT_FOUND) {
533 return contentTypeValue.subSequence(0, indexOfSemicolon);
534 } else {
535 return contentTypeValue.length() > 0 ? contentTypeValue : null;
536 }
537 }
538
539 /**
540 * Formats the host string of an address so it can be used for computing an HTTP component
541 * such as a URL or a Host header
542 *
543 * @param addr the address
544 * @return the formatted String
545 */
546 public static String formatHostnameForHttp(InetSocketAddress addr) {
547 String hostString = NetUtil.getHostname(addr);
548 if (NetUtil.isValidIpV6Address(hostString)) {
549 if (!addr.isUnresolved()) {
550 hostString = NetUtil.toAddressString(addr.getAddress());
551 }
552 return '[' + hostString + ']';
553 }
554 return hostString;
555 }
556
557 /**
558 * Validates, and optionally extracts the content length from headers. This method is not intended for
559 * general use, but is here to be shared between HTTP/1 and HTTP/2 parsing.
560 *
561 * @param contentLengthFields the content-length header fields.
562 * @param isHttp10OrEarlier {@code true} if we are handling HTTP/1.0 or earlier
563 * @param allowDuplicateContentLengths {@code true} if multiple, identical-value content lengths should be allowed.
564 * @return the normalized content length from the headers or {@code -1} if the fields were empty.
565 * @throws IllegalArgumentException if the content-length fields are not valid
566 */
567 @UnstableApi
568 public static long normalizeAndGetContentLength(
569 List<? extends CharSequence> contentLengthFields, boolean isHttp10OrEarlier,
570 boolean allowDuplicateContentLengths) {
571 if (contentLengthFields.isEmpty()) {
572 return -1;
573 }
574
575 // Guard against multiple Content-Length headers as stated in
576 // https://tools.ietf.org/html/rfc7230#section-3.3.2:
577 //
578 // If a message is received that has multiple Content-Length header
579 // fields with field-values consisting of the same decimal value, or a
580 // single Content-Length header field with a field value containing a
581 // list of identical decimal values (e.g., "Content-Length: 42, 42"),
582 // indicating that duplicate Content-Length header fields have been
583 // generated or combined by an upstream message processor, then the
584 // recipient MUST either reject the message as invalid or replace the
585 // duplicated field-values with a single valid Content-Length field
586 // containing that decimal value prior to determining the message body
587 // length or forwarding the message.
588 String firstField = contentLengthFields.get(0).toString();
589 boolean multipleContentLengths =
590 contentLengthFields.size() > 1 || firstField.indexOf(COMMA) >= 0;
591
592 if (multipleContentLengths && !isHttp10OrEarlier) {
593 if (allowDuplicateContentLengths) {
594 // Find and enforce that all Content-Length values are the same
595 String firstValue = null;
596 for (CharSequence field : contentLengthFields) {
597 String[] tokens = field.toString().split(COMMA_STRING, -1);
598 for (String token : tokens) {
599 String trimmed = token.trim();
600 if (firstValue == null) {
601 firstValue = trimmed;
602 } else if (!trimmed.equals(firstValue)) {
603 throw new IllegalArgumentException(
604 "Multiple Content-Length values found: " + contentLengthFields);
605 }
606 }
607 }
608 // Replace the duplicated field-values with a single valid Content-Length field
609 firstField = firstValue;
610 } else {
611 // Reject the message as invalid
612 throw new IllegalArgumentException(
613 "Multiple Content-Length values found: " + contentLengthFields);
614 }
615 }
616 // Ensure we not allow sign as part of the content-length:
617 // See https://github.com/squid-cache/squid/security/advisories/GHSA-qf3v-rc95-96j5
618 if (firstField.isEmpty() || !Character.isDigit(firstField.charAt(0))) {
619 // Reject the message as invalid
620 throw new IllegalArgumentException(
621 "Content-Length value is not a number: " + firstField);
622 }
623 try {
624 final long value = Long.parseLong(firstField);
625 return checkPositiveOrZero(value, "Content-Length value");
626 } catch (NumberFormatException e) {
627 // Reject the message as invalid
628 throw new IllegalArgumentException(
629 "Content-Length value is not a number: " + firstField, e);
630 }
631 }
632 }