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