/***************************************************************************
 *                                  _   _ ____  _
 *  Project                     ___| | | |  _ \| |
 *                             / __| | | | |_) | |
 *                            | (__| |_| |  _ <| |___
 *                             \___|\___/|_| \_\_____|
 *
 * 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 "tool_setup.h"

#include "tool_cfgable.h"
#include "tool_setopt.h"
#include "tool_findfile.h"
#include "tool_formparse.h"
#include "tool_msgs.h"
#include "tool_libinfo.h"
#include "tool_cb_soc.h"
#include "tool_operate.h"
#include "config2setopts.h"
#include "tool_ipfs.h"
#include "tool_cb_wrt.h"
#include "tool_cb_rea.h"
#include "tool_cb_see.h"
#include "tool_cb_dbg.h"
#include "tool_helpers.h"
#include "tool_version.h"

#define BUFFER_SIZE 102400L

#ifdef IP_TOS
static int get_address_family(curl_socket_t sockfd)
{
  struct sockaddr addr;
  curl_socklen_t addrlen = sizeof(addr);
  memset(&addr, 0, sizeof(addr));
  if(getsockname(sockfd, &addr, &addrlen) == 0)
    return addr.sa_family;
  return AF_UNSPEC;
}
#endif

#ifndef SOL_IP
#define SOL_IP IPPROTO_IP
#endif

#if defined(IP_TOS) || defined(IPV6_TCLASS) || defined(SO_PRIORITY)
static int sockopt_callback(void *clientp, curl_socket_t curlfd,
                            curlsocktype purpose)
{
  struct OperationConfig *config = (struct OperationConfig *)clientp;
  if(purpose != CURLSOCKTYPE_IPCXN)
    return CURL_SOCKOPT_OK;
  (void)config;
  (void)curlfd;
#if defined(IP_TOS) || defined(IPV6_TCLASS)
  if(config->ip_tos > 0) {
    int tos = (int)config->ip_tos;
    int result = 0;
    switch(get_address_family(curlfd)) {
    case AF_INET:
#ifdef IP_TOS
      result = setsockopt(curlfd, SOL_IP, IP_TOS, (void *)&tos, sizeof(tos));
#endif
      break;
#if defined(IPV6_TCLASS) && defined(AF_INET6)
    case AF_INET6:
      result = setsockopt(curlfd, IPPROTO_IPV6, IPV6_TCLASS,
                          (void *)&tos, sizeof(tos));
      break;
#endif
    }
    if(result < 0) {
      char buffer[STRERROR_LEN];
      int error = errno;
      warnf("Setting type of service to %d failed with errno %d: %s", tos,
            error, curlx_strerror(error, buffer, sizeof(buffer)));
    }
  }
#endif
#ifdef SO_PRIORITY
  if(config->vlan_priority > 0) {
    int priority = (int)config->vlan_priority;
    if(setsockopt(curlfd, SOL_SOCKET, SO_PRIORITY,
                  (void *)&priority, sizeof(priority)) != 0) {
      char buffer[STRERROR_LEN];
      int error = errno;
      warnf("VLAN priority %d failed with errno %d: %s", priority,
            error, curlx_strerror(error, buffer, sizeof(buffer)));
    }
  }
#endif
  return CURL_SOCKOPT_OK;
}
#endif /* IP_TOD || IPV6_TCLASS || SO_PRIORITY */

/* return current SSL backend name, chop off multissl */
static char *ssl_backend(void)
{
  static char ssl_ver[80] = "no ssl";
  static bool already = FALSE;
  if(!already) { /* if there is no existing version */
    const char *v = curl_version_info(CURLVERSION_NOW)->ssl_version;
    if(v)
      curl_msnprintf(ssl_ver, sizeof(ssl_ver),
                     "%.*s", (int)strcspn(v, " "), v);
    already = TRUE;
  }
  return ssl_ver;
}

/*
 * Possibly rewrite the URL for IPFS and return the protocol token for the
 * scheme used in the given URL.
 */
static CURLcode url_proto_and_rewrite(char **url,
                                      struct OperationConfig *config,
                                      const char **scheme)
{
  CURLcode result = CURLE_OK;
  CURLU *uh = curl_url();
  const char *proto = NULL;
  *scheme = NULL;

  DEBUGASSERT(url && *url);
  if(uh) {
    char *schemep = NULL;
    CURLUcode uc =
      curl_url_set(uh, CURLUPART_URL, *url,
                   CURLU_GUESS_SCHEME | CURLU_NON_SUPPORT_SCHEME);
    if(!uc) {
      uc = curl_url_get(uh, CURLUPART_SCHEME, &schemep, CURLU_DEFAULT_SCHEME);
      if(!uc) {
#ifdef CURL_DISABLE_IPFS
        (void)config;
#else
        if(curl_strequal(schemep, proto_ipfs) ||
           curl_strequal(schemep, proto_ipns)) {
          result = ipfs_url_rewrite(uh, schemep, url, config);
          /* short-circuit proto_token, we know it is ipfs or ipns */
          if(curl_strequal(schemep, proto_ipfs))
            proto = proto_ipfs;
          else if(curl_strequal(schemep, proto_ipns))
            proto = proto_ipns;
          if(result)
            config->synthetic_error = TRUE;
        }
        else
#endif /* !CURL_DISABLE_IPFS */
          proto = proto_token(schemep);
        curl_free(schemep);
      }
      else if(uc == CURLUE_OUT_OF_MEMORY)
        result = CURLE_OUT_OF_MEMORY;
    }
    else if(uc == CURLUE_OUT_OF_MEMORY)
      result = CURLE_OUT_OF_MEMORY;
    curl_url_cleanup(uh);
  }
  else
    result = CURLE_OUT_OF_MEMORY;

  *scheme = proto ? proto : "?"; /* Never match if not found. */
  return result;
}

static CURLcode ssh_setopts(struct OperationConfig *config, CURL *curl,
                            const char *use_proto)
{
  CURLcode result;

  if(use_proto != proto_scp && use_proto != proto_sftp)
    return CURLE_OK;

  /* SSH and SSL private key uses same command-line option */
  MY_SETOPT_STR(curl, CURLOPT_SSH_PRIVATE_KEYFILE, config->key);
  MY_SETOPT_STR(curl, CURLOPT_SSH_PUBLIC_KEYFILE, config->pubkey);

  /* SSH host key md5 checking allows us to fail if we are not talking to who
     we think we should */
  MY_SETOPT_STR(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_MD5, config->hostpubmd5);

  /* SSH host key sha256 checking allows us to fail if we are not talking to
     who we think we should */
  MY_SETOPT_STR(curl, CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256,
                config->hostpubsha256);

  if(config->ssh_compression)
    my_setopt_long(curl, CURLOPT_SSH_COMPRESSION, 1);

  if(!config->insecure_ok) {
    char *known = config->knownhosts;
    if(!known)
      known = findfile(".ssh/known_hosts", FALSE);
    if(known) {
      result = my_setopt_str(curl, CURLOPT_SSH_KNOWNHOSTS, known);
      if(result) {
        config->knownhosts = NULL;
        curl_free(known);
        return result;
      }
      /* store it in global to avoid repeated checks */
      config->knownhosts = known;
    }
    else if(!config->hostpubmd5 && !config->hostpubsha256) {
      errorf("Could not find a known_hosts file");
      return CURLE_FAILED_INIT;
    }
    else
      warnf("Could not find a known_hosts file");
  }
  return CURLE_OK; /* ignore if SHA256 did not work */
}

static long tlsversion(unsigned char mintls,
                       unsigned char maxtls)
{
  long tlsver = 0;
  if(!mintls) { /* minimum is at default */
    /* minimum is set to default, which we want to be 1.2 */
    if(maxtls && (maxtls < 3))
      /* max is set lower than 1.2 and minimum is default, change minimum to
         the same as max */
      mintls = maxtls;
  }
  switch(mintls) {
  case 1:
    tlsver = CURL_SSLVERSION_TLSv1_0;
    break;
  case 2:
    tlsver = CURL_SSLVERSION_TLSv1_1;
    break;
  case 0: /* let default minimum be 1.2 */
  case 3:
    tlsver = CURL_SSLVERSION_TLSv1_2;
    break;
  case 4:
  default: /* in case */
    tlsver = CURL_SSLVERSION_TLSv1_3;
    break;
  }
  switch(maxtls) {
  case 0: /* not set, leave it */
    break;
  case 1:
    tlsver |= CURL_SSLVERSION_MAX_TLSv1_0;
    break;
  case 2:
    tlsver |= CURL_SSLVERSION_MAX_TLSv1_1;
    break;
  case 3:
    tlsver |= CURL_SSLVERSION_MAX_TLSv1_2;
    break;
  case 4:
  default: /* in case */
    tlsver |= CURL_SSLVERSION_MAX_TLSv1_3;
    break;
  }
  return tlsver;
}

/* only called if libcurl supports TLS */
static CURLcode ssl_ca_setopts(struct OperationConfig *config, CURL *curl)
{
  CURLcode result = CURLE_OK;

  if(config->cacert)
    MY_SETOPT_STR(curl, CURLOPT_CAINFO, config->cacert);
  if(config->proxy_cacert)
    MY_SETOPT_STR(curl, CURLOPT_PROXY_CAINFO, config->proxy_cacert);
  if(config->capath)
    MY_SETOPT_STR(curl, CURLOPT_CAPATH, config->capath);

  /* For the time being if --proxy-capath is not set then we use the
     --capath value for it, if any. See #1257 */
  if(config->proxy_capath || config->capath) {
    MY_SETOPT_STR(curl, CURLOPT_PROXY_CAPATH,
                  (config->proxy_capath ? config->proxy_capath :
                   config->capath));
    if(result && config->proxy_capath) {
      warnf("ignoring %s, not supported by libcurl with %s",
            config->proxy_capath ? "--proxy-capath" : "--capath",
            ssl_backend());
    }
  }
  if(result)
    return result;

#ifdef CURL_CA_EMBED
  if(!config->cacert && !config->capath) {
    struct curl_blob blob;
    blob.data = CURL_UNCONST(curl_ca_embed);
    blob.len = strlen((const char *)curl_ca_embed);
    blob.flags = CURL_BLOB_NOCOPY;
    notef("Using embedded CA bundle (%zu bytes)", blob.len);
    result = curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &blob);
    if((result == CURLE_NOT_BUILT_IN) || (result == CURLE_UNKNOWN_OPTION)) {
      warnf("ignoring %s, not supported by libcurl with %s",
            "embedded CA bundle", ssl_backend());
      result = CURLE_OK;
    }
  }
  if(!config->proxy_cacert && !config->proxy_capath) {
    struct curl_blob blob;
    blob.data = CURL_UNCONST(curl_ca_embed);
    blob.len = strlen((const char *)curl_ca_embed);
    blob.flags = CURL_BLOB_NOCOPY;
    notef("Using embedded CA bundle, for proxies (%zu bytes)", blob.len);
    result = curl_easy_setopt(curl, CURLOPT_PROXY_CAINFO_BLOB, &blob);
    if((result == CURLE_NOT_BUILT_IN) || (result == CURLE_UNKNOWN_OPTION)) {
      warnf("ignoring %s, not supported by libcurl with %s",
            "embedded CA bundle", ssl_backend());
      result = CURLE_OK;
    }
  }
#endif
  return result;
}

/* only called if libcurl supports TLS */
static CURLcode ssl_setopts(struct OperationConfig *config, CURL *curl)
{
  CURLcode result = CURLE_OK;

  if(config->crlfile)
    MY_SETOPT_STR(curl, CURLOPT_CRLFILE, config->crlfile);
  if(config->proxy_crlfile)
    MY_SETOPT_STR(curl, CURLOPT_PROXY_CRLFILE, config->proxy_crlfile);
  else if(config->crlfile)
    /* CURLOPT_PROXY_CRLFILE default is crlfile */
    MY_SETOPT_STR(curl, CURLOPT_PROXY_CRLFILE, config->crlfile);

  if(config->pinnedpubkey) {
    MY_SETOPT_STR(curl, CURLOPT_PINNEDPUBLICKEY, config->pinnedpubkey);
    if(result)
      warnf("ignoring %s, not supported by libcurl with %s",
            "--pinnedpubkey", ssl_backend());
  }
  if(config->proxy_pinnedpubkey) {
    MY_SETOPT_STR(curl, CURLOPT_PROXY_PINNEDPUBLICKEY,
                  config->proxy_pinnedpubkey);
    if(result)
      warnf("ignoring %s, not supported by libcurl with %s",
            "--proxy-pinnedpubkey", ssl_backend());
  }

  if(config->ssl_ec_curves)
    MY_SETOPT_STR(curl, CURLOPT_SSL_EC_CURVES, config->ssl_ec_curves);

  if(config->ssl_signature_algorithms)
    MY_SETOPT_STR(curl, CURLOPT_SSL_SIGNATURE_ALGORITHMS,
                  config->ssl_signature_algorithms);

  if(config->writeout)
    my_setopt_long(curl, CURLOPT_CERTINFO, 1);

  MY_SETOPT_STR(curl, CURLOPT_SSLCERT, config->cert);
  MY_SETOPT_STR(curl, CURLOPT_PROXY_SSLCERT, config->proxy_cert);
  MY_SETOPT_STR(curl, CURLOPT_SSLCERTTYPE, config->cert_type);
  MY_SETOPT_STR(curl, CURLOPT_PROXY_SSLCERTTYPE, config->proxy_cert_type);
  MY_SETOPT_STR(curl, CURLOPT_SSLKEY, config->key);
  MY_SETOPT_STR(curl, CURLOPT_PROXY_SSLKEY, config->proxy_key);
  MY_SETOPT_STR(curl, CURLOPT_SSLKEYTYPE, config->key_type);
  MY_SETOPT_STR(curl, CURLOPT_PROXY_SSLKEYTYPE, config->proxy_key_type);

  /* libcurl default is strict verifyhost -> 1L, verifypeer -> 1L */
  if(config->insecure_ok) {
    my_setopt_long(curl, CURLOPT_SSL_VERIFYPEER, 0);
    my_setopt_long(curl, CURLOPT_SSL_VERIFYHOST, 0);
  }

  if(config->doh_insecure_ok) {
    my_setopt_long(curl, CURLOPT_DOH_SSL_VERIFYPEER, 0);
    my_setopt_long(curl, CURLOPT_DOH_SSL_VERIFYHOST, 0);
  }

  if(config->proxy_insecure_ok) {
    my_setopt_long(curl, CURLOPT_PROXY_SSL_VERIFYPEER, 0);
    my_setopt_long(curl, CURLOPT_PROXY_SSL_VERIFYHOST, 0);
  }

  if(config->verifystatus)
    my_setopt_long(curl, CURLOPT_SSL_VERIFYSTATUS, 1);

  if(config->doh_verifystatus)
    my_setopt_long(curl, CURLOPT_DOH_SSL_VERIFYSTATUS, 1);

  my_setopt_SSLVERSION(curl, CURLOPT_SSLVERSION,
                       tlsversion(config->ssl_version,
                                  config->ssl_version_max));
  if(config->proxy)
    my_setopt_SSLVERSION(curl, CURLOPT_PROXY_SSLVERSION,
                         config->proxy_ssl_version);

  {
    long mask =
      (config->ssl_allow_beast ? CURLSSLOPT_ALLOW_BEAST : 0) |
      (config->ssl_allow_earlydata ? CURLSSLOPT_EARLYDATA : 0) |
      (config->ssl_no_revoke ? CURLSSLOPT_NO_REVOKE : 0) |
      (config->ssl_revoke_best_effort ? CURLSSLOPT_REVOKE_BEST_EFFORT : 0) |
      (config->native_ca_store ? CURLSSLOPT_NATIVE_CA : 0) |
      (config->ssl_auto_client_cert ? CURLSSLOPT_AUTO_CLIENT_CERT : 0);

    if(mask)
      my_setopt_bitmask(curl, CURLOPT_SSL_OPTIONS, mask);
  }

  {
    long mask =
      (config->proxy_ssl_allow_beast ? CURLSSLOPT_ALLOW_BEAST : 0) |
      (config->proxy_ssl_auto_client_cert ? CURLSSLOPT_AUTO_CLIENT_CERT : 0) |
      (config->proxy_native_ca_store ? CURLSSLOPT_NATIVE_CA : 0);

    if(mask)
      my_setopt_bitmask(curl, CURLOPT_PROXY_SSL_OPTIONS, mask);
  }

  if(config->cipher_list) {
    MY_SETOPT_STR(curl, CURLOPT_SSL_CIPHER_LIST, config->cipher_list);
    if(result)
      warnf("ignoring %s, not supported by libcurl with %s",
            "--ciphers", ssl_backend());
  }
  if(config->proxy_cipher_list) {
    MY_SETOPT_STR(curl, CURLOPT_PROXY_SSL_CIPHER_LIST,
                  config->proxy_cipher_list);
    if(result)
      warnf("ignoring %s, not supported by libcurl with %s",
            "--proxy-ciphers", ssl_backend());
  }
  if(config->cipher13_list) {
    MY_SETOPT_STR(curl, CURLOPT_TLS13_CIPHERS, config->cipher13_list);
    if(result)
      warnf("ignoring %s, not supported by libcurl with %s",
            "--tls13-ciphers", ssl_backend());
  }
  if(config->proxy_cipher13_list) {
    MY_SETOPT_STR(curl, CURLOPT_PROXY_TLS13_CIPHERS,
                  config->proxy_cipher13_list);
    if(result)
      warnf("ignoring %s, not supported by libcurl with %s",
            "--proxy-tls13-ciphers", ssl_backend());
  }

  /* curl 7.16.0 */
  if(config->disable_sessionid)
    /* disable it */
    my_setopt_long(curl, CURLOPT_SSL_SESSIONID_CACHE, 0);

  if(feature_ech) {
    /* only if enabled in libcurl */
    if(config->ech) /* only if set (optional) */
      MY_SETOPT_STR(curl, CURLOPT_ECH, config->ech);
    if(config->ech_public) /* only if set (optional) */
      MY_SETOPT_STR(curl, CURLOPT_ECH, config->ech_public);
    if(config->ech_config) /* only if set (optional) */
      MY_SETOPT_STR(curl, CURLOPT_ECH, config->ech_config);
  }

  if(config->engine)
    MY_SETOPT_STR(curl, CURLOPT_SSLENGINE, config->engine);

  if(config->ftp_ssl_reqd)
    my_setopt_enum(curl, CURLOPT_USE_SSL, CURLUSESSL_ALL);

  else if(config->ftp_ssl)
    my_setopt_enum(curl, CURLOPT_USE_SSL, CURLUSESSL_TRY);

  else if(config->ftp_ssl_control)
    my_setopt_enum(curl, CURLOPT_USE_SSL, CURLUSESSL_CONTROL);

  if(config->noalpn)
    my_setopt_long(curl, CURLOPT_SSL_ENABLE_ALPN, 0);

  return CURLE_OK;
}

static CURLcode cookie_setopts(struct OperationConfig *config, CURL *curl)
{
  CURLcode result = CURLE_OK;
  if(config->cookies) {
    struct dynbuf cookies;
    struct curl_slist *cl;

    /* The maximum size needs to match MAX_NAME in cookie.h */
#define MAX_COOKIE_LINE 8200
    curlx_dyn_init(&cookies, MAX_COOKIE_LINE);
    for(cl = config->cookies; cl; cl = cl->next) {
      if(cl == config->cookies)
        result = curlx_dyn_add(&cookies, cl->data);
      else
        result = curlx_dyn_addf(&cookies, ";%s%s",
                                ISBLANK(cl->data[0]) ? "" : " ", cl->data);
      if(result) {
        warnf("skipped provided cookie, the cookie header "
              "would go over %u bytes", MAX_COOKIE_LINE);
        return result;
      }
    }

    result = my_setopt_str(curl, CURLOPT_COOKIE, curlx_dyn_ptr(&cookies));
    curlx_dyn_free(&cookies);
    if(result)
      return result;
  }

  if(config->cookiefiles) {
    struct curl_slist *cfl;

    for(cfl = config->cookiefiles; cfl; cfl = cfl->next)
      MY_SETOPT_STR(curl, CURLOPT_COOKIEFILE, cfl->data);
  }

  if(config->cookiejar)
    MY_SETOPT_STR(curl, CURLOPT_COOKIEJAR, config->cookiejar);

  my_setopt_long(curl, CURLOPT_COOKIESESSION, config->cookiesession);

  return result;
}

/* only for HTTP transfers */
static CURLcode http_setopts(struct OperationConfig *config, CURL *curl,
                             const char *use_proto)
{
  CURLcode result = CURLE_OK;
  long postRedir = 0;

  if(use_proto != proto_http && use_proto != proto_https)
    return CURLE_OK;

  my_setopt_long(curl, CURLOPT_FOLLOWLOCATION, config->followlocation);
  my_setopt_long(curl, CURLOPT_UNRESTRICTED_AUTH, config->unrestricted_auth);
#ifndef CURL_DISABLE_AWS
  MY_SETOPT_STR(curl, CURLOPT_AWS_SIGV4, config->aws_sigv4);
#endif
  my_setopt_long(curl, CURLOPT_AUTOREFERER, config->autoreferer);

  if(config->proxyheaders) {
    my_setopt_slist(curl, CURLOPT_PROXYHEADER, config->proxyheaders);
  }

  my_setopt_long(curl, CURLOPT_MAXREDIRS, config->maxredirs);

  if(config->httpversion)
    my_setopt_enum(curl, CURLOPT_HTTP_VERSION, config->httpversion);

  if(config->post301)
    postRedir |= CURL_REDIR_POST_301;
  if(config->post302)
    postRedir |= CURL_REDIR_POST_302;
  if(config->post303)
    postRedir |= CURL_REDIR_POST_303;
  my_setopt_long(curl, CURLOPT_POSTREDIR, postRedir);

  if(config->encoding)
    MY_SETOPT_STR(curl, CURLOPT_ACCEPT_ENCODING, "");

  if(config->tr_encoding)
    my_setopt_long(curl, CURLOPT_TRANSFER_ENCODING, 1);

  my_setopt_long(curl, CURLOPT_HTTP09_ALLOWED, config->http09_allowed);

  if(config->altsvc)
    MY_SETOPT_STR(curl, CURLOPT_ALTSVC, config->altsvc);

  if(config->hsts)
    MY_SETOPT_STR(curl, CURLOPT_HSTS, config->hsts);

  if(config->expect100timeout_ms > 0)
    my_setopt_long(curl, CURLOPT_EXPECT_100_TIMEOUT_MS,
                   config->expect100timeout_ms);

  if(!result)
    result = cookie_setopts(config, curl);
  if(result)
    return result;
  /* Enable header separation when using a proxy with HTTPS or proxytunnel
   * to prevent --header content from leaking into CONNECT requests */
  if((config->proxy || config->proxyheaders) &&
     (use_proto == proto_https || config->proxytunnel))
    my_setopt_long(curl, CURLOPT_HEADEROPT, CURLHEADER_SEPARATE);
  return result;
}

static void tcp_setopts(struct OperationConfig *config, CURL *curl)
{
  if(!config->tcp_nodelay)
    my_setopt_long(curl, CURLOPT_TCP_NODELAY, 0);

  if(config->tcp_fastopen)
    my_setopt_long(curl, CURLOPT_TCP_FASTOPEN, 1);

  if(config->mptcp)
    my_setopt_ptr(curl, CURLOPT_OPENSOCKETFUNCTION, tool_socket_open_mptcp_cb);

  /* curl 7.17.1 */
  if(!config->nokeepalive) {
    my_setopt_long(curl, CURLOPT_TCP_KEEPALIVE, 1);
    if(config->alivetime) {
      my_setopt_long(curl, CURLOPT_TCP_KEEPIDLE, config->alivetime);
      my_setopt_long(curl, CURLOPT_TCP_KEEPINTVL, config->alivetime);
    }
    if(config->alivecnt)
      my_setopt_long(curl, CURLOPT_TCP_KEEPCNT, config->alivecnt);
  }
  else
    my_setopt_long(curl, CURLOPT_TCP_KEEPALIVE, 0);
}

static CURLcode ftp_setopts(struct OperationConfig *config, CURL *curl,
                            const char *use_proto)
{
  CURLcode result;

  if(use_proto != proto_ftp && use_proto != proto_ftps)
    return CURLE_OK;

  MY_SETOPT_STR(curl, CURLOPT_FTPPORT, config->ftpport);

  if(config->disable_epsv)
    /* disable it */
    my_setopt_long(curl, CURLOPT_FTP_USE_EPSV, 0);

  if(config->disable_eprt)
    /* disable it */
    my_setopt_long(curl, CURLOPT_FTP_USE_EPRT, 0);

  if(config->ftp_ssl_ccc)
    my_setopt_enum(curl, CURLOPT_FTP_SSL_CCC, config->ftp_ssl_ccc_mode);

  MY_SETOPT_STR(curl, CURLOPT_FTP_ACCOUNT, config->ftp_account);
  my_setopt_long(curl, CURLOPT_FTP_SKIP_PASV_IP, config->ftp_skip_ip);
  my_setopt_long(curl, CURLOPT_FTP_FILEMETHOD, config->ftp_filemethod);
  MY_SETOPT_STR(curl, CURLOPT_FTP_ALTERNATIVE_TO_USER,
                config->ftp_alternative_to_user);

  if(config->ftp_pret)
    my_setopt_long(curl, CURLOPT_FTP_USE_PRET, 1);

  return result;
}

static void gen_trace_setopts(struct OperationConfig *config, CURL *curl)
{
  if(global->tracetype != TRACE_NONE) {
    my_setopt_ptr(curl, CURLOPT_DEBUGFUNCTION, tool_debug_cb);
    my_setopt_ptr(curl, CURLOPT_DEBUGDATA, config);
    my_setopt_long(curl, CURLOPT_VERBOSE, 1L);
  }
}

static void gen_cb_setopts(struct OperationConfig *config,
                           struct per_transfer *per,
                           CURL *curl)
{
  (void)config; /* when --libcurl is disabled */

  /* where to store */
  my_setopt_ptr(curl, CURLOPT_WRITEDATA, per);
  my_setopt_ptr(curl, CURLOPT_INTERLEAVEDATA, per);

  /* what call to write */
  my_setopt_ptr(curl, CURLOPT_WRITEFUNCTION, tool_write_cb);

  /* what to read */
  my_setopt_ptr(curl, CURLOPT_READDATA, per);
  my_setopt_ptr(curl, CURLOPT_READFUNCTION, tool_read_cb);

  /* in 7.18.0, the CURLOPT_SEEKFUNCTION/DATA pair is taking over what
     CURLOPT_IOCTLFUNCTION/DATA pair previously provided for seeking */
  my_setopt_ptr(curl, CURLOPT_SEEKDATA, per);
  my_setopt_ptr(curl, CURLOPT_SEEKFUNCTION, tool_seek_cb);

  if((global->progressmode == CURL_PROGRESS_BAR) &&
     !global->noprogress && !global->silent) {
    /* we want the alternative style, then we have to implement it
       ourselves! */
    my_setopt_ptr(curl, CURLOPT_XFERINFOFUNCTION, tool_progress_cb);
    my_setopt_ptr(curl, CURLOPT_XFERINFODATA, per);
  }
  else if(per->uploadfile && !strcmp(per->uploadfile, ".")) {
    /* when reading from stdin in non-blocking mode, we use the progress
       function to unpause a busy read */
    my_setopt_long(curl, CURLOPT_NOPROGRESS, 0);
    my_setopt_ptr(curl, CURLOPT_XFERINFOFUNCTION, tool_readbusy_cb);
    my_setopt_ptr(curl, CURLOPT_XFERINFODATA, per);
  }

  my_setopt_ptr(curl, CURLOPT_HEADERFUNCTION, tool_header_cb);
  my_setopt_ptr(curl, CURLOPT_HEADERDATA, per);
}

static CURLcode proxy_setopts(struct OperationConfig *config, CURL *curl)
{
  CURLcode result;
  if(config->proxy) {
    result = my_setopt_str(curl, CURLOPT_PROXY, config->proxy);
    if(result) {
      errorf("proxy support is disabled in this libcurl");
      config->synthetic_error = TRUE;
      return CURLE_NOT_BUILT_IN;
    }
  }

  if(config->proxy)
    my_setopt_enum(curl, CURLOPT_PROXYTYPE, config->proxyver);

  MY_SETOPT_STR(curl, CURLOPT_PROXYUSERPWD, config->proxyuserpwd);
  my_setopt_long(curl, CURLOPT_HTTPPROXYTUNNEL, config->proxytunnel);
  if(config->preproxy)
    MY_SETOPT_STR(curl, CURLOPT_PRE_PROXY, config->preproxy);

  if(config->proxyanyauth)
    my_setopt_bitmask(curl, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
  else if(config->proxynegotiate)
    my_setopt_bitmask(curl, CURLOPT_PROXYAUTH, CURLAUTH_GSSNEGOTIATE);
  else if(config->proxyntlm)
    my_setopt_bitmask(curl, CURLOPT_PROXYAUTH, CURLAUTH_NTLM);
  else if(config->proxydigest)
    my_setopt_bitmask(curl, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
  else if(config->proxybasic)
    my_setopt_bitmask(curl, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);

  MY_SETOPT_STR(curl, CURLOPT_NOPROXY, config->noproxy);
  my_setopt_long(curl, CURLOPT_SUPPRESS_CONNECT_HEADERS,
                 config->suppress_connect_headers);

  if(config->proxy_service_name)
    MY_SETOPT_STR(curl, CURLOPT_PROXY_SERVICE_NAME,
                  config->proxy_service_name);

  if(config->haproxy_protocol)
    my_setopt_long(curl, CURLOPT_HAPROXYPROTOCOL, 1);

  if(config->haproxy_clientip)
    MY_SETOPT_STR(curl, CURLOPT_HAPROXY_CLIENT_IP, config->haproxy_clientip);

  return result;
}

static CURLcode tls_srp_setopts(struct OperationConfig *config, CURL *curl)
{
  CURLcode result = CURLE_OK;
  if(config->tls_username)
    MY_SETOPT_STR(curl, CURLOPT_TLSAUTH_USERNAME, config->tls_username);
  if(config->tls_password)
    MY_SETOPT_STR(curl, CURLOPT_TLSAUTH_PASSWORD, config->tls_password);
  if(config->tls_authtype)
    MY_SETOPT_STR(curl, CURLOPT_TLSAUTH_TYPE, config->tls_authtype);
  if(config->proxy_tls_username)
    MY_SETOPT_STR(curl, CURLOPT_PROXY_TLSAUTH_USERNAME,
                  config->proxy_tls_username);
  if(config->proxy_tls_password)
    MY_SETOPT_STR(curl, CURLOPT_PROXY_TLSAUTH_PASSWORD,
                  config->proxy_tls_password);
  if(config->proxy_tls_authtype)
    MY_SETOPT_STR(curl, CURLOPT_PROXY_TLSAUTH_TYPE,
                  config->proxy_tls_authtype);
  return result;
}

static CURLcode setopt_post(struct OperationConfig *config, CURL *curl)
{
  CURLcode result = CURLE_OK;
  switch(config->httpreq) {
  case TOOL_HTTPREQ_SIMPLEPOST:
    if(config->resume_from) {
      errorf("cannot mix --continue-at with --data");
      result = CURLE_FAILED_INIT;
    }
    else {
      MY_SETOPT_STR(curl, CURLOPT_POSTFIELDS,
                    curlx_dyn_ptr(&config->postdata));
      my_setopt_offt(curl, CURLOPT_POSTFIELDSIZE_LARGE,
                     curlx_dyn_len(&config->postdata));
    }
    break;
  case TOOL_HTTPREQ_MIMEPOST:
    /* free previous remainders */
    curl_mime_free(config->mimepost);
    config->mimepost = NULL;
    if(config->resume_from) {
      errorf("cannot mix --continue-at with --form");
      result = CURLE_FAILED_INIT;
    }
    else {
      result = tool2curlmime(curl, config->mimeroot, &config->mimepost);
      if(!result)
        result = my_setopt_mimepost(curl, CURLOPT_MIMEPOST, config->mimepost);
    }
    break;
  default:
    break;
  }
  return result;
}

static void buffersize(struct OperationConfig *config, CURL *curl)
{
#ifdef DEBUGBUILD
  char *env = getenv("CURL_BUFFERSIZE");
  if(env) {
    curl_off_t num;
    const char *p = env;
    if(!curlx_str_number(&p, &num, LONG_MAX))
      my_setopt_long(curl, CURLOPT_BUFFERSIZE, (long)num);
    return;
  }
#endif
  if(config->recvpersecond && (config->recvpersecond < BUFFER_SIZE))
    /* use a smaller sized buffer for better sleeps */
    my_setopt_long(curl, CURLOPT_BUFFERSIZE, (long)config->recvpersecond);
  else
    my_setopt_long(curl, CURLOPT_BUFFERSIZE, BUFFER_SIZE);
}

CURLcode config2setopts(struct OperationConfig *config,
                        struct per_transfer *per,
                        CURL *curl,
                        CURLSH *share)
{
  const char *use_proto;
  CURLcode result = url_proto_and_rewrite(&per->url, config, &use_proto);

  /* Avoid having this setopt added to the --libcurl source output. */
  if(!result)
    result = curl_easy_setopt(curl, CURLOPT_SHARE, share);
  if(result)
    return result;

#ifndef DEBUGBUILD
  /* On most modern OSes, exiting works thoroughly,
     we will clean everything up via exit(), so do not bother with
     slow cleanups. Crappy ones might need to skip this.
     Note: avoid having this setopt added to the --libcurl source
     output. */
  result = curl_easy_setopt(curl, CURLOPT_QUICK_EXIT, 1L);
  if(result)
    return result;
#endif

  gen_trace_setopts(config, curl);

  buffersize(config, curl);

  MY_SETOPT_STR(curl, CURLOPT_URL, per->url);
  my_setopt_long(curl, CURLOPT_NOPROGRESS,
                 global->noprogress || global->silent);
  /* call after the line above. It may override CURLOPT_NOPROGRESS */
  gen_cb_setopts(config, per, curl);

  my_setopt_long(curl, CURLOPT_NOBODY, config->no_body);
  MY_SETOPT_STR(curl, CURLOPT_XOAUTH2_BEARER, config->oauth_bearer);
  result = proxy_setopts(config, curl);
  if(setopt_bad(result) || config->synthetic_error)
    return result;

  my_setopt_long(curl, CURLOPT_FAILONERROR, config->fail == FAIL_WO_BODY);
  MY_SETOPT_STR(curl, CURLOPT_REQUEST_TARGET, config->request_target);
  my_setopt_long(curl, CURLOPT_UPLOAD, !!per->uploadfile);
  my_setopt_long(curl, CURLOPT_DIRLISTONLY, config->dirlistonly);
  my_setopt_long(curl, CURLOPT_APPEND, config->ftp_append);

  if(config->netrc_opt)
    my_setopt_enum(curl, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
  else if(config->netrc || config->netrc_file)
    my_setopt_enum(curl, CURLOPT_NETRC, CURL_NETRC_REQUIRED);
  else
    my_setopt_enum(curl, CURLOPT_NETRC, CURL_NETRC_IGNORED);

  MY_SETOPT_STR(curl, CURLOPT_NETRC_FILE, config->netrc_file);
  my_setopt_long(curl, CURLOPT_TRANSFERTEXT, config->use_ascii);
  MY_SETOPT_STR(curl, CURLOPT_LOGIN_OPTIONS, config->login_options);
  MY_SETOPT_STR(curl, CURLOPT_USERPWD, config->userpwd);
  MY_SETOPT_STR(curl, CURLOPT_RANGE, config->range);
  my_setopt_ptr(curl, CURLOPT_ERRORBUFFER, per->errorbuffer);
  my_setopt_long(curl, CURLOPT_TIMEOUT_MS, config->timeout_ms);

  result = setopt_post(config, curl);
  if(result)
    return result;

  if(config->mime_options)
    my_setopt_long(curl, CURLOPT_MIME_OPTIONS, config->mime_options);

  if(config->authtype)
    my_setopt_bitmask(curl, CURLOPT_HTTPAUTH, config->authtype);

  my_setopt_slist(curl, CURLOPT_HTTPHEADER, config->headers);

  if(proto_http || proto_rtsp) {
    MY_SETOPT_STR(curl, CURLOPT_REFERER, config->referer);
    MY_SETOPT_STR(curl, CURLOPT_USERAGENT, config->useragent ?
                  config->useragent : CURL_NAME "/" CURL_VERSION);
  }

  result = http_setopts(config, curl, use_proto);
  if(!result)
    result = ftp_setopts(config, curl, use_proto);
  if(result)
    return result;

  my_setopt_long(curl, CURLOPT_LOW_SPEED_LIMIT, config->low_speed_limit);
  my_setopt_long(curl, CURLOPT_LOW_SPEED_TIME, config->low_speed_time);
  my_setopt_offt(curl, CURLOPT_MAX_SEND_SPEED_LARGE, config->sendpersecond);
  my_setopt_offt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, config->recvpersecond);

  if(config->use_resume)
    my_setopt_offt(curl, CURLOPT_RESUME_FROM_LARGE, config->resume_from);
  else
    my_setopt_offt(curl, CURLOPT_RESUME_FROM_LARGE, 0);

  MY_SETOPT_STR(curl, CURLOPT_KEYPASSWD, config->key_passwd);
  MY_SETOPT_STR(curl, CURLOPT_PROXY_KEYPASSWD, config->proxy_key_passwd);

  result = ssh_setopts(config, curl, use_proto);
  if(setopt_bad(result))
    return result;

  if(feature_ssl) {
    result = ssl_ca_setopts(config, curl);
    if(!result)
      result = ssl_setopts(config, curl);
    if(setopt_bad(result))
      return result;
  }

  if(config->path_as_is)
    my_setopt_long(curl, CURLOPT_PATH_AS_IS, 1);

  if(config->no_body || config->remote_time)
    /* no body or use remote time */
    my_setopt_long(curl, CURLOPT_FILETIME, 1);

  my_setopt_long(curl, CURLOPT_CRLF, config->crlf);
  my_setopt_slist(curl, CURLOPT_QUOTE, config->quote);
  my_setopt_slist(curl, CURLOPT_POSTQUOTE, config->postquote);
  my_setopt_slist(curl, CURLOPT_PREQUOTE, config->prequote);

  my_setopt_enum(curl, CURLOPT_TIMECONDITION, config->timecond);
  my_setopt_offt(curl, CURLOPT_TIMEVALUE_LARGE, config->condtime);
  MY_SETOPT_STR(curl, CURLOPT_CUSTOMREQUEST, config->customrequest);
  customrequest_helper(config->httpreq, config->customrequest);
  my_setopt_ptr(curl, CURLOPT_STDERR, tool_stderr);
  MY_SETOPT_STR(curl, CURLOPT_INTERFACE, config->iface);
  progressbarinit(&per->progressbar, config);
  MY_SETOPT_STR(curl, CURLOPT_DNS_SERVERS, config->dns_servers);
  MY_SETOPT_STR(curl, CURLOPT_DNS_INTERFACE, config->dns_interface);
  MY_SETOPT_STR(curl, CURLOPT_DNS_LOCAL_IP4, config->dns_ipv4_addr);
  MY_SETOPT_STR(curl, CURLOPT_DNS_LOCAL_IP6, config->dns_ipv6_addr);
  my_setopt_slist(curl, CURLOPT_TELNETOPTIONS, config->telnet_options);
  my_setopt_long(curl, CURLOPT_CONNECTTIMEOUT_MS, config->connecttimeout_ms);
  MY_SETOPT_STR(curl, CURLOPT_DOH_URL, config->doh_url);
  my_setopt_long(curl, CURLOPT_FTP_CREATE_MISSING_DIRS,
                 (config->ftp_create_dirs ?
                  CURLFTP_CREATE_DIR_RETRY : CURLFTP_CREATE_DIR_NONE));
  my_setopt_offt(curl, CURLOPT_MAXFILESIZE_LARGE, config->max_filesize);
  my_setopt_long(curl, CURLOPT_IPRESOLVE, config->ip_version);
  if(config->socks5_gssapi_nec)
    my_setopt_long(curl, CURLOPT_SOCKS5_GSSAPI_NEC, 1);
  if(config->socks5_auth)
    my_setopt_bitmask(curl, CURLOPT_SOCKS5_AUTH, config->socks5_auth);
  MY_SETOPT_STR(curl, CURLOPT_SERVICE_NAME, config->service_name);
  my_setopt_long(curl, CURLOPT_IGNORE_CONTENT_LENGTH, config->ignorecl);

  if(config->localport) {
    my_setopt_long(curl, CURLOPT_LOCALPORT, config->localport);
    my_setopt_long(curl, CURLOPT_LOCALPORTRANGE, config->localportrange);
  }

  if(config->raw) {
    my_setopt_long(curl, CURLOPT_HTTP_CONTENT_DECODING, 0);
    my_setopt_long(curl, CURLOPT_HTTP_TRANSFER_DECODING, 0);
  }

  tcp_setopts(config, curl);

  if(config->tftp_blksize && proto_tftp)
    my_setopt_long(curl, CURLOPT_TFTP_BLKSIZE, config->tftp_blksize);

  MY_SETOPT_STR(curl, CURLOPT_MAIL_FROM, config->mail_from);
  my_setopt_slist(curl, CURLOPT_MAIL_RCPT, config->mail_rcpt);
  my_setopt_long(curl, CURLOPT_MAIL_RCPT_ALLOWFAILS,
                 config->mail_rcpt_allowfails);
  if(config->create_file_mode)
    my_setopt_long(curl, CURLOPT_NEW_FILE_PERMS, config->create_file_mode);

  if(config->proto_present)
    MY_SETOPT_STR(curl, CURLOPT_PROTOCOLS_STR, config->proto_str);
  if(config->proto_redir_present)
    MY_SETOPT_STR(curl, CURLOPT_REDIR_PROTOCOLS_STR, config->proto_redir_str);

  my_setopt_slist(curl, CURLOPT_RESOLVE, config->resolve);
  my_setopt_slist(curl, CURLOPT_CONNECT_TO, config->connect_to);

  if(feature_tls_srp) {
    result = tls_srp_setopts(config, curl);
    if(setopt_bad(result))
      return result;
  }

  if(config->gssapi_delegation)
    my_setopt_long(curl, CURLOPT_GSSAPI_DELEGATION, config->gssapi_delegation);

  MY_SETOPT_STR(curl, CURLOPT_MAIL_AUTH, config->mail_auth);
  MY_SETOPT_STR(curl, CURLOPT_SASL_AUTHZID, config->sasl_authzid);
  my_setopt_long(curl, CURLOPT_SASL_IR, config->sasl_ir);

  if(config->unix_socket_path) {
    if(config->abstract_unix_socket)
      MY_SETOPT_STR(curl, CURLOPT_ABSTRACT_UNIX_SOCKET,
                    config->unix_socket_path);
    else
      MY_SETOPT_STR(curl, CURLOPT_UNIX_SOCKET_PATH,
                    config->unix_socket_path);
  }

  MY_SETOPT_STR(curl, CURLOPT_DEFAULT_PROTOCOL, config->proto_default);
  my_setopt_long(curl, CURLOPT_TFTP_NO_OPTIONS,
                 config->tftp_no_options && proto_tftp);

  if(config->happy_eyeballs_timeout_ms != CURL_HET_DEFAULT)
    my_setopt_long(curl, CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
                   config->happy_eyeballs_timeout_ms);

  my_setopt_long(curl, CURLOPT_DISALLOW_USERNAME_IN_URL,
                 config->disallow_username_in_url);

  if(config->ip_tos > 0 || config->vlan_priority > 0) {
#if defined(IP_TOS) || defined(IPV6_TCLASS) || defined(SO_PRIORITY)
    my_setopt_ptr(curl, CURLOPT_SOCKOPTFUNCTION, sockopt_callback);
    my_setopt_ptr(curl, CURLOPT_SOCKOPTDATA, config);
#else
    if(config->ip_tos > 0) {
      errorf("Type of service is not supported in this build.");
      result = CURLE_NOT_BUILT_IN;
    }
    if(config->vlan_priority > 0) {
      errorf("VLAN priority is not supported in this build.");
      result = CURLE_NOT_BUILT_IN;
    }
#endif
  }
  my_setopt_long(curl, CURLOPT_UPLOAD_FLAGS, config->upload_flags);
  return result;
}
