/*
 * Copyright (C) 2005 Alfresco, Inc.
 *
 * Licensed under the Mozilla Public License version 1.1 
 * with a permitted attribution clause. You may obtain a
 * copy of the License at
 *
 *   http://www.alfresco.org/legal/license.txt
 *
 * 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.alfresco.filesys.smb.server;

import java.io.IOException;

import org.alfresco.filesys.netbios.RFCNetBIOSProtocol;
import org.alfresco.filesys.server.filesys.TreeConnection;
import org.alfresco.filesys.smb.DataType;
import org.alfresco.filesys.smb.PacketType;
import org.alfresco.filesys.smb.SMBStatus;
import org.alfresco.filesys.smb.TransactBuffer;
import org.alfresco.filesys.smb.dcerpc.DCEBuffer;
import org.alfresco.filesys.smb.dcerpc.DCEBufferException;
import org.alfresco.filesys.smb.dcerpc.DCECommand;
import org.alfresco.filesys.smb.dcerpc.DCEDataPacker;
import org.alfresco.filesys.smb.dcerpc.DCEPipeType;
import org.alfresco.filesys.smb.dcerpc.UUID;
import org.alfresco.filesys.smb.dcerpc.server.DCEPipeFile;
import org.alfresco.filesys.smb.dcerpc.server.DCESrvPacket;
import org.alfresco.filesys.util.DataBuffer;
import org.alfresco.filesys.util.DataPacker;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * DCE/RPC Protocol Handler Class
 */
public class DCERPCHandler
{
    private static final Log logger = LogFactory.getLog("org.alfresco.smb.protocol");

    /**
     * Process a DCE/RPC request
     * 
     * @param sess SMBSrvSession
     * @param srvTrans SMBSrvTransPacket
     * @param outPkt SMBSrvPacket
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void processDCERPCRequest(SMBSrvSession sess, SMBSrvTransPacket srvTrans, SMBSrvPacket outPkt)
            throws IOException, SMBSrvException
    {

        // Get the tree id from the received packet and validate that it is a valid
        // connection id.

        int treeId = srvTrans.getTreeId();
        TreeConnection conn = sess.findConnection(treeId);

        if (conn == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos);
            return;
        }

        // Get the file id and validate

        int fid = srvTrans.getSetupParameter(1);
        int maxData = srvTrans.getParameter(3) - DCEBuffer.OPERATIONDATA;

        // Get the IPC pipe file for the specified file id

        DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid);
        if (pipeFile == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos);
            return;
        }

        // Create a DCE/RPC buffer from the received data

        DCEBuffer dceBuf = new DCEBuffer(srvTrans.getBuffer(), srvTrans.getParameter(10)
                + RFCNetBIOSProtocol.HEADER_LEN);

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
            logger.debug("TransactNmPipe pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", dceCmd=0x"
                    + Integer.toHexString(dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE)));

        // Process the received DCE buffer

        processDCEBuffer(sess, dceBuf, pipeFile);

        // Check if there is a reply buffer to return to the caller

        if (pipeFile.hasBufferedData() == false)
            return;

        DCEBuffer txBuf = pipeFile.getBufferedData();

        // Initialize the reply

        DCESrvPacket dcePkt = new DCESrvPacket(outPkt.getBuffer());

        // Always only one fragment as the data either fits into the first reply fragment or the
        // client will read the remaining data by issuing read requests on the pipe

        int flags = DCESrvPacket.FLG_ONLYFRAG;

        dcePkt.initializeDCEReply();
        txBuf.setHeaderValue(DCEBuffer.HDR_FLAGS, flags);

        // Build the reply data

        byte[] buf = dcePkt.getBuffer();
        int pos = DCEDataPacker.longwordAlign(dcePkt.getByteOffset());

        // Set the DCE fragment size and send the reply DCE/RPC SMB

        int dataLen = txBuf.getLength();
        txBuf.setHeaderValue(DCEBuffer.HDR_FRAGLEN, dataLen);

        // Copy the data from the DCE output buffer to the reply SMB packet

        int len = txBuf.getLength();
        int sts = SMBStatus.NTSuccess;

        if (len > maxData)
        {

            // Write the maximum transmit fragment to the reply

            len = maxData + DCEBuffer.OPERATIONDATA;
            dataLen = maxData + DCEBuffer.OPERATIONDATA;

            // Indicate a buffer overflow status

            sts = SMBStatus.NTBufferOverflow;
        }
        else
        {

            // Clear the DCE/RPC pipe buffered data, the reply will fit into a single response
            // packet

            pipeFile.setBufferedData(null);
        }

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
            logger.debug("Reply DCEbuf flags=0x" + Integer.toHexString(flags) + ", len=" + len + ", status=0x"
                    + Integer.toHexString(sts));

        // Copy the reply data to the reply packet

        try
        {
            pos += txBuf.copyData(buf, pos, len);
        }
        catch (DCEBufferException ex)
        {
            sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv);
            return;
        }

        // Set the SMB transaction data length

        int byteLen = pos - dcePkt.getByteOffset();
        dcePkt.setParameter(1, dataLen);
        dcePkt.setParameter(6, dataLen);
        dcePkt.setByteCount(byteLen);
        dcePkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE);
        dcePkt.setLongErrorCode(sts);

        sess.sendResponseSMB(dcePkt);
    }

    /**
     * Process a DCE/RPC request
     * 
     * @param sess SMBSrvSession
     * @param tbuf TransactBuffer
     * @param outPkt SMBSrvPacket
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void processDCERPCRequest(SMBSrvSession sess, TransactBuffer tbuf, SMBSrvPacket outPkt)
            throws IOException, SMBSrvException
    {

        // Check if the transaction buffer has setup and data buffers

        if (tbuf.hasSetupBuffer() == false || tbuf.hasDataBuffer() == false)
        {
            sess.sendErrorResponseSMB(SMBStatus.SRVUnrecognizedCommand, SMBStatus.ErrSrv);
            return;
        }

        // Get the tree id from the received packet and validate that it is a valid
        // connection id.

        int treeId = tbuf.getTreeId();
        TreeConnection conn = sess.findConnection(treeId);

        if (conn == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos);
            return;
        }

        // Get the file id and validate

        DataBuffer setupBuf = tbuf.getSetupBuffer();

        setupBuf.skipBytes(2);
        int fid = setupBuf.getShort();
        int maxData = tbuf.getReturnDataLimit() - DCEBuffer.OPERATIONDATA;

        // Get the IPC pipe file for the specified file id

        DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid);
        if (pipeFile == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos);
            return;
        }

        // Create a DCE/RPC buffer from the received transaction data

        DCEBuffer dceBuf = new DCEBuffer(tbuf);

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
            logger.debug("TransactNmPipe pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", dceCmd=0x"
                    + Integer.toHexString(dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE)));

        // Process the received DCE buffer

        processDCEBuffer(sess, dceBuf, pipeFile);

        // Check if there is a reply buffer to return to the caller

        if (pipeFile.hasBufferedData() == false)
            return;

        DCEBuffer txBuf = pipeFile.getBufferedData();

        // Initialize the reply

        DCESrvPacket dcePkt = new DCESrvPacket(outPkt.getBuffer());

        // Always only one fragment as the data either fits into the first reply fragment or the
        // client will read the remaining data by issuing read requests on the pipe

        int flags = DCESrvPacket.FLG_ONLYFRAG;

        dcePkt.initializeDCEReply();
        txBuf.setHeaderValue(DCEBuffer.HDR_FLAGS, flags);

        // Build the reply data

        byte[] buf = dcePkt.getBuffer();
        int pos = DCEDataPacker.longwordAlign(dcePkt.getByteOffset());

        // Set the DCE fragment size and send the reply DCE/RPC SMB

        int dataLen = txBuf.getLength();
        txBuf.setHeaderValue(DCEBuffer.HDR_FRAGLEN, dataLen);

        // Copy the data from the DCE output buffer to the reply SMB packet

        int len = txBuf.getLength();
        int sts = SMBStatus.NTSuccess;

        if (len > maxData)
        {

            // Write the maximum transmit fragment to the reply

            len = maxData + DCEBuffer.OPERATIONDATA;
            dataLen = maxData + DCEBuffer.OPERATIONDATA;

            // Indicate a buffer overflow status

            sts = SMBStatus.NTBufferOverflow;
        }
        else
        {

            // Clear the DCE/RPC pipe buffered data, the reply will fit into a single response
            // packet

            pipeFile.setBufferedData(null);
        }

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
            logger.debug("Reply DCEbuf flags=0x" + Integer.toHexString(flags) + ", len=" + len + ", status=0x"
                    + Integer.toHexString(sts));

        // Copy the reply data to the reply packet

        try
        {
            pos += txBuf.copyData(buf, pos, len);
        }
        catch (DCEBufferException ex)
        {
            sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv);
            return;
        }

        // Set the SMB transaction data length

        int byteLen = pos - dcePkt.getByteOffset();
        dcePkt.setParameter(1, dataLen);
        dcePkt.setParameter(6, dataLen);
        dcePkt.setByteCount(byteLen);
        dcePkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE);
        dcePkt.setLongErrorCode(sts);

        sess.sendResponseSMB(dcePkt);
    }

    /**
     * Process a DCE/RPC write request to the named pipe file
     * 
     * @param sess SMBSrvSession
     * @param inPkt SMBSrvPacket
     * @param outPkt SMBSrvPacket
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void processDCERPCRequest(SMBSrvSession sess, SMBSrvPacket inPkt, SMBSrvPacket outPkt)
            throws IOException, SMBSrvException
    {

        // Get the tree id from the received packet and validate that it is a valid
        // connection id.

        int treeId = inPkt.getTreeId();
        TreeConnection conn = sess.findConnection(treeId);

        if (conn == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos);
            return;
        }

        // Determine if this is a write or write andX request

        int cmd = inPkt.getCommand();

        // Get the file id and validate

        int fid = -1;
        if (cmd == PacketType.WriteFile)
            fid = inPkt.getParameter(0);
        else
            fid = inPkt.getParameter(2);

        // Get the IPC pipe file for the specified file id

        DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid);
        if (pipeFile == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos);
            return;
        }

        // Create a DCE buffer for the received data

        DCEBuffer dceBuf = null;
        byte[] buf = inPkt.getBuffer();
        int pos = 0;
        int len = 0;

        if (cmd == PacketType.WriteFile)
        {

            // Get the data offset

            pos = inPkt.getByteOffset();

            // Check that the received data is valid

            if (buf[pos++] != DataType.DataBlock)
            {
                sess.sendErrorResponseSMB(SMBStatus.DOSInvalidData, SMBStatus.ErrDos);
                return;
            }

            len = DataPacker.getIntelShort(buf, pos);
            pos += 2;

        }
        else
        {

            // Get the data offset and length

            len = inPkt.getParameter(10);
            pos = inPkt.getParameter(11) + RFCNetBIOSProtocol.HEADER_LEN;
        }

        // Create a DCE buffer mapped to the received packet

        dceBuf = new DCEBuffer(buf, pos);

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC))
            logger.debug("Write pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", dceCmd=0x"
                    + Integer.toHexString(dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE)));

        // Process the DCE buffer

        processDCEBuffer(sess, dceBuf, pipeFile);

        // Check if there is a valid reply buffered

        int bufLen = 0;
        if (pipeFile.hasBufferedData())
            bufLen = pipeFile.getBufferedData().getLength();

        // Send the write/write andX reply

        if (cmd == PacketType.WriteFile)
        {

            // Build the write file reply

            outPkt.setParameterCount(1);
            outPkt.setParameter(0, len);
            outPkt.setByteCount(0);
        }
        else
        {

            // Build the write andX reply

            outPkt.setParameterCount(6);

            outPkt.setAndXCommand(0xFF);
            outPkt.setParameter(1, 0);
            outPkt.setParameter(2, len);
            outPkt.setParameter(3, bufLen);
            outPkt.setParameter(4, 0);
            outPkt.setParameter(5, 0);
            outPkt.setByteCount(0);
        }

        // Send the write reply

        outPkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE);
        sess.sendResponseSMB(outPkt);
    }

    /**
     * Process a DCE/RPC pipe read request
     * 
     * @param sess SMBSrvSession
     * @param inPkt SMBSrvPacket
     * @param outPkt SMBSrvPacket
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void processDCERPCRead(SMBSrvSession sess, SMBSrvPacket inPkt, SMBSrvPacket outPkt)
            throws IOException, SMBSrvException
    {

        // Get the tree id from the received packet and validate that it is a valid
        // connection id.

        int treeId = inPkt.getTreeId();
        TreeConnection conn = sess.findConnection(treeId);

        if (conn == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidDrive, SMBStatus.ErrDos);
            return;
        }

        // Determine if this is a read or read andX request

        int cmd = inPkt.getCommand();

        // Get the file id and read length, and validate

        int fid = -1;
        int rdLen = -1;

        if (cmd == PacketType.ReadFile)
        {
            fid = inPkt.getParameter(0);
            rdLen = inPkt.getParameter(1);
        }
        else
        {
            fid = inPkt.getParameter(2);
            rdLen = inPkt.getParameter(5);
        }

        // Get the IPC pipe file for the specified file id

        DCEPipeFile pipeFile = (DCEPipeFile) conn.findFile(fid);
        if (pipeFile == null)
        {
            sess.sendErrorResponseSMB(SMBStatus.DOSInvalidHandle, SMBStatus.ErrDos);
            return;
        }

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC))
            logger.debug("Read pipeFile=" + pipeFile.getName() + ", fid=" + fid + ", rdLen=" + rdLen);

        // Check if there is a valid reply buffered

        if (pipeFile.hasBufferedData())
        {

            // Get the buffered data

            DCEBuffer bufData = pipeFile.getBufferedData();
            int bufLen = bufData.getAvailableLength();

            // Debug

            if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC))
                logger.debug("  Buffered data available=" + bufLen);

            // Check if there is less data than the read size

            if (rdLen > bufLen)
                rdLen = bufLen;

            // Build the read response

            if (cmd == PacketType.ReadFile)
            {

                // Build the read response

                outPkt.setParameterCount(5);
                outPkt.setParameter(0, rdLen);
                for (int i = 1; i < 5; i++)
                    outPkt.setParameter(i, 0);
                outPkt.setByteCount(rdLen + 3);

                // Copy the data to the response

                byte[] buf = outPkt.getBuffer();
                int pos = outPkt.getByteOffset();

                buf[pos++] = (byte) DataType.DataBlock;
                DataPacker.putIntelShort(rdLen, buf, pos);
                pos += 2;

                try
                {
                    bufData.copyData(buf, pos, rdLen);
                }
                catch (DCEBufferException ex)
                {
                    logger.error("DCR/RPC read", ex);
                }
            }
            else
            {

                // Build the read andX response

                outPkt.setParameterCount(12);
                outPkt.setAndXCommand(0xFF);
                for (int i = 1; i < 12; i++)
                    outPkt.setParameter(i, 0);

                // Copy the data to the response

                byte[] buf = outPkt.getBuffer();
                int pos = DCEDataPacker.longwordAlign(outPkt.getByteOffset());

                outPkt.setParameter(5, rdLen);
                outPkt.setParameter(6, pos - RFCNetBIOSProtocol.HEADER_LEN);
                outPkt.setByteCount((pos + rdLen) - outPkt.getByteOffset());

                try
                {
                    bufData.copyData(buf, pos, rdLen);
                }
                catch (DCEBufferException ex)
                {
                    logger.error("DCE/RPC error", ex);
                }
            }
        }
        else
        {

            // Debug

            if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_IPC))
                logger.debug("  No buffered data available");

            // Return a zero length read response

            if (cmd == PacketType.ReadFile)
            {

                // Initialize the read response

                outPkt.setParameterCount(5);
                for (int i = 0; i < 5; i++)
                    outPkt.setParameter(i, 0);
                outPkt.setByteCount(0);
            }
            else
            {

                // Return a zero length read andX response

                outPkt.setParameterCount(12);

                outPkt.setAndXCommand(0xFF);
                for (int i = 1; i < 12; i++)
                    outPkt.setParameter(i, 0);
                outPkt.setByteCount(0);
            }
        }

        // Clear the status code

        outPkt.setLongErrorCode(SMBStatus.NTSuccess);

        // Send the read reply

        outPkt.setFlags2(SMBPacket.FLG2_LONGERRORCODE);
        sess.sendResponseSMB(outPkt);
    }

    /**
     * Process the DCE/RPC request buffer
     * 
     * @param sess SMBSrvSession
     * @param buf DCEBuffer
     * @param pipeFile DCEPipeFile
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void processDCEBuffer(SMBSrvSession sess, DCEBuffer dceBuf, DCEPipeFile pipeFile)
            throws IOException, SMBSrvException
    {

        // Process the DCE/RPC request

        switch (dceBuf.getHeaderValue(DCEBuffer.HDR_PDUTYPE))
        {

        // DCE Bind

        case DCECommand.BIND:
            procDCEBind(sess, dceBuf, pipeFile);
            break;

        // DCE Request

        case DCECommand.REQUEST:
            procDCERequest(sess, dceBuf, pipeFile);
            break;

        default:
            sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv);
            break;
        }
    }

    /**
     * Process a DCE bind request
     * 
     * @param sess SMBSrvSession
     * @param dceBuf DCEBuffer
     * @param pipeFile DCEPipeFile
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void procDCEBind(SMBSrvSession sess, DCEBuffer dceBuf, DCEPipeFile pipeFile)
            throws IOException, SMBSrvException
    {

        try
        {

            // DEBUG

            if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
                logger.debug("DCE Bind");

            // Get the call id and skip the DCE header

            int callId = dceBuf.getHeaderValue(DCEBuffer.HDR_CALLID);
            dceBuf.skipBytes(DCEBuffer.DCEDATA);

            // Unpack the bind request

            int maxTxSize = dceBuf.getShort();
            int maxRxSize = dceBuf.getShort();
            int groupId = dceBuf.getInt();
            int ctxElems = dceBuf.getByte(DCEBuffer.ALIGN_INT);
            int presCtxId = dceBuf.getByte(DCEBuffer.ALIGN_SHORT);
            int trfSyntax = dceBuf.getByte(DCEBuffer.ALIGN_SHORT);

            UUID uuid1 = dceBuf.getUUID(true);
            UUID uuid2 = dceBuf.getUUID(true);

            // Debug

            if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
            {
                logger.debug("Bind: maxTx=" + maxTxSize + ", maxRx=" + maxRxSize + ", groupId=" + groupId
                        + ", ctxElems=" + ctxElems + ", presCtxId=" + presCtxId + ", trfSyntax=" + trfSyntax);
                logger.debug("      uuid1=" + uuid1.toString());
                logger.debug("      uuid2=" + uuid2.toString());
            }

            // Update the IPC pipe file

            pipeFile.setMaxTransmitFragmentSize(maxTxSize);
            pipeFile.setMaxReceiveFragmentSize(maxRxSize);

            // Create an output DCE buffer for the reply and add the bind acknowledge header

            DCEBuffer txBuf = new DCEBuffer();
            txBuf.putBindAckHeader(dceBuf.getHeaderValue(DCEBuffer.HDR_CALLID));
            txBuf.setHeaderValue(DCEBuffer.HDR_FLAGS, DCEBuffer.FLG_ONLYFRAG);

            // Pack the bind acknowledge DCE reply

            txBuf.putShort(maxTxSize);
            txBuf.putShort(maxRxSize);
            txBuf.putInt(0x53F0);

            String srvPipeName = DCEPipeType.getServerPipeName(pipeFile.getPipeId());
            txBuf.putShort(srvPipeName.length() + 1);
            txBuf.putASCIIString(srvPipeName, true, DCEBuffer.ALIGN_INT);
            txBuf.putInt(1);
            txBuf.putShort(0);
            txBuf.putShort(0);
            txBuf.putUUID(uuid2, true);

            txBuf.setHeaderValue(DCEBuffer.HDR_FRAGLEN, txBuf.getLength());

            // Attach the reply buffer to the pipe file

            pipeFile.setBufferedData(txBuf);
        }
        catch (DCEBufferException ex)
        {
            sess.sendErrorResponseSMB(SMBStatus.SRVNotSupported, SMBStatus.ErrSrv);
            return;
        }
    }

    /**
     * Process a DCE request
     * 
     * @param sess SMBSrvSession
     * @param dceBuf DCEBuffer
     * @param pipeFile DCEPipeFile
     * @exception IOException
     * @exception SMBSrvException
     */
    public static final void procDCERequest(SMBSrvSession sess, DCEBuffer inBuf, DCEPipeFile pipeFile)
            throws IOException, SMBSrvException
    {

        // Debug

        if (logger.isDebugEnabled() && sess.hasDebug(SMBSrvSession.DBG_DCERPC))
            logger.debug("DCE Request opNum=0x" + Integer.toHexString(inBuf.getHeaderValue(DCEBuffer.HDR_OPCODE)));

        // Pass the request to the DCE pipe request handler

        if (pipeFile.hasRequestHandler())
            pipeFile.getRequestHandler().processRequest(sess, inBuf, pipeFile);
        else
            sess.sendErrorResponseSMB(SMBStatus.SRVNoAccessRights, SMBStatus.ErrSrv);
    }
}
