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>
69 * <LI>
70 * If <code>isValid()</code> returns false, send an HTTP response message indicating
71 * the error.
72 * <BR>
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 }