// fgftpcon.cc

#include "fgftpcon.h"
#include "fgexc.h"
#include "fgstring.h"
#include "fgdlist.h"

#ifndef _FGGLOB_H
#include "fgglob.h"
#endif

#include <assert.h>

#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
// For IPTOS_THROUGHPUT
#include <netinet/ip.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <unistd.h>

const int FGFTPCon::msFTPPort = IPPORT_FTP;

const int FGFTPCon::msRespPassRequired = 331;
const int FGFTPCon::msRespBadLogin = 530;
const int FGFTPCon::msRespLoginOK = 230;
const int FGFTPCon::msRespNotFound = 550;
const int FGFTPCon::msRespTransferDone = 226;
const int FGFTPCon::msRespChangeDirOK = 250;
const int FGFTPCon::msRespCommandOK = 200;
const int FGFTPCon::msRespGoodbye = 221;
const int FGFTPCon::msRespPleaseLogin = 220;
const int FGFTPCon::msRespPassiveOK = 227;
const int FGFTPCon::msRespTransferOpen = 150;

FGFTPCon::FGFTPCon()
: mConnected(false), mCommandFD(-1), mpCommandStream(NULL),
  mDataFD(-1), mRemotePort(-1), mpDirStream(NULL),
  mRemoteAddr((unsigned long)-1)
{
}

FGFTPCon::~FGFTPCon()
{
  assert(mConnected == false);
  assert(mDataFD == -1);
}

void
FGFTPCon::Connect(const FGString& host)
{
  // Preconditions
  assert(mConnected == false);

  struct hostent resBuf;
  int thread_h_errno;
  struct hostent* pHost;
  char randomBuf[1024];
  // Look up the host
  (void)
    gethostbyname_r((const char*)host, &resBuf, randomBuf,
                    sizeof(randomBuf), &pHost, &thread_h_errno);
  if (pHost == NULL) {
    if (thread_h_errno == TRY_AGAIN) {
      throw FGException(FGException::kHostResolveRetryableFail, host);
    } else {
      throw FGException(FGException::kHostResolveFail, host);
    }
  }

  // Ok make a socket and connect it to the host's FTP port
  InternalConnect(&mCommandFD, msFTPPort, pHost->h_addr);

  mpCommandStream = fdopen(mCommandFD, "r+");

  mConnected = true;

  // Parse the FTP server's greeting. Throw exception if we're not
  // let in cleanly.
  try {
    do {
      GetResponse();

      switch (mResponse) {
      case msRespLoginOK:
        break;
      case msRespBadLogin:
        throw FGException(FGException::kAccessDenied);
      case msRespPleaseLogin:
        SendCommand("user anonymous");
        break;
      case msRespPassRequired:
      {
        FGString pword("pass ftpgrab@");
        // Fix: if the hostname has come back not fully qualified then
        // don't append it to "ftpgrab@". This is because many FTP sites
        // reject a hostname without a "." in it. For example the password
        // "ftpgrab@localhost" often fails.
        if (FGGlob::gHostName.Contains('.'))
        {
          pword += FGGlob::gHostName;
        }
        SendCommand(pword);
        break;
      }
      default:
        throw FGException(FGException::kResponseMalformed);
      }
    } while (mResponse != msRespLoginOK);

    // Set type to binary
    SendCommand("type i");
    GetResponse();

    // XXX - check response?

    // Bargain - we're connected
  }
  catch (FGException&) {
    mConnected = false;
    fclose(mpCommandStream);
    mpCommandStream = NULL;
    throw;
  }
  return;
}

void
FGFTPCon::GetResponse(void)
{
  // Preconditions
  assert(mConnected == true);
  assert(mCommandFD >= 0);

  char msgType;
  do {
    // Throws exception if overly long line encountered
    GetLine();

    int len = strlen(mBuf);

    // Need at least 4 chars - 3 digit number and carrige return
    if (len < 4 || !isdigit(mBuf[0]) || !isdigit(mBuf[1]) || !isdigit(mBuf[2])) {
      // Damn. ProFTPd appears to emit greeting lines not prefixed by
      // a numeric response code. So we must assume the line is OK
      //throw FGException(FGException::kResponseMalformed, "Bad numeric response");
      msgType = '-';
      continue;
    }

    // Expect space or "-" for more lines
    msgType = mBuf[3];

    char miniBuf[4];
    miniBuf[3] = '\0';
    strncpy(miniBuf, mBuf, 3);
    mResponse = atoi(miniBuf);

  } while (msgType == '-');
}

void
FGFTPCon::GetLine(void)
{
  // Preconditions
  assert(mConnected == true);
  assert(mpCommandStream != NULL);

  char* pRet = fgets(mBuf, sizeof(mBuf), mpCommandStream);
  if (pRet == 0)
  {
    // Oh dear
    throw FGException(FGException::kConnectLost);
  }

  if (strchr(mBuf, '\n') == NULL) {
    throw FGException(FGException::kResponseTooLong);
  }
}

void
FGFTPCon::SendCommand(const FGString& cmd) const
{
  // Preconditions
  assert(mConnected == true);
  assert(mpCommandStream != NULL);

  char cmdbuf[2048];
  snprintf(cmdbuf, sizeof(cmdbuf), "%s\r\n", (const char*)cmd);

  int numWrite = strlen(cmdbuf);
  int written = fwrite(cmdbuf, 1, numWrite, mpCommandStream);

  if (written != numWrite)
  {
    throw FGException(FGException::kConnectLost);
  }
}

void
FGFTPCon::ChangeDir(const FGString& dir)
{
  // Preconditions
  assert(mConnected == true);

  FGString command("cwd ");
  command += dir;

  SendCommand(command);

  GetResponse();

  if (mResponse == msRespNotFound) {
    throw FGException(FGException::kNoSuchDir, dir);
  } else if (mResponse != msRespChangeDirOK) {
    throw FGException(FGException::kResponseUnexpected);
  }
}

void
FGFTPCon::SetupPassivePort(void)
{
  // Preconditions
  assert(mRemotePort == -1);

  SendCommand("pasv");
  GetResponse();

  if (mResponse != msRespPassiveOK) {
    throw FGException(FGException::kResponseUnexpected);
  }

  // We have to parse a line which for example may look like
  // "227 Entering Passive Mode (127,0,0,1,4,133)"
  int a, b, c, d, e, f;

  char* pBrace = strchr(mBuf, '(');
  if (pBrace == NULL) {
    throw FGException(FGException::kResponseMalformed, "Missing brace in PASV reply");
  }

  int found = sscanf(pBrace, "(%d,%d,%d,%d,%d,%d)", &a, &b, &c, &d, &e, &f);

  if (found != 6) {
    throw FGException(FGException::kResponseMalformed, "Failed to parse 6 args in PASV reply");
  }

  mRemotePort = (e << 8) | f;

  char buf[32];
  snprintf(buf, sizeof(buf), "%d.%d.%d.%d", a, b, c, d);
  // Obsolete but never mind.. :)
  mRemoteAddr = inet_addr(buf);

  // Connect to indicated remote port and IP
  InternalConnect(&mDataFD, mRemotePort, &mRemoteAddr);

  // It's a data stream (dir listing or file) so set IP TOS
  // to maximize throughput
  int tosVal = IPTOS_THROUGHPUT;
  setsockopt(mDataFD, IPPROTO_IP, IP_TOS, &tosVal, sizeof(tosVal));
}

FGDirListing
FGFTPCon::GetDirListing(void)
{
  // Preconditions
  assert(mConnected == true);
  assert(mpCommandStream != NULL);
  assert(mDataFD == -1);
  assert(mpDirStream == NULL);

  FGDirListing ret("NOTSET");

  // Use passive mode due to laziness - it's marginally easier to code
  // as a first effort
  SetupPassivePort();

  // Get remote end to set up data transfer
  // NLST just lists filename without extra details
  // So we don't use that we use LIST instead
  SendCommand("list");
  GetResponse();

  if (mResponse != msRespTransferOpen) {
    throw FGException(FGException::kResponseUnexpected);
  }

  // Make a stream from connection
  mpDirStream = fdopen(mDataFD, "r");

  // Read off connection - 1 line, 1 file
  while (fgets(mBuf, sizeof(mBuf), mpDirStream) != NULL) {
    // Safety
    if (strlen(mBuf) == 0) {
      continue;
    }
    // Don't forget to strip any trailing \r or \n characters
    char* pEndNul = mBuf + strlen(mBuf);
    bool replaced;
    do {
      replaced = false;
      pEndNul--;
      char theChar = *pEndNul;
      if (theChar == '\r' || theChar == '\n') {
        *pEndNul = '\0';
        replaced = true;
      }
    } while (replaced && pEndNul > mBuf);

    int fSize;
    int duff;
    int day;
    char fileBuf[256];
    char duffBuf[32];
    char perms[20];
    char mon[10];
    char time[10];
    int numFound = sscanf(mBuf, "%19s %d %31s %31s %d %9s %d %9s %255s",
                          perms, &duff, duffBuf, duffBuf, &fSize,
                          mon, &day, time, fileBuf);

    if (numFound == 9) {
      FGString theFileName(fileBuf);
      bool isDir = (perms[0] == 'd');
      bool isFile = (perms[0] == '-');
      FGFileInfo theFileInfo(theFileName, fSize, isDir, isFile);

      ret.push_back(theFileInfo);
    }
  }

  // Deallocate resources
  fclose(mpDirStream);
  mpDirStream = NULL;
  mDataFD = -1;
  mRemotePort = -1;
  mRemoteAddr = (unsigned long)-1;

  // XXX check return for "transfer complete"
  GetResponse();

  return ret;
}

int
FGFTPCon::GetFile(const FGString& file)
{
  // Preconditions
  assert(mConnected == true);
  assert(mpCommandStream != NULL);
  assert(mDataFD == -1);
  assert(mpDirStream == NULL);

  // Use passive mode due to laziness - it's marginally easier to code
  // as a first effort
  SetupPassivePort();

  // Get remote end to set up data transfer
  // NLST just lists filename without extra details - date etc.
  FGString cmd("retr ");
  cmd += file;

  try {
    SendCommand(cmd);
    GetResponse();

    if (mResponse != msRespTransferOpen) {
      throw FGException(FGException::kResponseUnexpected);
    }
  }
  catch (FGException&) {
    PostFileTransfer();
    throw;
  }

  // Try and set low water mark on socket for efficency
  // (less syscalls per transfer)
  // Bah! Linux 2.2 doesn't support this
#ifdef SO_RCVLOWAT
  int val = 1024 * 64;
  (void) setsockopt(mDataFD, SOL_SOCKET, SO_RCVLOWAT, &val,
                          sizeof(val));
#endif

  return mDataFD;
}

void
FGFTPCon::Disconnect(void)
{
  assert(mConnected == true);
  assert(mDataFD == -1);
  fclose(mpCommandStream);
  mpCommandStream = NULL;
}

void
FGFTPCon::InternalConnect(int* pDestFD, int port, void* pInetAddr)
{
  // Connect to indicated remote port and IP
  // Hardcoded constant should get got from "getprotobyname(tcp)"

  *pDestFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

  if (*pDestFD == -1) {
    throw FGException(FGException::kConnectResourceAllocFail, strerror(errno));
  }

#ifdef SO_KEEPALIVE
  // Get the OS to check the remote end doesn't silently drop the connection
  int one = 1;
  setsockopt(*pDestFD, SOL_SOCKET, SO_KEEPALIVE, &one, sizeof(int));
#endif

  struct sockaddr_in addr;
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);
  memcpy(&addr.sin_addr, pInetAddr, sizeof(addr.sin_addr));
  int conRet = connect(*pDestFD, (struct sockaddr*)&addr, sizeof(addr));
  if (conRet == -1) {
    switch (errno) {
    case ECONNREFUSED:
      throw FGException(FGException::kConnectRefused);
    case ETIMEDOUT:
    case ENETUNREACH:
      throw FGException(FGException::kConnectHostUncontactable, strerror(errno));
    default:
      throw FGException(FGException::kConnectMiscFail, strerror(errno));
    }
  }
}

void
FGFTPCon::PostFileTransfer(void)
{
  // Preconditions
  assert(mConnected == true);
  assert(mDataFD != -1);

  // Close stuff before checking response otherwise an exception may
  // be thrown => resource leak
  close(mDataFD);
  mDataFD = -1;
  mRemotePort = -1;
  mRemoteAddr = (unsigned long)-1;  

  // Eat up the (hopefully!) "transfer OK" message
  GetResponse();

  // XXX verify response
}
