|| cf_hc_baller_data_pending(&ctx->h21_baller, data);
}
+static struct curltime get_max_baller_time(struct Curl_cfilter *cf,
+ struct Curl_easy *data,
+ int query)
+{
+ struct cf_hc_ctx *ctx = cf->ctx;
+ struct Curl_cfilter *cfb;
+ struct curltime t, tmax;
+
+ memset(&tmax, 0, sizeof(tmax));
+ memset(&t, 0, sizeof(t));
+ cfb = ctx->h21_baller.enabled? ctx->h21_baller.cf : NULL;
+ if(cfb && !cfb->cft->query(cfb, data, query, NULL, &t)) {
+ if((t.tv_sec || t.tv_usec) && Curl_timediff_us(t, tmax) > 0)
+ tmax = t;
+ }
+ memset(&t, 0, sizeof(t));
+ cfb = ctx->h3_baller.enabled? ctx->h3_baller.cf : NULL;
+ if(cfb && !cfb->cft->query(cfb, data, query, NULL, &t)) {
+ if((t.tv_sec || t.tv_usec) && Curl_timediff_us(t, tmax) > 0)
+ tmax = t;
+ }
+ return tmax;
+}
+
+static CURLcode cf_hc_query(struct Curl_cfilter *cf,
+ struct Curl_easy *data,
+ int query, int *pres1, void *pres2)
+{
+ if(!cf->connected) {
+ switch(query) {
+ case CF_QUERY_TIMER_CONNECT: {
+ struct curltime *when = pres2;
+ *when = get_max_baller_time(cf, data, CF_QUERY_TIMER_CONNECT);
+ return CURLE_OK;
+ }
+ case CF_QUERY_TIMER_APPCONNECT: {
+ struct curltime *when = pres2;
+ *when = get_max_baller_time(cf, data, CF_QUERY_TIMER_APPCONNECT);
+ return CURLE_OK;
+ }
+ default:
+ break;
+ }
+ }
+ return cf->next?
+ cf->next->cft->query(cf->next, data, query, pres1, pres2) :
+ CURLE_UNKNOWN_OPTION;
+}
+
static void cf_hc_close(struct Curl_cfilter *cf, struct Curl_easy *data)
{
DEBUGF(LOG_CF(data, cf, "close"));
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
- Curl_cf_def_query,
+ cf_hc_query,
};
static CURLcode cf_hc_create(struct Curl_cfilter **pcf,
case CF_CTRL_CONN_INFO_UPDATE:
cf_socket_active(cf, data);
break;
- case CF_CTRL_CONN_REPORT_STATS:
- switch(ctx->transport) {
- case TRNSPRT_UDP:
- case TRNSPRT_QUIC:
- /* Since UDP connected sockets work different from TCP, we use the
- * time of the first byte from the peer as the "connect" time. */
- if(ctx->got_first_byte) {
- Curl_pgrsTimeWas(data, TIMER_CONNECT, ctx->first_byte_at);
- break;
- }
- /* FALLTHROUGH */
- default:
- Curl_pgrsTimeWas(data, TIMER_CONNECT, ctx->connected_at);
- break;
- }
- break;
case CF_CTRL_DATA_SETUP:
Curl_persistconninfo(data, cf->conn, ctx->l_ip, ctx->l_port);
break;
else
*pres1 = -1;
return CURLE_OK;
+ case CF_QUERY_TIMER_CONNECT: {
+ struct curltime *when = pres2;
+ switch(ctx->transport) {
+ case TRNSPRT_UDP:
+ case TRNSPRT_QUIC:
+ /* Since UDP connected sockets work different from TCP, we use the
+ * time of the first byte from the peer as the "connect" time. */
+ if(ctx->got_first_byte) {
+ *when = ctx->first_byte_at;
+ break;
+ }
+ /* FALLTHROUGH */
+ default:
+ *when = ctx->connected_at;
+ break;
+ }
+ return CURLE_OK;
+ }
default:
break;
}
result = cf->cft->connect(cf, data, blocking, done);
if(!result && *done) {
Curl_conn_ev_update_info(data, data->conn);
- Curl_conn_ev_report_stats(data, data->conn);
+ Curl_conn_report_connect_stats(data, data->conn);
data->conn->keepalive = Curl_now();
}
+ else if(result) {
+ Curl_conn_report_connect_stats(data, data->conn);
+ }
}
return result;
cf_cntrl_all(conn, data, TRUE, CF_CTRL_CONN_INFO_UPDATE, 0, NULL);
}
-void Curl_conn_ev_report_stats(struct Curl_easy *data,
- struct connectdata *conn)
+void Curl_conn_report_connect_stats(struct Curl_easy *data,
+ struct connectdata *conn)
{
- cf_cntrl_all(conn, data, TRUE, CF_CTRL_CONN_REPORT_STATS, 0, NULL);
+ struct Curl_cfilter *cf = conn->cfilter[FIRSTSOCKET];
+ if(cf) {
+ struct curltime connected;
+ struct curltime appconnected;
+
+ memset(&connected, 0, sizeof(connected));
+ cf->cft->query(cf, data, CF_QUERY_TIMER_CONNECT, NULL, &connected);
+ if(connected.tv_sec || connected.tv_usec)
+ Curl_pgrsTimeWas(data, TIMER_CONNECT, connected);
+
+ memset(&appconnected, 0, sizeof(appconnected));
+ cf->cft->query(cf, data, CF_QUERY_TIMER_APPCONNECT, NULL, &appconnected);
+ if(appconnected.tv_sec || appconnected.tv_usec)
+ Curl_pgrsTimeWas(data, TIMER_APPCONNECT, appconnected);
+ }
}
bool Curl_conn_is_alive(struct Curl_easy *data, struct connectdata *conn)
#define CF_CTRL_DATA_DONE_SEND 8 /* 0 NULL ignored */
/* update conn info at connection and data */
#define CF_CTRL_CONN_INFO_UPDATE (256+0) /* 0 NULL ignored */
-/* report conn statistics (timers) for connection and data */
-#define CF_CTRL_CONN_REPORT_STATS (256+1) /* 0 NULL ignored */
/**
* Handle event/control for the filter.
#define CF_QUERY_MAX_CONCURRENT 1 /* number - */
#define CF_QUERY_CONNECT_REPLY_MS 2 /* number - */
#define CF_QUERY_SOCKET 3 /* - curl_socket_t */
+#define CF_QUERY_TIMER_CONNECT 4 /* - struct curltime */
+#define CF_QUERY_TIMER_APPCONNECT 5 /* - struct curltime */
/**
* Query the cfilter for properties. Filters ignorant of a query will
struct connectdata *conn);
/**
- * Inform connection filters to report statistics.
+ * Update connection statistics
*/
-void Curl_conn_ev_report_stats(struct Curl_easy *data,
- struct connectdata *conn);
+void Curl_conn_report_connect_stats(struct Curl_easy *data,
+ struct connectdata *conn);
/**
* Check if FIRSTSOCKET's cfilter chain deems connection alive.
return FALSE;
}
+static struct curltime get_max_baller_time(struct Curl_cfilter *cf,
+ struct Curl_easy *data,
+ int query)
+{
+ struct cf_he_ctx *ctx = cf->ctx;
+ struct curltime t, tmax;
+ size_t i;
+
+ memset(&tmax, 0, sizeof(tmax));
+ for(i = 0; i < sizeof(ctx->baller)/sizeof(ctx->baller[0]); i++) {
+ struct eyeballer *baller = ctx->baller[i];
+
+ memset(&t, 0, sizeof(t));
+ if(baller && baller->cf &&
+ !baller->cf->cft->query(baller->cf, data, query, NULL, &t)) {
+ if((t.tv_sec || t.tv_usec) && Curl_timediff_us(t, tmax) > 0)
+ tmax = t;
+ }
+ }
+ return tmax;
+}
+
static CURLcode cf_he_query(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query, int *pres1, void *pres2)
DEBUGF(LOG_CF(data, cf, "query connect reply: %dms", *pres1));
return CURLE_OK;
}
-
+ case CF_QUERY_TIMER_CONNECT: {
+ struct curltime *when = pres2;
+ *when = get_max_baller_time(cf, data, CF_QUERY_TIMER_CONNECT);
+ return CURLE_OK;
+ }
+ case CF_QUERY_TIMER_APPCONNECT: {
+ struct curltime *when = pres2;
+ *when = get_max_baller_time(cf, data, CF_QUERY_TIMER_APPCONNECT);
+ return CURLE_OK;
+ }
default:
break;
}
struct Curl_easy *data,
int event, int arg1, void *arg2)
{
- struct cf_msh3_ctx *ctx = cf->ctx;
struct HTTP *stream = data->req.p.http;
CURLcode result = CURLE_OK;
DEBUGF(LOG_CF(data, cf, "req: update info"));
cf_msh3_active(cf, data);
break;
- case CF_CTRL_CONN_REPORT_STATS:
- if(cf->sockindex == FIRSTSOCKET)
- Curl_pgrsTimeWas(data, TIMER_APPCONNECT, ctx->handshake_at);
- break;
-
default:
break;
}
*pres1 = 100;
return CURLE_OK;
}
+ case CF_QUERY_TIMER_CONNECT: {
+ struct curltime *when = pres2;
+ /* we do not know when the first byte arrived */
+ if(cf->connected)
+ *when = ctx->handshake_at;
+ return CURLE_OK;
+ }
+ case CF_QUERY_TIMER_APPCONNECT: {
+ struct curltime *when = pres2;
+ if(cf->connected)
+ *when = ctx->handshake_at;
+ return CURLE_OK;
+ }
default:
break;
}
}
}
break;
- case CF_CTRL_CONN_REPORT_STATS:
- if(cf->sockindex == FIRSTSOCKET) {
- if(ctx->got_first_byte)
- Curl_pgrsTimeWas(data, TIMER_CONNECT, ctx->first_byte_at);
- Curl_pgrsTimeWas(data, TIMER_APPCONNECT, ctx->handshake_at);
- }
- break;
default:
break;
}
else
*pres1 = -1;
return CURLE_OK;
+ case CF_QUERY_TIMER_CONNECT: {
+ struct curltime *when = pres2;
+ if(ctx->got_first_byte)
+ *when = ctx->first_byte_at;
+ return CURLE_OK;
+ }
+ case CF_QUERY_TIMER_APPCONNECT: {
+ struct curltime *when = pres2;
+ if(cf->connected)
+ *when = ctx->handshake_at;
+ return CURLE_OK;
+ }
default:
break;
}
case CF_CTRL_DATA_IDLE:
/* anything to do? */
break;
- case CF_CTRL_CONN_REPORT_STATS:
- if(cf->sockindex == FIRSTSOCKET) {
- if(ctx->got_first_byte)
- Curl_pgrsTimeWas(data, TIMER_CONNECT, ctx->first_byte_at);
- Curl_pgrsTimeWas(data, TIMER_APPCONNECT, ctx->handshake_at);
- }
- break;
default:
break;
}
else
*pres1 = -1;
return CURLE_OK;
+ case CF_QUERY_TIMER_CONNECT: {
+ struct curltime *when = pres2;
+ if(ctx->got_first_byte)
+ *when = ctx->first_byte_at;
+ return CURLE_OK;
+ }
+ case CF_QUERY_TIMER_APPCONNECT: {
+ struct curltime *when = pres2;
+ if(cf->connected)
+ *when = ctx->handshake_at;
+ return CURLE_OK;
+ }
default:
break;
}
struct Curl_easy *data,
int event, int arg1, void *arg2)
{
- struct ssl_connect_data *connssl = cf->ctx;
struct cf_call_data save;
(void)arg1;
(void)arg2;
switch(event) {
- case CF_CTRL_CONN_REPORT_STATS:
- if(cf->sockindex == FIRSTSOCKET && !Curl_ssl_cf_is_proxy(cf))
- Curl_pgrsTimeWas(data, TIMER_APPCONNECT, connssl->handshake_done);
- break;
case CF_CTRL_DATA_ATTACH:
if(Curl_ssl->attach_data) {
CF_DATA_SAVE(save, cf, data);
return CURLE_OK;
}
+static CURLcode ssl_cf_query(struct Curl_cfilter *cf,
+ struct Curl_easy *data,
+ int query, int *pres1, void *pres2)
+{
+ struct ssl_connect_data *connssl = cf->ctx;
+
+ switch(query) {
+ case CF_QUERY_TIMER_APPCONNECT: {
+ struct curltime *when = pres2;
+ if(cf->connected && !Curl_ssl_cf_is_proxy(cf))
+ *when = connssl->handshake_done;
+ return CURLE_OK;
+ }
+ default:
+ break;
+ }
+ return cf->next?
+ cf->next->cft->query(cf->next, data, query, pres1, pres2) :
+ CURLE_UNKNOWN_OPTION;
+}
+
static bool cf_ssl_is_alive(struct Curl_cfilter *cf, struct Curl_easy *data)
{
struct cf_call_data save;
ssl_cf_cntrl,
cf_ssl_is_alive,
Curl_cf_def_conn_keep_alive,
- Curl_cf_def_query,
+ ssl_cf_query,
};
struct Curl_cftype Curl_cft_ssl_proxy = {
@pytest.mark.skipif(condition=Env.setup_incomplete(),
reason=f"missing: {Env.incomplete_reason()}")
-@pytest.mark.skipif(condition=not Env.have_h3_server(),
- reason=f"missing HTTP/3 server")
-@pytest.mark.skipif(condition=not Env.have_h3_curl(),
- reason=f"curl built without HTTP/3")
class TestEyeballs:
@pytest.fixture(autouse=True, scope='class')
nghttpx.start_if_needed()
# download using only HTTP/3 on working server
+ @pytest.mark.skipif(condition=not Env.have_h3(), reason=f"missing HTTP/3 support")
def test_06_01_h3_only(self, env: Env, httpd, nghttpx, repeat):
curl = CurlClient(env=env)
urln = f'https://{env.authority_for(env.domain1, "h3")}/data.json'
assert r.stats[0]['http_version'] == '3'
# download using only HTTP/3 on missing server
+ @pytest.mark.skipif(condition=not Env.have_h3(), reason=f"missing HTTP/3 support")
def test_06_02_h3_only(self, env: Env, httpd, nghttpx, repeat):
nghttpx.stop_if_running()
curl = CurlClient(env=env)
assert r.exit_code == 7, f'{r}' # could not connect
# download using HTTP/3 on missing server with fallback on h2
+ @pytest.mark.skipif(condition=not Env.have_h3(), reason=f"missing HTTP/3 support")
def test_06_03_h3_fallback_h2(self, env: Env, httpd, nghttpx, repeat):
nghttpx.stop_if_running()
curl = CurlClient(env=env)
assert r.stats[0]['http_version'] == '2'
# download using HTTP/3 on missing server with fallback on http/1.1
+ @pytest.mark.skipif(condition=not Env.have_h3(), reason=f"missing HTTP/3 support")
def test_06_04_h3_fallback_h1(self, env: Env, httpd, nghttpx, repeat):
nghttpx.stop_if_running()
curl = CurlClient(env=env)
assert r.exit_code == 0, f'{r}'
r.check_stats(count=1, exp_status=200)
assert r.stats[0]['http_version'] == '1.1'
+
+ # make a successful https: transfer and observer the timer stats
+ def test_06_10_stats_success(self, env: Env, httpd, nghttpx, repeat):
+ curl = CurlClient(env=env)
+ urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json'
+ r = curl.http_download(urls=[urln])
+ assert r.exit_code == 0, f'{r}'
+ r.check_stats(count=1, exp_status=200)
+ assert r.stats[0]['time_connect'] > 0.0
+ assert r.stats[0]['time_appconnect'] > 0.0
+
+ # make https: to a hostname that tcp connects, but will not verify
+ def test_06_11_stats_fail_verify(self, env: Env, httpd, nghttpx, repeat):
+ curl = CurlClient(env=env)
+ urln = f'https://not-valid.com:{env.https_port}/data.json'
+ r = curl.http_download(urls=[urln], extra_args=[
+ '--resolve', f'not-valid.com:{env.https_port}:127.0.0.1'
+ ])
+ assert r.exit_code != 0, f'{r}'
+ r.check_stats(count=1, exp_status=0)
+ assert r.stats[0]['time_connect'] > 0.0 # was tcp connected
+ assert r.stats[0]['time_appconnect'] == 0 # but not SSL verified
+
+ # make https: to an invalid address
+ def test_06_12_stats_fail_tcp(self, env: Env, httpd, nghttpx, repeat):
+ curl = CurlClient(env=env)
+ urln = f'https://not-valid.com:1/data.json'
+ r = curl.http_download(urls=[urln], extra_args=[
+ '--resolve', f'not-valid.com:{1}:127.0.0.1'
+ ])
+ assert r.exit_code != 0, f'{r}'
+ r.check_stats(count=1, exp_status=0)
+ assert r.stats[0]['time_connect'] == 0 # no one should have listened
+ assert r.stats[0]['time_appconnect'] == 0 # did not happen either
+