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

#include "http_proxy.h"

#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY)

#include <curl/curl.h>
#ifdef USE_HYPER
#include <hyper.h>
#endif
#include "sendf.h"
#include "http.h"
#include "url.h"
#include "select.h"
#include "progress.h"
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "cf-h2-proxy.h"
#include "connect.h"
#include "curlx.h"
#include "vtls/vtls.h"
#include "transfer.h"
#include "multiif.h"

/* The last 3 #include files should be in this order */
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"


struct cf_proxy_ctx {
  /* the protocol specific sub-filter we install during connect */
  struct Curl_cfilter *cf_protocol;
};

static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf,
                                      struct Curl_easy *data,
                                      bool blocking, bool *done)
{
  struct cf_proxy_ctx *ctx = cf->ctx;
  CURLcode result;

  if(cf->connected) {
    *done = TRUE;
    return CURLE_OK;
  }

  DEBUGF(LOG_CF(data, cf, "connect"));
connect_sub:
  result = cf->next->cft->do_connect(cf->next, data, blocking, done);
  if(result || !*done)
    return result;

  *done = FALSE;
  if(!ctx->cf_protocol) {
    struct Curl_cfilter *cf_protocol = NULL;
    int alpn = Curl_conn_cf_is_ssl(cf->next)?
      cf->conn->proxy_alpn : CURL_HTTP_VERSION_1_1;

    /* First time call after the subchain connected */
    switch(alpn) {
    case CURL_HTTP_VERSION_NONE:
    case CURL_HTTP_VERSION_1_0:
    case CURL_HTTP_VERSION_1_1:
      DEBUGF(LOG_CF(data, cf, "installing subfilter for HTTP/1.1"));
      infof(data, "CONNECT tunnel: HTTP/1.%d negotiated",
            (alpn == CURL_HTTP_VERSION_1_0)? 0 : 1);
      result = Curl_cf_h1_proxy_insert_after(cf, data);
      if(result)
        goto out;
      cf_protocol = cf->next;
      break;
#ifdef USE_NGHTTP2
    case CURL_HTTP_VERSION_2:
      DEBUGF(LOG_CF(data, cf, "installing subfilter for HTTP/2"));
      infof(data, "CONNECT tunnel: HTTP/2 negotiated");
      result = Curl_cf_h2_proxy_insert_after(cf, data);
      if(result)
        goto out;
      cf_protocol = cf->next;
      break;
#endif
    default:
      DEBUGF(LOG_CF(data, cf, "installing subfilter for default HTTP/1.1"));
      infof(data, "CONNECT tunnel: unsupported ALPN(%d) negotiated", alpn);
      result = CURLE_COULDNT_CONNECT;
      goto out;
    }

    ctx->cf_protocol = cf_protocol;
    /* after we installed the filter "below" us, we call connect
     * on out sub-chain again.
     */
    goto connect_sub;
  }
  else {
    /* subchain connected and we had already installed the protocol filter.
     * This means the protocol tunnel is established, we are done.
     */
    DEBUGASSERT(ctx->cf_protocol);
    result = CURLE_OK;
  }

out:
  if(!result) {
    cf->connected = TRUE;
    *done = TRUE;
  }
  return result;
}

void Curl_cf_http_proxy_get_host(struct Curl_cfilter *cf,
                                 struct Curl_easy *data,
                                 const char **phost,
                                 const char **pdisplay_host,
                                 int *pport)
{
  (void)data;
  if(!cf->connected) {
    *phost = cf->conn->http_proxy.host.name;
    *pdisplay_host = cf->conn->http_proxy.host.dispname;
    *pport = (int)cf->conn->http_proxy.port;
  }
  else {
    cf->next->cft->get_host(cf->next, data, phost, pdisplay_host, pport);
  }
}

static void http_proxy_cf_destroy(struct Curl_cfilter *cf,
                                  struct Curl_easy *data)
{
  struct cf_proxy_ctx *ctx = cf->ctx;

  (void)data;
  DEBUGF(LOG_CF(data, cf, "destroy"));
  free(ctx);
}

static void http_proxy_cf_close(struct Curl_cfilter *cf,
                                struct Curl_easy *data)
{
  struct cf_proxy_ctx *ctx = cf->ctx;

  DEBUGF(LOG_CF(data, cf, "close"));
  cf->connected = FALSE;
  if(ctx->cf_protocol) {
    struct Curl_cfilter *f;
    /* if someone already removed it, we assume he also
     * took care of destroying it. */
    for(f = cf->next; f; f = f->next) {
      if(f == ctx->cf_protocol) {
        /* still in our sub-chain */
        Curl_conn_cf_discard_sub(cf, ctx->cf_protocol, data, FALSE);
        break;
      }
    }
    ctx->cf_protocol = NULL;
  }
  if(cf->next)
    cf->next->cft->do_close(cf->next, data);
}


struct Curl_cftype Curl_cft_http_proxy = {
  "HTTP-PROXY",
  CF_TYPE_IP_CONNECT,
  0,
  http_proxy_cf_destroy,
  http_proxy_cf_connect,
  http_proxy_cf_close,
  Curl_cf_http_proxy_get_host,
  Curl_cf_def_get_select_socks,
  Curl_cf_def_data_pending,
  Curl_cf_def_send,
  Curl_cf_def_recv,
  Curl_cf_def_cntrl,
  Curl_cf_def_conn_is_alive,
  Curl_cf_def_conn_keep_alive,
  Curl_cf_def_query,
};

CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
                                         struct Curl_easy *data)
{
  struct Curl_cfilter *cf;
  struct cf_proxy_ctx *ctx = NULL;
  CURLcode result;

  (void)data;
  ctx = calloc(1, sizeof(*ctx));
  if(!ctx) {
    result = CURLE_OUT_OF_MEMORY;
    goto out;
  }
  result = Curl_cf_create(&cf, &Curl_cft_http_proxy, ctx);
  if(result)
    goto out;
  ctx = NULL;
  Curl_conn_cf_insert_after(cf_at, cf);

out:
  free(ctx);
  return result;
}

#endif /* ! CURL_DISABLE_HTTP && !CURL_DISABLE_PROXY */
