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 trueif 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 trueif 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 falseif 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 trueif 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 trueif the specified message contains an expect header specifying an expectation that is not
256      * supported. Note that this method returns {@code falseif 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 trueif 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 nullif 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 nullif 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 nullif 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 nullif 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 nullif 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