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