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