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