001: /*
002: * $Id: HTTPSession.java 6825 2006-04-04 03:00:44Z dfs $
003: *
004: * Copyright 2001,2006 Daniel F. Savarese
005: *
006: * Licensed under the Apache License, Version 2.0 (the "License");
007: * you may not use this file except in compliance with the License.
008: * You may obtain a copy of the License at
009: *
010: * http://www.savarese.org/software/ApacheLicense-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS,
014: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015: * See the License for the specific language governing permissions and
016: * limitations under the License.
017: */
018:
019: package org.savarese.barehttp;
020:
021: import java.io.*;
022: import java.net.*;
023: import java.text.*;
024: import java.util.*;
025:
026: import org.apache.oro.text.regex.*;
027:
028: /**
029: * An HTTPSession executes an HTTP conversation via an InputStream and
030: * OutputSream. It processes only one request and then terminates the
031: * session. Only HTTP/0.9, 1.0, and 1.1 GET requests and HTTP/1.0 and
032: * 1.1 HEAD requests are supported. The canonical pathname of served
033: * files is checked against the document root. If the file path does
034: * not fall under the document root, an HTTP "403 Forbidden Resource"
035: * error is returned.
036: *
037: * @author <a href="http://www.savarese.org/">Daniel F. Savarese</a>
038: */
039: public class HTTPSession {
040:
041: final class Request {
042: static final int UNKNOWN_REQUEST = -1;
043: static final int SIMPLE_REQUEST = 0;
044: static final int GET_REQUEST = 1;
045: static final int HEAD_REQUEST = 2;
046: static final int HEADER = 3;
047: static final int HEADER_CONTINUATION = 4;
048: static final int END_OF_REQUEST = 5;
049: static final String DEFAULT_VERSION = "1.0";
050:
051: int type;
052: HashMap<String, String> headers;
053: String version;
054: String uri;
055:
056: Request() {
057: headers = new HashMap<String, String>();
058: reset();
059: }
060:
061: void setVersion(String version) {
062: this .version = version;
063: }
064:
065: // unused
066: // String getVersion() { return version; }
067:
068: void setType(int type) {
069: this .type = type;
070: }
071:
072: int getType() {
073: return type;
074: }
075:
076: void setHeaderValue(String field, String value) {
077: headers.put(field, value);
078: }
079:
080: String getHeaderValue(String field) {
081: return headers.get(field);
082: }
083:
084: void setURI(String uri) throws IOException {
085: this .uri = uri;
086: }
087:
088: String getURI() {
089: return uri;
090: }
091:
092: void reset() {
093: headers.clear();
094: type = UNKNOWN_REQUEST;
095: version = DEFAULT_VERSION;
096: uri = null;
097: }
098: }
099:
100: static final String URL_PATTERN_STRING = "([^ ?]*)(?:\\?[^ ]*)?";
101:
102: static final String[] PATTERN_STRING = {
103: "^GET " + URL_PATTERN_STRING + "$",
104: "^GET " + URL_PATTERN_STRING + " HTTP/(\\d+\\.\\d+)$",
105: "^HEAD " + URL_PATTERN_STRING + " HTTP/(\\d+\\.\\d+)$",
106: "^(\\S+): (.*)$", "^ (.*)$", "^$" };
107:
108: static final String HTTP_VERSION = "HTTP/1.0";
109: static final String DEFAULT_HEADERS = "Server: BareHTTP 1.0.0 (Java)\r\n"
110: + "Allow: GET, HEAD\r\n" + "Connection: close\r\n";
111:
112: static final String OK_STATUS = "200 OK";
113: static final String FORBIDDEN_STATUS = "403 Forbidden Resource";
114: static final String NOT_FOUND_STATUS = "404 Resource Not Found";
115: static final String NOT_IMPLEMENTED_STATUS = "501 Not Implemented";
116:
117: static final Pattern[] PATTERN;
118: static final String DATE_FORMAT = "EEE, d MMM yyyy hh:mm:ss z";
119:
120: BufferedReader input;
121: OutputStreamWriter output;
122: Perl5Matcher matcher;
123: Request request;
124: SimpleDateFormat dateFormat;
125: String documentRoot;
126:
127: static {
128: Perl5Compiler compiler = new Perl5Compiler();
129:
130: PATTERN = new Perl5Pattern[PATTERN_STRING.length];
131:
132: try {
133: for (int i = 0; i < PATTERN.length; ++i)
134: PATTERN[i] = compiler.compile(PATTERN_STRING[i],
135: Perl5Compiler.READ_ONLY_MASK);
136: } catch (MalformedPatternException e) {
137: // This should happen only during development.
138: throw new RuntimeException(e);
139: }
140: }
141:
142: boolean parseRequest() throws IOException {
143: String line;
144: int pattern;
145: MatchResult match;
146: String lastHeader = null;
147:
148: request.reset();
149:
150: loop: while (true) {
151: line = input.readLine();
152:
153: if (line == null)
154: break;
155:
156: for (pattern = 0; pattern < PATTERN.length; ++pattern)
157: if (matcher.matches(line, PATTERN[pattern]))
158: break;
159:
160: match = matcher.getMatch();
161:
162: switch (pattern) {
163: case Request.SIMPLE_REQUEST:
164: request.setType(pattern);
165: request.setURI(documentRoot + match.group(1));
166: request.setVersion("0.9");
167: break loop;
168: case Request.GET_REQUEST:
169: case Request.HEAD_REQUEST:
170: request.setType(pattern);
171: request.setURI(documentRoot + match.group(1));
172: request.setVersion(match.group(2));
173: break;
174: case Request.HEADER:
175: lastHeader = match.group(1);
176: request.setHeaderValue(lastHeader, match.group(2));
177: break;
178: case Request.HEADER_CONTINUATION:
179: request.setHeaderValue(lastHeader, request
180: .getHeaderValue(lastHeader)
181: + match.group(1));
182: break;
183: case Request.END_OF_REQUEST:
184: break loop;
185: default:
186: reportError(NOT_IMPLEMENTED_STATUS);
187: return false;
188: }
189: }
190:
191: return true;
192: }
193:
194: void processRequest() throws IOException {
195: int type = request.getType();
196:
197: switch (type) {
198: case Request.SIMPLE_REQUEST:
199: case Request.GET_REQUEST:
200: case Request.HEAD_REQUEST:
201: File file = new File(request.getURI());
202:
203: if (file.isDirectory())
204: file = new File(file, "index.html");
205:
206: if (!file.exists()) {
207: reportError(NOT_FOUND_STATUS);
208: return;
209: }
210:
211: if (!validateFile(file)) {
212: reportError(FORBIDDEN_STATUS);
213: return;
214: }
215:
216: if (type != Request.SIMPLE_REQUEST)
217: output.write(HTTP_VERSION + " " + OK_STATUS
218: + "\r\nDate: " + getDateHeader() + "\r\n"
219: + DEFAULT_HEADERS + "Last-Modified: "
220: + getDateHeader(file.lastModified()) + "\r\n"
221: + "Content-Length: " + file.length() + "\r\n"
222: + "Content-Type: " + getContentType(file)
223: + "\r\n\r\n");
224:
225: if (type == Request.HEAD_REQUEST)
226: return;
227:
228: FileReader input = new FileReader(file);
229: char[] buffer = new char[1024];
230: int chars;
231:
232: while ((chars = input.read(buffer, 0, buffer.length)) != -1)
233: output.write(buffer, 0, chars);
234: input.close();
235: break;
236: default:
237: reportError(NOT_IMPLEMENTED_STATUS);
238: return;
239: }
240: }
241:
242: String getContentType(File file) throws IOException {
243: String path = file.getCanonicalPath();
244:
245: if (path.endsWith(".html"))
246: return "text/html";
247: else if (path.endsWith(".txt"))
248: return "text/plain";
249: else
250: return "application/octet-stream";
251: }
252:
253: /**
254: * Makes a feeble attempt to confine the file to a tree
255: * rooted at the server's document directory.
256: */
257: boolean validateFile(File file) throws IOException {
258: return (file.getCanonicalPath().startsWith(documentRoot));
259: }
260:
261: void closeSession() throws IOException {
262: output.flush();
263: output.close();
264: input.close();
265: }
266:
267: String getDateHeader(long time) {
268: return dateFormat.format(new Date(time));
269: }
270:
271: String getDateHeader() {
272: return getDateHeader(System.currentTimeMillis());
273: }
274:
275: void reportError(String statusLine) throws IOException {
276: output
277: .write(HTTP_VERSION
278: + " "
279: + statusLine
280: + "\r\nDate: "
281: + getDateHeader()
282: + "\r\n"
283: + DEFAULT_HEADERS
284: + "\r\n"
285: + "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n"
286: + "<HTML><HEAD>\r\n" + "<TITLE>" + statusLine
287: + "</TITLE>\r\n" + "</HEAD><BODY>\r\n" + "<H1>"
288: + statusLine + "</H1>\r\n"
289: + "</BODY></HTML>\r\n");
290: }
291:
292: /**
293: * Creates an HTTPSession rooted at the specified document directory
294: * and communicating via the specified streams.
295: *
296: * @param documentRoot The fully qualified directory pathname to
297: * serve as the document root.
298: * @param in The InputStream via which the client submits its request.
299: * @param out The OutputStram via which the HTTPSession sends its reply.
300: */
301: public HTTPSession(String documentRoot, InputStream in,
302: OutputStream out) throws IOException {
303: input = new BufferedReader(new InputStreamReader(in));
304: output = new OutputStreamWriter(out);
305: matcher = new Perl5Matcher();
306: request = new Request();
307: dateFormat = new SimpleDateFormat(DATE_FORMAT);
308: dateFormat.setTimeZone(new SimpleTimeZone(0, "GMT"));
309: this .documentRoot = documentRoot;
310: }
311:
312: /**
313: * Executes the HTTP conversation and closes the session after
314: * satisfying the first request.
315: */
316: public void execute() throws IOException {
317: if (parseRequest())
318: processRequest();
319: closeSession();
320: }
321:
322: }
|