/* * 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 com.sun.jini.jeri.internal.mux; import com.sun.jini.jeri.internal.runtime.HexDumpEncoder; import com.sun.jini.thread.Executor; import com.sun.jini.thread.GetThreadPoolAction; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.security.AccessController; import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; /** * Mux is the abstract superclass of both client-side and server-side * multiplexed connections. * * @author Sun Microsystems, Inc. **/ abstract class Mux { static final int CLIENT = 0; static final int SERVER = 1; static final int MAX_SESSION_ID = 0x7F; public static final int MAX_REQUESTS = MAX_SESSION_ID + 1; static final int NoOperation = 0x00; // 00000000 static final int Shutdown = 0x02; // 00000010 static final int Ping = 0x04; // 00000100 static final int PingAck = 0x06; // 00000110 static final int Error = 0x08; // 00001000 static final int IncrementRation = 0x10; // 0001***0 static final int Abort = 0x20; // 001000*0 static final int Close = 0x30; // 00110000 static final int Acknowledgment = 0x40; // 00100000 static final int Data = 0x80; // 100****0 static final int IncrementRation_shift = 0x0E; static final int Abort_partial = 0x02; static final int Data_open = 0x10; static final int Data_close = 0x08; static final int Data_eof = 0x04; static final int Data_ackRequired = 0x02; static final int ClientConnectionHeader_negotiate = 0x01; private static final byte[] magic = { (byte) 'J', (byte) 'm', (byte) 'u', (byte) 'x' // 0x4A6D7578 }; private static final int VERSION = 0x01; /** * pool of threads for executing tasks in system thread group: * used for shutting down sessions when a connnection goes down */ private static final Executor systemThreadPool = (Executor) AccessController.doPrivileged( new GetThreadPoolAction(false)); /** session shutdown tasks to be executed asynchronously */ private static final LinkedList sessionShutdownQueue = new LinkedList(); private static class SessionShutdownTask implements Runnable { private final Session[] sessions; private final String message; private final Throwable cause; SessionShutdownTask(Session[] sessions, String message, Throwable cause) { this.sessions = sessions; this.message = message; this.cause = cause; } public void run() { for (int i = 0; i < sessions.length; i++) { sessions[i].setDown(message, cause); } } } /** mux logger */ private static final Logger logger = Logger.getLogger("net.jini.jeri.connection.mux"); final int role; final int initialInboundRation; final int maxFragmentSize; private final ConnectionIO connectionIO; private final boolean directBuffersUseful; /** lock guarding all mutable instance state (below) */ final Object muxLock = new Object(); int initialOutboundRation; // set from remote connection header private boolean clientConnectionReady = false; // server header received boolean serverConnectionReady = false; // server header sent boolean muxDown = false; String muxDownMessage; Throwable muxDownCause; final BitSet busySessions = new BitSet(); final Map sessions = new HashMap(5); private int expectedPingCookie = -1; /** * Constructs a new Mux instance for a connection accessible through * standard (blocking) I/O streams. */ Mux(OutputStream out, InputStream in, int role, int initialInboundRation, int maxFragmentSize) throws IOException { this.role = role; if ((initialInboundRation & ~0x00FFFF00) != 0) { throw new IllegalArgumentException( "illegal initial inbound ration: " + toHexString(initialInboundRation)); } this.initialInboundRation = initialInboundRation; this.maxFragmentSize = maxFragmentSize; this.connectionIO = new StreamConnectionIO(this, out, in); directBuffersUseful = false; } Mux(SocketChannel channel, int role, int initialInboundRation, int maxFragmentSize) throws IOException { this.role = role; if ((initialInboundRation & ~0x00FFFF00) != 0) { throw new IllegalArgumentException( "illegal initial inbound ration: " + toHexString(initialInboundRation)); } this.initialInboundRation = initialInboundRation; this.maxFragmentSize = maxFragmentSize; this.connectionIO = new SocketChannelConnectionIO(this, channel); directBuffersUseful = true; } /** * Starts I/O processing. * * This method should be invoked only after this instance has * been completely initialized, so that subclasses will not * see uninitialized state. */ public void start() throws IOException { if (role == CLIENT) { readState = READ_SERVER_CONNECTION_HEADER; } else { assert role == SERVER; readState = READ_CLIENT_CONNECTION_HEADER; } try { connectionIO.start(); } catch (IOException e) { setDown("I/O error starting connection", e); throw e; } if (role == CLIENT) { asyncSendClientConnectionHeader(); synchronized (muxLock) { while (!muxDown && !clientConnectionReady) { try { muxLock.wait(); // REMIND: timeout? } catch (InterruptedException e) { setDown("interrupt waiting for connection header", e); } } if (muxDown) { IOException ioe = new IOException(muxDownMessage); ioe.initCause(muxDownCause); throw ioe; } } } } /** * Handles indication that this multiplexed connection has * gone down, either through normal operation or failure. * * This method should be overridden by subclasses that want to * implement custom behavior when this connection has gone down. */ protected void handleDown() { } /** * This method is invoked internally and is intended to be * overridden by subclasses. */ void handleOpen(int sessionID) throws ProtocolException { throw new ProtocolException( "remote endpoint attempted to open session"); } /** * * This method is intended to be invoked by subclasses only. * * This method must ONLY be invoked while synchronized on muxLock * and while muxDown is false. */ final void addSession(int sessionID, Session session) { assert Thread.holdsLock(muxLock); assert !muxDown; assert !busySessions.get(sessionID); assert sessions.get(new Integer(sessionID)) == null; busySessions.set(sessionID); sessions.put(new Integer(sessionID), session); } /** * * This method is intended to be invoked by this class and * subclasses only. * * This method MAY be invoked while synchronized on muxLock. */ final void setDown(final String message, final Throwable cause) { synchronized (muxLock) { if (muxDown) { return; } muxDown = true; muxDownMessage = message; muxDownCause = cause; muxLock.notifyAll(); } /* * The following should be safe because we just left the * synchonized block, and after setting the muxDown latch * therein, no other thread should ever touch the "sessions" * data structure. * * Sessions are shut down asynchronously in a separate thread * to avoid deadlock, in case our caller holds muxLock, * because individual session locks must never be acquired * while holding muxLock. */ boolean needWorker = false; synchronized (sessionShutdownQueue) { if (!sessions.values().isEmpty()) { sessionShutdownQueue.add(new SessionShutdownTask( (Session[]) sessions.values().toArray( new Session[sessions.values().size()]), message, cause)); needWorker = true; } else { needWorker = !sessionShutdownQueue.isEmpty(); } } if (needWorker) { try { systemThreadPool.execute(new Runnable() { public void run() { while (true) { Runnable task; synchronized (sessionShutdownQueue) { if (sessionShutdownQueue.isEmpty()) { break; } task = (Runnable) sessionShutdownQueue.removeFirst(); } task.run(); } } }, "mux session shutdown"); } catch (OutOfMemoryError e) { // assume out of threads try { logger.log(Level.WARNING, "could not create thread for session shutdown", e); } catch (Throwable t) { } // absorb exception to proceed with connection shutdown; // session shutdown task will remain on queue for later } } handleDown(); } /** * Removes the identified session from the session table. * * This method is intended to be invoked by the associated Session * object only. */ final void removeSession(int sessionID) { synchronized (muxLock) { if (muxDown) { return; } assert busySessions.get(sessionID); busySessions.clear(sessionID); sessions.remove(new Integer(sessionID)); } } /** * Returns true if it would be useful to pass direct buffers to * this instance's *Send* methods (because the underlying I/O * implementation will pass such buffers directly to channel write * operations); returns false otherwise. */ final boolean directBuffersUseful() { return directBuffersUseful; } /** * Sends the ClientConnectionHeader message for this connection. */ final void asyncSendClientConnectionHeader() { assert role == CLIENT; ByteBuffer header = ByteBuffer.allocate(8); header.put(magic) .put((byte) VERSION) .putShort((short) (initialInboundRation >> 8)) .put((byte) 0) .flip(); connectionIO.asyncSend(header); } /** * Sends the ServerConnectionHeader message for this connection. */ final void asyncSendServerConnectionHeader() { assert role == SERVER; ByteBuffer header = ByteBuffer.allocate(8); header.put(magic) .put((byte) VERSION) .putShort((short) (initialInboundRation >> 8)) .put((byte) 0) .flip(); connectionIO.asyncSend(header); } /** * Sends a NoOperation message with the contents of the supplied buffer * as the data. * * The "length" of the NoOperation message will be the number of bytes * remaining in the buffer, and the data sent will be the contents * of the buffer between its current position and its limit. Or if * the buffer argument is null, "length" will simply be zero... * REMIND: split into two methods instead? * * The actual writing to the underlying connection, including access to * the buffer's content and other state, is asynchronous with the * invocation of this method; therefore, the supplied buffer must not * be mutated even after this method has returned. */ final void asyncSendNoOperation(ByteBuffer buffer) { ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) NoOperation) .put((byte) 0); if (buffer != null) { assert buffer.remaining() <= 0xFFFF; header.putShort((short) buffer.remaining()) .flip(); connectionIO.asyncSend(header, buffer); } else { header.putShort((short) 0) .flip(); connectionIO.asyncSend(header); } } /** * Sends a Shutdown message with the UTF-8 encoding of the supplied * message as the data. If message is null, then zero bytes of data * will be sent with the message header. */ final void asyncSendShutdown(String message) { ByteBuffer data = (message != null ? getUTF8BufferFromString(message) : null); ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) Shutdown) .put((byte) 0); if (data != null) { assert data.remaining() <= 0xFFFF; header.putShort((short) data.remaining()) .flip(); connectionIO.asyncSend(header, data); } else { header.putShort((short) 0) .flip(); connectionIO.asyncSend(header); } } /** * Sends a Ping message with the specified "cookie". */ final void asyncSendPing(int cookie) { assert cookie >= 0 && cookie <= 0xFFFF; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) Ping) .put((byte) 0) .putShort((short) cookie) .flip(); connectionIO.asyncSend(header); } /** * Sends a PingAck message with the specified "cookie". */ final void asyncSendPingAck(int cookie) { assert cookie >= 0 && cookie <= 0xFFFF; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) PingAck) .put((byte) 0) .putShort((short) cookie) .flip(); connectionIO.asyncSend(header); } /** * Sends an Error message with the UTF-8 encoding of the supplied * message as the data. If message is null, then zero bytes of data * will be sent with the message header. */ final void asyncSendError(String message) { ByteBuffer data = (message != null ? getUTF8BufferFromString(message) : null); ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) Error) .put((byte) 0); if (data != null) { assert data.remaining() <= 0xFFFF; header.putShort((short) data.remaining()) .flip(); connectionIO.asyncSend(header, data); } else { header.putShort((short) 0) .flip(); connectionIO.asyncSend(header); } } /** * Sends an Error message with the UTF-8 encoding of the supplied * message as the data. If message is null, then zero bytes of data * will be sent with the message header. */ final IOFuture futureSendError(String message) { ByteBuffer data = getUTF8BufferFromString(message); ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) Error) .put((byte) 0); assert data.remaining() <= 0xFFFF; header.putShort((short) data.remaining()) .flip(); return connectionIO.futureSend(header, data); } /** * Sends an IncrementRation message for the specified "sessionID" and * the specified "increment". */ final void asyncSendIncrementRation(/*****int op, *****/int sessionID, int increment) { final int op = IncrementRation; // assert (op & 0xF1) == IncrementRation; // validate operation code // assert (op & 0xE0) == 0; // NYI: support use of shift assert sessionID >= 0 && sessionID <= MAX_SESSION_ID; assert increment >= 0 && increment <= 0xFFFF; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) op) .put((byte) sessionID) .putShort((short) increment) .flip(); connectionIO.asyncSend(header); } /** * Sends an Abort message for the specified "sessionID" with the contents * of the specified buffer as the data. * * The "length" of the Abort message will be the number of bytes * remaining in the buffer, and the data sent will be the contents * of the buffer between its current position and its limit. Or if * the buffer argument is null, "length" will simply be zero... * REMIND: split into two methods instead? * * For efficiency, the caller is responsible for pre-computing the first * byte of the message, including any control flags if appropriate. */ final void asyncSendAbort(int op, int sessionID, ByteBuffer data) { assert (op & 0xFD) == Abort; // validate operation code assert sessionID >= 0 && sessionID <= MAX_SESSION_ID; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) op) .put((byte) sessionID); if (data != null) { assert data.remaining() <= 0xFFFF; header.putShort((short) data.remaining()) .flip(); connectionIO.asyncSend(header, data); } else { header.putShort((short) 0) .flip(); connectionIO.asyncSend(header); } } /** * Sends a Close message for the specified "sessionID". */ final void asyncSendClose(int sessionID) { assert sessionID >= 0 && sessionID <= MAX_SESSION_ID; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) Close) .put((byte) sessionID) .putShort((short) 0) .flip(); connectionIO.asyncSend(header); } /** * Sends an Acknowledgment message for the specified "sessionID". */ final void asyncSendAcknowledgment(int sessionID) { assert sessionID >= 0 && sessionID <= MAX_SESSION_ID; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) Acknowledgment) .put((byte) sessionID) .putShort((short) 0) .flip(); connectionIO.asyncSend(header); } /** * Sends a Data message for the specified "sessionID" with the contents * of the supplied buffer as the data. * * The "length" of the Data message will be the number of bytes * remaining in the buffer, and the data sent will be the contents * of the buffer between its current position and its limit. Or if * the buffer argument is null, "length" will simply be zero... * REMIND: split into two methods instead? * * For efficiency, the caller is responsible for pre-computing the first * byte of the Data message, including any control flags if appropriate. * * The actual writing to the underlying connection, including access to * the buffer's content and other state, is asynchronous with the * invocation of this method; therefore, the supplied buffer must not * be mutated even after this method has returned. */ final void asyncSendData(int op, int sessionID, ByteBuffer data) { assert (op & 0xE1) == Data; // validate operation code assert (op & Data_eof) != 0 || // close and ackRequired require eof (op & Data_close & Data_ackRequired) == 0; assert sessionID >= 0 && sessionID <= MAX_SESSION_ID; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) op) .put((byte) sessionID); if (data != null) { assert data.remaining() <= 0xFFFF; header.putShort((short) data.remaining()) .flip(); connectionIO.asyncSend(header, data); } else { header.putShort((short) 0) .flip(); connectionIO.asyncSend(header); } } /** * Sends a Data message for the specified sessionID with the contents * of the supplied buffer as the data. * * The "length" of the Data message will be the number of bytes * remaining in the buffer, and the data sent will be the contents * of the buffer between its current position and its limit. * * For efficiency, the caller is responsible for pre-computing the first * byte of the Data message, including any control flags if appropriate. * * The actual writing to the underlying connection, including access to * the buffer's content and other state, is asynchronous with the * invocation of this method; therefore, the supplied buffer must not * be mutated even after this method has returned, until it is guaranteed * that use of the buffer has completed. * * The returned IOFuture object can be used to wait until the write has * definitely completed (or will definitely not complete due to some * failure). After the write has completed, the buffer's position will * have been incremented to its limit (which will not have changed). */ final IOFuture futureSendData(int op, int sessionID, ByteBuffer data) { assert (op & 0xE1) == Data; // verify operation code assert (op & Data_eof) != 0 || // close and ackRequired require eof (op & Data_close & Data_ackRequired) == 0; assert sessionID >= 0 && sessionID <= MAX_SESSION_ID; assert data.remaining() <= 0xFFFF; ByteBuffer header = ByteBuffer.allocate(4); header.put((byte) op) .put((byte) sessionID) .putShort((short) data.remaining()) .flip(); return connectionIO.futureSend(header, data); } /* * read states */ private static final int READ_CLIENT_CONNECTION_HEADER = 0; private static final int READ_SERVER_CONNECTION_HEADER = 1; private static final int READ_MESSAGE_HEADER = 2; private static final int READ_MESSAGE_BODY = 3; /* * current read state lock and variables */ private final Object readStateLock = new Object(); private int readState; private int currentOp; private int currentSessionID; private int currentLengthRemaining; private ByteBuffer currentDataBuffer = null; void processIncomingData(ByteBuffer buffer) throws ProtocolException { buffer.flip(); // process data that has been read into buffer assert buffer.hasRemaining(); synchronized (readStateLock) { stateLoop: do { switch (readState) { case READ_CLIENT_CONNECTION_HEADER: if (!readClientConnectionHeader(buffer)) { break stateLoop; } break; case READ_SERVER_CONNECTION_HEADER: if (!readServerConnectionHeader(buffer)) { break stateLoop; } break; case READ_MESSAGE_HEADER: if (!readMessageHeader(buffer)) { break stateLoop; } break; case READ_MESSAGE_BODY: if (!readMessageBody(buffer)) { break stateLoop; } break; default: throw new AssertionError(); } } while (buffer.hasRemaining()); } buffer.compact(); } private boolean readClientConnectionHeader(ByteBuffer buffer) throws ProtocolException { assert role == SERVER; validatePartialMagicNumber(buffer); if (buffer.remaining() < 8) { return false; // wait for complete header to arrive } int headerPosition = buffer.position(); buffer.position(headerPosition + 4); // skip header already checked int version = (buffer.get() & 0xFF); int ration = (buffer.getShort() & 0xFFFF) << 8; int flags = (buffer.get() & 0xFF); boolean negotiate = (flags & ClientConnectionHeader_negotiate) != 0; synchronized (muxLock) { initialOutboundRation = ration; asyncSendServerConnectionHeader(); if (version == 0) { throw new ProtocolException( "bad protocol version: " + version); } if (version > VERSION) { if (!negotiate) { setDown("unsupported protocol version: " + version, null); throw new ProtocolException( "unsupported protocol version: " + version); } } serverConnectionReady = true; } readState = READ_MESSAGE_HEADER; return true; } private boolean readServerConnectionHeader(ByteBuffer buffer) throws ProtocolException { assert role == CLIENT; validatePartialMagicNumber(buffer); if (buffer.remaining() < 8) { return false; } int headerPosition = buffer.position(); buffer.position(headerPosition + 4); // skip header already checked int version = (buffer.get() & 0xFF); int ration = (buffer.getShort() & 0xFFFF) << 8; int flags = (buffer.get() & 0xFF); synchronized (muxLock) { initialOutboundRation = ration; if (version == 0) { throw new ProtocolException( "bad protocol version: " + version); } if (version > VERSION) { throw new ProtocolException( "unexpected protocol version: " + version); } clientConnectionReady = true; muxLock.notifyAll(); } readState = READ_MESSAGE_HEADER; return true; } private void validatePartialMagicNumber(ByteBuffer buffer) throws ProtocolException { if (buffer.remaining() > 0) { byte[] temp = new byte[Math.min(buffer.remaining(), magic.length)]; buffer.mark(); buffer.get(temp); buffer.reset(); for (int i = 0; i < temp.length; i++) { if (temp[i] != magic[i]) { setDown((role == CLIENT ? "server" : "client") + " sent bad magic number: " + toHexString(temp), null); throw new ProtocolException("bad magic number: " + toHexString(temp)); } } } } private boolean readMessageHeader(ByteBuffer buffer) throws ProtocolException { if (buffer.remaining() < 4) { return false; // wait for complete header to arrive } int headerPosition = buffer.position(); if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "message header: " + toHexString(buffer.getInt(headerPosition))); } int op = (buffer.get() & 0xFF); if ((op & 0xE1) == Data) { int sessionID = (buffer.get() & 0xFF); if (sessionID > MAX_SESSION_ID) { throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } currentOp = op; currentSessionID = sessionID; currentLengthRemaining = (buffer.getShort() & 0xFFFF); if (currentLengthRemaining > 0) { currentDataBuffer = ByteBuffer.allocate(currentLengthRemaining); readState = READ_MESSAGE_BODY; } else { dispatchCurrentMessage(); } return true; } else if ((op & 0xF1) == IncrementRation) { int sessionID = (buffer.get() & 0xFF); if (sessionID > MAX_SESSION_ID) { throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } int increment = (buffer.getShort() & 0xFFFF); int shift = op & IncrementRation_shift; increment <<= shift; handleIncrementRation(sessionID, increment); return true; } else if ((op & 0xFD) == Abort) { int sessionID = (buffer.get() & 0xFF); if (sessionID > MAX_SESSION_ID) { throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } currentOp = op; currentSessionID = sessionID; currentLengthRemaining = (buffer.getShort() & 0xFFFF); if (currentLengthRemaining > 0) { currentDataBuffer = ByteBuffer.allocate(currentLengthRemaining); readState = READ_MESSAGE_BODY; } else { dispatchCurrentMessage(); } return true; } switch (op) { case NoOperation: { if (buffer.get() != 0) { // ignore sign extension throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } currentOp = op; currentLengthRemaining = (buffer.getShort() & 0xFFFF); currentDataBuffer = null; // ignore data for NoOperation if (currentLengthRemaining > 0) { readState = READ_MESSAGE_BODY; } else { dispatchCurrentMessage(); } return true; } case Shutdown: { if (buffer.get() != 0) { // ignore sign extension throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } currentOp = op; currentLengthRemaining = (buffer.getShort() & 0xFFFF); if (currentLengthRemaining > 0) { currentDataBuffer = ByteBuffer.allocate(currentLengthRemaining); readState = READ_MESSAGE_BODY; } else { dispatchCurrentMessage(); } return true; } case Ping: { if (buffer.get() != 0) { // ignore sign extension throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } int cookie = (buffer.getShort() & 0xFFFF); handlePing(cookie); return true; } case PingAck: { if (buffer.get() != 0) { // ignore sign extension throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } int cookie = (buffer.getShort() & 0xFFFF); handlePingAck(cookie); return true; } case Error: { if (buffer.get() != 0) { // ignore sign extension throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } currentOp = op; currentLengthRemaining = (buffer.getShort() & 0xFFFF); if (currentLengthRemaining > 0) { currentDataBuffer = ByteBuffer.allocate(currentLengthRemaining); readState = READ_MESSAGE_BODY; } else { dispatchCurrentMessage(); } return true; } case Close: { int sessionID = (buffer.get() & 0xFF); if (sessionID > MAX_SESSION_ID || buffer.getShort() != 0) // ignore sign extension { throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } handleClose(sessionID); return true; } case Acknowledgment: { int sessionID = (buffer.get() & 0xFF); if (sessionID > MAX_SESSION_ID || buffer.getShort() != 0) // ignore sign extension { throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } handleAcknowledgment(sessionID); return true; } default: throw new ProtocolException("bad message header: " + toHexString(buffer.getInt(headerPosition))); } } private boolean readMessageBody(ByteBuffer buffer) throws ProtocolException { assert currentLengthRemaining > 0; assert currentDataBuffer == null || currentDataBuffer.remaining() == currentLengthRemaining; if (buffer.remaining() > currentLengthRemaining) { int origLimit = buffer.limit(); buffer.limit(buffer.position() + currentLengthRemaining); if (currentDataBuffer != null) { currentDataBuffer.put(buffer); } else { buffer.position(buffer.position() + currentLengthRemaining); } currentLengthRemaining = 0; buffer.limit(origLimit); } else { currentLengthRemaining -= buffer.remaining(); if (currentDataBuffer != null) { currentDataBuffer.put(buffer); } else { buffer.position(buffer.limit()); } } if (currentLengthRemaining > 0) { return false; } else { currentDataBuffer.flip(); dispatchCurrentMessage(); currentDataBuffer = null; // don't let this linger readState = READ_MESSAGE_HEADER; return true; } } private void dispatchCurrentMessage() throws ProtocolException { assert currentDataBuffer == null || currentDataBuffer.hasRemaining(); int op = currentOp; if ((op & 0xE1) == Data) { boolean open = (op & Data_open) != 0; boolean close = (op & Data_close) != 0; boolean eof = (op & Data_eof) != 0; boolean ackRequired = (op & Data_ackRequired) != 0; handleData(currentSessionID, open, close, eof, ackRequired, (currentDataBuffer != null ? currentDataBuffer : ByteBuffer.allocate(0))); return; } else if ((op & 0xFD) == Abort) { boolean partial = (op & Abort_partial) != 0; handleAbort(currentSessionID, partial, (currentDataBuffer != null ? getStringFromUTF8Buffer(currentDataBuffer) : "")); return; } switch (op) { case NoOperation: handleNoOperation(); return; case Shutdown: handleShutdown(currentDataBuffer != null ? getStringFromUTF8Buffer(currentDataBuffer) : ""); return; case Error: handleError(currentDataBuffer != null ? getStringFromUTF8Buffer(currentDataBuffer) : ""); return; default: throw new AssertionError(Integer.toHexString((byte) op)); } } private void handleNoOperation() throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "NoOperation"); } // do nothing } private void handleShutdown(String message) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Shutdown"); } if (role != CLIENT) { throw new ProtocolException("Shutdown sent by client"); } setDown("mux connection shut down gracefully", null); throw new ProtocolException("received Shutdown message"); } private void handlePing(int cookie) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Ping: cookie=" + cookie); } asyncSendPingAck(cookie); } private void handlePingAck(int cookie) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "PingAck: cookie=" + cookie); } synchronized (muxLock) { if (cookie != expectedPingCookie) { throw new ProtocolException( "unexpected ping cookie: " + cookie); } else { expectedPingCookie = -1; // NYI: rest of ping machinery } } } private void handleError(String message) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Error"); } setDown((role == CLIENT ? "server" : "client") + " reported protocol error: " + message, null); throw new ProtocolException("received Error message"); } private void handleIncrementRation(int sessionID, int increment) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "IncrementRation: sessionID=" + sessionID + ",increment=" + increment); } getSession(sessionID).handleIncrementRation(increment); } private void handleAbort(int sessionID, boolean partial, String message) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Abort: sessionID=" + sessionID + ",partial=" + partial); } getSession(sessionID).handleAbort(partial); } private void handleClose(int sessionID) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Close: sessionID=" + sessionID); } getSession(sessionID).handleClose(); } private void handleAcknowledgment(int sessionID) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "Acknowledgment: sessionID=" + sessionID); } getSession(sessionID).handleAcknowledgment(); } private void handleData(int sessionID, boolean open, boolean close, boolean eof, boolean ackRequired, ByteBuffer data) throws ProtocolException { if (logger.isLoggable(Level.FINEST)) { int length = data.remaining(); HexDumpEncoder encoder = new HexDumpEncoder(); byte[] bytes = new byte[data.remaining()]; data.mark(); data.get(bytes); data.reset(); logger.log(Level.FINEST, "Data: sessionID=" + sessionID + (open ? ",open" : "") + (close ? ",close" : "") + (eof ? ",eof" : "") + (ackRequired ? ",ackRequired" : "") + ",length=" + length + (length > 0 ? ",data=\n" + encoder.encode(bytes) : "")); } if (!eof && (close || ackRequired)) { throw new ProtocolException("Data: eof=" + eof + ",close=" + close + ",ackRequired=" + ackRequired); } if (open) { handleOpen(sessionID); } getSession(sessionID).handleData(data, eof, close, ackRequired); } private Session getSession(int sessionID) throws ProtocolException { synchronized (muxLock) { if (!busySessions.get(sessionID)) { throw new ProtocolException( "inactive sessionID: " + sessionID); } return (Session) sessions.get(new Integer(sessionID)); } } private static ByteBuffer getUTF8BufferFromString(String s) { CharsetEncoder encoder = Charset.forName("UTF-8").newEncoder(); try { return encoder.encode(CharBuffer.wrap(s)); } catch (CharacterCodingException e) { return null; } } private static String getStringFromUTF8Buffer(ByteBuffer buffer) { CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder(); try { return decoder.decode(buffer).toString(); } catch (CharacterCodingException e) { return "(error decoding UTF-8 message: " + e.toString() + ")"; } } private static String toHexString(byte x) { char[] buf = new char[2]; buf[0] = toHexChar((x >> 4) & 0xF); buf[1] = toHexChar(x & 0xF); return new String(buf); } private static String toHexString(int x) { char[] buf = new char[8]; for (int i = 0; i < 8; i++) { buf[i] = toHexChar((x >> ((7 - i) * 4)) & 0xF); } return new String(buf); } private static String toHexString(byte[] b) { char[] buf = new char[b.length * 2]; int j = 0; for (int i = 0; i < b.length; i++) { buf[j++] = toHexChar((b[i] >> 4) & 0xF); buf[j++] = toHexChar(b[i] & 0xF); } return new String(buf); } private static char toHexChar(int x) { return x < 10 ? (char) ('0' + x) : (char) ('A' - 10 + x); } }