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