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