/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2013 Red Hat, Inc. and/or its affiliates, and individual
* contributors as indicated by the @author tags.
*
* Licensed 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.xnio.ssl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.xnio.ssl.mock.SSLEngineMock.CLOSE_MSG;
import static org.xnio.ssl.mock.SSLEngineMock.HANDSHAKE_MSG;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.FINISH;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_UNWRAP;
import static org.xnio.ssl.mock.SSLEngineMock.HandshakeAction.NEED_WRAP;
import java.io.IOException;
import java.nio.ByteBuffer;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import org.jmock.Expectations;
import org.junit.Test;
import org.xnio.BufferAllocator;
import org.xnio.ByteBufferSlicePool;
import org.xnio.ChannelListener;
import org.xnio.Options;
import org.xnio.Pool;
import org.xnio.channels.AssembledConnectedSslStreamChannel;
import org.xnio.channels.ConnectedSslStreamChannel;
import org.xnio.channels.ConnectedStreamChannel;
import org.xnio.channels.SslChannel;
import org.xnio.ssl.mock.SSLEngineMock.HandshakeAction;
/**
* Test for AssembledConnectedSslStreamChannel on start tls mode.
*
* @author <a href="mailto:frainone@redhat.com">Flavia Rainone</a>
*/
public class StartTLSChannelTestCase extends AbstractConnectedSslStreamChannelTest {
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
protected ConnectedSslStreamChannel createSslChannel() {
final Pool<ByteBuffer> socketBufferPool = new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 17000, 17000 * 16);
final Pool<ByteBuffer> applicationBufferPool = new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 17000, 17000 * 16);
final SslConnection connection = new JsseSslConnection(connectionMock, engineMock, socketBufferPool, applicationBufferPool);
final ConnectedSslStreamChannel channel = new AssembledConnectedSslStreamChannel(connection, connection.getSourceChannel(), connection.getSinkChannel());
final ChannelListener readListener = context.mock(ChannelListener.class, "read listener");
channel.getReadSetter().set(readListener);
context.checking(new Expectations() {{
allowing(readListener).handleEvent(with(any(ConnectedStreamChannel.class)));
}});
channel.getWriteSetter().set(context.mock(ChannelListener.class, "write listener"));
return channel;
}
@Test
public void getSecureOption() throws IOException {
assertFalse(sslChannel.getOption(Options.SECURE));
sslChannel.startHandshake();
assertTrue(sslChannel.getOption(Options.SECURE));
}
@Test
public void getSslSession() throws IOException {
assertNull(sslChannel.getSslSession());
sslChannel.startHandshake();
assertNotNull(sslChannel.getSslSession());
}
@Test
public void testSimpleFlush() throws IOException {
// handshake action: NEED_WRAP
conduitMock.enableWrites(true);
final ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("MSG".getBytes("UTF-8")).flip();
assertEquals(3, sslChannel.write(buffer));
assertWrittenMessage("MSG");
assertFalse(conduitMock.isFlushed());
sslChannel.flush();
assertTrue(conduitMock.isFlushed());
assertWrittenMessage("MSG");
}
@Test
public void testFlushWithHandshaking() throws IOException {
// handshake action: NEED_WRAP... this hanshake action will be ignored on START_TLS mode
engineMock.setHandshakeActions(NEED_WRAP);
conduitMock.enableWrites(true);
final ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("MSG".getBytes("UTF-8")).flip();
assertEquals(3, sslChannel.write(buffer));
assertWrittenMessage("MSG");
assertFalse(conduitMock.isFlushed());
sslChannel.flush();
assertTrue(conduitMock.isFlushed());
assertWrittenMessage("MSG");
}
@Test
public void readNeedsWrapWriteAndReadDisabled() throws IOException {
// handshake action: NEED_WRAP
engineMock.setHandshakeActions(NEED_WRAP, FINISH);
conduitMock.setReadData(CLOSE_MSG);
conduitMock.enableReads(false);
conduitMock.enableWrites(false);
final ByteBuffer buffer = ByteBuffer.allocate(100);
assertFalse(conduitMock.isReadAwaken());
sslChannel.resumeReads();
assertTrue(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadAwaken());
assertFalse(conduitMock.isWriteResumed());
// attempt to read... channel is expected to return 0 as it stumbles upon a NEED_WRAP that cannot be executed
assertEquals(0, sslChannel.read(new ByteBuffer[]{buffer}));
// everything is kept the same
assertFalse(conduitMock.isReadResumed());
assertTrue(sslChannel.isReadResumed());
assertFalse(conduitMock.isWriteResumed());
assertFalse(sslChannel.isWriteResumed());
// no handshake is performed
assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus());
assertEquals(0, sslChannel.read(buffer));
assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus());
conduitMock.enableReads(true);
assertEquals(7, sslChannel.read(buffer));
assertWrittenMessage(new String[0]);
// close message is just read as a plain message, as sslChannel.read is simply delegated to conduitMock
assertReadMessage(buffer, CLOSE_MSG);
assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus());
sslChannel.shutdownWrites();
assertTrue(sslChannel.flush());
assertTrue(conduitMock.isWriteShutdown());
sslChannel.shutdownReads();
assertTrue(conduitMock.isReadShutdown());
conduitMock.enableWrites(true);
sslChannel.shutdownWrites();
assertTrue(sslChannel.flush());
assertTrue(conduitMock.isWriteShutdown());
assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus());
assertWrittenMessage(new String[0]);
assertTrue(conduitMock.isFlushed());
}
@Test
public void writeNeedsUnwrapReadAndFlushDisabled () throws IOException {
// handshake action: NEED_WRAP
engineMock.setHandshakeActions(NEED_UNWRAP, FINISH);
conduitMock.setReadData(HANDSHAKE_MSG);
conduitMock.enableReads(false);
conduitMock.enableWrites(true);
conduitMock.enableFlush(false);
final ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("MSG".getBytes("UTF-8")).flip();
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isWriteResumed());
// attempt to write... channel is expected to write because, on STARTLS mode, it wil simply delegate the
// write request to conduitMock
assertEquals(3, sslChannel.write(buffer));
// no read/write operation has been resumed either
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isWriteResumed());
// the first handshake action is as before, nothing has changed
assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());
assertFalse(sslChannel.flush());
assertWrittenMessage("MSG");
assertFalse(conduitMock.isFlushed());
conduitMock.enableFlush(true);
assertTrue(sslChannel.flush());
assertWrittenMessage("MSG");
sslChannel.shutdownWrites();
assertTrue(sslChannel.flush());
assertWrittenMessage("MSG");
assertTrue(conduitMock.isFlushed());
}
@Test
public void cantForceResumeReadsOnResumedReadChannel() throws IOException {
sslChannel.resumeReads();
sslChannel.resumeWrites();
assertTrue(sslChannel.isReadResumed());
assertTrue(sslChannel.isWriteResumed());
assertFalse(conduitMock.isReadAwaken());
assertFalse(conduitMock.isWriteAwaken());
engineMock.setHandshakeActions(NEED_UNWRAP);
final ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("COULDNT WRITE WITHOUT UNWRAP".getBytes("UTF-8")).flip();
assertEquals(28, sslChannel.write(buffer));
assertWrittenMessage("COULDNT WRITE WITHOUT UNWRAP");
assertTrue(sslChannel.isWriteResumed());
assertTrue(sslChannel.isReadResumed());
assertTrue(conduitMock.isWriteResumed());
assertFalse(conduitMock.isReadAwaken());
// everything keeps the same at conduitMock when we try to resume reads
sslChannel.resumeWrites();
assertTrue(sslChannel.isWriteResumed());
assertTrue(sslChannel.isReadResumed());
assertTrue(conduitMock.isWriteResumed());
assertFalse(conduitMock.isReadAwaken());
}
@Test
public void cantForceResumeReadsOnSuspendedReadChannel() throws IOException {
// resume writes, reads are suspended
sslChannel.resumeWrites();
assertFalse(sslChannel.isReadResumed());
assertTrue(sslChannel.isWriteResumed());
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isWriteAwaken());
// write needs to unwrap... try to write
engineMock.setHandshakeActions(NEED_UNWRAP);
final ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("COULDNT WRITE WITHOUT UNWRAP".getBytes("UTF-8")).flip();
assertEquals(28, sslChannel.write(buffer));
assertWrittenMessage("COULDNT WRITE WITHOUT UNWRAP");
// nothing happens with read/write resumed on START_TLS channel
assertTrue(sslChannel.isWriteResumed());
assertFalse(sslChannel.isReadResumed());
assertTrue(conduitMock.isWriteResumed());
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isReadAwaken());
// everything keeps the same at conduitMock when we try to resume writes
sslChannel.resumeWrites();
assertTrue(sslChannel.isWriteResumed());
assertFalse(sslChannel.isReadResumed());
assertTrue(conduitMock.isWriteResumed());
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isReadAwaken());
}
@Test
public void resumeAndSuspendReadsOnNewChannel() throws Exception {
// brand newly created sslChannel, isReadable returns aLWAYS and resuming read will awakeReads for conduitMock
assertFalse(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadResumed());
sslChannel.resumeReads();
assertTrue(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadAwaken());
sslChannel.suspendReads();
assertFalse(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadResumed());
}
@Test
public void resumeAndSuspendReads() throws IOException {
assertEquals(0, sslChannel.read(ByteBuffer.allocate(5)));
// not a brand newly created sslChannel, isReadable returns OKAY and resuming read will just reasumeReads for conduitMock
assertFalse(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadResumed());
sslChannel.resumeReads();
assertTrue(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadAwaken());
assertFalse(conduitMock.isReadResumed());
sslChannel.suspendReads();
assertFalse(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadResumed());
}
@Test
public void resumeAndSuspendWritesOnNewChannel() throws Exception {
// brand newly created sslChannel, isWritable returns aLWAYS and resuming writes will awakeWrites for conduitMock
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
sslChannel.resumeWrites();
assertTrue(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteAwaken());
sslChannel.suspendWrites();
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
}
@Test
public void resumeAndSuspendWrites() throws Exception {
assertEquals(0, sslChannel.read(ByteBuffer.allocate(5)));
// not a brand newly created sslChannel, isWritable returns OKAY and resuming writes will just reasumeWritesfor conduitMock
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
sslChannel.resumeWrites();
assertTrue(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteAwaken());
assertTrue(conduitMock.isWriteResumed());
sslChannel.suspendWrites();
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
}
@Test
public void resumeAndSuspendWritesOnWriteNeedsUnwrapChannel() throws Exception {
// create the read needs wrap channel\
engineMock.setHandshakeActions(HandshakeAction.NEED_UNWRAP, HandshakeAction.FINISH);
ByteBuffer buffer = ByteBuffer.allocate(5);
buffer.put("12345".getBytes("UTF-8")).flip();
// channel manages to write anyway, because we are on START_TLS mode
assertEquals(5, sslChannel.write(new ByteBuffer[]{buffer}));
assertWrittenMessage("12345");
// no read action is forced
assertFalse(conduitMock.isReadResumed());
assertFalse(sslChannel.isReadResumed());
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
// try to resume writes
sslChannel.resumeWrites();
assertTrue(sslChannel.isWriteResumed());
assertTrue(conduitMock.isWriteResumed());
sslChannel.suspendWrites();
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
}
@Test
public void suspendWritesOnResumedWriteNeedsUnwrapChannel() throws Exception {
// resume writes first of all
sslChannel.resumeWrites();
assertTrue(sslChannel.isWriteResumed());
assertTrue(conduitMock.isWriteResumed());
assertFalse(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadResumed());
// need unwrap is the first handshake action, and conduitMock has read ops disabled
engineMock.setHandshakeActions(HandshakeAction.NEED_UNWRAP, HandshakeAction.FINISH);
// channel can write regardless of the NEED_UNWRAP handshake action, given START_TLS mode is on
ByteBuffer buffer = ByteBuffer.allocate(1);
buffer.put((byte) 0).flip();
assertEquals(1, sslChannel.write(buffer));
assertWrittenMessage("\0");
assertTrue(sslChannel.isWriteResumed());
assertTrue(conduitMock.isWriteResumed());
assertFalse(sslChannel.isReadResumed());
assertFalse(conduitMock.isReadResumed());
// suspendWrites
sslChannel.suspendWrites();
assertFalse(sslChannel.isWriteResumed());
assertFalse(conduitMock.isWriteResumed());
}
@SuppressWarnings("unchecked")
@Test
public void startTLSWithWriteNeedsUnwrap() throws IOException {
//set a handshake listener
final ChannelListener<SslChannel> listener = context.mock(ChannelListener.class, "write needs unwrap");
context.checking(new Expectations() {{
atLeast(1).of(listener).handleEvent(sslChannel);
}});
sslChannel.getHandshakeSetter().set(listener);
// handshake action: NEED_WRAP
engineMock.setHandshakeActions(NEED_UNWRAP, FINISH);
conduitMock.setReadData(HANDSHAKE_MSG);
conduitMock.enableReads(false);
conduitMock.enableWrites(false);
final ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.put("MSG".getBytes("UTF-8")).flip();
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isWriteResumed());
// start tls
sslChannel.startHandshake();
// attempt to write... channel is expected to return 0 as it stumbles upon a NEED_UNWRAP that cannot be executed
assertEquals(0, sslChannel.write(buffer));
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isWriteResumed());
assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());
assertEquals(0, sslChannel.write(buffer));
assertSame(HandshakeStatus.NEED_UNWRAP, engineMock.getHandshakeStatus());
conduitMock.enableReads(true);
assertEquals(3, sslChannel.write(buffer));
assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());
assertWrittenMessage(new String[0]);
assertFalse(sslChannel.flush());
assertWrittenMessage(new String[0]);
sslChannel.shutdownWrites();
assertFalse(sslChannel.flush());
assertFalse(conduitMock.isWriteShutdown());
conduitMock.enableWrites(true);
/* One might assume that this flush should return false, since the close message has not yet been
received; however, this is not part of the write shutdown contract. Thus as soon as we flush our own
write termination message to the wire, we consider writes to be fully shut down, and that's that. It
is up to the reader side to make sure that read is carried through to EOF before terminating reads. If
a read termination is received before flushing the write shutdown, then shutdown will close the channel.
If the read termination comes after, then upon reading the -1, when reads are terminated, the channel will
close at that time. */
assertTrue(sslChannel.flush());
assertTrue(sslChannel.flush());
conduitMock.setReadData(CLOSE_MSG);
buffer.clear();
assertEquals(-1, sslChannel.read(buffer));
sslChannel.shutdownReads();
assertTrue(sslChannel.flush());
assertTrue(conduitMock.isReadShutdown());
assertTrue(conduitMock.isWriteShutdown());
assertFalse(conduitMock.isOpen());
assertWrittenMessage("MSG", CLOSE_MSG);
assertTrue(conduitMock.isFlushed());
}
@SuppressWarnings("unchecked")
@Test
public void startTLSWithReadNeedsWrap() throws IOException {
//set a handshake listener
final ChannelListener<SslChannel> listener = context.mock(ChannelListener.class, "read needs wrap");
context.checking(new Expectations() {{
atLeast(1).of(listener).handleEvent(sslChannel);
}});
sslChannel.getHandshakeSetter().set(listener);
// handshake action: NEED_WRAP
engineMock.setHandshakeActions(NEED_WRAP, FINISH);
conduitMock.setReadData("MSG");
conduitMock.enableReads(true);
conduitMock.enableWrites(false);
final ByteBuffer buffer = ByteBuffer.allocate(100);
assertFalse(conduitMock.isReadResumed());
assertFalse(conduitMock.isWriteResumed());
// start tls
sslChannel.startHandshake();
// attempt to write... channel is expected to return 3 regardless of the fact it cannot flush
assertSame(HandshakeStatus.NEED_WRAP, engineMock.getHandshakeStatus());
assertEquals(3, sslChannel.read(new ByteBuffer[]{buffer}));
assertReadMessage(buffer, "MSG");
assertSame(HandshakeStatus.NOT_HANDSHAKING, engineMock.getHandshakeStatus());
assertWrittenMessage("");
assertFalse(sslChannel.flush());
// enable writes and hence flush handshake message
conduitMock.enableWrites(true);
assertTrue(sslChannel.flush());
assertWrittenMessage(HANDSHAKE_MSG);
sslChannel.shutdownReads();
assertFalse(conduitMock.isWriteShutdown());
conduitMock.setReadData(CLOSE_MSG);
sslChannel.shutdownWrites();
assertTrue(sslChannel.flush());
assertTrue(conduitMock.isWriteShutdown());
// channel closed
assertFalse(conduitMock.isOpen());
assertWrittenMessage(HANDSHAKE_MSG, CLOSE_MSG);
assertTrue(conduitMock.isFlushed());
}
}