/******************************************************************************* * Copyright (c) 2003, 2011 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - Initial API and implementation *******************************************************************************/ package org.eclipse.wst.internet.monitor.core.internal.http; import java.io.*; import org.eclipse.wst.internet.monitor.core.internal.Connection; import org.eclipse.wst.internet.monitor.core.internal.Messages; import org.eclipse.wst.internet.monitor.core.internal.Trace; import org.eclipse.wst.internet.monitor.core.internal.provisional.Request; /** * Monitor server I/O thread. */ public class HTTPThread extends Thread { private static final int BUFFER = 2048; private static final byte CR = (byte) '\r'; private static final byte LF = (byte) '\n'; protected static int threadCount = 0; private byte[] readBuffer = new byte[BUFFER]; // buffer and index protected byte[] buffer = new byte[0]; protected int bufferIndex = 0; protected InputStream in; protected OutputStream out; protected HTTPConnection conn; protected boolean isRequest; protected Connection conn2; protected HTTPThread request; protected boolean isWaiting; // user to translate the Host: header protected String host; protected int port; protected int contentLength = -1; protected byte transferEncoding = -1; protected String responseType = null; protected boolean connectionKeepAlive = false; protected boolean connectionClose = false; protected static final String[] ENCODING_STRING = new String[] { "chunked", "identity", "gzip", "compressed", "deflate"}; protected static final byte ENCODING_CHUNKED = 0; protected static final byte ENCODING_IDENTITY = 1; protected static final byte ENCODING_GZIP = 2; protected static final byte ENCODING_COMPRESSED = 3; protected static final byte ENCODING_DEFLATE = 4; /* change: Referer: http://localhost:8081/index.html Host: localhost:8081 */ /* The Connection header has the following grammar: Connection = "Connection" ":" 1#(connection-token) connection-token = token HTTP/1.1 proxies MUST parse the Connection header field before a message is forwarded and, for each connection-token in this field, remove any header field(s) from the message with the same name as the connection-token. */ /** * Create a new HTTP thread. * * @param conn2 * @param in * @param out * @param conn * @param isRequest * @param host * @param port */ public HTTPThread(Connection conn2, InputStream in, OutputStream out, HTTPConnection conn, boolean isRequest, String host, int port) { super("TCP/IP Monitor HTTP Connection"); this.conn2 = conn2; this.in = in; this.out = out; this.conn = conn; this.isRequest = isRequest; this.host = host; this.port = port; setName("HTTP (" + host + ":" + port + ") " + (isRequest ? "REQUEST" : "RESPONSE") + " " + (threadCount++)); setPriority(Thread.NORM_PRIORITY + 1); setDaemon(true); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Started: " + this); } } /** * Create a new HTTP thread. * * @param conn2 * @param in * @param out * @param conn * @param isRequest * @param host * @param port * @param request */ public HTTPThread(Connection conn2, InputStream in, OutputStream out, HTTPConnection conn, boolean isRequest, String host, int port, HTTPThread request) { this(conn2, in, out, conn, isRequest, host, port); this.request = request; } /** * Add a line feed to the end of the byte array. * @return byte[] * @param b byte[] */ protected static byte[] convert(byte[] b) { if (b == null || b.length == 0) return b; int size = b.length; byte[] x = new byte[size + 2]; System.arraycopy(b, 0, x, 0, size); x[size] = (byte) '\r'; // CR x[size + 1] = (byte) '\n'; // LF return x; } /** * Read more data into the buffer. */ protected void fillBuffer() throws IOException { int n = in.read(readBuffer); if (n <= 0) throw new IOException("End of input"); // add to full buffer int len = buffer.length - bufferIndex; if (len < 0) len = 0; byte[] x = new byte[n + len]; System.arraycopy(buffer, bufferIndex, x, 0, len); System.arraycopy(readBuffer, 0, x, len, n); bufferIndex = 0; buffer = x; } /** * Returns the first location of a CRLF. * * @return int */ protected int getFirstCRLF() { int size = buffer.length; int i = bufferIndex + 1; while (i < size) { if (buffer[i - 1] == CR && buffer[i] == LF) return i; i++; } return -1; } /** * Output the given bytes. * @param b byte[] */ protected void outputBytes(byte[] b, boolean isNew) throws IOException { out.write(b); if (isRequest) conn.addRequest(b, isNew); else conn.addResponse(b, isNew); } /** * Parse the HTTP body. * * @throws IOException */ public void parseBody() throws IOException { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Parsing body for: " + this); } if (responseType != null && ("204".equals(responseType) || "304".equals(responseType))) { setHTTPBody(new byte[0]); return; } if (isRequest) { if (contentLength != -1) { byte[] b2 = null; int b2Index = 0; if (contentLength < 1024 * 1024) b2 = new byte[contentLength]; byte[] b = removeFromBuffer(Math.min(buffer.length, bufferIndex + contentLength)); if (b2 != null) { System.arraycopy(b, 0, b2, 0, b.length); b2Index += b.length; } int bytesLeft = contentLength - b.length; if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "[Request] bytesLeft: " + bytesLeft); } out.write(b); int n = 0; while (bytesLeft > 0) { n = in.read(readBuffer, 0, Math.min(readBuffer.length, bytesLeft)); bytesLeft -= n; if (b2 != null) { System.arraycopy(readBuffer, 0, b2, b2Index, n); b2Index += n; } out.write(readBuffer, 0, n); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "[Request] bytes read: " + n + " bytesLeft: " + bytesLeft); } } // restore the byte array for display if (b2 == null) b2 = Messages.errorContentSize.getBytes(); conn.addRequest(b2, false); setHTTPBody(b2); } else if (transferEncoding != -1 && transferEncoding != ENCODING_IDENTITY) { parseChunk(); } if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Done parsing request body for: " + this); } return; } // just return body for HTTP 1.0 responses if (!isRequest && !connectionKeepAlive && contentLength == -1 && transferEncoding == -1) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Assuming HTTP 1.0 for: " + this); } int n = buffer.length - bufferIndex; byte[] b = readBytes(n); byte[] body = new byte[0]; while (n >= 0) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Bytes read: " + n + " " + this); } if (b != null && n > 0) { byte[] x = null; if (n == b.length) x = b; else { x = new byte[n]; System.arraycopy(b, 0, x, 0, n); } outputBytes(x, false); // copy to HTTP body byte[] temp = new byte[body.length + x.length]; System.arraycopy(body, 0, temp, 0, body.length); System.arraycopy(x, 0, temp, body.length, x.length); body = temp; } if (b == null || b.length < BUFFER) b = new byte[BUFFER]; n = in.read(b); Thread.yield(); } out.flush(); setHTTPBody(body); return; } // spec 4.4.1 if (responseType != null && responseType.startsWith("1")) { setHTTPBody(new byte[0]); return; } // spec 4.4.2 if (transferEncoding != -1 && transferEncoding != ENCODING_IDENTITY) { parseChunk(); return; } // spec 4.4.3 if (contentLength != -1) { byte[] b2 = null; int b2Index = 0; if (contentLength < 1024 * 1024) b2 = new byte[contentLength]; byte[] b = removeFromBuffer(Math.min(buffer.length, bufferIndex + contentLength)); if (b2 != null) { System.arraycopy(b, 0, b2, 0, b.length); b2Index += b.length; } int bytesLeft = contentLength - b.length; if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "bytesLeft: " + bytesLeft); } out.write(b); int n = 0; while (bytesLeft > 0) { n = in.read(readBuffer, 0, Math.min(readBuffer.length, bytesLeft)); bytesLeft -= n; if (b2 != null) { System.arraycopy(readBuffer, 0, b2, b2Index, n); b2Index += n; } if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "bytes read: " + n + " bytesLeft: " + bytesLeft); } out.write(readBuffer, 0, n); } // restore the byte array for display if (b2 == null) b2 = Messages.errorContentSize.getBytes(); if (isRequest) conn.addRequest(b2, false); else conn.addResponse(b2, false); setHTTPBody(b2); return; } // spec 4.4.4 (?) if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Unknown body for: " + this); } } // Use this method to dump the content of a byte array // // private void dumpBuffer(byte[] b) { // Trace.trace(Trace.PARSING, "Buffer dump to default.out:"); // Trace.trace(Trace.PARSING, "Byte array: " + b.length); // for (int i = 0; i < b.length; i++) { // System.out.print(" [" + (char) b[i] + "]"); // +" ["+b[i+1]+"] " // if (i % 20 == 0) { // System.out.println(); // } // } // } /** * Parse an HTTP chunk. * * @throws IOException */ public void parseChunk() throws IOException { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Parsing chunk for: " + this); } boolean done = false; byte[] body = new byte[0]; while (!done) { // read chunk size byte[] b = readLine(); String s = new String(b); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Chunk-length: " + s); } int index = s.indexOf(" "); int length = -1; try { if (index > 0) s = s.substring(0, index); length = Integer.parseInt(s.trim(), 16); } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error chunk for: " + this, e); } } // output bytes outputBytes(b, false); if (length <= 0) done = true; else { // read and output chunk data plus CRLF b = readBytes(length + 2); outputBytes(b, false); // copy to HTTP body byte[] temp = new byte[body.length + b.length - 2]; System.arraycopy(body, 0, temp, 0, body.length); System.arraycopy(b, 0, temp, body.length, b.length - 2); body = temp; } } // read trailer byte[] b = readLine(); while (b.length > 2) { outputBytes(b, false); b = readLine(); } outputBytes(b, false); setHTTPBody(body); } /** * Parse an HTTP header. * * @throws IOException */ public void parseHeader() throws IOException { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Parsing header for: " + this); } // read until first blank line boolean isFirstLine = true; boolean isNew = true; byte[] b = readLine(); while (b.length > 5) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Parsing header line: '" + new String(b) + "'"); } if (isFirstLine) { String s = new String(b); if (isRequest) { setLabel(s); isNew = false; } if (!isRequest) { int index1 = s.indexOf(' '); int index2 = s.indexOf(' ', index1 + 1); try { responseType = s.substring(index1 + 1, index2).trim(); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Response Type: " + this + " " + responseType); } } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error parsing response type for: " + this, e); } } if (responseType != null && responseType.equals("100")) { outputBytes(b, isNew); isNew = false; b = readLine(); outputBytes(b, false); b = readLine(); index1 = s.indexOf(' '); index2 = s.indexOf(' ', index1 + 1); try { responseType = s.substring(index1 + 1, index2).trim(); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Response Type: " + this + " " + responseType); } } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error parsing response type for: " + this, e); } } } } isFirstLine = false; } // translate b = translateHeaderLine(b); outputBytes(b, isNew); isNew = false; b = readLine(); } if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Parsing final header line: '" + new String(b) + "'"); } outputBytes(b, false); Request rr = conn.getRequestResponse(isRequest); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Setting header length: " + rr.getRequest(Request.ALL).length); } setHTTPHeader(rr); } /** * Read bytes from the stream. * @return byte[] */ protected byte[] readBytes(int n) throws IOException { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "readBytes() " + n + " for: " + this); } while (buffer.length - bufferIndex < n) fillBuffer(); return removeFromBuffer(bufferIndex + n); } /** * Read and return the next full line. * * @return byte[] */ protected byte[] readLine() throws IOException { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "readLine() for: " + this); } int n = getFirstCRLF(); while (n < 0) { fillBuffer(); n = getFirstCRLF(); } return removeFromBuffer(n + 1); } /** * Remove data from the buffer up to the absolute index n. * Return the data from between bufferIndex and n. * * @param n the bytes to remove * @return a byte array */ protected byte[] removeFromBuffer(int n) { // copy line out of buffer byte[] b = new byte[n - bufferIndex]; System.arraycopy(buffer, bufferIndex, b, 0, n - bufferIndex); if (buffer.length > BUFFER * 2 || bufferIndex > BUFFER) { // remove line from buffer int size = buffer.length; byte[] x = new byte[size - n]; System.arraycopy(buffer, n, x, 0, size - n); buffer = x; bufferIndex = 0; } else bufferIndex = n; return b; } /** * Listen for input, save it, and pass to the output stream. * Philosophy: Read a single line separately and translate. * When blank line is reached, just pass all other data through. */ public void run() { try { try { while (true) { contentLength = -1; transferEncoding = -1; connectionKeepAlive = false; connectionClose = false; parseHeader(); parseBody(); if (isRequest && connectionKeepAlive) waitForResponse(); //Request r = conn.getRequestResponse(true); //r.fireChangedEvent(); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Done HTTP request for " + this + " " + connectionKeepAlive); } if (!isRequest && (!request.connectionKeepAlive || connectionClose)) { conn2.close(); if (request.connectionKeepAlive && connectionClose) request.connectionKeepAlive = false; notifyRequest(); break; } if (!isRequest) notifyRequest(); Thread.yield(); } } catch (IOException e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "End of buffer for: " + this, e); } if (!isRequest) { try { request.connectionKeepAlive = false; request.conn2.close(); notifyRequest(); } catch (Exception ex) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error closing request in response to error: " + this, e); } } } } // send rest of buffer out.write(buffer, bufferIndex, buffer.length - bufferIndex); out.flush(); } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error in: " + this, e); } } //if (!isRequest) // conn2.close(); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Closing thread " + this); } } /** * Sets the title of the call. * * @param s java.lang.String */ protected void setLabel(String s) { try { int index1 = s.indexOf(' '); if (index1 < 0 || index1 > 15) return; int index2 = s.indexOf(' ', index1 + 1); if (index2 < 0) return; conn.setLabel(s.substring(index1 + 1, index2), true); } catch (Exception e) { // ignore } } /** * Translate the header line. * * @return byte[] * @param b byte[] */ protected byte[] translateHeaderLine(byte[] b) { String s = new String(b); if (isRequest && s.toLowerCase().startsWith("host: ")) { String t = "Host: " + host; if (port != 80) t += ":" + port; return convert(t.getBytes()); } else if (s.toLowerCase().startsWith("content-length: ")) { try { contentLength = Integer.parseInt(s.substring(16).trim()); if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Content length: " + this + " " + contentLength); } } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Content length error", e); } } } else if (s.toLowerCase().startsWith("connection: ")) { try { String t = s.substring(11).trim(); if (t.equalsIgnoreCase("Keep-Alive")) connectionKeepAlive = true; // response contains "Connection: close" header // close connection to the client even if "keepalive" had been requested // we can't just reset request.keepAlive - it's used as indicator whether // the request thread is (going to) wait for the response thread // (and must be notified (only then)), // so we have to let it alone if (t.equalsIgnoreCase("close")) connectionClose = true; if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Keep alive: " + connectionKeepAlive); } } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error getting Connection: from header", e); } } } else if (s.toLowerCase().startsWith("transfer-encoding: ")) { String t = s.substring(19).trim(); int size = ENCODING_STRING.length; for (int i = 0; i < size; i++) { if (ENCODING_STRING[i].equalsIgnoreCase(t)) { transferEncoding = (byte) i; if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Transfer encoding: " + ENCODING_STRING[i]); } } } } return b; } protected void close() { try { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Closing: " + this); } out.close(); } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error closing connection " + this, e); } } } protected void waitForResponse() { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Waiting for response " + this); } synchronized (this) { try { isWaiting = true; wait(); } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error in waitForResponse() " + this, e); } } isWaiting = false; } if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Done waiting for response " + this); } } protected void notifyRequest() { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Notifying request " + this); } while (request.connectionKeepAlive && !request.isWaiting) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Waiting for request " + this); } try { Thread.sleep(100); } catch (Exception e) { // ignore } } synchronized (request) { try { request.notify(); } catch (Exception e) { if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Error in notifyRequest() " + this, e); } } } if (Trace.PARSING) { Trace.trace(Trace.STRING_PARSING, "Done notifying request " + this); } } protected void setHTTPHeader(Request rr) { if (isRequest) { byte[] b = rr.getRequest(Request.ALL); byte[] h = new byte[b.length]; System.arraycopy(b, 0, h, 0, b.length); rr.setProperty(HTTPRequest.HTTP_REQUEST_HEADER, h); } else { byte[] b = rr.getResponse(Request.ALL); byte[] h = new byte[b.length]; System.arraycopy(b, 0, h, 0, b.length); rr.setProperty(HTTPRequest.HTTP_RESPONSE_HEADER, h); } } protected void setHTTPBody(byte[] b) { Request rr = conn.getRequestResponse(isRequest); if (isRequest) rr.setProperty(HTTPRequest.HTTP_REQUEST_BODY, b); else rr.setProperty(HTTPRequest.HTTP_RESPONSE_BODY, b); } }