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