/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Communicator client code, released
 * March 31, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Samir Gehani <sgehani@netscape.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>

#if defined(__unix) || defined(__unix__) || defined(_AIX)
#include <sys/param.h>
#elif defined(_WINDOWS)
#include <windows.h>
#define MAXPATHLEN MAX_PATH
#elif defined(macintosh)
#define MAXPATHLEN 512
#elif defined(__OS2__)
#include <os2.h>
#define MAXPATHLEN CCHMAXPATH
#endif

#include "nsSocket.h"
#include "nsFTPConn.h"

const int kCntlPort = 21;
const int kDataPort = 20;
const int kCmdBufSize = 64 + MAXPATHLEN;
const int kRespBufSize = 1024;
const int kKilobyte = 1024;
const int kUsecsPerSec = 1000000;
const int kDlBufSize = 1024;
const int kMaxBailOnTimeOut = 50;

nsFTPConn::nsFTPConn(char *aHost, int (*aEventPumpCB)(void)) :
    mEventPumpCB(aEventPumpCB),
    mHost(aHost),
    mState(CLOSED),
    mPassive(FALSE),
    mCntlSock(NULL),
    mDataSock(NULL)
{
}

nsFTPConn::nsFTPConn(char *aHost) :
    mEventPumpCB(NULL),
    mHost(aHost),
    mState(CLOSED),
    mPassive(FALSE),
    mCntlSock(NULL),
    mDataSock(NULL)
{
}

nsFTPConn::~nsFTPConn()
{
    // don't release mHost cause we don't own it
}

int
nsFTPConn::Open()
{
    int err = OK;
    char cmd[kCmdBufSize], resp[kRespBufSize];

    if (!mHost)
        return E_PARAM;
    if (mState != CLOSED)
        return E_ALREADY_OPEN;

    /* open control connection on port 21 */
    mCntlSock = new nsSocket(mHost, kCntlPort, mEventPumpCB);
    if (!mCntlSock)
        return E_MEM;
    ERR_CHECK(mCntlSock->Open());
    FlushCntlSock(mCntlSock);

    /* issue USER command on control connection */
    err = IssueCmd("USER anonymous\r\n", resp, kRespBufSize, mCntlSock);

    /* issue PASS command on control connection */
    ERR_CHECK(IssueCmd("PASS -linux@installer.sbg\r\n", resp, kRespBufSize, mCntlSock));

    mState = OPEN;

    return err;

BAIL:
    if (mCntlSock)
    {
        /* issue QUIT command on control connection */
        sprintf(cmd, "QUIT\r\n");
        IssueCmd(cmd, resp, kRespBufSize, mCntlSock);
    }
    if (mDataSock)
    {
        mDataSock->Close();
        delete mDataSock;
        mDataSock = NULL;
        FlushCntlSock(mCntlSock);
    }
    if (mCntlSock)
    {
        mCntlSock->Close();
        delete mCntlSock;
        mCntlSock = NULL;
    }
    return err;
}

int
nsFTPConn::Open(char *aHost)
{
    if (!aHost)
        return E_PARAM;

    mHost = aHost;
    return Open();
}

int
nsFTPConn::ResumeOrGet(char *aSrvPath, char *aLoclPath, int aType, 
                       int aOvWrite, FTPGetCB aCBFunc)
{
    struct stat stbuf;
    int err = OK;
    int resPos = 0;

    if (!aLoclPath)
        return E_PARAM;

    /* stat local file */
    err = stat(aLoclPath, &stbuf);
    if (err == 0)
        resPos = stbuf.st_size;

    return Get(aSrvPath, aLoclPath, aType, resPos, aOvWrite, aCBFunc);
}

int
nsFTPConn::Get(char *aSrvPath, char *aLoclPath, int aType,
               int aOvWrite, FTPGetCB aCBFunc)
{
    // deprecated API; wrapper for backwards compatibility

    return ResumeOrGet(aSrvPath, aLoclPath, aType, aOvWrite, aCBFunc);
}

int
nsFTPConn::Get(char *aSrvPath, char *aLoclPath, int aType, int aResumePos,
               int aOvWrite, FTPGetCB aCBFunc)
{
    struct stat dummy;
    int err = OK, totBytesRd = 0;
    char cmd[kCmdBufSize], resp[kRespBufSize];
    int fileSize = 0;
    FILE *loclfd = NULL;

    if (!aSrvPath || !aLoclPath)
        return E_PARAM;
    if (mState != OPEN || !mCntlSock)
        return E_NOT_OPEN;

    /* stat local path and verify aOvWrite is set if file already exists */
    err = stat(aLoclPath, &dummy);
    if (err != -1 && aOvWrite == FALSE)
        return E_CANT_OVWRITE;

    mState = GETTING;

    /* initialize data connection */
    ERR_CHECK(DataInit(mHost, kDataPort, &mDataSock));

    /* issue TYPE command on control connection */
    sprintf(cmd, "TYPE %s\r\n", aType==BINARY ? "I" : "A");
    ERR_CHECK(IssueCmd(cmd, resp, kRespBufSize, mCntlSock));
    
    /* issue SIZE command on control connection */
    sprintf(cmd, "SIZE %s\r\n", aSrvPath);
    err = IssueCmd(cmd, resp, kRespBufSize, mCntlSock); /* non-fatal */
    if (err == OK && (resp[0] == '2'))
        fileSize = atoi(&resp[4]);   // else ???

    if (aResumePos > 0)
    {
        /* issue restart command */
        sprintf(cmd, "REST %d\r\n", aResumePos);
        ERR_CHECK(IssueCmd(cmd, resp, kRespBufSize, mCntlSock));
    }

    /* issue RETR command on control connection */
    sprintf(cmd, "RETR %s\r\n", aSrvPath);
    ERR_CHECK(IssueCmd(cmd, resp, kRespBufSize, mCntlSock));

    /* get file contents on data connection */
    if (!mPassive)
        ERR_CHECK(mDataSock->SrvAccept());

    /* initialize locl file */
    if (aResumePos <= 0)
    {
        if (!(loclfd = fopen(aLoclPath, aType==BINARY ? "w+b" : "w+")))
        {
            err = E_LOCL_INIT;
            goto BAIL;
        }
    }
    else
    {
        if (!(loclfd = fopen(aLoclPath, aType==BINARY ? "r+b" : "r+")) ||
            (fseek(loclfd, aResumePos, SEEK_SET) != 0)) 
        {
            err = E_LOCL_INIT;
            goto BAIL;
        }
    }

    do 
    {
        int respBufSize = kDlBufSize;
        err = mDataSock->Recv((unsigned char *)resp, &respBufSize);
        if (err != nsSocket::E_READ_MORE && 
            err != nsSocket::E_EOF_FOUND &&
            err != nsSocket::OK)
            goto BAIL;
        totBytesRd += respBufSize;
        if (err == nsSocket::E_READ_MORE && aCBFunc)
            if ((err = aCBFunc(totBytesRd, fileSize)) == E_USER_CANCEL)
                goto BAIL;
            
        /* append to local file */
        const int wrote = fwrite((void *)resp, 1, respBufSize, loclfd);
        if (wrote != respBufSize)
        {   
            err = E_WRITE;
            goto BAIL;
        }
        if ( mEventPumpCB )
            mEventPumpCB();
    }
    while (err == nsSocket::E_READ_MORE || err == nsSocket::OK);
    if (err == nsSocket::E_EOF_FOUND)
        err = OK;

BAIL:
	if ( err != OK && err != E_USER_CANCEL ) {
		sprintf(cmd, "QUIT\r\n");
		IssueCmd(cmd, resp, kRespBufSize, mCntlSock);
    	FlushCntlSock(mCntlSock);
    }
    
    /* close locl file if open */
    if (loclfd)
        fclose(loclfd);
    if ( err == E_USER_CANCEL ) {
        return err;
    }
    /* kill data connection if it exists */
    if (mDataSock)
    {
        /* err != OK means that the file attempted to be downloaded
         * was not successfully initiated.  This means that the server
         * will not post a response when the data socket is closed.
         * Since it's not going to post a response, we don't have to
         * wait kMaxBailOnTimeOut. */
        int bailOnTimeOut = err != OK ? 1 : kMaxBailOnTimeOut;

        mDataSock->Close();
        delete mDataSock;
        mDataSock = NULL;

        /* We are expecting a response from this call to FlushCntlSock()
         * _only_ if err == OK (meaning that a file was downloaded).
         * We return only when a responce has been received, or else the next
         * file that is to be downloaded will catch this response
         * and fail. */
        FlushCntlSock(mCntlSock, bailOnTimeOut);
    }

    mState = OPEN;
    mPassive = FALSE;

    return err;
}

int 
nsFTPConn::Close()
{
    int err = OK;
    char cmd[kCmdBufSize], resp[kRespBufSize];

    /* close sockets */
    if (mCntlSock)
    {
        /* issue QUIT command on control connection */
        sprintf(cmd, "ABORT\r\n");
        IssueCmd(cmd, resp, kRespBufSize, mCntlSock);
    }
    if (mDataSock)
    {
        mDataSock->Close();
        delete mDataSock;
        mDataSock = NULL;
        FlushCntlSock(mCntlSock);
    }
    if (mCntlSock)
    {
        mCntlSock->Close();
        delete mCntlSock;
        mCntlSock = NULL;
    }

    return err;
}

int
nsFTPConn::FlushCntlSock(nsSocket *aSock, int bailOnTimeOut)
{
    int err = OK;
    char resp[kRespBufSize];
    int bailOnTimeOutCount = 0;

    /* param check */
    if (!aSock)
        return E_PARAM;

    do
    {
        // Time out value is in Usecs. This should give us 3 tries.
        const int timeout = 300000;
        ++bailOnTimeOutCount;
        int respSize = kRespBufSize;
        err = aSock->Recv((unsigned char *)resp, &respSize, timeout);
        if (err != nsSocket::OK && 
            err != nsSocket::E_READ_MORE && 
            err != nsSocket::E_EOF_FOUND)
        {
            if((err == nsSocket::E_TIMEOUT) &&
               (bailOnTimeOutCount < bailOnTimeOut))
                // wait a little longer for response
                err = nsSocket::E_READ_MORE;
            else
                goto BAIL;
        }

        DUMP(resp);
        if ( mEventPumpCB )
            if (mEventPumpCB() == E_USER_CANCEL)
                return E_USER_CANCEL;
    }
    while (err == nsSocket::E_READ_MORE);

    switch (*resp)
    {
        case '2':
            break;
        case '1':
        case '3':
            err = E_CMD_ERR;
            break;
        case '4':
        case '5':
            err = E_CMD_FAIL;
            break;
        default:
            err = E_CMD_UNEXPECTED;
            break;
    }

BAIL:
    return err;
}

int
nsFTPConn::IssueCmd(const char *aCmd, char *aResp, int aRespSize, nsSocket *aSock)
{
    int err = OK;
    int len;

    /* param check */
    if (!aSock || !aCmd || !aResp || aRespSize <= 0)
        return E_PARAM;

    /* send command */
    len = strlen(aCmd);
    ERR_CHECK(aSock->Send((unsigned char *)aCmd, &len));
    DUMP(aCmd);

    /* receive response */
    do
    {
        int respSize = aRespSize;
        err = aSock->Recv((unsigned char *)aResp, &respSize);
        if (err != nsSocket::OK && 
            err != nsSocket::E_READ_MORE && 
            err != nsSocket::E_EOF_FOUND)
            goto BAIL;
        DUMP(aResp);
        if ( mEventPumpCB )
            if (mEventPumpCB() == E_USER_CANCEL)
                return E_USER_CANCEL;
    }
    while (err == nsSocket::E_READ_MORE);

    /* alternate interpretation of err codes */
    if ( (strncmp(aCmd, "APPE", 4) == 0) ||
         (strncmp(aCmd, "LIST", 4) == 0) ||
         (strncmp(aCmd, "NLST", 4) == 0) ||
         (strncmp(aCmd, "REIN", 4) == 0) ||
         (strncmp(aCmd, "RETR", 4) == 0) ||
         (strncmp(aCmd, "STOR", 4) == 0) ||
         (strncmp(aCmd, "STOU", 4) == 0) )
    {
        switch (*aResp)
        {
            case '1':   /* exception: 100 series is OK */
            case '2':
                break;
            case '3':
                err = E_CMD_ERR;
                break;
            case '4':
            case '5':
                err = E_CMD_FAIL;
                break;
            default:
                err = E_CMD_UNEXPECTED;
                break;
        }
    }

    /* restart command case */
    else if (strncmp(aCmd, "REST", 4) == 0)
    {
        switch (*aResp)
        {
            case '1':
            case '2':
                err = E_CMD_ERR;
                break;
            case '3':
                break;
            case '4':
            case '5':
                err = E_CMD_FAIL;
                break;
            default:
                err = E_CMD_UNEXPECTED;
                break;
        }
    }

    /* quit command case */
    else if (strncmp(aCmd, "QUIT", 4) == 0)
    {
        err = OK;
        goto BAIL;
    }

    /* regular interpretation of err codes */
    else
    {
        switch (*aResp)
        {
            case '2':
                break;
            case '1':
            case '3':
                err = E_CMD_ERR;
                break;
            case '4':
            case '5':
                err = E_CMD_FAIL;
                break;
            default:
                err = E_CMD_UNEXPECTED;
                break;
        }
    }

BAIL:
    return err;
}

int
nsFTPConn::ParseAddr(char *aBuf, char **aHost, int *aPort)
{
    int err = OK;
    char *c;
    int addr[6];

    /* param check */
    if (!aBuf || !aHost || !aPort)
        return E_PARAM;

    c = aBuf + strlen("227 "); /* pass by return code */
    while (!isdigit((int)(*c)))
    {
        if (*c == '\0')
            return E_INVALID_ADDR;
        c++;
    }

    if (sscanf(c, "%d,%d,%d,%d,%d,%d", 
        &addr[0], &addr[1], &addr[2], &addr[3], &addr[4], &addr[5]) != 6)
        return E_INVALID_ADDR;

    *aHost = (char *)malloc(strlen("XXX.XXX.XXX.XXX") + 1);
    sprintf(*aHost, "%d.%d.%d.%d", addr[0], addr[1], addr[2], addr[3]);

    *aPort = ((addr[4] & 0xFF) << 8) | (addr[5] & 0xFF);

#ifdef DEBUG
    printf("%s %d: PASV response: %d,%d,%d,%d,%d,%d\n", __FILE__, __LINE__,
            addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
    printf("%s %d: Host = %s\tPort = %d\n", __FILE__, __LINE__, *aHost, *aPort);
#endif
    
    return err;
}

int
nsFTPConn::DataInit(char *aHost, int aPort, nsSocket **aSock)
{
    int err = OK;
    char cmd[kCmdBufSize], resp[kRespBufSize];
    char *srvHost = NULL; 
    int srvPort = 0;
    char *hostPort = NULL;

    /* param check */
    if (!aHost || !aSock)
        return E_PARAM;

    /* issue PASV command */
    sprintf(cmd, "PASV\r\n");
    err = IssueCmd(cmd, resp, kRespBufSize, mCntlSock);
    if (err != OK)
    {
        err = OK;
        goto ACTIVE; /* failover to active mode */
    }
       
    mPassive = TRUE;

    ERR_CHECK(ParseAddr(resp, &srvHost, &srvPort));
    *aSock = new nsSocket(srvHost, srvPort, mEventPumpCB);
    if (!*aSock)
    {
        err = E_MEM;
        goto BAIL;
    }
    
    ERR_CHECK((*aSock)->Open());

    if (srvHost) 
    {
        free(srvHost);
        srvHost = NULL;
    }

    return err;

ACTIVE:

    *aSock = new nsSocket(aHost, aPort, mEventPumpCB);
    if (!*aSock)
    {
        err = E_MEM;
        goto BAIL;
    }

    /* init data socket making it listen */
    ERR_CHECK((*aSock)->SrvOpen());

    ERR_CHECK((*aSock)->GetHostPortString(&hostPort)); // allocates
    if (!hostPort)
    {
        err = E_MEM;
        goto BAIL;
    }

    sprintf(cmd, "PORT %s\r\n", hostPort);
    ERR_CHECK(IssueCmd(cmd, resp, kRespBufSize, mCntlSock));

BAIL:
    if (mPassive && err != OK)
        mPassive = FALSE;

    if (err != OK && (*aSock))
    {
        delete *aSock;
        *aSock = NULL;
    }

    if (srvHost)
    {
        free(srvHost);
        srvHost = NULL;
    }

    if (hostPort)
    {
        free(hostPort);
        hostPort = NULL;
    }

    return err;
}

#ifdef TEST_NSFTPCONN
static struct timeval init;
int
TestFTPGetCB(int aBytesRd, int aTotal)
{
    struct timeval now;
    float rate;

    gettimeofday(&now, NULL);
    rate = nsSocket::CalcRate(&init, &now, aBytesRd);
    printf("br=%d\ttot=%d\trt=%f\tirt=%d\n",aBytesRd, aTotal, rate, (int)rate);
    return 0;
}

int
main(int argc, char **argv)
{
    int err = nsFTPConn::OK;
    nsFTPConn *conn = 0;
    char *leaf = NULL;
    
    if (argc < 2)
    {
        printf("usage: %s <host> <path/on/server>\n", argv[0]);
        exit(0);
    }

    if ((leaf = strrchr(argv[2], '/'))) leaf++;
    else leaf = argv[2];

    conn = new nsFTPConn(argv[1]);

    printf("Opening connection to %s...\n", argv[1]);
    err = conn->Open();
    if (err != nsFTPConn::OK) { printf("error: %d\n", err); exit(err); }

    printf("Getting binary file %s...\n", argv[2]);
    gettimeofday(&init, NULL);
    err = conn->Get(argv[2], leaf, nsFTPConn::BINARY, TRUE, TestFTPGetCB);
    if (err != nsFTPConn::OK) { printf("error: %d\n", err); exit(err); }

    printf("Closing connection to %s...\n", argv[1]);
    err = conn->Close();
    if (err != nsFTPConn::OK) { printf("error: %d\n", err); exit(err); }

    printf("Test successful!\n");
    exit(err);
}
#endif /* TEST_NSFTPCONN */
