/* jcifs smb client library in Java
 * Copyright (C) 2005  "Michael B. Allen" <jcifs at samba dot org>
 *                  "Eric Glass" <jcifs at samba dot org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package jcifs.smb;


import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

import javax.crypto.Cipher;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jcifs.Address;
import jcifs.CIFSContext;
import jcifs.CIFSException;
import jcifs.DfsReferralData;
import jcifs.DialectVersion;
import jcifs.SmbConstants;
import jcifs.SmbTransport;
import jcifs.internal.CommonServerMessageBlock;
import jcifs.internal.CommonServerMessageBlockRequest;
import jcifs.internal.CommonServerMessageBlockResponse;
import jcifs.internal.RequestWithPath;
import jcifs.internal.SMBProtocolDecodingException;
import jcifs.internal.SMBSigningDigest;
import jcifs.internal.SmbNegotiation;
import jcifs.internal.SmbNegotiationResponse;
import jcifs.internal.dfs.DfsReferralDataImpl;
import jcifs.internal.dfs.DfsReferralRequestBuffer;
import jcifs.internal.dfs.DfsReferralResponseBuffer;
import jcifs.internal.dfs.Referral;
import jcifs.internal.smb1.AndXServerMessageBlock;
import jcifs.internal.smb1.ServerMessageBlock;
import jcifs.internal.smb1.com.SmbComBlankResponse;
import jcifs.internal.smb1.com.SmbComLockingAndX;
import jcifs.internal.smb1.com.SmbComNegotiate;
import jcifs.internal.smb1.com.SmbComNegotiateResponse;
import jcifs.internal.smb1.com.SmbComReadAndXResponse;
import jcifs.internal.smb1.trans.SmbComTransaction;
import jcifs.internal.smb1.trans.SmbComTransactionResponse;
import jcifs.internal.smb1.trans2.Trans2GetDfsReferral;
import jcifs.internal.smb1.trans2.Trans2GetDfsReferralResponse;
import jcifs.internal.smb2.ServerMessageBlock2;
import jcifs.internal.smb2.ServerMessageBlock2Request;
import jcifs.internal.smb2.ServerMessageBlock2Response;
import jcifs.internal.smb2.Smb2Constants;
import jcifs.internal.smb2.io.Smb2ReadResponse;
import jcifs.internal.smb2.ioctl.Smb2IoctlRequest;
import jcifs.internal.smb2.ioctl.Smb2IoctlResponse;
import jcifs.internal.smb2.lock.Smb2OplockBreakNotification;
import jcifs.internal.smb2.nego.EncryptionNegotiateContext;
import jcifs.internal.smb2.nego.Smb2NegotiateRequest;
import jcifs.internal.smb2.nego.Smb2NegotiateResponse;
import jcifs.netbios.Name;
import jcifs.netbios.NbtException;
import jcifs.netbios.SessionRequestPacket;
import jcifs.netbios.SessionServicePacket;
import jcifs.util.Crypto;
import jcifs.util.Encdec;
import jcifs.util.Hexdump;
import jcifs.util.transport.Request;
import jcifs.util.transport.Response;
import jcifs.util.transport.Transport;
import jcifs.util.transport.TransportException;


/**
 *
 */
class SmbTransportImpl extends Transport implements SmbTransportInternal, SmbConstants {

    private static Logger log = LoggerFactory.getLogger(SmbTransportImpl.class);

    private boolean smb2 = false;
    private InetAddress localAddr;
    private int localPort;
    private Address address;
    private Socket socket;
    private int port;
    private final AtomicLong mid = new AtomicLong();
    private OutputStream out;
    private InputStream in;
    private final byte[] sbuf = new byte[1024]; /* small local buffer */
    private long sessionExpiration;
    private final List<SmbSessionImpl> sessions = new LinkedList<>();

    private String tconHostName = null;

    private final CIFSContext transportContext;
    private final boolean signingEnforced;

    private SmbNegotiationResponse negotiated;

    private SMBSigningDigest digest;

    private final Semaphore credits = new Semaphore(1, true);

    private final int desiredCredits = 512;

    private byte[] preauthIntegrityHash = new byte[64];


    SmbTransportImpl ( CIFSContext tc, Address address, int port, InetAddress localAddr, int localPort, boolean forceSigning ) {
        this.transportContext = tc;

        this.signingEnforced = forceSigning || this.getContext().getConfig().isSigningEnforced();
        this.sessionExpiration = System.currentTimeMillis() + tc.getConfig().getSessionTimeout();

        this.address = address;
        this.port = port;
        this.localAddr = localAddr;
        this.localPort = localPort;

    }


    /**
     * {@inheritDoc}
     *
     * @see jcifs.util.transport.Transport#getResponseTimeout()
     */
    @Override
    protected int getResponseTimeout ( Request req ) {
        if ( req instanceof CommonServerMessageBlockRequest ) {
            Integer overrideTimeout = ( (CommonServerMessageBlockRequest) req ).getOverrideTimeout();
            if ( overrideTimeout != null ) {
                return overrideTimeout;
            }
        }
        return getContext().getConfig().getResponseTimeout();
    }


    @Override
    public Address getRemoteAddress () {
        return this.address;
    }


    @Override
    public String getRemoteHostName () {
        return this.tconHostName;
    }


    /**
     *
     * @return number of sessions on this transport
     */
    public int getNumSessions () {
        return this.sessions.size();
    }


    @Override
    public int getInflightRequests () {
        return this.response_map.size();
    }


    @Override
    public boolean isDisconnected () {
        Socket s = this.socket;
        return super.isDisconnected() || s == null || s.isClosed();
    }


    @Override
    public boolean isFailed () {
        Socket s = this.socket;
        return super.isFailed() || s == null || s.isClosed();
    }


    @Override
    public boolean hasCapability ( int cap ) throws SmbException {
        return getNegotiateResponse().haveCapabilitiy(cap);
    }


    /**
     * @return the negotiated
     * @throws SmbException
     */
    SmbNegotiationResponse getNegotiateResponse () throws SmbException {
        try {
            if ( this.negotiated == null ) {
                connect(this.transportContext.getConfig().getResponseTimeout());
            }
        }
        catch ( IOException ioe ) {
            throw new SmbException(ioe.getMessage(), ioe);
        }
        SmbNegotiationResponse r = this.negotiated;
        if ( r == null ) {
            throw new SmbException("Connection did not complete, failed to get negotiation response");
        }
        return r;
    }


    /**
     * @return whether this is SMB2 transport
     * @throws SmbException
     */
    @Override
    public boolean isSMB2 () throws SmbException {
        return this.smb2 || getNegotiateResponse() instanceof Smb2NegotiateResponse;
    }


    /**
     * @param digest
     */
    public void setDigest ( SMBSigningDigest digest ) {
        this.digest = digest;
    }


    /**
     * @return the digest
     */
    public SMBSigningDigest getDigest () {
        return this.digest;
    }


    /**
     * @return the context associated with this transport connection
     */
    @Override
    public CIFSContext getContext () {
        return this.transportContext;
    }


    /**
     * {@inheritDoc}
     *
     * @see jcifs.util.transport.Transport#acquire()
     */
    @Override
    public SmbTransportImpl acquire () {
        return (SmbTransportImpl) super.acquire();
    }


    /**
     * @return the server's encryption key
     */
    @Override
    public byte[] getServerEncryptionKey () {
        if ( this.negotiated == null ) {
            return null;
        }

        if ( this.negotiated instanceof SmbComNegotiateResponse ) {
            return ( (SmbComNegotiateResponse) this.negotiated ).getServerData().encryptionKey;
        }
        return null;
    }


    @Override
    public boolean isSigningOptional () throws SmbException {
        if ( this.signingEnforced ) {
            return false;
        }
        SmbNegotiationResponse nego = getNegotiateResponse();
        return nego.isSigningNegotiated() && !nego.isSigningRequired();
    }


    @Override
    public boolean isSigningEnforced () throws SmbException {
        if ( this.signingEnforced ) {
            return true;
        }
        return getNegotiateResponse().isSigningRequired();
    }


    /**
     * {@inheritDoc}
     *
     * @see jcifs.SmbTransport#unwrap(java.lang.Class)
     */
    @SuppressWarnings ( "unchecked" )
    @Override
    public <T extends SmbTransport> T unwrap ( Class<T> type ) {
        if ( type.isAssignableFrom(this.getClass()) ) {
            return (T) this;
        }
        throw new ClassCastException();
    }


    /**
     *
     * @param tf
     * @return a session for the context
     */
    @Override
    public SmbSessionImpl getSmbSession ( CIFSContext tf ) {
        return getSmbSession(tf, null, null);
    }


    /**
     *
     * @param tf
     *            context to use
     * @return a session for the context
     */
    @Override
    @SuppressWarnings ( "resource" )
    public synchronized SmbSessionImpl getSmbSession ( CIFSContext tf, String targetHost, String targetDomain ) {
        long now;

        if ( log.isTraceEnabled() ) {
            log.trace("Currently " + this.sessions.size() + " session(s) active for " + this);
        }

        if ( targetHost != null ) {
            targetHost = targetHost.toLowerCase(Locale.ROOT);
        }

        if ( targetDomain != null ) {
            targetDomain = targetDomain.toUpperCase(Locale.ROOT);
        }

        ListIterator<SmbSessionImpl> iter = this.sessions.listIterator();
        while ( iter.hasNext() ) {
            SmbSessionImpl ssn = iter.next();
            if ( ssn.matches(tf, targetHost, targetDomain) ) {
                if ( log.isTraceEnabled() ) {
                    log.trace("Reusing existing session " + ssn);
                }
                return ssn.acquire();
            }
            else if ( log.isTraceEnabled() ) {
                log.trace("Existing session " + ssn + " does not match " + tf.getCredentials());
            }
        }

        /* logoff old sessions */
        if ( tf.getConfig().getSessionTimeout() > 0 && this.sessionExpiration < ( now = System.currentTimeMillis() ) ) {
            this.sessionExpiration = now + tf.getConfig().getSessionTimeout();
            iter = this.sessions.listIterator();
            while ( iter.hasNext() ) {
                SmbSessionImpl ssn = iter.next();
                if ( ssn.getExpiration() != null && ssn.getExpiration() < now && !ssn.isInUse() ) {
                    if ( log.isDebugEnabled() ) {
                        log.debug("Closing session after timeout " + ssn);
                    }
                    ssn.logoff(false, false);
                }
            }
        }
        SmbSessionImpl ssn = new SmbSessionImpl(tf, targetHost, targetDomain, this);
        if ( log.isDebugEnabled() ) {
            log.debug("Establishing new session " + ssn + " on " + this.name);
        }
        this.sessions.add(ssn);
        return ssn;
    }


    boolean matches ( Address addr, int prt, InetAddress laddr, int lprt, String hostName ) {
        if ( this.state == 5 || this.state == 6 ) {
            // don't reuse disconnecting/disconnected transports
            return false;
        }
        if ( hostName == null )
            hostName = addr.getHostName();
        return ( this.tconHostName == null || hostName.equalsIgnoreCase(this.tconHostName) ) && addr.equals(this.address)
                && ( prt == 0 || prt == this.port ||
                /* port 139 is ok if 445 was requested */
                        ( prt == 445 && this.port == 139 ) )
                && ( laddr == this.localAddr || ( laddr != null && laddr.equals(this.localAddr) ) ) && lprt == this.localPort;
    }


    void ssn139 () throws IOException {
        CIFSContext tc = this.transportContext;
        Name calledName = new Name(tc.getConfig(), this.address.firstCalledName(), 0x20, null);
        do {
            this.socket = new Socket();
            if ( this.localAddr != null )
                this.socket.bind(new InetSocketAddress(this.localAddr, this.localPort));
            this.socket.connect(new InetSocketAddress(this.address.getHostAddress(), 139), tc.getConfig().getConnTimeout());
            this.socket.setSoTimeout(tc.getConfig().getSoTimeout());

            this.out = this.socket.getOutputStream();
            this.in = this.socket.getInputStream();

            SessionServicePacket ssp = new SessionRequestPacket(tc.getConfig(), calledName, tc.getNameServiceClient().getLocalName());
            this.out.write(this.sbuf, 0, ssp.writeWireFormat(this.sbuf, 0));
            if ( readn(this.in, this.sbuf, 0, 4) < 4 ) {
                try {
                    this.socket.close();
                }
                catch ( IOException ioe ) {
                    log.debug("Failed to close socket", ioe);
                }
                throw new SmbException("EOF during NetBIOS session request");
            }
            switch ( this.sbuf[ 0 ] & 0xFF ) {
            case SessionServicePacket.POSITIVE_SESSION_RESPONSE:
                if ( log.isDebugEnabled() ) {
                    log.debug("session established ok with " + this.address);
                }
                return;
            case SessionServicePacket.NEGATIVE_SESSION_RESPONSE:
                int errorCode = this.in.read() & 0xFF;
                switch ( errorCode ) {
                case NbtException.CALLED_NOT_PRESENT:
                case NbtException.NOT_LISTENING_CALLED:
                    this.socket.close();
                    break;
                default:
                    disconnect(true);
                    throw new NbtException(NbtException.ERR_SSN_SRVC, errorCode);
                }
                break;
            case -1:
                disconnect(true);
                throw new NbtException(NbtException.ERR_SSN_SRVC, NbtException.CONNECTION_REFUSED);
            default:
                disconnect(true);
                throw new NbtException(NbtException.ERR_SSN_SRVC, 0);
            }
        }
        while ( ( calledName.name = this.address.nextCalledName(tc) ) != null );

        throw new IOException("Failed to establish session with " + this.address);
    }


    private SmbNegotiation negotiate ( int prt ) throws IOException {
        /*
         * We cannot use Transport.sendrecv() yet because
         * the Transport thread is not setup until doConnect()
         * returns and we want to suppress all communication
         * until we have properly negotiated.
         */
        synchronized ( this.inLock ) {
            if ( prt == 139 ) {
                ssn139();
            }
            else {
                if ( prt == 0 )
                    prt = DEFAULT_PORT; // 445

                this.socket = new Socket();
                if ( this.localAddr != null )
                    this.socket.bind(new InetSocketAddress(this.localAddr, this.localPort));
                this.socket.connect(new InetSocketAddress(this.address.getHostAddress(), prt), this.transportContext.getConfig().getConnTimeout());
                this.socket.setSoTimeout(this.transportContext.getConfig().getSoTimeout());

                this.out = this.socket.getOutputStream();
                this.in = this.socket.getInputStream();
            }

            if ( this.credits.drainPermits() == 0 ) {
                log.debug("It appears we previously lost some credits");
            }

            if ( this.smb2 || this.getContext().getConfig().isUseSMB2OnlyNegotiation() ) {
                log.debug("Using SMB2 only negotiation");
                return negotiate2(null);
            }

            SmbComNegotiate comNeg = new SmbComNegotiate(getContext().getConfig(), this.signingEnforced);
            int n = negotiateWrite(comNeg, true);
            negotiatePeek();

            SmbNegotiationResponse resp = null;

            if ( !this.smb2 ) {
                if ( this.getContext().getConfig().getMinimumVersion().isSMB2() ) {
                    throw new CIFSException("Server does not support SMB2");
                }
                resp = new SmbComNegotiateResponse(getContext());
                resp.decode(this.sbuf, 4);
                resp.received();

                if ( log.isTraceEnabled() ) {
                    log.trace(resp.toString());
                    log.trace(Hexdump.toHexString(this.sbuf, 4, n));
                }
            }
            else {
                Smb2NegotiateResponse r = new Smb2NegotiateResponse(getContext().getConfig());
                r.decode(this.sbuf, 4);
                r.received();

                if ( r.getDialectRevision() == Smb2Constants.SMB2_DIALECT_ANY ) {
                    return negotiate2(r);
                }
                else if ( r.getDialectRevision() != Smb2Constants.SMB2_DIALECT_0202 ) {
                    throw new CIFSException("Server returned invalid dialect verison in multi protocol negotiation");
                }

                int permits = r.getInitialCredits();
                if ( permits > 0 ) {
                    this.credits.release(permits);
                }
                Arrays.fill(this.sbuf, (byte) 0);
                return new SmbNegotiation(
                    new Smb2NegotiateRequest(
                        getContext().getConfig(),
                        this.signingEnforced ? Smb2Constants.SMB2_NEGOTIATE_SIGNING_REQUIRED : Smb2Constants.SMB2_NEGOTIATE_SIGNING_ENABLED),
                    r,
                    null,
                    null);
            }

            int permits = resp.getInitialCredits();
            if ( permits > 0 ) {
                this.credits.release(permits);
            }
            Arrays.fill(this.sbuf, (byte) 0);
            return new SmbNegotiation(comNeg, resp, null, null);
        }
    }


    /**
     * @return
     * @throws IOException
     */
    private int negotiateWrite ( CommonServerMessageBlockRequest req, boolean setmid ) throws IOException {
        if ( setmid ) {
            makeKey(req);
        }
        else {
            req.setMid(0);
            this.mid.set(1);
        }
        int n = req.encode(this.sbuf, 4);
        Encdec.enc_uint32be(n & 0xFFFF, this.sbuf, 0); /* 4 byte ssn msg header */

        if ( log.isTraceEnabled() ) {
            log.trace(req.toString());
            log.trace(Hexdump.toHexString(this.sbuf, 4, n));
        }

        this.out.write(this.sbuf, 0, 4 + n);
        this.out.flush();
        log.trace("Wrote negotiate request");
        return n;
    }


    /**
     * @throws SocketException
     * @throws IOException
     */
    private void negotiatePeek () throws SocketException, IOException {
        /*
         * Note the Transport thread isn't running yet so we can
         * read from the socket here.
         */
        try {
            this.socket.setSoTimeout(this.transportContext.getConfig().getConnTimeout());
            if ( peekKey() == null ) /* try to read header */
                throw new IOException("transport closed in negotiate");
        }
        finally {
            this.socket.setSoTimeout(this.transportContext.getConfig().getSoTimeout());
        }
        int size = Encdec.dec_uint16be(this.sbuf, 2) & 0xFFFF;
        if ( size < 33 || ( 4 + size ) > this.sbuf.length ) {
            throw new IOException("Invalid payload size: " + size);
        }
        int hdrSize = this.smb2 ? Smb2Constants.SMB2_HEADER_LENGTH : SMB1_HEADER_LENGTH;
        readn(this.in, this.sbuf, 4 + hdrSize, size - hdrSize);
        log.trace("Read negotiate response");
    }


    /**
     * @param first
     * @param n
     * @return
     * @throws IOException
     * @throws SocketException
     * @throws InterruptedException
     */
    private SmbNegotiation negotiate2 ( Smb2NegotiateResponse first ) throws IOException, SocketException {
        int size = 0;

        int securityMode = getRequestSecurityMode(first);

        // further negotiation needed
        Smb2NegotiateRequest smb2neg = new Smb2NegotiateRequest(getContext().getConfig(), securityMode);
        Smb2NegotiateResponse r = null;
        byte[] negoReqBuffer = null;
        byte[] negoRespBuffer = null;
        try {
            smb2neg.setRequestCredits(Math.max(1, this.desiredCredits - this.credits.availablePermits()));

            int reqLen = negotiateWrite(smb2neg, first != null);
            boolean doPreauth = getContext().getConfig().getMaximumVersion().atLeast(DialectVersion.SMB311);
            if ( doPreauth ) {
                negoReqBuffer = new byte[reqLen];
                System.arraycopy(this.sbuf, 4, negoReqBuffer, 0, reqLen);
            }

            negotiatePeek();

            r = smb2neg.initResponse(getContext());
            int respLen = r.decode(this.sbuf, 4);
            r.received();

            if ( doPreauth ) {
                negoRespBuffer = new byte[respLen];
                System.arraycopy(this.sbuf, 4, negoRespBuffer, 0, respLen);
            }
            else {
                negoReqBuffer = null;
            }

            if ( log.isTraceEnabled() ) {
                log.trace(r.toString());
                log.trace(Hexdump.toHexString(this.sbuf, 4, size));
            }
            return new SmbNegotiation(smb2neg, r, negoReqBuffer, negoRespBuffer);
        }
        finally {
            int grantedCredits = r != null ? r.getGrantedCredits() : 0;
            if ( grantedCredits == 0 ) {
                grantedCredits = 1;
            }
            this.credits.release(grantedCredits);
            Arrays.fill(this.sbuf, (byte) 0);
        }
    }


    /**
     * Connect the transport
     *
     * @throws SmbException
     */
    @Override
    public boolean ensureConnected () throws SmbException {
        try {
            return super.connect(this.transportContext.getConfig().getResponseTimeout());
        }
        catch ( TransportException te ) {
            throw new SmbException("Failed to connect: " + this.address, te);
        }

    }


    @Override
    protected void doConnect () throws IOException {
        /*
         * Negotiate Protocol Request / Response
         */
        if ( log.isDebugEnabled() ) {
            log.debug("Connecting in state " + this.state + " addr " + this.address.getHostAddress());
        }

        SmbNegotiation resp;
        try {
            resp = negotiate(this.port);
        }
        catch ( IOException ce ) {
            if ( getContext().getConfig().isPort139FailoverEnabled() ) {
                this.port = ( this.port == 0 || this.port == DEFAULT_PORT ) ? 139 : DEFAULT_PORT;
                this.smb2 = false;
                this.mid.set(0);
                resp = negotiate(this.port);
            }
            else {
                throw ce;
            }
        }

        if ( resp == null || resp.getResponse() == null ) {
            throw new SmbException("Failed to connect.");
        }

        if ( log.isDebugEnabled() ) {
            log.debug("Negotiation response on " + this.name + " :" + resp);
        }

        if ( !resp.getResponse().isValid(getContext(), resp.getRequest()) ) {
            throw new SmbException("This client is not compatible with the server.");
        }

        boolean serverRequireSig = resp.getResponse().isSigningRequired();
        boolean serverEnableSig = resp.getResponse().isSigningEnabled();
        if ( log.isDebugEnabled() ) {
            log.debug(
                "Signature negotiation enforced " + this.signingEnforced + " (server " + serverRequireSig + ") enabled "
                        + this.getContext().getConfig().isSigningEnabled() + " (server " + serverEnableSig + ")");
        }

        /* Adjust negotiated values */
        this.tconHostName = this.address.getHostName();
        this.negotiated = resp.getResponse();
        if ( resp.getResponse().getSelectedDialect().atLeast(DialectVersion.SMB311) ) {
            updatePreauthHash(resp.getRequestRaw());
            updatePreauthHash(resp.getResponseRaw());
            if ( log.isDebugEnabled() ) {
                log.debug("Preauth hash after negotiate " + Hexdump.toHexString(this.preauthIntegrityHash));
            }
        }
    }


    protected synchronized void doDisconnect ( boolean hard ) throws IOException {
        doDisconnect(hard, false);
    }


    @Override
    protected synchronized boolean doDisconnect ( boolean hard, boolean inUse ) throws IOException {
        ListIterator<SmbSessionImpl> iter = this.sessions.listIterator();
        boolean wasInUse = false;
        long l = getUsageCount();
        if ( ( inUse && l != 1 ) || ( !inUse && l > 0 ) ) {
            log.warn("Disconnecting transport while still in use " + this + ": " + this.sessions);
            wasInUse = true;
        }

        if ( log.isDebugEnabled() ) {
            log.debug("Disconnecting transport " + this);
        }

        try {
            if ( log.isTraceEnabled() ) {
                log.trace("Currently " + this.sessions.size() + " session(s) active for " + this);
            }
            while ( iter.hasNext() ) {
                @SuppressWarnings ( "resource" )
                SmbSessionImpl ssn = iter.next();
                try {
                    wasInUse |= ssn.logoff(hard, false);
                }
                catch ( Exception e ) {
                    log.debug("Failed to close session", e);
                }
                finally {
                    iter.remove();
                }
            }

            if ( this.socket != null ) {
                this.socket.shutdownOutput();
                this.out.close();
                this.in.close();
                this.socket.close();
                log.trace("Socket closed");
            }
            else {
                log.trace("Not yet initialized");
            }
        }
        catch ( Exception e ) {
            log.debug("Exception in disconnect", e);
        }
        finally {
            this.socket = null;
            this.digest = null;
            this.tconHostName = null;
            this.transportContext.getTransportPool().removeTransport(this);
        }
        return wasInUse;
    }


    @Override
    protected long makeKey ( Request request ) throws IOException {
        long m = this.mid.incrementAndGet() - 1;
        if ( !this.smb2 ) {
            m = ( m % 32000 );
        }
        ( (CommonServerMessageBlock) request ).setMid(m);
        return m;
    }


    @Override
    protected Long peekKey () throws IOException {
        do {
            if ( ( readn(this.in, this.sbuf, 0, 4) ) < 4 ) {
                return null;
            }
        }
        while ( this.sbuf[ 0 ] == (byte) 0x85 ); /* Dodge NetBIOS keep-alive */
        /* read smb header */
        if ( ( readn(this.in, this.sbuf, 4, SmbConstants.SMB1_HEADER_LENGTH) ) < SmbConstants.SMB1_HEADER_LENGTH ) {
            return null;
        }

        if ( log.isTraceEnabled() ) {
            log.trace("New data read: " + this);
            log.trace(Hexdump.toHexString(this.sbuf, 4, 32));
        }

        for ( ;; ) {
            /*
             * 01234567
             * 00SSFSMB
             * 0 - 0's
             * S - size of payload
             * FSMB - 0xFF SMB magic #
             */

            if ( this.sbuf[ 0 ] == (byte) 0x00 && this.sbuf[ 4 ] == (byte) 0xFE && this.sbuf[ 5 ] == (byte) 'S' && this.sbuf[ 6 ] == (byte) 'M'
                    && this.sbuf[ 7 ] == (byte) 'B' ) {
                this.smb2 = true;
                // also read the rest of the header
                int lenDiff = Smb2Constants.SMB2_HEADER_LENGTH - SmbConstants.SMB1_HEADER_LENGTH;
                if ( readn(this.in, this.sbuf, 4 + SmbConstants.SMB1_HEADER_LENGTH, lenDiff) < lenDiff ) {
                    return null;
                }
                return (long) Encdec.dec_uint64le(this.sbuf, 28);
            }

            if ( this.sbuf[ 0 ] == (byte) 0x00 && this.sbuf[ 1 ] == (byte) 0x00 && ( this.sbuf[ 4 ] == (byte) 0xFF ) && this.sbuf[ 5 ] == (byte) 'S'
                    && this.sbuf[ 6 ] == (byte) 'M' && this.sbuf[ 7 ] == (byte) 'B' ) {
                break; /* all good (SMB) */
            }

            /* out of phase maybe? */
            /* inch forward 1 byte and try again */
            for ( int i = 0; i < 35; i++ ) {
                log.warn("Possibly out of phase, trying to resync " + Hexdump.toHexString(this.sbuf, 0, 16));
                this.sbuf[ i ] = this.sbuf[ i + 1 ];
            }
            int b;
            if ( ( b = this.in.read() ) == -1 )
                return null;
            this.sbuf[ 35 ] = (byte) b;
        }

        /*
         * Unless key returned is null or invalid Transport.loop() always
         * calls doRecv() after and no one else but the transport thread
         * should call doRecv(). Therefore it is ok to expect that the data
         * in sbuf will be preserved for copying into BUF in doRecv().
         */

        return (long) Encdec.dec_uint16le(this.sbuf, 34) & 0xFFFF;
    }


    @Override
    protected void doSend ( Request request ) throws IOException {

        CommonServerMessageBlock smb = (CommonServerMessageBlock) request;
        byte[] buffer = this.getContext().getBufferCache().getBuffer();
        try {
            // synchronize around encode and write so that the ordering for SMB1 signing can be maintained
            synchronized ( this.outLock ) {
                int n = smb.encode(buffer, 4);
                Encdec.enc_uint32be(n & 0xFFFF, buffer, 0); /* 4 byte session message header */
                if ( log.isTraceEnabled() ) {
                    do {
                        log.trace(smb.toString());
                    }
                    while ( smb instanceof AndXServerMessageBlock && ( smb = ( (AndXServerMessageBlock) smb ).getAndx() ) != null );
                    log.trace(Hexdump.toHexString(buffer, 4, n));

                }
                /*
                 * For some reason this can sometimes get broken up into another
                 * "NBSS Continuation Message" frame according to WireShark
                 */

                this.out.write(buffer, 0, 4 + n);
                this.out.flush();
            }
        }
        finally {
            this.getContext().getBufferCache().releaseBuffer(buffer);
        }
    }


    @SuppressWarnings ( "unchecked" )
    public <T extends CommonServerMessageBlockResponse> T sendrecv ( CommonServerMessageBlockRequest request, T response, Set<RequestParam> params )
            throws IOException {
        if ( request instanceof jcifs.internal.Request ) {
            if ( response == null ) {
                response = (T) ( (jcifs.internal.Request<?>) request ).initResponse(getContext());
            }
            else if ( isSMB2() ) {
                throw new IOException("Should not provide response argument for SMB2");
            }
        }
        else {
            request.setResponse(response);
        }
        if ( response == null ) {
            throw new IOException("Invalid response");
        }

        CommonServerMessageBlockRequest curHead = request;

        int maxSize = getContext().getConfig().getMaximumBufferSize();

        while ( curHead != null ) {
            CommonServerMessageBlockRequest nextHead = null;
            int totalSize = 0;
            int n = 0;
            CommonServerMessageBlockRequest last = null;
            CommonServerMessageBlockRequest chain = curHead;
            while ( chain != null ) {
                n++;
                int size = chain.size();
                int cost = chain.getCreditCost();
                CommonServerMessageBlockRequest next = chain.getNext();
                if ( log.isTraceEnabled() ) {
                    log.trace(
                        String.format("%s costs %d avail %d (%s)", chain.getClass().getName(), cost, this.credits.availablePermits(), this.name));
                }
                if ( ( next == null || chain.allowChain(next) ) && totalSize + size < maxSize && this.credits.tryAcquire(cost) ) {
                    totalSize += size;
                    last = chain;
                    chain = next;
                }
                else if ( last == null && totalSize + size > maxSize ) {
                    throw new SmbException(String.format("Request size %d exceeds allowable size %d: %s", size, maxSize, chain));
                }
                else if ( last == null ) {
                    // don't have enough credits/space for the first request, block until available
                    // for space there is nothing we can do, callers need to make sure that a single message fits

                    try {
                        long timeout = getResponseTimeout(chain);
                        if ( params.contains(RequestParam.NO_TIMEOUT) ) {
                            this.credits.acquire(cost);
                        }
                        else {
                            if ( !this.credits.tryAcquire(cost, timeout, TimeUnit.MILLISECONDS) ) {
                                throw new SmbException("Failed to acquire credits in time");
                            }
                        }
                        totalSize += size;
                        // split off first request

                        synchronized ( chain ) {
                            CommonServerMessageBlockRequest snext = chain.split();
                            nextHead = snext;
                            if ( log.isDebugEnabled() && snext != null ) {
                                log.debug("Insufficient credits, send only first " + chain + " next is " + snext);
                            }
                        }
                        break;
                    }
                    catch ( InterruptedException e ) {
                        InterruptedIOException ie = new InterruptedIOException("Interrupted while acquiring credits");
                        ie.initCause(e);
                        throw ie;
                    }
                }
                else {
                    // not enough credits available or too big, split
                    if ( log.isDebugEnabled() ) {
                        log.debug("Not enough credits, split at " + last);
                    }
                    synchronized ( last ) {
                        nextHead = last.split();
                    }
                    break;
                }
            }

            int reqCredits = Math.max(1, this.desiredCredits - this.credits.availablePermits() - n + 1);
            if ( log.isTraceEnabled() ) {
                log.trace("Request credits " + reqCredits);
            }
            request.setRequestCredits(reqCredits);

            CommonServerMessageBlockRequest thisReq = curHead;
            try {
                CommonServerMessageBlockResponse resp = thisReq.getResponse();
                if ( log.isTraceEnabled() ) {
                    log.trace("Sending " + thisReq);
                }
                resp = super.sendrecv(curHead, resp, params);

                if ( !checkStatus(curHead, resp) ) {
                    if ( log.isDebugEnabled() ) {
                        log.debug("Breaking on error " + resp);
                    }
                    break;
                }

                if ( nextHead != null ) {
                    // prepare remaining
                    // (e.g. set session/tree/fileid returned by the previous requests)
                    resp.prepare(nextHead);
                }
                curHead = nextHead;
            }
            finally {
                CommonServerMessageBlockRequest curReq = thisReq;
                int grantedCredits = 0;
                // if
                while ( curReq != null ) {
                    if ( curReq.isResponseAsync() ) {
                        log.trace("Async");
                        break;
                    }

                    CommonServerMessageBlockResponse resp = curReq.getResponse();

                    if ( resp.isReceived() ) {
                        grantedCredits += resp.getGrantedCredits();
                    }
                    CommonServerMessageBlockRequest next = curReq.getNext();
                    if ( next == null ) {
                        break;
                    }
                    curReq = next;
                }
                if ( !isDisconnected() && !curReq.isResponseAsync() && !curReq.getResponse().isAsync() && !curReq.getResponse().isError()
                        && grantedCredits == 0 ) {
                    if ( this.credits.availablePermits() > 0 || n > 0 ) {
                        log.debug("Server " + this + " returned zero credits for " + curReq);
                    }
                    else {
                        log.warn("Server " + this + " took away all our credits");
                    }
                }
                else if ( !curReq.isResponseAsync() ) {
                    if ( log.isTraceEnabled() ) {
                        log.trace("Adding credits " + grantedCredits);
                    }
                    this.credits.release(grantedCredits);
                }
            }
        }

        if ( !response.isReceived() ) {
            throw new IOException("No response", response.getException());
        }
        return response;

    }


    @Override
    protected <T extends Response> boolean handleIntermediate ( Request request, T response ) {
        if ( !this.smb2 ) {
            return false;
        }
        ServerMessageBlock2Request<?> req = (ServerMessageBlock2Request<?>) request;
        ServerMessageBlock2Response resp = (ServerMessageBlock2Response) response;
        synchronized ( resp ) {
            if ( resp.isAsync() && !resp.isAsyncHandled() && resp.getStatus() == NtStatus.NT_STATUS_PENDING && resp.getAsyncId() != 0 ) {
                resp.setAsyncHandled(true);
                boolean first = !req.isAsync();
                req.setAsyncId(resp.getAsyncId());
                Long exp = resp.getExpiration();
                if ( exp != null ) {
                    resp.setExpiration(System.currentTimeMillis() + getResponseTimeout(request));
                }
                if ( log.isDebugEnabled() ) {
                    log.debug("Have intermediate reply " + response);
                }

                if ( first ) {
                    int credit = resp.getCredit();
                    if ( log.isDebugEnabled() ) {
                        log.debug("Credit from intermediate " + credit);
                    }
                    this.credits.release(credit);
                }
                return true;
            }
        }
        return false;
    }


    protected void doSend0 ( Request request ) throws IOException {
        try {
            doSend(request);
        }
        catch ( IOException ioe ) {
            log.warn("send failed", ioe);
            try {
                disconnect(true);
            }
            catch ( IOException ioe2 ) {
                ioe.addSuppressed(ioe2);
                log.error("disconnect failed", ioe2);
            }
            throw ioe;
        }
    }


    // must be synchronized with peekKey
    @Override
    protected void doRecv ( Response response ) throws IOException {
        CommonServerMessageBlock resp = (CommonServerMessageBlock) response;
        this.negotiated.setupResponse(response);
        try {
            if ( this.smb2 ) {
                doRecvSMB2(resp);
            }
            else {
                doRecvSMB1(resp);
            }
        }
        catch ( Exception e ) {
            log.warn("Failure decoding message, disconnecting transport", e);
            response.exception(e);
            synchronized ( response ) {
                response.notifyAll();
            }
            throw e;
        }

    }


    /**
     * @param response
     * @throws IOException
     * @throws SMBProtocolDecodingException
     */
    private void doRecvSMB2 ( CommonServerMessageBlock response ) throws IOException, SMBProtocolDecodingException {
        int size = ( Encdec.dec_uint16be(this.sbuf, 2) & 0xFFFF ) | ( this.sbuf[ 1 ] & 0xFF ) << 16;
        if ( size < ( Smb2Constants.SMB2_HEADER_LENGTH + 1 ) ) {
            throw new IOException("Invalid payload size: " + size);
        }

        if ( this.sbuf[ 0 ] != (byte) 0x00 || this.sbuf[ 4 ] != (byte) 0xFE || this.sbuf[ 5 ] != (byte) 'S' || this.sbuf[ 6 ] != (byte) 'M'
                || this.sbuf[ 7 ] != (byte) 'B' ) {
            throw new IOException("Houston we have a synchronization problem");
        }

        int nextCommand = Encdec.dec_uint32le(this.sbuf, 4 + 20);
        int maximumBufferSize = getContext().getConfig().getMaximumBufferSize();
        int msgSize = nextCommand != 0 ? nextCommand : size;
        if ( msgSize > maximumBufferSize ) {
            throw new IOException(String.format("Message size %d exceeds maxiumum buffer size %d", msgSize, maximumBufferSize));
        }

        ServerMessageBlock2Response cur = (ServerMessageBlock2Response) response;
        byte[] buffer = getContext().getBufferCache().getBuffer();
        try {
            int rl = nextCommand != 0 ? nextCommand : size;

            // read and decode first
            System.arraycopy(this.sbuf, 4, buffer, 0, Smb2Constants.SMB2_HEADER_LENGTH);
            readn(this.in, buffer, Smb2Constants.SMB2_HEADER_LENGTH, rl - Smb2Constants.SMB2_HEADER_LENGTH);

            cur.setReadSize(rl);
            int len = cur.decode(buffer, 0);

            if ( len > rl ) {
                throw new IOException(String.format("WHAT? ( read %d decoded %d ): %s", rl, len, cur));
            }
            else if ( nextCommand != 0 && len > nextCommand ) {
                throw new IOException("Overlapping commands");
            }
            size -= rl;

            while ( size > 0 && nextCommand != 0 ) {
                cur = (ServerMessageBlock2Response) cur.getNextResponse();
                if ( cur == null ) {
                    log.warn("Response not properly set up");
                    this.in.skip(size);
                    break;
                }

                // read next header
                readn(this.in, buffer, 0, Smb2Constants.SMB2_HEADER_LENGTH);
                nextCommand = Encdec.dec_uint32le(buffer, 20);

                if ( ( nextCommand != 0 && nextCommand > maximumBufferSize ) || ( nextCommand == 0 && size > maximumBufferSize ) ) {
                    throw new IOException(
                        String.format("Message size %d exceeds maxiumum buffer size %d", nextCommand != 0 ? nextCommand : size, maximumBufferSize));
                }

                rl = nextCommand != 0 ? nextCommand : size;

                if ( log.isDebugEnabled() ) {
                    log.debug(String.format("Compound next command %d read size %d remain %d", nextCommand, rl, size));
                }

                cur.setReadSize(rl);
                readn(this.in, buffer, Smb2Constants.SMB2_HEADER_LENGTH, rl - Smb2Constants.SMB2_HEADER_LENGTH);

                len = cur.decode(buffer, 0, true);
                if ( len > rl ) {
                    throw new IOException(String.format("WHAT? ( read %d decoded %d ): %s", rl, len, cur));
                }
                else if ( nextCommand != 0 && len > nextCommand ) {
                    throw new IOException("Overlapping commands");
                }
                size -= rl;
            }
        }
        finally {
            getContext().getBufferCache().releaseBuffer(buffer);
        }
    }


    /**
     * @param resp
     * @throws IOException
     * @throws SMBProtocolDecodingException
     */
    private void doRecvSMB1 ( CommonServerMessageBlock resp ) throws IOException, SMBProtocolDecodingException {
        byte[] buffer = getContext().getBufferCache().getBuffer();
        try {
            System.arraycopy(this.sbuf, 0, buffer, 0, 4 + SMB1_HEADER_LENGTH);
            int size = ( Encdec.dec_uint16be(buffer, 2) & 0xFFFF );
            if ( size < ( SMB1_HEADER_LENGTH + 1 ) || ( 4 + size ) > Math.min(0xFFFF, getContext().getConfig().getMaximumBufferSize()) ) {
                throw new IOException("Invalid payload size: " + size);
            }
            int errorCode = Encdec.dec_uint32le(buffer, 9) & 0xFFFFFFFF;
            if ( resp.getCommand() == ServerMessageBlock.SMB_COM_READ_ANDX
                    && ( errorCode == 0 || errorCode == NtStatus.NT_STATUS_BUFFER_OVERFLOW ) ) {
                // overflow indicator normal for pipe
                SmbComReadAndXResponse r = (SmbComReadAndXResponse) resp;
                int off = SMB1_HEADER_LENGTH;
                /* WordCount thru dataOffset always 27 */
                readn(this.in, buffer, 4 + off, 27);
                off += 27;
                resp.decode(buffer, 4);
                /* EMC can send pad w/o data */
                int pad = r.getDataOffset() - off;
                if ( r.getByteCount() > 0 && pad > 0 && pad < 4 )
                    readn(this.in, buffer, 4 + off, pad);

                if ( r.getDataLength() > 0 ) {
                    readn(this.in, r.getData(), r.getOffset(), r.getDataLength()); /* read direct */
                }
            }
            else {
                readn(this.in, buffer, 4 + SMB1_HEADER_LENGTH, size - SMB1_HEADER_LENGTH);
                resp.decode(buffer, 4);
            }
        }
        finally {
            getContext().getBufferCache().releaseBuffer(buffer);
        }
    }


    @Override
    protected void doSkip ( Long key ) throws IOException {
        synchronized ( this.inLock ) {
            int size = Encdec.dec_uint16be(this.sbuf, 2) & 0xFFFF;
            if ( size < 33 || ( 4 + size ) > this.getContext().getConfig().getReceiveBufferSize() ) {
                /* log message? */
                log.warn("Flusing stream input");
                this.in.skip(this.in.available());
            }
            else {
                Response notification = createNotification(key);
                if ( notification != null ) {
                    log.debug("Parsing notification");
                    doRecv(notification);
                    handleNotification(notification);
                    return;
                }
                log.warn("Skipping message " + key);
                this.in.skip(size - 32);
            }
        }
    }


    /**
     * @param notification
     */
    protected void handleNotification ( Response notification ) {
        log.info("Received notification " + notification);
    }


    /**
     * @param key
     * @return
     * @throws SmbException
     */
    protected Response createNotification ( Long key ) throws SmbException {
        if ( key == null ) {
            // no valid header
            return null;
        }
        if ( this.smb2 ) {
            if ( key != -1 ) {
                return null;
            }
            int cmd = Encdec.dec_uint16le(this.sbuf, 4 + 12) & 0xFFFF;
            if ( cmd == 0x12 ) {
                return new Smb2OplockBreakNotification(getContext().getConfig());
            }
        }
        else {
            if ( key != 0xFFFF ) {
                return null;
            }
            int cmd = this.sbuf[ 4 + 4 ];
            if ( cmd == 0x24 ) {
                return new SmbComLockingAndX(getContext().getConfig());
            }
        }
        return null;
    }


    boolean checkStatus ( ServerMessageBlock req, ServerMessageBlock resp ) throws SmbException {
        boolean cont = false;
        if ( resp.getErrorCode() == 0x30002 ) {
            // if using DOS error codes this indicates a DFS referral
            resp.setErrorCode(NtStatus.NT_STATUS_PATH_NOT_COVERED);
        }
        else {
            resp.setErrorCode(SmbException.getStatusByCode(resp.getErrorCode()));
        }
        switch ( resp.getErrorCode() ) {
        case NtStatus.NT_STATUS_OK:
            cont = true;
            break;
        case NtStatus.NT_STATUS_ACCESS_DENIED:
        case NtStatus.NT_STATUS_WRONG_PASSWORD:
        case NtStatus.NT_STATUS_LOGON_FAILURE:
        case NtStatus.NT_STATUS_ACCOUNT_RESTRICTION:
        case NtStatus.NT_STATUS_INVALID_LOGON_HOURS:
        case NtStatus.NT_STATUS_INVALID_WORKSTATION:
        case NtStatus.NT_STATUS_PASSWORD_EXPIRED:
        case NtStatus.NT_STATUS_ACCOUNT_DISABLED:
        case NtStatus.NT_STATUS_ACCOUNT_LOCKED_OUT:
        case NtStatus.NT_STATUS_TRUSTED_DOMAIN_FAILURE:
            throw new SmbAuthException(resp.getErrorCode());
        case 0xC00000BB: // NT_STATUS_NOT_SUPPORTED
            throw new SmbUnsupportedOperationException();
        case NtStatus.NT_STATUS_PATH_NOT_COVERED:
            // samba fails to report the proper status for some operations
        case 0xC00000A2: // NT_STATUS_MEDIA_WRITE_PROTECTED
            checkReferral(resp, req.getPath(), req);
        case NtStatus.NT_STATUS_BUFFER_OVERFLOW:
            break; /* normal for DCERPC named pipes */
        case NtStatus.NT_STATUS_MORE_PROCESSING_REQUIRED:
            break; /* normal for NTLMSSP */
        default:
            if ( log.isDebugEnabled() ) {
                log.debug("Error code: 0x" + Hexdump.toHexString(resp.getErrorCode(), 8) + " for " + req.getClass().getSimpleName());
            }
            throw new SmbException(resp.getErrorCode(), null);
        }
        if ( resp.isVerifyFailed() ) {
            throw new SmbException("Signature verification failed.");
        }
        return cont;
    }


    /**
     * @param request
     * @param response
     * @throws SmbException
     */
    boolean checkStatus2 ( ServerMessageBlock2 req, Response resp ) throws SmbException {
        boolean cont = false;
        switch ( resp.getErrorCode() ) {
        case NtStatus.NT_STATUS_OK:
        case NtStatus.NT_STATUS_NO_MORE_FILES:
            cont = true;
            break;
        case NtStatus.NT_STATUS_PENDING:
            // must be the last
            cont = false;
            break;
        case NtStatus.NT_STATUS_ACCESS_DENIED:
        case NtStatus.NT_STATUS_WRONG_PASSWORD:
        case NtStatus.NT_STATUS_LOGON_FAILURE:
        case NtStatus.NT_STATUS_ACCOUNT_RESTRICTION:
        case NtStatus.NT_STATUS_INVALID_LOGON_HOURS:
        case NtStatus.NT_STATUS_INVALID_WORKSTATION:
        case NtStatus.NT_STATUS_PASSWORD_EXPIRED:
        case NtStatus.NT_STATUS_ACCOUNT_DISABLED:
        case NtStatus.NT_STATUS_ACCOUNT_LOCKED_OUT:
        case NtStatus.NT_STATUS_TRUSTED_DOMAIN_FAILURE:
            throw new SmbAuthException(resp.getErrorCode());
        case NtStatus.NT_STATUS_MORE_PROCESSING_REQUIRED:
            break; /* normal for SPNEGO */
        case 0x10B: // NT_STATUS_NOTIFY_CLEANUP:
        case NtStatus.NT_STATUS_NOTIFY_ENUM_DIR:
            break;
        case 0xC00000BB: // NT_STATUS_NOT_SUPPORTED
            throw new SmbUnsupportedOperationException();
        case NtStatus.NT_STATUS_PATH_NOT_COVERED:
            if ( ! ( req instanceof RequestWithPath ) ) {
                throw new SmbException("Invalid request for a DFS NT_STATUS_PATH_NOT_COVERED response " + req.getClass().getName());
            }
            String path = ( (RequestWithPath) req ).getFullUNCPath();
            checkReferral(resp, path, ( (RequestWithPath) req ));
            // checkReferral always throws and exception but put break here for clarity
            break;
        case NtStatus.NT_STATUS_BUFFER_OVERFLOW:
            if ( resp instanceof Smb2ReadResponse ) {
                break;
            }
            if ( resp instanceof Smb2IoctlResponse ) {
                int ctlCode = ( (Smb2IoctlResponse) resp ).getCtlCode();
                if ( ctlCode == Smb2IoctlRequest.FSCTL_PIPE_TRANSCEIVE || ctlCode == Smb2IoctlRequest.FSCTL_PIPE_PEEK ) {
                    break;
                }
            }
            // fall through
        default:
            if ( log.isDebugEnabled() ) {
                log.debug("Error code: 0x" + Hexdump.toHexString(resp.getErrorCode(), 8) + " for " + req.getClass().getSimpleName());
            }
            throw new SmbException(resp.getErrorCode(), null);
        }
        if ( resp.isVerifyFailed() ) {
            throw new SMBSignatureValidationException("Signature verification failed.");
        }
        return cont;
    }


    /**
     * @param resp
     * @param path
     * @param req
     * @throws SmbException
     * @throws DfsReferral
     */
    private void checkReferral ( Response resp, String path, RequestWithPath req ) throws SmbException, DfsReferral {
        DfsReferralData dr = null;
        if ( !getContext().getConfig().isDfsDisabled() ) {
            try {
                dr = getDfsReferrals(getContext(), path, req.getServer(), req.getDomain(), 1);
            }
            catch ( CIFSException e ) {
                throw new SmbException("Failed to get DFS referral", e);
            }
        }
        if ( dr == null ) {
            if ( log.isDebugEnabled() ) {
                log.debug("Error code: 0x" + Hexdump.toHexString(resp.getErrorCode(), 8));
            }
            throw new SmbException(resp.getErrorCode(), null);
        }

        if ( req.getDomain() != null && getContext().getConfig().isDfsConvertToFQDN() && dr instanceof DfsReferralDataImpl ) {
            ( (DfsReferralDataImpl) dr ).fixupDomain(req.getDomain());
        }
        if ( log.isDebugEnabled() ) {
            log.debug("Got referral " + dr);
        }

        getContext().getDfs().cache(getContext(), path, dr);
        throw new DfsReferral(dr);
    }


    <T extends CommonServerMessageBlockResponse> T send ( CommonServerMessageBlockRequest request, T response ) throws SmbException {
        return send(request, response, Collections.<RequestParam> emptySet());
    }


    <T extends CommonServerMessageBlockResponse> T send ( CommonServerMessageBlockRequest request, T response, Set<RequestParam> params )
            throws SmbException {
        ensureConnected(); /* must negotiate before we can test flags2, useUnicode, etc */
        if ( this.smb2 && ! ( request instanceof ServerMessageBlock2 ) ) {
            throw new SmbException("Not an SMB2 request " + request.getClass().getName());
        }
        else if ( !this.smb2 && ! ( request instanceof ServerMessageBlock ) ) {
            throw new SmbException("Not an SMB1 request");
        }

        this.negotiated.setupRequest(request);

        if ( response != null ) {
            request.setResponse(response); /* needed by sign */
            response.setDigest(request.getDigest());
        }

        try {
            if ( log.isTraceEnabled() ) {
                log.trace("Sending " + request);
            }
            if ( request.isCancel() ) {
                doSend0(request);
                return null;
            }
            else if ( request instanceof SmbComTransaction ) {
                response = sendComTransaction(request, response, params);
            }
            else {
                if ( response != null ) {
                    response.setCommand(request.getCommand());
                }
                response = sendrecv(request, response, params);
            }
        }
        catch ( SmbException se ) {
            throw se;
        }
        catch ( IOException ioe ) {
            throw new SmbException(ioe.getMessage(), ioe);
        }

        if ( log.isTraceEnabled() ) {
            log.trace("Response is " + response);
        }

        checkStatus(request, response);
        return response;
    }


    /**
     * @param request
     * @param response
     * @throws SmbException
     */
    private <T extends CommonServerMessageBlockResponse> boolean checkStatus ( CommonServerMessageBlockRequest request, T response )
            throws SmbException {
        CommonServerMessageBlockRequest cur = request;
        while ( cur != null ) {
            if ( this.smb2 ) {
                if ( !checkStatus2((ServerMessageBlock2) cur, cur.getResponse()) ) {
                    return false;
                }
            }
            else {
                if ( !checkStatus((ServerMessageBlock) cur, (ServerMessageBlock) cur.getResponse()) ) {
                    return false;
                }
            }
            cur = cur.getNext();
        }
        return true;
    }


    /**
     * @param request
     * @param response
     * @param params
     * @throws IOException
     * @throws SmbException
     * @throws TransportException
     * @throws EOFException
     */
    private <T extends CommonServerMessageBlock & Response> T sendComTransaction ( CommonServerMessageBlockRequest request, T response,
            Set<RequestParam> params ) throws IOException, SmbException, TransportException, EOFException {
        response.setCommand(request.getCommand());
        SmbComTransaction req = (SmbComTransaction) request;
        SmbComTransactionResponse resp = (SmbComTransactionResponse) response;
        resp.reset();

        long k;

        /*
         * First request w/ interim response
         */
        try {
            req.setBuffer(getContext().getBufferCache().getBuffer());
            req.nextElement();
            if ( req.hasMoreElements() ) {
                SmbComBlankResponse interim = new SmbComBlankResponse(getContext().getConfig());
                super.sendrecv(req, interim, params);
                if ( interim.getErrorCode() != 0 ) {
                    checkStatus(req, interim);
                }
                k = req.nextElement().getMid();
            }
            else {
                k = makeKey(req);
            }

            try {
                resp.clearReceived();
                long timeout = getResponseTimeout(req);
                if ( !params.contains(RequestParam.NO_TIMEOUT) ) {
                    resp.setExpiration(System.currentTimeMillis() + timeout);
                }
                else {
                    resp.setExpiration(null);
                }

                byte[] txbuf = getContext().getBufferCache().getBuffer();
                resp.setBuffer(txbuf);

                this.response_map.put(k, resp);

                /*
                 * Send multiple fragments
                 */

                do {
                    doSend0(req);
                }
                while ( req.hasMoreElements() && req.nextElement() != null );

                /*
                 * Receive multiple fragments
                 */
                synchronized ( resp ) {
                    while ( !resp.isReceived() || resp.hasMoreElements() ) {
                        if ( !params.contains(RequestParam.NO_TIMEOUT) ) {
                            resp.wait(timeout);
                            timeout = resp.getExpiration() - System.currentTimeMillis();
                            if ( timeout <= 0 ) {
                                throw new TransportException(this + " timedout waiting for response to " + req);
                            }
                        }
                        else {
                            resp.wait();
                            if ( log.isTraceEnabled() ) {
                                log.trace("Wait returned " + isDisconnected());
                            }
                            if ( isDisconnected() ) {
                                throw new EOFException("Transport closed while waiting for result");
                            }
                        }
                    }
                }

                if ( !resp.isReceived() ) {
                    throw new TransportException("Failed to read response");
                }

                if ( resp.getErrorCode() != 0 ) {
                    checkStatus(req, resp);
                }
                return response;
            }
            finally {
                this.response_map.remove(k);
            }
        }
        catch ( InterruptedException ie ) {
            throw new TransportException(ie);
        }
        finally {
            getContext().getBufferCache().releaseBuffer(req.releaseBuffer());
            getContext().getBufferCache().releaseBuffer(resp.releaseBuffer());
        }

    }


    @Override
    public String toString () {
        return super.toString() + "[" + this.address + ":" + this.port + ",state=" + this.state + ",signingEnforced=" + this.signingEnforced
                + ",usage=" + this.getUsageCount() + "]";
    }


    /* DFS */
    @Override
    public DfsReferralData getDfsReferrals ( CIFSContext ctx, String path, String targetHost, String targetDomain, int rn ) throws CIFSException {
        if ( log.isDebugEnabled() ) {
            log.debug("Resolving DFS path " + path);
        }

        if ( path.length() >= 2 && path.charAt(0) == '\\' && path.charAt(1) == '\\' ) {
            throw new SmbException("Path must not start with double slash: " + path);
        }

        try ( SmbSessionImpl sess = getSmbSession(ctx, targetHost, targetDomain);
              SmbTransportImpl transport = sess.getTransport();
              SmbTreeImpl ipc = sess.getSmbTree("IPC$", null) ) {

            DfsReferralRequestBuffer dfsReq = new DfsReferralRequestBuffer(path, 3);
            DfsReferralResponseBuffer dfsResp;
            if ( isSMB2() ) {
                Smb2IoctlRequest req = new Smb2IoctlRequest(ctx.getConfig(), Smb2IoctlRequest.FSCTL_DFS_GET_REFERRALS);
                req.setFlags(Smb2IoctlRequest.SMB2_O_IOCTL_IS_FSCTL);
                req.setInputData(dfsReq);
                dfsResp = ipc.send(req).getOutputData(DfsReferralResponseBuffer.class);
            }
            else {
                Trans2GetDfsReferralResponse resp = new Trans2GetDfsReferralResponse(ctx.getConfig());
                ipc.send(new Trans2GetDfsReferral(ctx.getConfig(), path), resp);
                dfsResp = resp.getDfsResponse();
            }

            if ( dfsResp.getNumReferrals() == 0 ) {
                return null;
            }
            else if ( rn == 0 || dfsResp.getNumReferrals() < rn ) {
                rn = dfsResp.getNumReferrals();
            }

            DfsReferralDataImpl cur = null;
            long expiration = System.currentTimeMillis() + ( ctx.getConfig().getDfsTtl() * 1000 );
            Referral[] refs = dfsResp.getReferrals();
            for ( int di = 0; di < rn; di++ ) {
                DfsReferralDataImpl dr = DfsReferralDataImpl.fromReferral(refs[ di ], path, expiration, dfsResp.getPathConsumed());
                dr.setDomain(targetDomain);

                if ( ( dfsResp.getTflags() & 0x2 ) == 0 && ( dr.getFlags() & 0x2 ) == 0 ) {
                    log.debug("Non-root referral is not final " + dfsResp);
                    dr.intermediate();
                }

                if ( cur == null ) {
                    cur = dr;
                }
                else {
                    cur.append(dr);
                    cur = dr;
                }
            }

            if ( log.isDebugEnabled() ) {
                log.debug("Got referral " + cur);
            }
            return cur;
        }
    }


    byte[] getPreauthIntegrityHash () {
        return this.preauthIntegrityHash;
    }


    private void updatePreauthHash ( byte[] input ) throws CIFSException {
        synchronized ( this.preauthIntegrityHash ) {
            this.preauthIntegrityHash = calculatePreauthHash(input, 0, input.length, this.preauthIntegrityHash);
        }
    }


    byte[] calculatePreauthHash ( byte[] input, int off, int len, byte[] oldHash ) throws CIFSException {
        if ( !this.smb2 || this.negotiated == null ) {
            throw new SmbUnsupportedOperationException();
        }

        Smb2NegotiateResponse resp = (Smb2NegotiateResponse) this.negotiated;
        if ( !resp.getSelectedDialect().atLeast(DialectVersion.SMB311) ) {
            throw new SmbUnsupportedOperationException();
        }

        MessageDigest dgst;
        switch ( resp.getSelectedPreauthHash() ) {
        case 1:
            dgst = Crypto.getSHA512();
            break;
        default:
            throw new SmbUnsupportedOperationException();
        }

        if ( oldHash != null ) {
            dgst.update(oldHash);
        }
        dgst.update(input, off, len);
        return dgst.digest();
    }


    Cipher createEncryptionCipher ( byte[] key ) throws CIFSException {
        if ( !this.smb2 || this.negotiated == null ) {
            throw new SmbUnsupportedOperationException();
        }

        Smb2NegotiateResponse resp = (Smb2NegotiateResponse) this.negotiated;
        int cipherId = -1;

        if ( resp.getSelectedDialect().atLeast(DialectVersion.SMB311) ) {
            cipherId = resp.getSelectedCipher();
        }
        else if ( resp.getSelectedDialect().atLeast(DialectVersion.SMB300) ) {
            cipherId = EncryptionNegotiateContext.CIPHER_AES128_CCM;
        }
        else {
            throw new SmbUnsupportedOperationException();
        }

        switch ( cipherId ) {
        case EncryptionNegotiateContext.CIPHER_AES128_CCM:
        case EncryptionNegotiateContext.CIPHER_AES128_GCM:
        default:
            throw new SmbUnsupportedOperationException();
        }
    }


    public int getRequestSecurityMode ( Smb2NegotiateResponse first ) {
        int securityMode = Smb2Constants.SMB2_NEGOTIATE_SIGNING_ENABLED;
        if ( this.signingEnforced || ( first != null && first.isSigningRequired() ) ) {
            securityMode = Smb2Constants.SMB2_NEGOTIATE_SIGNING_REQUIRED | Smb2Constants.SMB2_NEGOTIATE_SIGNING_ENABLED;
        }

        return securityMode;
    }
}