e81ee784d3
The test is updated to ignore plain text connections Reviewed-by: chegar, michaelm
984 lines
34 KiB
Java
984 lines
34 KiB
Java
/*
|
|
* Copyright (c) 2002, 2019, Oracle and/or its affiliates. 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import java.net.*;
|
|
import java.io.*;
|
|
import java.nio.*;
|
|
import java.nio.channels.*;
|
|
import sun.net.www.MessageHeader;
|
|
import java.util.*;
|
|
import javax.net.ssl.*;
|
|
import javax.net.ssl.SSLEngineResult.*;
|
|
import java.security.*;
|
|
|
|
/**
|
|
* This class implements a simple HTTPS server. It uses multiple threads to
|
|
* handle connections in parallel, and will spin off a new thread to handle
|
|
* each request. (this is easier to implement with SSLEngine)
|
|
* <p>
|
|
* It must be instantiated with a {@link HttpCallback} object to which
|
|
* requests are given and must be handled.
|
|
* <p>
|
|
* Simple synchronization between the client(s) and server can be done
|
|
* using the {@link #waitForCondition(String)}, {@link #setCondition(String)} and
|
|
* {@link #rendezvous(String,int)} methods.
|
|
*
|
|
* NOTE NOTE NOTE NOTE NOTE NOTE NOTE
|
|
*
|
|
* If you make a change in here, please don't forget to make the
|
|
* corresponding change in the J2SE equivalent.
|
|
*
|
|
* NOTE NOTE NOTE NOTE NOTE NOTE NOTE
|
|
*/
|
|
|
|
public class TestHttpsServer {
|
|
|
|
ServerSocketChannel schan;
|
|
int threads;
|
|
int cperthread;
|
|
HttpCallback cb;
|
|
Server[] servers;
|
|
|
|
// ssl related fields
|
|
static SSLContext sslCtx;
|
|
|
|
/**
|
|
* Create a <code>TestHttpsServer<code> instance with the specified callback object
|
|
* for handling requests. One thread is created to handle requests,
|
|
* and up to ten TCP connections will be handled simultaneously.
|
|
* @param cb the callback object which is invoked to handle each
|
|
* incoming request
|
|
*/
|
|
|
|
public TestHttpsServer(HttpCallback cb) throws IOException {
|
|
this(cb, 1, 10, 0);
|
|
}
|
|
|
|
/**
|
|
* Create a <code>TestHttpsServer<code> instance with the specified number of
|
|
* threads and maximum number of connections per thread. This functions
|
|
* the same as the 4 arg constructor, where the port argument is set to zero.
|
|
* @param cb the callback object which is invoked to handle each
|
|
* incoming request
|
|
* @param threads the number of threads to create to handle requests
|
|
* in parallel
|
|
* @param cperthread the number of simultaneous TCP connections to
|
|
* handle per thread
|
|
*/
|
|
|
|
public TestHttpsServer(HttpCallback cb, int threads, int cperthread)
|
|
throws IOException {
|
|
this(cb, threads, cperthread, 0);
|
|
}
|
|
|
|
/**
|
|
* Create a <code>TestHttpsServer<code> instance with the specified number
|
|
* of threads and maximum number of connections per thread and running on
|
|
* the specified port. The specified number of threads are created to
|
|
* handle incoming requests, and each thread is allowed
|
|
* to handle a number of simultaneous TCP connections.
|
|
* @param cb the callback object which is invoked to handle
|
|
* each incoming request
|
|
* @param threads the number of threads to create to handle
|
|
* requests in parallel
|
|
* @param cperthread the number of simultaneous TCP connections
|
|
* to handle per thread
|
|
* @param port the port number to bind the server to. <code>Zero</code>
|
|
* means choose any free port.
|
|
*/
|
|
public TestHttpsServer(HttpCallback cb, int threads, int cperthread, int port)
|
|
throws IOException {
|
|
this(cb, threads, cperthread, null, port);
|
|
}
|
|
|
|
/**
|
|
* Create a <code>TestHttpsServer<code> instance with the specified number
|
|
* of threads and maximum number of connections per thread and running on
|
|
* the specified port. The specified number of threads are created to
|
|
* handle incoming requests, and each thread is allowed
|
|
* to handle a number of simultaneous TCP connections.
|
|
* @param cb the callback object which is invoked to handle
|
|
* each incoming request
|
|
* @param threads the number of threads to create to handle
|
|
* requests in parallel
|
|
* @param cperthread the number of simultaneous TCP connections
|
|
* to handle per thread
|
|
* @param address the InetAddress to bind to. {@code Null} means the
|
|
* wildcard address.
|
|
* @param port the port number to bind the server to. {@code Zero}
|
|
* means choose any free port.
|
|
*/
|
|
|
|
public TestHttpsServer(HttpCallback cb, int threads, int cperthread, InetAddress address, int port)
|
|
throws IOException {
|
|
schan = ServerSocketChannel.open();
|
|
InetSocketAddress addr = new InetSocketAddress(address, port);
|
|
schan.socket().bind(addr);
|
|
this.threads = threads;
|
|
this.cb = cb;
|
|
this.cperthread = cperthread;
|
|
|
|
try {
|
|
// create and initialize a SSLContext
|
|
KeyStore ks = KeyStore.getInstance("JKS");
|
|
KeyStore ts = KeyStore.getInstance("JKS");
|
|
char[] passphrase = "passphrase".toCharArray();
|
|
|
|
ks.load(new FileInputStream(System.getProperty("javax.net.ssl.keyStore")), passphrase);
|
|
ts.load(new FileInputStream(System.getProperty("javax.net.ssl.trustStore")), passphrase);
|
|
|
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
|
|
kmf.init(ks, passphrase);
|
|
|
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
|
|
tmf.init(ts);
|
|
|
|
sslCtx = SSLContext.getInstance("TLS");
|
|
|
|
sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
|
|
|
|
servers = new Server[threads];
|
|
for (int i=0; i<threads; i++) {
|
|
servers[i] = new Server(cb, schan, cperthread);
|
|
servers[i].start();
|
|
}
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException("test failed. cause: "+ex.getMessage());
|
|
}
|
|
}
|
|
|
|
/** Tell all threads in the server to exit within 5 seconds.
|
|
* This is an abortive termination. Just prior to the thread exiting
|
|
* all channels in that thread waiting to be closed are forceably closed.
|
|
*/
|
|
|
|
public void terminate() {
|
|
for (int i=0; i<threads; i++) {
|
|
servers[i].terminate ();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* return the local port number to which the server is bound.
|
|
* @return the local port number
|
|
*/
|
|
|
|
public int getLocalPort () {
|
|
return schan.socket().getLocalPort ();
|
|
}
|
|
|
|
public String getAuthority() {
|
|
InetAddress address = schan.socket().getInetAddress();
|
|
String hostaddr = address.getHostAddress();
|
|
if (address.isAnyLocalAddress()) hostaddr = "localhost";
|
|
if (hostaddr.indexOf(':') > -1) hostaddr = "[" + hostaddr + "]";
|
|
return hostaddr + ":" + getLocalPort();
|
|
}
|
|
|
|
static class Server extends Thread {
|
|
|
|
ServerSocketChannel schan;
|
|
Selector selector;
|
|
SelectionKey listenerKey;
|
|
SelectionKey key; /* the current key being processed */
|
|
HttpCallback cb;
|
|
ByteBuffer consumeBuffer;
|
|
int maxconn;
|
|
int nconn;
|
|
ClosedChannelList clist;
|
|
boolean shutdown;
|
|
|
|
Server(HttpCallback cb, ServerSocketChannel schan, int maxconn) {
|
|
this.schan = schan;
|
|
this.maxconn = maxconn;
|
|
this.cb = cb;
|
|
nconn = 0;
|
|
consumeBuffer = ByteBuffer.allocate(512);
|
|
clist = new ClosedChannelList();
|
|
try {
|
|
selector = Selector.open();
|
|
schan.configureBlocking(false);
|
|
listenerKey = schan.register(selector, SelectionKey.OP_ACCEPT);
|
|
} catch (IOException e) {
|
|
System.err.println("Server could not start: " + e);
|
|
}
|
|
}
|
|
|
|
/* Stop the thread as soon as possible */
|
|
public synchronized void terminate() {
|
|
shutdown = true;
|
|
}
|
|
|
|
public void run() {
|
|
try {
|
|
while (true) {
|
|
selector.select(1000);
|
|
Set selected = selector.selectedKeys();
|
|
Iterator iter = selected.iterator();
|
|
while (iter.hasNext()) {
|
|
key = (SelectionKey)iter.next();
|
|
if (key.equals (listenerKey)) {
|
|
SocketChannel sock = schan.accept();
|
|
if (sock == null) {
|
|
/* false notification */
|
|
iter.remove();
|
|
continue;
|
|
}
|
|
sock.configureBlocking(true);
|
|
SSLEngine sslEng = sslCtx.createSSLEngine();
|
|
sslEng.setUseClientMode(false);
|
|
new ServerWorker(cb, sock, sslEng).start();
|
|
nconn ++;
|
|
if (nconn == maxconn) {
|
|
/* deregister */
|
|
listenerKey.cancel();
|
|
listenerKey = null;
|
|
}
|
|
} else {
|
|
if (key.isReadable()) {
|
|
boolean closed = false;
|
|
SocketChannel chan = (SocketChannel)key.channel();
|
|
if (key.attachment() != null) {
|
|
closed = consume(chan);
|
|
}
|
|
|
|
if (closed) {
|
|
chan.close();
|
|
key.cancel();
|
|
if (nconn == maxconn) {
|
|
listenerKey = schan.register(selector, SelectionKey.OP_ACCEPT);
|
|
}
|
|
nconn --;
|
|
}
|
|
}
|
|
}
|
|
iter.remove();
|
|
}
|
|
clist.check();
|
|
|
|
synchronized (this) {
|
|
if (shutdown) {
|
|
clist.terminate();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} catch (IOException e) {
|
|
System.out.println("Server exception: " + e);
|
|
// TODO finish
|
|
}
|
|
}
|
|
|
|
/* read all the data off the channel without looking at it
|
|
* return true if connection closed
|
|
*/
|
|
boolean consume(SocketChannel chan) {
|
|
try {
|
|
consumeBuffer.clear();
|
|
int c = chan.read(consumeBuffer);
|
|
if (c == -1)
|
|
return true;
|
|
} catch (IOException e) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static class ServerWorker extends Thread {
|
|
private ByteBuffer inNetBB;
|
|
private ByteBuffer outNetBB;
|
|
private ByteBuffer inAppBB;
|
|
private ByteBuffer outAppBB;
|
|
|
|
SSLEngine sslEng;
|
|
SocketChannel schan;
|
|
HttpCallback cb;
|
|
HandshakeStatus currentHSStatus;
|
|
boolean initialHSComplete;
|
|
boolean handshakeStarted;
|
|
/*
|
|
* All inbound data goes through this buffer.
|
|
*
|
|
* It might be nice to use a cache of ByteBuffers so we're
|
|
* not alloc/dealloc'ing all over the place.
|
|
*/
|
|
|
|
/*
|
|
* Application buffers, also used for handshaking
|
|
*/
|
|
private int appBBSize;
|
|
|
|
ServerWorker(HttpCallback cb, SocketChannel schan, SSLEngine sslEng) {
|
|
this.sslEng = sslEng;
|
|
this.schan = schan;
|
|
this.cb = cb;
|
|
currentHSStatus = HandshakeStatus.NEED_UNWRAP;
|
|
initialHSComplete = false;
|
|
int netBBSize = sslEng.getSession().getPacketBufferSize();
|
|
inNetBB = ByteBuffer.allocate(netBBSize);
|
|
outNetBB = ByteBuffer.allocate(netBBSize);
|
|
appBBSize = sslEng.getSession().getApplicationBufferSize();
|
|
inAppBB = ByteBuffer.allocate(appBBSize);
|
|
outAppBB = ByteBuffer.allocate(appBBSize);
|
|
}
|
|
|
|
public SSLEngine getSSLEngine() {
|
|
return sslEng;
|
|
}
|
|
|
|
public ByteBuffer outNetBB() {
|
|
return outNetBB;
|
|
}
|
|
|
|
public ByteBuffer outAppBB() {
|
|
return outAppBB;
|
|
}
|
|
|
|
public void run () {
|
|
try {
|
|
SSLEngineResult result;
|
|
|
|
while (!initialHSComplete) {
|
|
|
|
switch (currentHSStatus) {
|
|
|
|
case NEED_UNWRAP:
|
|
int bytes = schan.read(inNetBB);
|
|
if (!handshakeStarted && bytes > 0) {
|
|
handshakeStarted = true;
|
|
int byte0 = inNetBB.get(0);
|
|
if (byte0 != 0x16) {
|
|
// first byte of a TLS connection is supposed to be
|
|
// 0x16. If not it may be a plain text connection.
|
|
//
|
|
// Sometime a rogue client may try to open a plain
|
|
// connection with our server. Calling this method
|
|
// gives a chance to the test logic to ignore such
|
|
// rogue connections.
|
|
//
|
|
if (cb.dropPlainTextConnections()) {
|
|
try { schan.close(); } catch (IOException x) { };
|
|
return;
|
|
}
|
|
// else sslEng.unwrap will throw later on...
|
|
}
|
|
}
|
|
|
|
needIO:
|
|
while (currentHSStatus == HandshakeStatus.NEED_UNWRAP) {
|
|
/*
|
|
* Don't need to resize requestBB, since no app data should
|
|
* be generated here.
|
|
*/
|
|
inNetBB.flip();
|
|
result = sslEng.unwrap(inNetBB, inAppBB);
|
|
inNetBB.compact();
|
|
currentHSStatus = result.getHandshakeStatus();
|
|
|
|
switch (result.getStatus()) {
|
|
|
|
case OK:
|
|
switch (currentHSStatus) {
|
|
case NOT_HANDSHAKING:
|
|
throw new IOException(
|
|
"Not handshaking during initial handshake");
|
|
|
|
case NEED_TASK:
|
|
Runnable task;
|
|
while ((task = sslEng.getDelegatedTask()) != null) {
|
|
task.run();
|
|
currentHSStatus = sslEng.getHandshakeStatus();
|
|
}
|
|
break;
|
|
}
|
|
|
|
break;
|
|
|
|
case BUFFER_UNDERFLOW:
|
|
break needIO;
|
|
|
|
default: // BUFFER_OVERFLOW/CLOSED:
|
|
throw new IOException("Received" + result.getStatus() +
|
|
"during initial handshaking");
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Just transitioned from read to write.
|
|
*/
|
|
if (currentHSStatus != HandshakeStatus.NEED_WRAP) {
|
|
break;
|
|
}
|
|
|
|
// Fall through and fill the write buffer.
|
|
|
|
case NEED_WRAP:
|
|
/*
|
|
* The flush above guarantees the out buffer to be empty
|
|
*/
|
|
outNetBB.clear();
|
|
result = sslEng.wrap(inAppBB, outNetBB);
|
|
outNetBB.flip();
|
|
schan.write (outNetBB);
|
|
outNetBB.compact();
|
|
currentHSStatus = result.getHandshakeStatus();
|
|
|
|
switch (result.getStatus()) {
|
|
case OK:
|
|
|
|
if (currentHSStatus == HandshakeStatus.NEED_TASK) {
|
|
Runnable task;
|
|
while ((task = sslEng.getDelegatedTask()) != null) {
|
|
task.run();
|
|
currentHSStatus = sslEng.getHandshakeStatus();
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
default: // BUFFER_OVERFLOW/BUFFER_UNDERFLOW/CLOSED:
|
|
throw new IOException("Received" + result.getStatus() +
|
|
"during initial handshaking");
|
|
}
|
|
break;
|
|
|
|
case FINISHED:
|
|
initialHSComplete = true;
|
|
break;
|
|
default: // NOT_HANDSHAKING/NEED_TASK
|
|
throw new RuntimeException("Invalid Handshaking State" +
|
|
currentHSStatus);
|
|
} // switch
|
|
}
|
|
// read the application data; using non-blocking mode
|
|
schan.configureBlocking(false);
|
|
read(schan, sslEng);
|
|
} catch (Exception ex) {
|
|
throw new RuntimeException(ex);
|
|
}
|
|
}
|
|
|
|
/* return true if the connection is closed, false otherwise */
|
|
|
|
private boolean read(SocketChannel chan, SSLEngine sslEng) {
|
|
HttpTransaction msg;
|
|
boolean res;
|
|
try {
|
|
InputStream is = new BufferedInputStream(new NioInputStream(chan, sslEng, inNetBB, inAppBB));
|
|
String requestline = readLine(is);
|
|
MessageHeader mhead = new MessageHeader(is);
|
|
String clen = mhead.findValue("Content-Length");
|
|
String trferenc = mhead.findValue("Transfer-Encoding");
|
|
String data = null;
|
|
if (trferenc != null && trferenc.equals("chunked"))
|
|
data = new String(readChunkedData(is));
|
|
else if (clen != null)
|
|
data = new String(readNormalData(is, Integer.parseInt(clen)));
|
|
String[] req = requestline.split(" ");
|
|
if (req.length < 2) {
|
|
/* invalid request line */
|
|
return false;
|
|
}
|
|
String cmd = req[0];
|
|
URI uri = null;
|
|
try {
|
|
uri = new URI(req[1]);
|
|
msg = new HttpTransaction(this, cmd, uri, mhead, data, null, chan);
|
|
cb.request(msg);
|
|
} catch (URISyntaxException e) {
|
|
System.err.println ("Invalid URI: " + e);
|
|
msg = new HttpTransaction(this, cmd, null, null, null, null, chan);
|
|
msg.sendResponse(501, "Whatever");
|
|
}
|
|
res = false;
|
|
} catch (IOException e) {
|
|
res = true;
|
|
}
|
|
return res;
|
|
}
|
|
|
|
byte[] readNormalData(InputStream is, int len) throws IOException {
|
|
byte[] buf = new byte[len];
|
|
int c, off=0, remain=len;
|
|
while (remain > 0 && ((c=is.read (buf, off, remain))>0)) {
|
|
remain -= c;
|
|
off += c;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
private void readCRLF(InputStream is) throws IOException {
|
|
int cr = is.read();
|
|
int lf = is.read();
|
|
|
|
if (((cr & 0xff) != 0x0d) ||
|
|
((lf & 0xff) != 0x0a)) {
|
|
throw new IOException(
|
|
"Expected <CR><LF>: got '" + cr + "/" + lf + "'");
|
|
}
|
|
}
|
|
|
|
byte[] readChunkedData(InputStream is) throws IOException {
|
|
LinkedList l = new LinkedList();
|
|
int total = 0;
|
|
for (int len=readChunkLen(is); len!=0; len=readChunkLen(is)) {
|
|
l.add(readNormalData(is, len));
|
|
total += len;
|
|
readCRLF(is); // CRLF at end of chunk
|
|
}
|
|
readCRLF(is); // CRLF at end of Chunked Stream.
|
|
byte[] buf = new byte[total];
|
|
Iterator i = l.iterator();
|
|
int x = 0;
|
|
while (i.hasNext()) {
|
|
byte[] b = (byte[])i.next();
|
|
System.arraycopy(b, 0, buf, x, b.length);
|
|
x += b.length;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
private int readChunkLen(InputStream is) throws IOException {
|
|
int c, len=0;
|
|
boolean done=false, readCR=false;
|
|
while (!done) {
|
|
c = is.read();
|
|
if (c == '\n' && readCR) {
|
|
done = true;
|
|
} else {
|
|
if (c == '\r' && !readCR) {
|
|
readCR = true;
|
|
} else {
|
|
int x=0;
|
|
if (c >= 'a' && c <= 'f') {
|
|
x = c - 'a' + 10;
|
|
} else if (c >= 'A' && c <= 'F') {
|
|
x = c - 'A' + 10;
|
|
} else if (c >= '0' && c <= '9') {
|
|
x = c - '0';
|
|
}
|
|
len = len * 16 + x;
|
|
}
|
|
}
|
|
}
|
|
return len;
|
|
}
|
|
|
|
private String readLine(InputStream is) throws IOException {
|
|
boolean done=false, readCR=false;
|
|
byte[] b = new byte[512];
|
|
int c, l = 0;
|
|
|
|
while (!done) {
|
|
c = is.read();
|
|
if (c == '\n' && readCR) {
|
|
done = true;
|
|
} else {
|
|
if (c == '\r' && !readCR) {
|
|
readCR = true;
|
|
} else {
|
|
b[l++] = (byte)c;
|
|
}
|
|
}
|
|
}
|
|
return new String(b);
|
|
}
|
|
|
|
/** close the channel associated with the current key by:
|
|
* 1. shutdownOutput (send a FIN)
|
|
* 2. mark the key so that incoming data is to be consumed and discarded
|
|
* 3. After a period, close the socket
|
|
*/
|
|
|
|
synchronized void orderlyCloseChannel(SocketChannel ch) throws IOException {
|
|
ch.socket().shutdownOutput();
|
|
}
|
|
|
|
synchronized void abortiveCloseChannel(SocketChannel ch) throws IOException {
|
|
Socket s = ch.socket();
|
|
s.setSoLinger(true, 0);
|
|
ch.close();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Implements blocking reading semantics on top of a non-blocking channel
|
|
*/
|
|
|
|
static class NioInputStream extends InputStream {
|
|
SSLEngine sslEng;
|
|
SocketChannel channel;
|
|
Selector selector;
|
|
ByteBuffer inNetBB;
|
|
ByteBuffer inAppBB;
|
|
SelectionKey key;
|
|
int available;
|
|
byte[] one;
|
|
boolean closed;
|
|
ByteBuffer markBuf; /* reads may be satisifed from this buffer */
|
|
boolean marked;
|
|
boolean reset;
|
|
int readlimit;
|
|
|
|
public NioInputStream(SocketChannel chan, SSLEngine sslEng, ByteBuffer inNetBB, ByteBuffer inAppBB) throws IOException {
|
|
this.sslEng = sslEng;
|
|
this.channel = chan;
|
|
selector = Selector.open();
|
|
this.inNetBB = inNetBB;
|
|
this.inAppBB = inAppBB;
|
|
key = chan.register(selector, SelectionKey.OP_READ);
|
|
available = 0;
|
|
one = new byte[1];
|
|
closed = marked = reset = false;
|
|
}
|
|
|
|
public synchronized int read(byte[] b) throws IOException {
|
|
return read(b, 0, b.length);
|
|
}
|
|
|
|
public synchronized int read() throws IOException {
|
|
return read(one, 0, 1);
|
|
}
|
|
|
|
public synchronized int read(byte[] b, int off, int srclen) throws IOException {
|
|
|
|
int canreturn, willreturn;
|
|
|
|
if (closed)
|
|
return -1;
|
|
|
|
if (reset) { /* satisfy from markBuf */
|
|
canreturn = markBuf.remaining();
|
|
willreturn = canreturn > srclen ? srclen : canreturn;
|
|
markBuf.get(b, off, willreturn);
|
|
if (canreturn == willreturn) {
|
|
reset = false;
|
|
}
|
|
} else { /* satisfy from channel */
|
|
canreturn = available();
|
|
if (canreturn == 0) {
|
|
block();
|
|
canreturn = available();
|
|
}
|
|
willreturn = canreturn > srclen ? srclen : canreturn;
|
|
inAppBB.get(b, off, willreturn);
|
|
available -= willreturn;
|
|
|
|
if (marked) { /* copy into markBuf */
|
|
try {
|
|
markBuf.put(b, off, willreturn);
|
|
} catch (BufferOverflowException e) {
|
|
marked = false;
|
|
}
|
|
}
|
|
}
|
|
return willreturn;
|
|
}
|
|
|
|
public synchronized int available() throws IOException {
|
|
if (closed)
|
|
throw new IOException("Stream is closed");
|
|
|
|
if (reset)
|
|
return markBuf.remaining();
|
|
|
|
if (available > 0)
|
|
return available;
|
|
|
|
inAppBB.clear();
|
|
int bytes = channel.read(inNetBB);
|
|
|
|
int needed = sslEng.getSession().getApplicationBufferSize();
|
|
if (needed > inAppBB.remaining()) {
|
|
inAppBB = ByteBuffer.allocate(needed);
|
|
}
|
|
inNetBB.flip();
|
|
SSLEngineResult result = sslEng.unwrap(inNetBB, inAppBB);
|
|
inNetBB.compact();
|
|
available = result.bytesProduced();
|
|
|
|
if (available > 0)
|
|
inAppBB.flip();
|
|
else if (available == -1)
|
|
throw new IOException("Stream is closed");
|
|
return available;
|
|
}
|
|
|
|
/**
|
|
* block() only called when available==0 and buf is empty
|
|
*/
|
|
private synchronized void block() throws IOException {
|
|
//assert available == 0;
|
|
int n = selector.select();
|
|
//assert n == 1;
|
|
selector.selectedKeys().clear();
|
|
available();
|
|
}
|
|
|
|
public void close() throws IOException {
|
|
if (closed)
|
|
return;
|
|
channel.close();
|
|
closed = true;
|
|
}
|
|
|
|
public synchronized void mark(int readlimit) {
|
|
if (closed)
|
|
return;
|
|
this.readlimit = readlimit;
|
|
markBuf = ByteBuffer.allocate(readlimit);
|
|
marked = true;
|
|
reset = false;
|
|
}
|
|
|
|
public synchronized void reset() throws IOException {
|
|
if (closed )
|
|
return;
|
|
if (!marked)
|
|
throw new IOException("Stream not marked");
|
|
marked = false;
|
|
reset = true;
|
|
markBuf.flip();
|
|
}
|
|
}
|
|
|
|
static class NioOutputStream extends OutputStream {
|
|
SSLEngine sslEng;
|
|
SocketChannel channel;
|
|
ByteBuffer outNetBB;
|
|
ByteBuffer outAppBB;
|
|
SelectionKey key;
|
|
Selector selector;
|
|
boolean closed;
|
|
byte[] one;
|
|
|
|
public NioOutputStream(SocketChannel channel, SSLEngine sslEng, ByteBuffer outNetBB, ByteBuffer outAppBB) throws IOException {
|
|
this.sslEng = sslEng;
|
|
this.channel = channel;
|
|
this.outNetBB = outNetBB;
|
|
this.outAppBB = outAppBB;
|
|
selector = Selector.open();
|
|
key = channel.register(selector, SelectionKey.OP_WRITE);
|
|
closed = false;
|
|
one = new byte[1];
|
|
}
|
|
|
|
public synchronized void write(int b) throws IOException {
|
|
one[0] = (byte)b;
|
|
write(one, 0, 1);
|
|
}
|
|
|
|
public synchronized void write(byte[] b) throws IOException {
|
|
write(b, 0, b.length);
|
|
}
|
|
|
|
public synchronized void write(byte[] b, int off, int len) throws IOException {
|
|
if (closed)
|
|
throw new IOException("stream is closed");
|
|
|
|
outAppBB = ByteBuffer.allocate(len);
|
|
outAppBB.put(b, off, len);
|
|
outAppBB.flip();
|
|
int n;
|
|
outNetBB.clear();
|
|
int needed = sslEng.getSession().getPacketBufferSize();
|
|
if (outNetBB.capacity() < needed) {
|
|
outNetBB = ByteBuffer.allocate(needed);
|
|
}
|
|
SSLEngineResult ret = sslEng.wrap(outAppBB, outNetBB);
|
|
outNetBB.flip();
|
|
int newLen = ret.bytesProduced();
|
|
while ((n = channel.write (outNetBB)) < newLen) {
|
|
newLen -= n;
|
|
if (newLen == 0)
|
|
return;
|
|
selector.select();
|
|
selector.selectedKeys().clear();
|
|
}
|
|
}
|
|
|
|
public void close() throws IOException {
|
|
if (closed)
|
|
return;
|
|
channel.close();
|
|
closed = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utilities for synchronization. A condition is
|
|
* identified by a string name, and is initialized
|
|
* upon first use (ie. setCondition() or waitForCondition()). Threads
|
|
* are blocked until some thread calls (or has called) setCondition() for the same
|
|
* condition.
|
|
* <P>
|
|
* A rendezvous built on a condition is also provided for synchronizing
|
|
* N threads.
|
|
*/
|
|
|
|
private static HashMap conditions = new HashMap();
|
|
|
|
/*
|
|
* Modifiable boolean object
|
|
*/
|
|
private static class BValue {
|
|
boolean v;
|
|
}
|
|
|
|
/*
|
|
* Modifiable int object
|
|
*/
|
|
private static class IValue {
|
|
int v;
|
|
IValue(int i) {
|
|
v =i;
|
|
}
|
|
}
|
|
|
|
|
|
private static BValue getCond(String condition) {
|
|
synchronized (conditions) {
|
|
BValue cond = (BValue) conditions.get(condition);
|
|
if (cond == null) {
|
|
cond = new BValue();
|
|
conditions.put(condition, cond);
|
|
}
|
|
return cond;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the condition to true. Any threads that are currently blocked
|
|
* waiting on the condition, will be unblocked and allowed to continue.
|
|
* Threads that subsequently call waitForCondition() will not block.
|
|
* If the named condition did not exist prior to the call, then it is created
|
|
* first.
|
|
*/
|
|
|
|
public static void setCondition(String condition) {
|
|
BValue cond = getCond(condition);
|
|
synchronized (cond) {
|
|
if (cond.v) {
|
|
return;
|
|
}
|
|
cond.v = true;
|
|
cond.notifyAll();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the named condition does not exist, then it is created and initialized
|
|
* to false. If the condition exists or has just been created and its value
|
|
* is false, then the thread blocks until another thread sets the condition.
|
|
* If the condition exists and is already set to true, then this call returns
|
|
* immediately without blocking.
|
|
*/
|
|
|
|
public static void waitForCondition(String condition) {
|
|
BValue cond = getCond(condition);
|
|
synchronized (cond) {
|
|
if (!cond.v) {
|
|
try {
|
|
cond.wait();
|
|
} catch (InterruptedException e) {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* conditions must be locked when accessing this */
|
|
static HashMap rv = new HashMap();
|
|
|
|
/**
|
|
* Force N threads to rendezvous (ie. wait for each other) before proceeding.
|
|
* The first thread(s) to call are blocked until the last
|
|
* thread makes the call. Then all threads continue.
|
|
* <p>
|
|
* All threads that call with the same condition name, must use the same value
|
|
* for N (or the results may be not be as expected).
|
|
* <P>
|
|
* Obviously, if fewer than N threads make the rendezvous then the result
|
|
* will be a hang.
|
|
*/
|
|
|
|
public static void rendezvous(String condition, int N) {
|
|
BValue cond;
|
|
IValue iv;
|
|
String name = "RV_"+condition;
|
|
|
|
/* get the condition */
|
|
|
|
synchronized (conditions) {
|
|
cond = (BValue)conditions.get(name);
|
|
if (cond == null) {
|
|
/* we are first caller */
|
|
if (N < 2) {
|
|
throw new RuntimeException("rendezvous must be called with N >= 2");
|
|
}
|
|
cond = new BValue();
|
|
conditions.put(name, cond);
|
|
iv = new IValue(N-1);
|
|
rv.put(name, iv);
|
|
} else {
|
|
/* already initialised, just decrement the counter */
|
|
iv = (IValue) rv.get(name);
|
|
iv.v--;
|
|
}
|
|
}
|
|
|
|
if (iv.v > 0) {
|
|
waitForCondition(name);
|
|
} else {
|
|
setCondition(name);
|
|
synchronized (conditions) {
|
|
clearCondition(name);
|
|
rv.remove(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the named condition exists and is set then remove it, so it can
|
|
* be re-initialized and used again. If the condition does not exist, or
|
|
* exists but is not set, then the call returns without doing anything.
|
|
* Note, some higher level synchronization
|
|
* may be needed between clear and the other operations.
|
|
*/
|
|
|
|
public static void clearCondition(String condition) {
|
|
BValue cond;
|
|
synchronized (conditions) {
|
|
cond = (BValue) conditions.get(condition);
|
|
if (cond == null) {
|
|
return;
|
|
}
|
|
synchronized (cond) {
|
|
if (cond.v) {
|
|
conditions.remove(condition);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|