Skip to content

Commit

Permalink
Support HTTP2_STREAMS to set http stream priorities
Browse files Browse the repository at this point in the history
  • Loading branch information
perklet committed Jan 9, 2024
1 parent 2c199f1 commit 1c725f7
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 3 deletions.
6 changes: 6 additions & 0 deletions include/curl/curl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2255,6 +2255,12 @@ typedef enum {
/* set ECH configuration, XXX, the official one is 324 */
CURLOPT(CURLOPT_ECH, CURLOPTTYPE_STRINGPOINT, 333),

/*
* curl-impersonate:
* Set the initial streams settings for http2.
*/
CURLOPT(CURLOPT_HTTP2_STREAMS, CURLOPTTYPE_STRINGPOINT, 334),

CURLOPT_LASTENTRY /* the last unused */
} CURLoption;

Expand Down
10 changes: 8 additions & 2 deletions lib/easy.c
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,12 @@ CURLcode _do_impersonate(struct Curl_easy *data,
return ret;
}

if(opts->http2_streams) {
ret = curl_easy_setopt(data, CURLOPT_HTTP2_STREAMS, opts->http2_streams);
if(ret)
return ret;
}

if(opts->ech) {
ret = curl_easy_setopt(data, CURLOPT_ECH, opts->ech);
if(ret)
Expand All @@ -488,7 +494,7 @@ CURLcode curl_easy_impersonate_customized(struct Curl_easy *data,
{
int ret;

ret = _do_impersonate(data, opts, default_headers)
ret = _do_impersonate(data, opts, default_headers);
if(ret)
return ret;

Expand Down Expand Up @@ -518,7 +524,7 @@ CURLcode curl_easy_impersonate(struct Curl_easy *data, const char *target,
return CURLE_BAD_FUNCTION_ARGUMENT;
}

ret = _do_impersonate(data, opts, default_headers)
ret = _do_impersonate(data, opts, default_headers);
if(ret)
return ret;

Expand Down
3 changes: 2 additions & 1 deletion lib/easyoptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ struct curl_easyoption Curl_easyopts[] = {
CURLOT_STRING, 0},
{"HTTP2_SETTINGS", CURLOPT_HTTP2_SETTINGS, CURLOT_STRING, 0},
{"HTTP2_WINDOW_UPDATE", CURLOPT_HTTP2_WINDOW_UPDATE, CURLOT_LONG, 0},
{"HTTP2_STREAMS", CURLOPT_HTTP2_STREAMS, CURLOT_STRING, 0},
{"HTTPAUTH", CURLOPT_HTTPAUTH, CURLOT_VALUES, 0},
{"HTTPGET", CURLOPT_HTTPGET, CURLOT_LONG, 0},
{"HTTPBASEHEADER", CURLOPT_HTTPBASEHEADER, CURLOT_SLIST, 0},
Expand Down Expand Up @@ -384,6 +385,6 @@ struct curl_easyoption Curl_easyopts[] = {
*/
int Curl_easyopts_check(void)
{
return ((CURLOPT_LASTENTRY%10000) != (333 + 1));
return ((CURLOPT_LASTENTRY%10000) != (334 + 1));
}
#endif
90 changes: 90 additions & 0 deletions lib/http2.c
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ static int populate_settings(nghttp2_settings_entry *iv,
return i;
}


static ssize_t populate_binsettings(uint8_t *binsettings,
struct Curl_easy *data)
{
Expand Down Expand Up @@ -226,6 +227,75 @@ static void cf_h2_ctx_free(struct cf_h2_ctx *ctx)
}
}

static CURLcode http2_set_stream_priority(struct Curl_cfilter *cf,
struct Curl_easy *data,
int32_t stream_id,
int32_t dep_stream_id,
int32_t weight,
int exclusive
)
{
int rv;
struct cf_h2_ctx *ctx = cf->ctx;
nghttp2_priority_spec pri_spec;

nghttp2_priority_spec_init(&pri_spec, dep_stream_id, weight, exclusive);
rv = nghttp2_submit_priority(ctx->h2, NGHTTP2_FLAG_NONE,
stream_id, &pri_spec);
if(rv) {
failf(data, "nghttp2_submit_priority() failed: %s(%d)",
nghttp2_strerror(rv), rv);
return CURLE_HTTP2;
}

return CURLE_OK;
}

/*
* curl-impersonate: Firefox uses an elaborate scheme of http/2 streams to
* split the load for html/js/css/images. It builds a tree of streams with
* different weights (priorities) by default and communicates this to the
* server. Imitate that behavior.
*/
static CURLcode http2_set_stream_priorities(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
CURLcode result;
char *stream_delimiter = ",";
char *value_delimiter = ":";

if(!data->set.str[STRING_HTTP2_STREAMS])
return CURLE_OK;

char *tmp1 = strdup(data->set.str[STRING_HTTP2_STREAMS]);
char *end1;
char *stream = strtok_r(tmp1, stream_delimiter, &end1);

while(stream != NULL) {

char *tmp2 = strdup(stream);
char *end2;

int32_t stream_id = atoi(strtok_r(tmp2, value_delimiter, &end2));
int exclusive = atoi(strtok_r(NULL, value_delimiter, &end2));
int32_t dep_stream_id = atoi(strtok_r(NULL, value_delimiter, &end2));
int32_t weight = atoi(strtok_r(NULL, value_delimiter, &end2));

free(tmp2);

result = http2_set_stream_priority(cf, data, stream_id, dep_stream_id, weight, exclusive);
if(result) {
free(tmp1);
return result;
}

stream = strtok_r(NULL, stream_delimiter, &end1);
}

free(tmp1);
return CURLE_OK;
}

static CURLcode h2_progress_egress(struct Curl_cfilter *cf,
struct Curl_easy *data);

Expand Down Expand Up @@ -588,6 +658,16 @@ static CURLcode cf_h2_ctx_init(struct Curl_cfilter *cf,
goto out;
}

// curl-impersonate: set stream priorities
result = http2_set_stream_priorities(cf, data);
if(result)
goto out;

#define FIREFOX_DEFAULT_STREAM_ID (15)
/* Best effort to set the request's stream id to 15, like Firefox does. */
// Let's ignore this for now, as it seems not to be targeted as fingerprints.
// nghttp2_session_set_next_stream_id(ctx->h2, FIREFOX_DEFAULT_STREAM_ID);

/* all set, traffic will be send on connect */
result = CURLE_OK;
CURL_TRC_CF(data, cf, "[0] created h2 session%s",
Expand Down Expand Up @@ -1827,19 +1907,22 @@ static ssize_t http2_handle_stream_close(struct Curl_cfilter *cf,
* instead of NGINX default stream weight.
*/
#define CHROME_DEFAULT_STREAM_WEIGHT (256)
#define FIREFOX_DEFAULT_STREAM_WEIGHT (42)

static int sweight_wanted(const struct Curl_easy *data)
{
/* 0 weight is not set by user and we take the nghttp2 default one */
return data->set.priority.weight?
data->set.priority.weight : CHROME_DEFAULT_STREAM_WEIGHT;
// data->set.priority.weight : FIREFOX_DEFAULT_STREAM_WEIGHT;
}

static int sweight_in_effect(const struct Curl_easy *data)
{
/* 0 weight is not set by user and we take the nghttp2 default one */
return data->state.priority.weight?
data->state.priority.weight : NGHTTP2_DEFAULT_WEIGHT;
// data->state.priority.weight : FIREFOX_DEFAULT_STREAM_WEIGHT;
}

/*
Expand All @@ -1848,12 +1931,19 @@ static int sweight_in_effect(const struct Curl_easy *data)
* struct.
*/

/*
* curl-impersonate: By default Firefox uses stream 13 as the "parent" of the
* stream that fetches the main html resource of the web page.
*/
#define FIREFOX_DEFAULT_STREAM_DEP (13)

static void h2_pri_spec(struct Curl_easy *data,
nghttp2_priority_spec *pri_spec)
{
struct Curl_data_priority *prio = &data->set.priority;
struct stream_ctx *depstream = H2_STREAM_CTX(prio->parent);
int32_t depstream_id = depstream? depstream->id:0;
// int32_t depstream_id = depstream? depstream->id:FIREFOX_DEFAULT_STREAM_DEP;
/* curl-impersonate: Set stream exclusive flag to true. */
int exclusive = 1;
nghttp2_priority_spec_init(pri_spec, depstream_id,
Expand Down
39 changes: 39 additions & 0 deletions lib/impersonate.c
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,45 @@ const struct impersonate_opts impersonations[] = {
.http2_window_update = 10485760,
.http2_pseudo_headers_order = "mspa"
},
{
.target = "firefox120",
.httpversion = CURL_HTTP_VERSION_2_0,
.ssl_version = CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_DEFAULT,
.ciphers =
"TLS_AES_128_GCM_SHA256,"
"TLS_CHACHA20_POLY1305_SHA256,"
"TLS_AES_256_GCM_SHA384,"
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,"
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,"
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,"
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,"
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,"
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,"
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,"
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,"
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,"
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,"
"TLS_RSA_WITH_AES_128_GCM_SHA256,"
"TLS_RSA_WITH_AES_256_GCM_SHA384,"
"TLS_RSA_WITH_AES_128_CBC_SHA,"
"TLS_RSA_WITH_AES_256_CBC_SHA",
.http_headers = {
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Te: trailers"
},
.http2_settings = "1:65536;4:131072;5:16384",
.http2_window_update = 12517377,
.http2_pseudo_headers_order = "mpas",
.http2_streams = "3:0:0:201,5:0:0:101,7:0:0:1,9:0:7:1,11:0:3:1,13:0:0:241"
},
{
/* Last one must be NULL. */
.target = NULL
Expand Down
1 change: 1 addition & 0 deletions lib/impersonate.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct impersonate_opts {
const char *http2_pseudo_headers_order;
const char *http2_settings;
int http2_window_update;
const char *http2_streams;
bool tls_permute_extensions;
const char *ech;
/* Other TLS options will come here in the future once they are
Expand Down
4 changes: 4 additions & 0 deletions lib/setopt.c
Original file line number Diff line number Diff line change
Expand Up @@ -2999,6 +2999,10 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
return CURLE_BAD_FUNCTION_ARGUMENT;
data->set.http2_window_update = arg;
break;
case CURLOPT_HTTP2_STREAMS:
result = Curl_setstropt(&data->set.str[STRING_HTTP2_STREAMS],
va_arg(param, char *));
break;
#endif
#ifdef USE_UNIX_SOCKETS
case CURLOPT_UNIX_SOCKET_PATH:
Expand Down
1 change: 1 addition & 0 deletions lib/urldata.h
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,7 @@ enum dupstring {
STRING_SSL_CERT_COMPRESSION,
STRING_HTTP2_PSEUDO_HEADERS_ORDER,
STRING_HTTP2_SETTINGS,
STRING_HTTP2_STREAMS,
STRING_ECH_CONFIG, /* CURLOPT_ECH_CONFIG */
STRING_ECH_PUBLIC, /* CURLOPT_ECH_PUBLIC */

Expand Down
1 change: 1 addition & 0 deletions src/tool_cfgable.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ struct OperationConfig {
char *http2_pseudo_headers_order;
char *http2_settings;
long http2_window_update;
char *http2_streams;
long httpversion;
bool http09_allowed;
bool nobuffer;
Expand Down
6 changes: 6 additions & 0 deletions src/tool_getparam.c
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ static const struct LongShort aliases[]= {
#ifdef USE_ECH
{"ER", "ech", ARG_STRING},
#endif
{"EV", "http2-streams", ARG_STRING},
{"f", "fail", ARG_BOOL},
{"fa", "fail-early", ARG_BOOL},
{"fb", "styled-output", ARG_BOOL},
Expand Down Expand Up @@ -2179,6 +2180,11 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
return PARAM_BAD_NUMERIC;
break;

case 'V':
/* --http2-streams */
GetStr(&config->http2_streams, nextarg);
break;

#ifdef USE_ECH
case 'R':
if(strlen(nextarg) != 6 || !strncasecompare("GREASE", nextarg, 6)) {
Expand Down
4 changes: 4 additions & 0 deletions src/tool_operate.c
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,10 @@ static CURLcode single_transfer(struct GlobalConfig *global,
CURLOPT_HTTP2_WINDOW_UPDATE,
config->http2_window_update);

if(config->http2_streams)
my_setopt_str(curl,
CURLOPT_HTTP2_STREAMS,
config->http2_streams);

} /* (proto_http) */

Expand Down

0 comments on commit 1c725f7

Please sign in to comment.