mirror of
https://github.com/curl/curl.git
synced 2026-08-25 01:03:31 +03:00
HTTP/3: add proxy CONNECT and MASQUE CONNECT-UDP support (ngtcp2 QUIC)
This patch adds two major proxy capabilities to curl (ngtcp2 QUIC):
- HTTP/3 Proxy CONNECT: Tunnel HTTP/1.1 or HTTP/2 traffic through an
HTTPS proxy that speaks HTTP/3 (QUIC) using the standard CONNECT
method over an HTTP/3 connection.
- MASQUE CONNECT-UDP: Tunnel HTTP/3 (QUIC) traffic through an HTTP
proxy (speaking HTTP/1.1, HTTP/2, or HTTP/3) using the extended
CONNECT method with the CONNECT-UDP protocol (RFC9297 & RFC9298).
Public API additions:
- `CURLPROXY_HTTPS3`: new proxy type constant for HTTP/3 proxy
- `--proxy-http3`: new CLI flag to negotiate HTTP/3 with HTTPS proxy
The implementation adds two new filters:
- `H3-PROXY` - enables negotiating HTTP/3 (QUIC) to the proxy and
running CONNECT/CONNECT-UDP through that proxy transport.
- `CAPSULE` - dedicated filter inserted between QUIC transport and
HTTP-PROXY to handle datagram capsule encapsulation/decapsulation.
Here is how the curl filter chaining looks in different scenarios:
- HTTP/3 Proxy CONNECT (tunneling TCP protocols over QUIC proxy):
conn -> HTTP/1.1 or HTTP/2 -> SSL -> HTTP-PROXY ->
H3-PROXY -> HAPPY-EYEBALLS -> UDP
- MASQUE CONNECT-UDP (tunneling QUIC over any proxy):
conn -> HTTP/3 -> CAPSULE -> HTTP-PROXY -> H3-PROXY ->
HAPPY-EYEBALLS -> UDP
conn -> HTTP/3 -> CAPSULE -> HTTP-PROXY -> H1-PROXY or H2-PROXY ->
SSL -> HAPPY-EYEBALLS -> TCP
- Both features currently require the ngtcp2 QUIC backend.
- Both features are experimental (disabled by default). Enable with
`--enable-proxy-http3`(autotools) or `-DUSE_PROXY_HTTP3=ON`(CMake).
Tests:
- tests/unit/unit3400.c: Unit tests for capsule protocol encode/decode
- tests/http/test_60_h3_proxy.py: Comprehensive pytest integration suite
- tests/http/testenv/h2o.py: Managing h2o instances with HTTP/1.1, HTTP/2,
and HTTP/3 (QUIC) listeners, proxy.connect and proxy.connect-udp enabled.
References:
RFC 9297 - HTTP Datagrams and the Capsule Protocol
RFC 9298 - Proxying UDP in HTTP
RFC 9000 §16 — Variable-Length Integer Encoding
Signed-off-by: Aritra Basu <aritrbas+gh@cisco.com>
Closes #21153
This commit is contained in:
parent
efc3f2309e
commit
e78b1b3ecc
66 changed files with 7401 additions and 473 deletions
|
|
@ -25,6 +25,8 @@
|
|||
|
||||
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
|
||||
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include "urldata.h"
|
||||
#include "curlx/dynbuf.h"
|
||||
#include "sendf.h"
|
||||
|
|
@ -33,6 +35,7 @@
|
|||
#include "http_proxy.h"
|
||||
#include "select.h"
|
||||
#include "progress.h"
|
||||
#include "multiif.h"
|
||||
#include "cfilters.h"
|
||||
#include "cf-h1-proxy.h"
|
||||
#include "connect.h"
|
||||
|
|
@ -40,7 +43,6 @@
|
|||
#include "strcase.h"
|
||||
#include "curlx/strparse.h"
|
||||
|
||||
|
||||
typedef enum {
|
||||
H1_TUNNEL_INIT, /* init/default/no tunnel state */
|
||||
H1_TUNNEL_CONNECT, /* CONNECT request is being send */
|
||||
|
|
@ -72,6 +74,12 @@ struct h1_tunnel_state {
|
|||
BIT(leading_unfold);
|
||||
};
|
||||
|
||||
/* Persistent context for the H1-PROXY filter */
|
||||
struct cf_h1_proxy_ctx {
|
||||
struct h1_tunnel_state *ts;
|
||||
BIT(udp_tunnel);
|
||||
};
|
||||
|
||||
static bool tunnel_is_established(struct h1_tunnel_state *ts)
|
||||
{
|
||||
return ts && (ts->tunnel_state == H1_TUNNEL_ESTABLISHED);
|
||||
|
|
@ -82,6 +90,12 @@ static bool tunnel_is_failed(struct h1_tunnel_state *ts)
|
|||
return ts && (ts->tunnel_state == H1_TUNNEL_FAILED);
|
||||
}
|
||||
|
||||
static bool h1_proxy_is_udp(struct Curl_cfilter *cf)
|
||||
{
|
||||
struct cf_h1_proxy_ctx *pctx = cf->ctx;
|
||||
return (pctx->udp_tunnel ? TRUE : FALSE);
|
||||
}
|
||||
|
||||
static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct h1_tunnel_state *ts)
|
||||
|
|
@ -97,6 +111,8 @@ static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
|
|||
ts->close_connection = FALSE;
|
||||
ts->maybe_folded = FALSE;
|
||||
ts->leading_unfold = FALSE;
|
||||
ts->nsent = 0;
|
||||
ts->headerlines = 0;
|
||||
return CURLE_OK;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +174,9 @@ static void h1_tunnel_go_state(struct Curl_cfilter *cf,
|
|||
|
||||
case H1_TUNNEL_ESTABLISHED:
|
||||
CURL_TRC_CF(data, cf, "new tunnel state 'established'");
|
||||
infof(data, "CONNECT phase completed");
|
||||
infof(data, "CONNECT%s phase completed for HTTP proxy",
|
||||
h1_proxy_is_udp(cf) ? "-UDP" : "");
|
||||
|
||||
data->state.authproxy.done = TRUE;
|
||||
data->state.authproxy.multipass = FALSE;
|
||||
FALLTHROUGH();
|
||||
|
|
@ -195,11 +213,12 @@ static void cf_tunnel_free(struct Curl_cfilter *cf,
|
|||
struct Curl_easy *data)
|
||||
{
|
||||
if(cf) {
|
||||
struct h1_tunnel_state *ts = cf->ctx;
|
||||
struct cf_h1_proxy_ctx *pctx = cf->ctx;
|
||||
struct h1_tunnel_state *ts = pctx ? pctx->ts : NULL;
|
||||
if(ts) {
|
||||
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
|
||||
tunnel_free(ts, data);
|
||||
cf->ctx = NULL;
|
||||
pctx->ts = NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -217,17 +236,17 @@ static CURLcode start_CONNECT(struct Curl_cfilter *cf,
|
|||
int http_minor;
|
||||
CURLcode result;
|
||||
|
||||
DEBUGASSERT(data);
|
||||
/* This only happens if we have looped here due to authentication reasons,
|
||||
and we do not really use the newly cloned URL here then. Free it. */
|
||||
curlx_safefree(data->req.newurl);
|
||||
|
||||
result = Curl_http_proxy_create_CONNECT(&req, cf, data,
|
||||
ts->dest, ts->httpversion);
|
||||
result = Curl_http_proxy_create_tunnel_request(&req, cf, data, ts->dest,
|
||||
PROXY_HTTP_V1,
|
||||
h1_proxy_is_udp(cf));
|
||||
if(result)
|
||||
goto out;
|
||||
|
||||
infof(data, "Establish HTTP proxy tunnel to %s", req->authority);
|
||||
|
||||
curlx_dyn_reset(&ts->request_data);
|
||||
ts->nsent = 0;
|
||||
ts->headerlines = 0;
|
||||
|
|
@ -280,6 +299,92 @@ out:
|
|||
return result;
|
||||
}
|
||||
|
||||
static CURLcode on_resp_header_udp(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct h1_tunnel_state *ts,
|
||||
const char *header)
|
||||
{
|
||||
CURLcode result = CURLE_OK;
|
||||
struct SingleRequest *k = &data->req;
|
||||
|
||||
if((checkprefix("WWW-Authenticate:", header) && (401 == k->httpcode)) ||
|
||||
(checkprefix("Proxy-authenticate:", header) && (407 == k->httpcode))) {
|
||||
|
||||
bool proxy = (k->httpcode == 407);
|
||||
char *auth = Curl_copy_header_value(header);
|
||||
if(!auth)
|
||||
return CURLE_OUT_OF_MEMORY;
|
||||
|
||||
CURL_TRC_CF(data, cf, "CONNECT-UDP: fwd auth header '%s'", header);
|
||||
result = Curl_http_input_auth(data, proxy, auth);
|
||||
|
||||
curlx_free(auth);
|
||||
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
else if(checkprefix("Content-Length:", header)) {
|
||||
if(k->httpcode / 100 == 2 || k->httpcode == 101) {
|
||||
infof(data, "Ignoring Content-Length in CONNECT-UDP %03d response",
|
||||
k->httpcode);
|
||||
}
|
||||
else {
|
||||
const char *p = header + strlen("Content-Length:");
|
||||
if(curlx_str_numblanks(&p, &ts->cl)) {
|
||||
failf(data, "Unsupported Content-Length value");
|
||||
return CURLE_WEIRD_SERVER_REPLY;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(checkprefix("Transfer-Encoding:", header)) {
|
||||
if(k->httpcode / 100 == 2 || k->httpcode == 101) {
|
||||
infof(data, "Ignoring Transfer-Encoding in "
|
||||
"CONNECT-UDP %03d response", k->httpcode);
|
||||
}
|
||||
else if(Curl_compareheader(header,
|
||||
STRCONST("Transfer-Encoding:"),
|
||||
STRCONST("chunked"))) {
|
||||
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> "
|
||||
"Transfer-Encoding: chunked");
|
||||
ts->chunked_encoding = TRUE;
|
||||
/* reset our chunky engine */
|
||||
Curl_httpchunk_reset(data, &ts->ch, TRUE);
|
||||
}
|
||||
}
|
||||
else if(checkprefix("Capsule-protocol:", header)) {
|
||||
if(Curl_compareheader(header,
|
||||
STRCONST("Capsule-protocol:"),
|
||||
STRCONST("?1"))) {
|
||||
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> Capsule-protocol: ?1");
|
||||
}
|
||||
}
|
||||
else if(Curl_compareheader(header,
|
||||
STRCONST("Connection:"), STRCONST("close"))) {
|
||||
ts->close_connection = TRUE;
|
||||
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> Connection: close");
|
||||
}
|
||||
else if(Curl_compareheader(header,
|
||||
STRCONST("Proxy-Connection:"),
|
||||
STRCONST("close"))) {
|
||||
ts->close_connection = TRUE;
|
||||
CURL_TRC_CF(data, cf,
|
||||
"CONNECT-UDP Response --> Proxy-Connection: close");
|
||||
}
|
||||
else if(!strncmp(header, "HTTP/1.", 7) &&
|
||||
((header[7] == '0') || (header[7] == '1')) &&
|
||||
(header[8] == ' ') &&
|
||||
ISDIGIT(header[9]) && ISDIGIT(header[10]) && ISDIGIT(header[11]) &&
|
||||
!ISDIGIT(header[12])) {
|
||||
/* store the HTTP code from the proxy */
|
||||
data->info.httpproxycode = k->httpcode =
|
||||
((header[9] - '0') * 100) +
|
||||
((header[10] - '0') * 10) +
|
||||
(header[11] - '0');
|
||||
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> %d", k->httpcode);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static CURLcode on_resp_header(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct h1_tunnel_state *ts,
|
||||
|
|
@ -418,7 +523,13 @@ static CURLcode single_header(struct Curl_cfilter *cf,
|
|||
return result;
|
||||
}
|
||||
|
||||
result = on_resp_header(cf, data, ts, linep);
|
||||
if(h1_proxy_is_udp(cf)) {
|
||||
result = on_resp_header_udp(cf, data, ts, linep);
|
||||
}
|
||||
else {
|
||||
result = on_resp_header(cf, data, ts, linep);
|
||||
}
|
||||
|
||||
if(result)
|
||||
return result;
|
||||
|
||||
|
|
@ -460,6 +571,13 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
|
|||
}
|
||||
|
||||
if(!nread) {
|
||||
if(ts->maybe_folded) {
|
||||
/* EOF right after LF: finalize the pending header line. */
|
||||
result = single_header(cf, data, ts);
|
||||
if(result)
|
||||
return result;
|
||||
ts->maybe_folded = FALSE;
|
||||
}
|
||||
if(data->set.proxyauth && data->state.authproxy.avail &&
|
||||
data->req.hd_proxy_auth) {
|
||||
/* proxy auth was requested and there was proxy auth available,
|
||||
|
|
@ -551,12 +669,16 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
|
|||
ts->maybe_folded = TRUE;
|
||||
}
|
||||
|
||||
if(result)
|
||||
return result;
|
||||
} /* while there is buffer left and loop is requested */
|
||||
|
||||
if(error)
|
||||
result = CURLE_RECV_ERROR;
|
||||
*done = (ts->keepon == KEEPON_DONE);
|
||||
if(!result && *done && data->info.httpproxycode / 100 != 2) {
|
||||
if(!result && *done &&
|
||||
data->info.httpproxycode / 100 != 2 &&
|
||||
!(h1_proxy_is_udp(cf) && data->info.httpproxycode == 101)) {
|
||||
/* Deal with the possibly already received authenticate
|
||||
headers. 'newurl' is set to a new URL if we must loop. */
|
||||
result = Curl_http_auth_act(data);
|
||||
|
|
@ -637,7 +759,7 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
|
|||
infof(data, "Connect me again please");
|
||||
Curl_conn_cf_close(cf, data);
|
||||
result = Curl_conn_cf_connect(cf->next, data, &done);
|
||||
goto out;
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
/* staying on this connection, reset state */
|
||||
|
|
@ -653,17 +775,36 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
|
|||
} while(data->req.newurl);
|
||||
|
||||
DEBUGASSERT(ts->tunnel_state == H1_TUNNEL_RESPONSE);
|
||||
if(data->info.httpproxycode / 100 != 2) {
|
||||
/* a non-2xx response and we have no next URL to try. */
|
||||
curlx_safefree(data->req.newurl);
|
||||
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
|
||||
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode);
|
||||
return CURLE_COULDNT_CONNECT;
|
||||
if(h1_proxy_is_udp(cf)) {
|
||||
/* RFC 9298: Accept 101 Upgrade for HTTP/1.1 and
|
||||
* 2xx responses for HTTP/2 and HTTP/3 proxies. */
|
||||
if(data->info.httpproxycode / 100 != 2 &&
|
||||
data->info.httpproxycode != 101) {
|
||||
curlx_safefree(data->req.newurl);
|
||||
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
|
||||
failf(data, "CONNECT-UDP tunnel failed, response %d",
|
||||
data->req.httpcode);
|
||||
return CURLE_COULDNT_CONNECT;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(data->info.httpproxycode / 100 != 2) {
|
||||
/* a non-2xx response and we have no next URL to try. */
|
||||
curlx_safefree(data->req.newurl);
|
||||
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
|
||||
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode);
|
||||
return CURLE_COULDNT_CONNECT;
|
||||
}
|
||||
}
|
||||
/* 2xx response, SUCCESS! */
|
||||
/* 101 Switching Protocol for CONNECT-UDP */
|
||||
h1_tunnel_go_state(cf, ts, H1_TUNNEL_ESTABLISHED, data);
|
||||
infof(data, "CONNECT tunnel established, response %d",
|
||||
data->info.httpproxycode);
|
||||
if(h1_proxy_is_udp(cf))
|
||||
infof(data, "CONNECT-UDP tunnel established, response %d",
|
||||
data->info.httpproxycode);
|
||||
else
|
||||
infof(data, "CONNECT tunnel established, response %d",
|
||||
data->info.httpproxycode);
|
||||
result = CURLE_OK;
|
||||
|
||||
out:
|
||||
|
|
@ -677,7 +818,8 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
|
|||
bool *done)
|
||||
{
|
||||
CURLcode result;
|
||||
struct h1_tunnel_state *ts = cf->ctx;
|
||||
struct cf_h1_proxy_ctx *pctx = cf->ctx;
|
||||
struct h1_tunnel_state *ts = pctx->ts;
|
||||
|
||||
if(cf->connected) {
|
||||
*done = TRUE;
|
||||
|
|
@ -694,7 +836,7 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
|
|||
result = tunnel_init(cf, data, &ts);
|
||||
if(result)
|
||||
return result;
|
||||
cf->ctx = ts;
|
||||
pctx->ts = ts;
|
||||
}
|
||||
|
||||
/* We want "seamless" operations through HTTP proxy tunnel */
|
||||
|
|
@ -705,14 +847,13 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
|
|||
curlx_safefree(data->req.hd_proxy_auth);
|
||||
|
||||
out:
|
||||
*done = (result == CURLE_OK) && tunnel_is_established(cf->ctx);
|
||||
*done = (result == CURLE_OK) && tunnel_is_established(pctx->ts);
|
||||
if(*done) {
|
||||
cf->connected = TRUE;
|
||||
/* The real request will follow the CONNECT, reset request partially */
|
||||
Curl_req_soft_reset(&data->req, data);
|
||||
Curl_client_reset(data);
|
||||
Curl_pgrsReset(data);
|
||||
|
||||
cf_tunnel_free(cf, data);
|
||||
}
|
||||
return result;
|
||||
|
|
@ -722,7 +863,8 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
|
|||
struct Curl_easy *data,
|
||||
struct easy_pollset *ps)
|
||||
{
|
||||
struct h1_tunnel_state *ts = cf->ctx;
|
||||
struct cf_h1_proxy_ctx *pctx = cf->ctx;
|
||||
struct h1_tunnel_state *ts = pctx->ts;
|
||||
CURLcode result = CURLE_OK;
|
||||
|
||||
if(!cf->connected) {
|
||||
|
|
@ -742,37 +884,49 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
|
|||
else
|
||||
result = Curl_pollset_set_out_only(data, ps, sock);
|
||||
}
|
||||
else {
|
||||
if(cf->next)
|
||||
result = cf->next->cft->adjust_pollset(cf->next, data, ps);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool cf_h1_proxy_data_pending(struct Curl_cfilter *cf,
|
||||
const struct Curl_easy *data)
|
||||
{
|
||||
return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE;
|
||||
}
|
||||
|
||||
static void cf_h1_proxy_destroy(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data)
|
||||
{
|
||||
CURL_TRC_CF(data, cf, "destroy");
|
||||
cf_tunnel_free(cf, data);
|
||||
curlx_safefree(cf->ctx);
|
||||
}
|
||||
|
||||
static void cf_h1_proxy_close(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data)
|
||||
{
|
||||
struct cf_h1_proxy_ctx *pctx = cf->ctx;
|
||||
CURL_TRC_CF(data, cf, "close");
|
||||
if(cf) {
|
||||
cf->connected = FALSE;
|
||||
if(cf->ctx) {
|
||||
h1_tunnel_go_state(cf, cf->ctx, H1_TUNNEL_INIT, data);
|
||||
}
|
||||
if(cf->next)
|
||||
cf->next->cft->do_close(cf->next, data);
|
||||
}
|
||||
cf->connected = FALSE;
|
||||
if(pctx && pctx->ts)
|
||||
h1_tunnel_go_state(cf, pctx->ts, H1_TUNNEL_INIT, data);
|
||||
if(cf->next)
|
||||
cf->next->cft->do_close(cf->next, data);
|
||||
}
|
||||
|
||||
static CURLcode cf_h1_proxy_query(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
int query, int *pres1, void *pres2)
|
||||
{
|
||||
struct h1_tunnel_state *ts = cf->ctx;
|
||||
struct cf_h1_proxy_ctx *pctx = cf->ctx;
|
||||
struct h1_tunnel_state *ts = pctx ? pctx->ts : NULL;
|
||||
switch(query) {
|
||||
case CF_QUERY_HOST_PORT:
|
||||
if(!ts || !ts->dest)
|
||||
break;
|
||||
*pres1 = (int)ts->dest->port;
|
||||
*((const char **)pres2) = ts->dest->hostname;
|
||||
return CURLE_OK;
|
||||
|
|
@ -799,7 +953,7 @@ struct Curl_cftype Curl_cft_h1_proxy = {
|
|||
cf_h1_proxy_close,
|
||||
Curl_cf_def_shutdown,
|
||||
cf_h1_proxy_adjust_pollset,
|
||||
Curl_cf_def_data_pending,
|
||||
cf_h1_proxy_data_pending,
|
||||
Curl_cf_def_send,
|
||||
Curl_cf_def_recv,
|
||||
Curl_cf_def_cntrl,
|
||||
|
|
@ -811,9 +965,11 @@ struct Curl_cftype Curl_cft_h1_proxy = {
|
|||
CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
|
||||
struct Curl_easy *data,
|
||||
struct Curl_peer *dest,
|
||||
int httpversion)
|
||||
int httpversion,
|
||||
bool udp_tunnel)
|
||||
{
|
||||
struct Curl_cfilter *cf;
|
||||
struct cf_h1_proxy_ctx *pctx;
|
||||
struct h1_tunnel_state *ts;
|
||||
CURLcode result;
|
||||
|
||||
|
|
@ -834,9 +990,18 @@ CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
|
|||
curlx_dyn_init(&ts->request_data, DYN_HTTP_REQUEST);
|
||||
Curl_httpchunk_init(data, &ts->ch, TRUE);
|
||||
|
||||
result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, ts);
|
||||
if(result)
|
||||
pctx = curlx_calloc(1, sizeof(*pctx));
|
||||
if(!pctx) {
|
||||
result = CURLE_OUT_OF_MEMORY;
|
||||
goto out;
|
||||
}
|
||||
pctx->udp_tunnel = udp_tunnel;
|
||||
pctx->ts = ts;
|
||||
result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, pctx);
|
||||
if(result) {
|
||||
curlx_free(pctx);
|
||||
goto out;
|
||||
}
|
||||
ts = NULL;
|
||||
Curl_conn_cf_insert_after(cf_at, cf);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue