| /* |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| package org.apache.qpid.proton.engine.impl.ssl; |
| |
| import static org.apache.qpid.proton.engine.impl.ByteBufferUtils.pour; |
| import static org.apache.qpid.proton.engine.impl.TransportTestHelper.assertByteBufferContentEquals; |
| import static org.apache.qpid.proton.engine.impl.TransportTestHelper.pourBufferToString; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.charset.StandardCharsets; |
| |
| import javax.net.ssl.SSLException; |
| |
| import org.apache.qpid.proton.engine.Transport; |
| import org.apache.qpid.proton.engine.TransportException; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.ExpectedException; |
| |
| /** |
| * TODO unit test handshaking |
| * TODO unit test closing |
| * TODO unit test graceful handling of SSLEngine.wrap throwing an SSLException |
| */ |
| public class SimpleSslTransportWrapperTest |
| { |
| private RememberingTransportInput _underlyingInput; |
| private CannedTransportOutput _underlyingOutput; |
| private SimpleSslTransportWrapper _sslWrapper; |
| private CapitalisingDummySslEngine _dummySslEngine; |
| |
| @Rule |
| public ExpectedException _expectedException = ExpectedException.none(); |
| |
| @Before |
| public void setUp() |
| { |
| _underlyingInput = new RememberingTransportInput(); |
| _underlyingOutput = new CannedTransportOutput(); |
| _dummySslEngine = new CapitalisingDummySslEngine(); |
| _sslWrapper = new SimpleSslTransportWrapper(_dummySslEngine, _underlyingInput, _underlyingOutput); |
| } |
| |
| @Test |
| public void testInputDecodesOnePacket() |
| { |
| String encodedBytes = "<-A->"; |
| |
| putBytesIntoTransport(encodedBytes); |
| |
| assertEquals("a_", _underlyingInput.getAcceptedInput()); |
| assertEquals(CapitalisingDummySslEngine.MAX_ENCODED_CHUNK_SIZE, _sslWrapper.capacity()); |
| assertEquals(2, _dummySslEngine.getUnwrapCount());// 1 packet, 1 underflow |
| assertEquals(1, _underlyingInput.getProcessCount()); |
| } |
| |
| /** |
| * Note that this only feeds 1 encoded packet in at a time due to default settings of the dummy engine, |
| * See {@link #testUnderlyingInputUsingSmallBuffer_receivesAllDecodedInputRequiringMultipleUnwraps} |
| * for a related test that passes multiple encoded packets into the ssl wrapper at once. |
| */ |
| @Test |
| public void testInputWithMultiplePackets() |
| { |
| String encodedBytes = "<-A-><-B-><-C-><>"; |
| |
| putBytesIntoTransport(encodedBytes); |
| |
| assertEquals("a_b_c_z_", _underlyingInput.getAcceptedInput()); |
| assertEquals(CapitalisingDummySslEngine.MAX_ENCODED_CHUNK_SIZE, _sslWrapper.capacity()); |
| assertEquals(8, _dummySslEngine.getUnwrapCount()); // (1 decode + 1 underflow) * 4 packets |
| assertEquals(4, _underlyingInput.getProcessCount()); // 1 process per decoded packet |
| } |
| |
| @Test |
| public void testInputIncompletePacket_isNotPassedToUnderlyingInputUntilCompleted() |
| { |
| String incompleteEncodedBytes = "<-A-><-B-><-C"; // missing the trailing '>' to cause the underflow |
| String remainingEncodedBytes = "-><-D->"; |
| |
| putBytesIntoTransport(incompleteEncodedBytes); |
| assertEquals("a_b_", _underlyingInput.getAcceptedInput()); |
| assertEquals(5, _dummySslEngine.getUnwrapCount()); // 2 * (1 decode + 1 underflow) + 1 underflow |
| assertEquals(2, _underlyingInput.getProcessCount()); // 1 process per decoded packet |
| |
| putBytesIntoTransport(remainingEncodedBytes); |
| assertEquals("a_b_c_d_", _underlyingInput.getAcceptedInput()); |
| assertEquals(4, _underlyingInput.getProcessCount()); // earlier + 2 |
| assertEquals(9, _dummySslEngine.getUnwrapCount()); // Earlier + 2 * (1 decode + 1 underflow) |
| // due to way the bytes are fed in across |
| // boundary of encoded packets |
| } |
| |
| /** |
| * As per {@link #testInputIncompletePacket_isNotPassedToUnderlyingInputUntilCompleted()} |
| * but this time it takes TWO chunks to complete the "dangling" packet. |
| */ |
| @Test |
| public void testInputIncompletePacketInThreeParts() |
| { |
| String firstEncodedBytes = "<-A-><-B-><-"; |
| String secondEncodedBytes = "C"; // Sending this causes the impl to have to hold the data without producing more input yet |
| String thirdEncodedBytes = "-><-D->"; |
| |
| putBytesIntoTransport(firstEncodedBytes); |
| assertEquals("a_b_", _underlyingInput.getAcceptedInput()); |
| assertEquals(5, _dummySslEngine.getUnwrapCount()); // 2 * (1 decode + 1 underflow) + 1 underflow |
| assertEquals(2, _underlyingInput.getProcessCount()); // 1 process per decoded packet |
| |
| putBytesIntoTransport(secondEncodedBytes); |
| assertEquals("a_b_", _underlyingInput.getAcceptedInput()); |
| assertEquals(6, _dummySslEngine.getUnwrapCount()); // earlier + 1 underflow |
| assertEquals(2, _underlyingInput.getProcessCount()); // as earlier |
| |
| putBytesIntoTransport(thirdEncodedBytes); |
| assertEquals("a_b_c_d_", _underlyingInput.getAcceptedInput()); |
| assertEquals(4, _underlyingInput.getProcessCount()); // 1 process per decoded packet |
| assertEquals(10, _dummySslEngine.getUnwrapCount()); // Earlier + (decode + underflow) * 2 |
| // due to way the bytes are fed in across |
| // boundary of encoded packets |
| } |
| |
| /** |
| * Tests that when a small underlying input buffer (1 byte here) is used, all of the encoded |
| * data packet (5 bytes each here) can be processed despite multiple attempts being required to |
| * pass the decoded bytes (2 bytes here) to the underlying input layer for processing. |
| */ |
| @Test |
| public void testUnderlyingInputUsingSmallBuffer_receivesAllDecodedInputRequiringMultipleUnderlyingProcesses() |
| { |
| int underlyingInputBufferSize = 1; |
| int encodedPacketSize = 5; |
| |
| _underlyingInput.setInputBufferSize(underlyingInputBufferSize); |
| assertEquals("Unexpected underlying input capacity", underlyingInputBufferSize, _underlyingInput.capacity()); |
| |
| assertEquals("Unexpected max encoded chunk size", encodedPacketSize, CapitalisingDummySslEngine.MAX_ENCODED_CHUNK_SIZE); |
| |
| byte[] bytes = "<-A-><-B->".getBytes(StandardCharsets.UTF_8); |
| ByteBuffer encodedByteSource = ByteBuffer.wrap(bytes); |
| |
| assertEquals("Unexpected initial capacity", encodedPacketSize, _sslWrapper.capacity()); |
| |
| // Process the first 'encoded packet' (<-A->) |
| int numberPoured = pour(encodedByteSource, _sslWrapper.tail()); |
| assertEquals("Unexpected number of bytes poured into the wrapper input buffer", encodedPacketSize, numberPoured); |
| assertEquals("Unexpected position in encoded source byte buffer", encodedPacketSize * 1, encodedByteSource.position()); |
| assertEquals("Unexpected capacity", 0, _sslWrapper.capacity()); |
| _sslWrapper.process(); |
| assertEquals("Unexpected capacity", encodedPacketSize, _sslWrapper.capacity()); |
| |
| assertEquals("unexpected underlying output after first wrapper process", "a_", _underlyingInput.getAcceptedInput()); |
| assertEquals("unexpected underlying process count after first wrapper process", 2 , _underlyingInput.getProcessCount()); |
| |
| // Process the second 'encoded packet' (<-B->) |
| numberPoured = pour(encodedByteSource, _sslWrapper.tail()); |
| assertEquals("Unexpected number of bytes poured into the wrapper input buffer", encodedPacketSize, numberPoured); |
| assertEquals("Unexpected position in encoded source byte buffer", encodedPacketSize * 2, encodedByteSource.position()); |
| assertEquals("Unexpected capacity", 0, _sslWrapper.capacity()); |
| _sslWrapper.process(); |
| assertEquals("Unexpected capacity", encodedPacketSize, _sslWrapper.capacity()); |
| |
| assertEquals("unexpected underlying output after second wrapper process", "a_b_", _underlyingInput.getAcceptedInput()); |
| assertEquals("unexpected underlying process count after second wrapper process", 4 , _underlyingInput.getProcessCount()); |
| } |
| |
| /** |
| * Tests that when a small underlying input buffer (1 byte here) is used, all of the encoded |
| * data packets (20 bytes total here) can be processed despite multiple unwraps being required |
| * to process a given set of input (3 packets, 15 bytes here) and then as a result also multiple |
| * attempts to pass the decoded packet (2 bytes here) to the underlying input layer for processing. |
| */ |
| @Test |
| public void testUnderlyingInputUsingSmallBuffer_receivesAllDecodedInputRequiringMultipleUnwraps() |
| { |
| int underlyingInputBufferSize = 1; |
| int encodedPacketSize = 5; |
| int sslEngineBufferSize = 15; |
| |
| assertEquals("Unexpected max encoded chunk size", encodedPacketSize, CapitalisingDummySslEngine.MAX_ENCODED_CHUNK_SIZE); |
| |
| _underlyingOutput = new CannedTransportOutput(); |
| _underlyingInput = new RememberingTransportInput(); |
| _underlyingInput.setInputBufferSize(underlyingInputBufferSize); |
| assertEquals("Unexpected underlying input capacity", underlyingInputBufferSize, _underlyingInput.capacity()); |
| |
| // Create a dummy ssl engine that has buffers that holds multiple encoded/decoded |
| // packets, but still can't fit all of the input |
| _dummySslEngine = new CapitalisingDummySslEngine(); |
| _dummySslEngine.setApplicationBufferSize(sslEngineBufferSize); |
| _dummySslEngine.setPacketBufferSize(sslEngineBufferSize); |
| |
| _sslWrapper = new SimpleSslTransportWrapper(_dummySslEngine, _underlyingInput, _underlyingOutput); |
| |
| byte[] bytes = "<-A-><-B-><-C-><-D->".getBytes(StandardCharsets.UTF_8); |
| ByteBuffer encodedByteSource = ByteBuffer.wrap(bytes); |
| |
| assertEquals("Unexpected initial capacity", sslEngineBufferSize, _sslWrapper.capacity()); |
| |
| // Process the first three 'encoded packets' (<-A-><-B-><-C->). This will require 3 'proper' unwraps, and |
| // as each decoded packet is 2 bytes, each of those will require 2 underlying input processes. |
| int numberPoured = pour(encodedByteSource, _sslWrapper.tail()); |
| assertEquals("Unexpected number of bytes poured into the wrapper input buffer", sslEngineBufferSize, numberPoured); |
| assertEquals("Unexpected position in encoded source byte buffer", encodedPacketSize * 3, encodedByteSource.position()); |
| assertEquals("Unexpected capacity", 0, _sslWrapper.capacity()); |
| |
| _sslWrapper.process(); |
| |
| assertEquals("a_b_c_", _underlyingInput.getAcceptedInput()); |
| assertEquals("Unexpected capacity", sslEngineBufferSize, _sslWrapper.capacity()); |
| assertEquals("unexpected underlying process count after wrapper process", 6 , _underlyingInput.getProcessCount()); |
| assertEquals(4, _dummySslEngine.getUnwrapCount()); // 3 decodes + 1 underflow |
| |
| // Process the fourth 'encoded packet' (<-D->) |
| numberPoured = pour(encodedByteSource, _sslWrapper.tail()); |
| assertEquals("Unexpected number of bytes poured into the wrapper input buffer", encodedPacketSize, numberPoured); |
| assertEquals("Unexpected position in encoded source byte buffer", encodedPacketSize * 4, encodedByteSource.position()); |
| assertEquals("Unexpected capacity", sslEngineBufferSize - encodedPacketSize, _sslWrapper.capacity()); |
| |
| _sslWrapper.process(); |
| |
| assertEquals("a_b_c_d_", _underlyingInput.getAcceptedInput()); |
| assertEquals("Unexpected capacity", sslEngineBufferSize, _sslWrapper.capacity()); |
| assertEquals("unexpected underlying process count after second wrapper process", 8 , _underlyingInput.getProcessCount()); |
| assertEquals(6, _dummySslEngine.getUnwrapCount()); // earlier + 1 decode + 1 underflow |
| } |
| |
| /** |
| * Tests that an exception is thrown when the underlying input has zero capacity when the call |
| * with newly decoded input is initially made. |
| */ |
| @Test (timeout = 5000) |
| public void testUnderlyingInputHasZeroCapacityInitially() |
| { |
| int underlyingInputBufferSize = 1; |
| int encodedPacketSize = 5; |
| |
| assertEquals("Unexpected max encoded chunk size", encodedPacketSize, CapitalisingDummySslEngine.MAX_ENCODED_CHUNK_SIZE); |
| |
| // Set the input to have a small buffer, but then return 0 from the 2nd capacity call onward. |
| _underlyingInput.setInputBufferSize(underlyingInputBufferSize); |
| _underlyingInput.setZeroCapacityAtCount(2); |
| assertEquals("Unexpected initial underlying input capacity", underlyingInputBufferSize, _underlyingInput.capacity()); |
| assertEquals("Unexpected underlying input capacity", 0, _underlyingInput.capacity()); |
| |
| // Now try decoding the input, should fail |
| byte[] bytes = "<-A->".getBytes(StandardCharsets.UTF_8); |
| ByteBuffer encodedByteSource = ByteBuffer.wrap(bytes); |
| |
| assertEquals("Unexpected initial wrapper capacity", encodedPacketSize, _sslWrapper.capacity()); |
| |
| int numberPoured = pour(encodedByteSource, _sslWrapper.tail()); |
| assertEquals("Unexpected number of bytes poured into the wrapper input buffer", encodedPacketSize, numberPoured); |
| assertEquals("Unexpected position in encoded source byte buffer", encodedPacketSize, encodedByteSource.position()); |
| assertEquals("Unexpected wrapper capacity", 0, _sslWrapper.capacity()); |
| |
| try |
| { |
| _sslWrapper.process(); |
| fail("Expected an exception"); |
| } |
| catch (TransportException te) |
| { |
| // expected. |
| } |
| |
| //Check we got no chars of decoded output. |
| assertEquals("", _underlyingInput.getAcceptedInput()); |
| assertEquals("Unexpected wrapper capacity", -1, _sslWrapper.capacity()); |
| assertEquals("unexpected underlying process count after wrapper process", 0 , _underlyingInput.getProcessCount()); |
| assertEquals("unexpected underlying capacity count after wrapper process", 3, _underlyingInput.getCapacityCount()); |
| assertEquals("unexpected underlying capacity after wrapper process", 0 , _underlyingInput.capacity()); |
| assertEquals(1, _dummySslEngine.getUnwrapCount()); // 1 decode (then exception) |
| } |
| |
| /** |
| * Tests that an exception is thrown when the underlying input has no capacity (but isn't closed) |
| * during the process of incrementally passing the decoded bytes to its smaller input buffer |
| * for processing. |
| */ |
| @Test (timeout = 5000) |
| public void testUnderlyingInputHasZeroCapacityMidProcessing() |
| { |
| int underlyingInputBufferSize = 1; |
| int encodedPacketSize = 5; |
| |
| assertEquals("Unexpected max encoded chunk size", encodedPacketSize, CapitalisingDummySslEngine.MAX_ENCODED_CHUNK_SIZE); |
| |
| // Set the input to have a small buffer, but then return 0 from the 3rd capacity call onward. |
| _underlyingInput.setInputBufferSize(underlyingInputBufferSize); |
| _underlyingInput.setZeroCapacityAtCount(3); |
| assertEquals("Unexpected initial underlying input capacity", underlyingInputBufferSize, _underlyingInput.capacity()); |
| |
| // Now try decoding the input, should fail |
| byte[] bytes = "<-A->".getBytes(StandardCharsets.UTF_8); |
| ByteBuffer encodedByteSource = ByteBuffer.wrap(bytes); |
| |
| assertEquals("Unexpected initial wrapper capacity", encodedPacketSize, _sslWrapper.capacity()); |
| |
| int numberPoured = pour(encodedByteSource, _sslWrapper.tail()); |
| assertEquals("Unexpected number of bytes poured into the wrapper input buffer", encodedPacketSize, numberPoured); |
| assertEquals("Unexpected position in encoded source byte buffer", encodedPacketSize, encodedByteSource.position()); |
| assertEquals("Unexpected wrapper capacity", 0, _sslWrapper.capacity()); |
| |
| try |
| { |
| _sslWrapper.process(); |
| fail("Expected an exception"); |
| } |
| catch (TransportException te) |
| { |
| // expected. |
| } |
| |
| //Check we got the first char (a) of decoded output, but not the second (_). |
| assertEquals("a", _underlyingInput.getAcceptedInput()); |
| assertEquals("Unexpected wrapper capacity", -1, _sslWrapper.capacity()); |
| assertEquals("unexpected underlying process count after wrapper process", 1 , _underlyingInput.getProcessCount()); |
| assertEquals("unexpected underlying capacity count after wrapper process", 3, _underlyingInput.getCapacityCount()); |
| assertEquals("unexpected underlying capacity after wrapper process", 0 , _underlyingInput.capacity()); |
| assertEquals(1, _dummySslEngine.getUnwrapCount()); // 1 decode (then exception) |
| } |
| |
| @Test |
| public void testSslUnwrapThrowsException_returnsErrorResultAndRefusesFurtherInput() throws Exception |
| { |
| String unwrapExceptionMessage = "unwrap exception message"; |
| SSLException sslException = new SSLException(unwrapExceptionMessage); |
| _dummySslEngine.rejectNextEncodedPacket(sslException); |
| |
| _sslWrapper.tail().put("<-A->".getBytes(StandardCharsets.UTF_8)); |
| try { |
| _sslWrapper.process(); |
| fail("no exception"); |
| } catch(TransportException e) { |
| assertEquals("javax.net.ssl.SSLException: " + unwrapExceptionMessage, e.getMessage()); |
| } |
| |
| assertEquals(Transport.END_OF_STREAM, _sslWrapper.capacity()); |
| } |
| |
| @Test |
| public void testUnderlyingInputReturnsErrorResult_returnsErrorResultAndRefusesFurtherInput() throws Exception |
| { |
| String underlyingErrorDescription = "dummy underlying error"; |
| _underlyingInput.rejectNextInput(underlyingErrorDescription); |
| |
| _sslWrapper.tail().put("<-A->".getBytes(StandardCharsets.UTF_8)); |
| |
| try { |
| _sslWrapper.process(); |
| fail("no exception"); |
| } catch (TransportException e) { |
| assertEquals(underlyingErrorDescription, e.getMessage()); |
| } |
| } |
| |
| @Test |
| public void testHeadIsReadOnly() |
| { |
| _underlyingOutput.setOutput(""); |
| assertTrue(_sslWrapper.head().isReadOnly()); |
| } |
| |
| @Test |
| public void testOutputEncodesOnePacket() |
| { |
| _underlyingOutput.setOutput("a_"); |
| |
| ByteBuffer outputBuffer = _sslWrapper.head(); |
| |
| assertByteBufferContentEquals("<-A->".getBytes(StandardCharsets.UTF_8), outputBuffer); |
| } |
| |
| @Test |
| public void testOutputEncodesMultiplePackets() |
| { |
| _underlyingOutput.setOutput("a_b_c_"); |
| |
| assertEquals("<-A-><-B-><-C->", getAllBytesFromTransport()); |
| } |
| |
| @Test |
| public void testOutputEncodesMultiplePacketsOfVaryingSize() |
| { |
| _underlyingOutput.setOutput("z_a_b_"); |
| |
| assertEquals("<><-A-><-B->", getAllBytesFromTransport()); |
| } |
| |
| @Test |
| public void testClientConsumesEncodedOutputInMultipleChunks() |
| { |
| _underlyingOutput.setOutput("a_b_"); |
| |
| { |
| ByteBuffer buffer = _sslWrapper.head(); |
| String output = pourBufferToString(buffer, 2); |
| assertEquals("<-", output); |
| _sslWrapper.pop(buffer.position()); |
| } |
| |
| { |
| ByteBuffer buffer = _sslWrapper.head(); |
| String output = pourBufferToString(buffer, 3); |
| assertEquals("A->", output); |
| _sslWrapper.pop(buffer.position()); |
| } |
| |
| assertEquals("<-B->", getAllBytesFromTransport()); |
| } |
| |
| @Test |
| public void testNoOutputToEncode() |
| { |
| _underlyingOutput.setOutput(""); |
| |
| assertFalse(_sslWrapper.head().hasRemaining()); |
| } |
| |
| private void putBytesIntoTransport(String encodedBytes) |
| { |
| ByteBuffer byteBuffer = ByteBuffer.wrap(encodedBytes.getBytes(StandardCharsets.UTF_8)); |
| while(byteBuffer.hasRemaining()) |
| { |
| int numberPoured = pour(byteBuffer, _sslWrapper.tail()); |
| assertTrue("We should be able to pour some bytes into the input buffer", |
| numberPoured > 0); |
| _sslWrapper.process(); |
| } |
| } |
| |
| private String getAllBytesFromTransport() |
| { |
| StringBuilder readBytes = new StringBuilder(); |
| while (true) |
| { |
| int pending = _sslWrapper.pending(); |
| if (pending > 0) { |
| ByteBuffer buffer = _sslWrapper.head(); |
| readBytes.append(pourBufferToString(buffer)); |
| _sslWrapper.pop(pending); |
| continue; |
| } else { |
| break; |
| } |
| } |
| |
| return readBytes.toString(); |
| } |
| |
| } |