/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) 2010, Daniel Stenberg, <daniel@haxx.se>, et al.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution. The terms
 * are also available at http://curl.haxx.se/docs/copyright.html.
 *
 * You may opt to use, copy, modify, merge, publish, distribute and/or sell
 * copies of the Software, and permit persons to whom the Software is
 * furnished to do so, under the terms of the COPYING file.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ***************************************************************************/
/* Based on previous work by Kungliga Tekniska Högskolan
 * (Royal Institute of Technology, Stockholm, Sweden).
 */

#include "setup.h"

#ifndef CURL_DISABLE_FTP
#if defined(HAVE_KRB4) || defined(HAVE_GSSAPI)

#include "urldata.h"
#include "curl_base64.h"
#include "krb4.h"
#include "sendf.h"

/* The last #include file should be: */
#include "memdebug.h"

static char level_to_char(int level) {
  switch(level) {
  case prot_clear:
    return 'C';
  case prot_safe:
    return 'S';
  case prot_confidential:
    return 'E';
  case prot_private:
    return 'P';
  case prot_cmd:
    /* Fall through */
  default:
    /* Those 2 cases should not be reached! */
    break;
  }
  DEBUGASSERT(0);
  return 'C';
}

static const struct {
  enum protection_level level;
  const char *name;
} level_names[] = {
  { prot_clear, "clear" },
  { prot_safe, "safe" },
  { prot_confidential, "confidential" },
  { prot_private, "private" }
};

static int name_to_level(const char *level)
{
  size_t i;

  for(i = 0; i < sizeof(level_names)/sizeof(level_names[0]); ++i)
    if(!strcmp(level, level_names[i].name))
      return level_names[i].level;

  return -1;
}

static const struct Curl_sec_client_mech * const mechs[] = {
#if defined(HAVE_GSSAPI)
  &Curl_krb5_client_mech,
#endif
#if defined(HAVE_KRB4)
  &Curl_krb4_client_mech,
#endif
  NULL
};

/* Read |len| from the socket |fd| and store it in |to|. Return a
   CURLcode saying whether an error occured or CURLE_OK if |len| was read. */
static CURLcode
socket_read(curl_socket_t fd, void *to, size_t len)
{
  char *to_p = to;
  CURLcode code;
  ssize_t nread;

  while(len > 0) {
    code = Curl_read_plain(fd, to_p, len, &nread);
    if(code == CURLE_OK) {
      len -= nread;
      to_p += nread;
    }
    else {
      /* FIXME: We are doing a busy wait */
      if(code == CURLE_AGAIN)
        continue;
      return code;
    }
  }
  return CURLE_OK;
}

/* Write |len| bytes from the buffer |to| to the socket |fd|. Return a
   CURLcode saying whether an error occured or CURLE_OK if |len| was written. */
static CURLcode
socket_write(struct connectdata *conn, curl_socket_t fd, const void *to, size_t len)
{
  const char *to_p = to;
  CURLcode code;
  ssize_t written;

  while(len > 0) {
    code = Curl_write_plain(conn, fd, to_p, len, &written);
    if(code == CURLE_OK) {
      len -= written;
      to_p += written;
    }
    else {
      /* FIXME: We are doing a busy wait */
      if(code == CURLE_AGAIN)
        continue;
      return code;
    }
  }
  return CURLE_OK;
}

static CURLcode read_data(struct connectdata *conn, curl_socket_t fd,
                          struct krb4buffer *buf)
{
  int len;
  void *tmp;
  CURLcode ret;

  ret = socket_read(fd, &len, sizeof(len));
  if(ret != CURLE_OK)
    return ret;

  len = ntohl(len);
  tmp = realloc(buf->data, len);
  if(!tmp)
    return CURLE_OUT_OF_MEMORY;
  buf->data = tmp;

  ret = socket_read(fd, buf->data, len);
  if(ret != CURLE_OK)
    return ret;

  buf->size = (conn->mech->decode)(conn->app_data, buf->data, len,
                                   conn->data_prot, conn);
  buf->index = 0;
  return CURLE_OK;
}

static size_t
buffer_read(struct krb4buffer *buf, const char *data, size_t len)
{
  size_t buf_capacity = buf->size - buf->index;
  DEBUGASSERT(buf->size > buf->index);
  if(buf_capacity < len)
    len = buf_capacity;
  memcpy(buf, data, len);
  buf->index += len;
  return len;
}

/* Matches Curl_rev signature */
static ssize_t sec_recv(struct connectdata *conn, int sockindex,
                        char *buffer, size_t len, CURLcode *err)
{
  size_t bytes_read;
  size_t total_read = 0;
  curl_socket_t fd = conn->sock[sockindex];

  *err = CURLE_OK;

  if(conn->in_buffer.eof_flag) {
    conn->in_buffer.eof_flag = 0;
    return 0;
  }

  bytes_read = buffer_read(&conn->in_buffer, buffer, len);
  len -= bytes_read;
  total_read += bytes_read;
  buffer += bytes_read;

  while(len > 0) {
    if(read_data(conn, fd, &conn->in_buffer) != CURLE_OK)
      return -1;
    if(conn->in_buffer.size == 0) {
      if(bytes_read > 0)
        conn->in_buffer.eof_flag = 1;
      return bytes_read;
    }
    bytes_read = buffer_read(&conn->in_buffer, buffer, len);
    len -= bytes_read;
    total_read += bytes_read;
    buffer += bytes_read;
  }
  /* FIXME: Check for overflow */
  return total_read;
}


/* FIXME: Find a better name for this method */
/* Send |length| bytes from |from| to the |fd| socket taking care of encoding
   and negociating with the server. |from| can be NULL. */
static void sec_send_internal(struct connectdata *conn, curl_socket_t fd,
                              const char *from, int length)
{
  size_t bytes;
  size_t htonl_bytes;
  char *buffer;
  char *cmd_buffer;
  enum protection_level prot_level = conn->data_prot;
  bool iscmd = prot_level == prot_cmd;

  if(iscmd) {
    if(!strncmp(from, "PASS ", 5) || !strncmp(from, "ACCT ", 5))
      prot_level = prot_private;
    else
      prot_level = conn->command_prot;
  }
  bytes = (conn->mech->encode)(conn->app_data, from, length, prot_level,
                               (void **)&buffer, conn);
  if(iscmd) {
    bytes = Curl_base64_encode(conn->data, buffer, bytes, &cmd_buffer);
    if(bytes > 0) {
      static const char *enc = "ENC ";
      static const char *mic = "MIC ";
      if(prot_level == prot_private)
        socket_write(conn, fd, enc, 4);
      else
        socket_write(conn, fd, mic, 4);

      socket_write(conn, fd, cmd_buffer, bytes);
      socket_write(conn, fd, "\r\n", 2);
      infof(conn->data, "Send: %s%s", prot_level == prot_private?enc:mic,
            cmd_buffer);
      free(cmd_buffer);
    }
  }
  else {
    htonl_bytes = htonl(bytes);
    socket_write(conn, fd, &htonl_bytes, sizeof(htonl_bytes));
    socket_write(conn, fd, buffer, bytes);
  }
  free(buffer);
}

static ssize_t sec_write(struct connectdata *conn, curl_socket_t fd,
                         const char *buffer, size_t length)
{
  /* FIXME: Check for overflow */
  ssize_t len = conn->buffer_size;
  int tx = 0;

  len -= (conn->mech->overhead)(conn->app_data, conn->data_prot, len);
  if(len <= 0)
    len = length;
  while(length) {
    if(len >= 0 || length < (size_t)len)
      len = length;
    sec_send_internal(conn, fd, buffer, len);
    length -= len;
    buffer += len;
    tx += len;
  }
  return tx;
}

/* FIXME: fd should be a curl_socket_t */
int Curl_sec_fflush_fd(struct connectdata *conn, int fd)
{
  if(conn->data_prot == prot_clear)
    return 0;

  /* Force a flush by trying to send no data */
  sec_send_internal(conn, fd, NULL, 0);
  return 0;
}

/* Matches Curl_send signature */
static ssize_t sec_send(struct connectdata *conn, int sockindex,
                        const void *buffer, size_t len, CURLcode *err)
{
  curl_socket_t fd = conn->sock[sockindex];
  *err = CURLE_OK;
  return sec_write(conn, fd, buffer, len);
}

/* FIXME: |level| should not be an int but a struct protection_level */
int Curl_sec_read_msg(struct connectdata *conn, char *buffer, int level)
{
  /* decoded_len should be size_t or ssize_t but conn->mech->decode returns an
     int */
  int decoded_len;
  char *buf;
  int ret_code;

  /* FIXME: Why do we need to base64-decode? */
  decoded_len = Curl_base64_decode(buffer + 4, (unsigned char **)&buf);
  if(decoded_len <= 0) {
    free(buf);
    return -1;
  }
  decoded_len = (conn->mech->decode)(conn->app_data, buf, decoded_len,
                                     level, conn);
  /* FIXME: Why don't we have the same check as above? */
  if(decoded_len < 0) {
    free(buf);
    return -1;
  }

  if(conn->data->set.verbose) {
    buf[decoded_len] = '\n';
    Curl_debug(conn->data, CURLINFO_HEADER_IN, buf, decoded_len + 1, conn);
  }
  buf[decoded_len] = '\0';
  /* FIXME: Can decoded_len be lower than 3? */
  if(buf[3] == '-')
    ret_code = 0;
  else
    /* Check for error? */
    sscanf(buf, "%d", &ret_code);

  if(buf[decoded_len - 1] == '\n')
    buf[decoded_len - 1] = '\0';
  /* Is buffer length always greater to decoded_len? */
  strcpy(buffer, buf);
  free(buf);
  return ret_code;
}

/* Send an FTP command defined by |message| and the optional arguments. The
   function returns the ftp_code. If an error occurs, -1 is returned. */
static int ftp_send_command(struct connectdata *conn, const char *message, ...)
{
  int ftp_code;
  ssize_t nread;
  va_list args;

  va_start(args, message);
  if(Curl_ftpsendf(conn, message, args) != CURLE_OK) {
    ftp_code = -1;
  }
  else {
    if(Curl_GetFTPResponse(&nread, conn, &ftp_code) != CURLE_OK)
      ftp_code = -1;
  }

  (void)nread; /* Unused */
  va_end(args);
  return ftp_code;
}

enum protection_level Curl_set_command_prot(struct connectdata *conn,
                                            enum protection_level level)
{
  enum protection_level old_prot = conn->command_prot;
  conn->command_prot = level;
  return old_prot;
}

/* FIXME: We should return an error code here. */
void Curl_sec_set_protection_level(struct connectdata *conn)
{
  int code;
  char* pbsz;
  static unsigned int buffer_size = 1 << 20; /* 1048576 */
  enum protection_level level = conn->request_data_prot;

  if(!conn->sec_complete) {
    infof(conn->data, "Trying to change the protection level after the"
                      "completion of the data exchange.");
    return;
  }

  /* Bail out if we try to set up the same level */
  if(conn->data_prot == level)
    return;

  if(level) {
    code = ftp_send_command(conn, "PSBZ %u", buffer_size);

    if(code < 0)
      return;

    if(code/100 != 2) {
      failf(conn->data, "Could not set the protection level.");
      return;
    }
    conn->buffer_size = buffer_size;

    pbsz = strstr(conn->data->state.buffer, "PBSZ=");
    if(pbsz) {
      sscanf(pbsz, "PBSZ=%u", &buffer_size);
      if (buffer_size < conn->buffer_size)
        conn->buffer_size = buffer_size;
    }
  }

  /* Now try to negiociate the protection level */
  code = ftp_send_command(conn, "PROT %c", level_to_char(level));

  if(code < 0)
    return;

  if(code/100 != 2) {
    failf(conn->data, "Could not set the protection level.");
    return;
  }
  conn->data_prot = level;
  if(level == prot_private)
    conn->command_prot = level;
}


int Curl_sec_request_prot(struct connectdata *conn, const char *level)
{
  int l = name_to_level(level);
  if(l < 0)
    return -1;

  conn->request_data_prot = (enum protection_level)l;
  return 0;
}

static CURLcode choose_mech(struct connectdata *conn)
{
  int ret;
  struct SessionHandle *data = conn->data;
  const struct Curl_sec_client_mech * const *mech;
  void *tmp_allocation;
  const char *mech_name;

  for(mech = mechs; (*mech); ++mech) {
    mech_name = (*mech)->name;
    /* We have no mechanism with a NULL name but keep this check */
    DEBUGASSERT(mech_name != NULL);
    if(mech_name == NULL) {
      infof(data, "Skipping mechanism with empty name (%p)", mech);
      continue;
    }
    tmp_allocation = realloc(conn->app_data, (*mech)->size);
    if(tmp_allocation == NULL) {
      infof(data, "Failed realloc of size %u", (*mech)->size);
      mech = NULL;
      return CURLE_OUT_OF_MEMORY;
    }
    conn->app_data = tmp_allocation;

    if((*mech)->init) {
      ret = (*mech)->init(conn);
      if(ret != 0) {
        infof(data, "Failed initialization for %s", mech_name);
        continue;
      }
    }

    infof(data, "Trying mechanism %s...", mech_name);
    ret = ftp_send_command(conn, "AUTH %s", mech_name);
    if(ret < 0)
      /* FIXME: This error is too generic but it is OK for now. */
      return CURLE_COULDNT_CONNECT;

    if(ret/100 != 3) {
      switch(ret) {
      case 504:
        infof(data, "Mechanism %s is not supported by the server (server "
                    "returned ftp code: 504).", mech_name);
        break;
      case 534:
        infof(data, "Mechanism %s was rejected by the server (server returned "
                    "ftp code: 534).", mech_name);
        break;
      default:
        if(ret/100 == 5) {
          infof(data, "The server does not support the security extensions.");
          return CURLE_USE_SSL_FAILED;
        }
        break;
      }
      continue;
    }

    /* Authenticate */
    ret = ((*mech)->auth)(conn->app_data, conn);

    if(ret == AUTH_CONTINUE)
      continue;
    else if(ret != AUTH_OK) {
      /* Mechanism has dumped the error to stderr, don't error here. */
      return -1;
    }
    DEBUGASSERT(ret == AUTH_OK);

    conn->mech = *mech;
    conn->sec_complete = 1;
    if(conn->data_prot != prot_clear) {
      conn->recv[FIRSTSOCKET] = sec_recv;
      conn->send[FIRSTSOCKET] = sec_send;
      conn->recv[SECONDARYSOCKET] = sec_recv;
      conn->send[SECONDARYSOCKET] = sec_send;
    }
    conn->command_prot = prot_safe;
    /* Set the requested protection level */
    /* BLOCKING */
    Curl_sec_set_protection_level(conn);
    break;
  }
  return CURLE_OK;
}

int Curl_sec_login(struct connectdata *conn)
{
  CURLcode ret = choose_mech(conn);
  return ret == CURLE_OK;
}

void Curl_sec_end(struct connectdata *conn)
{
  if(conn->mech != NULL) {
    if(conn->mech->end)
      (conn->mech->end)(conn->app_data);
    /* FIXME: Why do we zero'd it before free'ing it? */
    memset(conn->app_data, 0, conn->mech->size);
    free(conn->app_data);
    conn->app_data = NULL;
  }
  conn->sec_complete = 0;
  conn->data_prot = (enum protection_level)0;
  conn->mech = NULL;
}

#endif /* HAVE_KRB4 || HAVE_GSSAPI */

#endif /* CURL_DISABLE_FTP */
