/
niceSOFT
/
wget2
Обзор
Документация
Войти
/
niceSOFT
/
wget2
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
libwget/http2.c
435 строк
14 KB
Tim Rühsen
Add Content-Length: 0, following RFC 9110
29 июл 2026, 20:05
Не верифицирован
29 июл 2026, 20:05
491a400
Код
Авторство
О чём код?
#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; if (wget_decompress(ctx->decompressor, (char *) data, len)) return NGHTTP2_ERR_CALLBACK_FAILURE; } 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; } // Takes ownership of req on success, means the caller's req will be set to NULL to avoid // double-free and free-after-use failures. 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]; bool add_content_length = ((*req)->body && (*req)->body_length) || wget_http_method_defines_content((*req)->method); 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; } if (!wget_strcasecmp_ascii(param->name, "Content-Length")) { add_content_length = false; // Keep user supplied Content-Length header } init_nv(nvp++, param->name, param->value); } if (add_content_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->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, (*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; } debug_printf("HTTP2 stream id %d\n", (*req)->stream_id); ctx->resp->req = *req; *req = NULL; conn->pending_http2_requests++; 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; }