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