View Javadoc
1   //******************************************************************************
2   //
3   // File:    HttpRequest.java
4   // Package: edu.rit.http
5   // Unit:    Class edu.rit.http.HttpRequest
6   //
7   // This Java source file is copyright (C) 2006 by Alan Kaminsky. All rights
8   // reserved. For further information, contact the author, Alan Kaminsky, at
9   // ark@cs.rit.edu.
10  //
11  // This Java source file is part of the Parallel Java Library ("PJ"). PJ is free
12  // software; you can redistribute it and/or modify it under the terms of the GNU
13  // General Public License as published by the Free Software Foundation; either
14  // version 3 of the License, or (at your option) any later version.
15  //
16  // PJ is distributed in the hope that it will be useful, but WITHOUT ANY
17  // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
18  // A PARTICULAR PURPOSE. See the GNU General Public License for more details.
19  //
20  // Linking this library statically or dynamically with other modules is making a
21  // combined work based on this library. Thus, the terms and conditions of the GNU
22  // General Public License cover the whole combination.
23  //
24  // As a special exception, the copyright holders of this library give you
25  // permission to link this library with independent modules to produce an
26  // executable, regardless of the license terms of these independent modules, and
27  // to copy and distribute the resulting executable under terms of your choice,
28  // provided that you also meet, for each linked independent module, the terms
29  // and conditions of the license of that module. An independent module is a module
30  // which is not derived from or based on this library. If you modify this library,
31  // you may extend this exception to your version of the library, but you are not
32  // obligated to do so. If you do not wish to do so, delete this exception
33  // statement from your version.
34  //
35  // A copy of the GNU General Public License is provided in the file gpl.txt. You
36  // may also obtain a copy of the GNU General Public License on the World Wide
37  // Web at http://www.gnu.org/licenses/gpl.html.
38  //
39  //******************************************************************************
40  package edu.rit.http;
41  
42  import java.io.IOException;
43  import java.net.Socket;
44  import java.util.Collection;
45  import java.util.Collections;
46  import java.util.HashMap;
47  import java.util.Map;
48  import java.util.Scanner;
49  
50  // For unit test main program
51  // import java.io.PrintWriter;
52  // import java.net.InetSocketAddress;
53  // import java.net.ServerSocket;
54  // import java.nio.charset.Charset;
55  /**
56   * Class HttpRequest encapsulates an HTTP request received from a web browser.
57   * <P>
58   * HTTP/1.0 and HTTP/1.1 requests are supported. The obsolete HTTP/0.9 requests
59   * are <I>not</I> supported.
60   * <P>
61   * This class provides methods for examining the request line and the headers.
62   * This class does <I>not</I> support reading the entity body if any.
63   * <P>
64   * To receive an HTTP request message:
65   * <OL TYPE=1>
66   * <LI>
67   * Call the <code>isValid()</code> method.
68   * <BR>&nbsp;
69   * <LI>
70   * If <code>isValid()</code> returns false, send an HTTP response message indicating
71   * the error.
72   * <BR>&nbsp;
73   * <LI>
74   * If <code>isValid()</code> returns true, call the other methods to examine the
75   * contents of the HTTP request message, and send an appropriate HTTP response
76   * message.
77   * </OL>
78   *
79   * @author Alan Kaminsky
80   * @version 29-Jul-2010
81   */
82  public class HttpRequest {
83  
84  // Exported constants.
85      /**
86       * The GET method string, <code>"GET"</code>.
87       */
88      public static final String GET_METHOD = "GET";
89  
90      /**
91       * The HEAD method string, <code>"HEAD"</code>.
92       */
93      public static final String HEAD_METHOD = "HEAD";
94  
95      /**
96       * The POST method string, <code>"POST"</code>.
97       */
98      public static final String POST_METHOD = "POST";
99  
100     /**
101      * The HTTP/1.0 version string <code>"HTTP/1.0"</code>.
102      */
103     public static final String HTTP_1_0_VERSION = "HTTP/1.0";
104 
105     /**
106      * The HTTP/1.1 version string, <code>"HTTP/1.1"</code>.
107      */
108     public static final String HTTP_1_1_VERSION = "HTTP/1.1";
109 
110 // Hidden data members.
111     private Socket mySocket;
112 
113     private String myMethod;
114     private String myUri;
115     private String myHttpVersion;
116 
117     private Map<String, String> myHeaderMap
118             = new HashMap<String, String>();
119     private Map<String, String> myUnmodifiableHeaderMap
120             = Collections.unmodifiableMap(myHeaderMap);
121 
122     private boolean iamValid;
123 
124 // Exported constructors.
125     /**
126      * Construct a new HTTP request. The request is read from the input stream
127      * of the given socket.
128      *
129      * @param theSocket Socket.
130      * @exception NullPointerException (unchecked exception) Thrown if
131      * <code>theSocket</code> is null.
132      */
133     public HttpRequest(Socket theSocket) {
134         if (theSocket == null) {
135             throw new NullPointerException("HttpRequest(): theSocket is null");
136         }
137         mySocket = theSocket;
138     }
139 
140 // Exported operations.
141     /**
142      * Determine if this HTTP request is valid. If the data read from the input
143      * stream of the socket given to the constructor represents a valid HTTP
144      * request message, true is returned, otherwise false is returned. If an I/O
145      * exception is thrown while reading the input, this HTTP request is marked
146      * as invalid, but the I/O exception is not propagated to the caller.
147      *
148      * @return True if this HTTP request is valid, false otherwise.
149      */
150     public boolean isValid() {
151         parse();
152         return iamValid;
153     }
154 
155     /**
156      * Obtain this HTTP request's method.
157      *
158      * @return Method string, e.g. <code>"GET"</code>, <code>"POST"</code>.
159      * @exception IllegalStateException (unchecked exception) Thrown if this
160      * HTTP request is invalid.
161      */
162     public String getMethod() {
163         if (!isValid()) {
164             throw new IllegalStateException("HTTP request is invalid");
165         }
166         return myMethod;
167     }
168 
169     /**
170      * Obtain this HTTP request's URI.
171      *
172      * @return URI string.
173      * @exception IllegalStateException (unchecked exception) Thrown if this
174      * HTTP request is invalid.
175      */
176     public String getUri() {
177         if (!isValid()) {
178             throw new IllegalStateException("HTTP request is invalid");
179         }
180         return myUri;
181     }
182 
183     /**
184      * Obtain this HTTP request's version.
185      *
186      * @return HTTP version string, e.g. <code>"HTTP/1.0"</code>,
187      * <code>"HTTP/1.1"</code>.
188      * @exception IllegalStateException (unchecked exception) Thrown if this
189      * HTTP request is invalid.
190      */
191     public String getHttpVersion() {
192         if (!isValid()) {
193             throw new IllegalStateException("HTTP request is invalid");
194         }
195         return myHttpVersion;
196     }
197 
198     /**
199      * Obtain the value of the given header in this HTTP request.
200      *
201      * @param theHeaderName Header name.
202      * @return Header value, or null if there is no header for
203      * <code>theHeaderName</code>.
204      * @exception IllegalStateException (unchecked exception) Thrown if this
205      * HTTP request is invalid.
206      */
207     public String getHeader(String theHeaderName) {
208         if (!isValid()) {
209             throw new IllegalStateException("HTTP request is invalid");
210         }
211         return myHeaderMap.get(theHeaderName);
212     }
213 
214     /**
215      * Obtain a collection of all the headers in this HTTP request. The returned
216      * object is an unmodifiable collection of zero or more map entries. Each
217      * map entry's key is the header name. Each map entry's value is the
218      * corresponding header value.
219      *
220      * @return Unmodifiable collection of header name-value mappings.
221      * @exception IllegalStateException (unchecked exception) Thrown if this
222      * HTTP request is invalid.
223      */
224     public Collection<Map.Entry<String, String>> getHeaders() {
225         if (!isValid()) {
226             throw new IllegalStateException("HTTP request is invalid");
227         }
228         return myUnmodifiableHeaderMap.entrySet();
229     }
230 
231 // Hidden operations.
232     /**
233      * Parse the input data read from this HTTP request's socket.
234      */
235     private void parse() {
236         // Early return if already parsed.
237         if (myMethod != null) {
238             return;
239         }
240 
241         // Assume the request is invalid.
242         iamValid = false;
243         myMethod = "";
244         myUri = "";
245         myHttpVersion = "";
246 
247         try {
248             // Set up to scan lines from the socket input stream.
249             Scanner scanner = new Scanner(mySocket.getInputStream());
250 
251             // Read the first line. If none, invalid.
252             if (!scanner.hasNextLine()) {
253                 return;
254             }
255             String line = scanner.nextLine();
256 
257             // Parse the first line.
258             Scanner linescanner = new Scanner(line);
259             if (!linescanner.hasNext()) {
260                 return;
261             }
262             String method = linescanner.next();
263             if (!linescanner.hasNext()) {
264                 return;
265             }
266             String uri = linescanner.next();
267             if (!linescanner.hasNext()) {
268                 return;
269             }
270             String httpVersion = linescanner.next();
271             if (linescanner.hasNext()) {
272                 return;
273             }
274 
275             // Read remaining lines if any until an empty line.
276             String headerName = null;
277             StringBuilder headerValue = new StringBuilder();
278             for (;;) {
279                 if (!scanner.hasNextLine()) {
280                     return;
281                 }
282                 line = scanner.nextLine();
283                 if (line.isEmpty()) {
284                     break;
285                 }
286 
287                 // Check whether line is starting or continuing a header.
288                 if (Character.isWhitespace(line.charAt(0))) {
289                     // Continuing previous header.
290                     if (headerName == null) {
291                         return;
292                     }
293                     headerValue.append(line);
294                 } else {
295                     // Starting new header. Record previous header if any.
296                     if (headerName != null) {
297                         myHeaderMap.put(headerName, headerValue.toString());
298                         headerName = null;
299                         headerValue = new StringBuilder();
300                     }
301 
302                     // Parse header name and value.
303                     int i = line.indexOf(':');
304                     if (i <= 0) {
305                         return;
306                     }
307                     if (i >= line.length() - 1) {
308                         return;
309                     }
310                     if (!Character.isWhitespace(line.charAt(i + 1))) {
311                         return;
312                     }
313                     headerName = line.substring(0, i);
314                     headerValue.append(line.substring(i + 2));
315                 }
316             }
317 
318             // If we get here, all is well. Record final header if any.
319             if (headerName != null) {
320                 myHeaderMap.put(headerName, headerValue.toString());
321             }
322 
323             // Record method, URI, and HTTP version.
324             myMethod = method;
325             myUri = uri;
326             myHttpVersion = httpVersion;
327 
328             // Mark it valid.
329             iamValid = true;
330         } catch (IOException exc) {
331             // Leave it marked invalid.
332         }
333     }
334 
335 // Unit test main program.
336 //	/**
337 //	 * Unit test main program. The program listens for connections to
338 //	 * localhost:8080. The program reads each HTTP request from a web browser
339 //	 * and merely echoes the request data back to the browser.
340 //	 * <P>
341 //	 * Usage: java edu.rit.http.HttpRequest
342 //	 */
343 //	public static void main
344 //		(String[] args)
345 //		throws Exception
346 //		{
347 //		ServerSocket serversocket = new ServerSocket();
348 //		serversocket.bind (new InetSocketAddress ("localhost", 8080));
349 //		for (;;)
350 //			{
351 //			Socket socket = serversocket.accept();
352 //			HttpRequest request = new HttpRequest (socket);
353 //			PrintWriter out = new PrintWriter (socket.getOutputStream());
354 //			out.print ("HTTP/1.0 200 OK\r\n");
355 //			out.print ("Content-Type: text/plain; charset=");
356 //			out.print (Charset.defaultCharset() + "\r\n");
357 //			out.print ("\r\n");
358 //			if (request.isValid())
359 //				{
360 //				out.print ("Method = \"" + request.getMethod() + "\"\r\n");
361 //				out.print ("URI = \"" + request.getUri() + "\"\r\n");
362 //				out.print ("Version = \"" + request.getHttpVersion() + "\"\r\n");
363 //				for (Map.Entry<String,String> entry : request.getHeaders())
364 //					{
365 //					out.print ("Header name = \"" + entry.getKey());
366 //					out.print ("\", value = \"" + entry.getValue() + "\"\r\n");
367 //					}
368 //				}
369 //			else
370 //				{
371 //				out.print ("Invalid request\r\n");
372 //				}
373 //			out.close();
374 //			socket.close();
375 //			}
376 //		}
377 }