1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.server.web; |
7 | |
8 | import java.io.BufferedInputStream; |
9 | import java.io.BufferedOutputStream; |
10 | import java.io.File; |
11 | import java.io.FileOutputStream; |
12 | import java.io.IOException; |
13 | import java.io.InputStream; |
14 | import java.io.OutputStream; |
15 | import java.io.RandomAccessFile; |
16 | import java.net.Socket; |
17 | import java.net.UnknownHostException; |
18 | import java.util.Iterator; |
19 | import java.util.Locale; |
20 | import java.util.Properties; |
21 | import java.util.StringTokenizer; |
22 | import org.h2.engine.Constants; |
23 | import org.h2.engine.SysProperties; |
24 | import org.h2.message.DbException; |
25 | import org.h2.mvstore.DataUtils; |
26 | import org.h2.util.IOUtils; |
27 | import org.h2.util.NetUtils; |
28 | import org.h2.util.StringUtils; |
29 | |
30 | /** |
31 | * For each connection to a session, an object of this class is created. |
32 | * This class is used by the H2 Console. |
33 | */ |
34 | class WebThread extends WebApp implements Runnable { |
35 | |
36 | protected OutputStream output; |
37 | protected final Socket socket; |
38 | private final Thread thread; |
39 | private InputStream input; |
40 | private int headerBytes; |
41 | private String ifModifiedSince; |
42 | |
43 | WebThread(Socket socket, WebServer server) { |
44 | super(server); |
45 | this.socket = socket; |
46 | thread = new Thread(this, "H2 Console thread"); |
47 | } |
48 | |
49 | /** |
50 | * Start the thread. |
51 | */ |
52 | void start() { |
53 | thread.start(); |
54 | } |
55 | |
56 | /** |
57 | * Wait until the thread is stopped. |
58 | * |
59 | * @param millis the maximum number of milliseconds to wait |
60 | */ |
61 | void join(int millis) throws InterruptedException { |
62 | thread.join(millis); |
63 | } |
64 | |
65 | /** |
66 | * Close the connection now. |
67 | */ |
68 | void stopNow() { |
69 | this.stop = true; |
70 | try { |
71 | socket.close(); |
72 | } catch (IOException e) { |
73 | // ignore |
74 | } |
75 | } |
76 | |
77 | private String getAllowedFile(String requestedFile) { |
78 | if (!allow()) { |
79 | return "notAllowed.jsp"; |
80 | } |
81 | if (requestedFile.length() == 0) { |
82 | return "index.do"; |
83 | } |
84 | return requestedFile; |
85 | } |
86 | |
87 | @Override |
88 | public void run() { |
89 | try { |
90 | input = new BufferedInputStream(socket.getInputStream()); |
91 | output = new BufferedOutputStream(socket.getOutputStream()); |
92 | while (!stop) { |
93 | if (!process()) { |
94 | break; |
95 | } |
96 | } |
97 | } catch (Exception e) { |
98 | DbException.traceThrowable(e); |
99 | } |
100 | IOUtils.closeSilently(output); |
101 | IOUtils.closeSilently(input); |
102 | try { |
103 | socket.close(); |
104 | } catch (IOException e) { |
105 | // ignore |
106 | } finally { |
107 | server.remove(this); |
108 | } |
109 | } |
110 | |
111 | @SuppressWarnings("unchecked") |
112 | private boolean process() throws IOException { |
113 | boolean keepAlive = false; |
114 | String head = readHeaderLine(); |
115 | if (head.startsWith("GET ") || head.startsWith("POST ")) { |
116 | int begin = head.indexOf('/'), end = head.lastIndexOf(' '); |
117 | String file; |
118 | if (begin < 0 || end < begin) { |
119 | file = ""; |
120 | } else { |
121 | file = head.substring(begin + 1, end).trim(); |
122 | } |
123 | trace(head + ": " + file); |
124 | file = getAllowedFile(file); |
125 | attributes = new Properties(); |
126 | int paramIndex = file.indexOf("?"); |
127 | session = null; |
128 | if (paramIndex >= 0) { |
129 | String attrib = file.substring(paramIndex + 1); |
130 | parseAttributes(attrib); |
131 | String sessionId = attributes.getProperty("jsessionid"); |
132 | file = file.substring(0, paramIndex); |
133 | session = server.getSession(sessionId); |
134 | } |
135 | keepAlive = parseHeader(); |
136 | String hostAddr = socket.getInetAddress().getHostAddress(); |
137 | file = processRequest(file, hostAddr); |
138 | if (file.length() == 0) { |
139 | // asynchronous request |
140 | return true; |
141 | } |
142 | String message; |
143 | byte[] bytes; |
144 | if (cache && ifModifiedSince != null && |
145 | ifModifiedSince.equals(server.getStartDateTime())) { |
146 | bytes = null; |
147 | message = "HTTP/1.1 304 Not Modified\r\n"; |
148 | } else { |
149 | bytes = server.getFile(file); |
150 | if (bytes == null) { |
151 | message = "HTTP/1.1 404 Not Found\r\n"; |
152 | bytes = ("File not found: " + file).getBytes(Constants.UTF8); |
153 | message += "Content-Length: " + bytes.length + "\r\n"; |
154 | } else { |
155 | if (session != null && file.endsWith(".jsp")) { |
156 | String page = new String(bytes, Constants.UTF8); |
157 | if (SysProperties.CONSOLE_STREAM) { |
158 | Iterator<String> it = (Iterator<String>) session.map.remove("chunks"); |
159 | if (it != null) { |
160 | message = "HTTP/1.1 200 OK\r\n"; |
161 | message += "Content-Type: " + mimeType + "\r\n"; |
162 | message += "Cache-Control: no-cache\r\n"; |
163 | message += "Transfer-Encoding: chunked\r\n"; |
164 | message += "\r\n"; |
165 | trace(message); |
166 | output.write(message.getBytes()); |
167 | while (it.hasNext()) { |
168 | String s = it.next(); |
169 | s = PageParser.parse(s, session.map); |
170 | bytes = s.getBytes(Constants.UTF8); |
171 | if (bytes.length == 0) { |
172 | continue; |
173 | } |
174 | output.write(Integer.toHexString(bytes.length).getBytes()); |
175 | output.write("\r\n".getBytes()); |
176 | output.write(bytes); |
177 | output.write("\r\n".getBytes()); |
178 | output.flush(); |
179 | } |
180 | output.write("0\r\n\r\n".getBytes()); |
181 | output.flush(); |
182 | return keepAlive; |
183 | } |
184 | } |
185 | page = PageParser.parse(page, session.map); |
186 | bytes = page.getBytes(Constants.UTF8); |
187 | } |
188 | message = "HTTP/1.1 200 OK\r\n"; |
189 | message += "Content-Type: " + mimeType + "\r\n"; |
190 | if (!cache) { |
191 | message += "Cache-Control: no-cache\r\n"; |
192 | } else { |
193 | message += "Cache-Control: max-age=10\r\n"; |
194 | message += "Last-Modified: " + server.getStartDateTime() + "\r\n"; |
195 | } |
196 | message += "Content-Length: " + bytes.length + "\r\n"; |
197 | } |
198 | } |
199 | message += "\r\n"; |
200 | trace(message); |
201 | output.write(message.getBytes()); |
202 | if (bytes != null) { |
203 | output.write(bytes); |
204 | } |
205 | output.flush(); |
206 | } |
207 | return keepAlive; |
208 | } |
209 | |
210 | private String readHeaderLine() throws IOException { |
211 | StringBuilder buff = new StringBuilder(); |
212 | while (true) { |
213 | headerBytes++; |
214 | int c = input.read(); |
215 | if (c == -1) { |
216 | throw new IOException("Unexpected EOF"); |
217 | } else if (c == '\r') { |
218 | headerBytes++; |
219 | if (input.read() == '\n') { |
220 | return buff.length() > 0 ? buff.toString() : null; |
221 | } |
222 | } else if (c == '\n') { |
223 | return buff.length() > 0 ? buff.toString() : null; |
224 | } else { |
225 | buff.append((char) c); |
226 | } |
227 | } |
228 | } |
229 | |
230 | private void parseAttributes(String s) { |
231 | trace("data=" + s); |
232 | while (s != null) { |
233 | int idx = s.indexOf('='); |
234 | if (idx >= 0) { |
235 | String property = s.substring(0, idx); |
236 | s = s.substring(idx + 1); |
237 | idx = s.indexOf('&'); |
238 | String value; |
239 | if (idx >= 0) { |
240 | value = s.substring(0, idx); |
241 | s = s.substring(idx + 1); |
242 | } else { |
243 | value = s; |
244 | } |
245 | String attr = StringUtils.urlDecode(value); |
246 | attributes.put(property, attr); |
247 | } else { |
248 | break; |
249 | } |
250 | } |
251 | trace(attributes.toString()); |
252 | } |
253 | |
254 | private boolean parseHeader() throws IOException { |
255 | boolean keepAlive = false; |
256 | trace("parseHeader"); |
257 | int len = 0; |
258 | ifModifiedSince = null; |
259 | boolean multipart = false; |
260 | while (true) { |
261 | String line = readHeaderLine(); |
262 | if (line == null) { |
263 | break; |
264 | } |
265 | trace(" " + line); |
266 | String lower = StringUtils.toLowerEnglish(line); |
267 | if (lower.startsWith("if-modified-since")) { |
268 | ifModifiedSince = getHeaderLineValue(line); |
269 | } else if (lower.startsWith("connection")) { |
270 | String conn = getHeaderLineValue(line); |
271 | if ("keep-alive".equals(conn)) { |
272 | keepAlive = true; |
273 | } |
274 | } else if (lower.startsWith("content-type")) { |
275 | String type = getHeaderLineValue(line); |
276 | if (type.startsWith("multipart/form-data")) { |
277 | multipart = true; |
278 | } |
279 | } else if (lower.startsWith("content-length")) { |
280 | len = Integer.parseInt(getHeaderLineValue(line)); |
281 | trace("len=" + len); |
282 | } else if (lower.startsWith("user-agent")) { |
283 | boolean isWebKit = lower.contains("webkit/"); |
284 | if (isWebKit && session != null) { |
285 | // workaround for what seems to be a WebKit bug: |
286 | // http://code.google.com/p/chromium/issues/detail?id=6402 |
287 | session.put("frame-border", "1"); |
288 | session.put("frameset-border", "2"); |
289 | } |
290 | } else if (lower.startsWith("accept-language")) { |
291 | Locale locale = session == null ? null : session.locale; |
292 | if (locale == null) { |
293 | String languages = getHeaderLineValue(line); |
294 | StringTokenizer tokenizer = new StringTokenizer(languages, ",;"); |
295 | while (tokenizer.hasMoreTokens()) { |
296 | String token = tokenizer.nextToken(); |
297 | if (!token.startsWith("q=")) { |
298 | if (server.supportsLanguage(token)) { |
299 | int dash = token.indexOf('-'); |
300 | if (dash >= 0) { |
301 | String language = token.substring(0, dash); |
302 | String country = token.substring(dash + 1); |
303 | locale = new Locale(language, country); |
304 | } else { |
305 | locale = new Locale(token, ""); |
306 | } |
307 | headerLanguage = locale.getLanguage(); |
308 | if (session != null) { |
309 | session.locale = locale; |
310 | session.put("language", headerLanguage); |
311 | server.readTranslations(session, headerLanguage); |
312 | } |
313 | break; |
314 | } |
315 | } |
316 | } |
317 | } |
318 | } else if (line.trim().length() == 0) { |
319 | break; |
320 | } |
321 | } |
322 | if (multipart) { |
323 | uploadMultipart(input, len); |
324 | } else if (session != null && len > 0) { |
325 | byte[] bytes = DataUtils.newBytes(len); |
326 | for (int pos = 0; pos < len;) { |
327 | pos += input.read(bytes, pos, len - pos); |
328 | } |
329 | String s = new String(bytes); |
330 | parseAttributes(s); |
331 | } |
332 | return keepAlive; |
333 | } |
334 | |
335 | private void uploadMultipart(InputStream in, int len) throws IOException { |
336 | if (!new File(WebServer.TRANSFER).exists()) { |
337 | return; |
338 | } |
339 | String fileName = "temp.bin"; |
340 | headerBytes = 0; |
341 | String boundary = readHeaderLine(); |
342 | while (true) { |
343 | String line = readHeaderLine(); |
344 | if (line == null) { |
345 | break; |
346 | } |
347 | int index = line.indexOf("filename=\""); |
348 | if (index > 0) { |
349 | fileName = line.substring(index + |
350 | "filename=\"".length(), line.lastIndexOf('"')); |
351 | } |
352 | trace(" " + line); |
353 | } |
354 | if (!WebServer.isSimpleName(fileName)) { |
355 | return; |
356 | } |
357 | len -= headerBytes; |
358 | File file = new File(WebServer.TRANSFER, fileName); |
359 | OutputStream out = new FileOutputStream(file); |
360 | IOUtils.copy(in, out, len); |
361 | out.close(); |
362 | // remove the boundary |
363 | RandomAccessFile f = new RandomAccessFile(file, "rw"); |
364 | int testSize = (int) Math.min(f.length(), Constants.IO_BUFFER_SIZE); |
365 | f.seek(f.length() - testSize); |
366 | byte[] bytes = DataUtils.newBytes(Constants.IO_BUFFER_SIZE); |
367 | f.readFully(bytes, 0, testSize); |
368 | String s = new String(bytes, "ASCII"); |
369 | int x = s.lastIndexOf(boundary); |
370 | f.setLength(f.length() - testSize + x - 2); |
371 | f.close(); |
372 | } |
373 | |
374 | private static String getHeaderLineValue(String line) { |
375 | return line.substring(line.indexOf(':') + 1).trim(); |
376 | } |
377 | |
378 | @Override |
379 | protected String adminShutdown() { |
380 | stopNow(); |
381 | return super.adminShutdown(); |
382 | } |
383 | |
384 | private boolean allow() { |
385 | if (server.getAllowOthers()) { |
386 | return true; |
387 | } |
388 | try { |
389 | return NetUtils.isLocalAddress(socket); |
390 | } catch (UnknownHostException e) { |
391 | server.traceError(e); |
392 | return false; |
393 | } |
394 | } |
395 | |
396 | private void trace(String s) { |
397 | server.trace(s); |
398 | } |
399 | } |