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  
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          text = text.trim();
74  
75          if (text.isEmpty()) {
76              throw new IllegalArgumentException("text is empty (possibly HTTP/0.9)");
77          }
78  
79          // Try to match without convert to uppercase first as this is what 99% of all clients
80          // will send anyway. Also there is a change to the RFC to make it clear that it is
81          // expected to be case-sensitive
82          //
83          // See:
84          // * https://trac.tools.ietf.org/wg/httpbis/trac/ticket/1
85          // * https://trac.tools.ietf.org/wg/httpbis/trac/wiki
86          //
87          HttpVersion version = version0(text);
88          if (version == null) {
89              version = new HttpVersion(text, strict, true);
90          }
91          return version;
92      }
93  
94      private static HttpVersion version0(String text) {
95          if (HTTP_1_1_STRING.equals(text)) {
96              return HTTP_1_1;
97          }
98          if (HTTP_1_0_STRING.equals(text)) {
99              return HTTP_1_0;
100         }
101         return null;
102     }
103 
104     private final String protocolName;
105     private final int majorVersion;
106     private final int minorVersion;
107     private final String text;
108     private final boolean keepAliveDefault;
109     private final byte[] bytes;
110 
111     /**
112      * Creates a new HTTP version with the specified version string.  You will
113      * not need to create a new instance unless you are implementing a protocol
114      * derived from HTTP, such as
115      * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
116      * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
117      *
118      * @param keepAliveDefault
119      *        {@code true} if and only if the connection is kept alive unless
120      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
121      */
122     public HttpVersion(String text, boolean keepAliveDefault) {
123         this(text, false, keepAliveDefault);
124     }
125 
126     HttpVersion(String text, boolean strict, boolean keepAliveDefault) {
127         text = checkNonEmptyAfterTrim(text, "text").toUpperCase();
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             Matcher m = VERSION_PATTERN.matcher(text);
142             if (!m.matches()) {
143                 throw new IllegalArgumentException("invalid version format: " + text);
144             }
145 
146             protocolName = m.group(1);
147             majorVersion = Integer.parseInt(m.group(2));
148             minorVersion = Integer.parseInt(m.group(3));
149         }
150 
151         this.text = protocolName + '/' + majorVersion + '.' + minorVersion;
152         this.keepAliveDefault = keepAliveDefault;
153         bytes = null;
154     }
155 
156     private static int toDecimal(final int value) {
157         if (value < '0' || value > '9') {
158             throw new IllegalArgumentException("Invalid version number, only 0-9 (0x30-0x39) allowed," +
159                     " but received a '" + (char) value + "' (0x" + Integer.toHexString(value) + ")");
160         }
161         return value - '0';
162     }
163 
164     /**
165      * Creates a new HTTP version with the specified protocol name and version
166      * numbers.  You will not need to create a new instance unless you are
167      * implementing a protocol derived from HTTP, such as
168      * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
169      * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>
170      *
171      * @param keepAliveDefault
172      *        {@code true} if and only if the connection is kept alive unless
173      *        the {@code "Connection"} header is set to {@code "close"} explicitly.
174      */
175     public HttpVersion(
176             String protocolName, int majorVersion, int minorVersion,
177             boolean keepAliveDefault) {
178         this(protocolName, majorVersion, minorVersion, keepAliveDefault, false);
179     }
180 
181     private HttpVersion(
182             String protocolName, int majorVersion, int minorVersion,
183             boolean keepAliveDefault, boolean bytes) {
184         protocolName = checkNonEmptyAfterTrim(protocolName, "protocolName").toUpperCase();
185 
186         for (int i = 0; i < protocolName.length(); i ++) {
187             if (Character.isISOControl(protocolName.charAt(i)) ||
188                     Character.isWhitespace(protocolName.charAt(i))) {
189                 throw new IllegalArgumentException("invalid character in protocolName");
190             }
191         }
192 
193         checkPositiveOrZero(majorVersion, "majorVersion");
194         checkPositiveOrZero(minorVersion, "minorVersion");
195 
196         this.protocolName = protocolName;
197         this.majorVersion = majorVersion;
198         this.minorVersion = minorVersion;
199         text = protocolName + '/' + majorVersion + '.' + minorVersion;
200         this.keepAliveDefault = keepAliveDefault;
201 
202         if (bytes) {
203             this.bytes = text.getBytes(CharsetUtil.US_ASCII);
204         } else {
205             this.bytes = null;
206         }
207     }
208 
209     /**
210      * Returns the name of the protocol such as {@code "HTTP"} in {@code "HTTP/1.0"}.
211      */
212     public String protocolName() {
213         return protocolName;
214     }
215 
216     /**
217      * Returns the name of the protocol such as {@code 1} in {@code "HTTP/1.0"}.
218      */
219     public int majorVersion() {
220         return majorVersion;
221     }
222 
223     /**
224      * Returns the name of the protocol such as {@code 0} in {@code "HTTP/1.0"}.
225      */
226     public int minorVersion() {
227         return minorVersion;
228     }
229 
230     /**
231      * Returns the full protocol version text such as {@code "HTTP/1.0"}.
232      */
233     public String text() {
234         return text;
235     }
236 
237     /**
238      * Returns {@code true} if and only if the connection is kept alive unless
239      * the {@code "Connection"} header is set to {@code "close"} explicitly.
240      */
241     public boolean isKeepAliveDefault() {
242         return keepAliveDefault;
243     }
244 
245     /**
246      * Returns the full protocol version text such as {@code "HTTP/1.0"}.
247      */
248     @Override
249     public String toString() {
250         return text();
251     }
252 
253     @Override
254     public int hashCode() {
255         return (protocolName().hashCode() * 31 + majorVersion()) * 31 +
256                minorVersion();
257     }
258 
259     @Override
260     public boolean equals(Object o) {
261         if (!(o instanceof HttpVersion)) {
262             return false;
263         }
264 
265         HttpVersion that = (HttpVersion) o;
266         return minorVersion() == that.minorVersion() &&
267                majorVersion() == that.majorVersion() &&
268                protocolName().equals(that.protocolName());
269     }
270 
271     @Override
272     public int compareTo(HttpVersion o) {
273         int v = protocolName().compareTo(o.protocolName());
274         if (v != 0) {
275             return v;
276         }
277 
278         v = majorVersion() - o.majorVersion();
279         if (v != 0) {
280             return v;
281         }
282 
283         return minorVersion() - o.minorVersion();
284     }
285 
286     void encode(ByteBuf buf) {
287         if (bytes == null) {
288             buf.writeCharSequence(text, CharsetUtil.US_ASCII);
289         } else {
290             buf.writeBytes(bytes);
291         }
292     }
293 }