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.http.message;
29
30 import org.apache.http.Header;
31 import org.apache.http.HttpVersion;
32 import org.apache.http.ParseException;
33 import org.apache.http.ProtocolVersion;
34 import org.apache.http.RequestLine;
35 import org.apache.http.StatusLine;
36 import org.apache.http.annotation.ThreadingBehavior;
37 import org.apache.http.annotation.Contract;
38 import org.apache.http.protocol.HTTP;
39 import org.apache.http.util.Args;
40 import org.apache.http.util.CharArrayBuffer;
41
42 /**
43  * Basic parser for lines in the head section of an HTTP message.
44  * There are individual methods for parsing a request line, a
45  * status line, or a header line.
46  * The lines to parse are passed in memory, the parser does not depend
47  * on any specific IO mechanism.
48  * Instances of this class are stateless and thread-safe.
49  * Derived classes MUST maintain these properties.
50  *
51  * <p>
52  * Note: This class was created by refactoring parsing code located in
53  * various other classes. The author tags from those other classes have
54  * been replicated here, although the association with the parsing code
55  * taken from there has not been traced.
56  * </p>
57  *
58  * @since 4.0
59  */

60 @Contract(threading = ThreadingBehavior.IMMUTABLE)
61 public class BasicLineParser implements LineParser {
62
63     /**
64      * A default instance of this classfor use as default or fallback.
65      * Note that {@link BasicLineParser} is not a singleton, there can
66      * be many instances of the class itself and of derived classes.
67      * The instance here provides non-customized, default behavior.
68      *
69      * @deprecated (4.3) use {@link #INSTANCE}
70      */

71     @Deprecated
72     public final static BasicLineParser DEFAULT = new BasicLineParser();
73
74     public final static BasicLineParser INSTANCE = new BasicLineParser();
75
76     /**
77      * A version of the protocol to parse.
78      * The version is typically not relevant, but the protocol name.
79      */

80     protected final ProtocolVersion protocol;
81
82
83     /**
84      * Creates a new line parser for the given HTTP-like protocol.
85      *
86      * @param proto     a version of the protocol to parse, or
87      *                  {@code nullfor HTTP. The actual version
88      *                  is not relevant, only the protocol name.
89      */

90     public BasicLineParser(final ProtocolVersion proto) {
91         this.protocol = proto != null? proto : HttpVersion.HTTP_1_1;
92     }
93
94
95     /**
96      * Creates a new line parser for HTTP.
97      */

98     public BasicLineParser() {
99         this(null);
100     }
101
102
103     public static
104         ProtocolVersion parseProtocolVersion(final String value,
105                                              final LineParser parser) throws ParseException {
106         Args.notNull(value, "Value");
107
108         final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
109         buffer.append(value);
110         final ParserCursor cursor = new ParserCursor(0, value.length());
111         return (parser != null ? parser : BasicLineParser.INSTANCE)
112                 .parseProtocolVersion(buffer, cursor);
113     }
114
115
116     // non-javadoc, see interface LineParser
117     @Override
118     public ProtocolVersion parseProtocolVersion(final CharArrayBuffer buffer,
119                                                 final ParserCursor cursor) throws ParseException {
120         Args.notNull(buffer, "Char array buffer");
121         Args.notNull(cursor, "Parser cursor");
122         final String protoname = this.protocol.getProtocol();
123         final int protolength  = protoname.length();
124
125         final int indexFrom = cursor.getPos();
126         final int indexTo = cursor.getUpperBound();
127
128         skipWhitespace(buffer, cursor);
129
130         int i = cursor.getPos();
131
132         // long enough for "HTTP/1.1"?
133         if (i + protolength + 4 > indexTo) {
134             throw new ParseException
135                 ("Not a valid protocol version: " +
136                  buffer.substring(indexFrom, indexTo));
137         }
138
139         // check the protocol name and slash
140         boolean ok = true;
141         for (int j=0; ok && (j<protolength); j++) {
142             ok = (buffer.charAt(i+j) == protoname.charAt(j));
143         }
144         if (ok) {
145             ok = (buffer.charAt(i+protolength) == '/');
146         }
147         if (!ok) {
148             throw new ParseException
149                 ("Not a valid protocol version: " +
150                  buffer.substring(indexFrom, indexTo));
151         }
152
153         i += protolength+1;
154
155         final int period = buffer.indexOf('.', i, indexTo);
156         if (period == -1) {
157             throw new ParseException
158                 ("Invalid protocol version number: " +
159                  buffer.substring(indexFrom, indexTo));
160         }
161         final int major;
162         try {
163             major = Integer.parseInt(buffer.substringTrimmed(i, period));
164         } catch (final NumberFormatException e) {
165             throw new ParseException
166                 ("Invalid protocol major version number: " +
167                  buffer.substring(indexFrom, indexTo));
168         }
169         i = period + 1;
170
171         int blank = buffer.indexOf(' ', i, indexTo);
172         if (blank == -1) {
173             blank = indexTo;
174         }
175         final int minor;
176         try {
177             minor = Integer.parseInt(buffer.substringTrimmed(i, blank));
178         } catch (final NumberFormatException e) {
179             throw new ParseException(
180                 "Invalid protocol minor version number: " +
181                 buffer.substring(indexFrom, indexTo));
182         }
183
184         cursor.updatePos(blank);
185
186         return createProtocolVersion(major, minor);
187
188     } // parseProtocolVersion
189
190
191     /**
192      * Creates a protocol version.
193      * Called from {@link #parseProtocolVersion}.
194      *
195      * @param major     the major version number, for example 1 in HTTP/1.0
196      * @param minor     the minor version number, for example 0 in HTTP/1.0
197      *
198      * @return  the protocol version
199      */

200     protected ProtocolVersion createProtocolVersion(final int major, final int minor) {
201         return protocol.forVersion(major, minor);
202     }
203
204
205
206     // non-javadoc, see interface LineParser
207     @Override
208     public boolean hasProtocolVersion(final CharArrayBuffer buffer,
209                                       final ParserCursor cursor) {
210         Args.notNull(buffer, "Char array buffer");
211         Args.notNull(cursor, "Parser cursor");
212         int index = cursor.getPos();
213
214         final String protoname = this.protocol.getProtocol();
215         final int  protolength = protoname.length();
216
217         if (buffer.length() < protolength+4)
218          {
219             return false// not long enough for "HTTP/1.1"
220         }
221
222         if (index < 0) {
223             // end of line, no tolerance for trailing whitespace
224             // this works only for single-digit major and minor version
225             index = buffer.length() -4 -protolength;
226         } else if (index == 0) {
227             // beginning of line, tolerate leading whitespace
228             while ((index < buffer.length()) &&
229                     HTTP.isWhitespace(buffer.charAt(index))) {
230                  index++;
231              }
232         } // else within line, don't tolerate whitespace
233
234
235         if (index + protolength + 4 > buffer.length()) {
236             return false;
237         }
238
239
240         // just check protocol name and slash, no need to analyse the version
241         boolean ok = true;
242         for (int j=0; ok && (j<protolength); j++) {
243             ok = (buffer.charAt(index+j) == protoname.charAt(j));
244         }
245         if (ok) {
246             ok = (buffer.charAt(index+protolength) == '/');
247         }
248
249         return ok;
250     }
251
252
253
254     public static
255         RequestLine parseRequestLine(final String value,
256                                      final LineParser parser) throws ParseException {
257         Args.notNull(value, "Value");
258
259         final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
260         buffer.append(value);
261         final ParserCursor cursor = new ParserCursor(0, value.length());
262         return (parser != null ? parser : BasicLineParser.INSTANCE)
263             .parseRequestLine(buffer, cursor);
264     }
265
266
267     /**
268      * Parses a request line.
269      *
270      * @param buffer    a buffer holding the line to parse
271      *
272      * @return  the parsed request line
273      *
274      * @throws ParseException        in case of a parse error
275      */

276     @Override
277     public RequestLine parseRequestLine(final CharArrayBuffer buffer,
278                                         final ParserCursor cursor) throws ParseException {
279
280         Args.notNull(buffer, "Char array buffer");
281         Args.notNull(cursor, "Parser cursor");
282         final int indexFrom = cursor.getPos();
283         final int indexTo = cursor.getUpperBound();
284
285         try {
286             skipWhitespace(buffer, cursor);
287             int i = cursor.getPos();
288
289             int blank = buffer.indexOf(' ', i, indexTo);
290             if (blank < 0) {
291                 throw new ParseException("Invalid request line: " +
292                         buffer.substring(indexFrom, indexTo));
293             }
294             final String method = buffer.substringTrimmed(i, blank);
295             cursor.updatePos(blank);
296
297             skipWhitespace(buffer, cursor);
298             i = cursor.getPos();
299
300             blank = buffer.indexOf(' ', i, indexTo);
301             if (blank < 0) {
302                 throw new ParseException("Invalid request line: " +
303                         buffer.substring(indexFrom, indexTo));
304             }
305             final String uri = buffer.substringTrimmed(i, blank);
306             cursor.updatePos(blank);
307
308             final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
309
310             skipWhitespace(buffer, cursor);
311             if (!cursor.atEnd()) {
312                 throw new ParseException("Invalid request line: " +
313                         buffer.substring(indexFrom, indexTo));
314             }
315
316             return createRequestLine(method, uri, ver);
317         } catch (final IndexOutOfBoundsException e) {
318             throw new ParseException("Invalid request line: " +
319                                      buffer.substring(indexFrom, indexTo));
320         }
321     } // parseRequestLine
322
323
324     /**
325      * Instantiates a new request line.
326      * Called from {@link #parseRequestLine}.
327      *
328      * @param method    the request method
329      * @param uri       the requested URI
330      * @param ver       the protocol version
331      *
332      * @return  a new status line with the given data
333      */

334     protected RequestLine createRequestLine(final String method,
335                                             final String uri,
336                                             final ProtocolVersion ver) {
337         return new BasicRequestLine(method, uri, ver);
338     }
339
340
341
342     public static
343         StatusLine parseStatusLine(final String value,
344                                    final LineParser parser) throws ParseException {
345         Args.notNull(value, "Value");
346
347         final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
348         buffer.append(value);
349         final ParserCursor cursor = new ParserCursor(0, value.length());
350         return (parser != null ? parser : BasicLineParser.INSTANCE)
351                 .parseStatusLine(buffer, cursor);
352     }
353
354
355     // non-javadoc, see interface LineParser
356     @Override
357     public StatusLine parseStatusLine(final CharArrayBuffer buffer,
358                                       final ParserCursor cursor) throws ParseException {
359         Args.notNull(buffer, "Char array buffer");
360         Args.notNull(cursor, "Parser cursor");
361         final int indexFrom = cursor.getPos();
362         final int indexTo = cursor.getUpperBound();
363
364         try {
365             // handle the HTTP-Version
366             final ProtocolVersion ver = parseProtocolVersion(buffer, cursor);
367
368             // handle the Status-Code
369             skipWhitespace(buffer, cursor);
370             int i = cursor.getPos();
371
372             int blank = buffer.indexOf(' ', i, indexTo);
373             if (blank < 0) {
374                 blank = indexTo;
375             }
376             final int statusCode;
377             final String s = buffer.substringTrimmed(i, blank);
378             for (int j = 0; j < s.length(); j++) {
379                 if (!Character.isDigit(s.charAt(j))) {
380                     throw new ParseException(
381                             "Status line contains invalid status code: "
382                             + buffer.substring(indexFrom, indexTo));
383                 }
384             }
385             try {
386                 statusCode = Integer.parseInt(s);
387             } catch (final NumberFormatException e) {
388                 throw new ParseException(
389                         "Status line contains invalid status code: "
390                         + buffer.substring(indexFrom, indexTo));
391             }
392             //handle the Reason-Phrase
393             i = blank;
394             final String reasonPhrase;
395             if (i < indexTo) {
396                 reasonPhrase = buffer.substringTrimmed(i, indexTo);
397             } else {
398                 reasonPhrase = "";
399             }
400             return createStatusLine(ver, statusCode, reasonPhrase);
401
402         } catch (final IndexOutOfBoundsException e) {
403             throw new ParseException("Invalid status line: " +
404                                      buffer.substring(indexFrom, indexTo));
405         }
406     } // parseStatusLine
407
408
409     /**
410      * Instantiates a new status line.
411      * Called from {@link #parseStatusLine}.
412      *
413      * @param ver       the protocol version
414      * @param status    the status code
415      * @param reason    the reason phrase
416      *
417      * @return  a new status line with the given data
418      */

419     protected StatusLine createStatusLine(final ProtocolVersion ver,
420                                           final int status,
421                                           final String reason) {
422         return new BasicStatusLine(ver, status, reason);
423     }
424
425
426
427     public static
428         Header parseHeader(final String value,
429                            final LineParser parser) throws ParseException {
430         Args.notNull(value, "Value");
431
432         final CharArrayBuffer buffer = new CharArrayBuffer(value.length());
433         buffer.append(value);
434         return (parser != null ? parser : BasicLineParser.INSTANCE)
435                 .parseHeader(buffer);
436     }
437
438
439     // non-javadoc, see interface LineParser
440     @Override
441     public Header parseHeader(final CharArrayBuffer buffer)
442         throws ParseException {
443
444         // the actual parser code is in the constructor of BufferedHeader
445         return new BufferedHeader(buffer);
446     }
447
448
449     /**
450      * Helper to skip whitespace.
451      */

452     protected void skipWhitespace(final CharArrayBuffer buffer, final ParserCursor cursor) {
453         int pos = cursor.getPos();
454         final int indexTo = cursor.getUpperBound();
455         while ((pos < indexTo) &&
456                HTTP.isWhitespace(buffer.charAt(pos))) {
457             pos++;
458         }
459         cursor.updatePos(pos);
460     }
461
462// class BasicLineParser
463