1 | /* Copyright 2001,2006,2010,2012 Daniel F. Savarese |
2 | * |
3 | * Licensed under the Apache License, Version 2.0 (the "License"); |
4 | * you may not use this file except in compliance with the License. |
5 | * You may obtain a copy of the License at |
6 | * |
7 | * https://www.savarese.org/software/ApacheLicense-2.0 |
8 | * |
9 | * Unless required by applicable law or agreed to in writing, software |
10 | * distributed under the License is distributed on an "AS IS" BASIS, |
11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | * See the License for the specific language governing permissions and |
13 | * limitations under the License. |
14 | */ |
15 | |
16 | package org.savarese.barehttp; |
17 | |
18 | import java.io.*; |
19 | import java.net.*; |
20 | import java.text.*; |
21 | import java.util.*; |
22 | |
23 | import org.apache.oro.text.regex.*; |
24 | |
25 | /** |
26 | * An HTTPSession executes an HTTP conversation via an InputStream and |
27 | * OutputSream. It processes only one request and then terminates the |
28 | * session. Only HTTP/0.9, 1.0, and 1.1 GET requests and HTTP/1.0 and |
29 | * 1.1 HEAD requests are supported. The canonical pathname of served |
30 | * files is checked against the document root. If the file path does |
31 | * not fall under the document root, an HTTP "403 Forbidden Resource" |
32 | * error is returned. |
33 | * |
34 | * @author <a href="https://www.savarese.org/">Daniel F. Savarese</a> |
35 | */ |
36 | public class HTTPSession { |
37 | |
38 | final class Request { |
39 | static final int UNKNOWN_REQUEST = -1; |
40 | static final int SIMPLE_REQUEST = 0; |
41 | static final int GET_REQUEST = 1; |
42 | static final int HEAD_REQUEST = 2; |
43 | static final int HEADER = 3; |
44 | static final int HEADER_CONTINUATION = 4; |
45 | static final int END_OF_REQUEST = 5; |
46 | static final String DEFAULT_VERSION = "1.0"; |
47 | |
48 | int type; |
49 | HashMap<String,String> headers; |
50 | String version; |
51 | String uri; |
52 | |
53 | Request() { |
54 | headers = new HashMap<String,String>(); |
55 | reset(); |
56 | } |
57 | |
58 | void setVersion(String version) { |
59 | this.version = version; |
60 | } |
61 | |
62 | // unused |
63 | // String getVersion() { return version; } |
64 | |
65 | |
66 | void setType(int type) { |
67 | this.type = type; |
68 | } |
69 | |
70 | int getType() { return type; } |
71 | |
72 | void setHeaderValue(String field, String value) { |
73 | headers.put(field, value); |
74 | } |
75 | |
76 | String getHeaderValue(String field) { |
77 | return headers.get(field); |
78 | } |
79 | |
80 | void setURI(String uri) throws IOException { |
81 | this.uri = uri; |
82 | } |
83 | |
84 | String getURI() { return uri; } |
85 | |
86 | void reset() { |
87 | headers.clear(); |
88 | type = UNKNOWN_REQUEST; |
89 | version = DEFAULT_VERSION; |
90 | uri = null; |
91 | } |
92 | } |
93 | |
94 | static final String URL_PATTERN_STRING = "([^ ?]*)(?:\\?[^ ]*)?"; |
95 | |
96 | static final String[] PATTERN_STRING = { |
97 | "^GET " + URL_PATTERN_STRING + "$", |
98 | "^GET " + URL_PATTERN_STRING + " HTTP/(\\d+\\.\\d+)$", |
99 | "^HEAD " + URL_PATTERN_STRING + " HTTP/(\\d+\\.\\d+)$", |
100 | "^(\\S+): (.*)$", |
101 | "^ (.*)$", |
102 | "^$" |
103 | }; |
104 | |
105 | static final String HTTP_VERSION = "HTTP/1.0"; |
106 | static final String DEFAULT_HEADERS = |
107 | "Server: BareHTTP @version@ (Java)\r\n" + |
108 | "Allow: GET, HEAD\r\n" + |
109 | "Connection: close\r\n"; |
110 | |
111 | static final String OK_STATUS = "200 OK"; |
112 | static final String FORBIDDEN_STATUS = "403 Forbidden Resource"; |
113 | static final String NOT_FOUND_STATUS = "404 Resource Not Found"; |
114 | static final String NOT_IMPLEMENTED_STATUS = "501 Not Implemented"; |
115 | |
116 | static final Pattern[] PATTERN; |
117 | static final String DATE_FORMAT = "EEE, d MMM yyyy hh:mm:ss z"; |
118 | static final Properties MimeTypes = new Properties(); |
119 | |
120 | BufferedReader input; |
121 | OutputStream 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] = |
135 | compiler.compile(PATTERN_STRING[i], |
136 | Perl5Compiler.READ_ONLY_MASK); |
137 | } |
138 | } catch(MalformedPatternException e) { |
139 | // This should happen only during development. |
140 | throw new RuntimeException(e); |
141 | } |
142 | |
143 | try { |
144 | MimeTypes.load(HTTPSession.class.getResourceAsStream("mime.properties")); |
145 | } catch(IOException ioe) { |
146 | throw new RuntimeException(ioe); |
147 | } |
148 | } |
149 | |
150 | boolean parseRequest() throws IOException { |
151 | String line; |
152 | int pattern; |
153 | MatchResult match; |
154 | String lastHeader = null; |
155 | |
156 | request.reset(); |
157 | |
158 | loop: |
159 | while(true) { |
160 | line = input.readLine(); |
161 | |
162 | if(line == null) { |
163 | break; |
164 | } |
165 | |
166 | |
167 | for(pattern = 0; pattern < PATTERN.length; ++pattern) { |
168 | if(matcher.matches(line, PATTERN[pattern])) { |
169 | break; |
170 | } |
171 | } |
172 | |
173 | match = matcher.getMatch(); |
174 | |
175 | switch(pattern) { |
176 | case Request.SIMPLE_REQUEST: |
177 | request.setType(pattern); |
178 | request.setURI(documentRoot + match.group(1)); |
179 | request.setVersion("0.9"); |
180 | break loop; |
181 | case Request.GET_REQUEST: |
182 | case Request.HEAD_REQUEST: |
183 | request.setType(pattern); |
184 | request.setURI(documentRoot + match.group(1)); |
185 | request.setVersion(match.group(2)); |
186 | break; |
187 | case Request.HEADER: |
188 | lastHeader = match.group(1); |
189 | request.setHeaderValue(lastHeader, match.group(2)); |
190 | break; |
191 | case Request.HEADER_CONTINUATION: |
192 | request.setHeaderValue(lastHeader, |
193 | request.getHeaderValue(lastHeader) + |
194 | match.group(1)); |
195 | break; |
196 | case Request.END_OF_REQUEST: |
197 | break loop; |
198 | default: |
199 | reportError(NOT_IMPLEMENTED_STATUS); |
200 | return false; |
201 | } |
202 | } |
203 | |
204 | return true; |
205 | } |
206 | |
207 | void processRequest() throws IOException { |
208 | int type = request.getType(); |
209 | |
210 | switch(type) { |
211 | case Request.SIMPLE_REQUEST: |
212 | case Request.GET_REQUEST: |
213 | case Request.HEAD_REQUEST: |
214 | File file = new File(request.getURI()); |
215 | |
216 | if(file.isDirectory()) { |
217 | file = new File(file, "index.html"); |
218 | } |
219 | |
220 | if(!file.exists()) { |
221 | reportError(NOT_FOUND_STATUS); |
222 | return; |
223 | } |
224 | |
225 | if(!validateFile(file)) { |
226 | reportError(FORBIDDEN_STATUS); |
227 | return; |
228 | } |
229 | |
230 | if(type != Request.SIMPLE_REQUEST) { |
231 | output.write((HTTP_VERSION + " " + OK_STATUS + "\r\nDate: " + |
232 | getDateHeader() + "\r\n" + |
233 | DEFAULT_HEADERS + "Last-Modified: " + |
234 | getDateHeader(file.lastModified()) + "\r\n" + |
235 | "Content-Length: " + file.length() + "\r\n" + |
236 | "Content-Type: " + getContentType(file) + |
237 | "\r\n\r\n").getBytes()); |
238 | } |
239 | |
240 | if(type == Request.HEAD_REQUEST) { |
241 | return; |
242 | } |
243 | |
244 | FileInputStream input = new FileInputStream(file); |
245 | byte[] buffer = new byte[1024]; |
246 | int bytes; |
247 | |
248 | while((bytes = input.read(buffer, 0, buffer.length)) != -1) { |
249 | output.write(buffer, 0, bytes); |
250 | } |
251 | input.close(); |
252 | break; |
253 | default: |
254 | reportError(NOT_IMPLEMENTED_STATUS); |
255 | return; |
256 | } |
257 | } |
258 | |
259 | String getContentType(File file) throws IOException { |
260 | String path = file.getCanonicalPath(); |
261 | int index = path.lastIndexOf('.') + 1; |
262 | String type = MimeTypes.getProperty(path.substring(index)); |
263 | |
264 | if(type == null) { |
265 | type = "application/octet-stream"; |
266 | } |
267 | |
268 | return type; |
269 | } |
270 | |
271 | /** |
272 | * Makes a feeble attempt to confine the file to a tree |
273 | * rooted at the server's document directory. |
274 | */ |
275 | boolean validateFile(File file) throws IOException { |
276 | return (file.getCanonicalPath().startsWith(documentRoot)); |
277 | //return (file.getPath().startsWith(documentRoot)); |
278 | } |
279 | |
280 | void closeSession() throws IOException { |
281 | output.flush(); |
282 | output.close(); |
283 | input.close(); |
284 | } |
285 | |
286 | |
287 | String getDateHeader(long time) { |
288 | return dateFormat.format(new Date(time)); |
289 | } |
290 | |
291 | String getDateHeader() { |
292 | return getDateHeader(System.currentTimeMillis()); |
293 | } |
294 | |
295 | void reportError(String statusLine) throws IOException { |
296 | output.write((HTTP_VERSION + " " + statusLine + |
297 | "\r\nDate: " + getDateHeader() + "\r\n" + |
298 | DEFAULT_HEADERS + "\r\n" + |
299 | "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n" + |
300 | "<HTML><HEAD>\r\n" + |
301 | "<TITLE>" + statusLine + "</TITLE>\r\n" + |
302 | "</HEAD><BODY>\r\n" + |
303 | "<H1>" + statusLine + "</H1>\r\n" + |
304 | "</BODY></HTML>\r\n").getBytes()); |
305 | } |
306 | |
307 | /** |
308 | * Creates an HTTPSession rooted at the specified document directory |
309 | * and communicating via the specified streams. |
310 | * |
311 | * @param documentRoot The fully qualified directory pathname to |
312 | * serve as the document root. |
313 | * @param in The InputStream via which the client submits its request. |
314 | * @param out The OutputStram via which the HTTPSession sends its reply. |
315 | */ |
316 | public HTTPSession(String documentRoot, InputStream in, OutputStream out) |
317 | throws IOException |
318 | { |
319 | input = new BufferedReader(new InputStreamReader(in, "ISO-8859-1")); |
320 | output = out; |
321 | matcher = new Perl5Matcher(); |
322 | request = new Request(); |
323 | dateFormat = new SimpleDateFormat(DATE_FORMAT); |
324 | dateFormat.setTimeZone(new SimpleTimeZone(0, "GMT")); |
325 | this.documentRoot = documentRoot; |
326 | } |
327 | |
328 | /** |
329 | * Executes the HTTP conversation and closes the session after |
330 | * satisfying the first request. |
331 | */ |
332 | public void execute() throws IOException { |
333 | if(parseRequest()) { |
334 | processRequest(); |
335 | } |
336 | closeSession(); |
337 | } |
338 | |
339 | } |
340 | |