001: /* Copyright (c) 2001-2005, The HSQL Development Group
002: * All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * Redistributions of source code must retain the above copyright notice, this
008: * list of conditions and the following disclaimer.
009: *
010: * Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * Neither the name of the HSQL Development Group nor the names of its
015: * contributors may be used to endorse or promote products derived from this
016: * software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
020: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
021: * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
022: * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
026: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package org.hsqldb;
032:
033: import java.io.BufferedOutputStream;
034: import java.io.DataInputStream;
035: import java.io.File;
036: import java.io.FileInputStream;
037: import java.io.IOException;
038: import java.io.InputStream;
039: import java.io.OutputStream;
040: import java.net.HttpURLConnection;
041: import java.net.Socket;
042:
043: import org.hsqldb.lib.ArrayUtil;
044: import org.hsqldb.lib.InOutUtil;
045: import org.hsqldb.persist.HsqlDatabaseProperties;
046: import org.hsqldb.resources.BundleHandler;
047: import org.hsqldb.rowio.RowInputBinary;
048: import org.hsqldb.rowio.RowOutputBinary;
049:
050: // fredt@users 20021002 - patch 1.7.1 - changed notification method
051: // unsaved@users 20021113 - patch 1.7.2 - SSL support
052: // boucherb@users 20030510 - patch 1.7.2 - SSL support moved to factory interface
053: // boucherb@users 20030510 - patch 1.7.2 - general lint removal
054: // boucherb@users 20030514 - patch 1.7.2 - localized error responses
055: // fredt@users 20030628 - patch 1.7.2 - new protocol, persistent sessions
056:
057: /**
058: * A web server connection is a transient object that lasts for the duration
059: * of the SQL call and its result. This class uses the notification
060: * mechanism in WebServer to allow cleanup after a SHUTDOWN.<p>
061: *
062: * The POST method is used for login and subsequent remote calls. In 1.7.2
063: * The initial login establishes a persistent Session and returns its handle
064: * to the client. Subsequent calls are executed in the context of this
065: * session.<p>
066: * (fredt@users)
067: *
068: * Rewritten in version HSQLDB 1.7.2, based on original Hypersonic code.
069: *
070: * @author Thomas Mueller (Hypersonic SQL Group)
071: * @author fredt@users
072: * @version 1.7.2
073: * @since Hypersonic SQL
074: */
075: class WebServerConnection implements Runnable {
076:
077: static final String ENCODING = "8859_1";
078: private Socket socket;
079: private WebServer server;
080: private static final int REQUEST_TYPE_BAD = 0;
081: private static final int REQUEST_TYPE_GET = 1;
082: private static final int REQUEST_TYPE_HEAD = 2;
083: private static final int REQUEST_TYPE_POST = 3;
084: private static final String HEADER_OK = "HTTP/1.0 200 OK";
085: private static final String HEADER_BAD_REQUEST = "HTTP/1.0 400 Bad Request";
086: private static final String HEADER_NOT_FOUND = "HTTP/1.0 404 Not Found";
087: private static final String HEADER_FORBIDDEN = "HTTP/1.0 403 Forbidden";
088: static final int BUFFER_SIZE = 256;
089: private RowOutputBinary rowOut = new RowOutputBinary(BUFFER_SIZE);
090: private RowInputBinary rowIn = new RowInputBinary(rowOut);
091:
092: //
093: static final byte[] BYTES_GET = "GET".getBytes();
094: static final byte[] BYTES_HEAD = "HEAD".getBytes();
095: static final byte[] BYTES_POST = "POST".getBytes();
096: static final byte[] BYTES_CONTENT = "Content-Length: ".getBytes();
097: static final byte[] BYTES_WHITESPACE = new byte[] { (byte) ' ',
098: (byte) '\t' };
099:
100: // default mime type mappings
101: private static final int hnd_content_types = BundleHandler
102: .getBundleHandle("content-types", null);
103:
104: /**
105: * Creates a new WebServerConnection to the specified WebServer on the
106: * specified socket.
107: *
108: * @param socket the network socket on which WebServer communication
109: * takes place
110: * @param server the WebServer instance to which the object
111: * represents a connection
112: */
113: WebServerConnection(Socket socket, WebServer server) {
114: this .server = server;
115: this .socket = socket;
116: }
117:
118: /**
119: * Retrieves a best-guess mime-type string using the file extension
120: * of the name argument.
121: *
122: * @return a best-guess mime-type string using the file extension
123: * of the name argument.
124: */
125: private String getMimeTypeString(String name) {
126:
127: int pos;
128: String key;
129: String mimeType;
130:
131: if (name == null) {
132: return ServerConstants.SC_DEFAULT_WEB_MIME;
133: }
134:
135: pos = name.lastIndexOf('.');
136: key = null;
137: mimeType = null;
138:
139: // first search user-specified mapping
140: if (pos >= 0) {
141: key = name.substring(pos).toLowerCase();
142: mimeType = server.serverProperties.getProperty(key);
143: }
144:
145: // if not found, search default mapping
146: if (mimeType == null && key.length() > 1) {
147: mimeType = BundleHandler.getString(hnd_content_types, key
148: .substring(1));
149: }
150:
151: return mimeType == null ? ServerConstants.SC_DEFAULT_WEB_MIME
152: : mimeType;
153: }
154:
155: /**
156: * Causes this WebServerConnection to process its HTTP request
157: * in a blocking fashion until the request is fully processed
158: * or an exception occurs internally.
159: *
160: * This method reads the Request line then delegates action to subroutines.
161: */
162: public void run() {
163:
164: try {
165: DataInputStream inStream = new DataInputStream(socket
166: .getInputStream());
167: int count;
168: String request;
169: String name = null;
170: int method = REQUEST_TYPE_BAD;
171: int len = -1;
172:
173: // read line, ignoring any leading blank lines
174: do {
175: count = InOutUtil.readLine(inStream, rowOut);
176:
177: if (count == 0) {
178: throw new Exception();
179: }
180: } while (count < 2);
181:
182: byte[] byteArray = rowOut.getBuffer();
183: int offset = rowOut.size() - count;
184:
185: if (ArrayUtil.containsAt(byteArray, offset, BYTES_POST)) {
186: method = REQUEST_TYPE_POST;
187: offset += BYTES_POST.length;
188: } else if (ArrayUtil.containsAt(byteArray, offset,
189: BYTES_GET)) {
190: method = REQUEST_TYPE_GET;
191: offset += BYTES_GET.length;
192: } else if (ArrayUtil.containsAt(byteArray, offset,
193: BYTES_HEAD)) {
194: method = REQUEST_TYPE_HEAD;
195: offset += BYTES_HEAD.length;
196: } else {
197: throw new Exception();
198: }
199:
200: count = ArrayUtil.countStartElementsAt(byteArray, offset,
201: BYTES_WHITESPACE);
202:
203: if (count == 0) {
204: throw new Exception();
205: }
206:
207: offset += count;
208: count = ArrayUtil.countNonStartElementsAt(byteArray,
209: offset, BYTES_WHITESPACE);
210: name = new String(byteArray, offset, count, ENCODING);
211:
212: switch (method) {
213:
214: case REQUEST_TYPE_BAD:
215: processError(REQUEST_TYPE_BAD);
216: break;
217:
218: case REQUEST_TYPE_GET:
219: processGet(name, true);
220: break;
221:
222: case REQUEST_TYPE_HEAD:
223: processGet(name, false);
224: break;
225:
226: case REQUEST_TYPE_POST:
227: processPost(inStream, name);
228: break;
229: }
230:
231: inStream.close();
232: socket.close();
233: } catch (Exception e) {
234: server.printStackTrace(e);
235: }
236: }
237:
238: /**
239: * POST is used only for database access. So we can assume the strings
240: * are those generated by HTTPClientConnection
241: */
242: private void processPost(InputStream inStream, String name)
243: throws HsqlException, IOException {
244:
245: // fredt - parsing in this block is not actually necessary
246: try {
247:
248: // read the Content-Type line
249: InOutUtil.readLine(inStream, rowOut);
250:
251: // read and parse the Content-Length line
252: int count = InOutUtil.readLine(inStream, rowOut);
253: int offset = rowOut.size() - count;
254:
255: // get buffer always after reading into rowOut, else old buffer may
256: // be returned
257: byte[] byteArray = rowOut.getBuffer();
258:
259: if (!ArrayUtil.containsAt(byteArray, offset, BYTES_CONTENT)) {
260: throw new Exception();
261: }
262:
263: count -= BYTES_CONTENT.length;
264: offset += BYTES_CONTENT.length;
265:
266: // omit the last two characters
267: String lenStr = new String(byteArray, offset, count - 2);
268: int length = Integer.parseInt(lenStr);
269:
270: InOutUtil.readLine(inStream, rowOut);
271: } catch (Exception e) {
272: processError(HttpURLConnection.HTTP_BAD_REQUEST);
273:
274: return;
275: }
276:
277: processQuery(inStream);
278: }
279:
280: /**
281: * Processes a database query in HSQL protocol that has been
282: * tunneled over HTTP protocol.
283: *
284: * @param inStream the incoming byte stream representing the HSQL protocol
285: * database query
286: */
287: void processQuery(InputStream inStream) {
288:
289: try {
290: Result resultIn = Result.read(rowIn, new DataInputStream(
291: inStream));
292:
293: //
294: Result resultOut;
295:
296: if (resultIn.mode == ResultConstants.SQLCONNECT) {
297: try {
298: int dbID = server.getDBID(resultIn.subSubString);
299: Session session = DatabaseManager.newSession(dbID,
300: resultIn.getMainString(), resultIn
301: .getSubString());
302:
303: resultOut = new Result(ResultConstants.UPDATECOUNT);
304: resultOut.databaseID = dbID;
305: resultOut.sessionID = session.getId();
306: } catch (HsqlException e) {
307: resultOut = new Result(e, null);
308: } catch (RuntimeException e) {
309: resultOut = new Result(e, null);
310: }
311: } else {
312: int dbID = resultIn.databaseID;
313: Session session = DatabaseManager.getSession(dbID,
314: resultIn.sessionID);
315:
316: resultOut = session == null ? new Result(Trace
317: .error(Trace.DATABASE_NOT_EXISTS), null)
318: : session.execute(resultIn);
319: }
320:
321: //
322: rowOut.reset();
323: resultOut.write(rowOut);
324:
325: OutputStream outStream = socket.getOutputStream();
326: String header = getHead(HEADER_OK, false,
327: "application/octet-stream", rowOut.size());
328:
329: outStream.write(header.getBytes(ENCODING));
330: outStream.write(rowOut.getOutputStream().getBuffer(), 0,
331: rowOut.getOutputStream().size());
332: outStream.flush();
333: outStream.close();
334: } catch (Exception e) {
335: server.printStackTrace(e);
336: }
337: }
338:
339: /**
340: * Processes an HTTP GET request
341: *
342: * @param name the name of the content to get
343: * @param send whether to send the content as well, or just the header
344: */
345: private void processGet(String name, boolean send) {
346:
347: try {
348: String hdr;
349: OutputStream os;
350: InputStream is;
351: int b;
352:
353: if (name.endsWith("/")) {
354: name = name + server.getDefaultWebPage();
355: }
356:
357: // traversing up the directory structure is forbidden.
358: if (name.indexOf("..") != -1) {
359: processError(HttpURLConnection.HTTP_FORBIDDEN);
360:
361: return;
362: }
363:
364: name = server.getWebRoot() + name;
365:
366: if (File.separatorChar != '/') {
367: name = name.replace('/', File.separatorChar);
368: }
369:
370: is = null;
371:
372: server.printWithThread("GET " + name);
373:
374: try {
375: File file = new File(name);
376:
377: is = new DataInputStream(new FileInputStream(file));
378: hdr = getHead(HEADER_OK, true, getMimeTypeString(name),
379: (int) file.length());
380: } catch (IOException e) {
381: processError(HttpURLConnection.HTTP_NOT_FOUND);
382:
383: if (is != null) {
384: is.close();
385: }
386:
387: return;
388: }
389:
390: os = new BufferedOutputStream(socket.getOutputStream());
391:
392: os.write(hdr.getBytes(ENCODING));
393:
394: if (send) {
395: while ((b = is.read()) != -1) {
396: os.write(b);
397: }
398: }
399:
400: os.flush();
401: os.close();
402: is.close();
403: } catch (Exception e) {
404: server.printError("processGet: " + e.toString());
405: server.printStackTrace(e);
406: }
407: }
408:
409: /**
410: * Retrieves an HTTP protocol header given the supplied arguments.
411: *
412: * @param responseCodeString the HTTP response code
413: * @param addInfo true if additional header info is to be added
414: * @param mimeType the Content-Type field value
415: * @param length the Content-Length field value
416: * @return an HTTP protocol header
417: */
418: String getHead(String responseCodeString, boolean addInfo,
419: String mimeType, int length) {
420:
421: StringBuffer sb = new StringBuffer(128);
422:
423: sb.append(responseCodeString).append("\r\n");
424:
425: if (addInfo) {
426: sb.append("Allow: GET, HEAD, POST\nMIME-Version: 1.0\r\n");
427: sb.append("Server: ").append(
428: HsqlDatabaseProperties.PRODUCT_NAME).append("\r\n");
429: }
430:
431: if (mimeType != null) {
432: sb.append("Content-Type: ").append(mimeType).append("\r\n");
433: sb.append("Content-Length: ").append(length).append("\r\n");
434: }
435:
436: sb.append("\r\n");
437:
438: return sb.toString();
439: }
440:
441: /**
442: * Processess an HTTP error condition, sending an error response to
443: * the client.
444: *
445: * @param code the error condition code
446: */
447: private void processError(int code) {
448:
449: String msg;
450:
451: server.printWithThread("processError " + code);
452:
453: switch (code) {
454:
455: case HttpURLConnection.HTTP_BAD_REQUEST:
456: msg = getHead(HEADER_BAD_REQUEST, false, null, 0);
457: msg += BundleHandler.getString(WebServer.webBundleHandle,
458: "BAD_REQUEST");
459: break;
460:
461: case HttpURLConnection.HTTP_FORBIDDEN:
462: msg = getHead(HEADER_FORBIDDEN, false, null, 0);
463: msg += BundleHandler.getString(WebServer.webBundleHandle,
464: "FORBIDDEN");
465: break;
466:
467: case HttpURLConnection.HTTP_NOT_FOUND:
468: default:
469: msg = getHead(HEADER_NOT_FOUND, false, null, 0);
470: msg += BundleHandler.getString(WebServer.webBundleHandle,
471: "NOT_FOUND");
472: break;
473: }
474:
475: try {
476: OutputStream os = new BufferedOutputStream(socket
477: .getOutputStream());
478:
479: os.write(msg.getBytes(ENCODING));
480: os.flush();
481: os.close();
482: } catch (Exception e) {
483: server.printError("processError: " + e.toString());
484: server.printStackTrace(e);
485: }
486: }
487:
488: /**
489: * Retrieves the thread name to be used when
490: * this object is the Runnable object of a Thread.
491: *
492: * @return the thread name to be used when
493: * this object is the Runnable object of a Thread.
494: */
495: String getConnectionThreadName() {
496: return "HSQLDB HTTP Connection @"
497: + Integer.toString(hashCode(), 16);
498: }
499: }
|