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