/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * Copyright (C) Linus Nielsen Feltzing, <linus@haxx.se>
 * Copyright (C) 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 https://curl.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.
 *
 * SPDX-License-Identifier: curl
 *
 ***************************************************************************/
#include "curl_setup.h"

#include "urldata.h"
#include "url.h"
#include "cfilters.h"
#include "progress.h"
#include "multiif.h"
#include "multi_ev.h"
#include "curl_trc.h"
#include "cshutdn.h"
#include "sigpipe.h"
#include "connect.h"
#include "select.h"
#include "curlx/strparse.h"


static void cshutdn_run_conn_handler(struct Curl_easy *data,
                                     struct connectdata *conn)
{
  if(!conn->bits.shutdown_handler) {

    if(conn->handler && conn->handler->disconnect) {
      /* Some disconnect handlers do a blocking wait on server responses.
       * FTP/IMAP/SMTP and SFTP are among them. When using the internal
       * handle, set an overall short timeout so we do not hang for the
       * default 120 seconds. */
      if(data->state.internal) {
        data->set.timeout = DEFAULT_SHUTDOWN_TIMEOUT_MS;
        (void)Curl_pgrsTime(data, TIMER_STARTOP);
      }

      /* This is set if protocol-specific cleanups should be made */
      DEBUGF(infof(data, "connection #%" FMT_OFF_T
                   ", shutdown protocol handler (aborted=%d)",
                   conn->connection_id, conn->bits.aborted));
      /* There are protocol handlers that block on retrieving
       * server responses here (FTP). Set a short timeout. */
      conn->handler->disconnect(data, conn, conn->bits.aborted);
    }

    conn->bits.shutdown_handler = TRUE;
  }
}

static void cshutdn_run_once(struct Curl_easy *data,
                             struct connectdata *conn,
                             bool *done)
{
  CURLcode r1, r2;
  bool done1, done2;

  /* We expect to be attached when called */
  DEBUGASSERT(data->conn == conn);

  cshutdn_run_conn_handler(data, conn);

  if(conn->bits.shutdown_filters) {
    *done = TRUE;
    return;
  }

  if(!conn->connect_only && Curl_conn_is_connected(conn, FIRSTSOCKET))
    r1 = Curl_conn_shutdown(data, FIRSTSOCKET, &done1);
  else {
    r1 = CURLE_OK;
    done1 = TRUE;
  }

  if(!conn->connect_only && Curl_conn_is_connected(conn, SECONDARYSOCKET))
    r2 = Curl_conn_shutdown(data, SECONDARYSOCKET, &done2);
  else {
    r2 = CURLE_OK;
    done2 = TRUE;
  }

  /* we are done when any failed or both report success */
  *done = (r1 || r2 || (done1 && done2));
  if(*done)
    conn->bits.shutdown_filters = TRUE;
}

void Curl_cshutdn_run_once(struct Curl_easy *data,
                           struct connectdata *conn,
                           bool *done)
{
  DEBUGASSERT(!data->conn);
  Curl_attach_connection(data, conn);
  cshutdn_run_once(data, conn, done);
  CURL_TRC_M(data, "[SHUTDOWN] shutdown, done=%d", *done);
  Curl_detach_connection(data);
}

void Curl_cshutdn_terminate(struct Curl_easy *data,
                            struct connectdata *conn,
                            bool do_shutdown)
{
  struct Curl_easy *admin = data;
  bool done;

  /* there must be a connection to close */
  DEBUGASSERT(conn);
  /* it must be removed from the connection pool */
  DEBUGASSERT(!conn->bits.in_cpool);
  /* the transfer must be detached from the connection */
  DEBUGASSERT(data && !data->conn);

  /* If we can obtain an internal admin handle, use that to attach
   * and terminate the connection. Some protocol will try to mess with
   * `data` during shutdown and we do not want that with a `data` from
   * the application. */
  if(data->multi && data->multi->admin)
    admin = data->multi->admin;

  Curl_attach_connection(admin, conn);

  cshutdn_run_conn_handler(admin, conn);
  if(do_shutdown) {
    /* Make a last attempt to shutdown handlers and filters, if
     * not done so already. */
    cshutdn_run_once(admin, conn, &done);
  }
  CURL_TRC_M(admin, "[SHUTDOWN] %sclosing connection #%" FMT_OFF_T,
             conn->bits.shutdown_filters ? "" : "force ",
             conn->connection_id);
  Curl_conn_close(admin, SECONDARYSOCKET);
  Curl_conn_close(admin, FIRSTSOCKET);
  Curl_detach_connection(admin);

  if(data->multi)
    Curl_multi_ev_conn_done(data->multi, data, conn);
  Curl_conn_free(admin, conn);

  if(data->multi) {
    CURL_TRC_M(data, "[SHUTDOWN] trigger multi connchanged");
    Curl_multi_connchanged(data->multi);
  }
}

static bool cshutdn_destroy_oldest(struct cshutdn *cshutdn,
                                   struct Curl_easy *data,
                                   const char *destination)
{
  struct Curl_llist_node *e;
  struct connectdata *conn;

  e = Curl_llist_head(&cshutdn->list);
  while(e) {
    conn = Curl_node_elem(e);
    if(!destination || !strcmp(destination, conn->destination))
      break;
    e = Curl_node_next(e);
  }

  if(e) {
    SIGPIPE_VARIABLE(pipe_st);
    conn = Curl_node_elem(e);
    Curl_node_remove(e);
    sigpipe_init(&pipe_st);
    sigpipe_apply(data, &pipe_st);
    Curl_cshutdn_terminate(data, conn, FALSE);
    sigpipe_restore(&pipe_st);
    return TRUE;
  }
  return FALSE;
}

bool Curl_cshutdn_close_oldest(struct Curl_easy *data,
                               const char *destination)
{
  if(data && data->multi) {
    struct cshutdn *csd = &data->multi->cshutdn;
    return cshutdn_destroy_oldest(csd, data, destination);
  }
  return FALSE;
}

#define NUM_POLLS_ON_STACK 10

static CURLcode cshutdn_wait(struct cshutdn *cshutdn,
                             struct Curl_easy *data,
                             int timeout_ms)
{
  struct pollfd a_few_on_stack[NUM_POLLS_ON_STACK];
  struct curl_pollfds cpfds;
  CURLcode result;

  Curl_pollfds_init(&cpfds, a_few_on_stack, NUM_POLLS_ON_STACK);

  result = Curl_cshutdn_add_pollfds(cshutdn, data, &cpfds);
  if(result)
    goto out;

  Curl_poll(cpfds.pfds, cpfds.n, CURLMIN(timeout_ms, 1000));

out:
  Curl_pollfds_cleanup(&cpfds);
  return result;
}

static void cshutdn_perform(struct cshutdn *cshutdn,
                            struct Curl_easy *data)
{
  struct Curl_llist_node *e = Curl_llist_head(&cshutdn->list);
  struct Curl_llist_node *enext;
  struct connectdata *conn;
  timediff_t next_expire_ms = 0, ms;
  bool done;

  if(!e)
    return;

  CURL_TRC_M(data, "[SHUTDOWN] perform on %zu connections",
             Curl_llist_count(&cshutdn->list));
  while(e) {
    enext = Curl_node_next(e);
    conn = Curl_node_elem(e);
    Curl_cshutdn_run_once(data, conn, &done);
    if(done) {
      Curl_node_remove(e);
      Curl_cshutdn_terminate(data, conn, FALSE);
    }
    else {
      /* idata has one timer list, but maybe more than one connection.
       * Set EXPIRE_SHUTDOWN to the smallest time left for all. */
      ms = Curl_conn_shutdown_timeleft(data, conn);
      if(ms && ms < next_expire_ms)
        next_expire_ms = ms;
    }
    e = enext;
  }

  if(next_expire_ms)
    Curl_expire_ex(data, next_expire_ms, EXPIRE_SHUTDOWN);
}

static void cshutdn_terminate_all(struct cshutdn *cshutdn,
                                  struct Curl_easy *data,
                                  int timeout_ms)
{
  struct curltime started = *Curl_pgrs_now(data);
  struct Curl_llist_node *e;
  SIGPIPE_VARIABLE(pipe_st);

  DEBUGASSERT(cshutdn);
  DEBUGASSERT(data);

  CURL_TRC_M(data, "[SHUTDOWN] shutdown all");
  sigpipe_init(&pipe_st);
  sigpipe_apply(data, &pipe_st);

  while(Curl_llist_head(&cshutdn->list)) {
    timediff_t spent_ms;
    int remain_ms;

    cshutdn_perform(cshutdn, data);

    if(!Curl_llist_head(&cshutdn->list)) {
      CURL_TRC_M(data, "[SHUTDOWN] shutdown finished cleanly");
      break;
    }

    /* wait for activity, timeout or "nothing" */
    spent_ms = curlx_ptimediff_ms(Curl_pgrs_now(data), &started);
    if(spent_ms >= (timediff_t)timeout_ms) {
      CURL_TRC_M(data, "[SHUTDOWN] shutdown finished, %s",
                 (timeout_ms > 0) ? "timeout" : "best effort done");
      break;
    }

    remain_ms = timeout_ms - (int)spent_ms;
    if(cshutdn_wait(cshutdn, data, remain_ms)) {
      CURL_TRC_M(data, "[SHUTDOWN] shutdown finished, aborted");
      break;
    }
  }

  /* Terminate any remaining. */
  e = Curl_llist_head(&cshutdn->list);
  while(e) {
    struct connectdata *conn = Curl_node_elem(e);
    Curl_node_remove(e);
    Curl_cshutdn_terminate(data, conn, FALSE);
    e = Curl_llist_head(&cshutdn->list);
  }
  DEBUGASSERT(!Curl_llist_count(&cshutdn->list));

  sigpipe_restore(&pipe_st);
}

int Curl_cshutdn_init(struct cshutdn *cshutdn,
                      struct Curl_multi *multi)
{
  DEBUGASSERT(multi);
  cshutdn->multi = multi;
  Curl_llist_init(&cshutdn->list, NULL);
  cshutdn->initialised = TRUE;
  return 0; /* good */
}

void Curl_cshutdn_destroy(struct cshutdn *cshutdn,
                          struct Curl_easy *data)
{
  if(cshutdn->initialised && data) {
    int timeout_ms = 0;
    /* Just for testing, run graceful shutdown */
#ifdef DEBUGBUILD
    {
      const char *p = getenv("CURL_GRACEFUL_SHUTDOWN");
      if(p) {
        curl_off_t l;
        if(!curlx_str_number(&p, &l, INT_MAX))
          timeout_ms = (int)l;
      }
    }
#endif

    CURL_TRC_M(data, "[SHUTDOWN] destroy, %zu connections, timeout=%dms",
               Curl_llist_count(&cshutdn->list), timeout_ms);
    cshutdn_terminate_all(cshutdn, data, timeout_ms);
  }
  cshutdn->multi = NULL;
}

size_t Curl_cshutdn_count(struct Curl_easy *data)
{
  if(data && data->multi) {
    struct cshutdn *csd = &data->multi->cshutdn;
    return Curl_llist_count(&csd->list);
  }
  return 0;
}

size_t Curl_cshutdn_dest_count(struct Curl_easy *data,
                               const char *destination)
{
  if(data && data->multi) {
    struct cshutdn *csd = &data->multi->cshutdn;
    size_t n = 0;
    struct Curl_llist_node *e = Curl_llist_head(&csd->list);
    while(e) {
      struct connectdata *conn = Curl_node_elem(e);
      if(!strcmp(destination, conn->destination))
        ++n;
      e = Curl_node_next(e);
    }
    return n;
  }
  return 0;
}

static CURLMcode cshutdn_update_ev(struct cshutdn *cshutdn,
                                   struct Curl_easy *data,
                                   struct connectdata *conn)
{
  CURLMcode mresult;

  DEBUGASSERT(cshutdn);
  DEBUGASSERT(cshutdn->multi->socket_cb);

  Curl_attach_connection(data, conn);
  mresult = Curl_multi_ev_assess_conn(cshutdn->multi, data, conn);
  Curl_detach_connection(data);
  return mresult;
}

void Curl_cshutdn_add(struct cshutdn *cshutdn,
                      struct connectdata *conn,
                      size_t conns_in_pool)
{
  struct Curl_easy *data = cshutdn->multi->admin;
  size_t max_total = cshutdn->multi->max_total_connections;

  /* Add the connection to our shutdown list for non-blocking shutdown
   * during multi processing. */
  if(max_total > 0 && (max_total <=
        (conns_in_pool + Curl_llist_count(&cshutdn->list)))) {
    CURL_TRC_M(data, "[SHUTDOWN] discarding oldest shutdown connection "
               "due to connection limit of %zu", max_total);
    cshutdn_destroy_oldest(cshutdn, data, NULL);
  }

  if(cshutdn->multi->socket_cb) {
    if(cshutdn_update_ev(cshutdn, data, conn)) {
      CURL_TRC_M(data, "[SHUTDOWN] update events failed, discarding #%"
                 FMT_OFF_T, conn->connection_id);
      Curl_cshutdn_terminate(data, conn, FALSE);
      return;
    }
  }

  Curl_llist_append(&cshutdn->list, conn, &conn->cshutdn_node);
  CURL_TRC_M(data, "[SHUTDOWN] added #%" FMT_OFF_T
             " to shutdowns, now %zu conns in shutdown",
             conn->connection_id, Curl_llist_count(&cshutdn->list));
}

static void cshutdn_multi_socket(struct cshutdn *cshutdn,
                                 struct Curl_easy *data,
                                 curl_socket_t s)
{
  struct Curl_llist_node *e;
  struct connectdata *conn;
  bool done;

  DEBUGASSERT(cshutdn->multi->socket_cb);
  e = Curl_llist_head(&cshutdn->list);
  while(e) {
    conn = Curl_node_elem(e);
    if(s == conn->sock[FIRSTSOCKET] || s == conn->sock[SECONDARYSOCKET]) {
      Curl_cshutdn_run_once(data, conn, &done);
      if(done || cshutdn_update_ev(cshutdn, data, conn)) {
        Curl_node_remove(e);
        Curl_cshutdn_terminate(data, conn, FALSE);
      }
      break;
    }
    e = Curl_node_next(e);
  }
}

void Curl_cshutdn_perform(struct cshutdn *cshutdn,
                          struct Curl_easy *data,
                          curl_socket_t s)
{
  if((s == CURL_SOCKET_TIMEOUT) || (!cshutdn->multi->socket_cb))
    cshutdn_perform(cshutdn, data);
  else
    cshutdn_multi_socket(cshutdn, data, s);
}

/* return fd_set info about the shutdown connections */
void Curl_cshutdn_setfds(struct cshutdn *cshutdn,
                         struct Curl_easy *data,
                         fd_set *read_fd_set, fd_set *write_fd_set,
                         int *maxfd)
{
  if(Curl_llist_head(&cshutdn->list)) {
    struct Curl_llist_node *e;
    struct easy_pollset ps;

    Curl_pollset_init(&ps);
    for(e = Curl_llist_head(&cshutdn->list); e; e = Curl_node_next(e)) {
      unsigned int i;
      struct connectdata *conn = Curl_node_elem(e);
      CURLcode result;

      Curl_pollset_reset(&ps);
      Curl_attach_connection(data, conn);
      result = Curl_conn_adjust_pollset(data, conn, &ps);
      Curl_detach_connection(data);

      if(result)
        continue;

      for(i = 0; i < ps.n; i++) {
        curl_socket_t sock = ps.sockets[i];
        if(!FDSET_SOCK(sock))
          continue;
#ifdef __DJGPP__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Warith-conversion"
#endif
        if(ps.actions[i] & CURL_POLL_IN)
          FD_SET(sock, read_fd_set);
        if(ps.actions[i] & CURL_POLL_OUT)
          FD_SET(sock, write_fd_set);
#ifdef __DJGPP__
#pragma GCC diagnostic pop
#endif
        if((ps.actions[i] & (CURL_POLL_OUT | CURL_POLL_IN)) &&
           ((int)sock > *maxfd))
          *maxfd = (int)sock;
      }
    }
    Curl_pollset_cleanup(&ps);
  }
}

/* return information about the shutdown connections */
unsigned int Curl_cshutdn_add_waitfds(struct cshutdn *cshutdn,
                                      struct Curl_easy *data,
                                      struct Curl_waitfds *cwfds)
{
  unsigned int need = 0;

  if(Curl_llist_head(&cshutdn->list)) {
    struct Curl_llist_node *e;
    struct easy_pollset ps;
    struct connectdata *conn;
    CURLcode result;

    Curl_pollset_init(&ps);
    for(e = Curl_llist_head(&cshutdn->list); e; e = Curl_node_next(e)) {
      conn = Curl_node_elem(e);
      Curl_pollset_reset(&ps);
      Curl_attach_connection(data, conn);
      result = Curl_conn_adjust_pollset(data, conn, &ps);
      Curl_detach_connection(data);

      if(!result)
        need += Curl_waitfds_add_ps(cwfds, &ps);
    }
    Curl_pollset_cleanup(&ps);
  }
  return need;
}

CURLcode Curl_cshutdn_add_pollfds(struct cshutdn *cshutdn,
                                  struct Curl_easy *data,
                                  struct curl_pollfds *cpfds)
{
  CURLcode result = CURLE_OK;

  if(Curl_llist_head(&cshutdn->list)) {
    struct Curl_llist_node *e;
    struct easy_pollset ps;
    struct connectdata *conn;

    Curl_pollset_init(&ps);
    for(e = Curl_llist_head(&cshutdn->list); e; e = Curl_node_next(e)) {
      conn = Curl_node_elem(e);
      Curl_pollset_reset(&ps);
      Curl_attach_connection(data, conn);
      result = Curl_conn_adjust_pollset(data, conn, &ps);
      Curl_detach_connection(data);

      if(!result)
        result = Curl_pollfds_add_ps(cpfds, &ps);
      if(result) {
        Curl_pollset_cleanup(&ps);
        Curl_pollfds_cleanup(cpfds);
        goto out;
      }
    }
    Curl_pollset_cleanup(&ps);
  }
out:
  return result;
}
