changeset 948:7443278199cb

Merge
author tbell
date Fri, 20 Feb 2009 10:53:39 -0800
parents 1109646be6f6 6bdbb2f5c763
children 9b1bc2e28518
files
diffstat 6 files changed, 487 insertions(+), 141 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/sun/security/ssl/AppInputStream.java	Thu Feb 19 18:04:30 2009 -0800
+++ b/src/share/classes/sun/security/ssl/AppInputStream.java	Fri Feb 20 10:53:39 2009 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright 1996-2007 Sun Microsystems, Inc.  All Rights Reserved.
+ * Copyright 1996-2009 Sun Microsystems, Inc.  All Rights Reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -81,6 +81,14 @@
      */
     public synchronized int read(byte b[], int off, int len)
             throws IOException {
+        if (b == null) {
+            throw new NullPointerException();
+        } else if (off < 0 || len < 0 || len > b.length - off) {
+            throw new IndexOutOfBoundsException();
+        } else if (len == 0) {
+            return 0;
+        }
+
         if (c.checkEOF()) {
             return -1;
         }
--- a/src/share/classes/sun/security/ssl/AppOutputStream.java	Thu Feb 19 18:04:30 2009 -0800
+++ b/src/share/classes/sun/security/ssl/AppOutputStream.java	Fri Feb 20 10:53:39 2009 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright 1996-2007 Sun Microsystems, Inc.  All Rights Reserved.
+ * Copyright 1996-2009 Sun Microsystems, Inc.  All Rights Reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -58,18 +58,25 @@
      */
     synchronized public void write(byte b[], int off, int len)
             throws IOException {
+        if (b == null) {
+            throw new NullPointerException();
+        } else if (off < 0 || len < 0 || len > b.length - off) {
+            throw new IndexOutOfBoundsException();
+        } else if (len == 0) {
+            return;
+        }
+
         // check if the Socket is invalid (error or closed)
         c.checkWrite();
-        //
+
         // Always flush at the end of each application level record.
         // This lets application synchronize read and write streams
         // however they like; if we buffered here, they couldn't.
-        //
-        // NOTE: *must* call c.writeRecord() even for len == 0
         try {
             do {
                 int howmuch = Math.min(len, r.availableDataBytes());
 
+                // NOTE: *must* call c.writeRecord() even for howmuch == 0
                 if (howmuch > 0) {
                     r.write(b, off, howmuch);
                     off += howmuch;
--- a/src/share/classes/sun/security/ssl/ByteBufferInputStream.java	Thu Feb 19 18:04:30 2009 -0800
+++ b/src/share/classes/sun/security/ssl/ByteBufferInputStream.java	Fri Feb 20 10:53:39 2009 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright 2003-2007 Sun Microsystems, Inc.  All Rights Reserved.
+ * Copyright 2003-2009 Sun Microsystems, Inc.  All Rights Reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -89,8 +89,7 @@
 
         if (b == null) {
             throw new NullPointerException();
-        } else if ((off < 0) || (off > b.length) || (len < 0) ||
-                   ((off + len) > b.length) || ((off + len) < 0)) {
+        } else if (off < 0 || len < 0 || len > b.length - off) {
             throw new IndexOutOfBoundsException();
         } else if (len == 0) {
             return 0;
--- a/src/share/classes/sun/security/ssl/SSLSessionContextImpl.java	Thu Feb 19 18:04:30 2009 -0800
+++ b/src/share/classes/sun/security/ssl/SSLSessionContextImpl.java	Fri Feb 20 10:53:39 2009 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright 1999-2007 Sun Microsystems, Inc.  All Rights Reserved.
+ * Copyright 1999-2009 Sun Microsystems, Inc.  All Rights Reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -41,88 +41,112 @@
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
 
-import sun.misc.Cache;
+import sun.security.util.Cache;
 
 
-final class SSLSessionContextImpl implements SSLSessionContext
-{
-    private Cache       sessionCache = new Cache();
-    private Cache       sessionHostPortCache = new Cache();
-    private int         cacheLimit;
-    private long        timeoutMillis;
+final class SSLSessionContextImpl implements SSLSessionContext {
+    private Cache sessionCache;         // session cache, session id as key
+    private Cache sessionHostPortCache; // session cache, "host:port" as key
+    private int cacheLimit;             // the max cache size
+    private int timeout;                // timeout in seconds
+
     private static final Debug debug = Debug.getInstance("ssl");
 
-    // file private
-    SSLSessionContextImpl()
-    {
-        cacheLimit = getCacheLimit();
-        timeoutMillis = 86400000; // default, 24 hours
+    // package private
+    SSLSessionContextImpl() {
+        cacheLimit = getDefaultCacheLimit();    // default cache size
+        timeout = 86400;                        // default, 24 hours
+
+        // use soft reference
+        sessionCache = Cache.newSoftMemoryCache(cacheLimit, timeout);
+        sessionHostPortCache = Cache.newSoftMemoryCache(cacheLimit, timeout);
     }
 
     /**
-     * Returns the SSL session object associated with the
-     * specific session ID passed.
+     * Returns the <code>SSLSession</code> bound to the specified session id.
      */
-    public SSLSession   getSession(byte[] id)
-    {
-        SSLSession sess = (SSLSession) sessionCache.get(
-                                new SessionId(id));
-        return checkTimeValidity(sess);
+    public SSLSession getSession(byte[] sessionId) {
+        if (sessionId == null) {
+            throw new NullPointerException("session id cannot be null");
+        }
+
+        SSLSessionImpl sess =
+                (SSLSessionImpl)sessionCache.get(new SessionId(sessionId));
+        if (!isTimedout(sess)) {
+            return sess;
+        }
+
+        return null;
     }
 
     /**
      * Returns an enumeration of the active SSL sessions.
      */
     public Enumeration<byte[]> getIds() {
-        Vector<byte[]> v = new Vector<byte[]>(sessionCache.size());
-        SessionId sessId;
+        SessionCacheVisitor scVisitor = new SessionCacheVisitor();
+        sessionCache.accept(scVisitor);
 
-        for (Enumeration e = sessionCache.keys(); e.hasMoreElements(); ) {
-            sessId = (SessionId) e.nextElement();
-            if (!isTimedout((SSLSession)sessionCache.get(sessId)))
-                v.addElement(sessId.getId());
-        }
-        return v.elements();
+        return scVisitor.getSessionIds();
     }
 
+    /**
+     * Sets the timeout limit for cached <code>SSLSession</code> objects
+     *
+     * Note that after reset the timeout, the cached session before
+     * should be timed within the shorter one of the old timeout and the
+     * new timeout.
+     */
     public void setSessionTimeout(int seconds)
                  throws IllegalArgumentException {
-        if (seconds < 0)
+        if (seconds < 0) {
             throw new IllegalArgumentException();
-        timeoutMillis = seconds * 1000L;
+        }
+
+        if (timeout != seconds) {
+            sessionCache.setTimeout(seconds);
+            sessionHostPortCache.setTimeout(seconds);
+            timeout = seconds;
+        }
     }
 
+    /**
+     * Gets the timeout limit for cached <code>SSLSession</code> objects
+     */
     public int getSessionTimeout() {
-        return (int) (timeoutMillis / 1000);
+        return timeout;
     }
 
+    /**
+     * Sets the size of the cache used for storing
+     * <code>SSLSession</code> objects.
+     */
     public void setSessionCacheSize(int size)
                  throws IllegalArgumentException {
         if (size < 0)
             throw new IllegalArgumentException();
-        cacheLimit = size;
 
-        /**
-         * If cache size limit is reduced, when the cache is full to its
-         * previous limit, trim the cache before its contents
-         * are used.
-         */
-        if ((cacheLimit != 0) && (sessionCache.size() > cacheLimit))
-            adjustCacheSizeTo(cacheLimit);
+        if (cacheLimit != size) {
+            sessionCache.setCapacity(size);
+            sessionHostPortCache.setCapacity(size);
+            cacheLimit = size;
+        }
     }
 
+    /**
+     * Gets the size of the cache used for storing
+     * <code>SSLSession</code> objects.
+     */
     public int getSessionCacheSize() {
         return cacheLimit;
     }
 
+
+    // package-private method, used ONLY by ServerHandshaker
     SSLSessionImpl get(byte[] id) {
-        return (SSLSessionImpl) getSession(id);
+        return (SSLSessionImpl)getSession(id);
     }
 
-    /**
-     * Returns the SSL session object associated with the
-     * specific host name and port number passed.
-     */
+    // package-private method, used ONLY by ClientHandshaker
     SSLSessionImpl get(String hostname, int port) {
         /*
          * If no session caching info is available, we won't
@@ -131,96 +155,51 @@
         if (hostname == null && port == -1) {
             return null;
         }
-        SSLSession sess =  (SSLSessionImpl) sessionHostPortCache
-                                .get(getKey(hostname, port));
-        return (SSLSessionImpl) checkTimeValidity(sess);
+
+        SSLSessionImpl sess =
+            (SSLSessionImpl)sessionHostPortCache.get(getKey(hostname, port));
+        if (!isTimedout(sess)) {
+            return sess;
+        }
+
+        return null;
     }
 
     private String getKey(String hostname, int port) {
-        return (hostname + ":" + String.valueOf(port))
-                        .toLowerCase();
+        return (hostname + ":" + String.valueOf(port)).toLowerCase();
     }
 
+    // cache a SSLSession
+    //
+    // In SunJSSE implementation, a session is created while getting a
+    // client hello or a server hello message, and cached while the
+    // handshaking finished.
+    // Here we time the session from the time it cached instead of the
+    // time it created, which is a little longer than the expected. So
+    // please do check isTimedout() while getting entry from the cache.
     void put(SSLSessionImpl s) {
-        // make space for the new session to be added
-        if ((cacheLimit != 0) && (sessionCache.size() >= cacheLimit))
-            adjustCacheSizeTo(cacheLimit - 1);
-
-        /*
-         * Can always add the session id.
-         */
         sessionCache.put(s.getSessionId(), s);
 
-        /*
-         * If no hostname/port info is available, don't add this one.
-         */
+        // If no hostname/port info is available, don't add this one.
         if ((s.getPeerHost() != null) && (s.getPeerPort() != -1)) {
             sessionHostPortCache.put(
                 getKey(s.getPeerHost(), s.getPeerPort()), s);
         }
+
         s.setContext(this);
     }
 
-    private void adjustCacheSizeTo(int targetSize) {
-
-        int cacheSize = sessionCache.size();
-
-        if (targetSize < 0)
-           return;
-
-        while (cacheSize > targetSize) {
-            SSLSessionImpl lru = null;
-            SSLSessionImpl s = null;
-            Enumeration e;
-
-            if (debug != null && Debug.isOn("sessioncache")) {
-                System.out.println("exceeded cache limit of " + cacheLimit);
-            }
-
-            /*
-             * Count the number of elements in the cache. The size() method
-             * does not reflect the cache entries that are no longer available,
-             * i.e entries that are garbage collected (the cache entries are
-             * held using soft references and are garbage collected when not
-             * in use).
-             */
-            int count;
-            for (count = 0, e = sessionCache.elements();
-                         e.hasMoreElements(); count++) {
-                try {
-                    s = (SSLSessionImpl)e.nextElement();
-                } catch (NoSuchElementException nsee) {
-                    break;
-                }
-                if (isTimedout(s)) {
-                    lru = s;
-                    break;
-                } else if ((lru == null) || (s.getLastAccessedTime()
-                         < lru.getLastAccessedTime())) {
-                    lru = s;
-                }
-            }
-            if ((lru != null) && (count > targetSize)) {
-                if (debug != null && Debug.isOn("sessioncache")) {
-                    System.out.println("uncaching " + lru);
-                }
-                lru.invalidate();
-                count--; // element removed from the cache
-            }
-            cacheSize = count;
+    // package-private method, remove a cached SSLSession
+    void remove(SessionId key) {
+        SSLSessionImpl s = (SSLSessionImpl)sessionCache.get(key);
+        if (s != null) {
+            sessionCache.remove(key);
+            sessionHostPortCache.remove(
+                        getKey(s.getPeerHost(), s.getPeerPort()));
         }
     }
 
-    // file private
-    void remove(SessionId key)
-    {
-        SSLSessionImpl s = (SSLSessionImpl) sessionCache.get(key);
-        sessionCache.remove(key);
-        sessionHostPortCache.remove(getKey(s.getPeerHost(),
-                                         s.getPeerPort()));
-    }
-
-    private int getCacheLimit() {
+    private int getDefaultCacheLimit() {
         int cacheLimit = 0;
         try {
         String s = java.security.AccessController.doPrivileged(
@@ -237,21 +216,40 @@
         return (cacheLimit > 0) ? cacheLimit : 0;
     }
 
-    SSLSession checkTimeValidity(SSLSession sess) {
-        if (isTimedout(sess)) {
+    boolean isTimedout(SSLSession sess) {
+        if (timeout == 0) {
+            return false;
+        }
+
+        if ((sess != null) && ((sess.getCreationTime() + timeout * 1000L)
+                                        <= (System.currentTimeMillis()))) {
             sess.invalidate();
-            return null;
-        } else
-            return sess;
+            return true;
+        }
+
+        return false;
     }
 
-    boolean isTimedout(SSLSession sess) {
-        if (timeoutMillis == 0)
-            return false;
-        if ((sess != null) &&
-            ((sess.getCreationTime() + timeoutMillis)
-                <= (System.currentTimeMillis())))
-            return true;
-        return false;
+    final class SessionCacheVisitor
+            implements sun.security.util.Cache.CacheVisitor {
+        Vector<byte[]> ids = null;
+
+        // public void visit(java.util.Map<Object, Object> map) {}
+        public void visit(java.util.Map<Object, Object> map) {
+            ids = new Vector<byte[]>(map.size());
+
+            for (Object key : map.keySet()) {
+                SSLSessionImpl value = (SSLSessionImpl)map.get(key);
+                if (!isTimedout(value)) {
+                    ids.addElement(((SessionId)key).getId());
+                }
+            }
+        }
+
+        public Enumeration<byte[]> getSessionIds() {
+            return  ids != null ? ids.elements() :
+                                  new Vector<byte[]>().elements();
+        }
     }
+
 }
--- a/src/share/classes/sun/security/util/Cache.java	Thu Feb 19 18:04:30 2009 -0800
+++ b/src/share/classes/sun/security/util/Cache.java	Fri Feb 20 10:53:39 2009 -0800
@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2006 Sun Microsystems, Inc.  All Rights Reserved.
+ * Copyright 2002-2009 Sun Microsystems, Inc.  All Rights Reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -101,6 +101,21 @@
     public abstract void remove(Object key);
 
     /**
+     * Set the maximum size.
+     */
+    public abstract void setCapacity(int size);
+
+    /**
+     * Set the timeout(in seconds).
+     */
+    public abstract void setTimeout(int timeout);
+
+    /**
+     * accept a visitor
+     */
+    public abstract void accept(CacheVisitor visitor);
+
+    /**
      * Return a new memory cache with the specified maximum size, unlimited
      * lifetime for entries, with the values held by SoftReferences.
      */
@@ -178,6 +193,10 @@
         }
     }
 
+    public interface CacheVisitor {
+        public void visit(Map<Object, Object> map);
+    }
+
 }
 
 class NullCache extends Cache {
@@ -208,6 +227,18 @@
         // empty
     }
 
+    public void setCapacity(int size) {
+        // empty
+    }
+
+    public void setTimeout(int timeout) {
+        // empty
+    }
+
+    public void accept(CacheVisitor visitor) {
+        // empty
+    }
+
 }
 
 class MemoryCache extends Cache {
@@ -218,8 +249,8 @@
     private final static boolean DEBUG = false;
 
     private final Map<Object, CacheEntry> cacheMap;
-    private final int maxSize;
-    private final int lifetime;
+    private int maxSize;
+    private long lifetime;
     private final ReferenceQueue queue;
 
     public MemoryCache(boolean soft, int maxSize) {
@@ -328,7 +359,7 @@
             oldEntry.invalidate();
             return;
         }
-        if (cacheMap.size() > maxSize) {
+        if (maxSize > 0 && cacheMap.size() > maxSize) {
             expungeExpiredEntries();
             if (cacheMap.size() > maxSize) { // still too large?
                 Iterator<CacheEntry> t = cacheMap.values().iterator();
@@ -368,6 +399,55 @@
         }
     }
 
+    public synchronized void setCapacity(int size) {
+        expungeExpiredEntries();
+        if (size > 0 && cacheMap.size() > size) {
+            Iterator<CacheEntry> t = cacheMap.values().iterator();
+            for (int i = cacheMap.size() - size; i > 0; i--) {
+                CacheEntry lruEntry = t.next();
+                if (DEBUG) {
+                    System.out.println("** capacity reset removal "
+                        + lruEntry.getKey() + " | " + lruEntry.getValue());
+                }
+                t.remove();
+                lruEntry.invalidate();
+            }
+        }
+
+        maxSize = size > 0 ? size : 0;
+
+        if (DEBUG) {
+            System.out.println("** capacity reset to " + size);
+        }
+    }
+
+    public synchronized void setTimeout(int timeout) {
+        emptyQueue();
+        lifetime = timeout > 0 ? timeout * 1000L : 0L;
+
+        if (DEBUG) {
+            System.out.println("** lifetime reset to " + timeout);
+        }
+    }
+
+    // it is a heavyweight method.
+    public synchronized void accept(CacheVisitor visitor) {
+        expungeExpiredEntries();
+        Map<Object, Object> cached = getCachedEntries();
+
+        visitor.visit(cached);
+    }
+
+    private Map<Object, Object> getCachedEntries() {
+        Map<Object,Object> kvmap = new HashMap<Object,Object>(cacheMap.size());
+
+        for (CacheEntry entry : cacheMap.values()) {
+            kvmap.put(entry.getKey(), entry.getValue());
+        }
+
+        return kvmap;
+    }
+
     protected CacheEntry newEntry(Object key, Object value,
             long expirationTime, ReferenceQueue queue) {
         if (queue != null) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/sun/security/ssl/com/sun/net/ssl/internal/ssl/AppInputStream/ReadZeroBytes.java	Fri Feb 20 10:53:39 2009 -0800
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2009 Sun Microsystems, Inc.  All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
+ * CA 95054 USA or visit www.sun.com if you need additional information or
+ * have any questions.
+ */
+
+/*
+ * @test
+ * @bug 6697270
+ * @summary Inputstream dosent behave correct
+ */
+
+import java.io.*;
+import java.net.*;
+import javax.net.ssl.*;
+
+public class ReadZeroBytes {
+
+    /*
+     * =============================================================
+     * Set the various variables needed for the tests, then
+     * specify what tests to run on each side.
+     */
+
+    /*
+     * Should we run the client or server in a separate thread?
+     * Both sides can throw exceptions, but do you have a preference
+     * as to which side should be the main thread.
+     */
+    static boolean separateServerThread = false;
+
+    /*
+     * Where do we find the keystores?
+     */
+    static String pathToStores = "../../../../../../../etc";
+    static String keyStoreFile = "keystore";
+    static String trustStoreFile = "truststore";
+    static String passwd = "passphrase";
+
+    /*
+     * Is the server ready to serve?
+     */
+    volatile static boolean serverReady = false;
+
+    /*
+     * Turn on SSL debugging?
+     */
+    static boolean debug = false;
+
+    /*
+     * Define the server side of the test.
+     */
+    void doServerSide() throws Exception {
+        SSLServerSocketFactory sslssf =
+            (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
+        SSLServerSocket sslServerSocket =
+            (SSLServerSocket) sslssf.createServerSocket(serverPort);
+
+        serverPort = sslServerSocket.getLocalPort();
+
+        /*
+         * Signal Client, we're ready for his connect.
+         */
+        serverReady = true;
+
+        SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
+        InputStream sslIS = sslSocket.getInputStream();
+        OutputStream sslOS = sslSocket.getOutputStream();
+
+        // no read, no write.
+        SSLSession sess = sslSocket.getSession();
+        if (!sess.isValid()) {
+            throw new Exception("Error occurs during the initial handshake");
+        }
+
+        sslIS.close();
+        sslOS.close();
+        sslSocket.close();
+    }
+
+    /*
+     * Define the client side of the test.
+     */
+    void doClientSide() throws Exception {
+
+        /*
+         * Wait for server to get started.
+         */
+        while (!serverReady) {
+            Thread.sleep(50);
+        }
+
+        SSLSocketFactory sslsf =
+            (SSLSocketFactory) SSLSocketFactory.getDefault();
+        SSLSocket sslSocket = (SSLSocket)
+            sslsf.createSocket("localhost", serverPort);
+
+        InputStream sslIS = sslSocket.getInputStream();
+        OutputStream sslOS = sslSocket.getOutputStream();
+
+        // read zero byte, write zero byte.
+        sslIS.read(new byte[0], 0, 0);
+        sslOS.write(new byte[0], 0, 0);
+
+        // if byte array length matters.
+        sslIS.read(new byte[1], 0, 0);
+        sslOS.write(new byte[1], 0, 0);
+
+        // note that the above read/write should not kickoff handshaking.
+        SSLSession sess = sslSocket.getSession();
+        if (!sess.isValid()) {
+            throw new Exception("Error occurs during the initial handshake");
+        }
+
+        sslIS.close();
+        sslOS.close();
+        sslSocket.close();
+    }
+
+    /*
+     * =============================================================
+     * The remainder is just support stuff
+     */
+
+    // use any free port by default
+    volatile int serverPort = 0;
+
+    volatile Exception serverException = null;
+    volatile Exception clientException = null;
+
+    public static void main(String[] args) throws Exception {
+        String keyFilename =
+            System.getProperty("test.src", "./") + "/" + pathToStores +
+                "/" + keyStoreFile;
+        String trustFilename =
+            System.getProperty("test.src", "./") + "/" + pathToStores +
+                "/" + trustStoreFile;
+
+        System.setProperty("javax.net.ssl.keyStore", keyFilename);
+        System.setProperty("javax.net.ssl.keyStorePassword", passwd);
+        System.setProperty("javax.net.ssl.trustStore", trustFilename);
+        System.setProperty("javax.net.ssl.trustStorePassword", passwd);
+
+        if (debug)
+            System.setProperty("javax.net.debug", "all");
+
+        /*
+         * Start the tests.
+         */
+        new ReadZeroBytes();
+    }
+
+    Thread clientThread = null;
+    Thread serverThread = null;
+
+    /*
+     * Primary constructor, used to drive remainder of the test.
+     *
+     * Fork off the other side, then do your work.
+     */
+    ReadZeroBytes () throws Exception {
+        if (separateServerThread) {
+            startServer(true);
+            startClient(false);
+        } else {
+            startClient(true);
+            startServer(false);
+        }
+
+        /*
+         * Wait for other side to close down.
+         */
+        if (separateServerThread) {
+            serverThread.join();
+        } else {
+            clientThread.join();
+        }
+
+        /*
+         * When we get here, the test is pretty much over.
+         *
+         * If the main thread excepted, that propagates back
+         * immediately.  If the other thread threw an exception, we
+         * should report back.
+         */
+        if (serverException != null)
+            throw serverException;
+        if (clientException != null)
+            throw clientException;
+    }
+
+    void startServer(boolean newThread) throws Exception {
+        if (newThread) {
+            serverThread = new Thread() {
+                public void run() {
+                    try {
+                        doServerSide();
+                    } catch (Exception e) {
+                        /*
+                         * Our server thread just died.
+                         *
+                         * Release the client, if not already active...
+                         */
+                        System.out.println("Server died...");
+                        serverReady = true;
+                        serverException = e;
+                    }
+                }
+            };
+            serverThread.start();
+        } else {
+            doServerSide();
+        }
+    }
+
+    void startClient(boolean newThread) throws Exception {
+        if (newThread) {
+            clientThread = new Thread() {
+                public void run() {
+                    try {
+                        doClientSide();
+                    } catch (Exception e) {
+                        /*
+                         * Our client thread just died.
+                         */
+                        System.out.println("Client died...");
+                        clientException = e;
+                    }
+                }
+            };
+            clientThread.start();
+        } else {
+            doClientSide();
+        }
+    }
+}
+