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 }