Files
wget2/libwget/http2.c
Tim Rühsen ec27488fea Fix downloading multiple files via HTTP/2
* include/wget/wget.h: New function declaration wget_http_connection_receive_only().
* libwget/http.c: New function definition wget_http_connection_receive_only().
* libwget/http.h (struct wget_http_connection_st): Add member goaway.
* libwget/http2.c (struct http2_stream_context): Add member conn,
  (on_frame_recv_callback): Handle NGHTTP2_GOAWAY,
  (wget_http2_send_request): Initialize conn member.
* src/host.c (_release_job): Don't release parts if in 'done' state.
* src/wget.c (process_response_header): Remove handling of LINK headers,
  (process_response): Add handling of LINK headers,
  (downloader_thread): Handle pending responses correctly.

This patch fixes two related issues:
1. With HTTP/2, servers sometimes limit the number of requests per connection.
   Beforethis patch, the connection was closed too early after GOAWAY and pending
   responses weren't received.
2. In _release_job(), already downloaded parts of a file were erroneous released.
   This caused successful metalink and chunked downloads to fail in combination with
   a GOAWAY or a remotely closed connection.
2024-09-11 17:45:09 +02:00

425 lines
13 KiB
C

#include <config.h>
#include <errno.h>
#include <wget.h>
#include <nghttp2/nghttp2.h>
#include "private.h"
#include "http.h"
struct http2_stream_context {
wget_http_connection
*conn;
wget_http_response
*resp;
wget_decompressor
*decompressor;
};
static ssize_t send_callback(nghttp2_session *session WGET_GCC_UNUSED,
const uint8_t *data, size_t length, int flags WGET_GCC_UNUSED, void *user_data)
{
wget_http_connection *conn = (wget_http_connection *)user_data;
ssize_t rc;
// debug_printf("writing... %zd\n", length);
if ((rc = wget_tcp_write(conn->tcp, (const char *)data, length)) <= 0) {
// An error will be written by the wget_tcp_write function.
// debug_printf("write rc %d, errno=%d\n", rc, errno);
return rc ? NGHTTP2_ERR_CALLBACK_FAILURE : NGHTTP2_ERR_WOULDBLOCK;
}
// debug_printf("write rc %d\n",rc);
return rc;
}
static void print_frame_type(int type, const char tag, int streamid)
{
static const char *name[] = {
[NGHTTP2_DATA] = "DATA",
[NGHTTP2_HEADERS] = "HEADERS",
[NGHTTP2_PRIORITY] = "PRIORITY",
[NGHTTP2_RST_STREAM] = "RST_STREAM",
[NGHTTP2_SETTINGS] = "SETTINGS",
[NGHTTP2_PUSH_PROMISE] = "PUSH_PROMISE",
[NGHTTP2_PING] = "PING",
[NGHTTP2_GOAWAY] = "GOAWAY",
[NGHTTP2_WINDOW_UPDATE] = "WINDOW_UPDATE",
[NGHTTP2_CONTINUATION] = "CONTINUATION"
};
if ((unsigned) type < countof(name)) {
// Avoid printing frame info for DATA frames
if (type != NGHTTP2_DATA)
debug_printf("[FRAME %d] %c %s\n", streamid, tag, name[type]);
} else
debug_printf("[FRAME %d] %c Unknown type %d\n", streamid, tag, type);
}
static int on_frame_send_callback(nghttp2_session *session WGET_GCC_UNUSED,
const nghttp2_frame *frame, void *user_data WGET_GCC_UNUSED)
{
print_frame_type(frame->hd.type, '>', frame->hd.stream_id);
if (frame->hd.type == NGHTTP2_HEADERS) {
const nghttp2_nv *nva = frame->headers.nva;
for (unsigned i = 0; i < frame->headers.nvlen; i++)
debug_printf("[FRAME %d] > %.*s: %.*s\n", frame->hd.stream_id,
(int)nva[i].namelen, nva[i].name, (int)nva[i].valuelen, nva[i].value);
}
return 0;
}
static int on_frame_recv_callback(nghttp2_session *session,
const nghttp2_frame *frame, void *user_data WGET_GCC_UNUSED)
{
print_frame_type(frame->hd.type, '<', frame->hd.stream_id);
// header callback after receiving all header tags
if (frame->hd.type == NGHTTP2_HEADERS) {
struct http2_stream_context *ctx = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
wget_http_response *resp = ctx ? ctx->resp : NULL;
if (resp) {
if (resp->header && resp->req->header_callback) {
resp->req->header_callback(resp, resp->req->header_user_data);
}
http_fix_broken_server_encoding(resp);
if (!ctx->decompressor) {
ctx->decompressor = wget_decompress_open(resp->content_encoding,
http_get_body_cb, resp);
wget_decompress_set_error_handler(ctx->decompressor, http_decompress_error_handler_cb);
}
}
}
else if (frame->hd.type == NGHTTP2_GOAWAY) {
struct http2_stream_context *ctx = nghttp2_session_get_stream_user_data(session, frame->goaway.last_stream_id);
wget_http_connection *conn = ctx ? ctx->conn : NULL;
if (conn) {
conn->goaway = true;
}
}
return 0;
}
static int on_header_callback(nghttp2_session *session,
const nghttp2_frame *frame, const uint8_t *name, size_t namelen,
const uint8_t *value, size_t valuelen,
uint8_t flags WGET_GCC_UNUSED, void *user_data WGET_GCC_UNUSED)
{
struct http2_stream_context *ctx = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id);
wget_http_response *resp = ctx ? ctx->resp : NULL;
if (!resp)
return 0;
if (resp->req->response_keepheader || resp->req->header_callback) {
if (!resp->header)
resp->header = wget_buffer_alloc(1024);
}
if (frame->hd.type == NGHTTP2_HEADERS) {
if (frame->headers.cat == NGHTTP2_HCAT_RESPONSE) {
debug_printf("%.*s: %.*s\n", (int) namelen, name, (int) valuelen, value);
if (resp->header)
wget_buffer_printf_append(resp->header, "%.*s: %.*s\n", (int) namelen, name, (int) valuelen, value);
wget_http_parse_header_line(resp, (char *) name, namelen, (char *) value, valuelen);
}
}
return 0;
}
/*
* This function is called to indicate that a stream is closed.
*/
static int on_stream_close_callback(nghttp2_session *session, int32_t stream_id,
uint32_t error_code WGET_GCC_UNUSED, void *user_data)
{
struct http2_stream_context *ctx = nghttp2_session_get_stream_user_data(session, stream_id);
debug_printf("closing stream %d\n", stream_id);
if (ctx) {
wget_http_connection *conn = (wget_http_connection *) user_data;
ctx->resp->response_end = wget_get_timemillis(); // Final transmission time.
wget_vector_add(conn->received_http2_responses, ctx->resp);
wget_decompress_close(ctx->decompressor);
nghttp2_session_set_stream_user_data(session, stream_id, NULL);
xfree(ctx);
}
return 0;
}
/*
* The implementation of nghttp2_on_data_chunk_recv_callback type. We
* use this function to print the received response body.
*/
static int on_data_chunk_recv_callback(nghttp2_session *session,
uint8_t flags WGET_GCC_UNUSED, int32_t stream_id,
const uint8_t *data, size_t len, void *user_data WGET_GCC_UNUSED)
{
struct http2_stream_context *ctx = nghttp2_session_get_stream_user_data(session, stream_id);
if (ctx) {
// debug_printf("[INFO] C <---------------------------- S%d (DATA chunk - %zu bytes)\n", stream_id, len);
// debug_printf("nbytes %zu\n", len);
ctx->resp->req->first_response_start = wget_get_timemillis();
ctx->resp->cur_downloaded += len;
wget_decompress(ctx->decompressor, (char *) data, len);
}
return 0;
}
static void setup_nghttp2_callbacks(nghttp2_session_callbacks *callbacks)
{
nghttp2_session_callbacks_set_send_callback(callbacks, send_callback);
nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, on_frame_send_callback);
nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, on_frame_recv_callback);
nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, on_stream_close_callback);
nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, on_data_chunk_recv_callback);
nghttp2_session_callbacks_set_on_header_callback(callbacks, on_header_callback);
}
int wget_http2_open(wget_http_connection *conn)
{
int rc;
nghttp2_session_callbacks *callbacks;
if (nghttp2_session_callbacks_new(&callbacks)) {
error_printf(_("Failed to create HTTP2 callbacks\n"));
return WGET_E_INVALID;
}
setup_nghttp2_callbacks(callbacks);
rc = nghttp2_session_client_new(&conn->http2_session, callbacks, conn);
nghttp2_session_callbacks_del(callbacks);
if (rc) {
error_printf(_("Failed to create HTTP2 client session (%d)\n"), rc);
return WGET_E_INVALID;
}
nghttp2_settings_entry iv[] = {
// {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100},
{NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, 1 << 30}, // prevent window size changes
{NGHTTP2_SETTINGS_ENABLE_PUSH, 0}, // avoid push messages from server
};
if ((rc = nghttp2_submit_settings(conn->http2_session, NGHTTP2_FLAG_NONE, iv, countof(iv)))) {
error_printf(_("Failed to submit HTTP2 client settings (%d)\n"), rc);
return WGET_E_INVALID;
}
#if NGHTTP2_VERSION_NUM >= 0x010c00
// without this we experience slow downloads on fast networks
if ((rc = nghttp2_session_set_local_window_size(conn->http2_session, NGHTTP2_FLAG_NONE, 0, 1 << 30)))
debug_printf("Failed to set HTTP2 connection level window size (%d)\n", rc);
#endif
conn->received_http2_responses = wget_vector_create(16, NULL);
return rc;
}
void wget_http2_close(wget_http_connection **conn)
{
if ((*conn)->http2_session) {
int rc = nghttp2_session_terminate_session((*conn)->http2_session, NGHTTP2_NO_ERROR);
if (rc)
error_printf(_("Failed to terminate HTTP2 session (%d)\n"), rc);
nghttp2_session_del((*conn)->http2_session);
}
wget_vector_clear_nofree((*conn)->received_http2_responses);
wget_vector_free(&(*conn)->received_http2_responses);
}
static ssize_t data_prd_read_callback(
nghttp2_session *session, int32_t stream_id, uint8_t *buf, size_t length,
uint32_t *data_flags, nghttp2_data_source *source, void *user_data WGET_GCC_UNUSED)
{
struct http2_stream_context *ctx = nghttp2_session_get_stream_user_data(session, stream_id);
const char *bodyp = source->ptr;
if (!ctx)
return NGHTTP2_ERR_CALLBACK_FAILURE;
// debug_printf("[INFO] C ----------------------------> S (DATA post body), length:%zu %zu\n", length, ctx->resp->req->body_length);
size_t len = ctx->resp->req->body_length - (bodyp - ctx->resp->req->body);
if (len > length)
len = length;
memcpy(buf, bodyp, len);
source->ptr = (char *) (bodyp + len);
if (!len)
*data_flags = NGHTTP2_DATA_FLAG_EOF;
return len;
}
static void init_nv(nghttp2_nv *nv, const char *name, const char *value)
{
nv->name = (uint8_t *)name;
nv->namelen = strlen(name);
nv->value = (uint8_t *)value;
nv->valuelen = strlen(value);
nv->flags = NGHTTP2_NV_FLAG_NONE;
}
int wget_http2_send_request(wget_http_connection *conn, wget_http_request *req)
{
char length_str[32];
nghttp2_nv *nvs, *nvp;
char *resource;
if (!(nvs = wget_malloc(sizeof(nghttp2_nv) * (4 + wget_vector_size(req->headers))))) {
error_printf(_("Failed to allocate nvs[%d]\n"), 4 + wget_vector_size(req->headers));
return -1;
}
if (!(resource = wget_malloc(req->esc_resource.length + 2))) {
xfree(nvs);
error_printf(_("Failed to allocate resource[%zu]\n"), req->esc_resource.length + 2);
return -1;
}
resource[0] = '/';
memcpy(resource + 1, req->esc_resource.data, req->esc_resource.length + 1);
init_nv(&nvs[0], ":method", req->method);
init_nv(&nvs[1], ":path", resource);
init_nv(&nvs[2], ":scheme", "https");
// init_nv(&nvs[3], ":authority", req->esc_host.data);
nvp = &nvs[4];
for (int it = 0; it < wget_vector_size(req->headers); it++) {
wget_http_header_param *param = wget_vector_get(req->headers, it);
if (!param)
continue;
if (!wget_strcasecmp_ascii(param->name, "Connection"))
continue;
if (!wget_strcasecmp_ascii(param->name, "Transfer-Encoding"))
continue;
if (!wget_strcasecmp_ascii(param->name, "Host")) {
init_nv(&nvs[3], ":authority", param->value);
continue;
}
init_nv(nvp++, param->name, param->value);
}
if (req->body_length) {
wget_snprintf(length_str, sizeof(length_str), "%zu", req->body_length);
init_nv(nvp++, "Content-Length", length_str);
}
struct http2_stream_context *ctx = wget_calloc(1, sizeof(struct http2_stream_context));
if (!ctx) {
return -1;
}
// HTTP/2.0 has the streamid as a link to request and connection.
ctx->conn = conn;
ctx->resp = wget_calloc(1, sizeof(wget_http_response));
if (!ctx->resp) {
xfree(ctx);
return -1;
}
ctx->resp->req = req;
ctx->resp->major = 2;
// we do not get a Keep-Alive header in HTTP2 - let's assume the connection stays open
ctx->resp->keep_alive = 1;
req->request_start = wget_get_timemillis();
if (req->body_length) {
nghttp2_data_provider data_prd;
data_prd.source.ptr = (void *) req->body;
debug_printf("body length: %zu %zu\n", req->body_length, ctx->resp->req->body_length);
data_prd.read_callback = data_prd_read_callback;
req->stream_id = nghttp2_submit_request(conn->http2_session, NULL, nvs, nvp - nvs, &data_prd, ctx);
} else {
// nghttp2 does strdup of name+value and lowercase conversion of 'name'
req->stream_id = nghttp2_submit_request(conn->http2_session, NULL, nvs, nvp - nvs, NULL, ctx);
}
xfree(resource);
xfree(nvs);
if (req->stream_id < 0) {
error_printf(_("Failed to submit HTTP2 request\n"));
wget_http_free_response(&ctx->resp);
xfree(ctx);
return -1;
}
conn->pending_http2_requests++;
debug_printf("HTTP2 stream id %d\n", req->stream_id);
return 0;
}
wget_http_response *wget_http2_get_response_cb(wget_http_connection *conn, wget_server_stats_callback *server_stats_callback)
{
char *buf;
size_t bufsize;
ssize_t nbytes;
wget_http_response *resp = NULL;
debug_printf(" ## pending_requests = %d\n", conn->pending_http2_requests);
if (conn->pending_http2_requests > 0)
conn->pending_http2_requests--;
else
return NULL;
// reuse generic connection buffer
buf = conn->buf->data;
bufsize = conn->buf->size;
while (!wget_vector_size(conn->received_http2_responses) && !http_connection_is_aborted(conn)) {
int rc;
while (nghttp2_session_want_write(conn->http2_session) && nghttp2_session_send(conn->http2_session) == 0)
;
if ((nbytes = wget_tcp_read(conn->tcp, buf, bufsize)) <= 0) {
debug_printf("failed to receive: %d (nbytes=%ld)\n", errno, (long) nbytes);
if (nbytes == -1)
break;
// nbytes == 0 has been seen on Win11, continue looping
}
if ((nbytes = nghttp2_session_mem_recv(conn->http2_session, (uint8_t *) buf, nbytes)) < 0) {
rc = (int) nbytes;
debug_printf("mem_recv failed: %d %s\n", rc, nghttp2_strerror(rc));
break;
}
// debug_printf(" ## loop responses=%d rc=%d nbytes=%zd\n", wget_vector_size(conn->received_http2_responses), rc, nbytes);
}
resp = wget_vector_get(conn->received_http2_responses, 0); // should use double linked lists here
if (server_stats_callback)
server_stats_callback(conn, resp);
if (resp) {
debug_printf(" ## response status %d\n", resp->code);
wget_vector_remove_nofree(conn->received_http2_responses, 0);
}
return resp;
}