/***************************************************************************
 *                                  _   _ ____  _
 *  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
 *
 ***************************************************************************/
/*
 * IDN conversions
 */
#include "curl_setup.h"

#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h>
#endif
#ifdef HAVE_NETDB_H
#include <netdb.h>
#endif
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
#ifdef HAVE_NET_IF_H
#include <net/if.h>
#endif
#ifdef HAVE_IPHLPAPI_H
#include <Iphlpapi.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif

#ifdef __VMS
#include <in.h>
#include <inet.h>
#endif

#ifdef HAVE_SYS_UN_H
#include <sys/un.h>
#endif

#if defined(HAVE_IF_NAMETOINDEX) && defined(USE_WINSOCK)
#if defined(__MINGW32__) && (__MINGW64_VERSION_MAJOR <= 5)
#include <wincrypt.h>  /* workaround for old mingw-w64 missing to include it */
#endif
#include <iphlpapi.h>
#endif

#include "curl_addrinfo.h"
#include "curl_trc.h"
#include "protocol.h"
#include "http_proxy.h"
#include "idn.h"
#include "curlx/strdup.h"
#include "curlx/strparse.h"
#include "peer.h"
#include "urldata.h"
#include "url.h"
#include "urlapi-int.h"
#include "vtls/vtls.h"

struct peer_parse {
  const struct Curl_scheme *scheme;
  struct Curl_str host_user;
  struct Curl_str host;
  struct Curl_str zoneid;
  char *tmp_host_user;
  char *tmp_host;
  char *tmp_zoneid;
  uint32_t scopeid;
  uint16_t port;
  bool ipv6;
  bool unix_socket;
  bool abstract_uds;
};

static void peer_parse_clear(struct peer_parse *pp)
{
  curlx_free(pp->tmp_host_user);
  curlx_free(pp->tmp_host);
  curlx_free(pp->tmp_zoneid);
  memset(pp, 0, sizeof(*pp));
}

static CURLcode peer_create(struct peer_parse *pp,
                            struct Curl_peer **ppeer)
{
  struct Curl_peer *peer = NULL;
  CURLcode result = CURLE_OK;
  size_t zone_alen = 0, host_alen = 0;

  if(!pp || !pp->scheme)
    return CURLE_FAILED_INIT;
  if(!pp->host.len && !(pp->scheme->flags & PROTOPT_NONETWORK))
    return CURLE_FAILED_INIT;

  if((pp->host.str != pp->host_user.str) ||
     (pp->host.len != pp->host_user.len)) {
    host_alen = pp->host.len + 1;
  }
  zone_alen = pp->zoneid.len ? (pp->zoneid.len + 1) : 0;

  /* null-terminator already part of struct */
  peer = curlx_calloc(1, sizeof(*peer) +
                         pp->host_user.len + host_alen + zone_alen);
  if(!peer) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }

  peer->refcount = 1;
  peer->scheme = pp->scheme;
  peer->hostname = peer->user_hostname;
  peer->port = pp->port;
  peer->scopeid = pp->scopeid;
  peer->ipv6 = pp->ipv6;
  peer->unix_socket = pp->unix_socket;
  peer->abstract_uds = pp->abstract_uds;

  if(pp->host_user.len)
    memcpy(peer->user_hostname, pp->host_user.str, pp->host_user.len);

  if(host_alen) {
    peer->hostname = peer->user_hostname + pp->host_user.len + 1;
    memcpy(peer->hostname, pp->host.str, pp->host.len);
  }

  if(zone_alen) {
    peer->zoneid = peer->user_hostname + pp->host_user.len + 1 + host_alen;
    memcpy(peer->zoneid, pp->zoneid.str, pp->zoneid.len);
#ifdef USE_IPV6
    /* Determine scope_id if not already provided */
    if(!peer->scopeid) {
      const char *p = peer->zoneid;
      curl_off_t scope;
      if(!curlx_str_number(&p, &scope, UINT_MAX)) {
        /* A plain number, use it directly as a scope id. */
        peer->scopeid = (uint32_t)scope;
      }
#ifdef HAVE_IF_NAMETOINDEX
      else {
        /* Zone identifier is not numeric */
        unsigned int idx = 0;
        idx = if_nametoindex(peer->zoneid);
        if(idx) {
          peer->scopeid = (uint32_t)idx;
        }
        else {
          /* Do we want to return an error here? */
        }
      }
#endif /* HAVE_IF_NAMETOINDEX */
    }
#endif /* USE_IPV6 */
  }

out:
  if(!result)
    *ppeer = peer;
  else
    Curl_peer_unlink(&peer);
  return result;
}

static CURLcode peer_parse_host(struct Curl_easy *data,
                                struct peer_parse *pp,
                                bool scan_for_ipv6)
{
  if(!pp || !pp->host_user.str || !pp->host_user.len)
    return CURLE_FAILED_INIT;

  if(pp->host_user.str[0] == '[') {
    const char *s = pp->host_user.str + 1;
    struct Curl_str tmp;
    if(curlx_str_until(&s, &tmp, pp->host_user.len - 1, ']'))
      return CURLE_URL_MALFORMAT;

    if(!Curl_looks_like_ipv6(tmp.str, tmp.len, TRUE,
                             &pp->host, &pp->zoneid)) {
      failf(data, "Invalid IPv6 address format in '%.*s'",
            (int)pp->host_user.len, pp->host_user.str);
      return CURLE_URL_MALFORMAT;
    }
    pp->ipv6 = TRUE;
  }
  else {
#ifdef USE_IDN
    if(!Curl_is_ASCII_str(&pp->host_user)) {
      CURLcode result;
      if(!pp->tmp_host_user) {
        /* need a null-terminated string for IDN */
        pp->tmp_host_user = curlx_memdup0(pp->host_user.str,
                                          pp->host_user.len);
        if(!pp->tmp_host_user)
          return CURLE_OUT_OF_MEMORY;
      }
      result = Curl_idn_decode(pp->tmp_host_user, &pp->tmp_host);
      if(result)
        return result;
      pp->host.str = pp->tmp_host;
      pp->host.len = strlen(pp->host.str);
    }
    else
#endif
    if(scan_for_ipv6 &&
       Curl_looks_like_ipv6(pp->host_user.str, pp->host_user.len, TRUE,
                            &pp->host, &pp->zoneid)) {
      if(pp->host_user.len < MAX_IPADR_LEN) {
        char tmp[MAX_IPADR_LEN];
        memcpy(tmp, pp->host_user.str, pp->host_user.len);
        tmp[pp->host_user.len] = 0;
        pp->ipv6 = !Curl_is_ipv4addr(tmp);
      }
      else
        pp->ipv6 = TRUE;
    }
    else
      pp->host = pp->host_user;
  }
  return CURLE_OK;
}

CURLcode Curl_peer_create(struct Curl_easy *data,
                          const struct Curl_scheme *scheme,
                          const char *hostname,
                          uint16_t port,
                          struct Curl_peer **ppeer)
{
  struct peer_parse pp;
  CURLcode result;

  Curl_peer_unlink(ppeer);
  memset(&pp, 0, sizeof(pp));
  pp.scheme = scheme;
  pp.host_user.str = hostname;
  pp.host_user.len = strlen(hostname);
  pp.port = port;

  result = peer_parse_host(data, &pp, TRUE);
  if(!result)
    result = peer_create(&pp, ppeer);

  peer_parse_clear(&pp);
  return result;
}

#ifdef USE_UNIX_SOCKETS
CURLcode Curl_peer_uds_create(const struct Curl_scheme *scheme,
                              const char *path,
                              bool abstract_unix_socket,
                              struct Curl_peer **ppeer)
{
  struct peer_parse pp;
  size_t pathlen = path ? strlen(path) : 0;
  CURLcode result = CURLE_OK;

  Curl_peer_unlink(ppeer);
  memset(&pp, 0, sizeof(pp));
  if(!scheme)
    return CURLE_FAILED_INIT;
  if(!pathlen)
    return CURLE_FAILED_INIT;

  pp.scheme = scheme;
  pp.host_user.str = pp.host.str = path;
  pp.host_user.len = pp.host.len = pathlen;
  pp.unix_socket = TRUE;
  pp.abstract_uds = abstract_unix_socket;

  result = peer_create(&pp, ppeer);
  peer_parse_clear(&pp);
  return result;
}
#endif /* USE_UNIX_SOCKETS */

void Curl_peer_link(struct Curl_peer **pdest, struct Curl_peer *src)
{
  if(*pdest != src) {
    Curl_peer_unlink(pdest);
    *pdest = src;
    if(src) {
      DEBUGASSERT(src->refcount < UINT32_MAX);
      src->refcount++;
    }
  }
}

void Curl_peer_unlink(struct Curl_peer **ppeer)
{
  if(*ppeer) {
    struct Curl_peer *peer = *ppeer;

    DEBUGASSERT(peer->refcount);
    *ppeer = NULL;
    if(peer->refcount)
      peer->refcount--;
    if(!peer->refcount) {
      curlx_free(peer);
    }
  }
}

bool Curl_peer_equal(struct Curl_peer *p1, struct Curl_peer *p2)
{
  return (p1 == p2) ||
         (p1 && p2 &&
          (p1->scheme == p2->scheme) &&
          Curl_peer_same_destination(p1, p2));
}

static bool peer_same_hostname(struct Curl_peer *p1, struct Curl_peer *p2)
{
  /* UNIX domain socket paths must be compared case-sensitive,
   * as many filesystem are like that. */
  return (p1->unix_socket == p2->unix_socket) &&
         (p1->abstract_uds == p2->abstract_uds) &&
         (p1->ipv6 == p2->ipv6) &&
         (p1->unix_socket ?
          !strcmp(p1->hostname, p2->hostname) :
          curl_strequal(p1->hostname, p2->hostname));
}

bool Curl_peer_same_destination(struct Curl_peer *p1, struct Curl_peer *p2)
{
  return (p1 == p2) ||
         (p1 && p2 &&
          (p1->port == p2->port) &&
          peer_same_hostname(p1, p2) &&
          (p1->scopeid == p2->scopeid) &&
          (p1->scopeid || curl_strequal(p1->zoneid, p2->zoneid)));
}

CURLcode Curl_peer_from_url(CURLU *uh, struct Curl_easy *data,
                            uint16_t port_override,
                            uint32_t scopeid_override,
                            struct Curl_peer **ppeer)
{
  struct peer_parse pp;
  char *zoneid = NULL, *scheme = NULL, *hostname = NULL;
  CURLUcode uc;
  CURLcode result;

  Curl_peer_unlink(ppeer);
  memset(&pp, 0, sizeof(pp));

  uc = curl_url_get(uh, CURLUPART_SCHEME, &scheme, 0);
  if(uc)
    return Curl_uc_to_curlcode(uc);
  pp.scheme = Curl_get_scheme(scheme);
  if(!pp.scheme) {
    failf(data, "Protocol \"%s\" not supported%s", scheme,
          data->state.this_is_a_follow ? " (in redirect)" : "");
    result = CURLE_UNSUPPORTED_PROTOCOL;
    goto out;
  }

  uc = curl_url_get(uh, CURLUPART_HOST, &hostname, 0);
  if(uc) {
    if((uc == CURLUE_NO_HOST) && (pp.scheme->flags & PROTOPT_NONETWORK))
      ; /* acceptable */
    else {
      result = CURLE_OUT_OF_MEMORY;
      goto out;
    }
  }
  else if(strlen(hostname) > MAX_URL_LEN) {
    failf(data, "Too long hostname (maximum is %d)", MAX_URL_LEN);
    result = CURLE_URL_MALFORMAT;
    goto out;
  }

  pp.host_user.str = hostname ? hostname : "";
  pp.host_user.len = strlen(pp.host_user.str);
  if(pp.host_user.len) {
    result = peer_parse_host(data, &pp, FALSE);
    if(result)
      goto out;
  }
  else
    pp.host = pp.host_user;

  if(port_override) {
    /* if set, we use this instead of the port possibly given in the URL */
    char portbuf[16];
    curl_msnprintf(portbuf, sizeof(portbuf), "%d", port_override);
    uc = curl_url_set(uh, CURLUPART_PORT, portbuf, 0);
    if(uc) {
      result = CURLE_OUT_OF_MEMORY;
      goto out;
    }
    else
      pp.port = port_override;
  }
  else {
    uc = Curl_url_get_port(uh, &pp.port);
    if(uc) {
      if(uc == CURLUE_OUT_OF_MEMORY) {
        result = CURLE_OUT_OF_MEMORY;
        goto out;
      }
      else if(!(pp.scheme->flags & PROTOPT_NONETWORK)) {
        result = CURLE_URL_MALFORMAT;
        goto out;
      }
      /* no port ok when not a network scheme */
    }
  }

  if(scopeid_override)
    /* Override any scope id from an URL zone. */
    pp.scopeid = scopeid_override;
  else {
    if(curl_url_get(uh, CURLUPART_ZONEID, &zoneid, 0) ==
       CURLUE_OUT_OF_MEMORY) {
      result = CURLE_OUT_OF_MEMORY;
      goto out;
    }
    if(zoneid) {
      pp.zoneid.str = zoneid;
      pp.zoneid.len = strlen(zoneid);
    }
  }

  result = peer_create(&pp, ppeer);
  if(result)
    failf(data, "Error %d creating peer for %s:%u",
          (int)result, pp.host_user.str, pp.port);

out:
  peer_parse_clear(&pp);
  curlx_free(scheme);
  curlx_free(hostname);
  curlx_free(zoneid);
  return result;
}

/* Parse a "host:port" string to connect to into a peer.
 * IPv6 addresses might appear in brackets or without them. */
CURLcode Curl_peer_from_connect_to(struct Curl_easy *data,
                                   const struct Curl_peer *dest,
                                   const char *connect_to,
                                   struct Curl_peer **ppeer)
{
  struct peer_parse pp;
  const char *portstr = NULL;
  CURLcode result;

  Curl_peer_unlink(ppeer);
  memset(&pp, 0, sizeof(pp));
  if(!connect_to || !*connect_to)
    return CURLE_FAILED_INIT;

  pp.scheme = dest->scheme;

  /* detect and extract RFC6874-style IPv6-addresses */
  if(connect_to[0] == '[') {
    const char *s = strchr(connect_to + 1, ']');
    if(!s) {
      failf(data, "Invalid IPv6 address format in '%s'", connect_to);
      result = CURLE_SETOPT_OPTION_SYNTAX;
      goto out;
    }
    portstr = strchr(s, ':');
    pp.host_user.str = connect_to;
    pp.host_user.len = s - pp.host_user.str + 1;
    pp.ipv6 = TRUE;
  }
  else {
    portstr = strchr(connect_to, ':');
    pp.host_user.str = connect_to;
    pp.host_user.len = portstr ?
      (size_t)(portstr - connect_to) : strlen(connect_to);
  }

  if(!pp.host_user.len) { /* no hostname found, only port switch */
    pp.host_user.str = dest->user_hostname;
    pp.host_user.len = strlen(dest->user_hostname);
  }

  result = peer_parse_host(data, &pp, FALSE);
  if(result)
    goto out;

  if(portstr && portstr[1]) {
    const char *p = portstr + 1;
    curl_off_t portparse;
    if(curlx_str_number(&p, &portparse, 0xffff)) {
      failf(data, "No valid port number in '%s'", connect_to);
      result = CURLE_SETOPT_OPTION_SYNTAX;
      goto out;
    }
    pp.port = (uint16_t)portparse; /* we know it will fit */
  }
  else
    pp.port = dest->port;

#ifndef USE_IPV6
  if(pp.ipv6) {
    failf(data, "Use of IPv6 in *_CONNECT_TO without IPv6 support built-in");
    result = CURLE_NOT_BUILT_IN;
    goto out;
  }
#endif

  result = peer_create(&pp, ppeer);
  CURL_TRC_M(data, "connect-to peer_create2 -> %d", (int)result);

out:
  CURL_TRC_M(data, "parse connect_to peer: %s -> %d", connect_to, (int)result);
  peer_parse_clear(&pp);
  return result;
}

#ifndef CURL_DISABLE_PROXY

#ifdef USE_UNIX_SOCKETS
#define UNIX_SOCKET_PREFIX "localhost"
#endif

CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data,
                                  const char *scheme,
                                  uint8_t *proxytype, const char *url)
{
  if(!scheme)
    return CURLE_OK;

  if(curl_strequal("https", scheme)) {
    if(*proxytype != CURLPROXY_HTTPS2 && *proxytype != CURLPROXY_HTTPS3)
      *proxytype = CURLPROXY_HTTPS;
  }
  else if(curl_strequal("socks5h", scheme))
    *proxytype = CURLPROXY_SOCKS5_HOSTNAME;
  else if(curl_strequal("socks5", scheme))
    *proxytype = CURLPROXY_SOCKS5;
  else if(curl_strequal("socks4a", scheme))
    *proxytype = CURLPROXY_SOCKS4A;
  else if(curl_strequal("socks4", scheme) || curl_strequal("socks", scheme))
    *proxytype = CURLPROXY_SOCKS4;
  else if(curl_strequal("http", scheme)) {
    if(*proxytype != CURLPROXY_HTTP_1_0)
      *proxytype = CURLPROXY_HTTP;
  }
  else {
    /* Any other xxx:// reject! */
    failf(data, "Unsupported proxy scheme for \'%s\'", url);
    return CURLE_COULDNT_CONNECT;
  }
  return CURLE_OK;
}

CURLcode Curl_peer_from_proxy_url(CURLU *uh,
                                  struct Curl_easy *data,
                                  const char *url,
                                  uint8_t proxytype,
                                  struct Curl_peer **ppeer,
                                  uint8_t *pproxytype)
{
  struct peer_parse pp;
  char *scheme = NULL;
  char *portptr = NULL;
#ifdef USE_UNIX_SOCKETS
  bool is_socks = FALSE;
#endif
  CURLUcode uc;
  CURLcode result = CURLE_OK;

  Curl_peer_unlink(ppeer);
  memset(&pp, 0, sizeof(pp));
  pp.port = CURL_DEFAULT_PROXY_PORT;
  uc = curl_url_get(uh, CURLUPART_SCHEME, &scheme,
                    CURLU_NON_SUPPORT_SCHEME | CURLU_NO_GUESS_SCHEME);
  if(uc) {
    if(uc == CURLUE_OUT_OF_MEMORY) {
      result = CURLE_OUT_OF_MEMORY;
      goto out;
    }
    /* URL came without scheme, the passed `proxytype` determines it */
    switch(proxytype) {
    case CURLPROXY_HTTP:
    case CURLPROXY_HTTP_1_0:
      pp.scheme = &Curl_scheme_http;
      break;
    case CURLPROXY_HTTPS:
    case CURLPROXY_HTTPS2:
    case CURLPROXY_HTTPS3:
      pp.scheme = &Curl_scheme_https;
      break;
    case CURLPROXY_SOCKS4:
      pp.scheme = &Curl_scheme_socks4;
      break;
    case CURLPROXY_SOCKS4A:
      pp.scheme = &Curl_scheme_socks4a;
      break;
    case CURLPROXY_SOCKS5:
      pp.scheme = &Curl_scheme_socks5;
      break;
    case CURLPROXY_SOCKS5_HOSTNAME:
      pp.scheme = &Curl_scheme_socks5h;
      break;
    default:
      failf(data, "Unsupported proxy type %u for \'%s\'", proxytype, url);
      result = CURLE_COULDNT_RESOLVE_PROXY;
      goto out;
    }
  }
  else {
    pp.scheme = Curl_get_scheme(scheme);
    result = Curl_scheme_to_proxytype(data, scheme, &proxytype, url);
    if(result)
      goto out;
  }
  DEBUGASSERT(pp.scheme);

  if(CURL_PROXY_IS_HTTPS(proxytype) &&
     !Curl_ssl_supports(data, SSLSUPP_HTTPS_PROXY)) {
    failf(data, "Unsupported proxy \'%s\', libcurl is built without the "
          "HTTPS-proxy support.", url);
    result = CURLE_NOT_BUILT_IN;
    goto out;
  }

  switch(pp.scheme->family) {
  case CURLPROTO_SOCKS:
#ifdef USE_UNIX_SOCKETS
    is_socks = TRUE;
#endif
    break;
  case CURLPROTO_HTTP:
    break;
  default:
    failf(data, "Unsupported proxy protocol for \'%s\'", url);
    result = CURLE_COULDNT_CONNECT;
    goto out;
  }

  uc = curl_url_get(uh, CURLUPART_PORT, &portptr, CURLU_NO_DEFAULT_PORT);
  if(uc == CURLUE_OUT_OF_MEMORY) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }
  if(portptr) {
    curl_off_t num;
    const char *p = portptr;
    if(!curlx_str_number(&p, &num, UINT16_MAX))
      pp.port = (uint16_t)num;
    /* Should we not error out when the port number is invalid? */
    curlx_free(portptr);
  }
  else {
    /* No port in URL, take the set one or the scheme's default */
    if(data->set.proxyport)
      pp.port = data->set.proxyport;
    else
      pp.port = pp.scheme->defport;
  }

  /* now, clone the proxy hostname */
  uc = curl_url_get(uh, CURLUPART_HOST, &pp.tmp_host_user, CURLU_URLDECODE);
  if(uc) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }
  pp.host_user.str = pp.tmp_host_user;
  pp.host_user.len = strlen(pp.tmp_host_user);

#ifdef USE_UNIX_SOCKETS
  if(is_socks && curl_strequal(UNIX_SOCKET_PREFIX, pp.tmp_host_user)) {
    uc = curl_url_get(uh, CURLUPART_PATH, &pp.tmp_host, CURLU_URLDECODE);
    if(uc) {
      result = CURLE_OUT_OF_MEMORY;
      goto out;
    }
    /* path will be "/", if no path was found */
    if(strcmp("/", pp.tmp_host)) {
      pp.host.str = pp.tmp_host;
      pp.host.len = strlen(pp.tmp_host);
      pp.unix_socket = TRUE;
    }
    else {
      pp.host = pp.host_user;
    }
  }
#endif /* USE_UNIX_SOCKETS */

  if(!pp.host.len) {
    result = peer_parse_host(data, &pp, FALSE);
    if(result)
      goto out;
  }

  uc = curl_url_get(uh, CURLUPART_ZONEID, &pp.tmp_zoneid, 0);
  if(uc == CURLUE_OUT_OF_MEMORY) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }
  if(pp.tmp_zoneid) {
    pp.zoneid.str = pp.tmp_zoneid;
    pp.zoneid.len = strlen(pp.tmp_zoneid);
  }

  *pproxytype = proxytype;
  result = peer_create(&pp, ppeer);

out:
  peer_parse_clear(&pp);
  curlx_free(scheme);
#ifdef DEBUGBUILD
  if(!result)
    DEBUGASSERT(*ppeer);
#endif
  return result;
}

#endif /* !CURL_DISABLE_PROXY */
