From: costin Date: Thu, 26 Nov 2009 06:35:43 +0000 (+0000) Subject: This is the first draft of the refactored coyote connector. X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=f478cab7fa9652989e59bf59ba5047ccde89e23a;p=tomcat7.0 This is the first draft of the refactored coyote connector. Lots of big changes: - MessageBytes is gone - BBuffer, CBuffer are used as buffers, BBucket/CBucket are used for wrapping bytes - C2B/B2C are gone - replaced by nio charsets in IOReader/IOWriter. CBuffer is still needed (instead of a StringBuilder ) to have access to the raw chars and avoid copy - speaking of copy, IOChannel uses a list of buffers instead of growing a buffer. For proxy we should be able to move buffers from in to out. This works a bit better with the char conversions, etc - Nio code heavily refactored - I didn't add APR but shouldn't be hard if anyonew wants. - Ssl is barely working - needs improvements git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@884410 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBucket.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBucket.java new file mode 100644 index 000000000..a12eb4a2c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBucket.java @@ -0,0 +1,40 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.nio.ByteBuffer; + + + +/** + * Holds raw data. Similar interface with a ByteBuffer in 'channel write' + * or 'read mode'. Data is between position and limit - allways. + * + * TODO: FileBucket, DirectBufferBucket, CharBucket, ... + * + * @author Costin Manolache + */ +public interface BBucket { + + public void release(); + + public byte[] array(); + public int position(); + public int remaining(); + public int limit(); + + public boolean hasRemaining(); + + public void position(int newStart); + + /** + * Return a byte buffer, with data between position and limit. + * Changes in the ByteBuffer position will not be reflected + * in the IOBucket. + * + * @return + */ + public ByteBuffer getByteBuffer(); + + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBuffer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBuffer.java new file mode 100644 index 000000000..a83c5fbde --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BBuffer.java @@ -0,0 +1,1193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/* + * In a server it is very important to be able to operate on + * the original byte[] without converting everything to chars. + * Some protocols are ASCII only, and some allow different + * non-UNICODE encodings. The encoding is not known beforehand, + * and can even change during the execution of the protocol. + * ( for example a multipart message may have parts with different + * encoding ) + * + * For HTTP it is not very clear how the encoding of RequestURI + * and mime values can be determined, but it is a great advantage + * to be able to parse the request without converting to string. + */ + +// Renamed from ByteChunk to make it easier to write code using both + +/** + * This class is used to represent a chunk of bytes, and utilities to manipulate + * byte[]. + * + * The buffer can be modified and used for both input and output. + * + * There are 2 modes: The chunk can be associated with a sink - ByteInputChannel + * or ByteOutputChannel, which will be used when the buffer is empty ( on input + * ) or filled ( on output ). For output, it can also grow. This operating mode + * is selected by calling setLimit() or allocate(initial, limit) with limit != + * -1. + * + * Various search and append method are defined - similar with String and + * StringBuffer, but operating on bytes. + * + * This is important because it allows processing the http headers directly on + * the received bytes, without converting to chars and Strings until the strings + * are needed. In addition, the charset is determined later, from headers or + * user code. + * + * + * @author dac@sun.com + * @author James Todd [gonzo@sun.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public class BBuffer implements Cloneable, Serializable, + BBucket { + + /** + * Default encoding used to convert to strings. It should be UTF8, but: + * - the servlet API requires 8859_1 as default + * - + */ + public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1"; + + // byte[] + private byte[] buff; + + private int start = 0; + + private int end; + + private ByteBuffer byteBuffer; + + public static final String CRLF = "\r\n"; + + /* Various constant "strings" */ + public static final byte[] CRLF_BYTES = convertToBytes(BBuffer.CRLF); + + /** + * HT. + */ + public static final byte HT = (byte) '\t'; + + /** + * SP. + */ + public static final byte SP = (byte) ' '; + + /** + * LF. + */ + public static final byte LF = (byte) '\n'; + + /** + * CR. + */ + public static final byte CR = (byte) '\r'; + + //private int useCount; + + + private static final boolean[] isDigit = new boolean[256]; + + static Charset UTF8; + + public static final byte A = (byte) 'A'; + + public static final byte Z = (byte) 'Z'; + + public static final byte a = (byte) 'a'; + + public static final byte LC_OFFSET = A - a; + private static final byte[] toLower = new byte[256]; + private static final boolean[] isUpper = new boolean[256]; + + static { + for (int i = 0; i < 256; i++) { + toLower[i] = (byte)i; + } + + for (int lc = 'a'; lc <= 'z'; lc++) { + int uc = lc + 'A' - 'a'; + toLower[uc] = (byte)lc; + isUpper[uc] = true; + } + } + + static { + for (int d = '0'; d <= '9'; d++) { + isDigit[d] = true; + } + UTF8 = Charset.forName("UTF-8"); + } + + public static BBuffer allocate() { + return new BBuffer(); + } + + public static BBuffer allocate(int initial) { + return new BBuffer().makeSpace(initial); + } + + + public static BBuffer allocate(String msg) { + BBuffer bc = allocate(); + byte[] data = msg.getBytes(UTF8); + bc.append(data, 0, data.length); + return bc; + } + + public static BBuffer wrapper(String msg) { + BBuffer bc = new IOBucketWrap(); + byte[] data = msg.getBytes(UTF8); + bc.setBytes(data, 0, data.length); + return bc; + } + + public static BBuffer wrapper() { + return new IOBucketWrap(); + } + + public static BBuffer wrapper(BBuffer bb) { + BBuffer res = new IOBucketWrap(); + res.setBytes(bb.array(), bb.position(), bb.remaining()); + return res; + } + + public static BBuffer wrapper(byte b[], int off, int len) { + BBuffer res = new IOBucketWrap(); + res.setBytes(b, off, len); + return res; + } + + public static BBuffer wrapper(BBucket bb, int start, int len) { + BBuffer res = new IOBucketWrap(); + res.setBytes(bb.array(), bb.position() + start, len); + return res; + } + + /** + * Creates a new, uninitialized ByteChunk object. + */ + private BBuffer() { + } + + public void append(BBuffer src) { + append(src.array(), src.getStart(), src.getLength()); + } + + /** + * Add data to the buffer + */ + public void append(byte src[], int off, int len) { + // will grow, up to limit + makeSpace(len); + + // assert: makeSpace made enough space + System.arraycopy(src, off, buff, end, len); + end += len; + return; + } + + // -------------------- Adding data to the buffer -------------------- + /** + * Append a char, by casting it to byte. This IS NOT intended for unicode. + * + * @param c + */ + public void append(char c) { + put((byte) c); + } + + // -------------------- Removing data from the buffer -------------------- + + /** + * Returns the message bytes. + */ + @Override + public byte[] array() { + return buff; + } + + public int capacity() { + return buff.length; + } + + public boolean equals(BBuffer bb) { + return equals(bb.array(), bb.getStart(), bb.getLength()); + } + + public boolean equals(byte b2[], int off2, int len2) { + byte b1[] = buff; + if (b1 == null && b2 == null) + return true; + + int len = end - start; + if (len2 != len || b1 == null || b2 == null) + return false; + + int off1 = start; + + while (len-- > 0) { + if (b1[off1++] != b2[off2++]) { + return false; + } + } + return true; + } + + + public boolean equals(char c2[], int off2, int len2) { + // XXX works only for enc compatible with ASCII/UTF !!! + byte b1[] = buff; + if (c2 == null && b1 == null) + return true; + + if (b1 == null || c2 == null || end - start != len2) { + return false; + } + int off1 = start; + int len = end - start; + + while (len-- > 0) { + if ((char) b1[off1++] != c2[off2++]) { + return false; + } + } + return true; + } + + // -------------------- Conversion and getters -------------------- + + /** + * Compares the message bytes to the specified String object. + * + * @param s + * the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + // XXX ENCODING - this only works if encoding is UTF8-compat + // ( ok for tomcat, where we compare ascii - header names, etc )!!! + + byte[] b = buff; + int blen = end - start; + if (b == null || blen != s.length()) { + return false; + } + int boff = start; + for (int i = 0; i < blen; i++) { + if (b[boff++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Compares the message bytes to the specified String object. + * + * @param s + * the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + byte[] b = buff; + int blen = end - start; + if (b == null || blen != s.length()) { + return false; + } + int boff = start; + for (int i = 0; i < blen; i++) { + if (toLower(b[boff++]) != toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + public byte get(int off) { + if (start + off >= end) { + throw new ArrayIndexOutOfBoundsException(); + } + return buff[start + off]; + } + + /** + * Return a byte buffer. Changes in the ByteBuffer position will + * not be reflected in the IOBucket + * @return + */ + public ByteBuffer getByteBuffer() { + if (byteBuffer == null || byteBuffer.array() != buff) { + byteBuffer = ByteBuffer.wrap(buff, start, end - start); + } else { + byteBuffer.position(start); + byteBuffer.limit(end); + } + return byteBuffer; + } + + // -------------------- + public BBuffer getClone() { + try { + return (BBuffer) this.clone(); + } catch (Exception ex) { + return null; + } + } + + public int getEnd() { + return end; + } + + public int getInt() { + return parseInt(buff, start, end - start); + } + /** + * Returns the length of the bytes. XXX need to clean this up + */ + public int getLength() { + return end - start; + } + + public long getLong() { + return parseLong(buff, start, end - start); + } + + public int getOffset() { + return start; + } + + // -------------------- equals -------------------- + + /** + * Returns the start offset of the bytes. For output this is the end of the + * buffer. + */ + public int getStart() { + return start; + } + + public ByteBuffer getWriteByteBuffer(int space) { + if (space == 0) { + space = 16; + } + makeSpace(space); + if (byteBuffer == null || byteBuffer.array() != buff) { + byteBuffer = ByteBuffer.wrap(buff, end, buff.length); + } else { + byteBuffer.position(end); + byteBuffer.limit(buff.length); + } + return byteBuffer; + } + + // -------------------- Hash code -------------------- + public int hashCode() { + return hashBytes(buff, start, end - start); + } + + public boolean hasLFLF() { + return hasLFLF(this); + } + + public boolean hasRemaining() { + return start < end; + } + + /** + * Returns true if the message bytes starts with the specified string. + * + * @param s + * the string + */ +// public boolean startsWith(String s) { +// // Works only if enc==UTF +// byte[] b = buff; +// int blen = s.length(); +// if (b == null || blen > end - start) { +// return false; +// } +// int boff = start; +// for (int i = 0; i < blen; i++) { +// if (b[boff++] != s.charAt(i)) { +// return false; +// } +// } +// return true; +// } + + /* Returns true if the message bytes start with the specified byte array */ +// public boolean startsWith(byte[] b2) { +// byte[] b1 = buff; +// if (b1 == null && b2 == null) { +// return true; +// } +// +// int len = end - start; +// if (b1 == null || b2 == null || b2.length > len) { +// return false; +// } +// for (int i = start, j = 0; i < end && j < b2.length;) { +// if (b1[i++] != b2[j++]) +// return false; +// } +// return true; +// } + + /** + * Returns true if the message bytes starts with the specified string. + * + * @param c + * the character + * @param starting + * The start position + */ + public int indexOf(char c, int starting) { + int ret = indexOf(buff, start + starting, end, c); + return (ret >= start) ? ret - start : -1; + } + + /** + * Returns true if the message bytes starts with the specified string. + * + * @param s + * the string + * @param pos + * The position + */ +// public boolean startsWithIgnoreCase(String s, int pos) { +// byte[] b = buff; +// int len = s.length(); +// if (b == null || len + pos > end - start) { +// return false; +// } +// int off = start + pos; +// for (int i = 0; i < len; i++) { +// if (Ascii.toLower(b[off++]) != Ascii.toLower(s.charAt(i))) { +// return false; +// } +// } +// return true; +// } + + public int indexOf(String src, int srcOff, int srcLen, int myOff) { + char first = src.charAt(srcOff); + + // Look for first char + int srcEnd = srcOff + srcLen; + + for (int i = myOff + start; i <= (end - srcLen); i++) { + if (buff[i] != first) + continue; + // found first char, now look for a match + int myPos = i + 1; + for (int srcPos = srcOff + 1; srcPos < srcEnd;) { + if (buff[myPos++] != src.charAt(srcPos++)) + break; + if (srcPos == srcEnd) + return i - start; // found it + } + } + return -1; + } + + // hash ignoring case +// public int hashIgnoreCase() { +// return hashBytesIC(buff, start, end - start); +// } + + public boolean isNull() { + return start == end; + } + +// private static int hashBytesIC(byte bytes[], int start, int bytesLen) { +// int max = start + bytesLen; +// byte bb[] = bytes; +// int code = 0; +// for (int i = start; i < max; i++) { +// code = code * 37 + Ascii.toLower(bb[i]); +// } +// return code; +// } + + @Override + public int limit() { + return end; + } + + public void limit(int newEnd) { + end = newEnd; + } + + /** + * Make space for len chars. + * If len is small, allocate a reserve space too. + */ + public BBuffer makeSpace(int count) { + byte[] tmp = null; + + int newSize; + int desiredSize = end + count; + + if (buff == null) { + if (desiredSize < 16) + desiredSize = 16; // take a minimum + buff = new byte[desiredSize]; + start = 0; + end = 0; + return this; + } + + // limit < buf.length ( the buffer is already big ) + // or we already have space XXX + if (desiredSize <= buff.length) { + return this; + } + // grow in larger chunks + if (desiredSize < 2 * buff.length) { + newSize = buff.length * 2; + tmp = new byte[newSize]; + } else { + newSize = buff.length * 2 + count; + tmp = new byte[newSize]; + } + + System.arraycopy(buff, start, tmp, 0, end - start); + buff = tmp; + tmp = null; + end = end - start; + start = 0; + return this; + } + +// /** +// * Find a character, no side effects. +// * +// * @return index of char if found, -1 if not +// */ +// public static int findChars(byte buf[], int start, int end, byte c[]) { +// int clen = c.length; +// int offset = start; +// while (offset < end) { +// for (int i = 0; i < clen; i++) +// if (buf[offset] == c[i]) { +// return offset; +// } +// offset++; +// } +// return -1; +// } + +// /** +// * Find the first character != c +// * +// * @return index of char if found, -1 if not +// */ +// public static int findNotChars(byte buf[], int start, int end, byte c[]) { +// int clen = c.length; +// int offset = start; +// boolean found; +// +// while (offset < end) { +// found = true; +// for (int i = 0; i < clen; i++) { +// if (buf[offset] == c[i]) { +// found = false; +// break; +// } +// } +// if (found) { // buf[offset] != c[0..len] +// return offset; +// } +// offset++; +// } +// return -1; +// } + + @Override + public int position() { + return start; + } + + + + @Override + public void position(int newStart) { + start = newStart; + } + + public void put(byte b) { + makeSpace(1); + buff[end++] = b; + } + + public int read(BBuffer res) { + res.setBytes(buff, start, remaining()); + end = start; + return res.remaining(); + } + + /** + * Read a chunk from is. + * + * You don't need to use buffered input stream, we do the + * buffering. + */ + public int read(InputStream is) throws IOException { + makeSpace(1024); + int res = is.read(buff, end, buff.length - end); + if (res > 0) { + end += res; + } + return res; + } + + public int readAll(InputStream is) throws IOException { + int size = 0; + while (true) { + int res = read(is); + if (res < 0) { + return size; + } + size += res; + } + } + + public int readByte() { + if (start == end) { + return -1; + } + return buff[start++]; + } + + + /** + * Read a line - excluding the line terminator, which is consummed as + * well but not included in the response. + * + * Line can end with CR, LF or CR/LF + * + * @param res + * @return number of bytes read, or -1 if line ending not found in buffer. + */ + public int readLine(BBuffer res) { + int cstart = start; + while(start < end) { + byte chr = buff[start++]; + if (chr == CR || chr == LF) { + res.setBytes(buff, cstart, start - cstart -1); + if (chr == CR) { + if (start < end) { + byte chr2 = buff[start]; + if (chr2 == LF) { + start++; + } + } + } + return res.remaining(); + } + } + start = cstart; + return -1; + } + /** + * Consume up to but not including delim. + * + */ + public final int readToDelimOrSpace(byte delim, + BBuffer res) { + int resStart = start; + while (true) { + if (start >= end) { + break; + } + byte chr = buff[start]; + if (chr == delim || chr == SP || chr == HT) { + break; + } + start++; + } + res.setBytes(buff, resStart, start - resStart); + return res.remaining(); + } + + + /** + * Consume all up to the first space or \t, which will be the + * first character in the buffer. + * + * Consumed data is wrapped in res. + */ + public int readToSpace(BBuffer res) { + int resStart = start; + while (true) { + if (start >= end) { + break; + } + if (buff[start] == SP + || buff[start] == HT) { + break; + } + start++; + } + res.setBytes(buff, resStart, start - resStart); + return res.remaining(); + } + /** + * Resets the message buff to an uninitialized state. + */ + public void recycle() { + start = 0; + end = 0; + } + @Override + public void release() { +// synchronized (this) { +// useCount--; +// if (useCount == -1) { +// // all slices have been released - +// // TODO: callback, return to pool +// } +// } + } + public int remaining() { + return end - start; + } + + public void reset() { + buff = null; + } + + // -------------------- Setup -------------------- + /** + * Sets the message bytes to the specified subarray of bytes. + * + * @param b + * the ascii bytes + * @param off + * the start offset of the bytes + * @param len + * the length of the bytes + */ + public void setBytes(byte[] b, int off, int len) { + throw new RuntimeException("Can't setBytes on allocated buffer"); + } + + public void wrap(BBucket b) { + setBytes(b.array(), b.position(), b.remaining()); + } + + public void wrap(ByteBuffer b) { + setBytes(b.array(), b.position(), b.remaining()); + } + + protected void setBytesInternal(byte[] b, int off, int len) { + buff = b; + start = off; + end = start + len; + } + +// public final void lowerCase() { +// while (start < end) { +// byte chr = buff[start]; +// if ((chr >= A) && (chr <= Z)) { +// buff[start] = (byte) (chr - LC_OFFSET); +// } +// start++; +// } +// } + + public void setEnd(int i) { + end = i; + } + + /** + * The old code from MessageBytes, used for setContentLength + * and setStatus. + * TODO: just use StringBuilder, the method is faster. + */ + public void setLong(long l) { + if (array() == null) { + makeSpace(20); + } + long current = l; + byte[] buf = array(); + int start = 0; + int end = 0; + if (l == 0) { + buf[end++] = (byte) '0'; + } else if (l < 0) { + current = -l; + buf[end++] = (byte) '-'; + } + while (current > 0) { + int digit = (int) (current % 10); + current = current / 10; + buf[end++] = Hex.HEX[digit]; + } + setOffset(0); + setEnd(end); + // Inverting buffer + end--; + if (l < 0) { + start++; + } + while (end > start) { + byte temp = buf[start]; + buf[start] = buf[end]; + buf[end] = temp; + start++; + end--; + } + } + + public void setOffset(int off) { + if (end < off) + end = off; + start = off; + } + + + public int skipEmptyLines() { + int resStart = start; + while (buff[start] == CR || buff[start] == LF) { + start++; + if (start == end) { + break; + } + } + return start - resStart; + } + + public int skipSpace() { + int cstart = start; + while (true) { + if (start >= end) { + return start - cstart; + } + if ((buff[start] == SP) || (buff[start] == HT)) { + start++; + } else { + return start - cstart; + } + } + } + + public int substract() { + + if ((end - start) == 0) { + return -1; + } + + return (buff[start++] & 0xFF); + + } + + public int substract(BBuffer src) { + + if ((end - start) == 0) { + return -1; + } + + int len = getLength(); + src.append(buff, start, len); + start = end; + return len; + + } + + public int substract(byte src[], int off, int len) { + + if ((end - start) == 0) { + return -1; + } + + int n = len; + if (len > getLength()) { + n = getLength(); + } + System.arraycopy(buff, start, src, off, n); + start += n; + return n; + + } + + public String toString() { + return toString(DEFAULT_CHARACTER_ENCODING); + } + + public String toString(String enc) { + if (null == buff) { + return null; + } else if (end - start == 0) { + return ""; + } + + String strValue = null; + try { + if (enc == null) { + enc = DEFAULT_CHARACTER_ENCODING; + } + + strValue = new String(buff, start, end - start, enc); + /* + * Does not improve the speed too much on most systems, it's safer + * to use the "clasical" new String(). + * + * Most overhead is in creating char[] and copying, the internal + * implementation of new String() is very close to what we do. The + * decoder is nice for large buffers and if we don't go to String ( + * so we can take advantage of reduced GC) + * + * // Method is commented out, in: return B2CConverter.decodeString( + * enc ); + */ + } catch (java.io.UnsupportedEncodingException e) { + // Use the platform encoding in that case; the usage of a bad + // encoding will have been logged elsewhere already + strValue = new String(buff, start, end - start); + } + return strValue; + } + + public void wrapTo(BBuffer res) { + res.setBytes(buff, start, remaining()); + } + + /** + * Convert specified String to a byte array. This ONLY WORKS for ascii, UTF + * chars will be truncated. + * + * @param value + * to convert to byte array + * @return the byte array value + */ + public static final byte[] convertToBytes(String value) { + byte[] result = new byte[value.length()]; + for (int i = 0; i < value.length(); i++) { + result[i] = (byte) value.charAt(i); + } + return result; + } + + /** + * Find a character, no side effects. + * + * @return index of char if found, -1 if not + */ + public static int findChar(byte buf[], int start, int end, char c) { + byte b = (byte) c; + int offset = start; + while (offset < end) { + if (buf[offset] == b) { + return offset; + } + offset++; + } + return -1; + } + private static int hashBytes(byte buff[], int start, int bytesLen) { + int max = start + bytesLen; + byte bb[] = buff; + int code = 0; + for (int i = start; i < max; i++) { + code = code * 31 + bb[i]; + // TODO: if > 0x7F, convert to chars / switch to UTF8 + } + return code; + } + + public static boolean hasLFLF(BBucket bucket) { + int pos = bucket.position(); + int lastValid = bucket.limit(); + byte[] buf = bucket.array(); + + for (int i = pos; i < lastValid; i++) { + byte chr = buf[i]; + if (chr == LF) { + if (i + 1 < lastValid && buf[i + 1] == CR) { + // \n\r\n + i++; + } + if (i + 1 < lastValid && buf[i + 1] == LF) { + return true; // \n\n + } + } else if (chr == CR) { + if (i + 1 < lastValid && buf[i + 1] == CR) { + return true; // \r\r + } + if (i + 1 < lastValid && buf[i + 1] == LF) { + // \r\n + i++; // skip LF + if (i + 1 < lastValid && buf[i + 1] == CR && + i + 2 < lastValid && buf[i + 2] == LF) { + i++; + return true; + } + } + + } + } + return false; + } + + public static int indexOf(byte bytes[], int off, int end, char qq) { + // Works only for UTF + while (off < end) { + byte b = bytes[off]; + if (b == qq) + return off; + off++; + } + return -1; + } + + /** + * Returns true if the specified ASCII character is a digit. + */ + + public static boolean isDigit(int c) { + return isDigit[c & 0xff]; + } + + /** + * Parses an unsigned integer from the specified subarray of bytes. + * @param b the bytes to parse + * @param off the start offset of the bytes + * @param len the length of the bytes + * @exception NumberFormatException if the integer format was invalid + */ + public static int parseInt(byte[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + int n = c - '0'; + + while (--len > 0) { + if (!isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + n = n * 10 + c - '0'; + } + + return n; + } + + /** + * Parses an unsigned long from the specified subarray of bytes. + * @param b the bytes to parse + * @param off the start offset of the bytes + * @param len the length of the bytes + * @exception NumberFormatException if the long format was invalid + */ + public static long parseLong(byte[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + long n = c - '0'; + long m; + + while (--len > 0) { + if (!isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + m = n * 10 + c - '0'; + + if (m < n) { + // Overflow + throw new NumberFormatException(); + } else { + n = m; + } + } + + return n; + } + + + + /** + * Returns the lower case equivalent of the specified ASCII character. + */ + public static int toLower(int c) { + if (c > 0x7f) return c; + return toLower[c & 0xff] & 0xff; + } + + /** + * Returns true if the specified ASCII character is upper case. + */ + + public static boolean isUpper(int c) { + return c < 0x7f && isUpper[c]; + } + + /** + * A slice of a bucket, holding reference to a parent bucket. + * + * This is used when a filter splits a bucket - the original + * will be replaced with 1 or more slices. When all slices are + * released, the parent will also be released. + * + * It is not possible to add data. + * + * @author Costin Manolache + */ + static class IOBucketWrap extends BBuffer { + //IOBucket parent; + + + public BBuffer makeSpace(int count) { + throw new RuntimeException("Attempting to change buffer " + + "on a wrapped BBuffer"); + } + + public void release() { +// if (parent != null) { +// parent.release(); +// } + } + + public void setBytes(byte[] b, int off, int len) { + super.setBytesInternal(b, off, len); + } + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BufferedIOReader.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BufferedIOReader.java new file mode 100644 index 000000000..6f7dd0d93 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/BufferedIOReader.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.lite.io; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.CharBuffer; + + +/** + * Cut&pasted from Harmony buffered reader ( apache license ). + * Changes: + * - additional method to recycle to avoid re-allocating on + * each request. + */ +public class BufferedIOReader extends BufferedReader { + + // Not recycled - the buffer is tied to the message/IOReader + IOReader in; + + private String enc; + boolean closed; + private char[] buf; + private int marklimit = -1; + + private int count; + + private int markpos = -1; + + private int pos; + + public BufferedIOReader(IOReader realReader) { + // we're not using super - we override all methods, but need the + // signature + super(DUMMY_READER, 1); + this.in = realReader; + buf = new char[8192]; + } + + public void recycle() { + enc = null; + closed = false; + + if (in != null) { + in.recycle(); + } + marklimit = -1; + count = 0; + markpos = -1; + pos = 0; + } + + private void checkClosed() throws IOException { + if (closed) throw new IOException("closed"); + } + + public int read(CharBuffer target) throws IOException { + checkClosed(); + int len = target.remaining(); + int n = read(target.array(), target.position(), target.remaining()); + if (n > 0) + target.position(target.position() + n); + return n; + } + + + public int read(char[] cbuf) throws IOException { + return read(cbuf, 0, cbuf.length); + } + + + /** + * Closes this reader. This implementation closes the buffered source reader + * and releases the buffer. Nothing is done if this reader has already been + * closed. + * + * @throws IOException + * if an error occurs while closing this reader. + */ + @Override + public void close() throws IOException { + synchronized (lock) { + if (!isClosed()) { + in.close(); + closed = true; + // buf remains + } + } + } + + private int fillbuf() throws IOException { + if (markpos == -1 || (pos - markpos >= marklimit)) { + /* Mark position not set or exceeded readlimit */ + int result = in.read(buf, 0, buf.length); + if (result > 0) { + markpos = -1; + pos = 0; + count = result == -1 ? 0 : result; + } + return result; + } + if (markpos == 0 && marklimit > buf.length) { + /* Increase buffer size to accommodate the readlimit */ + int newLength = buf.length * 2; + if (newLength > marklimit) { + newLength = marklimit; + } + char[] newbuf = new char[newLength]; + System.arraycopy(buf, 0, newbuf, 0, buf.length); + buf = newbuf; + } else if (markpos > 0) { + System.arraycopy(buf, markpos, buf, 0, buf.length - markpos); + } + + /* Set the new position and mark position */ + pos -= markpos; + count = markpos = 0; + int charsread = in.read(buf, pos, buf.length - pos); + count = charsread == -1 ? pos : pos + charsread; + return charsread; + } + + private boolean isClosed() { + return closed; + } + + @Override + public void mark(int readlimit) throws IOException { + if (readlimit < 0) { + throw new IllegalArgumentException(); + } + synchronized (lock) { + checkClosed(); + marklimit = readlimit; + markpos = pos; + } + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public int read() throws IOException { + synchronized (lock) { + checkClosed(); + /* Are there buffered characters available? */ + if (pos < count || fillbuf() != -1) { + return buf[pos++]; + } + markpos = -1; + return -1; + } + } + + @Override + public int read(char[] buffer, int offset, int length) throws IOException { + synchronized (lock) { + checkClosed(); + if (offset < 0 || offset > buffer.length - length || length < 0) { + throw new IndexOutOfBoundsException(); + } + if (length == 0) { + return 0; + } + int required; + if (pos < count) { + /* There are bytes available in the buffer. */ + int copylength = count - pos >= length ? length : count - pos; + System.arraycopy(buf, pos, buffer, offset, copylength); + pos += copylength; + if (copylength == length || !in.ready()) { + return copylength; + } + offset += copylength; + required = length - copylength; + } else { + required = length; + } + + while (true) { + int read; + /* + * If we're not marked and the required size is greater than the + * buffer, simply read the bytes directly bypassing the buffer. + */ + if (markpos == -1 && required >= buf.length) { + read = in.read(buffer, offset, required); + if (read == -1) { + return required == length ? -1 : length - required; + } + } else { + if (fillbuf() == -1) { + return required == length ? -1 : length - required; + } + read = count - pos >= required ? required : count - pos; + System.arraycopy(buf, pos, buffer, offset, read); + pos += read; + } + required -= read; + if (required == 0) { + return length; + } + if (!in.ready()) { + return length - required; + } + offset += read; + } + } + } + + /** + * Returns the next line of text available from this reader. A line is + * represented by zero or more characters followed by {@code '\n'}, + * {@code '\r'}, {@code "\r\n"} or the end of the reader. The string does + * not include the newline sequence. + * + * @return the contents of the line or {@code null} if no characters were + * read before the end of the reader has been reached. + * @throws IOException + * if this reader is closed or some other I/O error occurs. + */ + public String readLine() throws IOException { + synchronized (lock) { + checkClosed(); + /* Are there buffered characters available? */ + if ((pos >= count) && (fillbuf() == -1)) { + return null; + } + for (int charPos = pos; charPos < count; charPos++) { + char ch = buf[charPos]; + if (ch > '\r') { + continue; + } + if (ch == '\n') { + String res = new String(buf, pos, charPos - pos); + pos = charPos + 1; + return res; + } else if (ch == '\r') { + String res = new String(buf, pos, charPos - pos); + pos = charPos + 1; + if (((pos < count) || (fillbuf() != -1)) + && (buf[pos] == '\n')) { + pos++; + } + return res; + } + } + + char eol = '\0'; + StringBuilder result = new StringBuilder(80); + /* Typical Line Length */ + + result.append(buf, pos, count - pos); + pos = count; + while (true) { + /* Are there buffered characters available? */ + if (pos >= count) { + if (eol == '\n') { + return result.toString(); + } + // attempt to fill buffer + if (fillbuf() == -1) { + // characters or null. + return result.length() > 0 || eol != '\0' ? result + .toString() : null; + } + } + for (int charPos = pos; charPos < count; charPos++) { + if (eol == '\0') { + if ((buf[charPos] == '\n' || buf[charPos] == '\r')) { + eol = buf[charPos]; + } + } else if (eol == '\r' && (buf[charPos] == '\n')) { + if (charPos > pos) { + result.append(buf, pos, charPos - pos - 1); + } + pos = charPos + 1; + return result.toString(); + } else { + if (charPos > pos) { + result.append(buf, pos, charPos - pos - 1); + } + pos = charPos; + return result.toString(); + } + } + if (eol == '\0') { + result.append(buf, pos, count - pos); + } else { + result.append(buf, pos, count - pos - 1); + } + pos = count; + } + } + + } + + + @Override + public boolean ready() throws IOException { + synchronized (lock) { + checkClosed(); + return ((count - pos) > 0) || in.ready(); + } + } + + @Override + public void reset() throws IOException { + synchronized (lock) { + checkClosed(); + if (markpos == -1) { + throw new IOException("No mark"); + } + pos = markpos; + } + } + + @Override + public long skip(long amount) throws IOException { + if (amount < 0) { + throw new IllegalArgumentException(); + } + synchronized (lock) { + checkClosed(); + if (amount < 1) { + return 0; + } + if (count - pos >= amount) { + pos += amount; + return amount; + } + + long read = count - pos; + pos = count; + while (read < amount) { + if (fillbuf() == -1) { + return read; + } + if (count - pos >= amount - read) { + pos += amount - read; + return amount; + } + // Couldn't get all the characters, skip what we read + read += (count - pos); + pos = count; + } + return amount; + } + } + + private static Reader DUMMY_READER = new Reader() { + @Override + public void close() throws IOException { + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + return 0; + } + }; + + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBucket.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBucket.java new file mode 100644 index 000000000..e28d16a6a --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBucket.java @@ -0,0 +1,510 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.Serializable; +import java.nio.CharBuffer; + +/** + * Wraps a char[]. + * + * Doesn't provide any mutation methods. Classes in this package + * have access to the buffer, for conversions. + * + * + * @author Costin Manolache + */ +public class CBucket implements CharSequence, Comparable, Serializable { + protected char value[]; + + protected int start; + + protected int end; + + // Reused. + protected CharBuffer cb; + + // cache + protected String strValue; + protected int hash; + + public CBucket() { + } + + /** + * Used by IOWriter for conversion. Will not modify the content. + */ + CharBuffer getNioBuffer() { + if (cb == null || cb.array() != value) { + cb = CharBuffer.wrap(value, start, end - start); + } else { + cb.position(start); + cb.limit(end); + } + return cb; + } + + public void recycle() { + start = 0; + end = 0; + value = null; + strValue = null; + hash = 0; + } + + public String toString() { + if (null == value) { + return null; + } else if (end - start == 0) { + return ""; + } + if (strValue == null) { + strValue = new String(value, start, end - start); + } + return strValue; + } + + /** + * Same as String + */ + public int hashCode() { + int h = hash; + if (h == 0) { + int off = start; + char val[] = value; + + for (int i = start; i < end; i++) { + h = 31*h + val[off++]; + } + hash = h; + } + return h; + } + + public long getLong() { + return parseLong(value, start, end - start); + } + + public int getInt() { + return parseInt(value, start, end - start); + } + + public static int parseInt(char[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !BBuffer.isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + int n = c - '0'; + + while (--len > 0) { + if (!BBuffer.isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + n = n * 10 + c - '0'; + } + + return n; + } + + + public static long parseLong(char[] b, int off, int len) + throws NumberFormatException + { + int c; + + if (b == null || len <= 0 || !BBuffer.isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + + long n = c - '0'; + long m; + + while (--len > 0) { + if (!BBuffer.isDigit(c = b[off++])) { + throw new NumberFormatException(); + } + m = n * 10 + c - '0'; + + if (m < n) { + // Overflow + throw new NumberFormatException(); + } else { + n = m; + } + } + + return n; + } + + + + + /** + * Compares the message bytes to the specified String object. + * + * @param s + * the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equals(String s) { + char[] c = value; + int len = end - start; + if (c == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Compares the message bytes to the specified String object. + * + * @param s + * the String to compare + * @return true if the comparison succeeded, false otherwise + */ + public boolean equalsIgnoreCase(String s) { + char[] c = value; + int len = end - start; + if (c == null || len != s.length()) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (BBuffer.toLower(c[off++]) != BBuffer.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + public boolean equals(Object obj) { + if (obj instanceof CBuffer) { + CBuffer cc = (CBuffer) obj; + return equals(cc.value, cc.start, cc.length()); + } else if (obj instanceof String) { + return equals((String)obj); + } + return false; + } + + public boolean equals(char b2[], int off2, int len2) { + char b1[] = value; + if (b1 == null && b2 == null) + return true; + + if (b1 == null || b2 == null || end - start != len2) { + return false; + } + int off1 = start; + int len = end - start; + while (len-- > 0) { + if (b1[off1++] != b2[off2++]) { + return false; + } + } + return true; + } + + public boolean equals(byte b2[], int off2, int len2) { + char b1[] = value; + if (b2 == null && b1 == null) + return true; + + if (b1 == null || b2 == null || end - start != len2) { + return false; + } + int off1 = start; + int len = end - start; + + while (len-- > 0) { + if (b1[off1++] != (char) b2[off2++]) { + return false; + } + } + return true; + } + + + /** + * Returns true if the message bytes starts with the specified string. + * + * @param s + * the string + */ + public boolean startsWith(String s) { + char[] c = value; + int len = s.length(); + if (c == null || len > end - start) { + return false; + } + int off = start; + for (int i = 0; i < len; i++) { + if (c[off++] != s.charAt(i)) { + return false; + } + } + return true; + } + + /** + * Returns true if the message bytes starts with the specified string. + * + * @param s + * the string + */ + public boolean startsWithIgnoreCase(String s, int pos) { + char[] c = value; + int len = s.length(); + if (c == null || len + pos > end - start) { + return false; + } + int off = start + pos; + for (int i = 0; i < len; i++) { + if (BBuffer.toLower(c[off++]) != BBuffer.toLower(s.charAt(i))) { + return false; + } + } + return true; + } + + public int indexOf(char c) { + return indexOf(c, start); + } + + public int lastIndexOf(char c) { + return lastIndexOf(c, 0, end - start); + } + + /** + */ + public int lastIndexOf(char c, int off, int len) { + char[] buf = value; + int slash = -1; + for (int i = start + len - 1; i >= start + off; i--) { + if (buf[i] == c) { + slash = i - start; + break; + } + } + return slash; + } + + /** + * Returns true if the message bytes starts with the specified string. + * + * @param c + * the character + */ + public int indexOf(char c, int starting) { + int ret = indexOf(value, start + starting, end, c); + return (ret >= start) ? ret - start : -1; + } + + public static int indexOf(char chars[], int off, int cend, char qq) { + while (off < cend) { + char b = chars[off]; + if (b == qq) + return off; + off++; + } + return -1; + } + + public int indexOf(String src) { + return indexOf(src, 0, src.length(), 0); + } + + public int indexOf(String src, int srcOff, int srcLen, int myOff) { + char first = src.charAt(srcOff); + + // Look for first char + int srcEnd = srcOff + srcLen; + + for (int i = myOff + start; i <= (end - srcLen); i++) { + if (value[i] != first) + continue; + // found first char, now look for a match + int myPos = i + 1; + for (int srcPos = srcOff + 1; srcPos < srcEnd;) { + if (value[myPos++] != src.charAt(srcPos++)) + break; + if (srcPos == srcEnd) + return i - start; // found it + } + } + return -1; + } + + public char lastChar() { + return value[end - 1]; + } + + public char charAt(int index) { + return value[index + start]; + } + + public void wrap(char[] buff, int start, int end) { + if (value != null) { + throw new RuntimeException("Can wrap only once"); + } + this.value = buff; + this.start = start; + this.end = end; + } + + public CharSequence subSequence(int sstart, int send) { + CBucket seq = new CBucket(); + seq.wrap(this.value, start + sstart, start + send); + return seq; + } + + public int length() { + return end - start; + } + + @Override + public int compareTo(Object o) { + // Code based on Harmony + if (o instanceof CBuffer) { + CBuffer dest = (CBuffer) o; + int o1 = start, o2 = dest.start, result; + int len = end - start; + int destLen = dest.end - dest.start; + int fin = (len < destLen ? + end : start + destLen); + char[] target = dest.value; + while (o1 < fin) { + if ((result = value[o1++] - target[o2++]) != 0) { + return result; + } + } + return len - destLen; + + } else if (o instanceof CharSequence) { + CharSequence dest = (CharSequence) o; + int o1 = start, o2 = 0, result; + int len = end - start; + int destLen = dest.length(); + int fin = (len < destLen ? + end : start + destLen); + while (o1 < fin) { + if ((result = value[o1++] - dest.charAt(o2++)) != 0) { + return result; + } + } + return len - destLen; + + } else { + throw new RuntimeException("CompareTo not supported " + o); + } + } + + /** + * Compare given char chunk with String ignoring case. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + public final int compareIgnoreCase(String compareTo) { + int result = 0; + char[] c = value; + int len = compareTo.length(); + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (BBuffer.toLower(c[i + start]) > BBuffer.toLower(compareTo.charAt(i))) { + result = 1; + } else if (BBuffer.toLower(c[i + start]) < BBuffer.toLower(compareTo.charAt(i))) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length() > (end - start)) { + result = -1; + } else if (compareTo.length() < (end - start)) { + result = 1; + } + } + return result; + } + + /** + * Compare given char chunk with String. + * Return -1, 0 or +1 if inferior, equal, or superior to the String. + */ + public final int compare(String compareTo) { + int result = 0; + char[] c = value; + int len = compareTo.length(); + if ((end - start) < len) { + len = end - start; + } + for (int i = 0; (i < len) && (result == 0); i++) { + if (c[i + start] > compareTo.charAt(i)) { + result = 1; + } else if (c[i + start] < compareTo.charAt(i)) { + result = -1; + } + } + if (result == 0) { + if (compareTo.length() > (end - start)) { + result = -1; + } else if (compareTo.length() < (end - start)) { + result = 1; + } + } + return result; + } + + public int getExtension(CBuffer ext, char slashC, char dotC) { + int slash = lastIndexOf(slashC); + if (slash < 0) { + slash = 0; + } + int dot = lastIndexOf(dotC, slash, length()); + if (dot < 0) { + return -1; + } + ext.wrap(this, dot + 1, length()); + return dot; + } + + /** + * Find the position of the nth slash, in the given char chunk. + */ + public final int nthSlash(int n) { + char[] c = value; + int pos = start; + int count = 0; + + while (pos < end) { + if ((c[pos++] == '/') && ((++count) == n)) { + pos--; + break; + } + } + + return pos - start; + } + + + public boolean hasUpper() { + for (int i = start; i < end; i++) { + char c = value[i]; + if (c < 0x7F && BBuffer.isUpper(c)) { + return true; + } + } + return false; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBuffer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBuffer.java new file mode 100644 index 000000000..bfc36f94a --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/CBuffer.java @@ -0,0 +1,371 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.CharBuffer; + + +/** + * Similar with StringBuilder or StringBuffer, but with access to the + * raw buffer - this avoids copying the data. + * + * Utilities to manipluate char chunks. While String is the easiest way to + * manipulate chars ( search, substrings, etc), it is known to not be the most + * efficient solution - Strings are designed as imutable and secure objects. + * + * @author dac@sun.com + * @author James Todd [gonzo@sun.com] + * @author Costin Manolache + * @author Remy Maucherat + */ +public class CBuffer extends CBucket implements Cloneable, + Appendable { + + + /** + * Creates a new, uninitialized CharChunk object. + */ + public static CBuffer newInstance() { + return new CBuffer(); + } + + private CBuffer() { + } + + /** + * Resets the message bytes to an uninitialized state. + */ + public void recycle() { + dirty(); + start = 0; + end = 0; + } + + /** + * Same as String + */ + public int hashCode() { + int h = 0; + int off = start; + char val[] = value; + + for (int i = start; i < end; i++) { + h = 31*h + val[off++]; + } + return h; + } + + public String toString() { + if (null == value) { + return null; + } else if (end - start == 0) { + return ""; + } + return new String(value, start, end - start); + } + + public void wrap(char[] buff, int start, int end) { + dirty(); + this.value = buff; + this.start = start; + this.end = end; + } + + public void wrap(CBucket buff, int off, int srcEnd) { + dirty(); + this.value = buff.value; + this.start = buff.start + off; + this.end = this.start + srcEnd - off; + } + + + // ----------- Used for IOWriter / conversion --------- + + public char[] array() { + return value; + } + + public int position() { + return start; + } + + CharBuffer getAppendCharBuffer() { + makeSpace(16); + if (cb == null || cb.array() != value) { + cb = CharBuffer.wrap(value, end, value.length - end); + } else { + cb.position(end); + cb.limit(value.length); + } + return cb; + } + + void returnNioBuffer(CharBuffer c) { + dirty(); + start = c.position(); + } + + void returnAppendCharBuffer(CharBuffer c) { + dirty(); + end = c.position(); + } + + // -------- Delete / replace --------------- + + /** + * 'Delete' all chars after offset. + * + * @param offset + */ + public void delete(int offset) { + dirty(); + end = start + offset; + } + + // -------------------- Adding data -------------------- + + /** + * Append methods take start and end - similar with this one. + * The source is not modified. + */ + @Override + public CBuffer append(CharSequence csq, int astart, int aend) + throws IOException { + makeSpace(aend - astart); + + for (int i = astart; i < aend; i++) { + value[end++] = csq.charAt(i); + } + return this; + } + + public CBuffer append(char b) { + makeSpace(1); + value[end++] = b; + return this; + } + + /** + * Add data to the buffer + */ + public CBuffer append(char src[], int srcStart, int srcEnd) { + int len = srcEnd - srcStart; + // will grow, up to limit + makeSpace(len); + + // assert: makeSpace made enough space + System.arraycopy(src, srcStart, value, end, len); + end += len; + return this; + } + + /** + * Add data to the buffer + */ + public CBuffer append(StringBuffer sb) { + int len = sb.length(); + makeSpace(len); + sb.getChars(0, len, value, end); + end += len; + return this; + } + + /** + * Append a string to the buffer + */ + public CBuffer append(String s) { + if (s == null) { + return this; + } + append(s, 0, s.length()); + return this; + } + + + + /** + * Append a string to the buffer + */ + public CBuffer append(String s, int off, int srcEnd) { + if (s == null) + return this; + + // will grow, up to limit + makeSpace(srcEnd - off); + + // assert: makeSpace made enough space + s.getChars(off, srcEnd, value, end); + end += srcEnd - off; + return this; + } + + // TODO: long, int conversions -> get from harmony Long + public CBuffer appendInt(int i) { + // TODO: copy from harmony StringBuffer + append(Integer.toString(i)); + return this; + } + + + public Appendable append(CharSequence cs) { + if (cs instanceof CBuffer) { + CBuffer src = (CBuffer) cs; + append(src.value, src.start, src.end); + } else if (cs instanceof String) { + append((String) cs); + } else { + for (int i = 0; i < cs.length(); i++) { + append(cs.charAt(i)); + } + } + return this; + } + + public CBuffer append(CBuffer src) { + append(src.value, src.start, src.end); + return this; + } + + + public CBuffer append(BBucket bb) { + byte[] bbuf = bb.array(); + int start = bb.position(); + appendAscii(bbuf, start, bb.remaining()); + return this; + } + + public CBuffer appendAscii(byte[] bbuf, int start, int len) { + makeSpace(len); + char[] cbuf = value; + for (int i = 0; i < len; i++) { + cbuf[end + i] = (char) (bbuf[i + start] & 0xff); + } + end += len; + return this; + } + + + /** + * Append and advance CharBuffer. + * + * @param c + */ + public CBuffer put(CharBuffer c) { + append(c.array(), c.position(), c.limit()); + c.position(c.limit()); + return this; + } + + // ------------- 'set' methods --------------- + // equivalent with clean + append + + public CBuffer set(CBuffer csq, int off, int len) { + recycle(); + append(csq.value, csq.start + off, csq.start + off + len); + return this; + } + + public CBuffer setChars(char[] c, int off, int len) { + recycle(); + append(c, off, off + len); + return this; + } + + public CBuffer set(BBucket bb) { + recycle(); + byte[] bbuf = bb.array(); + int start = bb.position(); + appendAscii(bbuf, start, bb.remaining()); + return this; + } + + public CBuffer set(CharSequence csq) { + recycle(); + append(csq); + return this; + } + + public CBuffer set(CBuffer csq) { + recycle(); + append(csq); + return this; + } + + public CBuffer set(String csq) { + recycle(); + append(csq); + return this; + } + + private void dirty() { + hash = 0; + strValue = null; + } + + /** + * Make space for len chars. If len is small, allocate a reserve space too. + * Never grow bigger than limit. + */ + private void makeSpace(int count) { + dirty(); + char[] tmp = null; + + int newSize; + int desiredSize = end + count; + + if (value == null) { + if (desiredSize < 256) + desiredSize = 256; // take a minimum + value = new char[desiredSize]; + } + + // limit < buf.length ( the buffer is already big ) + // or we already have space XXX + if (desiredSize <= value.length) { + return; + } + // grow in larger chunks + if (desiredSize < 2 * value.length) { + newSize = value.length * 2; + tmp = new char[newSize]; + } else { + newSize = value.length * 2 + count; + tmp = new char[newSize]; + } + + System.arraycopy(value, 0, tmp, 0, end); + value = tmp; + tmp = null; + } + + public void toLower() { + for (int i = start; i < end; i++) { + char c = value[i]; + if (c < 0x7F) { + if (BBuffer.isUpper(c)) { + value[i] = (char) BBuffer.toLower(c); + } + + } + } + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/DumpChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/DumpChannel.java new file mode 100644 index 000000000..bb8ce5f10 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/DumpChannel.java @@ -0,0 +1,93 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; + +// TODO: dump to a file, hex, etc. +/** + * For debug - will print all bytes that go trough the channel + */ +public class DumpChannel extends IOChannel { + + IOBuffer in = new IOBuffer(this); + IOBuffer out = new IOBuffer(this); + + public DumpChannel(String id) { + this.id = id; + } + + public String toString() { + return "Dump-" + id + "-" + net.toString(); + } + + @Override + public void handleReceived(IOChannel ch) throws IOException { + processInput(ch.getIn()); + } + + private void processInput(IOBuffer netIn) throws IOException { + boolean any = false; + while (true) { + BBucket first = netIn.popFirst(); + if (first == null) { + if (netIn.isClosedAndEmpty()) { + out("IN", first, true); + in.close(); + any = true; + } + if (any) { + sendHandleReceivedCallback(); + } + return; + } + any = true; + out("IN", first, false); + in.queue(first); + } + } + + public void startSending() throws IOException { + while (true) { + BBucket first = out.popFirst(); + if (first == null) { + if (out.isClosedAndEmpty()) { + out("OUT", first, true); + net.getOut().close(); + } + + net.startSending(); + return; + } + // Dump + out("OUT", first, net.getOut().isAppendClosed()); + net.getOut().queue(first); + } + } + + private void out(String dir, BBucket first, boolean closed) { + // Dump + if (first != null) { + String hd = Hex.getHexDump(first.array(), first.position(), + first.remaining(), true); + System.err.println("\n" + dir + ": " + id + " " + + (closed ? "CLS" : "") + + + first.remaining() + "\n" + + hd); + } else { + System.err.println("\n" + dir + ": " + id + " " + + (closed ? "CLS" : "") + + "END\n"); + } + } + + @Override + public IOBuffer getIn() { + return in; + } + + @Override + public IOBuffer getOut() { + return out; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FastHttpDateFormat.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FastHttpDateFormat.java new file mode 100644 index 000000000..568176789 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FastHttpDateFormat.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite.io; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Utility class to generate HTTP dates. + * + * @author Remy Maucherat + */ +public final class FastHttpDateFormat { + + + // -------------------------------------------------------------- Variables + + + protected static final int CACHE_SIZE = + Integer.parseInt(System.getProperty("org.apache.tomcat.util.http.FastHttpDateFormat.CACHE_SIZE", "1000")); + + + /** + * HTTP date format. + */ + protected static final SimpleDateFormat format = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + + + /** + * The set of SimpleDateFormat formats to use in getDateHeader(). + */ + protected static final SimpleDateFormat formats[] = { + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US), + new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US) + }; + + + protected final static TimeZone gmtZone = TimeZone.getTimeZone("GMT"); + + + /** + * GMT timezone - all HTTP dates are on GMT + */ + static { + + format.setTimeZone(gmtZone); + + formats[0].setTimeZone(gmtZone); + formats[1].setTimeZone(gmtZone); + formats[2].setTimeZone(gmtZone); + + } + + + /** + * Instant on which the currentDate object was generated. + */ + protected static long currentDateGenerated = 0L; + + + /** + * Current formatted date. + */ + protected static String currentDate = null; + + + /** + * Formatter cache. + */ + protected static final ConcurrentHashMap formatCache = + new ConcurrentHashMap(CACHE_SIZE); + + + /** + * Parser cache. + */ + protected static final ConcurrentHashMap parseCache = + new ConcurrentHashMap(CACHE_SIZE); + + + // --------------------------------------------------------- Public Methods + + + /** + * Get the current date in HTTP format. + */ + public static final String getCurrentDate() { + + long now = System.currentTimeMillis(); + if ((now - currentDateGenerated) > 1000) { + synchronized (format) { + if ((now - currentDateGenerated) > 1000) { + currentDateGenerated = now; + currentDate = format.format(new Date(now)); + } + } + } + return currentDate; + + } + + + /** + * Get the HTTP format of the specified date. + */ + public static final String formatDate + (long value, DateFormat threadLocalformat) { + + Long longValue = new Long(value); + String cachedDate = formatCache.get(longValue); + if (cachedDate != null) + return cachedDate; + + String newDate = null; + Date dateValue = new Date(value); + if (threadLocalformat != null) { + newDate = threadLocalformat.format(dateValue); + updateFormatCache(longValue, newDate); + } else { + synchronized (formatCache) { + synchronized (format) { + newDate = format.format(dateValue); + } + updateFormatCache(longValue, newDate); + } + } + return newDate; + + } + + + /** + * Try to parse the given date as a HTTP date. + */ + public static final long parseDate(String value, + DateFormat[] threadLocalformats) { + + Long cachedDate = parseCache.get(value); + if (cachedDate != null) + return cachedDate.longValue(); + + Long date = null; + if (threadLocalformats != null) { + date = internalParseDate(value, threadLocalformats); + updateParseCache(value, date); + } else { + synchronized (parseCache) { + date = internalParseDate(value, formats); + updateParseCache(value, date); + } + } + if (date == null) { + return (-1L); + } else { + return date.longValue(); + } + + } + + + /** + * Parse date with given formatters. + */ + private static final Long internalParseDate + (String value, DateFormat[] formats) { + Date date = null; + for (int i = 0; (date == null) && (i < formats.length); i++) { + try { + date = formats[i].parse(value); + } catch (ParseException e) { + ; + } + } + if (date == null) { + return null; + } + return new Long(date.getTime()); + } + + + /** + * Update cache. + */ + private static void updateFormatCache(Long key, String value) { + if (value == null) { + return; + } + if (formatCache.size() > CACHE_SIZE) { + formatCache.clear(); + } + formatCache.put(key, value); + } + + + /** + * Update cache. + */ + private static void updateParseCache(String key, Long value) { + if (value == null) { + return; + } + if (parseCache.size() > CACHE_SIZE) { + parseCache.clear(); + } + parseCache.put(key, value); + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnector.java new file mode 100644 index 000000000..5fb452524 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnector.java @@ -0,0 +1,27 @@ +/* + */ +package org.apache.tomcat.lite.io; + + +/** + * Initial abstraction for non-blocking File access and to + * support other abstraction. + * + * Tomcat uses JNDI - but that's blocking, does lots of data copy, + * is complex. + * + * Work in progress.. + */ +public abstract class FileConnector extends IOConnector { + + public static class FileInfo { + String type; + int mode; + long size; + + } + + public abstract boolean isDirectory(String path); + + public abstract boolean isFile(String path); +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnectorJavaIo.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnectorJavaIo.java new file mode 100644 index 000000000..2b6de3b30 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/FileConnectorJavaIo.java @@ -0,0 +1,49 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.File; +import java.io.IOException; + + +/** + * Catalina uses JNDI to abstract filesystem - this is both heavy and + * a bit complex. + * + * This is also a bit complex - but hopefully we can implement it as + * non-blocking and without much copy. + * + */ +public class FileConnectorJavaIo extends FileConnector { + File base; + + public FileConnectorJavaIo(File file) { + this.base = file; + } + + @Override + public boolean isDirectory(String path) { + File file = new File(base, path); + return file.isDirectory(); + } + + @Override + public boolean isFile(String path) { + File file = new File(base, path); + return file.exists() && !file.isDirectory(); + } + + @Override + public void acceptor(ConnectedCallback sc, + CharSequence port, + Object extra) throws IOException { + // TODO: unix domain socket impl. + // Maybe: detect new files in the filesystem ? + } + + @Override + public void connect(String host, int port, ConnectedCallback sc) + throws IOException { + } + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/Hex.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/Hex.java new file mode 100644 index 000000000..832b81375 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/Hex.java @@ -0,0 +1,249 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite.io; + +import java.io.ByteArrayOutputStream; + +/** + * Tables useful when converting byte arrays to and from strings of hexadecimal + * digits. + * Code from Ajp11, from Apache's JServ. + * + * @author Craig R. McClanahan + */ + +public final class Hex { + + + // -------------------------------------------------------------- Constants + + /** + * Table for HEX to DEC byte translation. + */ + public static final int[] DEC = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + + /** + * Table for DEC to HEX byte translation. + */ + public static final byte[] HEX = + { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'a', (byte) 'b', + (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f' }; + + + // --------------------------------------------------------- Static Methods + + + /** + * Convert a String of hexadecimal digits into the corresponding + * byte array by encoding each two hexadecimal digits as a byte. + * + * @param digits Hexadecimal digits representation + * + * @exception IllegalArgumentException if an invalid hexadecimal digit + * is found, or the input string contains an odd number of hexadecimal + * digits + */ + public static byte[] convert(String digits) { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + for (int i = 0; i < digits.length(); i += 2) { + char c1 = digits.charAt(i); + if ((i+1) >= digits.length()) + throw new IllegalArgumentException + ("hexUtil.odd"); + char c2 = digits.charAt(i + 1); + byte b = 0; + if ((c1 >= '0') && (c1 <= '9')) + b += ((c1 - '0') * 16); + else if ((c1 >= 'a') && (c1 <= 'f')) + b += ((c1 - 'a' + 10) * 16); + else if ((c1 >= 'A') && (c1 <= 'F')) + b += ((c1 - 'A' + 10) * 16); + else + throw new IllegalArgumentException + ("hexUtil.bad"); + if ((c2 >= '0') && (c2 <= '9')) + b += (c2 - '0'); + else if ((c2 >= 'a') && (c2 <= 'f')) + b += (c2 - 'a' + 10); + else if ((c2 >= 'A') && (c2 <= 'F')) + b += (c2 - 'A' + 10); + else + throw new IllegalArgumentException + ("hexUtil.bad"); + baos.write(b); + } + return (baos.toByteArray()); + + } + + + /** + * Convert a byte array into a printable format containing a + * String of hexadecimal digit characters (two per byte). + * + * @param bytes Byte array representation + */ + public static String convert(byte bytes[]) { + + StringBuffer sb = new StringBuffer(bytes.length * 2); + for (int i = 0; i < bytes.length; i++) { + sb.append(convertDigit((bytes[i] >> 4))); + sb.append(convertDigit((bytes[i] & 0x0f))); + } + return (sb.toString()); + + } + + + /** + * Convert 4 hex digits to an int, and return the number of converted + * bytes. + * + * @param hex Byte array containing exactly four hexadecimal digits + * + * @exception IllegalArgumentException if an invalid hexadecimal digit + * is included + */ + public static int convert2Int( byte[] hex ) { + // Code from Ajp11, from Apache's JServ + + // assert b.length==4 + // assert valid data + int len; + if(hex.length < 4 ) return 0; + if( DEC[hex[0]]<0 ) + throw new IllegalArgumentException("hexUtil.bad"); + len = DEC[hex[0]]; + len = len << 4; + if( DEC[hex[1]]<0 ) + throw new IllegalArgumentException("hexUtil.bad"); + len += DEC[hex[1]]; + len = len << 4; + if( DEC[hex[2]]<0 ) + throw new IllegalArgumentException("hexUtil.bad"); + len += DEC[hex[2]]; + len = len << 4; + if( DEC[hex[3]]<0 ) + throw new IllegalArgumentException("hexUtil.bad"); + len += DEC[hex[3]]; + return len; + } + + + + /** + * Provide a mechanism for ensuring this class is loaded. + */ + public static void load() { + // Nothing to do + } + + /** + * [Private] Convert the specified value (0 .. 15) to the corresponding + * hexadecimal digit. + * + * @param value Value to be converted + */ + private static char convertDigit(int value) { + + value &= 0x0f; + if (value >= 10) + return ((char) (value - 10 + 'a')); + else + return ((char) (value + '0')); + + } + + /** + * getHexValue displays a formatted hex + * representation of the passed byte array. It also + * allows for only a specified offset and length of + * a particular array to be returned. + * + * @param bytes byte[] array to process. + * @param pos offset to begin processing. + * @param len number of bytes to process. + * @return String formatted hex representation of processed + * array. + */ + public static String getHexDump(byte[] bytes, int pos, int len, + boolean displayOffset) { + StringBuffer out = new StringBuffer( len * 2 ); + + for (int j = 0; j < len; j += 16) { + hexLine(out, bytes, pos + j, pos + len, displayOffset); + } + + return out.toString(); + } + + private static void hexLine(StringBuffer out, + byte[] bytes, int start, int end, + boolean displayOffset) { + + if ( displayOffset ) { + out.append(convertDigit((int) (start >> 12))); + out.append(convertDigit((int) (start >> 8))); + out.append(convertDigit((int) (start >> 4))); + out.append(convertDigit(start & 0x0F)); + out.append(": "); + } + for (int i = start; i < start + 16; i++) { + + if (i < end) { + out.append(convertDigit((int) (bytes[i] >> 4))); + out.append(convertDigit(bytes[i] & 0x0F)); + out.append(" "); + } else { + out.append(" "); + } + } + + out.append(" | "); + + for (int i = start; i < start + 16 && i < end; i++) { + if( ! Character.isISOControl( (char)bytes[i] )) { + out.append( new Character((char)bytes[i]) ); + } else { + out.append( "." ); + } + } + + out.append("\n"); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOBuffer.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOBuffer.java new file mode 100644 index 000000000..135059c14 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOBuffer.java @@ -0,0 +1,666 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.http.FutureCallbacks; + +// TODO: append() will trigger callbacks - do it explicitely !!! +// TODO: queue() shouldn't modify the buffer + + +/** + * A list of data buckets. + * + * @author Costin Manolache + */ +public class IOBuffer { + static Logger log = Logger.getLogger("IOBrigade"); + + static int ALLOC_SIZE = 8192; + long defaultTimeout = Long.MAX_VALUE; + + private LinkedList buffers = new LinkedList(); + + // close() has been called for out, + // or EOF/FIN received for in. It may still have data. + boolean closeQueued; + + // Will be signalled (open) when there is data in the buffer. + // also used to sync on. + FutureCallbacks hasDataLock = new FutureCallbacks() { + protected boolean isSignaled() { + return hasData(); + } + }; + + // may be null + protected IOChannel ch; + + // Support for appending - needs improvements. + // appendable buffer is part of the buffer list if it has + // data, and kept here if empty. + BBuffer appendable; + boolean appending = false; + ByteBuffer writeBuffer; + + + public IOBuffer() { + } + + public IOBuffer(IOChannel ch) { + this.ch = ch; + } + + public IOChannel getChannel() { + return ch; + } + + // ===== Buffer access ===== + + BBucket first; + + /** + * Return first non-empty buffer. + * + * The append buffer is part of the buffer list, and is left alone and + * empty. + * + * @return + */ + public BBucket peekFirst() { + synchronized (buffers) { + BBucket o = (buffers.size() == 0) ? null : buffers.getFirst(); + + while (true) { + boolean empty = o == null || isEmpty(o); + if (o == null) { + //hasDataLock.reset(); + return null; // no data in buffers + } + // o != null + if (empty) { + buffers.removeFirst(); + o = (buffers.size() == 0) ? null : buffers.getFirst(); + } else { + first = o; + return o; + } + } + } + } + + public BBucket peekBucket(int idx) { + synchronized (buffers) { + return buffers.get(idx); + } + } + + + public void advance(int len) { + first.position(first.position() + len); + } + + public void queue(String s) throws IOException { + // TODO: decode with prober charset + byte[] bytes = s.getBytes("UTF8"); + queueInternal(BBuffer.wrapper(bytes, 0, bytes.length)); + } + + public void queue(BBuffer bc) throws IOException { + queueInternal(bc); + } + + public void queue(Object bb) throws IOException { + queueInternal(bb); + } + + private void queueInternal(Object bb) throws IOException { + if (closeQueued) { + throw new IOException("Closed"); + } + synchronized (buffers) { + if (appending) { + throw new RuntimeException("Unexpected queue while " + + "appending"); + } + BBucket add = wrap(bb); + buffers.add(add); + //log.info("QUEUED: " + add.remaining() + " " + this); + notifyDataAvailable(add); + } + + } + + public int getBufferCount() { + peekFirst(); + synchronized (buffers) { + return buffers.size(); + } + } + + public void clear() { + synchronized (buffers) { + buffers.clear(); + } + } + + public void recycle() { + closeQueued = false; + clear(); + // Normally unlocked + hasDataLock.recycle(); + + appending = false; + appendable = null; + } + + // =================== + /** + * Closed for append. It may still have data. + * @return + */ + public boolean isClosedAndEmpty() { + return closeQueued && 0 == getBufferCount(); + } + + + /** + * Mark as closed - but will not send data. + */ + public void close() throws IOException { + if (closeQueued) { + return; + } + closeQueued = true; + notifyDataAvailable(null); + } + + + private boolean isEmpty(BBucket o) { + if (o instanceof BBucket && + ((BBucket) o).remaining() == 0) { + return true; + } + return false; + } + + private BBucket wrap(Object src) { + if (src instanceof byte[]) { + return BBuffer.wrapper((byte[]) src, 0, ((byte[]) src).length); + } + if (src instanceof ByteBuffer) { + //return src; + ByteBuffer bb = (ByteBuffer) src; + return BBuffer.wrapper(bb.array(), bb.position(), + bb.remaining()); + } + if (src instanceof byte[]) { + byte[] bb = (byte[]) src; + return BBuffer.wrapper(bb, 0, bb.length); + } + return (BBucket) src; + } + + protected void notifyDataAvailable(Object bb) throws IOException { + synchronized (hasDataLock) { + hasDataLock.signal(this); // or bb ? + } + } + + public boolean hasData() { + return closeQueued || peekFirst() != null; + } + + public void waitData(long timeMs) throws IOException { + if (timeMs == 0) { + timeMs = defaultTimeout; + } + long exp = (timeMs == Long.MAX_VALUE) ? + Long.MAX_VALUE : System.currentTimeMillis() + timeMs; + synchronized (hasDataLock) { + if (hasData()) { + return; + } + hasDataLock.reset(); + } + if (timeMs == 0) { + timeMs = Long.MAX_VALUE; + } + long wait = (timeMs == Long.MAX_VALUE) ? Long.MAX_VALUE : + exp - System.currentTimeMillis(); + + hasDataLock.waitSignal(wait); + if (exp < System.currentTimeMillis()) { + throw new IOException("Timeout"); + } + } + + + + public boolean isAppendClosed() { + return closeQueued; + } + + // =================== Helper methods ================== + + /** + * Non-blocking. + */ + public int read() throws IOException { + BBucket bucket = peekFirst(); + if (bucket == null) { + return -1; + } + int res = bucket.array()[bucket.position()]; + bucket.position(bucket.position() + 1); + return res; + } + + public int peek() throws IOException { + BBucket bucket = peekFirst(); + if (bucket == null) { + return -1; + } + int res = bucket.array()[bucket.position()]; + return res; + } + + public int find(char c) { + int pos = 0; + for (int i = 0; i < buffers.size(); i++) { + BBucket bucket = buffers.get(i); + if (bucket == null || bucket.remaining() == 0) { + continue; + } + int found= BBuffer.findChar(bucket.array(), bucket.position(), + bucket.limit(), c); + if (found >= 0) { + return pos + found; + } + pos += bucket.remaining(); + } + return -1; + } + + public int readLine(BBuffer bc) throws IOException { + return readToDelim(bc, '\n'); + } + + /** + * Copy up to and including "delim". + * + * @return number of bytes read, or -1 for end of stream. + */ + int readToDelim(BBuffer bc, int delim) throws IOException { + int len = 0; + for (int idx = 0; idx < buffers.size(); idx++) { + BBucket bucket = buffers.get(idx); + if (bucket == null || bucket.remaining() == 0) { + continue; + } + byte[] data = bucket.array(); + int end = bucket.limit(); + int start = bucket.position(); + for (int i = start; i < end; i++) { + byte chr = data[i]; + bc.put(chr); + if (chr == delim) { + bucket.position(i + 1); + len += (i - start + 1); + return len; + } + } + bucket.position(end); // empty - should be removed + } + if (len == 0 && isClosedAndEmpty()) { + return -1; + } + return len; + } + + + public int write(ByteBuffer bb) throws IOException { + int len = bb.remaining(); + append(bb); + bb.position(bb.position() + len); + return len; + } + + public int read(byte[] buf, int off, int len) throws IOException { + BBucket bucket = peekFirst(); + if (isClosedAndEmpty()) { + return -1; + } + if (bucket == null) { + return 0; + } + int toCopy = Math.min(len, bucket.remaining()); + System.arraycopy(bucket.array(), bucket.position(), buf, + off, toCopy); + bucket.position(bucket.position() + toCopy); + return toCopy; + + } + + /** + * Non-blocking read. + */ + public int read(ByteBuffer bb) { + if (isClosedAndEmpty()) { + return -1; + } + int len = 0; + while (true) { + int space = bb.remaining(); // to append + if (space == 0) { + return len; + } + BBucket first = peekFirst(); + if (first == null) { + return len; + } + BBucket iob = ((BBucket) first); + if (space > iob.remaining()) { + space = iob.remaining(); + } + bb.put(iob.array(), iob.position(), space); + + iob.position(iob.position() + space); + iob.release(); + len += space; + } + } + + + public BBuffer readAll(BBuffer chunk) throws IOException { + if (chunk == null) { + chunk = allocate(); + } + while (true) { + if (isClosedAndEmpty()) { + return chunk; + } + BBucket first = peekFirst(); + if (first == null) { + return chunk; + } + BBucket iob = ((BBucket) first); + chunk.append(iob.array(), iob.position(), iob.remaining()); + iob.position(iob.position() + iob.remaining()); + iob.release(); + + } + } + + private BBuffer allocate() { + int size = 0; + for (int i = 0; i < getBufferCount(); i++) { + BBucket first = peekBucket(i); + if (first != null) { + size += first.remaining(); + } + } + return BBuffer.allocate(size); + } + + public BBuffer copyAll(BBuffer chunk) throws IOException { + if (chunk == null) { + chunk = allocate(); + } + for (int i = 0; i < getBufferCount(); i++) { + BBucket iob = peekBucket(i); + chunk.append(iob.array(), iob.position(), iob.remaining()); + } + return chunk; + } + + public IOBuffer append(InputStream is) throws IOException { + while (true) { + ByteBuffer bb = getWriteBuffer(); + int rd = is.read(bb.array(), bb.position(), bb.remaining()); + if (rd <= 0) { + return this; + } + bb.position(bb.position() + rd); + releaseWriteBuffer(rd); + } + } + + public IOBuffer append(BBuffer bc) throws IOException { + return append(bc.array(), bc.getStart(), bc.getLength()); + } + + public IOBuffer append(byte[] data) throws IOException { + return append(data, 0, data.length); + } + + public IOBuffer append(byte[] data, int start, int len) throws IOException { + if (closeQueued) { + throw new IOException("Closed"); + } + ByteBuffer bb = getWriteBuffer(); + + int i = start; + int end = start + len; + while (i < end) { + int rem = Math.min(end - i, bb.remaining()); + // to write + bb.put(data, i, rem); + i += rem; + if (bb.remaining() < 8) { + releaseWriteBuffer(1); + bb = getWriteBuffer(); + } + } + + releaseWriteBuffer(1); + return this; + } + + public IOBuffer append(int data) throws IOException { + if (closeQueued) { + throw new IOException("Closed"); + } + ByteBuffer bb = getWriteBuffer(); + bb.put((byte) data); + releaseWriteBuffer(1); + return this; + } + + public IOBuffer append(ByteBuffer cs) throws IOException { + return append(cs.array(), cs.position() + cs.arrayOffset(), + cs.remaining()); + } + + public IOBuffer append(BBucket cs) throws IOException { + append(cs.array(), cs.position(), cs.remaining()); + return this; + } + + public IOBuffer append(IOBuffer cs) throws IOException { + for (int i = 0; i < cs.getBufferCount(); i++) { + Object o = cs.peekBucket(i); + if (o instanceof BBucket) { + append((BBucket)o); + } else if (o instanceof ByteBuffer) { + append((ByteBuffer) o); + } else if (o instanceof CharSequence) { + append((CharSequence) o); + } else { + throw new IOException("Unknown type " + o); + } + } + + return this; + } + + public IOBuffer append(CharSequence cs) throws IOException { + byte[] data = cs.toString().getBytes(); + append(data, 0, data.length); + return this; + } + + public IOBuffer append(char c) throws IOException { + ByteBuffer bb = getWriteBuffer(); + bb.put((byte) c); + releaseWriteBuffer(1); + return this; + } + + /** + * All operations that iterate over buffers must be + * sync + * @return + */ + public synchronized int available() { + int a = 0; + int cnt = buffers.size(); + for (int i = 0; i < cnt; i++) { + a += buffers.get(i).remaining(); + } + return a; + } + + public String toString() { + return "IOB:{c:" + getBufferCount() + + ", b:" + available() + + (isAppendClosed() ? ", C}" : " }"); + } + + public BBucket popLen(int lenToConsume) { + BBucket o = peekFirst(); // skip empty + if (o == null) { + return null; + } + BBucket sb = BBuffer.wrapper(o.array(), + o.position(), lenToConsume); + o.position(o.position() + lenToConsume); + return sb; + } + + public BBucket popFirst() { + BBucket o = peekFirst(); // skip empty + if (o == null) { + return null; + } + if (o == appendable) { + synchronized (buffers) { + // TODO: concurrency ??? + BBucket sb = + BBuffer.wrapper(appendable.array(), + appendable.position(), + appendable.limit() - appendable.position()); + appendable.position(appendable.limit()); + return sb; + } + } else { + buffers.removeFirst(); + } + return o; + } + + + public ByteBuffer getWriteBuffer() throws IOException { + synchronized (buffers) { + if (closeQueued) { + throw new IOException("Closed"); + } + synchronized (buffers) { + BBucket last = (buffers.size() == 0) ? + null : buffers.getLast(); + if (last == null || last != appendable || + last.array().length - last.limit() < 16) { + last = BBuffer.allocate(ALLOC_SIZE); + } + appending = true; + appendable = (BBuffer) last; + } + + if (writeBuffer == null || writeBuffer.array() != appendable.array()) { + writeBuffer = ByteBuffer.wrap(appendable.array()); + } + writeBuffer.position(appendable.limit()); + writeBuffer.limit(appendable.array().length); + return writeBuffer; + } + } + + public void releaseWriteBuffer(int read) throws IOException { + if (!appending) { + throw new IOException("Not appending"); + } + synchronized (buffers) { + if (writeBuffer != null) { + if (read > 0) { + appendable.limit(writeBuffer.position()); + // We have some more data. + if (buffers.size() == 0 || + buffers.getLast() != appendable) { + buffers.add(appendable); + } + notifyDataAvailable(appendable); + } + } + } + appending = false; + } + + + + // ------ More utilities - for parsing request ( later )------- +// public final int skipBlank(ByteBuffer bb, int start) { +// // Skipping blank lines +// byte chr = 0; +// do { +// if (!bb.hasRemaining()) { +// return -1; +// } +// chr = bb.get(); +// } while ((chr == HttpParser.CR) || (chr == HttpParser.LF)); +// return bb.position(); +//} + +//public final int readToDelimAndLowerCase(ByteBuffer bb, +// byte delim, +// boolean lower) { +// boolean space = false; +// byte chr = 0; +// while (!space) { +// if (!bb.hasRemaining()) { +// return -1; +// } +// chr = bb.get(); +// if (chr == delim) { +// space = true; +// } +// if (lower && (chr >= HttpParser.A) && (chr <= HttpParser.Z)) { +// bb.put(bb.position() - 1, +// (byte) (chr - HttpParser.LC_OFFSET)); +// } +// } +// return bb.position(); +//} + +//public boolean skipSpace(ByteBuffer bb) { +// boolean space = true; +// while (space) { +// if (!bb.hasRemaining()) { +// return false; +// } +// byte chr = bb.get(); +// if ((chr == HttpParser.SP) || (chr == HttpParser.HT)) { +// // +// } else { +// space = false; +// bb.position(bb.position() -1); // move back +// } +// } +// return true; +//} +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOChannel.java new file mode 100644 index 000000000..23b642bf5 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOChannel.java @@ -0,0 +1,290 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; + + + +/** + * Buffered ByteChannel, backed by a buffer brigade to allow + * some zero-copy operations. + * + * - you can use it as a normal non-blocking ByteChannel. + * - you can call getRead + * + * Very different from MINA IoFilters, also much lower level. + * + * + * @author Costin Manolache + */ +public abstract class IOChannel implements ByteChannel, IOConnector.DataReceivedCallback, + IOConnector.DataFlushedCallback { //, IOConnector.ClosedCallback { + + protected IOChannel net; + protected IOChannel app; + + protected String id; + protected String target; + + protected IOConnector connector; + + protected IOConnector.ConnectedCallback connectedCallback; + protected IOConnector.DataReceivedCallback dataReceivedCallback; + protected IOConnector.DataFlushedCallback dataFlushedCallback; + + // Last activity timestamp. + // TODO: update, etc + public long ts; + + public void setConnectedCallback(IOConnector.ConnectedCallback connectedCallback) { + this.connectedCallback = connectedCallback; + } + + public void setDataReceivedCallback(IOConnector.DataReceivedCallback dataReceivedCallback) { + this.dataReceivedCallback = dataReceivedCallback; + } + + /** + * Callback called when the bottom ( OS ) channel has finished flushing. + * + * @param dataFlushedCallback + */ + public void setDataFlushedCallback(IOConnector.DataFlushedCallback dataFlushedCallback) { + this.dataFlushedCallback = dataFlushedCallback; + } + + protected IOChannel() { + } + + // Input + public abstract IOBuffer getIn(); + + // Output + public abstract IOBuffer getOut(); + + + /** + * From downstream ( NET ). Pass it to the next channel. + */ + public void handleReceived(IOChannel net) throws IOException { + sendHandleReceivedCallback(); + } + + /** + * Called from lower layer (NET) when the last flush is + * done and all buffers have been sent to OS ( or + * intended recipient ). + * + * Will call the callback or next filter, may do additional + * processing. + * + * @throws IOException + */ + public void handleFlushed(IOChannel net) throws IOException { + sendHandleFlushedCallback(); + } + + public void sendHandleFlushedCallback() throws IOException { + try { + if (dataFlushedCallback != null) { + dataFlushedCallback.handleFlushed(this); + } + if (app != null) { + app.handleFlushed(this); + } + } catch (Throwable t) { + close(); + if (t instanceof IOException) { + throw (IOException) t; + } else { + throw new WrappedException("Error in handleFlushed", t); + } + } + } + + + /** + * Notify next channel that data has been received. + */ + public void sendHandleReceivedCallback() throws IOException { + try { + if (dataReceivedCallback != null) { + dataReceivedCallback.handleReceived(this); + } + if (app != null) { + app.handleReceived(this); + } + } catch (Throwable t) { + t.printStackTrace(); + try { + close(); + } catch(Throwable t2) { + t2.printStackTrace(); + } + if (t instanceof IOException) { + throw (IOException) t; + } else { + throw new WrappedException(t); + } + } + } + + public void close() throws IOException { + shutdownOutput(); + // Should it read the buffers ? + + if (getIn().isAppendClosed()) { + return; + } else { + getIn().close(); + sendHandleReceivedCallback(); + } + getIn().hasDataLock.signal(getIn()); + } + + public boolean isOpen() { + return !getIn().isAppendClosed() && !getOut().isAppendClosed(); + } + + public void shutdownOutput() throws IOException { + if (getOut().isAppendClosed()) { + return; + } else { + getOut().close(); + startSending(); + } + } + + public void setSink(IOChannel previous) { + this.net = previous; + } + + public IOChannel getSink() { + return net; + } + + // Chaining/filtering + + /** + * Called to add an filter _after_ the current channel. + */ + public IOChannel addFilterAfter(IOChannel next) { + this.app = next; + app.setSink(this); + + // TODO: do we want to migrate them automatically ? + app.setDataReceivedCallback(dataReceivedCallback); + app.setDataFlushedCallback(dataFlushedCallback); + // app.setClosedCallback(closedCallback); + + dataReceivedCallback = null; + dataFlushedCallback = null; + return this; + } + + public IOChannel getFirst() { + IOChannel first = this; + while (true) { + if (!(first instanceof IOChannel)) { + return first; + } + IOChannel before = ((IOChannel) first).getSink(); + if (before == null) { + return first; + } else { + first = before; + } + } + } + + // Socket support + + public int getPort(boolean remote) { + if (net != null) { + return net.getPort(remote); + } + return 80; + } + + public void readInterest(boolean b) throws IOException { + if (net != null) { + net.readInterest(b); + } + } + + // Helpers + + public int read(ByteBuffer bb) throws IOException { + return getIn().read(bb); + } + + public int readNonBlocking(ByteBuffer bb) throws IOException { + return getIn().read(bb); + } + + public int readBlocking(ByteBuffer bb, long timeMs) throws IOException { + getIn().waitData(timeMs); + return getIn().read(bb); + } + + /** + * Capture all output in a buffer. + */ + public BBuffer readAll(BBuffer chunk, long to) + throws IOException { + if (chunk == null) { + chunk = BBuffer.allocate(); + } + while (true) { + getIn().waitData(to); + BBucket next = getIn().peekFirst(); + if (getIn().isClosedAndEmpty() && next == null) { + return chunk; + } + if (next == null) { + continue; // false positive + } + chunk.append(next.array(), next.position(), next.remaining()); + getIn().advance(next.remaining()); + } + } + + public int write(ByteBuffer bb) throws IOException { + return getOut().write(bb); + } + + public void write(byte[] data) throws IOException { + getOut().append(data, 0, data.length); + } + + public void write(String string) throws IOException { + write(string.getBytes()); + } + + /** + * Send data in out to the intended recipient. + * This is not blocking. + */ + public abstract void startSending() throws IOException; + + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public CharSequence getTarget() { + if (net != null) { + return net.getTarget(); + } + return target; + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java new file mode 100644 index 000000000..1bb33b30e --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOConnector.java @@ -0,0 +1,42 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; + + +/** + * Factory for IOChannels, with support for caching. + * + * + * @author Costin Manolache + */ +public abstract class IOConnector { + + public static interface DataReceivedCallback { + /** + * Called when data or EOF has been received. + */ + public void handleReceived(IOChannel ch) throws IOException; + } + + public static interface ConnectedCallback { + public void handleConnected(IOChannel ch) throws IOException; + } + + public static interface DataFlushedCallback { + public void handleFlushed(IOChannel ch) throws IOException; + } + + public abstract void acceptor(IOConnector.ConnectedCallback sc, + CharSequence port, Object extra) + throws IOException; + + // TODO: failures ? + public abstract void connect(String host, int port, + IOConnector.ConnectedCallback sc) throws IOException; + + public void stop() { + + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOInputStream.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOInputStream.java new file mode 100644 index 000000000..3c167f170 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOInputStream.java @@ -0,0 +1,71 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.io.InputStream; + + +/** + * Similar with ServletInputStream - adds readLine(byte[]..), using + * a IOBuffer. + * + * + * + * @author Costin Manolache + */ +public class IOInputStream extends InputStream { + + IOBuffer bb; + long timeout; + + public IOInputStream(IOChannel httpCh, long to) { + bb = httpCh.getIn(); + this.timeout = to; + } + + @Override + public int read() throws IOException { + // getReadableBucket/peekFirst returns a buffer with at least + // 1 byte in it. + if (bb.isClosedAndEmpty()) { + return -1; + } + bb.waitData(timeout); + if (bb.isClosedAndEmpty()) { + return -1; + } + + return bb.read(); + } + + public int read(byte[] buf, int off, int len) throws IOException { + if (bb.isClosedAndEmpty()) { + return -1; + } + bb.waitData(timeout); + if (bb.isClosedAndEmpty()) { + return -1; + } + return bb.read(buf, off, len); + } + + /** + * Servlet-style read line: terminator is \n or \r\n, left in buffer. + */ + public int readLine(byte[] b, int off, int len) throws IOException { + if (len <= 0) { + return 0; + } + int count = 0, c; + + while ((c = read()) != -1) { + b[off++] = (byte)c; + count++; + if (c == '\n' || count == len) { + break; + } + } + return count > 0 ? count : -1; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOOutputStream.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOOutputStream.java new file mode 100644 index 000000000..333160336 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOOutputStream.java @@ -0,0 +1,207 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.CharConversionException; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.text.MessageFormat; + +import org.apache.tomcat.lite.http.HttpMessage; +import org.apache.tomcat.lite.http.HttpWriter; + +/** + * Same methods with ServletOutputStream. + * + * There is no restriction in using the Writer and InputStream at the + * same time - the servlet layer will impose it for compat. You can also use + * IOBuffer directly. + * + * If you mix stream and writer: + * - call BufferWriter.push() to make sure all chars are sent down + * - the BufferOutputStream doesn't cache any data, all goes to the + * IOBuffer. + * - flush() on BufferOutputStream and BufferWriter will send the data + * to the network and block until it gets to the socket ( so it can + * throw exception ). + * - You can also use non-blocking flush methods in IOBuffer, and a + * callback if you want to know when the write was completed. + * + * @author Costin Manolache + */ +public class IOOutputStream extends OutputStream { + + IOBuffer bb; + HttpMessage message; + int bufferSize = HttpWriter.DEFAULT_BUFFER_SIZE; + + int wSinceFlush = 0; + + public IOOutputStream(IOBuffer out, HttpMessage httpMessage) { + this.bb = out; + message = httpMessage; + } + + public void recycle() { + wSinceFlush = 0; + bufferSize = HttpWriter.DEFAULT_BUFFER_SIZE; + } + + public void reset() { + wSinceFlush = 0; + bb.clear(); + } + + public int getWrittenSinceFlush() { + return wSinceFlush; + } + + + public int getBufferSize() { + return bufferSize; + } + + public void setBufferSize(int size) { + if (size > bufferSize) { + bufferSize = size; + } + } + + private void updateSize(int cnt) throws IOException { + wSinceFlush += cnt; + if (wSinceFlush > bufferSize) { + flush(); + } + } + + @Override + public void write(int b) throws IOException { + bb.append((char) b); + updateSize(1); + } + + @Override + public void write(byte data[]) throws IOException { + write(data, 0, data.length); + } + + @Override + public void write(byte data[], int start, int len) throws IOException { + bb.append(data, start, len); + updateSize(len); + } + + public void flush() throws IOException { + if (message.getHttpChannel() != null) { + message.getHttpChannel().startSending(); + + message.getHttpChannel().waitFlush(Long.MAX_VALUE); + } + wSinceFlush = 0; + } + + public void close() throws IOException { + flush(); + bb.close(); + } + + + public void write(ByteBuffer source) throws IOException { + write(source.array(), source.position(), source.remaining()); + source.position(source.limit()); + } + + public void print(String s) throws IOException { + if (s==null) s="null"; + int len = s.length(); + for (int i = 0; i < len; i++) { + char c = s.charAt (i); + + // + // XXX NOTE: This is clearly incorrect for many strings, + // but is the only consistent approach within the current + // servlet framework. It must suffice until servlet output + // streams properly encode their output. + // + if ((c & 0xff00) != 0) { // high order byte must be zero + String errMsg = "Not ISO-8859-1"; + Object[] errArgs = new Object[1]; + errArgs[0] = new Character(c); + errMsg = MessageFormat.format(errMsg, errArgs); + throw new CharConversionException(errMsg); + } + write (c); + } + } + + + public void print(boolean b) throws IOException { + String msg; + if (b) { + msg = "true"; + } else { + msg = "false"; + } + print(msg); + } + + public void print(char c) throws IOException { + print(String.valueOf(c)); + } + + public void print(int i) throws IOException { + print(String.valueOf(i)); + } + + public void print(long l) throws IOException { + print(String.valueOf(l)); + } + + public void print(float f) throws IOException { + print(String.valueOf(f)); + } + + public void print(double d) throws IOException { + print(String.valueOf(d)); + } + + public void println() throws IOException { + print("\r\n"); + } + + public void println(String s) throws IOException { + print(s); + println(); + } + + public void println(boolean b) throws IOException { + print(b); + println(); + } + + public void println(char c) throws IOException { + print(c); + println(); + } + + public void println(int i) throws IOException { + print(i); + println(); + } + + public void println(long l) throws IOException { + print(l); + println(); + } + + public void println(float f) throws IOException { + print(f); + println(); + } + + public void println(double d) throws IOException { + print(d); + println(); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOReader.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOReader.java new file mode 100644 index 000000000..2095dcee7 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOReader.java @@ -0,0 +1,231 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.MalformedInputException; +import java.nio.charset.UnmappableCharacterException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * Conversion from Bytes to Chars and support for decoding. + * + * Replaces tomcat B2CConverter with NIO equivalent. B2CConverter was a hack + * (re)using an dummy InputStream backed by a ByteChunk. + * + * @author Costin Manolache + */ +public class IOReader extends Reader { + + IOBuffer iob; + Map decoders = new HashMap(); + CharsetDecoder decoder; + + private static boolean REUSE = true; + String enc; + private boolean closed; + public static final String DEFAULT_ENCODING = "ISO-8859-1"; + + public IOReader(IOBuffer iob) { + this.iob = iob; + } + + public void setEncoding(String charset) { + enc = charset; + if (enc == null) { + enc = DEFAULT_ENCODING; + } + decoder = REUSE ? decoders.get(enc) : null; + if (decoder == null) { + decoder = Charset.forName(enc).newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + if (REUSE) { + decoders.put(enc, decoder); + } + } + } + + public String getEncoding() { + return enc; + } + + public void recycle() { + if (decoder != null) { + decoder.reset(); + } + closed = false; + enc = null; + } + + private void checkClosed() throws IOException { + if (closed) throw new IOException("closed"); + } + + public boolean ready() { + return iob.peekFirst() != null; + } + + public int read(java.nio.CharBuffer target) throws IOException { + int len = target.remaining(); + char[] cbuf = new char[len]; + int n = read(cbuf, 0, len); + if (n > 0) + target.put(cbuf, 0, n); + return n; + } + + public int read() throws IOException { + char cb[] = new char[1]; + if (read(cb, 0, 1) == -1) + return -1; + else + return cb[0]; + } + + @Override + public void close() throws IOException { + closed = true; + iob.close(); + } + + /** + * Used if a bucket ends on a char boundary + */ + BBuffer underFlowBuffer = BBuffer.allocate(10); + public static AtomicInteger underFlows = new AtomicInteger(); + + /** + * Decode all bytes - for example a URL or header. + */ + public void decodeAll(BBucket bb, CBuffer c) { + + while (bb.hasRemaining()) { + CharBuffer charBuffer = c.getAppendCharBuffer(); + CoderResult res = decode1(bb, charBuffer, true); + c.returnAppendCharBuffer(charBuffer); + if (res != CoderResult.OVERFLOW) { + if (res == CoderResult.UNDERFLOW || bb.hasRemaining()) { + System.err.println("Ignored trailing bytes " + bb.remaining()); + } + return; + } + } + + } + + /** + * Do one decode pass. + */ + public CoderResult decode1(BBucket bb, CharBuffer c, boolean eof) { + ByteBuffer b = bb.getByteBuffer(); + + if (underFlowBuffer.hasRemaining()) { + // Need to get rid of the underFlow first + for (int i = 0; i < 10; i++) { + underFlowBuffer.put(b.get()); + bb.position(b.position()); + ByteBuffer ub = underFlowBuffer.getByteBuffer(); + CoderResult res = decoder.decode(ub, c, eof); + if (! ub.hasRemaining()) { + // underflow resolved + break; + } + if (res == CoderResult.OVERFLOW) { + return res; + } + } + if (underFlowBuffer.hasRemaining()) { + throw new RuntimeException("Can't resolve underflow after " + + "10 bytes"); + } + } + + CoderResult res = decoder.decode(b, c, eof); + bb.position(b.position()); + + if (res == CoderResult.UNDERFLOW && bb.hasRemaining()) { + // b ends on a boundary + underFlowBuffer.append(bb.array(), bb.position(), bb.remaining()); + bb.position(bb.limit()); + } + return res; + } + + @Override + public int read(char[] cbuf, int offset, int length) throws IOException { + checkClosed(); + if (length == 0) { + return 0; + } + // we can either allocate a new CharBuffer or use a + // static one and copy. Seems simpler this way - needs some + // load test, but InputStreamReader seems to do the same. + CharBuffer out = CharBuffer.wrap(cbuf, offset, length); + + CoderResult result = CoderResult.UNDERFLOW; + + BBucket bucket = iob.peekFirst(); + + // Consume as much as possible without blocking + while (result == CoderResult.UNDERFLOW) { + // fill the buffer if needed + if (bucket == null || ! bucket.hasRemaining()) { + if (out.position() > offset) { + // we could return the result without blocking read + break; + } + bucket = null; + while (bucket == null) { + iob.waitData(0); + bucket = iob.peekFirst(); + if (bucket == null && iob.isClosedAndEmpty()) { + // EOF, we couldn't decode anything + break; + } + } + + if (bucket == null) { + // eof + break; + } + } + + result = decode1(bucket, out, false); + } + + if (result == CoderResult.UNDERFLOW && iob.isClosedAndEmpty()) { + // Flush out any remaining data + ByteBuffer bytes = bucket == null ? + underFlowBuffer.getByteBuffer() : bucket.getByteBuffer(); + result = decoder.decode(bytes, out, true); + if (bucket == null) { + underFlowBuffer.position(bytes.position()); + } else { + bucket.position(bytes.position()); + } + + decoder.flush(out); + decoder.reset(); + } + + if (result.isMalformed()) { + throw new MalformedInputException(result.length()); + } else if (result.isUnmappable()) { + throw new UnmappableCharacterException(result.length()); + } + + int rd = out.position() - offset; + return rd == 0 ? -1 : rd; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOWriter.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOWriter.java new file mode 100644 index 000000000..ad8f9520c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/IOWriter.java @@ -0,0 +1,180 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; + +/** + * Converts chars to bytes, and associated encoding. + * + * Replaces C2B from old tomcat. + * + * @author Costin Manolache + */ +public class IOWriter extends Writer { + + IOBuffer iob; + Map encoders = new HashMap(); + CharsetEncoder encoder; + + private static boolean REUSE = true; + String enc; + private boolean closed; + IOChannel ioCh; + + public IOWriter(IOChannel iob) { + this.ioCh = iob; + if (iob != null) { + this.iob = iob.getOut(); + } + } + + public void setEncoding(String charset) { + if (charset == null) { + charset = "UTF-8"; + } + enc = charset; + encoder = getEncoder(charset); + if (encoder == null) { + encoder = Charset.forName(charset).newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + if (REUSE) { + encoders.put(charset, encoder); + } + } + } + + CharsetEncoder getEncoder(String charset) { + if (charset == null) { + charset = "UTF-8"; + } + encoder = REUSE ? encoders.get(charset) : null; + if (encoder == null) { + encoder = Charset.forName(charset).newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + if (REUSE) { + encoders.put(charset, encoder); + } + } + return encoder; + } + + public String getEncoding() { + return enc; + } + + public void recycle() { + if (encoder != null) { + encoder.reset(); + } + closed = false; + enc = null; + } + + + private void checkClosed() throws IOException { + if (closed) throw new IOException("closed"); + } + + @Override + public void close() throws IOException { + closed = true; + // flush the buffer ? + ByteBuffer out = iob.getWriteBuffer(); + encoder.flush(out); + iob.releaseWriteBuffer(1); + + iob.close(); + } + + /** + * Used if a bucket ends on a char boundary + */ + CBuffer underFlowBuffer = CBuffer.newInstance(); + + public void encode1(CBuffer cc, + BBuffer bb, CharsetEncoder encoder, boolean eof) { + CharBuffer c = cc.getNioBuffer(); + ByteBuffer b = bb.getWriteByteBuffer(c.remaining() * 2); + encode1(c, b, encoder, eof); + cc.returnNioBuffer(c); + bb.limit(b.position()); + } + + /** + * + * @param cc + * @return + */ + public void encode1(CharBuffer c, + ByteBuffer b, CharsetEncoder encoder, boolean eof) { + + // TODO: buffer growth in caller + + CoderResult res = encoder.encode(c, b, eof); + if (res == CoderResult.OVERFLOW) { + // bb is full - next call will get a larger buffer ( it + // grows ) or maybe will be flushed. + } + if (res == CoderResult.UNDERFLOW && c.remaining() > 0 && !eof) { + // TODO: if eof -> exception ? + // cc has remaining chars - for example a surrogate start. + underFlowBuffer.put(c); + } + + } + + public void encodeAll(CBuffer cc, + BBuffer bb, CharsetEncoder encoder, boolean eof) { + while (cc.length() > 0) { + encode1(cc, bb, encoder, eof); + } + } + + public void encodeAll(CBuffer cc, + BBuffer bb, String cs) { + encodeAll(cc, bb, getEncoder(cs), true); + } + + @Override + public void flush() throws IOException { + if (ioCh != null) { + ioCh.startSending(); + } + } + + /** + * Just send the chars to the byte[], without flushing down. + * + * @throws IOException + */ + public void push() throws IOException { + // we don't cache here. + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + checkClosed(); + CharBuffer cb = CharBuffer.wrap(cbuf, off, len); + + while (cb.remaining() > 0) { + ByteBuffer wb = iob.getWriteBuffer(); + encode1(cb, wb, encoder, false); + iob.releaseWriteBuffer(1); + } + } + + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java new file mode 100644 index 000000000..0b9f56b23 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/MemoryIOConnector.java @@ -0,0 +1,83 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; + +public class MemoryIOConnector extends IOConnector { + + public static class MemoryIOChannel extends IOChannel { + IOBuffer netIn = new IOBuffer(this) { + protected void notifyDataAvailable(Object bb) throws IOException { + sendHandleReceivedCallback(); + super.notifyDataAvailable(bb); + } + }; + IOBuffer netOut = new IOBuffer(this); + + /** + * All flushed output will be saved to 'out'. + */ + public BBuffer out = BBuffer.allocate(4096); + + public MemoryIOChannel() { + } + + public void startSending() throws IOException { + // + IOBuffer bb = netOut; + while (true) { + if (bb.isClosedAndEmpty()) { + break; + } + BBucket first = bb.peekFirst(); + if (first == null) { + break; + } + BBucket iob = ((BBucket) first); + out.append(iob.array(), iob.position(), iob.remaining()); + bb.advance(iob.remaining()); + iob.release(); + } + + handleFlushed(this); + } + + @Override + public IOBuffer getIn() { + return netIn; + } + @Override + public IOBuffer getOut() { + return netOut; + } + } + + // TODO: in-process communication without sockets for testing + ConnectedCallback acceptor; + MemoryIOConnector server; + + public MemoryIOConnector withServer(MemoryIOConnector server) { + this.server = server; + return server; + } + + @Override + public void acceptor(ConnectedCallback sc, CharSequence port, Object extra) + throws IOException { + this.acceptor = sc; + } + + @Override + public void connect(String host, int port, ConnectedCallback sc) + throws IOException { + IOChannel ch = new MemoryIOChannel(); + IOChannel sch = new MemoryIOChannel(); + // TODO: mix + if (server != null && server.acceptor != null) { + server.acceptor.handleConnected(sch); + } + sc.handleConnected(ch); + } + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioChannel.java new file mode 100644 index 000000000..c136b8b7d --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioChannel.java @@ -0,0 +1,200 @@ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channel; +import java.nio.channels.SelectionKey; + + +/** + * Wrapper around the real channel, with selector-specific info. + * + * It is stored as an attachment in the selector. + */ +public class NioChannel implements ByteChannel { + + public static interface NioChannelCallback { + public void handleConnected(NioChannel ch) throws IOException; + public void handleClosed(NioChannel ch) throws IOException; + public void handleReadable(NioChannel ch) throws IOException; + public void handleWriteable(NioChannel ch) throws IOException; + + } + + NioChannel(NioThread sel) { + this.sel = sel; + } + + // APR long is wrapped in a ByteChannel as well - with few other longs. + Channel channel; + + // sync access. + Object selKey; + + NioThread sel; + + /** + * If != 0 - the callback will be notified closely after this time. + * Used for timeouts. + */ + long nextTimeEvent = 0; + + // Callbacks + Runnable timeEvent; + + NioChannelCallback callback; + + + Throwable lastException; + + // True if the callback wants to be notified of read/write + boolean writeInterest; + boolean readInterest; + + // shutdownOutput has been called ? + private boolean outClosed = false; + + // read() returned -1 OR input buffer closed ( no longer interested ) + boolean inClosed = false; + + // Saved to allow debug messages for bad interest/looping + int lastReadResult; + int zeroReads = 0; + int lastWriteResult; + + protected NioChannel() { + + } + + public NioThread getSelectorThread() { + return sel; + } + + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("SelData/") + .append(writeInterest ? "W/" : "") + .append(readInterest ? "R/" : "") + .append(outClosed ? "Out-CLOSE/" : "") + .append(inClosed ? "In-CLOSE/" : "") + .append(selKey == null ? -1 : ((SelectionKey) (selKey)).interestOps()) + .append("/") + .append(channel.toString()); + + return sb.toString(); + } + + public Channel getChannel() { + return channel; + } + + public boolean isOpen() { + // in and out open + return channel.isOpen() && !outClosed && !inClosed; + } + + public int read(ByteBuffer bb) throws IOException { + return sel.readNonBlocking(this, bb); + } + + public int write(ByteBuffer bb) throws IOException { + return sel.writeNonBlocking(this, bb); + } + + public void readInterest(boolean b) throws IOException { + sel.readInterest(this, b); + } + + public void writeInterest() throws IOException { + sel.writeInterest(this); + } + + public InetAddress getAddress(boolean remote) { + return sel.getAddress(this, remote); + } + + public int getPort(boolean remote) { + return sel.getPort(this, remote); + } + + /** + * Run in selector thread. + */ + public void runInSelectorThread(Runnable t) throws IOException { + sel.runInSelectorThread(t); + } + + /** + * Request a timer event. The thread will generate the events at + * a configurable interval - for example no more often than 0.5 sec. + */ + public void setTimer(long timeMs, Runnable cb) { + this.nextTimeEvent = timeMs; + this.timeEvent = cb; + } + + /** + * shutdown out + in + * If there is still data in the input buffer - RST will be sent + * instead of FIN. + * + * + * The proper way to close a connection is to shutdownOutput() first, + * wait until read() return -1, then call close(). + * + * If read() returns -1, you need to finish sending, call shutdownOutput() + * than close. + * If read() returns -1 and there is an error - call close() + * directly. + * + */ + @Override + public void close() throws IOException { + shutdownOutput(); + sel.close(this, null); + } + + /** + * Send TCP close(FIN). HTTP uses this to transmit end of body. The other end + * detects this with a '-1' in read(). + * + * All other forms of close() are reported as exceptions in read(). + * + * @throws IOException + */ + public void shutdownOutput() throws IOException { + synchronized (channel) { + if (!outClosed) { + outClosed = true; + try { + sel.shutdownOutput(this); + } catch (IOException ex) { + // ignore + } + } + if (inClosed) { + sel.close(this, null); + } + } + } + + void inputClosed() throws IOException { + synchronized (channel) { + if (inClosed) { + // already closed + return; + } + inClosed = true; // detected end + if (outClosed) { + sel.close(this, null); + } else { + // Don't close the channel - write may still work ? + readInterest(false); + } + } + } + + boolean closeCalled = false; +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java new file mode 100644 index 000000000..95e34e740 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/NioThread.java @@ -0,0 +1,1147 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.channels.spi.SelectorProvider; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.io.NioChannel.NioChannelCallback; + +/** + * Abstract NIO/APR to avoid some of the complexity and allow more code + * sharing and experiments. + * + * SelectorThread provides non-blocking methods for read/write and generates + * callbacks using SelectorCallback. It has no buffers of its own. + * + * Additional requirements: + * - support timers ( setTimer() in SelectorChannel ) + * + * @author Costin Manolache + */ +public class NioThread implements Runnable { + + // ----------- IO handling ----------- + protected long inactivityTimeout = 5000; + protected Thread selectorThread; + + + static Logger log = Logger.getLogger("NIO"); + + Selector selector; + + // will be processed in the selector thread + List readInterest = new ArrayList(); + List writeInterest = new ArrayList(); + List connectAcceptInterest = new ArrayList(); + List updateCallback = new ArrayList(); + List closeInterest = new LinkedList(); + List runnableInterest = new ArrayList(); + + // Statistics + AtomicInteger opened = new AtomicInteger(); + AtomicInteger closed = new AtomicInteger(); + AtomicInteger loops = new AtomicInteger(); + + AtomicInteger callbackCount = new AtomicInteger(); + AtomicLong callbackTotalTime = new AtomicLong(); + long maxCallbackTime = 0; + + // actives are also stored in the Selector. This is only updated in the main + // thread + public ArrayList active = new ArrayList(); + + public static boolean debug = false; + boolean debugWakeup = false; + boolean running = true; + + long lastWakeup = System.currentTimeMillis(); // last time we woke + long nextWakeup; // next scheduled wakeup + + // Normally select will wait for the next time event - if it's + // too far in future, maxSleep will override it. + private long maxSleep = 600000; + long sleepTime = maxSleep; + + // Never sleep less than minSleep. This defines the resulution for + // time events. + private long minSleep = 100; + + boolean daemon = true; + + // TODO: trace log - record all events with timestamps, replay + + public NioThread(String name, boolean daemon) { + try { + selectorThread = (name == null) ? new Thread(this) : + new Thread(this, name); + + selector = Selector.open(); + // TODO: start it on-demand, close it when not in use + selectorThread.setDaemon(daemon); + this.daemon = daemon; + + selectorThread.start(); + + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Opened sockets, waiting for something ( close at least ) + */ + public int getOpen() { + return opened.get(); + } + + /** + * Closed - we're done with them. + */ + public int getClosed() { + return closed.get(); + } + + public int getActive() { + return active.size(); + } + + public int getCallbacks() { + return callbackCount.get(); + } + + public long getMaxCallbackTime() { + return maxCallbackTime; + } + + public long getAvgCallbackTime() { + int cnt = callbackCount.get(); + if (cnt == 0) { + return 0; + } + return callbackTotalTime.get() / cnt; + } + + /** + * How many times we looped + */ + public int getLoops() { + return loops.get(); + } + + public long getLastWakeup() { + return lastWakeup; + } + + public long getTimeSinceLastWakeup() { + return System.currentTimeMillis() - lastWakeup; + } + + /** + * Close all resources, stop accepting, stop the thread. + * The actual stop will happen in background. + */ + public void stop() { + running = false; + if (debug) { + log.info("Selector thread stop " + this); + } + selector.wakeup(); + } + + public void run() { + int sloops = 0; + if (debug) { + log.info("Start NIO thread, daemon=" + daemon); + } + while (running) { + // if we want timeouts - set here. + try { + loops.incrementAndGet(); + + // Check if new requests were added + processPending(); + + // Timers + long now = System.currentTimeMillis(); + if (nextWakeup < now) { + // We don't want to iterate on every I/O + updateSleepTimeAndProcessTimeouts(now); + } + + int selected = selector.select(sleepTime); + + lastWakeup = System.currentTimeMillis(); + long slept = lastWakeup - now; + + if (debugWakeup && selected == 0) { + if (sleepTime < maxSleep - 1000) { // short wakeup + log.info("Wakeup " + selected + " " + slept + + " " + sleepTime); + } + } + if (slept < 10 && selected == 0) { + if (sloops > 50) { + sloops = 0; + log.severe("Looping !"); + resetSelector(); + } + sloops++; + } + + // handle events for existing req first. + if (selected != 0) { + sloops = 0; + Set sel = selector.selectedKeys(); + Iterator i = sel.iterator(); + + while (i.hasNext()) { + SelectionKey sk = i.next(); + i.remove(); + + boolean valid = sk.isValid(); + int readyOps = (valid) ? sk.readyOps() : 0; + + NioChannel ch = (NioChannel) sk.attachment(); + if (debugWakeup) { + log.info("Wakeup selCnt=" + selected + " slept=" + (lastWakeup - now) + + " ready: " + readyOps + " v=" + + sk.isValid() + " ch=" + ch); + } + if (ch == null) { + log.severe("Missing channel"); + sk.cancel(); + continue; + } + if (ch.selKey != sk) { + // if (ch.selKey != null) { // null if closed + log.severe("Invalid state, selKey doesn't match "); + ch.selKey = sk; + } + if (ch.channel != sk.channel()) { + ch.channel = sk.channel(); + log.severe("Invalid state, channel doesn't match "); + } + + if (!sk.isValid()) { + if (debug) { + log.info("!isValid, closed socket " + ch); + } + ch.close(); + continue; + } + + try { + int ready = sk.readyOps(); + // callbacks + if (sk.isValid() && sk.isAcceptable()) { + handleAccept(ch, sk); + } + + if (sk.isValid() && sk.isConnectable()) { + sk.interestOps(sk.interestOps() & ~SelectionKey.OP_CONNECT); + SocketChannel sc = (SocketChannel) sk.channel(); + handleConnect(ch, sc); + } + + if (sk.isValid() && sk.isWritable()) { + // Needs to be explicitely re-enabled by callback + // if more data. + sk.interestOps(sk.interestOps() & ~SelectionKey.OP_WRITE); + ch.writeInterest = false; + handleDataWriteable(ch); + } + + if (sk.isValid() && sk.isReadable()) { + // Leave readable interest ! + handleReadable(ch); + } + + long callbackTime = + System.currentTimeMillis() - lastWakeup; + + if (callbackTime > 250) { + log.warning("Callback too long ! ops=" + ready + + " time=" + callbackTime + " ch=" + ch); + } + if (callbackTime > maxCallbackTime) { + maxCallbackTime = callbackTime; + } + callbackCount.incrementAndGet(); + this.callbackTotalTime.addAndGet(callbackTime); + + } catch (Throwable t) { + log.log(Level.SEVERE, "SelectorThread: Channel error, closing", t); + ch.lastException = t; + ch.close(); + } + + } + // All at once + sel.clear(); + } + + } catch (Throwable e) { + log.log(Level.SEVERE, "SelectorThread: Error in select", e); + } + } // while(running) + log.info("SelectorThread done"); + } + + private void log(String msg, int selected, long slept, SelectionKey sk, int readyOps) { + log.info(msg + " " + selected + + " " + slept + + " ready: " + readyOps + " " + + sk.readyOps() + " " + sk); + } + + private void resetSelector() throws IOException, ClosedChannelException { + // Let's close all sockets - one is bad, but we can't do much. + Set keys = selector.keys(); + //Set keys = selector.keys(); + ArrayList oldCh = new ArrayList(); + ArrayList interests = new ArrayList(); + for (SelectionKey k : keys) { + NioChannel cd = (NioChannel) k.attachment(); + interests.add(k.interestOps()); + oldCh.add(cd); + k.cancel(); + } + + selector.close(); + selector = Selector.open(); + for (int i = 0; i < oldCh.size(); i++) { + NioChannel selectorData = oldCh.get(i); + int interest = interests.get(i); + if (selectorData.channel instanceof ServerSocketChannel) { + ServerSocketChannel socketChannel = + (ServerSocketChannel) selectorData.channel; + selectorData.selKey = socketChannel.register(selector, SelectionKey.OP_ACCEPT); + } else { + SocketChannel socketChannel = + (SocketChannel) selectorData.channel; + if (interest != 0) { + selectorData.selKey = socketChannel.register(selector, + interest); + } + + } + } + } + + private void handleReadable(NioChannel ch) throws IOException { + ch.lastReadResult = 0; + if (ch.callback != null) { + ch.callback.handleReadable(ch); + } + if (ch.lastReadResult != 0 && ch.readInterest && !ch.inClosed) { + log.warning("LOOP: read interest" + + " after incomplete read"); + ch.close(); + } + } + + private void handleDataWriteable(NioChannel ch) throws IOException { + ch.lastWriteResult = 0; + if (ch.callback != null) { + ch.callback.handleWriteable(ch); + } + if (ch.lastWriteResult > 0 && ch.writeInterest) { + log.warning("SelectorThread: write interest" + + " after incomplete write, LOOP"); + } + } + + private void handleConnect(NioChannel ch, SocketChannel sc) + throws IOException, SocketException { + try { + if (!sc.finishConnect()) { + log.warning("handleConnected - finishConnect returns false"); + } + ch.sel = this; + //sc.socket().setSoLinger(true, 0); + if (debug) { + log.info("connected() " + ch + " isConnected()=" + sc.isConnected() + " " + + sc.isConnectionPending()); + } + + readInterest(ch, true); + + if (ch.callback != null) { + ch.callback.handleConnected(ch); + } + } catch (Throwable t) { + log.warning("Error in connect, closing "); + close(ch, t); + try { + if (ch.callback != null) { + ch.callback.handleConnected(ch); + } + } catch(Throwable t1) { + log.warning("Double error in connect, callback broken too"); + t1.printStackTrace(); + } + + } + } + + private void handleAccept(NioChannel ch, SelectionKey sk) + throws IOException, ClosedChannelException { + SelectableChannel selc = sk.channel(); + ServerSocketChannel ssc=(ServerSocketChannel)selc; + SocketChannel sockC = ssc.accept(); + sockC.configureBlocking(false); + + NioChannel acceptedChannel = new NioChannel(this); + acceptedChannel.selKey = sockC.register(selector, + SelectionKey.OP_READ, + acceptedChannel); + acceptedChannel.channel = sockC; + + synchronized (active) { + active.add(acceptedChannel); + } + + // Find the callback for the new socket + if (ch.callback != null) { + // TODO: use future ! + try { + ch.callback.handleConnected(acceptedChannel); + } catch (Throwable t) { + log.log(Level.SEVERE, "SelectorThread: Channel error, closing ", t); + acceptedChannel.lastException = t; + acceptedChannel.close(); + } + } + + //sk.interestOps(sk.interestOps() | SelectionKey.OP_ACCEPT); + if (debug) { + log.info("handleAccept " + ch); + } + } + + + public void shutdownOutput(NioChannel ch) throws IOException { + Channel channel = ch.channel; + if (channel instanceof SocketChannel) { + SocketChannel sc = (SocketChannel) channel; + if (sc.isOpen() && sc.isConnected()) { + sc.socket().shutdownOutput(); // TCP end to the other side + } + } + } + + /** + * Called from the IO thread + */ + private void closeIOThread(NioChannel ch, boolean remove) { + SelectionKey sk = (SelectionKey) ch.selKey; + Channel channel = ch.channel; + try { + synchronized(closeInterest) { + if (ch.closeCalled) { + if (debug) { + log.severe("Close called 2x "); + } + return; + } + ch.closeCalled = true; + int o = opened.decrementAndGet(); + if (debug) { + log.info("-------------> close: " + ch + " t=" + ch.lastException); + } + if (sk != null) { + if (sk.isValid()) { + sk.interestOps(0); + } + sk.cancel(); + ch.selKey = null; + } + + if (channel instanceof SocketChannel) { + SocketChannel sc = (SocketChannel) channel; + + if (sc.isConnected()) { + //System.err.println("Close socket, opened=" + o); + try { + sc.socket().shutdownInput(); + } catch(IOException io1) { + } + try { + sc.socket().shutdownOutput(); // TCP end to the other side + } catch(IOException io1) { + } + sc.socket().close(); + } + } + channel.close(); + + closed.incrementAndGet(); + + if (ch.callback != null) { + ch.callback.handleClosed(ch); + } + // remove from active - false only if already removed + if (remove) { + synchronized (active) { + boolean removed = active.remove(ch); + } + } + } + } catch (IOException ex2) { + log.severe("SelectorThread: Error closing socket " + ex2); + ex2.printStackTrace(); + } + } + + // --------------- Socket op abstractions ------------ + + public int readNonBlocking(NioChannel selectorData, ByteBuffer bb) + throws IOException { + try { + int off = bb.position(); + + int done = 0; + + done = ((SocketChannel) selectorData.channel).read(bb); + + if (debug) { + log.info("-------------readNB rd=" + done + " bb.limit=" + + bb.limit() + " pos=" + bb.position() + " " + selectorData); + } + if (done > 0) { + if (debug) { + if (!bb.isDirect()) { + String s = new String(bb.array(), off, + bb.position() - off); + log.info("Data:\n" + s); + } else { + log.info("Data: " + bb.toString()); + } + } + selectorData.zeroReads = 0; + } else if (done < 0) { + if (debug) { + log.info("SelectorThread: EOF while reading"); + } + } else { + // need more... + if (selectorData.lastReadResult == 0) { + selectorData.zeroReads++; + if (selectorData.zeroReads > 6) { + log.severe("LOOP 0 reading "); + selectorData.lastException = new IOException("Polling read"); + selectorData.close(); + return -1; + } + } + } + selectorData.lastReadResult = done; + return done; + } catch(IOException ex) { + if (debug) { + log.info("readNB error rd=" + -1 + " bblen=" + + (bb.limit() - bb.position()) + " " + selectorData + " " + ex); + } + // common case: other side closed the connection. No need for trace + if (ex.toString().indexOf("Connection reset by peer") < 0) { + ex.printStackTrace(); + } + selectorData.lastException = ex; + selectorData.close(); + return -1; + } + } + + /** + * May be called from any thread + */ + public int writeNonBlocking(NioChannel selectorData, ByteBuffer bb) + throws IOException { + try { + if (debug) { + log.info("writeNB pos=" + bb.position() + " len=" + + (bb.limit() - bb.position()) + " " + selectorData); + if (!bb.isDirect()) { + String s = new String(bb.array(), bb.position(), + + bb.limit() - bb.position()); + log.info("Data:\n" + s); + } + } + if (selectorData.writeInterest) { + // writeInterest will be false after a callback, if it is + // set it means we want to wait for the callback. + if (debug) { + log.info("Prevent writeNB when writeInterest is set"); + } + return 0; + } + + int done = 0; + done = ((SocketChannel) selectorData.channel).write(bb); + selectorData.lastWriteResult = done; + return done; + } catch(IOException ex) { + if (debug) { + log.info("writeNB error pos=" + bb.position() + " len=" + + (bb.limit() - bb.position()) + " " + selectorData + " " + + ex); + } + //ex.printStackTrace(); + selectorData.lastException = ex; + selectorData.close(); + throw ex; + // return -1; + } + } + + public int getPort(NioChannel sd, boolean remote) { + SocketChannel socketChannel = (SocketChannel) sd.channel; + + if (remote) { + return socketChannel.socket().getPort(); + } else { + return socketChannel.socket().getLocalPort(); + } + } + + public InetAddress getAddress(NioChannel sd, boolean remote) { + SocketChannel socketChannel = (SocketChannel) sd.channel; + + if (remote) { + return socketChannel.socket().getInetAddress(); + } else { + return socketChannel.socket().getLocalAddress(); + } + } + + /** + */ + public void connect(String host, int port, NioChannelCallback cstate) + throws IOException { + connect(new InetSocketAddress(host, port), cstate); + } + + + public void connect(SocketAddress sa, NioChannelCallback cstate) + throws IOException { + connect(sa, cstate, null); + } + + public void connect(SocketAddress sa, NioChannelCallback cstate, + NioChannel filter) + throws IOException { + + SocketChannel socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + NioChannel selectorData = new NioChannel(this); + selectorData.sel = this; + selectorData.callback = cstate; + selectorData.channel = socketChannel; + selectorData.channel = socketChannel; // no key + + socketChannel.connect(sa); + opened.incrementAndGet(); + + synchronized (connectAcceptInterest) { + connectAcceptInterest.add(selectorData); + } + selector.wakeup(); + } + + // TODO + public void configureSocket(ByteChannel ch, + boolean noDelay) throws IOException { + SocketChannel sockC = (SocketChannel) ch; + sockC.socket().setTcpNoDelay(noDelay); + } + + // TODO + public void setSocketOptions(NioChannel selectorData, + int linger, + boolean tcpNoDelay, + int socketTimeout) + throws IOException { + + SocketChannel socketChannel = + (SocketChannel) selectorData.channel; + Socket socket = socketChannel.socket(); + + if(linger >= 0 ) + socket.setSoLinger( true, linger); + if( tcpNoDelay ) + socket.setTcpNoDelay(tcpNoDelay); + if( socketTimeout > 0 ) + socket.setSoTimeout( socketTimeout ); + } + + /** + * Can be called from multiple threads or multiple times. + */ + public int close(NioChannel selectorData, Throwable exception) throws IOException { + synchronized (closeInterest) { + if (isSelectorThread()) { + closeIOThread(selectorData, true); + return 0; + } + if (exception != null) { + selectorData.lastException = exception; + } + if (!selectorData.inClosed) { + closeInterest.add(selectorData); + } + selectorData.readInterest = false; + } + selector.wakeup(); + return 0; + } + + + + public void acceptor(NioChannelCallback cstate, + int port, + InetAddress inet, + int backlog, + int serverTimeout) + throws IOException + { + ServerSocketChannel ssc=ServerSocketChannel.open(); + ServerSocket serverSocket = ssc.socket(); + + SocketAddress sa = null; + + if (inet == null) { + sa = new InetSocketAddress( port ); + } else { + sa = new InetSocketAddress(inet, port); + } + if (backlog > 0) { + serverSocket.bind( sa , backlog); + } else { + serverSocket.bind(sa); + } + if( serverTimeout >= 0 ) { + serverSocket.setSoTimeout( serverTimeout ); + } + + + ssc.configureBlocking(false); + + NioChannel selectorData = new NioChannel(this); + selectorData.channel = ssc; // no key yet + selectorData.callback = cstate; + // key will be set in pending + + // TODO: add SSL here + + synchronized (connectAcceptInterest) { + connectAcceptInterest.add(selectorData); + } + selector.wakeup(); + } + + public void runInSelectorThread(Runnable cb) throws IOException { + if (isSelectorThread()) { + cb.run(); + } else { + synchronized (runnableInterest) { + runnableInterest.add(cb); + } + selector.wakeup(); + } + } + + /** + * Example config: + * + * www stream tcp wait USER PATH_TO_tomcatInetd.sh + * + * For a different port, you need to add it to /etc/services. + * + * 'wait' is critical - the common use of inetd is 'nowait' for + * tcp services, which doesn't make sense for java ( too slow startup + * time ). It may make sense in future with something like android VM. + * + * In 'wait' mode, inetd will pass the acceptor socket to java - so + * you can listen on port 80 and run as regular user with no special + * code and magic. + * If tomcat dies, inetd will get back the acceptor and on next connection + * restart tomcat. + * + * This also works with xinetd. It might work with Apple launchd. + * + * TODO: detect inactivity for N minutes, exist - to free resources. + */ + public void inetdAcceptor(NioChannelCallback cstate) throws IOException { + SelectorProvider sp=SelectorProvider.provider(); + + Channel ch=sp.inheritedChannel(); + if(ch!=null ) { + log.info("Inherited: " + ch.getClass().getName()); + // blocking mode + ServerSocketChannel ssc=(ServerSocketChannel)ch; + ssc.configureBlocking(false); + + NioChannel selectorData = new NioChannel(this); + selectorData.channel = ssc; + selectorData.callback = cstate; + + synchronized (connectAcceptInterest) { + connectAcceptInterest.add(selectorData); + } + selector.wakeup(); + } else { + log.severe("No inet socket "); + throw new IOException("Invalid inheritedChannel"); + } + } + + // -------------- Housekeeping ------------- + /** + * Same as APR connector - iterate over tasks, get + * smallest timeout + * @throws IOException + */ + void updateSleepTimeAndProcessTimeouts(long now) + throws IOException { + long min = Long.MAX_VALUE; + // TODO: test with large sets, maybe sort + synchronized (active) { + Iterator activeIt = active.iterator(); + + while(activeIt.hasNext()) { + NioChannel selectorData = activeIt.next(); + if (! selectorData.channel.isOpen()) { + if (debug) { + log.info("Found closed socket, removing " + + selectorData.channel); + } +// activeIt.remove(); +// selectorData.close(); + } + + long t = selectorData.nextTimeEvent; + if (t == 0) { + continue; + } + if (t < now) { + // Timeout + if (debug) { + log.info("Time event " + selectorData); + } + if (selectorData.timeEvent != null) { + selectorData.timeEvent.run(); + } + // TODO: make sure this is updated if it was selected + continue; + } + if (t < min) { + min = t; + } + } + } + long nextSleep = min - now; + if (nextSleep > maxSleep) { + sleepTime = maxSleep; + } else if (nextSleep < minSleep) { + sleepTime = minSleep; + } else { + sleepTime = nextSleep; + } + nextWakeup = now + sleepTime; + } + + /** + * Request a callback whenever data can be written. + * When the callback is invoked, the write interest is removed ( to avoid + * looping ). If the write() operation doesn't complete, you must call + * writeInterest - AND stop writing, some implementations will throw + * exception. write() will actually attempt to detect this and avoid the + * error. + * + * @param sc + */ + public void writeInterest(NioChannel selectorData) { + // TODO: suspended ? + + SelectionKey sk = (SelectionKey) selectorData.selKey; + if (!sk.isValid()) { + return; + } + selectorData.writeInterest = true; + int interest = sk.interestOps(); + if ((interest & SelectionKey.OP_WRITE) != 0) { + return; + } + if (Thread.currentThread() == selectorThread) { + interest = + interest | SelectionKey.OP_WRITE; + sk.interestOps(interest); + if (debug) { + log.info("Write interest " + selectorData + " i=" + interest); + } + return; + } + if (debug) { + log.info("Pending write interest " + selectorData); + } + synchronized (writeInterest) { + writeInterest.add(selectorData); + } + selector.wakeup(); + } + + + public void readInterest(NioChannel selectorData, boolean b) throws IOException { + if (Thread.currentThread() == selectorThread) { + selectorData.readInterest = b; + selThreadReadInterest(selectorData); + return; + } + SelectionKey sk = (SelectionKey) selectorData.selKey; + if (sk == null) { + close(selectorData, null); + return; + } + int interest = sk.interestOps(); + selectorData.readInterest = b; + if (b && (interest & SelectionKey.OP_READ) != 0) { + return; + } + if (!b && (interest & SelectionKey.OP_READ) == 0) { + return; + } + // Schedule the interest update. + synchronized (readInterest) { + readInterest.add(selectorData); + } + if (debug) { + log.info("Registering pending read interest"); + } + selector.wakeup(); + } + + + private void selThreadReadInterest(NioChannel selectorData) throws IOException { + SelectionKey sk = (SelectionKey) selectorData.selKey; + if (sk == null) { + if (selectorData.readInterest) { + if (debug) { + log.info("Register again for read interest"); + } + SocketChannel socketChannel = + (SocketChannel) selectorData.channel; + if (socketChannel.isOpen()) { + selectorData.sel = this; + selectorData.selKey = + socketChannel.register(selector, + SelectionKey.OP_READ, selectorData); + selectorData.channel = socketChannel; + } + } + return; + } + if (!sk.isValid()) { + return; + } + int interest = sk.interestOps(); + if (sk != null && sk.isValid()) { + if (selectorData.readInterest) { +// if ((interest | SelectionKey.OP_READ) != 0) { +// return; +// } + interest = + interest | SelectionKey.OP_READ; + } else { +// if ((interest | SelectionKey.OP_READ) == 0) { +// return; +// } + interest = + interest & ~SelectionKey.OP_READ; + } + if (interest == 0) { + if (!selectorData.inClosed) { + new Throwable().printStackTrace(); + log.warning("No interest(rd removed) " + selectorData); + } + // TODO: should we remove it ? It needs to be re-activated + // later. + sk.cancel(); //?? + selectorData.selKey = null; + } else { + sk.interestOps(interest); + } + if (debug) { + log.info(((selectorData.readInterest) + ? "RESUME read " : "SUSPEND read ") + + selectorData); + } + } + } + + + private void processPendingConnectAccept() throws IOException { + synchronized (connectAcceptInterest) { + Iterator ci = connectAcceptInterest.iterator(); + + while (ci.hasNext()) { + NioChannel selectorData = ci.next(); + + // Find host, port - initiate connection + try { + // Accept interest ? + if (selectorData.channel instanceof ServerSocketChannel) { + ServerSocketChannel socketChannel = + (ServerSocketChannel) selectorData.channel; + selectorData.sel = this; + selectorData.selKey = + socketChannel.register(selector, + SelectionKey.OP_ACCEPT, selectorData); + + selectorData.channel = socketChannel; + synchronized (active) { + active.add(selectorData); + } + if (debug) { + log.info("Pending acceptor added: " + selectorData); + } + } else { + SocketChannel socketChannel = + (SocketChannel) selectorData.channel; + selectorData.sel = this; + selectorData.selKey = + socketChannel.register(selector, + SelectionKey.OP_CONNECT, selectorData); + synchronized (active) { + active.add(selectorData); + } + if (debug) { + log.info("Pending connect added: " + selectorData); + } + } + } catch (Throwable e) { + log.log(Level.SEVERE, "error registering connect/accept", + e); + } + } + connectAcceptInterest.clear(); + } + } + + private void processPending() throws IOException { + if (closeInterest.size() > 0) { + synchronized (closeInterest) { + List closeList = new ArrayList(closeInterest); + closeInterest.clear(); + + Iterator ci = closeList.iterator(); + + while (ci.hasNext()) { + try { + NioChannel selectorData = ci.next(); + closeIOThread(selectorData, true); + } catch (Throwable t) { + t.printStackTrace(); + } + } + } + } + processPendingConnectAccept(); + processPendingReadWrite(); + + if (runnableInterest.size() > 0) { + synchronized (runnableInterest) { + Iterator ci = runnableInterest.iterator(); + while (ci.hasNext()) { + Runnable cstate = ci.next(); + try { + cstate.run(); + } catch (Throwable t) { + t.printStackTrace(); + } + if (debug) { + log.info("Run in selthread: " + cstate); + } + } + runnableInterest.clear(); + } + } + //processPendingUpdateCallback(); + } + + private void processPendingReadWrite() throws IOException { + // Update interest + if (readInterest.size() > 0) { + synchronized (readInterest) { + Iterator ci = readInterest.iterator(); + while (ci.hasNext()) { + NioChannel cstate = ci.next(); + selThreadReadInterest(cstate); + if (debug) { + log.info("Read interest added: " + cstate); + } + } + readInterest.clear(); + } + } + if (writeInterest.size() > 0) { + synchronized (writeInterest) { + Iterator ci = writeInterest.iterator(); + while (ci.hasNext()) { + NioChannel cstate = ci.next(); + // Fake callback - will update as side effect + handleDataWriteable(cstate); + if (debug) { + log.info("Write interest, calling dataWritable: " + cstate); + } + } + writeInterest.clear(); + } + } + } + + + + protected boolean isSelectorThread() { + return Thread.currentThread() == selectorThread; + } + +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java new file mode 100644 index 000000000..8cd33ee2b --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketConnector.java @@ -0,0 +1,127 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import org.apache.tomcat.lite.io.NioChannel.NioChannelCallback; + +/** + * Class for handling sockets. It manages a pool of SelectorThreads, fully + * non-blocking. There is no caching or buffer management. SelectorChannel + * represents on connection. + * + * In the old types, the connector was socket-centric, and quite ugly. After + * many refactoring the buffers ( buckets and brigade ) and callbacks are + * used everywhere, and the sockets play a supporting role. + * + * TODO: discover if APR is available and use it, or fall back to NIO. + * + * @author Costin Manolache + */ +public class SocketConnector extends IOConnector { + static Logger log = Logger.getLogger(SocketConnector.class.getName()); + static boolean debug = false; + + // TODO: pool, balanced usage + // TODO: bind into OM or callback when created + + private NioThread selector; + + // For resolving DNS ( i.e. connect ) + Executor threadPool = Executors.newCachedThreadPool(); + + public SocketConnector() { + } + + /** + * This may be blocking - involves host resolution, connect. + * If the IP address is provided - it shouldn't block. + */ + @Override + public void connect(final String host, final int port, + final IOConnector.ConnectedCallback sc) throws IOException { + final SocketIOChannel ioch = new SocketIOChannel(this, null, host + ":" + port); + ioch.setConnectedCallback(sc); + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + getSelector().connect(new InetSocketAddress(host, port), ioch, null); + } catch (Throwable e) { + e.printStackTrace(); + try { + sc.handleConnected(ioch); + ioch.close(); + } catch (Throwable e1) { + e1.printStackTrace(); + } + } + } + }); + } + + /** + * Create a new server socket, register the callback. + * If port == 0 it'll use the inherited channel, i.e. inetd mode. + * TODO: if port == -1, detect a free port. May block. + */ + public void acceptor(final IOConnector.ConnectedCallback sc, + final CharSequence address, Object extra) + throws IOException + { + final int port = Integer.parseInt(address.toString()); + NioChannelCallback acceptCb = new NioChannelCallback() { + @Override + public void handleClosed(NioChannel ch) throws IOException { + } + + @Override + public void handleConnected(NioChannel ch) throws IOException { + SocketIOChannel ioch = new SocketIOChannel(SocketConnector.this, + ch, ":" + port); + sc.handleConnected(ioch); + } + + @Override + public void handleReadable(NioChannel ch) throws IOException { + } + + @Override + public void handleWriteable(NioChannel ch) throws IOException { + } + }; + + if (port == -1) { + // TODO: find an unused port + } else if (port == 0) { + getSelector().inetdAcceptor(acceptCb); + } else { + getSelector().acceptor(acceptCb, port, null, 200, 20000); + } + } + + static int id = 0; + + synchronized NioThread getSelector() { + if (selector == null) { + String name = "SelectorThread-" + id++; + selector = new NioThread(name, true); + } + + return selector; + } + + public void stop() { + getSelector().stop(); + } + + + + // TODO: suspendAccept(boolean) + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketIOChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketIOChannel.java new file mode 100644 index 000000000..785f4e02e --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SocketIOChannel.java @@ -0,0 +1,266 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; + +import org.apache.tomcat.lite.io.NioChannel.NioChannelCallback; + +/** + * Buffered socket channel + */ +public class SocketIOChannel extends IOChannel implements NioChannelCallback { + IOBuffer out; + IOBuffer in; + + NioChannel ch; + + SocketIOChannel(IOConnector connector, NioChannel data, + String target) + throws IOException { + this.connector = connector; + in = new IOBuffer(this); + out = new IOBuffer(this); + this.ch = data; + setOutBuffer(out); + setChannel(data); + this.target = target; + } + + void setChannel(NioChannel data) { + this.ch = data; + if (ch != null) { + ch.callback = this; + } + } + + + @Override + public IOBuffer getIn() { + return in; + } + + @Override + public IOBuffer getOut() { + return out; + } + + /** + * Both in and out open + */ + public boolean isOpen() { + if (ch == null) { + return false; + } + return ch.isOpen() && ch.channel != null && + ch.channel.isOpen() && !getIn().isAppendClosed() && + !getOut().isAppendClosed(); + } + + NioChannel getSelectorChannel() { + return ch; + } + + public String toString() { + return ch.toString(); + } + + public void setOutBuffer(IOBuffer out) { + this.out = out; + } + + ByteBuffer flushBuffer; + + /** + * Send as much as possible. + * + * Adjust write interest so we can send more when possible. + */ + private void flush(NioChannel ch) throws IOException { + synchronized (this) { + if (ch == null) { + if (out.isClosedAndEmpty()) { + return; + } + throw new IOException("flush() with closed socket"); + } + while (true) { + if (out.isClosedAndEmpty()) { + ch.shutdownOutput(); + break; + } + BBucket bb = out.peekFirst(); + if (bb == null) { + break; + } + flushBuffer = getReadableBuffer(flushBuffer, bb); + int before = flushBuffer.position(); + + int done = 0; + while (flushBuffer.remaining() > 0) { + try { + done = ch.write(flushBuffer); + } catch (IOException ex) { + // can't write - was closed ! + done = -1; + } + + if (done < 0) { + ch.close(); + out.close(); + handleFlushed(this); + //throw new IOException("Closed while writting "); + return; + } + if (done == 0) { + bb.position(flushBuffer.position()); + ch.writeInterest(); // it is cleared on next dataWriteable + return; + } + } + releaseReadableBuffer(flushBuffer, bb); + } + handleFlushed(this); + + } + } + + /** + * Data available for read, called from IO thread. + * You MUST read all data ( i.e. until read() returns 0). + * + * OP_READ remain active - call readInterest(false) to disable - + * for example to suspend reading if buffer is full. + */ + public void handleReceived(IOChannel net) throws IOException { + // All data will go to currentReceiveBuffer, until it's full. + // Then a new buffer will be allocated/pooled. + + // When we fill the buffers or finish this round of reading - + // we place the Buckets in the queue, as 'readable' buffers. + boolean newData = false; + try { + synchronized(in) { + // data between 0 and position + while (true) { + if (in.isAppendClosed()) { // someone closed me ? + ch.inputClosed(); // remove read interest. + // if outClosed - close completely + super.sendHandleReceivedCallback(); + return; + } + + ByteBuffer bb = in.getWriteBuffer(); + int read = ch.read(bb); + in.releaseWriteBuffer(read); + + if (in == null) { + // Detached. + if (newData) { + sendHandleReceivedCallback(); + } + return; + } + + if (read < 0) { + ch.inputClosed(); + // mark the in buffer as closed + in.close(); + sendHandleReceivedCallback(); + return; + } + if (read == 0) { + if (newData) { + super.sendHandleReceivedCallback(); + } + return; + } + newData = true; + } + } + } catch (Throwable t) { + close(); + if (t instanceof IOException) { + throw (IOException) t; + } else { + throw new IOException(t.toString()); + } + } + } + + public static final ByteBuffer getReadableBuffer(ByteBuffer orig, BBucket bucket) { + if (orig == null || orig.array() != bucket.array()) { + orig = ByteBuffer.wrap(bucket.array()); + } + orig.position(bucket.position()); + orig.limit(bucket.limit()); + return orig; + } + + public static final void releaseReadableBuffer(ByteBuffer bb, BBucket bucket) { + bucket.position(bb.position()); + } + + + + public void readInterest(boolean b) throws IOException { + ch.readInterest(b); + } + + public InetAddress getAddress(boolean remote) { + return ch.getAddress(remote); + } + + public int getPort(boolean remote) { + return ch.getPort(remote); + } + + public String getRemoteAddress() { + return getAddress(true).toString(); + } + + public int getRemotePort() { + return getPort(true); + } + + + public void startSending() throws IOException { + flush(ch); + } + + public void shutdownOutput() throws IOException { + getOut().close(); + if (ch != null) { + startSending(); + } + } + + @Override + public void handleClosed(NioChannel ch) throws IOException { + closed(); // our callback. + } + + public void closed() throws IOException { + getIn().close(); + sendHandleReceivedCallback(); + //super.closed(); + } + + @Override + public void handleConnected(NioChannel ch) throws IOException { + setChannel(ch); + connectedCallback.handleConnected(this); + } + + @Override + public void handleReadable(NioChannel ch) throws IOException { + handleReceived(this); + } + + @Override + public void handleWriteable(NioChannel ch) throws IOException { + flush(ch); + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslChannel.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslChannel.java new file mode 100644 index 000000000..7c5b708cc --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslChannel.java @@ -0,0 +1,421 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; + + +public class SslChannel extends IOChannel implements Runnable { + + static Logger log = Logger.getLogger("SSL"); + + SSLEngine sslEngine; + // Last result + SSLEngineResult unwrapR; + + boolean handshakeDone = false; + boolean handshakeInProgress = false; + boolean flushing = false; + + IOBuffer in = new IOBuffer(this); + IOBuffer out = new IOBuffer(this); + + public SslChannel() { + + } + + ByteBuffer myAppOutData; + ByteBuffer myNetOutData; + + /* + * Special: SSL works in packet mode, and we may receive an incomplete + * packet. This should be in compacted write mode (i.e. data from 0 to pos, + * limit at end ) + */ + ByteBuffer myNetInData; + + ByteBuffer myAppInData; + boolean client = true; + + private SSLContext sslCtx; + + private boolean closeHandshake = false; + + private void initSsl() throws GeneralSecurityException { + if (sslEngine != null) { + return; + } + + if (client) { + sslEngine = sslCtx.createSSLEngine(); + sslEngine.setUseClientMode(client); + } else { + sslEngine = sslCtx.createSSLEngine(); + sslEngine.setUseClientMode(false); + String[] cs = sslEngine.getEnabledCipherSuites(); + cs =sslEngine.getSupportedCipherSuites(); + + } + SSLSession session = sslEngine.getSession(); + + myAppOutData = ByteBuffer.allocate(session.getApplicationBufferSize()); + myNetOutData = ByteBuffer.allocate(session.getPacketBufferSize()); + myAppInData = ByteBuffer.allocate(session.getApplicationBufferSize()); + myNetInData = ByteBuffer.allocate(session.getPacketBufferSize()); + myNetInData.flip(); + myNetOutData.flip(); + myAppInData.flip(); + myAppOutData.flip(); + + // TODO: enable anon suites + //sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites()); + } + + public SslChannel withServer() { + client = false; + return this; + } + + + @Override + public void setSink(IOChannel net) { + try { + initSsl(); + super.setSink(net); + } catch (GeneralSecurityException e) { + log.log(Level.SEVERE, "Error initializing ", e); + } + } + + @Override + public IOBuffer getIn() { + return in; + } + + @Override + public IOBuffer getOut() { + return out; + } + + /** + * Typically called when a dataReceived callback is passed up. + * It's up to the higher layer to decide if it can handle more data + * and disable read interest and manage its buffers. + * + * We have to use one buffer. + * @throws IOException + */ + public int processInput(IOBuffer netIn, IOBuffer appIn) throws IOException { + if (log.isLoggable(Level.FINEST)) { + log.finest("JSSE: processInput " + handshakeInProgress + " " + netIn.getBufferCount()); + } + if (!handshakeDone && !handshakeInProgress) { + handshakeInProgress = true; + handleHandshking(); + return 0; + } + if (handshakeInProgress) { + return 0; // leave it there + } + return processRealInput(netIn, appIn); + } + + private synchronized int processRealInput(IOBuffer netIn, IOBuffer appIn) throws IOException { + int rd = 0; + boolean needsMore = true; + + while (needsMore) { + if (netIn.isClosedAndEmpty()) { + appIn.close(); + sendHandleReceivedCallback(); + return -1; + } + myNetInData.compact(); + int rdNow = netIn.read(myNetInData); + myNetInData.flip(); + if (rdNow == 0) { + return rd; + } + if (rdNow == -1) { + appIn.close(); + sendHandleReceivedCallback(); + return rd; + } + + while (myNetInData.remaining() > 0) { + myAppInData.compact(); + unwrapR = sslEngine.unwrap(myNetInData, myAppInData); + myAppInData.flip(); + if (myAppInData.remaining() > 0) { + in.write(myAppInData); // all will be written + } + if (unwrapR.getStatus() == Status.CLOSED) { + in.close(); + if (unwrapR.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) { + // TODO: send/receive one more packet ( handshake mode ? ) + handshakeInProgress = true; + closeHandshake = true; + handleHandshking(); + + startSending(); + } + break; + } + + if (unwrapR.getHandshakeStatus() == HandshakeStatus.NEED_TASK) { + tasks(); + } + if (unwrapR.getStatus() == Status.BUFFER_OVERFLOW || + unwrapR.getStatus() == Status.BUFFER_UNDERFLOW) { + log.severe("Unhandled overflow"); + break; + } + } + sendHandleReceivedCallback(); + + + } + return rd; + } + + protected SSLEngineResult.HandshakeStatus tasks() { + Runnable r = null; + while ( (r = sslEngine.getDelegatedTask()) != null) { + r.run(); + } + return sslEngine.getHandshakeStatus(); + } + + static ByteBuffer EMPTY = ByteBuffer.allocate(0); + + public void startSending() throws IOException { + + flushing = true; + + if (handshakeInProgress) { + return; // don't bother me. + } + + if (!handshakeDone) { + handshakeInProgress = true; + handleHandshking(); + return; // can't write yet. + } + startRealSending(); + } + + public void close() throws IOException { + sslEngine.closeOutbound(); // mark as closed + myNetOutData.compact(); + SSLEngineResult wrap = sslEngine.wrap(EMPTY, + myNetOutData); + myNetOutData.flip(); + net.getOut().write(myNetOutData); + // TODO: timer to close socket if we don't get + // clean close handshake + } + + private synchronized void startRealSending() throws IOException { + IOBuffer netOut = net.getOut(); + while (true) { + + myAppOutData.compact(); + int rd = out.read(myAppOutData); + myAppOutData.flip(); + if (rd == 0) { + break; + } + if (rd < 0) { + close(); + break; + } + + myNetOutData.compact(); + SSLEngineResult wrap = sslEngine.wrap(myAppOutData, + myNetOutData); + myNetOutData.flip(); + net.getOut().write(myNetOutData); + + if (wrap != null) { + switch (wrap.getStatus()) { + case BUFFER_UNDERFLOW: { + break; + } + case OK: { + break; + } + case BUFFER_OVERFLOW: { + throw new IOException("Overflow"); + } + } + } + } + + net.startSending(); + } + + + + // SSL handshake require slow tasks - that will need to be executed in a + // thread anyways. Better to keep it simple ( the code is very complex ) - + // and do the initial handshake in a thread, not in the IO thread. + // We'll need to unregister and register again from the selector. + private void handleHandshking() { + if (log.isLoggable(Level.FINEST)) { + log.finest("Starting handshake"); + } + handshakeInProgress = true; + + new Thread(this).start(); + } + + private void endHandshake() throws IOException { + if (log.isLoggable(Level.FINEST)) { + log.finest("Handshake done"); + } + handshakeDone = true; + handshakeInProgress = false; + if (flushing) { + flushing = false; + startSending(); + } + } + + /** + * Actual handshake magic, in background thread. + */ + public void run() { + try { + boolean initial = true; + SSLEngineResult wrap = null; + + HandshakeStatus hstatus = sslEngine.getHandshakeStatus(); + if (!closeHandshake && + (hstatus == HandshakeStatus.NOT_HANDSHAKING || initial)) { + sslEngine.beginHandshake(); + hstatus = sslEngine.getHandshakeStatus(); + } + + long t0 = System.currentTimeMillis(); + + while (hstatus != HandshakeStatus.FINISHED) { + if (wrap != null && wrap.getStatus() == Status.CLOSED) { + break; + } + if (log.isLoggable(Level.FINEST)) { + log.finest("-->doHandshake() loop: status = " + hstatus + " " + + sslEngine.getHandshakeStatus()); + } + + if (hstatus == HandshakeStatus.NEED_WRAP) { + // || initial - for client + initial = false; + myNetOutData.compact(); + + wrap = sslEngine.wrap(myAppOutData, myNetOutData); + myNetOutData.flip(); + + hstatus = wrap.getHandshakeStatus(); + + net.getOut().write(myNetOutData); + net.startSending(); + + + } else if (hstatus == HandshakeStatus.NEED_UNWRAP) { + + while (hstatus == HandshakeStatus.NEED_UNWRAP) { + // If we have few remaining bytes - process them + if (myNetInData.remaining() > 0) { + myAppInData.clear(); + wrap = sslEngine.unwrap(myNetInData, myAppInData); + hstatus = wrap.getHandshakeStatus(); + + myAppInData.flip(); + if (myAppInData.remaining() > 0) { + throw new IOException("Unexpected data"); + } + if (wrap.getStatus() == Status.CLOSED) { + break; + } + } + // Still need unwrap + if (wrap == null + || wrap.getStatus() == Status.BUFFER_UNDERFLOW + || (hstatus == HandshakeStatus.NEED_UNWRAP && myNetInData.remaining() == 0)) { + myNetInData.compact(); + int rd = net.getIn().read(myNetInData); + myNetInData.flip(); + if (rd == 0) { + net.getIn().waitData(10000); + continue; + } + if (rd < 0) { + // in closed + break; + } + } + if (log.isLoggable(Level.FINEST)) { + log.finest("Unwrap chunk done " + hstatus + " " + wrap + + " " + sslEngine.getHandshakeStatus()); + } + + } + + // rd may have some input bytes. + } else if (hstatus == HandshakeStatus.NEED_TASK) { + long t0task = System.currentTimeMillis(); + Runnable r; + while ((r = sslEngine.getDelegatedTask()) != null) { + r.run(); + } + long t1task = System.currentTimeMillis(); + hstatus = sslEngine.getHandshakeStatus(); + if (log.isLoggable(Level.FINEST)) { + log.finest("Tasks done in " + (t1task - t0task) + " new status " + + hstatus); + } + + } + if (hstatus == HandshakeStatus.NOT_HANDSHAKING) { + //log.warning("NOT HANDSHAKING " + this); + break; + } + } + endHandshake(); + processRealInput(net.getIn(), in); + } catch (Throwable t) { + log.log(Level.SEVERE, "Error handshaking", t); + try { + close(); + net.close(); + } catch (IOException ex) { + log.log(Level.SEVERE, "Error closing", ex); + } + } + } + + + @Override + public void handleReceived(IOChannel ch) throws IOException { + processInput(net.getIn(), in); + sendHandleReceivedCallback(); + } + + public SslChannel setSslContext(SSLContext sslCtx) { + this.sslCtx = sslCtx; + return this; + } +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslConnector.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslConnector.java new file mode 100644 index 000000000..39a313991 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/SslConnector.java @@ -0,0 +1,375 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyManagementException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAKeyGenParameterSpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.concurrent.Executor; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509TrustManager; + + +public class SslConnector extends IOConnector { + + public static class BasicTrustManager implements X509TrustManager { + + private X509Certificate[] chain; + + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + this.chain = chain; + } + + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + this.chain = chain; + } + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + + public static TrustManager[] trustAllCerts = new TrustManager[] { + new BasicTrustManager() }; + + static final boolean debug = false; + + static { + if (debug) { + System.setProperty("javax.net.debug", "ssl"); + } + } + IOConnector net; + private KeyManager[] keyManager; + SSLContext sslCtx; + boolean server; + private TrustManager[] trustManagers; + + Executor handshakeExecutor; + + public SslConnector() { + } + + public void start() { + + } + + public IOConnector getNet() { + if (net == null) { + try { + sslCtx = SSLContext.getInstance("TLS"); + if (trustManagers == null) { + trustManagers = + new TrustManager[] {new BasicTrustManager()}; + + } + sslCtx.init(keyManager, trustManagers, null); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (KeyManagementException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + net = new SocketConnector(); + } + return net; + } + + static int id = 0; + + @Override + public void acceptor(final ConnectedCallback sc, CharSequence port, Object extra) + throws IOException { + getNet().acceptor(new ConnectedCallback() { + @Override + public void handleConnected(IOChannel ch) throws IOException { + IOChannel first = ch; + if (debug) { + DumpChannel dch = new DumpChannel("S-ENC-" + id ); + ch.addFilterAfter(dch); + first = dch; + } + + IOChannel sslch = new SslChannel() + .setSslContext(sslCtx) + .withServer(); + sslch.setSink(first); + first.addFilterAfter(sslch); + + if (debug) { + DumpChannel dch2 = new DumpChannel("S-CLR-" + id); + sslch.addFilterAfter(dch2); + sslch = dch2; + id++; + } + + sc.handleConnected(sslch); + } + }, port, extra); + } + + @Override + public void connect(String host, int port, final ConnectedCallback sc) + throws IOException { + getNet().connect(host, port, new ConnectedCallback() { + + @Override + public void handleConnected(IOChannel ch) throws IOException { + IOChannel first = ch; + if (debug) { + DumpChannel dch = new DumpChannel("ENC-" + id); + ch.addFilterAfter(dch); + first = dch; + } + + IOChannel sslch = new SslChannel() + .setSslContext(sslCtx); + sslch.setSink(first); + first.addFilterAfter(sslch); + + if (debug) { + DumpChannel dch2 = new DumpChannel("CLR-" + id); + sslch.addFilterAfter(dch2); + sslch = dch2; + id++; + } + + sc.handleConnected(sslch); + } + + }); + } + + public SslConnector withKeyManager(KeyManager[] kms) { + this.keyManager = kms; + return this; + } + + public SslConnector setKeysFile(String file, String pass) throws IOException { + return setKeys(new FileInputStream(file), pass); + } + + public SslConnector setKeysResource(String res, String pass) throws IOException { + return setKeys(this.getClass().getClassLoader().getResourceAsStream(res), + pass); + } + + public SslConnector setKeys(InputStream file, String pass) { + char[] passphrase = pass.toCharArray(); + KeyStore ks; + try { + ks = KeyStore.getInstance("JKS"); + ks.load(file, passphrase); + KeyManagerFactory kmf = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, passphrase); + + TrustManagerFactory tmf = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ks); + + keyManager = kmf.getKeyManagers(); + trustManagers = tmf.getTrustManagers(); + } catch (KeyStoreException e) { + // TODO Auto-generated catch block + }catch (NoSuchAlgorithmException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (UnrecoverableKeyException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return this; + } + + public SslConnector setKeys(X509Certificate cert, PrivateKey privKey) { + keyManager = new KeyManager[] { + new TestKeyManager(cert, privKey) + }; + return this; + } + + /** + * Initialize using a PEM certificate and key bytes. + * ( TODO: base64 dep to set the key as PEM ) + * + * + * Key was generated with + * keytool -genkey -alias server -keyalg RSA -storepass changeit + * keytool -selfcert -storepass changeit -alias server + * + * Then the bytes printed with printPrivateKey() + * + * I found no way to generate the self-signed keys from jsse + * except CLI. + * + */ + public SslConnector setKeys(String certPem, byte[] keyBytes) throws NoSuchAlgorithmException, InvalidKeySpecException, GeneralSecurityException { + // convert key + KeyFactory kf = KeyFactory.getInstance("RSA"); + PKCS8EncodedKeySpec keysp = new PKCS8EncodedKeySpec(keyBytes); + PrivateKey priv = kf.generatePrivate (keysp); + + // Convert cert pem to certificate + InputStream is = new ByteArrayInputStream(certPem.getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + final X509Certificate cert = (X509Certificate) cf.generateCertificate(is); + + setKeys(cert, priv); + + return this; + } + + public class TestKeyManager extends X509ExtendedKeyManager { + X509Certificate cert; + PrivateKey privKey; + + public TestKeyManager(X509Certificate cert2, PrivateKey privKey2) { + cert = cert2; + privKey = privKey2; + } + + public String chooseEngineClientAlias(String[] keyType, + java.security.Principal[] issuers, javax.net.ssl.SSLEngine engine) { + return "client"; + } + + public String chooseEngineServerAlias(String keyType, + java.security.Principal[] issuers, javax.net.ssl.SSLEngine engine) { + return "server"; + } + + public String chooseClientAlias(String[] keyType, + Principal[] issuers, Socket socket) { + return "client"; + } + + public String chooseServerAlias(String keyType, + Principal[] issuers, Socket socket) { + return "server"; + } + + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] {cert}; + } + + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + public PrivateKey getPrivateKey(String alias) { + + return privKey; + } + + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + } + + public static void fixUrlConnection() { + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, SslConnector.trustAllCerts, null); + javax.net.ssl.HttpsURLConnection.setDefaultSSLSocketFactory( + sc.getSocketFactory()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Utilities + public static byte[] getPrivateKeyFromStore(String file, String pass) + throws Exception { + KeyStore store = KeyStore.getInstance("JKS"); + store.load(new FileInputStream(file), pass.toCharArray()); + Key key = store.getKey("tomcat", "changeit".toCharArray()); + PrivateKey pk = (PrivateKey) key; + byte[] encoded = pk.getEncoded(); + return encoded; + } + + public static byte[] getCertificateFromStore(String file, String pass) + throws Exception { + KeyStore store = KeyStore.getInstance("JKS"); + store.load(new FileInputStream(file), pass.toCharArray()); + Certificate certificate = store.getCertificate("tomcat"); + + return certificate.getEncoded(); + } + + public static KeyPair generateRsaOrDsa(boolean rsa) throws Exception { + if (rsa) { + KeyPairGenerator keyPairGen = + KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(1024); + + RSAKeyGenParameterSpec keySpec = new RSAKeyGenParameterSpec(1024, + RSAKeyGenParameterSpec.F0); + keyPairGen.initialize(keySpec); + + KeyPair rsaKeyPair = keyPairGen.generateKeyPair(); + + return rsaKeyPair; + } else { + KeyPairGenerator keyPairGen = + KeyPairGenerator.getInstance("DSA"); + keyPairGen.initialize(1024); + + KeyPair pair = keyPairGen.generateKeyPair(); + + return pair; + } + } + + /** + * I know 2 ways to generate certs: + * - keytool + * - openssl req -x509 -nodes -days 365 \ + * -newkey rsa:1024 -keyout mycert.pem -out mycert.pem + * openssl s_server -accept 9443 -cert mycert.pem -debug -msg -state -www + */ +} \ No newline at end of file diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/UrlEncoding.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/UrlEncoding.java new file mode 100644 index 000000000..326649136 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/UrlEncoding.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.tomcat.lite.io; + +import java.io.CharConversionException; +import java.io.IOException; +import java.nio.charset.CharsetEncoder; +import java.util.BitSet; + + +/** + * Support for %xx URL encoding. + * + * @author Costin Manolache + */ +public final class UrlEncoding { + + protected static final boolean ALLOW_ENCODED_SLASH = + Boolean.valueOf( + System.getProperty( + "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", + "false")).booleanValue(); + + public UrlEncoding() { + } + + // Utilities for URL encoding. + static BitSet SAFE_CHARS_URL = new BitSet(128); + static BitSet SAFE_CHARS = new BitSet(128); + BBuffer tmpBuffer = BBuffer.allocate(1024); + CBuffer tmpCharBuffer = CBuffer.newInstance(); + + public void urlEncode(CBuffer url, CBuffer encoded, IOWriter enc) { + tmpBuffer.recycle(); + urlEncode(url, tmpBuffer, encoded, enc.getEncoder("UTF-8"), + SAFE_CHARS_URL, true, enc); + } + + public void urlEncode(String url, CBuffer encoded, IOWriter enc) { + tmpCharBuffer.recycle(); + tmpCharBuffer.append(url); + urlEncode(tmpCharBuffer, encoded, enc); + } + + /** Only works for UTF-8 or charsets preserving ascii. + * + * @param url + * @param tmpBuffer + * @param encoded + * @param utf8Enc + * @param safeChars + */ + public void urlEncode(CBuffer url, + BBuffer tmpBuffer, + CBuffer encoded, + CharsetEncoder utf8Enc, + BitSet safeChars, boolean last, IOWriter enc) { + // tomcat charset-encoded each character first. I don't think + // this is needed. + + // TODO: space to + + enc.encodeAll(url, tmpBuffer, utf8Enc, last); + byte[] array = tmpBuffer.array(); + for (int i = tmpBuffer.position(); i < tmpBuffer.limit(); i++) { + int c = array[i]; + if (safeChars.get(c)) { + encoded.append((char) c); + } else { + encoded.append('%'); + char ch = Character.forDigit((c >> 4) & 0xF, 16); + encoded.append(ch); + ch = Character.forDigit(c & 0xF, 16); + encoded.append(ch); + } + } + } + + static { + initSafeChars(SAFE_CHARS); + initSafeChars(SAFE_CHARS_URL); + SAFE_CHARS_URL.set('/'); + } + + private static void initSafeChars(BitSet safeChars) { + int i; + for (i = 'a'; i <= 'z'; i++) { + safeChars.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + safeChars.set(i); + } + for (i = '0'; i <= '9'; i++) { + safeChars.set(i); + } + // safe + safeChars.set('-'); + safeChars.set('_'); + safeChars.set('.'); + + // Dangerous: someone may treat this as " " + // RFC1738 does allow it, it's not reserved + // safeChars.set('+'); + // extra + safeChars.set('*'); + // tomcat has them - not sure if this is correct + safeChars.set('$'); // ? + safeChars.set('!'); // ? + safeChars.set('\''); // ? + safeChars.set('('); // ? + safeChars.set(')'); // ? + safeChars.set(','); // ? + } + + public void urlDecode(BBuffer bb, CBuffer dest, boolean q, + IOReader charDec) throws IOException { + // Replace %xx + tmpBuffer.append(bb); + urlDecode(tmpBuffer, q); + charDec.decodeAll(bb, dest); + } + + + public void urlDecode(BBuffer bb, CBuffer dest, + IOReader charDec) throws IOException { + // Replace %xx + tmpBuffer.append(bb); + urlDecode(tmpBuffer, true); + charDec.decodeAll(bb, dest); + } + + + /** + * URLDecode, will modify the source. This is only at byte level - + * it needs conversion to chars using the right charset. + * + * @param query Converts '+' to ' ' and allow '/' + */ + public void urlDecode(BBuffer mb, boolean query) throws IOException { + int start = mb.getOffset(); + byte buff[] = mb.array(); + int end = mb.getEnd(); + + int idx = BBuffer.indexOf(buff, start, end, '%'); + int idx2 = -1; + if (query) + idx2 = BBuffer.indexOf(buff, start, end, '+'); + if (idx < 0 && idx2 < 0) { + return; + } + + // idx will be the smallest positive inxes ( first % or + ) + if (idx2 >= 0 && idx2 < idx) + idx = idx2; + if (idx < 0) + idx = idx2; + + //boolean noSlash = !query; + + for (int j = idx; j < end; j++, idx++) { + if (buff[j] == '+' && query) { + buff[idx] = (byte) ' '; + } else if (buff[j] != '%') { + buff[idx] = buff[j]; + } else { + // read next 2 digits + if (j + 2 >= end) { + throw new CharConversionException("EOF"); + } + byte b1 = buff[j + 1]; + byte b2 = buff[j + 2]; + if (!isHexDigit(b1) || !isHexDigit(b2)) + throw new CharConversionException("isHexDigit"); + + j += 2; + int res = x2c(b1, b2); +// if (noSlash && (res == '/')) { +// throw new CharConversionException("noSlash " + mb); +// } + buff[idx] = (byte) res; + } + } + + mb.setEnd(idx); + + return; + } + + + private static boolean isHexDigit(int c) { + return ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); + } + + private static int x2c(byte b1, byte b2) { + int digit = (b1 >= 'A') ? ((b1 & 0xDF) - 'A') + 10 : (b1 - '0'); + digit *= 16; + digit += (b2 >= 'A') ? ((b2 & 0xDF) - 'A') + 10 : (b2 - '0'); + return digit; + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/WrappedException.java b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/WrappedException.java new file mode 100644 index 000000000..3d8a281a7 --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/WrappedException.java @@ -0,0 +1,40 @@ +/* + */ +package org.apache.tomcat.lite.io; + +import java.io.IOException; + +/** + * For specific exceptions - also has cause ( good if compiling against + * JDK1.5 ) + * + * @author Costin Manolache + */ +public class WrappedException extends IOException { + + public WrappedException() { + super(); + } + + public WrappedException(String message) { + super(message); + } + + public WrappedException(String message, Throwable cause) { + super(message); + initCause(cause); + } + + public WrappedException(Throwable cause) { + super(""); + initCause(cause); + } + + + public static class ClientAbortException extends WrappedException { + public ClientAbortException(Throwable throwable) { + super(null, throwable); + } + } + +} diff --git a/modules/tomcat-lite/java/org/apache/tomcat/lite/io/package.html b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/package.html new file mode 100644 index 000000000..07a0f305c --- /dev/null +++ b/modules/tomcat-lite/java/org/apache/tomcat/lite/io/package.html @@ -0,0 +1,7 @@ +IO layer based on tomcat coyote connector and utils. + +There are many big changes: +
    +
  • +
  • +
\ No newline at end of file