View Javadoc
1   /*
2    * Copyright 2012 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    *   https://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.buffer.ByteBuf;
19  import io.netty.util.CharsetUtil;
20  import io.netty.util.internal.ObjectUtil;
21  
22  import java.util.Locale;
23  
24  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
25  
26  /**
27   * The version of HTTP or its derived protocols, such as
28   * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
29   * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
30   */
31  public class HttpVersion implements Comparable<HttpVersion> {
32  
33      static final String HTTP_1_0_STRING = "HTTP/1.0";
34      static final String HTTP_1_1_STRING = "HTTP/1.1";
35      private static final int MAX_INT_DIV_10 = Integer.MAX_VALUE / 10;
36      private static final int MAX_INT_MOD_10 = Integer.MAX_VALUE % 10;
37  
38      /**
39       * HTTP/1.0
40       */
41      public static final HttpVersion HTTP_1_0 = new HttpVersion("HTTP", 1, 0, false, true);
42  
43      /**
44       * HTTP/1.1
45       */
46      public static final HttpVersion HTTP_1_1 = new HttpVersion("HTTP", 1, 1, true, true);
47  
48      /**
49       * Returns an existing or new {@link HttpVersion} instance which matches to
50       * the specified protocol version string.  If the specified {@code text} is
51       * equal to {@code "HTTP/1.0"}, {@link #HTTP_1_0} will be returned.  If the
52       * specified {@code text} is equal to {@code "HTTP/1.1"}, {@link #HTTP_1_1}
53       * will be returned.  Otherwise, a new {@link HttpVersion} instance will be
54       * returned.
55       */
56      public static HttpVersion valueOf(String text) {
57          return valueOf(text, false);
58      }
59  
60      static HttpVersion valueOf(String text, boolean strict) {
61          ObjectUtil.checkNotNull(text, "text");
62  
63          // super fast-path
64          if (text == HTTP_1_1_STRING) {
65              return HTTP_1_1;
66          }
67          if (text == HTTP_1_0_STRING) {
68              return HTTP_1_0;
69          }
70  
71          if (text.isEmpty()) {
72              throw new IllegalArgumentException("text is empty (possibly HTTP/0.9)");
73          }
74  
75          // Try to match without convert to uppercase first as this is what 99% of all clients
76          // will send anyway. Also there is a change to the RFC to make it clear that it is
77          // expected to be case-sensitive
78          //
79          // See:
80          // * https://trac.tools.ietf.org/wg/httpbis/trac/ticket/1
81          // * https://trac.tools.ietf.org/wg/httpbis/trac/wiki
82          //
83          HttpVersion version = version0(text);
84          if (version == null) {
85              version = new HttpVersion(text, strict, true);
86          }
87          return version;
88      }
89  
90      private static HttpVersion version0(String text) {
91          if (HTTP_1_1_STRING.equals(text)) {
92              return HTTP_1_1;
93          }
94          if (HTTP_1_0_STRING.equals(text)) {
95              return HTTP_1_0;
96          }
97          return null;
98      }
99  
100     private final String protocolName;
101     private final int majorVersion;
102     private final int minorVersion;
103     private final String text;
104     private final boolean keepAliveDefault;
105     private final byte[] bytes;
106 
107     /**
108      * Creates a new HTTP version with the specified version string.  You will
109      * not need to create a new instance unless you are implementing a protocol
110      * derived from HTTP, such as
111      * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
112      * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
113      *
114      * @param keepAliveDefault
115      *        {@code true} if and only if the connection is kept alive unless
116      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
117      */
118     public HttpVersion(String text, boolean keepAliveDefault) {
119         this(text, false, keepAliveDefault);
120     }
121 
122     HttpVersion(String text, boolean strict, boolean keepAliveDefault) {
123         // toUpperCase() without an explicit Locale uses the JVM default. In Turkish locale
124         // (tr_TR) 'i' uppercases to 'İ' (U+0130), which would corrupt protocol strings such
125         // as "icap/1.0" or any custom HTTP-derived scheme that contains a lowercase 'i'.
126         // Control characters or whitespace at the token boundary must fail the checks below.
127         ObjectUtil.checkNotNull(text, "text");
128         if (text.isEmpty()) {
129             throw new IllegalArgumentException("text must not be empty");
130         }
131         text = text.toUpperCase(Locale.US);
132 
133         if (strict) {
134             // Only single digit major / minor version is allowed.
135             // See
136             //  - https://datatracker.ietf.org/doc/html/rfc7230#section-2.6
137             //  - https://datatracker.ietf.org/doc/html/rfc9110#name-protocol-version
138             if (text.length() != 8 || !text.startsWith("HTTP/") || text.charAt(6) != '.') {
139                 throw new IllegalArgumentException("invalid version format: " + text);
140             }
141             protocolName = "HTTP";
142             majorVersion = toDecimal(text.charAt(5));
143             minorVersion = toDecimal(text.charAt(7));
144         } else {
145             int slashIndex = text.indexOf('/');
146             int dotIndex = text.indexOf('.', slashIndex + 1);
147 
148             if (slashIndex <= 0 || dotIndex <= slashIndex + 1
149                     || dotIndex >= text.length() - 1 || hasControlOrWhitespace(text, slashIndex)) {
150                 throw new IllegalArgumentException("invalid version format: " + text);
151             }
152 
153             protocolName = text.substring(0, slashIndex);
154             majorVersion = parseInt(text, slashIndex + 1, dotIndex);
155             minorVersion = parseInt(text, dotIndex + 1, text.length());
156         }
157 
158         this.text = protocolName + '/' + majorVersion + '.' + minorVersion;
159         this.keepAliveDefault = keepAliveDefault;
160         bytes = null;
161     }
162 
163     private static boolean hasControlOrWhitespace(String s, int end) {
164         for (int i = 0; i < end; i++) {
165             char c = s.charAt(i);
166             if (Character.isISOControl(c) || Character.isWhitespace(c)) {
167                 return true;
168             }
169         }
170         return false;
171     }
172 
173     private static int parseInt(String text, int start, int end) {
174         int result = 0;
175         for (int i = start; i < end; i++) {
176             int digit = toDecimal(text.charAt(i));
177             if (result > MAX_INT_DIV_10 || result == MAX_INT_DIV_10 && digit > MAX_INT_MOD_10) {
178                 throw new IllegalArgumentException("Invalid version number, too large: " + text);
179             }
180             result = result * 10 + digit;
181         }
182         return result;
183     }
184 
185     private static int toDecimal(final int value) {
186         if (value < '0' || value > '9') {
187             throw new IllegalArgumentException("Invalid version number, only 0-9 (0x30-0x39) allowed," +
188                     " but received a '" + (char) value + "' (0x" + Integer.toHexString(value) + ")");
189         }
190         return value - '0';
191     }
192 
193     /**
194      * Creates a new HTTP version with the specified protocol name and version
195      * numbers.  You will not need to create a new instance unless you are
196      * implementing a protocol derived from HTTP, such as
197      * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
198      * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>
199      *
200      * @param keepAliveDefault
201      *        {@code true} if and only if the connection is kept alive unless
202      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
203      */
204     public HttpVersion(
205             String protocolName, int majorVersion, int minorVersion,
206             boolean keepAliveDefault) {
207         this(protocolName, majorVersion, minorVersion, keepAliveDefault, false);
208     }
209 
210     private HttpVersion(
211             String protocolName, int majorVersion, int minorVersion,
212             boolean keepAliveDefault, boolean bytes) {
213         // See the comment in the (text, strict, keepAliveDefault) constructor for why this needs
214         // an explicit Locale.US: avoids the Turkish-locale 'i' -> 'İ' corruption.
215         ObjectUtil.checkNotNull(protocolName, "protocolName");
216         if (protocolName.isEmpty()) {
217             throw new IllegalArgumentException("protocolName must not be empty");
218         }
219         protocolName = protocolName.toUpperCase(Locale.US);
220 
221         if (hasControlOrWhitespace(protocolName, protocolName.length())) {
222             throw new IllegalArgumentException("invalid character in protocolName");
223         }
224 
225         checkPositiveOrZero(majorVersion, "majorVersion");
226         checkPositiveOrZero(minorVersion, "minorVersion");
227 
228         this.protocolName = protocolName;
229         this.majorVersion = majorVersion;
230         this.minorVersion = minorVersion;
231         text = protocolName + '/' + majorVersion + '.' + minorVersion;
232         this.keepAliveDefault = keepAliveDefault;
233 
234         if (bytes) {
235             this.bytes = text.getBytes(CharsetUtil.US_ASCII);
236         } else {
237             this.bytes = null;
238         }
239     }
240 
241     /**
242      * Returns the name of the protocol such as {@code "HTTP"} in {@code "HTTP/1.0"}.
243      */
244     public String protocolName() {
245         return protocolName;
246     }
247 
248     /**
249      * Returns the name of the protocol such as {@code 1} in {@code "HTTP/1.0"}.
250      */
251     public int majorVersion() {
252         return majorVersion;
253     }
254 
255     /**
256      * Returns the name of the protocol such as {@code 0} in {@code "HTTP/1.0"}.
257      */
258     public int minorVersion() {
259         return minorVersion;
260     }
261 
262     /**
263      * Returns the full protocol version text such as {@code "HTTP/1.0"}.
264      */
265     public String text() {
266         return text;
267     }
268 
269     /**
270      * Returns {@code true} if and only if the connection is kept alive unless
271      * the {@code "Connection"} header is set to {@code "close"} explicitly.
272      */
273     public boolean isKeepAliveDefault() {
274         return keepAliveDefault;
275     }
276 
277     /**
278      * Returns the full protocol version text such as {@code "HTTP/1.0"}.
279      */
280     @Override
281     public String toString() {
282         return text();
283     }
284 
285     @Override
286     public int hashCode() {
287         return (protocolName().hashCode() * 31 + majorVersion()) * 31 +
288                minorVersion();
289     }
290 
291     @Override
292     public boolean equals(Object o) {
293         if (!(o instanceof HttpVersion)) {
294             return false;
295         }
296 
297         HttpVersion that = (HttpVersion) o;
298         return minorVersion() == that.minorVersion() &&
299                majorVersion() == that.majorVersion() &&
300                protocolName().equals(that.protocolName());
301     }
302 
303     @Override
304     public int compareTo(HttpVersion o) {
305         int v = protocolName().compareTo(o.protocolName());
306         if (v != 0) {
307             return v;
308         }
309 
310         v = majorVersion() - o.majorVersion();
311         if (v != 0) {
312             return v;
313         }
314 
315         return minorVersion() - o.minorVersion();
316     }
317 
318     void encode(ByteBuf buf) {
319         if (bytes == null) {
320             buf.writeCharSequence(text, CharsetUtil.US_ASCII);
321         } else {
322             buf.writeBytes(bytes);
323         }
324     }
325 }