SSLEngineMock.java

/*
 * JBoss, Home of Professional Open Source.
 *
 * Copyright 2012 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.mock;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;

import org.jboss.logging.Logger;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.xnio.Buffers;

/**
 * Mocks an SSLEngine for test purposes.<p>
 * The handshaking behavior of the mock is defined by a sequence of {@link HandshakeAction}s. On every request to wrap
 * or unwrap, this mock will take one of the predetermined actions, following the order those actions are {@link
 * #setHandshakeActions(HandshakeAction...) defined}. If an action cannot be performed at current wrap/unwrap request
 * (for example, take the case of a {@link HandshakeAction#NEED_WRAP NEED_WRAP} on an unwrap request), the resulting
 * {@code SSLEngineResult} will point that out and the action will be postponed to the next wrap/unwrap invocation,
 * until it can be executed. Only then, this mock will move on to the next handshake action.
 * <p>
 * Once this mock reaches the end of the handshake action list, it will no longer perform any handshaking, and will
 * always fulfill any wrap/unwrap request without failures. If this mock ever unwraps a {@link CLOSE_MSG}, it will
 * close itself and indicate in the future {@code SSLEngineResult}s it needs to wrap a {@link CLOSE_MSG} until requested
 * to wrap.
 * <p>
 * Invoking {@link #closeInbound} or {@link #closeOutbound} will also have the effect of closing this engine for both
 * wrap and unwrap. Notice that after {@code closeOutbound} is invoked, this engine will request to unwrap until it
 * unwraps a {@link CLOSE_MSG}, unless the engine has already done so.
 * <p>
 * Once closed, no handshake action is taken after this engine is closed, regardless of whether it has executed all 
 * handshake actions or not. The only exception to this rule is that the engine can request to wrap, or unwrap a {@link
 * CLOSE_MSG} when that happens.
 * <p>To mimic wrap and unwrap, this mock uses a wrap/unwrap register. This register works like a map, containing what
 * is the wrapped equivalent of an unwrapped message and vice-versa. To use this register, you just have to call {@link
 * #addWrapEntry(String, String)} for every unwrapped/wrapped pair of messages you want to add. That way, when requested
 * to wrap a message this mock will search for the unwrapped message in its register. If the register contains the
 * message to be wrapped, this mock will use the wrapped version of it to write that message; if not, the engine will
 * simply copy the message. Likewise, if a message needs to be unwrapped, this mock will check if this message has a
 * corresponding wrapped version in the register. If it does, it will use the wrapped message. If not, it will simply
 * copy the message when wrapping. Each wrap entry can be defined by calling {@link #addWrapEntry(String, String)}.
 * 
 * @author <a href="mailto:flavia.rainone@jboss.com">Flavia Rainone</a>
 *
 */
public class SSLEngineMock extends SSLEngine {
    private static final Logger log = Logger.getLogger("TEST");

    // every wrapped handshake message generated by this engine is the result of wrapping HANDSHAKE_MSG
    public static final String HANDSHAKE_MSG = "[handshake data]";
    // every wrapped close message generated by this engine is the resujlt of wrapping CLOSE_MSG
    public static final String CLOSE_MSG = "[close]";
    // mockery, to generate task mocks
    protected final Mockery mockery;
    // marks the index of current action
    private int actionIndex;
    // the sequence of handshake actions
    private HandshakeAction[] actions;
    // mapped wrapper, the unwrap/wrap register, performs wrap and unwrap
    private final MappedWrapper wrapper;
    // indicates if this engine is closed
    private boolean closed = false;
    // indicates if this engine has sent a wrapped CLOSE_MSG
    private boolean closeMessageWrapped = false;
    // indicates if this engine has unwrapped a CLOSE_MSG
    private boolean closeMessageUnwrapped = false;
    // supported cipher suites
    private String[] supportedCipherSuites = new String[0];
    // supported protocols
    private String[] supportedProtocols = new String[0];
    // enabled cipher suites
    private String[] enabledCipherSuites = null;
    // enabled protocols
    private String[] enabledProtocols = null;
    // indicates whether session creation is enabled
    private boolean enableSessionCreation = false;
    // client mode enabled
    private boolean useClientMode = false;
    // need client auth
    private boolean needClientAuth = false;
    // want client auth
    private boolean wantClientAuth = false;

    /**
     * Determines the handshake action this mock should take when requested to wrap/unwrap a message
     */
    public static enum HandshakeAction {
        /**
         * Will not proceed until client requests this engine to wrap data.
         * The generated wrapped data will be {@link SSLEngineMock#HANDSHAKE_MSG}. 
         */
        NEED_WRAP,
        /**
         * Will not proceed until client requests this engine to unwrap non-empty data.
         * This engine expects to read {@link SSLEngineMock#HANDSHAKE_MSG}.
         */
        NEED_UNWRAP,
        /**
         * Will not perform any related handshake action when requested to wrap/unwrap data.
         */
        PERFORM_REQUESTED_ACTION,
        /**
         * A task is needed to be executed so handshake can proceed.
         */
        NEED_TASK,
        /**
         * A task is needed to be executed so handshake can proceed. This mock will provide a faulty task,
         * one that will throw an exception when requested to execute.
         * TODO this action is not supported right now
         */
        NEED_FAULTY_TASK,
        /**
         * Engine will finish the handshake.
         */
        FINISH};

    /**
     * Constructor.
     * 
     * @param mockery mockery to create mocks for internal use
     */
    public SSLEngineMock(Mockery mockery) {
        this.mockery = mockery;
        this.wrapper = new MappedWrapper();
        this.actions = new HandshakeAction[0];
        this.actionIndex = 0;
    }

    /**
     * Sets the handshake actions this engine will take when requested to wrap or unwrap a message.
     * <p>
     * Once all requested actions have been successfully performed, this mock will simply perform any requested action
     * without any handshaking, which is equivalent to executing a
     * {@link HandshakeAction#PERFORM_REQUESTED_ACTION PERFORM_REQUESTED_ACTION}.
     * 
     * @param actions the actions that define the handshake behavior of this mock when it receives a request to unwrap
     *        or wrap new data. 
     */
    public void setHandshakeActions(HandshakeAction... actions) {
        this.actions = actions;
    }

    /**
     * A wrap entry is a pair of texts representing an unwrapped data and its wrapped equivalent.
     * Once this wrap entry is added to this mock, this mock will start writing {@code wrappedData} whenever it is
     * requested to wrap {@code unwrappedData}, and it will write {@code unwrappedData} whenever requested to unwrap
     * {@code wrappedData}.
     * 
     * @param unwrappedData    unwrapped data
     * @param wrappedData      the wrapped equivalent of {@code unwrappedData}
     */
    public void addWrapEntry(String unwrappedData, String wrappedData) {
        wrapper.put(unwrappedData, wrappedData);
    }

    /**
     * Does nothing.
     */
    @Override
    public void beginHandshake() throws SSLException {
        // do nothing
    }

    /**
     * Marks this engine as closed.
     */
    @Override
    public synchronized void closeInbound() throws SSLException {
        closed = true;
    }

    /**
     * Marks this engine as closed.
     */
    @Override
    public synchronized void closeOutbound() {
        closed = true;
    }

    // count used to differentiate delegated tasks (a requirement of JMock)
    private int taskCount = 0;

    /**
     * Returns:<ul>
     * <li>{@code null} if this engine is marked as closed, or if current action is not one of
     * {@link HandshakeAction#NEED_TASK NEED_TASK}, {@link HandshakeAction#NEED_FAULTY_TASK NEED_FAULTY_TASK}.
     * <li> a task mock if current action is {@link HandshakeAction#NEED_TASK NEED_TASK}. The mock expects to have its method
     * {@code run()} invoked exactly once by client
     * <li> a task mock whose {@code run()} method will throw an exception, if current action is {@link
     *      HandshakeAction#NEED_FAULTY_TASK NEED_FAULTY_TASK}. The mock expects to have its method {@code run()} invoked exactly once.
     * </ul>
     * 
     */
    @SuppressWarnings("incomplete-switch")
    @Override
    public synchronized Runnable getDelegatedTask() {
        if (!closed && actionIndex < actions.length) {
            switch (actions[actionIndex]) {
                case NEED_TASK: {
                    actionIndex ++;
                    synchronized (mockery){ 
                        final Runnable task = mockery.mock(Runnable.class, "RunnableMock" + taskCount++);
                        mockery.checking(new Expectations() {{
                            oneOf(task).run();
                        }});
                        return task;
                    }
                }
                case NEED_FAULTY_TASK:
                    synchronized (mockery){ 
                        final Runnable task = mockery.mock(Runnable.class, "RunnableFaultyMock" + taskCount++);
                        mockery.checking(new Expectations() {{
                            oneOf(task).run();
                            will(throwException(new RuntimeException()));
                        }});
                        return task;
                    }
            }
        }
        return null;
    }

    private int sessionCount = 0;

    @Override
    public SSLSession getSession() {
        synchronized (mockery) {
            final SSLSession sessionMock = mockery.mock(SSLSession.class, "Session" + sessionCount ++);
            if (sessionCount == 1) {
                mockery.checking(new Expectations() {{
                    oneOf(sessionMock).getPacketBufferSize();
                    will(returnValue(16916));
                    oneOf(sessionMock).getApplicationBufferSize();
                    will(returnValue(16921));
                }});
            }
            else {
                mockery.checking(new Expectations() {{
                    allowing(sessionMock).getPacketBufferSize();
                    will(returnValue(16916));
                    allowing(sessionMock).getApplicationBufferSize();
                    will(returnValue(16921));
                }});
            }
            return sessionMock;
        }
    }

    // this method avoids duplicate actionIndex increments for the same action
    private void actionAccountedFor(HandshakeAction action, int index) {
        synchronized(this) {
            if (actionIndex >= actions.length || closed) {
                return;
            }
            if (actionIndex == index && actions[actionIndex] == action) {
                actionIndex ++;
            }
        }
    }

    @Override
    public synchronized HandshakeStatus getHandshakeStatus() {
        final int currentIndex;
        synchronized (this) {
            if (closed) {
                if (!closeMessageWrapped) {
                    return HandshakeStatus.NEED_WRAP;
                }
                if (!closeMessageUnwrapped) {
                    return HandshakeStatus.NEED_UNWRAP;
                }
            }
            if (closed || actionIndex > actions.length) {
                return HandshakeStatus.NOT_HANDSHAKING;
            }
            currentIndex = actionIndex;
        }
        if(currentIndex >= actions.length) {
            return HandshakeStatus.NOT_HANDSHAKING;
        }
        log.debug("Current engine mock action: " + actions[currentIndex]);
        switch(actions[currentIndex]) {
            case NEED_TASK:
            case NEED_FAULTY_TASK:
                return HandshakeStatus.NEED_TASK;
            case NEED_WRAP:
                return HandshakeStatus.NEED_WRAP;
            case NEED_UNWRAP:
                return HandshakeStatus.NEED_UNWRAP;
            case PERFORM_REQUESTED_ACTION:
                return HandshakeStatus.NOT_HANDSHAKING;
            case FINISH:
                return HandshakeStatus.FINISHED;
            default:
                throw new IllegalStateException("Unexpected handshake action: " + actions[currentIndex]);
        }
    }

    private SSLEngineResult logOperation(String operation, SSLEngineResult result) {
        log.debugf("SSLEngineMock.%s returned [%s, %s, consumed:%d, produced:%d]", operation, result.getStatus(),
                result.getHandshakeStatus(), result.bytesConsumed(), result.bytesProduced());
        return result;
    }

    private SSLEngineResult logUnwrap(SSLEngineResult result) {
        return logOperation("unwrap", result);
    }

    private SSLEngineResult logWrap(SSLEngineResult result) {
        return logOperation("wrap", result);
    }

    @Override
    public synchronized SSLEngineResult unwrap(ByteBuffer src, ByteBuffer[] dsts, int offset, int length) throws SSLException {
        for (int i = offset; i < length; i++) {
            if (dsts[i].hasRemaining()) {
                break;
            }
            else if (i == length -1) {
                return logUnwrap(new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0));
            }
        }
        if (closed && closeMessageUnwrapped) {
            return logUnwrap(new SSLEngineResult(Status.CLOSED, HandshakeStatus.NOT_HANDSHAKING, 0, 0));
        }
        final HandshakeStatus status;
        final int currentActionIndex;
        synchronized (this) {
            status = getHandshakeStatus();
            currentActionIndex = actionIndex;
        }
        final SSLEngineResult result;
        switch(status) {
            case FINISHED:
                actionAccountedFor(HandshakeAction.FINISH, currentActionIndex);
                result = new SSLEngineResult(Status.OK, HandshakeStatus.FINISHED, 30, 0);
                break;
            case NEED_TASK:
                result = new SSLEngineResult(Status.OK, HandshakeStatus.NEED_TASK, 0, 0);
                break;
            case NEED_UNWRAP:
                result = wrapper.unwrap(dsts, offset, length, src, true, currentActionIndex);
                break;
            case NEED_WRAP:
                if (closed) {
                    result = new SSLEngineResult(Status.CLOSED, SSLEngineResult.HandshakeStatus.NEED_WRAP, 0, 0);
                } else {
                    result =  new SSLEngineResult(Status.OK, SSLEngineResult.HandshakeStatus.NEED_WRAP, 0, 0);
                }
                break;
            case NOT_HANDSHAKING: 
                actionAccountedFor(HandshakeAction.PERFORM_REQUESTED_ACTION, currentActionIndex);
                result = wrapper.unwrap(dsts, offset, length, src, false, currentActionIndex);
                break;
            default:
                throw new IllegalStateException("Unexpected handshake status: " + getHandshakeStatus());
        }
        logUnwrap(result);
        return result;
    }

    @Override
    public synchronized SSLEngineResult wrap(ByteBuffer[] srcs, int offset, int length, ByteBuffer dst) throws SSLException {
        if (dst.position() > 0) {
            return logWrap(new SSLEngineResult(SSLEngineResult.Status.BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0));
        }
        final HandshakeStatus status;
        final int currentActionIndex;
        synchronized (this) {
            status = getHandshakeStatus();
            currentActionIndex = actionIndex;
        }
        final SSLEngineResult result;
        switch(status) {
            case FINISHED: {
                actionAccountedFor(HandshakeAction.FINISH, currentActionIndex);
                result = new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.FINISHED, 0, 0);
                break;
            }
            case NEED_TASK:
                result = new SSLEngineResult(SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NEED_TASK, 0, 0);
                break;
            case NEED_UNWRAP:
                result = new SSLEngineResult(closed? SSLEngineResult.Status.CLOSED: SSLEngineResult.Status.OK, SSLEngineResult.HandshakeStatus.NEED_UNWRAP, 0, 0);
                break;
            case NEED_WRAP:
                result = wrapper.wrap(dst, srcs, offset, length, true, currentActionIndex);
                break;
            case NOT_HANDSHAKING: {
                if (closed) {
                    return new SSLEngineResult(SSLEngineResult.Status.CLOSED, SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING, 0, 0);
                }
                actionAccountedFor(HandshakeAction.PERFORM_REQUESTED_ACTION, currentActionIndex);
                result = wrapper.wrap(dst, srcs, offset, length, false, currentActionIndex);
                break;
            }
            default:
                throw new IllegalStateException("Unexpected handshake status: " + getHandshakeStatus());
        }
        logWrap(result);
        return result;
    }

    private final class MappedWrapper {

        private final Map<String, String> wrapMap = new HashMap<String, String>();
        private final Map<String, String> unwrapMap = new HashMap<String, String>();

        public void put(String unwrapped, String wrapped) {
            wrapMap.put(unwrapped, wrapped);
            // avoid use of wrapped text as it if it were wrapped
            if (!wrapMap.containsKey(wrapped)) {
                wrapMap.put(wrapped, "");
            }
            unwrapMap.put(wrapped, unwrapped);
            // avoid use of unwrapped text as if it were wrapped on read
            if (!unwrapMap.containsKey(unwrapped)) {
                unwrapMap.put(unwrapped, "");
            }
        }

        public SSLEngineResult unwrap(ByteBuffer[] dsts, int offset, int length, ByteBuffer src, boolean needUnwrap,int actionIndex) {
            if (!src.hasRemaining()) {
                return new SSLEngineResult(closed && closeMessageUnwrapped? Status.CLOSED: Status.BUFFER_UNDERFLOW, needUnwrap? HandshakeStatus.NEED_UNWRAP: HandshakeStatus.NOT_HANDSHAKING, 0, 0);
            }
            Status okStatus = closed && closeMessageUnwrapped? Status.CLOSED: Status.OK;
            // amount of bytes available at src
            int initialSrcRemaining = src.remaining();
            int bytesProduced = 0;
            while (src.hasRemaining()) {
                String unwrapped = unwrapBytes(dsts, offset, length, src, needUnwrap);
                int bytesConsumed = initialSrcRemaining - src.remaining();
                if (unwrapped == null) {
                    if (bytesProduced == 0) {
                        return new SSLEngineResult(Status.BUFFER_OVERFLOW, HandshakeStatus.NEED_UNWRAP, 0, 0);
                    }
                    return new SSLEngineResult(okStatus, HandshakeStatus.NEED_UNWRAP, bytesConsumed, bytesProduced);
                }
                // it means handshake message was found when it is not expected, ignore it and return everything that
                // was read up until now
                if (unwrapped.length() == 0) {
                    if (bytesConsumed > 0) {
                        actionAccountedFor(HandshakeAction.PERFORM_REQUESTED_ACTION, actionIndex);
                    }
                    return new SSLEngineResult(okStatus, getHandshakeStatus(), bytesConsumed, bytesProduced);
                }
                // it means we are on a needUnwrap and we found the handshake message we are seeking
                if (unwrapped.equals(HANDSHAKE_MSG)) {
                    actionAccountedFor(HandshakeAction.NEED_UNWRAP, actionIndex);
                    // move to next action, NEED_UNWRAP action is finally accomplished
                    return new SSLEngineResult(okStatus, getHandshakeStatus(), bytesConsumed, bytesProduced);
                }
                if (unwrapped.equals(CLOSE_MSG)) {
                    closed = true;
                    closeMessageUnwrapped = true;
                    return new SSLEngineResult(Status.CLOSED, getHandshakeStatus(), bytesConsumed, bytesProduced);
                }
                bytesProduced += unwrapped.length();
            }
            int bytesConsumed = initialSrcRemaining - src.remaining();
            if (bytesConsumed > 0 && !needUnwrap) {
                actionAccountedFor(HandshakeAction.PERFORM_REQUESTED_ACTION, actionIndex);
            }
            return new SSLEngineResult(okStatus, getHandshakeStatus(), bytesConsumed, bytesProduced);
        }

        private String unwrapBytes(ByteBuffer[] dsts, int offset, int length, ByteBuffer src, boolean readHandshakeMsg) {
            // define unwrapped data to be written to dsts
            String wrapped = new String(Buffers.take(src), StandardCharsets.ISO_8859_1);
            int wrappedEndIndex = wrapped.length();
            int wrappedLeftOver = -1;
            while (wrappedEndIndex > 0 && !unwrapMap.containsKey(wrapped.substring(0, wrappedEndIndex))) {
                wrappedLeftOver = wrappedEndIndex --;
            }
            // undo the reading of data that won't be used now
            if (wrappedLeftOver != -1 && wrappedEndIndex > 0) {
                src.position(src.position() - (wrapped.length() - wrappedEndIndex));
                wrapped = wrapped.substring(0, wrappedEndIndex);
            } else {
                int msgIndex;
                if ((msgIndex = wrapped.indexOf(HANDSHAKE_MSG)) != -1) {
                    if (msgIndex == 0) {
                        src.position(src.position() - (wrapped.length() - HANDSHAKE_MSG.length()));
                        wrapped = wrapped.substring(0, HANDSHAKE_MSG.length());
                    }
                    else {
                        src.position(src.position() - (wrapped.length() - msgIndex));
                        wrapped = wrapped.substring(0, msgIndex);
                    }
                }
                if ((msgIndex = wrapped.indexOf(CLOSE_MSG)) != -1) {
                    if (msgIndex == 0) {
                        src.position(src.position() - (wrapped.length() - CLOSE_MSG.length()));
                        wrapped = wrapped.substring(0, CLOSE_MSG.length());
                    }
                    else {
                        src.position(src.position() - (wrapped.length() - msgIndex));
                        wrapped = wrapped.substring(0, msgIndex);
                    }
                }
            }
            String unwrapped = unwrapMap.containsKey(wrapped)? unwrapMap.get(wrapped): wrapped;
            if (unwrapped.equals(HANDSHAKE_MSG) && !readHandshakeMsg) {
                src.position(src.position() - wrapped.length());
                return "";
            }
            if (!unwrapped.equals(CLOSE_MSG) && !unwrapped.equals(HANDSHAKE_MSG)) {
                if (CLOSE_MSG.startsWith(unwrapped) || HANDSHAKE_MSG.startsWith(unwrapped)) {
                    src.position(0);
                    return null;
                }
                // check if there is enough space to write unwrapped data, if not, do not write
                if (Buffers.remaining(dsts, offset, length) < unwrapped.length()) {
                    src.position(src.position() - wrapped.length());
                    return null;
                }
                // copy data to dsts
                Buffers.copy(dsts, offset, length, ByteBuffer.wrap(unwrapped.getBytes(StandardCharsets.ISO_8859_1)));
            }
            return unwrapped;
        }

        public SSLEngineResult wrap(ByteBuffer dst, ByteBuffer[] srcs, int offset, int length, boolean needWrap, int actionIndex) {
            if (needWrap) {
                actionAccountedFor(HandshakeAction.NEED_WRAP, actionIndex);
                // a valid needWrapActionIndex indicates that we musts wrap a handshake message
                if (closed) {
                    synchronized(SSLEngineMock.this) {
                        closeMessageWrapped = true;
                    }
                    return wrapMessage(dst, CLOSE_MSG, Status.CLOSED);
                }
                return wrapMessage(dst, HANDSHAKE_MSG, Status.OK);
            }
            int dstInitialRemaining = dst.remaining();
            int bytesConsumed = wrapBytes(dst, srcs, offset, length);
            int bytesProduced = dstInitialRemaining - dst.remaining();
            if (bytesConsumed > 0) {
                actionAccountedFor(HandshakeAction.PERFORM_REQUESTED_ACTION, actionIndex);
            }
            if (closed) {
                return new SSLEngineResult(Status.CLOSED, HandshakeStatus.NOT_HANDSHAKING, bytesConsumed, bytesProduced);
            }
            if (bytesConsumed == 0) {
                for (int i = offset; i < length; i++) {
                    if (srcs[i].hasRemaining()) {
                        return new SSLEngineResult(Status.BUFFER_OVERFLOW, HandshakeStatus.NOT_HANDSHAKING, bytesConsumed, bytesProduced);
                    }
                }
            }
            return new SSLEngineResult(Status.OK, HandshakeStatus.NOT_HANDSHAKING, bytesConsumed, bytesProduced);
        }

        private SSLEngineResult wrapMessage(ByteBuffer dst, String msg, Status okayStatus) {
            String wrappedMessage = wrapMap.containsKey(msg)? wrapMap.get(msg): msg;
            if (dst.remaining() < wrappedMessage.length()) {
                return new SSLEngineResult(Status.BUFFER_OVERFLOW, getHandshakeStatus(), 0, 0);
            }
            Buffers.putModifiedUtf8(dst, wrappedMessage);
            return new SSLEngineResult(okayStatus, getHandshakeStatus(), 0, wrappedMessage.length());
        }

        public int wrapBytes(ByteBuffer dst, ByteBuffer[] srcs, int offset, int length) {
            int totalLength = 0;
            int srcsLength = offset + length;
            for (int i = offset; i < srcsLength && dst.hasRemaining(); i++) {
                StringBuilder unwrappedBuilder = new StringBuilder();
                Buffers.readModifiedUtf8Line(srcs[i], unwrappedBuilder);
                int wrappedLength = wrapBytes(dst, unwrappedBuilder.toString());
                if (wrappedLength == 0 && unwrappedBuilder.length() > 0) {
                    srcs[i].position(srcs[i].position() - unwrappedBuilder.length());
                    break;
                }
                totalLength += wrappedLength;
            }
            return totalLength;
        }

        public int wrapBytes(ByteBuffer dst, String src) {
            String wrapped = wrapMap.containsKey(src)? wrapMap.get(src): src;
            if (dst.remaining() < wrapped.length()) {
                return 0;
            }
            Buffers.putModifiedUtf8(dst, wrapped);
            return src.length();
        }
    }

    @Override
    public boolean getEnableSessionCreation() {
        return enableSessionCreation;
    }

    @Override
    public String[] getEnabledCipherSuites() {
        return enabledCipherSuites;
    }

    @Override
    public String[] getEnabledProtocols() {
        return enabledProtocols;
    }

    @Override
    public boolean getNeedClientAuth() {
        return needClientAuth;
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return supportedCipherSuites;
    }

    public void setSupportedCipherSuites(String... supportedCipherSuites) {
        this.supportedCipherSuites = supportedCipherSuites;
    }

    @Override
    public String[] getSupportedProtocols() {
        return supportedProtocols;
    }

    public void setSupportedProtocols(String... supportedProtocols) {
        this.supportedProtocols = supportedProtocols;
    }

    @Override
    public boolean getUseClientMode() {
        return useClientMode;
    }

    @Override
    public boolean getWantClientAuth() {
        return wantClientAuth;
    }

    @Override
    public boolean isInboundDone() {
        return false;
    }

    @Override
    public boolean isOutboundDone() {
        return closeMessageWrapped;
    }

    @Override
    public void setEnableSessionCreation(boolean flag) {
        enableSessionCreation = flag;
    }

    @Override
    public void setEnabledCipherSuites(String[] suites) {
        enabledCipherSuites = suites;
    }

    @Override
    public void setEnabledProtocols(String[] protocols) {
        enabledProtocols = protocols;
    }

    @Override
    public void setNeedClientAuth(boolean need) {
        needClientAuth = need;
    }

    @Override
    public void setUseClientMode(boolean mode) {
        useClientMode = mode;
    }

    @Override
    public void setWantClientAuth(boolean want) {
        wantClientAuth = want;
    }
}