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