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 org.apache.hc.core5.annotation.Contract;
31  import org.apache.hc.core5.annotation.ThreadingBehavior;
32  import org.apache.hc.core5.http.Header;
33  import org.apache.hc.core5.http.HttpVersion;
34  import org.apache.hc.core5.http.ParseException;
35  import org.apache.hc.core5.http.ProtocolVersion;
36  import org.apache.hc.core5.http.ProtocolVersionParser;
37  import org.apache.hc.core5.util.Args;
38  import org.apache.hc.core5.util.CharArrayBuffer;
39  import org.apache.hc.core5.util.TextUtils;
40  import org.apache.hc.core5.util.Tokenizer;
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      private static final Tokenizer.Delimiter BLANKS = Tokenizer.delimiters(' ', '\t');
53      private static final Tokenizer.Delimiter COLON = Tokenizer.delimiters(':');
54  
55      /**
56       * A version of the protocol to parse.
57       * The version is typically not relevant, but the protocol name.
58       */
59      private final ProtocolVersion protocol;
60      private final Tokenizer tokenizer;
61  
62      /**
63       * Creates a new line parser for the given HTTP-like protocol.
64       *
65       * @param proto     a version of the protocol to parse, or
66       *                  {@code null} for HTTP. The actual version
67       *                  is not relevant, only the protocol name.
68       */
69      public BasicLineParser(final ProtocolVersion proto) {
70          this.protocol = proto != null? proto : HttpVersion.HTTP_1_1;
71          this.tokenizer = Tokenizer.INSTANCE;
72      }
73  
74      /**
75       * Creates a new line parser for HTTP.
76       */
77      public BasicLineParser() {
78          this(null);
79      }
80  
81      ProtocolVersion parseProtocolVersion(
82              final CharArrayBuffer buffer,
83              final ParserCursor cursor) throws ParseException {
84          final String protoname = this.protocol.getProtocol();
85          final int protolength  = protoname.length();
86  
87          this.tokenizer.skipWhiteSpace(buffer, cursor);
88  
89          final int pos = cursor.getPos();
90  
91          // long enough for "HTTP/1.1"?
92          if (pos + protolength + 4 > cursor.getUpperBound()) {
93              throw new ParseException("Invalid protocol version",
94                      buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
95          }
96  
97          // check the protocol name and slash
98          boolean ok = true;
99          for (int i = 0; ok && (i < protolength); i++) {
100             ok = buffer.charAt(pos + i) == protoname.charAt(i);
101         }
102         if (ok) {
103             ok = buffer.charAt(pos + protolength) == '/';
104         }
105         if (!ok) {
106             throw new ParseException("Invalid protocol version",
107                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
108         }
109 
110         cursor.updatePos(pos + protolength + 1);
111         return ProtocolVersionParser.INSTANCE.parse(protoname, HttpVersion::get, buffer, cursor, null);
112     }
113 
114     /**
115      * Parses a request line.
116      *
117      * @param buffer    a buffer holding the line to parse
118      *
119      * @return  the parsed request line
120      *
121      * @throws ParseException        in case of a parse error
122      */
123     @Override
124     public RequestLine parseRequestLine(final CharArrayBuffer buffer) throws ParseException {
125         Args.notNull(buffer, "Char array buffer");
126 
127         final ParserCursor cursor = new ParserCursor(0, buffer.length());
128         this.tokenizer.skipWhiteSpace(buffer, cursor);
129         final String method = this.tokenizer.parseToken(buffer, cursor, BLANKS);
130         if (TextUtils.isEmpty(method)) {
131             throw new ParseException("Invalid request line",
132                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
133         }
134         this.tokenizer.skipWhiteSpace(buffer, cursor);
135         final String uri = this.tokenizer.parseToken(buffer, cursor, BLANKS);
136         if (TextUtils.isEmpty(uri)) {
137             throw new ParseException("Invalid request line",
138                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
139         }
140         final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
141         this.tokenizer.skipWhiteSpace(buffer, cursor);
142         if (!cursor.atEnd()) {
143             throw new ParseException("Invalid request line",
144                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
145         }
146         return new RequestLine(method, uri, ver);
147     }
148 
149     @Override
150     public StatusLine parseStatusLine(final CharArrayBuffer buffer) throws ParseException {
151         Args.notNull(buffer, "Char array buffer");
152 
153         final ParserCursor cursor = new ParserCursor(0, buffer.length());
154         this.tokenizer.skipWhiteSpace(buffer, cursor);
155         final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
156         this.tokenizer.skipWhiteSpace(buffer, cursor);
157         int statusCode = 0;
158         for (int i = 1; i <= 3; i++) {
159             if (cursor.atEnd()) {
160                 throw new ParseException("Status line contains invalid status code",
161                         buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
162             }
163             final char ch = buffer.charAt(cursor.getPos());
164             if (ch >= '0' && ch <= '9') {
165                 statusCode = (statusCode * 10) + (ch - '0');
166             } else {
167                 throw new ParseException("Status line contains invalid status code",
168                         buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
169             }
170             cursor.updatePos(cursor.getPos() + 1);
171         }
172         if (!cursor.atEnd()) {
173             final char ch = buffer.charAt(cursor.getPos());
174             if (Tokenizer.isWhitespace(ch)) {
175                 cursor.updatePos(cursor.getPos() + 1);
176             } else {
177                 throw new ParseException("Status line contains invalid status code",
178                         buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
179             }
180         }
181         this.tokenizer.skipWhiteSpace(buffer, cursor);
182         final String text = buffer.substringTrimmed(cursor.getPos(), cursor.getUpperBound());
183         return new StatusLine(ver, statusCode, text);
184     }
185 
186     @Override
187     public Header parseHeader(final CharArrayBuffer buffer) throws ParseException {
188         Args.notNull(buffer, "Char array buffer");
189 
190         final ParserCursor cursor = new ParserCursor(0, buffer.length());
191         this.tokenizer.skipWhiteSpace(buffer, cursor);
192         final String name = this.tokenizer.parseToken(buffer, cursor, COLON);
193         if (cursor.getPos() == cursor.getLowerBound() || cursor.getPos() == cursor.getUpperBound() ||
194                 buffer.charAt(cursor.getPos()) != ':' ||
195                 TextUtils.isEmpty(name) ||
196                 Tokenizer.isWhitespace(buffer.charAt(cursor.getPos() - 1))) {
197             throw new ParseException("Invalid header",
198                     buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos());
199         }
200         final String value = buffer.substringTrimmed(cursor.getPos() + 1, cursor.getUpperBound());
201         return new BasicHeader(name, value);
202     }
203 
204 }