text: #include "hphp/runtime/ext/curl/curl-resource.h"
  #include "hphp/runtime/ext/curl/curl-multi-resource.h"
  #include "hphp/runtime/ext/curl/curl-share-resource.h"
  #include "hphp/runtime/ext/curl/ext_curl.h"
  
  #include "hphp/runtime/base/array-init.h"
  #include "hphp/runtime/base/array-iterator.h"
  #include "hphp/runtime/base/builtin-functions.h"
  #include "hphp/runtime/base/curl-tls-workarounds.h"
  #include "hphp/runtime/base/file.h"
  #include "hphp/runtime/base/file-util.h"
  #include "hphp/runtime/base/plain-file.h"
  #include "hphp/runtime/base/stack-logger.h"
  #include "hphp/runtime/ext/extension.h"
  #include "hphp/runtime/server/server-stats.h"
  #include "hphp/runtime/vm/vm-regs.h"
  
  #include <curl/curl.h>
  #include <curl/easy.h>
  #include <curl/multi.h>
  #include <folly/portability/OpenSSL.h>
  
  #if (LIBCURL_VERSION_NUM >= 0x074600) && (OPENSSL_VERSION_NUMBER >= 0x10101000L)
  #define CERT_CACHE_SUPPORTED 1
  #else
  #undef CERT_CACHE_SUPPORTED
  #endif
  
  #define PHP_CURL_STDOUT 0
  #define PHP_CURL_FILE   1
  #define PHP_CURL_USER   2
  #define PHP_CURL_DIRECT 3
  #define PHP_CURL_RETURN 4
  #define PHP_CURL_ASCII  5
  #define PHP_CURL_BINARY 6
  #define PHP_CURL_IGNORE 7
  
  namespace {
  const HPHP::StaticString
    s_name("name"),
    s_mime("mime"),
    s_postname("postname");
  }
  
  namespace HPHP {
  /////////////////////////////////////////////////////////////////////////////
  // CurlResource
  
  CurlResource::ToFree::~ToFree() {
    for (unsigned int i = 0; i < str.size(); i++) {
      req::free(str[i]);
    }
    for (unsigned int i = 0; i < post.size(); i++) {
      curl_formfree(post[i]);
    }
    for (unsigned int i = 0; i < slist.size(); i++) {
      curl_slist_free_all(slist[i]);
    }
  }
  
  CurlResource::CurlResource(const String& url)
      : m_emptyPost(true), m_safeUpload(true) {
    m_cp = curl_easy_init();
    m_multi = nullptr;
    m_url = url;
  
    memset(m_error_str, 0, sizeof(m_error_str));
    m_error_no = CURLE_OK;
    m_to_free = req::make_shared<ToFree>();
  
    m_write.method = PHP_CURL_STDOUT;
    m_write.type   = PHP_CURL_ASCII;
    m_read.method  = PHP_CURL_DIRECT;
    m_write_header.method = PHP_CURL_IGNORE;
  
    setDefaultOptions();
  
    if (!url.empty()) {
  #if LIBCURL_VERSION_NUM >= 0x071100
      /* Strings passed to libcurl as 'char *' arguments, are copied by
         the library... NOTE: before 7.17.0 strings were not copied. */
      curl_easy_setopt(m_cp, CURLOPT_URL, url.c_str());
  #else
      char *urlcopy = req::strndup(url.data(), url.size());
      curl_easy_setopt(m_cp, CURLOPT_URL, urlcopy);
      m_to_free->str.push_back(urlcopy);
  #endif
    }
  }
  
  void CurlResource::sweep() {
    m_write.buf.release();
    m_write_header.buf.release();
    closeForSweep();
  }
  
  void CurlResource::close() {
    if (m_in_callback) {
      raise_warning("curl_close(): Attempt to close cURL in callback, ignored.");
      return;
    }
    closeForSweep();
    m_opts.clear();
    m_to_free.reset();
  }
  
  void CurlResource::closeForSweep() {
    assertx(!m_exception);
    assertx(IMPLIES(m_multi, m_cp));
    if (!m_cp) return;
    if (m_multi) m_multi->remove(this, /*leak=*/true);
    curl_easy_cleanup(m_cp);
    m_cp = nullptr;
  }
  
  void CurlResource::check_exception() {
    if (!m_exception) return;
    auto e = std::move(m_exception);
    assertx(!m_exception);
    if (isPhpException(e)) {
      throw_object(getPhpException(e));
    } else {
      getCppException(e)->throwException();
    }
  }
  
  void CurlResource::reseat() {
    // Note: this is the minimum set of things to point the CURL*
   // to this CurlHandle
    curl_easy_setopt(m_cp, CURLOPT_ERRORBUFFER,       m_error_str);
    curl_easy_setopt(m_cp, CURLOPT_FILE,              (void*)this);
    curl_easy_setopt(m_cp, CURLOPT_INFILE,            (void*)this);
    curl_easy_setopt(m_cp, CURLOPT_WRITEHEADER,       (void*)this);
    curl_easy_setopt(m_cp, CURLOPT_SSL_CTX_DATA,      (void*)this);
  }
  
  void CurlResource::reset() {
    curl_easy_reset(m_cp);
    setDefaultOptions();
  }
  
  void CurlResource::setDefaultOptions() {
    curl_easy_setopt(m_cp, CURLOPT_NOPROGRESS,        1);
    curl_easy_setopt(m_cp, CURLOPT_VERBOSE,           0);
    curl_easy_setopt(m_cp, CURLOPT_WRITEFUNCTION,     curl_write);
    curl_easy_setopt(m_cp, CURLOPT_READFUNCTION,      curl_read);
    curl_easy_setopt(m_cp, CURLOPT_HEADERFUNCTION,    curl_write_header);
    curl_easy_setopt(m_cp, CURLOPT_DNS_USE_GLOBAL_CACHE, 0); // for thread-safe
    curl_easy_setopt(m_cp, CURLOPT_DNS_CACHE_TIMEOUT, 120);
    curl_easy_setopt(m_cp, CURLOPT_MAXREDIRS, 20); // no infinite redirects
    curl_easy_setopt(m_cp, CURLOPT_NOSIGNAL, 1); // for multithreading mode
    curl_easy_setopt(m_cp, CURLOPT_SSL_CTX_FUNCTION,
                     CurlResource::ssl_ctx_callback);
  
    curl_easy_setopt(m_cp, CURLOPT_TIMEOUT,
                     minTimeout(RuntimeOption::HttpDefaultTimeout));
    curl_easy_setopt(m_cp, CURLOPT_CONNECTTIMEOUT,
                     minTimeout(RuntimeOption::HttpDefaultTimeout));
    reseat();
  }
  
  void CurlResource::prepare() {
    if (!useCertCache()) {
      if (auto const path = cainfo(false).get()) {
        curl_easy_setopt(m_cp, CURLOPT_CAINFO, path->data());
      }
      if (auto const path = cainfo(true).get()) {
        curl_easy_setopt(m_cp, CURLOPT_PROXY_CAINFO, path->data());
      }
    } else {
      curl_easy_setopt(m_cp, CURLOPT_CAINFO, NULL);
      curl_easy_setopt(m_cp, CURLOPT_PROXY_CAINFO, NULL);
    }
  }
  
  Variant CurlResource::execute() {
    assertx(!m_exception);
    if (m_cp == nullptr) {
      return false;
    }
    if (m_emptyPost) {
      // As per curl docs, an empty post must set POSTFIELDSIZE to be 0 or
      // the reader function will be called
      curl_easy_setopt(m_cp, CURLOPT_POSTFIELDSIZE, 0);
    }
    m_write.buf.clear();
    m_write.content.clear();
    m_header.clear();
    memset(m_error_str, 0, sizeof(m_error_str));
  
    prepare();
  
    {
      IOStatusHelper io("curl_easy_perform", m_url.data());
      SYNC_VM_REGS_SCOPED();
      if (m_in_exec) {
        log_native_stack("unexpected re-entry into curl_exec");
      }
      m_in_exec = true;
      // T29358191: curl_easy_perform should not throw... trust but verify
      try {
        m_error_no = curl_easy_perform(m_cp);
      } catch (...) {
        m_in_exec = false;
        log_native_stack("unexpected exception from curl_easy_perform");
        throw;
      }
      m_in_exec = false;
      check_exception();
    }
    set_curl_statuses(m_cp, m_url.data());
  
    /* CURLE_PARTIAL_FILE is returned by HEAD requests */
    if (m_error_no != CURLE_OK && m_error_no != CURLE_PARTIAL_FILE) {
      m_write.buf.clear();
      m_write.content.clear();
      return false;
    }
    if (m_write.method == PHP_CURL_RETURN) {
      if (!m_write.buf.empty()) {
        m_write.content = m_write.buf.detach();
      }
      if (!m_write.content.empty()) {
        return m_write.content;
      }
    }
    if (m_write.method == PHP_CURL_RETURN) {
      return empty_string_variant();
    }
    return true;
  }
  
  String CurlResource::getContents() {
    if (m_write.method == PHP_CURL_RETURN) {
      if (!m_write.buf.empty()) {
        m_write.content = m_write.buf.detach();
      }
      return m_write.content;
    }
    return String();
  }
  
  bool CurlResource::setOption(long option, const Variant& value) {
    if (m_cp == nullptr) {
      return false;
    }
    m_error_no = CURLE_OK;
  
    bool ret;
    if (isLongOption(option)) {
      ret = setLongOption(option, value.toInt64());
    } else if (isStringOption(option)) {
      ret = setStringOption(option, value.toString());
    } else if (isNullableStringOption(option)) {
      ret = setNullableStringOption(option, value);
    } else if (isFileOption(option)) {
      auto fp = dyn_cast_or_null<File>(value);
      if (!fp) return false;
      ret = setFileOption(option, fp);
    } else if (isStringListOption(option)) {
      ret = setStringListOption(option, value);
    } else if (isNonCurlOption(option)) {
      ret = setNonCurlOption(option, value);
    } else if (isBlobOption(option)) {
      ret = setBlobOption(option, value.toString());
    } else if (option == CURLOPT_POSTFIELDS) {
      ret = setPostFieldsOption(value);
    } else if (option == CURLOPT_SHARE) {
      auto curlsh = dyn_cast_or_null<CurlShareResource>(value);
      if (!curlsh || curlsh->isInvalid()) {
        return false;
      }
      m_error_no = curlsh->attachToCurlHandle(m_cp);
      ret = true;
    } else if (option == CURLINFO_HEADER_OUT) {
      if (value.toInt64() == 1) {
        curl_easy_setopt(m_cp, CURLOPT_DEBUGFUNCTION, curl_debug);
        curl_easy_setopt(m_cp, CURLOPT_DEBUGDATA, (void *)this);
        curl_easy_setopt(m_cp, CURLOPT_VERBOSE, 1);
      } else {
        curl_easy_setopt(m_cp, CURLOPT_DEBUGFUNCTION, nullptr);
        curl_easy_setopt(m_cp, CURLOPT_DEBUGDATA, nullptr);
        curl_easy_setopt(m_cp, CURLOPT_VERBOSE, 0);
      }
      ret = true;
    } else {
      m_error_no = CURLE_FAILED_INIT;
      raise_invalid_argument_warning("option: %ld", option);
      ret = false;
    }
  
    if (!ret) { return false; }
  
    m_opts.set(int64_t(option), value);
    return m_error_no == CURLE_OK;
  }
  
  Variant CurlResource::getOption(long option) {
    if (option == 0) {
      return m_opts;
    }
  
    if (m_opts.exists(int64_t(option))) {
      return m_opts[int64_t(option)];
    }
  
    return false;
  }
  
  CURL* CurlResource::get() {
    if (!m_cp) {
      throw_null_pointer_exception();
    }
    return m_cp;
  }
  
  bool CurlResource::isLongOption(long option) {
    switch (option) {
      case CURLOPT_DNS_USE_GLOBAL_CACHE:
        // This is not thread-safe when set to true, so pretend we don't know what
        // it is.
        return false;
  
      // These first few are in their own case statements in PHP
      case CURLOPT_CLOSEPOLICY:
      case CURLOPT_SSL_VERIFYHOST:
      case CURLOPT_FOLLOWLOCATION:
  #if LIBCURL_VERSION_NUM >= 0x071301
      case CURLOPT_POSTREDIR:
  #endif
  
      // Everything else
      case CURLOPT_AUTOREFERER:
      case CURLOPT_BUFFERSIZE:
      case CURLOPT_CONNECTTIMEOUT:
      case CURLOPT_COOKIESESSION:
      case CURLOPT_CRLF:
      case CURLOPT_DNS_CACHE_TIMEOUT:
      case CURLOPT_FAILONERROR:
      case CURLOPT_FILETIME:
      case CURLOPT_FORBID_REUSE:
      case CURLOPT_FRESH_CONNECT:
      case CURLOPT_FTP_USE_EPRT:
      case CURLOPT_FTP_USE_EPSV:
      case CURLOPT_HEADER:
      case CURLOPT_HTTPGET:
      case CURLOPT_HTTPPROXYTUNNEL:
      case CURLOPT_HTTP_VERSION:
      case CURLOPT_INFILESIZE:
      case CURLOPT_LOW_SPEED_LIMIT:
      case CURLOPT_LOW_SPEED_TIME:
      case CURLOPT_MAXCONNECTS:
      case CURLOPT_MAXREDIRS:
      case CURLOPT_NETRC:
      case CURLOPT_NOBODY:
      case CURLOPT_NOPROGRESS:
      case CURLOPT_NOSIGNAL:
      case CURLOPT_PORT:
      case CURLOPT_POST:
      case CURLOPT_PROXYPORT:
      case CURLOPT_PROXYTYPE:
      case CURLOPT_PUT:
      case CURLOPT_RESUME_FROM:
      case CURLOPT_SSLVERSION:
      case CURLOPT_SSL_VERIFYPEER:
      case CURLOPT_TIMECONDITION:
      case CURLOPT_TIMEOUT:
      case CURLOPT_TIMEVALUE:
      case CURLOPT_TRANSFERTEXT:
      case CURLOPT_UNRESTRICTED_AUTH:
      case CURLOPT_UPLOAD:
      case CURLOPT_VERBOSE:
  #if LIBCURL_VERSION_NUM >= 0x070a06 /* Available since 7.10.6 */
      case CURLOPT_HTTPAUTH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070a07 /* Available since 7.10.7 */
      case CURLOPT_FTP_CREATE_MISSING_DIRS:
      case CURLOPT_PROXYAUTH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070a08 /* Available since 7.10.8 */
      case CURLOPT_FTP_RESPONSE_TIMEOUT:
      case CURLOPT_IPRESOLVE:
      case CURLOPT_MAXFILESIZE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070b02 /* Available since 7.11.2 */
      case CURLOPT_TCP_NODELAY:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070c02 /* Available since 7.12.2 */
      case CURLOPT_FTPSSLAUTH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070e01 /* Available since 7.14.1 */
      case CURLOPT_IGNORE_CONTENT_LENGTH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070f00 /* Available since 7.15.0 */
      case CURLOPT_FTP_SKIP_PASV_IP:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070f01 /* Available since 7.15.1 */
      case CURLOPT_FTP_FILEMETHOD:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070f02 /* Available since 7.15.2 */
      case CURLOPT_CONNECT_ONLY:
      case CURLOPT_LOCALPORT:
      case CURLOPT_LOCALPORTRANGE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071000 /* Available since 7.16.0 */
      case CURLOPT_SSL_SESSIONID_CACHE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071001 /* Available since 7.16.1 */
      case CURLOPT_FTP_SSL_CCC:
      case CURLOPT_SSH_AUTH_TYPES:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071002 /* Available since 7.16.2 */
      case CURLOPT_CONNECTTIMEOUT_MS:
      case CURLOPT_HTTP_CONTENT_DECODING:
      case CURLOPT_HTTP_TRANSFER_DECODING:
      case CURLOPT_TIMEOUT_MS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071004 /* Available since 7.16.4 */
      case CURLOPT_NEW_DIRECTORY_PERMS:
      case CURLOPT_NEW_FILE_PERMS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071100 /* Available since 7.17.0 */
      case CURLOPT_USE_SSL:
  #elif LIBCURL_VERSION_NUM >= 0x070b00 /* Available since 7.11.0 */
      case CURLOPT_FTP_SSL:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071100 /* Available since 7.17.0 */
      case CURLOPT_APPEND:
      case CURLOPT_DIRLISTONLY:
  #else
      case CURLOPT_FTPAPPEND:
      case CURLOPT_FTPLISTONLY:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071200 /* Available since 7.18.0 */
      case CURLOPT_PROXY_TRANSFER_MODE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071300 /* Available since 7.19.0 */
      case CURLOPT_ADDRESS_SCOPE:
  #endif
  #if LIBCURL_VERSION_NUM >  0x071301 /* Available since 7.19.1 */
      case CURLOPT_CERTINFO:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071304 /* Available since 7.19.4 */
      case CURLOPT_PROTOCOLS:
      case CURLOPT_REDIR_PROTOCOLS:
      case CURLOPT_SOCKS5_GSSAPI_NEC:
      case CURLOPT_TFTP_BLKSIZE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071400 /* Available since 7.20.0 */
      case CURLOPT_FTP_USE_PRET:
      case CURLOPT_RTSP_CLIENT_CSEQ:
      case CURLOPT_RTSP_REQUEST:
      case CURLOPT_RTSP_SERVER_CSEQ:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071500 /* Available since 7.21.0 */
      case CURLOPT_WILDCARDMATCH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071504 /* Available since 7.21.4 */
      case CURLOPT_TLSAUTH_TYPE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071600 /* Available since 7.22.0 */
      case CURLOPT_GSSAPI_DELEGATION:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071800 /* Available since 7.24.0 */
      case CURLOPT_ACCEPTTIMEOUT_MS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071900 /* Available since 7.25.0 */
      case CURLOPT_SSL_OPTIONS:
      case CURLOPT_TCP_KEEPALIVE:
      case CURLOPT_TCP_KEEPIDLE:
      case CURLOPT_TCP_KEEPINTVL:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071f00 /* Available since 7.31.0 */
      case CURLOPT_SASL_IR:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072400 /* Available since 7.36.0 */
      case CURLOPT_EXPECT_100_TIMEOUT_MS:
      case CURLOPT_SSL_ENABLE_ALPN:
      case CURLOPT_SSL_ENABLE_NPN:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072500 /* Available since 7.37.0 */
      case CURLOPT_HEADEROPT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072900 /* Available since 7.41.0 */
      case CURLOPT_SSL_VERIFYSTATUS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072a00 /* Available since 7.42.0 */
      case CURLOPT_PATH_AS_IS:
      case CURLOPT_SSL_FALSESTART:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072b00 /* Available since 7.43.0 */
      case CURLOPT_PIPEWAIT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072e00 /* Available since 7.46.0 */
      case CURLOPT_STREAM_WEIGHT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073000 /* Available since 7.48.0 */
      case CURLOPT_TFTP_NO_OPTIONS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073100 /* Available since 7.49.0 */
      case CURLOPT_TCP_FASTOPEN:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073300 /* Available since 7.51.0 */
      case CURLOPT_KEEP_SENDING_ON_ERROR:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073400 /* Available since 7.52.0 */
      case CURLOPT_PROXY_SSLVERSION:
      case CURLOPT_PROXY_SSL_OPTIONS:
      case CURLOPT_PROXY_SSL_VERIFYHOST:
      case CURLOPT_PROXY_SSL_VERIFYPEER:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073600 /* Available since 7.54.0 */
      case CURLOPT_SUPPRESS_CONNECT_HEADERS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073700 /* Available since 7.55.0 */
      case CURLOPT_SOCKS5_AUTH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073800 /* Available since 7.56.0 */
      case CURLOPT_SSH_COMPRESSION:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073b00 /* Available since 7.59.0 */
      case CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS:
      case CURLOPT_TIMEVALUE_LARGE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073c00 /* Available since 7.60.0 */
      case CURLOPT_DNS_SHUFFLE_ADDRESSES:
      case CURLOPT_HAPROXYPROTOCOL:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073d00 /* Available since 7.61.0 */
      case CURLOPT_DISALLOW_USERNAME_IN_URL:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073e00 /* Available since 7.62.0 */
      case CURLOPT_UPKEEP_INTERVAL_MS:
      case CURLOPT_UPLOAD_BUFFERSIZE:
  #endif
  #if CURLOPT_MUTE != 0
      case CURLOPT_MUTE:
  #endif
        return true;
      default:
        return false;
    }
  }
  
  bool CurlResource::setLongOption(long option, long value) {
    if (option == CURLOPT_TIMEOUT) {
      value = minTimeout(value);
  #if LIBCURL_VERSION_NUM >= 0x071002
    } else if (option == CURLOPT_TIMEOUT_MS) {
      value = minTimeoutMS(value);
  #endif
    } else if ((option == CURLOPT_SSL_VERIFYHOST) && (value == 1)) {
      raise_notice(
        "curl_setopt(): CURLOPT_SSL_VERIFYHOST set to true which disables "
        "common name validation "
        "(setting CURLOPT_SSL_VERIFYHOST to 2 enables common name validation)"
      );
    }
  
    m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, value);
    return m_error_no == CURLE_OK;
  }
  
  bool CurlResource::isStringFilePathOption(long option) {
    switch (option) {
      case CURLOPT_COOKIEFILE:
      case CURLOPT_COOKIEJAR:
      case CURLOPT_RANDOM_FILE:
      case CURLOPT_SSLCERT:
  #if LIBCURL_VERSION_NUM >= 0x070b00 /* Available since 7.11.0 */
      case CURLOPT_NETRC_FILE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071001 /* Available since 7.16.1 */
      case CURLOPT_SSH_PRIVATE_KEYFILE:
      case CURLOPT_SSH_PUBLIC_KEYFILE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071300 /* Available since 7.19.0 */
      case CURLOPT_CRLFILE:
      case CURLOPT_ISSUERCERT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071306 /* Available since 7.19.6 */
      case CURLOPT_SSH_KNOWNHOSTS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073400 /* Available since 7.52.0 */
      case CURLOPT_PROXY_CRLFILE:
      case CURLOPT_PROXY_SSLCERT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x074700 /* Available since 7.71.0 */
      case CURLOPT_PROXY_ISSUERCERT:
  #endif
        return true;
      default:
        return false;
    }
  }
  
  bool CurlResource::isStringOption(long option) {
    switch (option) {
      // Not in PHP's main string case
      case CURLOPT_PRIVATE:
      case CURLOPT_URL:
      case CURLOPT_KRB4LEVEL:
  
      // Everything else
      case CURLOPT_CAINFO:
      case CURLOPT_CAPATH:
      case CURLOPT_COOKIE:
      case CURLOPT_EGDSOCKET:
      case CURLOPT_INTERFACE:
      case CURLOPT_PROXY:
      case CURLOPT_PROXYUSERPWD:
      case CURLOPT_REFERER:
      case CURLOPT_SSLCERTTYPE:
      case CURLOPT_SSLENGINE:
      case CURLOPT_SSLENGINE_DEFAULT:
      case CURLOPT_SSLKEY:
      case CURLOPT_SSLKEYPASSWD:
      case CURLOPT_SSLKEYTYPE:
      case CURLOPT_SSL_CIPHER_LIST:
      case CURLOPT_USERAGENT:
      case CURLOPT_USERPWD:
  #if LIBCURL_VERSION_NUM >= 0x070e01 /* Available since 7.14.1 */
      case CURLOPT_COOKIELIST:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x070f05 /* Available since 7.15.5 */
      case CURLOPT_FTP_ALTERNATIVE_TO_USER:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071101 /* Available since 7.17.1 */
      case CURLOPT_SSH_HOST_PUBLIC_KEY_MD5:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071301 /* Available since 7.19.1 */
      case CURLOPT_PASSWORD:
      case CURLOPT_PROXYPASSWORD:
      case CURLOPT_PROXYUSERNAME:
      case CURLOPT_USERNAME:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071304 /* Available since 7.19.4 */
      case CURLOPT_NOPROXY:
      case CURLOPT_SOCKS5_GSSAPI_SERVICE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071400 /* Available since 7.20.0 */
      case CURLOPT_MAIL_FROM:
      case CURLOPT_RTSP_STREAM_URI:
      case CURLOPT_RTSP_TRANSPORT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071504 /* Available since 7.21.4 */
      case CURLOPT_TLSAUTH_PASSWORD:
      case CURLOPT_TLSAUTH_USERNAME:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071506 /* Available since 7.21.6 */
      case CURLOPT_ACCEPT_ENCODING:
      case CURLOPT_TRANSFER_ENCODING:
  #else
      case CURLOPT_ENCODING:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071800 /* Available since 7.24.0 */
      case CURLOPT_DNS_SERVERS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071900 /* Available since 7.25.0 */
      case CURLOPT_MAIL_AUTH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072200 /* Available since 7.34.0 */
      case CURLOPT_LOGIN_OPTIONS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072700 /* Available since 7.39.0 */
      case CURLOPT_PINNEDPUBLICKEY:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072b00 /* Available since 7.43.0 */
      case CURLOPT_PROXY_SERVICE_NAME:
      case CURLOPT_SERVICE_NAME:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072d00 /* Available since 7.45.0 */
      case CURLOPT_DEFAULT_PROTOCOL:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073400 /* Available since 7.52.0 */
      case CURLOPT_PROXY_CAINFO:
      case CURLOPT_PROXY_CAPATH:
      case CURLOPT_PROXY_KEYPASSWD:
      case CURLOPT_PROXY_PINNEDPUBLICKEY:
      case CURLOPT_PROXY_SSLCERTTYPE:
      case CURLOPT_PROXY_SSLKEY:
      case CURLOPT_PROXY_SSLKEYTYPE:
      case CURLOPT_PROXY_SSL_CIPHER_LIST:
      case CURLOPT_PROXY_TLSAUTH_PASSWORD:
      case CURLOPT_PROXY_TLSAUTH_TYPE:
      case CURLOPT_PROXY_TLSAUTH_USERNAME:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073d00 /* Available since 7.61.0 */
      case CURLOPT_PROXY_TLS13_CIPHERS:
      case CURLOPT_TLS13_CIPHERS:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073e00 /* Available since 7.62.0 */
      case CURLOPT_DOH_URL:
  #endif
        return true;
      default:
        return isStringFilePathOption(option);
    }
  }
  
  bool CurlResource::setStringOption(long option, const String& value) {
    assertx(isStringOption(option));
  
    // the following options deal with files, therefore the open_basedir check
    // is required.
    if (isStringFilePathOption(option) && !value.empty()) {
      String filename = File::TranslatePath(value);
      if (filename.empty()) {
        raise_warning(
          "open_basedir restriction in effect. File(%s) is not within "
          "the allowed paths",
          value.data()
        );
        return false;
      }
    }
  
    if (option == CURLOPT_URL) m_url = value;
  
  #if LIBCURL_VERSION_NUM >= 0x071100
    /* Strings passed to libcurl as 'char *' arguments, are copied
       by the library... NOTE: before 7.17.0 strings were not copied. */
    m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, value.c_str());
  #else
    char *copystr = req::strndup(value.data(), value.size());
    m_to_free->str.push_back(copystr);
    m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, copystr);
  #endif
  
    return m_error_no == CURLE_OK;
  }
  
  bool CurlResource::isNullableStringOption(long option) {
    switch (option) {
      case CURLOPT_CUSTOMREQUEST:
      case CURLOPT_FTPPORT:
      case CURLOPT_RANGE:
  #if LIBCURL_VERSION_NUM >= 0x070d00 /* Available since 7.13.0 */
      case CURLOPT_FTP_ACCOUNT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071400 /* Available since 7.20.0 */
      case CURLOPT_RTSP_SESSION_ID:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072100 /* Available since 7.33.0 */
      case CURLOPT_DNS_INTERFACE:
      case CURLOPT_DNS_LOCAL_IP4:
      case CURLOPT_DNS_LOCAL_IP6:
      case CURLOPT_XOAUTH2_BEARER:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072800 /* Available since 7.40.0 */
      case CURLOPT_UNIX_SOCKET_PATH:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073500 /* Available since 7.53.0 */
      case CURLOPT_ABSTRACT_UNIX_SOCKET:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073700 /* Available since 7.55.0 */
      case CURLOPT_REQUEST_TARGET:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071004 /* Available since 7.16.4 */
      case CURLOPT_KRBLEVEL:
  #else
      case CURLOPT_KRB4LEVEL:
  #endif
        return true;
      default:
        return false;
    }
  }
  
  bool CurlResource::setNullableStringOption(long option, const Variant& value) {
    assertx(isNullableStringOption(option));
  
    if (value.isNull()) {
      m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, nullptr);
    } else {
      auto const strValue = value.toString();
  #if LIBCURL_VERSION_NUM >= 0x071100
      /* Strings passed to libcurl as 'char *' arguments, are copied
         by the library... NOTE: before 7.17.0 strings were not copied. */
      m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, strValue.c_str());
  #else
      char *copystr = req::strndup(strValue.data(), strValue.size());
      m_to_free->str.push_back(copystr);
      m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, copystr);
  #endif
    }
  
    return m_error_no == CURLE_OK;
  }
  
  bool CurlResource::isBlobOption(long option) {
  #if LIBCURL_VERSION_NUM >= 0x074700 /* Available since 7.71.0 */
    switch (option) {
    case CURLOPT_SSLCERT_BLOB:
    case CURLOPT_SSLKEY_BLOB:
    case CURLOPT_PROXY_SSLCERT_BLOB:
    case CURLOPT_PROXY_SSLKEY_BLOB:
    case CURLOPT_ISSUERCERT_BLOB:
    case CURLOPT_PROXY_ISSUERCERT_BLOB:
      return true;
    }
  #endif
    return false;
  }
  
  bool CurlResource::setBlobOption(long option, const String& value) {
  #if LIBCURL_VERSION_NUM >= 0x074700 /* Available since 7.71.0 */
    assertx(isBlobOption(option));
    struct curl_blob blob;
    blob.data = (void *)value.data();
    blob.len = value.size();
    blob.flags = CURL_BLOB_COPY;
    m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, &blob);
    return m_error_no == CURLE_OK;
  #else
    return false;
  #endif
  }
  
  bool CurlResource::setPostFieldsOption(const Variant& value) {
    m_emptyPost = false;
  
    if (!value.isArray() && !value.is(KindOfObject)) {
      String svalue = value.toString();
  #if LIBCURL_VERSION_NUM >= 0x071100
      /* with curl 7.17.0 and later, we can use COPYPOSTFIELDS,
         but we have to provide size before */
      m_error_no = curl_easy_setopt(m_cp, CURLOPT_POSTFIELDSIZE,
                                    svalue.size());
      m_error_no = curl_easy_setopt(m_cp, CURLOPT_COPYPOSTFIELDS,
                                  svalue.c_str());
  #else
      char *post = req::strndup(svalue.data(), svalue.size());
      m_to_free->str.push_back(post);
  
      m_error_no = curl_easy_setopt(m_cp, CURLOPT_POSTFIELDS, post);
      m_error_no = curl_easy_setopt(m_cp, CURLOPT_POSTFIELDSIZE,
                                    svalue.size());
  #endif
      return m_error_no == CURLE_OK;
    }
  
    Array arr = value.toArray();
    curl_httppost *first = nullptr;
    curl_httppost *last  = nullptr;
    for (ArrayIter iter(arr); iter; ++iter) {
      String key = iter.first().toString();
      Variant var_val = iter.second();
      if (UNLIKELY(var_val.isObject()
          && var_val.toObject()->instanceof(SystemLib::s_CURLFileClass))) {
        Object val = var_val.toObject();
  
        String name = val->o_get(s_name).toString();
        String mime = val->o_get(s_mime).toString();
        String postname = val->o_get(s_postname).toString();
  
        m_error_no = (CURLcode)curl_formadd
          (&first, &last,
           CURLFORM_COPYNAME, key.data(),
           CURLFORM_NAMELENGTH, (long)key.size(),
           CURLFORM_FILENAME, postname.empty()
                              ? name.c_str()
                              : postname.c_str(),
           CURLFORM_CONTENTTYPE, mime.empty()
                                 ? "application/octet-stream"
                                 : mime.c_str(),
           CURLFORM_FILE, name.c_str(),
           CURLFORM_END);
      } else {
        String val = var_val.toString();
        auto postval = val.data();
  
        if (!RuntimeOption::PHP7_DisallowUnsafeCurlUploads &&
            !m_safeUpload &&
            *postval == '@' &&
            strlen(postval) == val.size()) {
          /* Given a string like:
           *   "@/foo/bar;type=herp/derp;filename=ponies\0"
           * - Temporarily convert to:
           *   "@/foo/bar\0type=herp/derp\0filename=ponies\0"
           * - Pass pointers to the relevant null-terminated substrings to
           *   curl_formadd
           * - Revert changes to postval at the end
           */
          raise_deprecated(
            "curl_setopt(): The usage of the @filename API for file uploading "
            "is deprecated. Please use the CURLFile class instead"
          );
  
          if (val.get()->hasMultipleRefs()) {
            val = String::attach(
              StringData::Make(val.data(), val.size(), CopyString));
          }
          auto slice = val.bufferSlice();
          char* mutablePostval = slice.begin() + 1;
          char* type = strstr(mutablePostval, ";type=");
          char* filename = strstr(mutablePostval, ";filename=");
  
          if (type) { *type = '\0'; }
          if (filename) { *filename = '\0'; }
  
          String localName = File::TranslatePath(mutablePostval);
  
          /* The arguments after _NAMELENGTH and _CONTENTSLENGTH
           * must be explicitly cast to long in curl_formadd
           * use since curl needs a long not an int. */
          m_error_no = (CURLcode)curl_formadd
            (&first, &last,
             CURLFORM_COPYNAME, key.data(),
             CURLFORM_NAMELENGTH, (long)key.size(),
             CURLFORM_FILENAME, filename
                                ? filename + sizeof(";filename=") - 1
                                : postval,
             CURLFORM_CONTENTTYPE, type
                                   ? type + sizeof(";type=") - 1
                                   : "application/octet-stream",
             CURLFORM_FILE, localName.c_str(),
             CURLFORM_END);
  
          if (type) { *type = ';'; }
          if (filename) { *filename = ';'; }
        } else {
          m_error_no = (CURLcode)curl_formadd
            (&first, &last,
             CURLFORM_COPYNAME, key.data(),
             CURLFORM_NAMELENGTH, (long)key.size(),
             CURLFORM_COPYCONTENTS, postval,
             CURLFORM_CONTENTSLENGTH,(long)val.size(),
             CURLFORM_END);
        }
      }
    }
  
    if (m_error_no != CURLE_OK) {
      return false;
    }
    m_to_free->post.push_back(first);
    m_error_no = curl_easy_setopt(m_cp, CURLOPT_HTTPPOST, first);
    return true;
  }
  
  bool CurlResource::isFileOption(long option) {
    return (option == CURLOPT_FILE) ||
           (option == CURLOPT_INFILE) ||
           (option == CURLOPT_WRITEHEADER) ||
           (option == CURLOPT_STDERR);
  }
  
  inline bool checkWritable(const std::string& mode) {
    // This check is really hinky, but it's what PHP does. :/
    if ((mode.find('r') != 0) || (mode.find('+') == 1)) {
      return true;
    }
    raise_warning("curl_setopt(): the provided file handle is not writable");
    return false;
  }
  
  bool CurlResource::setFileOption(long option, const req::ptr<File>& fp) {
    assertx(isFileOption(option));
    if (option == CURLOPT_FILE) {
      if (!checkWritable(fp->getMode())) return false;
      m_write.fp = fp;
      m_write.method = PHP_CURL_FILE;
      return true;
    }
    if (option == CURLOPT_WRITEHEADER) {
      if (!checkWritable(fp->getMode())) return false;
      m_write_header.fp = fp;
      m_write_header.method = PHP_CURL_FILE;
      return true;
    }
  
    if (option == CURLOPT_INFILE) {
      m_read.fp = fp;
      m_emptyPost = false;
      return true;
    }
  
    assertx(option == CURLOPT_STDERR);
    auto pf = dyn_cast<PlainFile>(fp);
    if (!pf) {
      return false;
    }
    auto innerFp = pf->getStream();
    if (!innerFp) {
      return false;
    }
    m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, innerFp);
    return true;
  }
  
  bool CurlResource::isStringListOption(long option) {
    switch (option) {
      case CURLOPT_HTTP200ALIASES:
      case CURLOPT_HTTPHEADER:
      case CURLOPT_POSTQUOTE:
      case CURLOPT_PREQUOTE:
      case CURLOPT_QUOTE:
      case CURLOPT_TELNETOPTIONS:
  #if LIBCURL_VERSION_NUM >= 0x071400 /* Available since 7.20.0 */
      case CURLOPT_MAIL_RCPT:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x071503 /* Available since 7.21.3 */
      case CURLOPT_RESOLVE:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x072500 /* Available since 7.37.0 */
      case CURLOPT_PROXYHEADER:
  #endif
  #if LIBCURL_VERSION_NUM >= 0x073100 /* Available since 7.49.0 */
      case CURLOPT_CONNECT_TO:
  #endif
        return true;
      default:
        return false;
    }
  }
  
  bool CurlResource::setStringListOption(long option, const Variant& value) {
    if (!value.isArray() && !value.is(KindOfObject)) {
      raise_warning("You must pass either an object or an array with "
                    "the CURLOPT_HTTPHEADER, CURLOPT_PROXYHEADER, CURLOPT_QUOTE, "
                    "CURLOPT_HTTP200ALIASES, CURLOPT_POSTQUOTE "
                    "and CURLOPT_RESOLVE arguments");
      return false;
    }
  
    Array arr = value.toArray();
    curl_slist *slist = nullptr;
    for (ArrayIter iter(arr); iter; ++iter) {
      String key = iter.first().toString();
      String val = iter.second().toString();
  
      slist = curl_slist_append(slist, val.c_str());
      if (!slist) {
        raise_warning("Could not build curl_slist");
        return false;
      }
    }
  
    m_to_free->slist.push_back(slist);
    m_error_no = curl_easy_setopt(m_cp, (CURLoption)option, slist);
    return true;
  }
  
  bool CurlResource::isNonCurlOption(long option) {
    return (option == CURLOPT_RETURNTRANSFER) ||
           (option == CURLOPT_BINARYTRANSFER) ||
           (option == CURLOPT_WRITEFUNCTION) ||
           (option == CURLOPT_READFUNCTION) ||
           (option == CURLOPT_HEADERFUNCTION) ||
           (option == CURLOPT_PROGRESSFUNCTION) ||
           (option == CURLOPT_FB_TLS_VER_MAX) ||
           (option == CURLOPT_FB_TLS_CIPHER_SPEC) ||
           (option == CURLOPT_SAFE_UPLOAD);
  }
  
  bool CurlResource::setNonCurlOption(long option, const Variant& value) {
    assertx(isNonCurlOption(option));
    switch (option) {
      case CURLOPT_RETURNTRANSFER:
        m_write.method = value.toBoolean() ? PHP_CURL_RETURN : PHP_CURL_STDOUT;
        return true;
      case CURLOPT_BINARYTRANSFER:
        m_write.type = value.toBoolean() ? PHP_CURL_BINARY : PHP_CURL_ASCII;
        return true;
      case CURLOPT_WRITEFUNCTION:
        m_write.callback = value;
        m_write.method = PHP_CURL_USER;
        return true;
      case CURLOPT_READFUNCTION:
        m_read.callback = value;
        m_read.method = PHP_CURL_USER;
        m_emptyPost = false;
        return true;
      case CURLOPT_HEADERFUNCTION:
        m_write_header.callback = value;
        m_write_header.method = PHP_CURL_USER;
        return true;
      case CURLOPT_PROGRESSFUNCTION:
        m_progress_callback = value;
        curl_easy_setopt(m_cp, CURLOPT_PROGRESSDATA, (void*) this);
        curl_easy_setopt(m_cp, CURLOPT_PROGRESSFUNCTION, curl_progress);
        return true;
      case CURLOPT_FB_TLS_VER_MAX:
        if (value.isInteger()) {
          auto val = value.toInt64();
          if (val == CURLOPT_FB_TLS_VER_MAX_1_0 ||
              val == CURLOPT_FB_TLS_VER_MAX_1_1 ||
              val == CURLOPT_FB_TLS_VER_MAX_NONE) {
            return true;
          }
        }
        raise_warning("You must pass CURLOPT_FB_TLS_VER_MAX_1_0, "
                      "CURLOPT_FB_TLS_VER_MAX_1_1 or "
                      "CURLOPT_FB_TLS_VER_MAX_NONE with "
                      "CURLOPT_FB_TLS_VER_MAX");
        return false;
      case CURLOPT_FB_TLS_CIPHER_SPEC:
        if (!value.isString() || value.toString().empty()) {
          raise_warning("CURLOPT_FB_TLS_CIPHER_SPEC requires a non-empty string");
        }
        return false;
      case CURLOPT_SAFE_UPLOAD:
        if (RuntimeOption::PHP7_DisallowUnsafeCurlUploads &&
            value.toInt64() == 0) {
          raise_warning(
            "curl_setopt(): Disabling safe uploads is no longer supported"
          );
          return false;
        }
        m_safeUpload = value.toBoolean();
        return true;
    }
    return false;
  }
  
  inline int64_t minTimeoutImpl(int64_t timeout, int64_t multiple) {
    auto info = RequestInfo::s_requestInfo.getNoCheck();
    auto& data = info->m_reqInjectionData;
    if (!data.getTimeout()) {
      return timeout;
    }
    return std::min<int64_t>(multiple * data.getRemainingTime(), timeout);
  }
  
  int64_t CurlResource::minTimeout(int64_t timeout) {
    return minTimeoutImpl(timeout, 1);
  }
  
  int64_t CurlResource::minTimeoutMS(int64_t timeout) {
    return minTimeoutImpl(timeout, 1000);
  }
  
  void CurlResource::handle_exception() {
    assertx(!m_exception);
    try {
      throw;
    } catch (const Object& e) {
      m_exception.emplace(e);
    } catch (Exception& e) {
      m_exception.emplace(e.clone());
    } catch (std::exception& e) {
      m_exception.emplace(
        new FatalErrorException(0,
                                "Unexpected error in curl callback: %s",
                                e.what())
      );
    } catch (...) {
      m_exception.emplace(
        new FatalErrorException("Unknown error in curl callback")
      );
    }
  }
  
  size_t CurlResource::curl_read(char *data,
                                 size_t size, size_t nmemb, void *ctx) {
    CurlResource *ch = (CurlResource *)ctx;
    if (!ch->m_in_exec) {
      // T29358191: who's calling, and are they dealing with m_exception?
      log_native_stack("unexpected curl_read");
    }
    ReadHandler *t  = &ch->m_read;
  
    int length = -1;
    try {
      switch (t->method) {
        case PHP_CURL_DIRECT:
          if (t->fp) {
            int data_size = size * nmemb;
            String ret = t->fp->read(data_size);
            length = ret.size();
            if (length) {
              memcpy(data, ret.data(), length);
            }
          }
          break;
        case PHP_CURL_USER: {
          int data_size = size * nmemb;
          ch->m_in_callback = true;
          SCOPE_EXIT {
            ch->m_in_callback = false;
          };
          Variant ret = vm_call_user_func(
            t->callback,
            make_vec_array(Resource(ch), Resource(t->fp), data_size));
          if (ret.isString()) {
            String sret = ret.toString();
            length = data_size < sret.size() ? data_size : sret.size();
            memcpy(data, sret.data(), length);
          }
          break;
        }
      }
    } catch (...) {
      ch->handle_exception();
      return CURL_READFUNC_ABORT;
    }
    return length;
  }
  
  size_t CurlResource::curl_write(char *data,
                                  size_t size, size_t nmemb, void *ctx) {
    CurlResource *ch = (CurlResource *)ctx;
    if (!ch->m_in_exec) {
      // T29358191: who's calling, and are they dealing with m_exception?
      log_native_stack("unexpected curl_write");
    }
    WriteHandler *t  = &ch->m_write;
    size_t length = size * nmemb;
  
    try {
      switch (t->method) {
        case PHP_CURL_STDOUT:
          g_context->write(data, length);
          break;
        case PHP_CURL_FILE:
          return t->fp->write(String(data, length, CopyString), length);
        case PHP_CURL_RETURN:
          if (length > 0) {
            t->buf.append(data, (int)length);
          }
          break;
        case PHP_CURL_USER: {
          ch->m_in_callback = true;
          SCOPE_EXIT {
            ch->m_in_callback = false;
          };
          Variant ret = vm_call_user_func(
            t->callback,
            make_vec_array(Resource(ch), String(data, length, CopyString)));
          length = ret.toInt64();
          break;
        }
      }
    } catch (...) {
      ch->handle_exception();
      return 0;
    }
    return length;
  }
  
  size_t CurlResource::curl_write_header(char *data,
                                         size_t size, size_t nmemb, void *ctx) {
    CurlResource *ch = (CurlResource *)ctx;
    if (!ch->m_in_exec) {
      // T29358191: who's calling, and are they dealing with m_exception?
      log_native_stack("unexpected curl_write_header");
    }
    WriteHandler *t  = &ch->m_write_header;
    size_t length = size * nmemb;
  
    try {
      switch (t->method) {
        case PHP_CURL_STDOUT:
          // Handle special case write when we're returning the entire transfer
          if (ch->m_write.method == PHP_CURL_RETURN && length > 0) {
            ch->m_write.buf.append(data, (int)length);
          } else {
            g_context->write(data, length);
          }
          break;
        case PHP_CURL_FILE:
          return t->fp->write(String(data, length, CopyString), length);
        case PHP_CURL_USER: {
          ch->m_in_callback = true;
          SCOPE_EXIT {
            ch->m_in_callback = false;
          };
          Variant ret = vm_call_user_func(
            t->callback,
            make_vec_array(Resource(ch), String(data, length, CopyString)));
          length = ret.toInt64();
          break;
        }
        case PHP_CURL_IGNORE:
          return length;
        default:
          return (size_t)-1;
      }
    } catch (...) {
      ch->handle_exception();
      return 0;
    }
    return length;
  }
  
  int CurlResource::curl_debug(CURL* /*cp*/, curl_infotype type, char* buf,
                               size_t buf_len, void* ctx) {
    CurlResource *ch = (CurlResource *)ctx;
    if (type == CURLINFO_HEADER_OUT && buf_len > 0) {
      ch->m_header = String(buf, buf_len, CopyString);
    }
    return 0;
  }
  
  int CurlResource::curl_progress(void* p,
                                  double dltotal, double dlnow,
                                  double ultotal, double ulnow) {
    assertx(p);
    CurlResource* curl = static_cast<CurlResource*>(p);
    if (!curl->m_in_exec) {
      // T29358191: who's calling, and are they dealing with m_exception?
      log_native_stack("unexpected curl_progress");
    }
  
    VecInit pai(5);
    pai.append(Resource(curl));
    pai.append(dltotal);
    pai.append(dlnow);
    pai.append(ultotal);
    pai.append(ulnow);
  
    try {
      Variant result = vm_call_user_func(
        curl->m_progress_callback,
        pai.toArray()
      );
      // Both PHP and libcurl are documented as return 0 to continue, non-zero
      // to abort, however this is what Zend actually implements
      return result.toInt64() == 0 ? 0 : 1;
    } catch (...) {
      curl->handle_exception();
      return 1;
    }
  }
  
  namespace {
  hphp_fast_map<String, X509_STORE*, hphp_string_hash, hphp_string_same> s_certCache;
  folly::SharedMutex s_mutex;
  }
  
  bool CurlResource::useCertCache() const {
  #ifdef CERT_CACHE_SUPPORTED
    auto const isNonEmpty = [&](int64_t option) {
      if (!m_opts.exists(option)) return false;
      Variant untyped_value = m_opts[option];
      if (untyped_value.isString() && !untyped_value.toString().empty()) {
        return true;
      }
      return false;
    };
  
    if (isNonEmpty(CURLOPT_PROXY) && cainfo(false) != cainfo(true)) {
      return false;
    }
    if (isNonEmpty(CURLOPT_CRLFILE)) {
      return false;
    }
    if (isNonEmpty(CURLOPT_CAPATH)) {
      return false;
    }
  
    return true;
  #else
    return false;
  #endif
  }
  
  String CurlResource::cainfo(bool proxy) const {
  #ifdef CERT_CACHE_SUPPORTED
    auto const option = proxy ? CURLOPT_PROXY_CAINFO : CURLOPT_CAINFO;
  
    static auto const defaultCainfoData = [&] () -> StringData* {
      auto const curlInfo = curl_version_info(CURLVERSION_NOW);
      if (!curlInfo->cainfo) return nullptr;
      return makeStaticString(curlInfo->cainfo);
    }();
  
    auto const defaultCainfo = defaultCainfoData ? String{defaultCainfoData} : String{};
    if (!m_opts.exists(int64_t(option))) return defaultCainfo;
  
    Variant untyped_value = m_opts[int64_t(option)];
    if (!untyped_value.isString()) return defaultCainfo;
  
    auto const string_value = untyped_value.toString();
    if (string_value.empty()) return defaultCainfo;
  
    return string_value;
  #else
    return String{};
  #endif
  }
  
  CURLcode CurlResource::ssl_ctx_callback(CURL *curl, void *sslctx, void *parm) {
    // Set defaults from config.hdf
    CURLcode r = curl_tls_workarounds_cb(curl, sslctx, parm);
    if (r != CURLE_OK) { return r; }
  
    // Convert params to proper types.
    SSL_CTX* ctx = (SSL_CTX*)sslctx;
    if (ctx == nullptr) {
      raise_warning("supplied argument is not a valid SSL_CTX");
      return CURLE_FAILED_INIT;
    }
    CurlResource* cp = (CurlResource*)parm;
    if (cp == nullptr) {
      raise_warning("supplied argument is not a valid cURL handle resource");
      return CURLE_FAILED_INIT;
    }
  
  #ifdef CERT_CACHE_SUPPORTED
    // Load the CA from the cache.
    if (cp->useCertCache()) {
      auto const cainfo = cp->cainfo(false);
      auto const store = [&] () -> X509_STORE* {
        {
          folly::SharedMutex::ReadHolder lock(s_mutex);
          auto const iter = s_certCache.find(cainfo);
          if (iter != s_certCache.end()) return iter->second;
        }
  
        STACK_OF(X509_INFO) *stack;
        BIO *in;
  
        in = BIO_new_file(cainfo.data(), "r");
        if (!in) return nullptr;
        stack = PEM_X509_INFO_read_bio(in, nullptr, nullptr, (void*)"");
        BIO_free(in);
        if (!stack) return nullptr;
  
        auto const store = X509_STORE_new();
        if (!store) return nullptr;
        X509_STORE_set_flags(store, X509_V_FLAG_TRUSTED_FIRST);
        X509_STORE_set_flags(store, X509_V_FLAG_PARTIAL_CHAIN);
  
        unsigned count = 0;
        for (int i = 0; i < sk_X509_INFO_num(stack); i++) {
          X509_INFO* info;
          info = sk_X509_INFO_value(stack, i);
          if (info->x509) {
            if (!X509_STORE_add_cert(store, info->x509)) {
              X509_STORE_free(store);
              sk_X509_INFO_pop_free(stack, X509_INFO_free);
              return nullptr;
            }
            count++;
          }
          if (info->crl) {
            if (!X509_STORE_add_crl(store, info->crl)) {
              X509_STORE_free(store);
              sk_X509_INFO_pop_free(stack, X509_INFO_free);
              return nullptr;
            }
            count++;
          }
        }
        sk_X509_INFO_pop_free(stack, X509_INFO_free);
        if (count == 0) {
          X509_STORE_free(store);
          return nullptr;
        }
  
        folly::SharedMutex::WriteHolder lock(s_mutex);
        auto const iter = s_certCache.find(cainfo);
        if (iter != s_certCache.end()) {
          X509_STORE_free(store);
          return iter->second;
        } else {
          s_certCache.emplace(cainfo, store);
          return store;
        }
      }();
      if (!store) return CURLE_FAILED_INIT;
      SSL_CTX_set1_cert_store(ctx, store);
    }
  #endif
  
    // Override cipher specs if necessary.
    if (cp->m_opts.exists(int64_t(CURLOPT_FB_TLS_CIPHER_SPEC))) {
      Variant untyped_value = cp->m_opts[int64_t(CURLOPT_FB_TLS_CIPHER_SPEC)];
      if (untyped_value.isString() && !untyped_value.toString().empty()) {
        SSL_CTX_set_cipher_list(ctx, untyped_value.toString().c_str());
      } else {
        raise_warning("CURLOPT_FB_TLS_CIPHER_SPEC requires a non-empty string");
      }
    }
  
    // Override the maximum client TLS version if necessary.
    if (cp->m_opts.exists(int64_t(CURLOPT_FB_TLS_VER_MAX))) {
      // Get current options, unsetting the NO_TLSv1_* bits.
      long cur_opts = SSL_CTX_get_options(ctx);
  #ifdef SSL_OP_NO_TLSv1_1
      cur_opts &= ~SSL_OP_NO_TLSv1_1;
  #endif
  #ifdef SSL_OP_NO_TLSv1_2
      cur_opts &= ~SSL_OP_NO_TLSv1_2;
  #endif
      int64_t value = cp->m_opts[int64_t(CURLOPT_FB_TLS_VER_MAX)].toInt64();
      if (value == CURLOPT_FB_TLS_VER_MAX_1_0) {
  #if defined (SSL_OP_NO_TLSv1_1) && defined (SSL_OP_NO_TLSv1_2)
        cur_opts |= SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2;
  #else
        raise_notice("Requesting SSL_OP_NO_TLSv1_1, but this version of "
                     "SSL does not support that option");
  #endif
      } else if (value == CURLOPT_FB_TLS_VER_MAX_1_1) {
  #ifdef SSL_OP_NO_TLSv1_2
        cur_opts |= SSL_OP_NO_TLSv1_2;
  #else
        raise_notice("Requesting SSL_OP_NO_TLSv1_2, but this version of "
                     "SSL does not support that option");
  #endif
      } else if (value != CURLOPT_FB_TLS_VER_MAX_NONE) {
        raise_notice("Invalid CURLOPT_FB_TLS_VER_MAX value");
      }
      SSL_CTX_set_options(ctx, cur_opts);
    }
  
    return CURLE_OK;
  }
  
  /////////////////////////////////////////////////////////////////////////////
  }
  