View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.http.message;
29  
30  import java.util.BitSet;
31  
32  import org.apache.hc.core5.annotation.Contract;
33  import org.apache.hc.core5.annotation.ThreadingBehavior;
34  import org.apache.hc.core5.http.Header;
35  import org.apache.hc.core5.http.HttpVersion;
36  import org.apache.hc.core5.http.ParseException;
37  import org.apache.hc.core5.http.ProtocolVersion;
38  import org.apache.hc.core5.util.Args;
39  import org.apache.hc.core5.util.CharArrayBuffer;
40  import org.apache.hc.core5.util.TextUtils;
41  
42  /**
43   * Default {@link org.apache.hc.core5.http.message.LineParser} implementation.
44   *
45   * @since 4.0
46   */
47  @Contract(threading = ThreadingBehavior.IMMUTABLE)
48  public class BasicLineParser implements LineParser {
49  
50      public final static BasicLineParser INSTANCE = new BasicLineParser();
51  
52      // IMPORTANT!
53      // These private static variables must be treated as immutable and never exposed outside this class
54      private static final BitSet FULL_STOP = TokenParser.INIT_BITSET('.');
55      private static final BitSet BLANKS = TokenParser.INIT_BITSET(' ', '\t');
56      private static final BitSet COLON = TokenParser.INIT_BITSET(':');
57  
58      /**
59       * A version of the protocol to parse.
60       * The version is typically not relevant, but the protocol name.
61       */
62      private final ProtocolVersion protocol;
63      private final TokenParser tokenParser;
64  
65      /**
66       * Creates a new line parser for the given HTTP-like protocol.
67       *
68       * @param proto     a version of the protocol to parse, or
69       *                  {@code null} for HTTP. The actual version
70       *                  is not relevant, only the protocol name.
71       */
72      public BasicLineParser(final ProtocolVersion proto) {
73          this.protocol = proto != null? proto : HttpVersion.HTTP_1_1;
74          this.tokenParser = TokenParser.INSTANCE;
75      }
76  
77      /**
78       * Creates a new line parser for HTTP.
79       */
80      public BasicLineParser() {
81          this(null);
82      }
83  
84      ProtocolVersion parseProtocolVersion(
85              final CharArrayBuffer buffer,
86              final ParserCursor cursor) throws ParseException {
87          final String protoname = this.protocol.getProtocol();
88          final int protolength  = protoname.length();
89  
90          this.tokenParser.skipWhiteSpace(buffer, cursor);
91  
92          final int pos = cursor.getPos();
93  
94          // long enough for "HTTP/1.1"?
95          if (pos + protolength + 4 > cursor.getUpperBound()) {
96              throw new ParseException("Invalid protocol version",
97                      buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
98          }
99  
100         // check the protocol name and slash
101         boolean ok = true;
102         for (int i = 0; ok && (i < protolength); i++) {
103             ok = buffer.charAt(pos + i) == protoname.charAt(i);
104         }
105         if (ok) {
106             ok = buffer.charAt(pos + protolength) == '/';
107         }
108         if (!ok) {
109             throw new ParseException("Invalid protocol version",
110                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
111         }
112 
113         cursor.updatePos(pos + protolength + 1);
114 
115         final String token1 = this.tokenParser.parseToken(buffer, cursor, FULL_STOP);
116         final int major;
117         try {
118             major = Integer.parseInt(token1);
119         } catch (final NumberFormatException e) {
120             throw new ParseException("Invalid protocol major version number",
121                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
122         }
123         if (cursor.atEnd()) {
124             throw new ParseException("Invalid protocol version",
125                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
126         }
127         cursor.updatePos(cursor.getPos() + 1);
128         final String token2 = this.tokenParser.parseToken(buffer, cursor, BLANKS);
129         final int minor;
130         try {
131             minor = Integer.parseInt(token2);
132         } catch (final NumberFormatException e) {
133             throw new ParseException("Invalid protocol minor version number",
134                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
135         }
136         return new HttpVersion(major, minor);
137     }
138 
139     /**
140      * Parses a request line.
141      *
142      * @param buffer    a buffer holding the line to parse
143      *
144      * @return  the parsed request line
145      *
146      * @throws ParseException        in case of a parse error
147      */
148     @Override
149     public RequestLine parseRequestLine(final CharArrayBuffer buffer) throws ParseException {
150         Args.notNull(buffer, "Char array buffer");
151 
152         final ParserCursor cursor = new ParserCursor(0, buffer.length());
153         this.tokenParser.skipWhiteSpace(buffer, cursor);
154         final String method = this.tokenParser.parseToken(buffer, cursor, BLANKS);
155         if (TextUtils.isEmpty(method)) {
156             throw new ParseException("Invalid request line",
157                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
158         }
159         this.tokenParser.skipWhiteSpace(buffer, cursor);
160         final String uri = this.tokenParser.parseToken(buffer, cursor, BLANKS);
161         if (TextUtils.isEmpty(uri)) {
162             throw new ParseException("Invalid request line",
163                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
164         }
165         final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
166         this.tokenParser.skipWhiteSpace(buffer, cursor);
167         if (!cursor.atEnd()) {
168             throw new ParseException("Invalid request line",
169                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
170         }
171         return new RequestLine(method, uri, ver);
172     }
173 
174     @Override
175     public StatusLine parseStatusLine(final CharArrayBuffer buffer) throws ParseException {
176         Args.notNull(buffer, "Char array buffer");
177 
178         final ParserCursor cursor = new ParserCursor(0, buffer.length());
179         this.tokenParser.skipWhiteSpace(buffer, cursor);
180         final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
181         this.tokenParser.skipWhiteSpace(buffer, cursor);
182         final String s = this.tokenParser.parseToken(buffer, cursor, BLANKS);
183         for (int i = 0; i < s.length(); i++) {
184             if (!Character.isDigit(s.charAt(i))) {
185                 throw new ParseException("Status line contains invalid status code",
186                         buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
187             }
188         }
189         final int statusCode;
190         try {
191             statusCode = Integer.parseInt(s);
192         } catch (final NumberFormatException e) {
193             throw new ParseException("Status line contains invalid status code",
194                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
195         }
196         final String text = buffer.substringTrimmed(cursor.getPos(), cursor.getUpperBound());
197         return new StatusLine(ver, statusCode, text);
198     }
199 
200     @Override
201     public Header parseHeader(final CharArrayBuffer buffer) throws ParseException {
202         Args.notNull(buffer, "Char array buffer");
203 
204         final ParserCursor cursor = new ParserCursor(0, buffer.length());
205         this.tokenParser.skipWhiteSpace(buffer, cursor);
206         final String name = this.tokenParser.parseToken(buffer, cursor, COLON);
207         if (cursor.getPos() == cursor.getLowerBound() || cursor.getPos() == cursor.getUpperBound() ||
208                 buffer.charAt(cursor.getPos()) != ':' ||
209                 TextUtils.isEmpty(name) ||
210                 TokenParser.isWhitespace(buffer.charAt(cursor.getPos() - 1))) {
211             throw new ParseException("Invalid header",
212                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
213         }
214         final String value = buffer.substringTrimmed(cursor.getPos() + 1, cursor.getUpperBound());
215         return new BasicHeader(name, value);
216     }
217 
218 }