| /* | |
| * GPAC Multimedia Framework | |
| * | |
| * Authors: Jean Le Feuvre | |
| * Copyright (c) Telecom ParisTech 2005-2026 | |
| * All rights reserved | |
| * | |
| * This file is part of GPAC / downloader sub-project | |
| * | |
| * GPAC is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU Lesser General Public License as published by | |
| * the Free Software Foundation; either version 2, or (at your option) | |
| * any later version. | |
| * | |
| * GPAC is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public | |
| * License along with this library; see the file COPYING. If not, write to | |
| * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. | |
| * | |
| */ | |
| #include "downloader.h" | |
| #if !defined(GPAC_DISABLE_NETWORK) && defined(GPAC_HAS_HTTP2) | |
| #include <nghttp2/nghttp2.h> | |
| #define HTTP2_BUFFER_SETTINGS_SIZE 128 | |
| //link for msvc | |
| #if !defined(__GNUC__) && ( defined(_WIN32_WCE) || defined (WIN32) ) | |
| #pragma comment(lib, "nghttp2") | |
| #endif | |
| static void h2_flush_send_ex(GF_DownloadSession *sess, Bool flush_local_buf); | |
| static int h2_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 , | |
| void *user_data) | |
| { | |
| GF_DownloadSession *sess; | |
| switch (frame->hd.type) { | |
| case NGHTTP2_HEADERS: | |
| sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| if (!sess) | |
| return NGHTTP2_ERR_CALLBACK_FAILURE; | |
| if ( | |
| (!sess->server_mode && (frame->headers.cat == NGHTTP2_HCAT_RESPONSE)) | |
| || (sess->server_mode && ((frame->headers.cat == NGHTTP2_HCAT_HEADERS) || (frame->headers.cat == NGHTTP2_HCAT_REQUEST))) | |
| ) { | |
| GF_HTTPHeader *hdrp; | |
| GF_SAFEALLOC(hdrp, GF_HTTPHeader); | |
| if (hdrp) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id "LLD" got header %s: %s\n", sess->hmux_stream_id, name, value)); | |
| hdrp->name = gf_strdup(name); | |
| hdrp->value = gf_strdup(value); | |
| gf_list_add(sess->headers, hdrp); | |
| } | |
| break; | |
| } | |
| } | |
| return 0; | |
| } | |
| static int h2_begin_headers_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) | |
| { | |
| GF_DownloadSession *sess = hmux_get_session(user_data, frame->hd.stream_id, GF_TRUE); | |
| if (!sess) { | |
| GF_HMUX_Session *h2sess = (GF_HMUX_Session *)user_data; | |
| GF_DownloadSession *par_sess = h2sess->net_sess; | |
| if (!par_sess || !par_sess->server_mode) | |
| return NGHTTP2_ERR_CALLBACK_FAILURE; | |
| if (par_sess->user_proc) { | |
| GF_NETIO_Parameter param; | |
| memset(¶m, 0, sizeof(GF_NETIO_Parameter)); | |
| param.msg_type = GF_NETIO_REQUEST_SESSION; | |
| par_sess->in_callback = GF_TRUE; | |
| param.sess = par_sess; | |
| param.stream_id = frame->hd.stream_id; | |
| par_sess->user_proc(par_sess->usr_cbk, ¶m); | |
| par_sess->in_callback = GF_FALSE; | |
| if (param.error == GF_OK) { | |
| sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| } | |
| } | |
| if (!sess) | |
| return NGHTTP2_ERR_CALLBACK_FAILURE; | |
| } | |
| switch (frame->hd.type) { | |
| case NGHTTP2_HEADERS: | |
| if (sess->server_mode) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id %d header callback\n", frame->hd.stream_id)); | |
| } else { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id %d (%s) header callback\n", frame->hd.stream_id, sess->remote_path)); | |
| } | |
| break; | |
| } | |
| return 0; | |
| } | |
| static int h2_frame_recv_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) | |
| { | |
| GF_DownloadSession *sess; | |
| switch (frame->hd.type) { | |
| case NGHTTP2_HEADERS: | |
| sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| if (!sess) | |
| return NGHTTP2_ERR_CALLBACK_FAILURE; | |
| if ( | |
| (!sess->server_mode && (frame->headers.cat == NGHTTP2_HCAT_RESPONSE)) | |
| || (sess->server_mode && ((frame->headers.cat == NGHTTP2_HCAT_HEADERS) || (frame->headers.cat == NGHTTP2_HCAT_REQUEST))) | |
| ) { | |
| sess->hmux_headers_seen = 1; | |
| if (sess->server_mode) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] All headers received for stream ID "LLD"\n", sess->hmux_stream_id)); | |
| } else { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] All headers received for stream ID "LLD"\n", sess->hmux_stream_id)); | |
| } | |
| } | |
| if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id %d (%s) data done\n", frame->hd.stream_id, sess->remote_path ? sess->remote_path : sess->orig_url)); | |
| sess->hmux_data_done = 1; | |
| sess->hmux_is_eos = 0; | |
| sess->hmux_send_data = NULL; | |
| sess->hmux_send_data_len = 0; | |
| } | |
| break; | |
| case NGHTTP2_DATA: | |
| if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { | |
| sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| //if no session with such ID this means we got all our bytes and considered the session done, do not throw an error | |
| if (sess) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id %d (%s) data done\n", frame->hd.stream_id, sess->remote_path ? sess->remote_path : sess->orig_url)); | |
| sess->hmux_data_done = 1; | |
| } | |
| } | |
| break; | |
| case NGHTTP2_RST_STREAM: | |
| sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| // cancel from remote peer, signal if not done | |
| if (sess && sess->server_mode && !sess->hmux_is_eos) { | |
| GF_NETIO_Parameter param; | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id %d (%s) canceled\n", frame->hd.stream_id, sess->remote_path ? sess->remote_path : sess->orig_url)); | |
| memset(¶m, 0, sizeof(GF_NETIO_Parameter)); | |
| param.msg_type = GF_NETIO_CANCEL_STREAM; | |
| gf_mx_p(sess->mx); | |
| sess->in_callback = GF_TRUE; | |
| param.sess = sess; | |
| sess->user_proc(sess->usr_cbk, ¶m); | |
| sess->in_callback = GF_FALSE; | |
| gf_mx_v(sess->mx); | |
| } | |
| break; | |
| } | |
| return 0; | |
| } | |
| static int h2_data_chunk_recv_callback(nghttp2_session *session, uint8_t flags, int32_t stream_id, const uint8_t *data, size_t len, void *user_data) | |
| { | |
| GF_DownloadSession *sess = hmux_get_session(user_data, stream_id, GF_FALSE); | |
| if (!sess) | |
| return NGHTTP2_ERR_CALLBACK_FAILURE; | |
| if (sess->hmux_buf.size + len > sess->hmux_buf.alloc) { | |
| sess->hmux_buf.alloc = sess->hmux_buf.size + (u32) len; | |
| sess->hmux_buf.data = gf_realloc(sess->hmux_buf.data, sizeof(u8) * sess->hmux_buf.alloc); | |
| if (!sess->hmux_buf.data) return NGHTTP2_ERR_NOMEM; | |
| } | |
| memcpy(sess->hmux_buf.data + sess->hmux_buf.size, data, len); | |
| sess->hmux_buf.size += (u32) len; | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id "LLD" received %d bytes (%d/%d total) - flags %d\n", sess->hmux_stream_id, len, sess->hmux_buf.size, sess->total_size, flags)); | |
| return 0; | |
| } | |
| static int h2_stream_close_callback(nghttp2_session *session, int32_t stream_id, uint32_t error_code, void *user_data) | |
| { | |
| Bool do_retry = GF_FALSE; | |
| GF_DownloadSession *sess = hmux_get_session(user_data, stream_id, GF_FALSE); | |
| if (!sess) | |
| return 0; | |
| gf_mx_p(sess->mx); | |
| if (error_code==NGHTTP2_REFUSED_STREAM) | |
| do_retry = GF_TRUE; | |
| else if (sess->hmux_sess->do_shutdown && !sess->server_mode && !sess->bytes_done) | |
| do_retry = GF_TRUE; | |
| if (do_retry) { | |
| sess->hmux_sess->do_shutdown = GF_TRUE; | |
| sess->hmux_switch_sess = GF_TRUE; | |
| sess->status = GF_NETIO_SETUP; | |
| SET_LAST_ERR(GF_OK) | |
| gf_mx_v(sess->mx); | |
| return 0; | |
| } | |
| if (error_code) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] stream_id %d (%s) closed with error_code=%d\n", stream_id, sess->remote_path ? sess->remote_path : sess->orig_url, error_code)); | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| SET_LAST_ERR(GF_IP_NETWORK_FAILURE) | |
| sess->put_state = 0; | |
| } else { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] stream_id %d (%s) closed (put state %d)\n", stream_id, sess->remote_path ? sess->remote_path : sess->orig_url, sess->put_state)); | |
| //except for PUT/POST, keep status in DATA_EXCHANGE as this frame might have been pushed while processing another session | |
| if (sess->put_state) { | |
| sess->put_state = 0; | |
| sess->status = GF_NETIO_DATA_TRANSFERED; | |
| sess->last_error = GF_OK; | |
| } | |
| } | |
| //stream closed | |
| sess->hmux_stream_id = -1; | |
| gf_mx_v(sess->mx); | |
| return 0; | |
| } | |
| static int h2_error_callback(nghttp2_session *session, const char *msg, size_t len, void *user_data) | |
| { | |
| if (session) | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] error %s\n", msg)); | |
| return 0; | |
| } | |
| static ssize_t h2_write_data(GF_DownloadSession *sess, const uint8_t *data, size_t length) | |
| { | |
| GF_Err e = dm_sess_write(sess, data, (u32) length); | |
| switch (e) { | |
| case GF_OK: | |
| return length; | |
| case GF_IP_NETWORK_EMPTY: | |
| if (sess->flags & GF_NETIO_SESSION_NO_BLOCK) | |
| return length; | |
| return NGHTTP2_ERR_WOULDBLOCK; | |
| case GF_IP_CONNECTION_CLOSED: | |
| SET_LAST_ERR(GF_IP_CONNECTION_CLOSED) | |
| return NGHTTP2_ERR_EOF; | |
| default: | |
| break; | |
| } | |
| return NGHTTP2_ERR_CALLBACK_FAILURE; | |
| } | |
| static ssize_t h2_send_callback(nghttp2_session *session, const uint8_t *data, size_t length, int flags, void *user_data) | |
| { | |
| GF_HMUX_Session *h2sess = (GF_HMUX_Session *)user_data; | |
| GF_DownloadSession *sess = h2sess->net_sess; | |
| return h2_write_data(sess, data, length); | |
| } | |
| static int h2_before_frame_send_callback(nghttp2_session *session, const nghttp2_frame *frame, void *user_data) | |
| { | |
| GF_DownloadSession *sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| if (!sess) | |
| return 0; | |
| sess->hmux_ready_to_send = 1; | |
| return 0; | |
| } | |
| ssize_t h2_data_source_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) | |
| { | |
| GF_DownloadSession *sess = (GF_DownloadSession *) source->ptr; | |
| if (!sess) | |
| return NGHTTP2_ERR_EOF; | |
| if (!sess->hmux_send_data_len) { | |
| sess->hmux_send_data = NULL; | |
| if (!sess->local_buf_len && sess->hmux_is_eos) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] send EOS for stream_id "LLD"\n", sess->hmux_stream_id)); | |
| *data_flags = NGHTTP2_DATA_FLAG_EOF; | |
| sess->hmux_is_eos = 0; | |
| return 0; | |
| } | |
| sess->hmux_data_paused = 1; | |
| return NGHTTP2_ERR_DEFERRED; | |
| } | |
| if (sess->hmux_sess->copy) { | |
| u32 copy = (sess->hmux_send_data_len > length) ? (u32) length : sess->hmux_send_data_len; | |
| memcpy(buf, sess->hmux_send_data, copy); | |
| sess->hmux_send_data += copy; | |
| sess->hmux_send_data_len -= copy; | |
| return copy; | |
| } | |
| *data_flags = NGHTTP2_DATA_FLAG_NO_COPY; | |
| if (sess->hmux_send_data_len > length) { | |
| return length; | |
| } | |
| return sess->hmux_send_data_len; | |
| } | |
| static int h2_send_data_callback(nghttp2_session *session, nghttp2_frame *frame, const uint8_t *framehd, size_t length, nghttp2_data_source *source, void *user_data) | |
| { | |
| char padding[256]; | |
| ssize_t rv; | |
| //ultimately we would like to use directly source->ptr but the session could have been destroyed and there is no way to tell nghttp2 to stop | |
| //calling us: we may get called before rst_stream is processed | |
| //we therefore need to validate the session is indeed still alive, but that's slower... | |
| #if 0 | |
| GF_DownloadSession *sess = (GF_DownloadSession *) source->ptr; | |
| if (!sess) | |
| return NGHTTP2_ERR_EOF; | |
| #else | |
| GF_DownloadSession *sess = hmux_get_session(user_data, frame->hd.stream_id, GF_FALSE); | |
| if (!sess) | |
| return NGHTTP2_ERR_EOF; | |
| #endif | |
| gf_assert(sess->hmux_send_data_len); | |
| gf_assert(sess->hmux_send_data_len >= length); | |
| rv = h2_write_data(sess, (u8 *) framehd, 9); | |
| if (rv<0) { | |
| if (rv==NGHTTP2_ERR_WOULDBLOCK) return NGHTTP2_ERR_WOULDBLOCK; | |
| goto err; | |
| } | |
| while (frame->data.padlen > 0) { | |
| u32 padlen = (u32) frame->data.padlen - 1; | |
| rv = h2_write_data(sess, padding, padlen); | |
| if (rv<0) { | |
| if (rv==NGHTTP2_ERR_WOULDBLOCK) continue; | |
| goto err; | |
| } | |
| break; | |
| } | |
| while (1) { | |
| rv = h2_write_data(sess, (u8 *) sess->hmux_send_data, length); | |
| if (rv<0) { | |
| if (rv==NGHTTP2_ERR_WOULDBLOCK) continue; | |
| goto err; | |
| } | |
| break; | |
| } | |
| sess->hmux_send_data += (u32) length; | |
| sess->hmux_send_data_len -= (u32) length; | |
| return 0; | |
| err: | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| SET_LAST_ERR(sess->last_error) | |
| return (int) rv; | |
| } | |
| static GF_Err h2_session_write(GF_DownloadSession *sess) | |
| { | |
| gf_assert(sess->hmux_sess); | |
| int rv = nghttp2_session_send(sess->hmux_sess->hmux_udta); | |
| if (rv != 0) { | |
| if (sess->last_error != GF_IP_CONNECTION_CLOSED) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] session_send error : %s\n", nghttp2_strerror(rv))); | |
| } | |
| if (sess->status != GF_NETIO_STATE_ERROR) { | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| if (rv==NGHTTP2_ERR_NOMEM) SET_LAST_ERR(GF_OUT_OF_MEM) | |
| else SET_LAST_ERR(GF_SERVICE_ERROR) | |
| } | |
| return sess->last_error; | |
| } | |
| // GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] session_send OK\n")); | |
| return GF_OK; | |
| } | |
| static void h2_stream_reset(GF_DownloadSession *sess, Bool is_abort) | |
| { | |
| nghttp2_submit_rst_stream(sess->hmux_sess->hmux_udta, NGHTTP2_FLAG_NONE, (int32_t) sess->hmux_stream_id, is_abort ? NGHTTP2_CANCEL : NGHTTP2_NO_ERROR); | |
| } | |
| static GF_Err h2_resume_stream(GF_DownloadSession *sess) | |
| { | |
| sess->hmux_data_paused = 0; | |
| nghttp2_session_resume_data(sess->hmux_sess->hmux_udta, (int32_t) sess->hmux_stream_id); | |
| return GF_OK; | |
| } | |
| static void h2_destroy(struct _http_mux_session *hmux_sess) | |
| { | |
| nghttp2_session_del(hmux_sess->hmux_udta); | |
| } | |
| static GF_Err h2_data_received(GF_DownloadSession *sess, const u8 *data, u32 nb_bytes) | |
| { | |
| if (nb_bytes) { | |
| ssize_t read_len = nghttp2_session_mem_recv(sess->hmux_sess->hmux_udta, data, nb_bytes); | |
| if (read_len < 0 ) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] nghttp2_session_mem_recv error: %s\n", nghttp2_strerror((int) read_len))); | |
| return GF_IO_ERR; | |
| } | |
| } | |
| /* send pending frames - hmux_sess may be NULL at this point if the connection was reset during processing of the above | |
| this typically happens if we have a refused stream | |
| */ | |
| h2_session_write(sess); | |
| return GF_OK; | |
| } | |
| #define NV_HDR(_hdr, _name, _value) { \ | |
| _hdr.name = (uint8_t *)_name;\ | |
| _hdr.value = (uint8_t *)_value;\ | |
| _hdr.namelen = (u32) strlen(_name);\ | |
| _hdr.valuelen = (u32) strlen(_value);\ | |
| _hdr.flags = NGHTTP2_NV_FLAG_NONE;\ | |
| } | |
| GF_Err h2_submit_request(GF_DownloadSession *sess, char *req_name, const char *url, const char *param_string, Bool has_body) | |
| { | |
| u32 nb_hdrs, i; | |
| char *hostport = NULL; | |
| char *path = NULL; | |
| char port[20]; | |
| nghttp2_nv *hdrs; | |
| nb_hdrs = gf_list_count(sess->headers); | |
| hdrs = gf_malloc(sizeof(nghttp2_nv) * (nb_hdrs + 4)); | |
| NV_HDR(hdrs[0], ":method", req_name); | |
| NV_HDR(hdrs[1], ":scheme", "https"); | |
| gf_dynstrcat(&hostport, sess->server_name, NULL); | |
| sprintf(port, ":%d", sess->port); | |
| gf_dynstrcat(&hostport, port, NULL); | |
| NV_HDR(hdrs[2], ":authority", hostport); | |
| if (param_string) { | |
| gf_dynstrcat(&path, url, NULL); | |
| if (strchr(sess->remote_path, '?')) { | |
| gf_dynstrcat(&path, param_string, "&"); | |
| } else { | |
| gf_dynstrcat(&path, param_string, "?"); | |
| } | |
| NV_HDR(hdrs[3], ":path", path); | |
| } else { | |
| NV_HDR(hdrs[3], ":path", url); | |
| } | |
| for (i=0; i<nb_hdrs; i++) { | |
| GF_HTTPHeader *hdr = gf_list_get(sess->headers, i); | |
| NV_HDR(hdrs[4+i], hdr->name, hdr->value); | |
| } | |
| #ifndef NDEBUG | |
| if (has_body) { | |
| nghttp2_data_provider *data_io = (nghttp2_data_provider*)sess->hmux_priv; | |
| gf_assert(data_io->read_callback); | |
| gf_assert(data_io->source.ptr != NULL); | |
| } | |
| #endif | |
| sess->hmux_data_done = 0; | |
| sess->hmux_headers_seen = 0; | |
| sess->hmux_stream_id = nghttp2_submit_request(sess->hmux_sess->hmux_udta, NULL, hdrs, nb_hdrs+4, | |
| has_body ? (nghttp2_data_provider *)sess->hmux_priv : NULL, sess); | |
| sess->hmux_ready_to_send = 0; | |
| #ifndef GPAC_DISABLE_LOG | |
| if (gf_log_tool_level_on(GF_LOG_HTTP, GF_LOG_DEBUG)) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[HTTP/2] send request (has_body %d) for new stream_id "LLD":\n", has_body, sess->hmux_stream_id)); | |
| for (i=0; i<nb_hdrs+4; i++) { | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("\t%s: %s\n", hdrs[i].name, hdrs[i].value)); | |
| } | |
| } | |
| #endif | |
| gf_free(hdrs); | |
| gf_free(hostport); | |
| if (path) gf_free(path); | |
| if (sess->hmux_stream_id < 0) { | |
| return GF_IP_NETWORK_FAILURE; | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err h2_send_reply(GF_DownloadSession *sess, u32 reply_code, const char *response_body, u32 body_len, Bool no_body) | |
| { | |
| u32 i, count; | |
| char szFmt[50]; | |
| if (!sess || !sess->server_mode) return GF_BAD_PARAM; | |
| count = gf_list_count(sess->headers); | |
| nghttp2_nv *hdrs; | |
| if (response_body) { | |
| no_body = GF_FALSE; | |
| sess->hmux_send_data = (u8 *) response_body; | |
| sess->hmux_send_data_len = body_len; | |
| sess->hmux_is_eos = 1; | |
| } else if (!no_body) { | |
| switch (reply_code) { | |
| case 200: | |
| case 206: | |
| no_body = GF_FALSE; | |
| sess->hmux_is_eos = 0; | |
| break; | |
| default: | |
| no_body = GF_TRUE; | |
| break; | |
| } | |
| } | |
| hdrs = gf_malloc(sizeof(nghttp2_nv) * (count + 1) ); | |
| sprintf(szFmt, "%d", reply_code); | |
| NV_HDR(hdrs[0], ":status", szFmt); | |
| GF_LOG(GF_LOG_INFO, GF_LOG_HTTP, ("[HTTP/2] send reply for stream_id "LLD" (body %d) headers:\n:status: %s\n", sess->hmux_stream_id, !no_body, szFmt)); | |
| for (i=0; i<count; i++) { | |
| GF_HTTPHeader *hdr = gf_list_get(sess->headers, i); | |
| NV_HDR(hdrs[i+1], hdr->name, hdr->value) | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("%s: %s\n", hdr->name, hdr->value)); | |
| } | |
| gf_mx_p(sess->mx); | |
| int rv = nghttp2_submit_response(sess->hmux_sess->hmux_udta, (int32_t) sess->hmux_stream_id, hdrs, count+1, no_body ? NULL : sess->hmux_priv); | |
| gf_free(hdrs); | |
| if (rv != 0) { | |
| gf_mx_v(sess->mx); | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] Failed to submit reply: %s\n", nghttp2_strerror(rv))); | |
| return GF_SERVICE_ERROR; | |
| } | |
| h2_session_write(sess); | |
| //in case we have a body already setup with this reply | |
| h2_flush_send_ex(sess, GF_FALSE); | |
| gf_mx_v(sess->mx); | |
| return GF_OK; | |
| } | |
| static GF_Err h2_async_flush(GF_DownloadSession *sess, Bool for_close) | |
| { | |
| if (!sess->local_buf_len && !sess->hmux_is_eos) return GF_OK; | |
| gf_assert(sess->hmux_send_data==NULL); | |
| //HTTP2: resume data sending, set data to local copy send packets and check what was sent | |
| GF_Err ret = GF_OK; | |
| u8 was_eos = sess->hmux_is_eos; | |
| gf_assert(sess->hmux_send_data==NULL); | |
| gf_mx_p(sess->mx); | |
| sess->hmux_send_data = sess->local_buf; | |
| sess->hmux_send_data_len = sess->local_buf_len; | |
| if (sess->hmux_data_paused) { | |
| sess->hmux_data_paused = 0; | |
| sess->hmux_sess->resume(sess); | |
| } | |
| h2_flush_send_ex(sess, GF_TRUE); | |
| gf_mx_v(sess->mx); | |
| if (sess->hmux_send_data_len) { | |
| //we will return empty to avoid pushing data too fast, but we also need to flush the async buffer | |
| ret = GF_IP_NETWORK_EMPTY; | |
| if (sess->hmux_send_data_len<sess->local_buf_len) { | |
| memmove(sess->local_buf, sess->hmux_send_data, sess->hmux_send_data_len); | |
| sess->local_buf_len = sess->hmux_send_data_len; | |
| } else { | |
| //nothing sent, check socket | |
| ret = gf_sk_probe(sess->sock); | |
| } | |
| sess->hmux_send_data = NULL; | |
| sess->hmux_send_data_len = 0; | |
| sess->hmux_is_eos = was_eos; | |
| } else { | |
| sess->local_buf_len = 0; | |
| sess->hmux_send_data = NULL; | |
| sess->hmux_send_data_len = 0; | |
| if (was_eos && !sess->hmux_is_eos) { | |
| if (sess->put_state==1) { | |
| sess->put_state = 2; | |
| sess->status = GF_NETIO_WAIT_FOR_REPLY; | |
| } | |
| } | |
| } | |
| return ret; | |
| } | |
| static GF_Err h2_setup_session(GF_DownloadSession *sess, Bool is_destroy) | |
| { | |
| if (is_destroy) { | |
| if (sess->hmux_priv) { | |
| gf_free(sess->hmux_priv); | |
| sess->hmux_priv = NULL; | |
| } | |
| return GF_OK; | |
| } | |
| if (!sess->hmux_priv) { | |
| GF_SAFEALLOC(sess->hmux_priv, nghttp2_data_provider); | |
| if (!sess->hmux_priv) return GF_OUT_OF_MEM; | |
| } | |
| nghttp2_data_provider *data_io = sess->hmux_priv; | |
| data_io->read_callback = h2_data_source_read_callback; | |
| data_io->source.ptr = sess; | |
| return GF_OK; | |
| } | |
| static void h2_flush_send_ex(GF_DownloadSession *sess, Bool flush_local_buf) | |
| { | |
| char h2_flush[1024]; | |
| u32 res; | |
| while (sess->hmux_send_data) { | |
| GF_Err e; | |
| u32 nb_bytes = sess->hmux_send_data_len; | |
| if (!sess->local_buf_len || flush_local_buf) { | |
| //read any frame pending from remote peer (window update and co) | |
| e = gf_dm_read_data(sess, h2_flush, 1023, &res); | |
| if ((e<0) && (e != GF_IP_NETWORK_EMPTY)) { | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| SET_LAST_ERR(sess->last_error) | |
| break; | |
| } | |
| h2_session_write(sess); | |
| //error or regular eos | |
| if (sess->hmux_stream_id<0) | |
| break; | |
| if (sess->status==GF_NETIO_STATE_ERROR) | |
| break; | |
| } | |
| if (nb_bytes > sess->hmux_send_data_len) continue; | |
| if (!(sess->flags & GF_NETIO_SESSION_NO_BLOCK)) continue; | |
| if (flush_local_buf) return; | |
| //stream will now block, no choice but do a local copy... | |
| if (sess->local_buf_len + nb_bytes > sess->local_buf_alloc) { | |
| sess->local_buf_alloc = sess->local_buf_len + nb_bytes; | |
| sess->local_buf = gf_realloc(sess->local_buf, sess->local_buf_alloc); | |
| if (!sess->local_buf) { | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| SET_LAST_ERR(sess->last_error) | |
| break; | |
| } | |
| } | |
| if (nb_bytes) { | |
| memcpy(sess->local_buf + sess->local_buf_len, sess->hmux_send_data, nb_bytes); | |
| sess->local_buf_len+=nb_bytes; | |
| } | |
| sess->hmux_send_data = NULL; | |
| sess->hmux_send_data_len = 0; | |
| } | |
| } | |
| static GF_Err h2_data_pending(GF_DownloadSession *sess) | |
| { | |
| GF_Err e = h2_session_write(sess); | |
| if (e) return e; | |
| h2_flush_send_ex(sess, GF_FALSE); | |
| return GF_OK; | |
| } | |
| static void h2_close_session(GF_DownloadSession *sess) | |
| { | |
| nghttp2_submit_shutdown_notice(sess->hmux_sess->hmux_udta); | |
| h2_session_write(sess); | |
| #if 1 | |
| u64 in_time; | |
| in_time = gf_sys_clock_high_res(); | |
| while (nghttp2_session_want_read(sess->hmux_sess->hmux_udta)) { | |
| u32 res; | |
| char h2_flush[2024]; | |
| GF_Err e; | |
| if (gf_sys_clock_high_res() - in_time > 100000) | |
| break; | |
| //read any frame pending from remote peer (window update and co) | |
| e = gf_dm_read_data(sess, h2_flush, 2023, &res); | |
| if ((e<0) && (e != GF_IP_NETWORK_EMPTY)) { | |
| if (e!=GF_IP_CONNECTION_CLOSED) { | |
| SET_LAST_ERR(e) | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| } | |
| break; | |
| } | |
| h2_session_write(sess); | |
| } | |
| #endif | |
| sess->status = GF_NETIO_DISCONNECTED; | |
| } | |
| void h2_initialize_session(GF_DownloadSession *sess) | |
| { | |
| int rv; | |
| int nb_settings=0; | |
| nghttp2_settings_entry init_settings[10]; | |
| char szMXName[100]; | |
| nghttp2_session_callbacks *callbacks; | |
| nghttp2_session_callbacks_new(&callbacks); | |
| nghttp2_session_callbacks_set_send_callback(callbacks, h2_send_callback); | |
| nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks, h2_frame_recv_callback); | |
| nghttp2_session_callbacks_set_before_frame_send_callback(callbacks, h2_before_frame_send_callback); | |
| nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks, h2_data_chunk_recv_callback); | |
| nghttp2_session_callbacks_set_on_stream_close_callback(callbacks, h2_stream_close_callback); | |
| nghttp2_session_callbacks_set_on_header_callback(callbacks, h2_header_callback); | |
| nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks, h2_begin_headers_callback); | |
| nghttp2_session_callbacks_set_error_callback(callbacks, h2_error_callback); | |
| GF_SAFEALLOC(sess->hmux_sess, GF_HMUX_Session) | |
| sess->hmux_sess->sessions = gf_list_new(); | |
| sess->hmux_sess->copy = gf_opts_get_bool("core", "h2-copy"); | |
| if (!sess->hmux_sess->copy) | |
| nghttp2_session_callbacks_set_send_data_callback(callbacks, h2_send_data_callback); | |
| if (sess->server_mode) { | |
| nghttp2_session_server_new((nghttp2_session**) &sess->hmux_sess->hmux_udta, callbacks, sess->hmux_sess); | |
| } else { | |
| nghttp2_session_client_new((nghttp2_session**) &sess->hmux_sess->hmux_udta, callbacks, sess->hmux_sess); | |
| } | |
| #define H2SET_ASSIGN(_key, _val) \ | |
| init_settings[nb_settings].settings_id = _key; \ | |
| init_settings[nb_settings].value = _val; \ | |
| nb_settings++;\ | |
| gf_assert(nb_settings<10); | |
| H2SET_ASSIGN(NGHTTP2_SETTINGS_ENABLE_PUSH, 0); | |
| u32 h2_opt_val = gf_opts_get_int("core", "hx-max-st"); | |
| if (!h2_opt_val) h2_opt_val = 100; | |
| H2SET_ASSIGN(NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, h2_opt_val); | |
| h2_opt_val = gf_opts_get_int("core", "hx-st-iwnd"); | |
| if (h2_opt_val) { | |
| H2SET_ASSIGN(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, h2_opt_val); | |
| } | |
| h2_opt_val = gf_opts_get_int("core", "h2-max-frame"); | |
| if (h2_opt_val) { | |
| H2SET_ASSIGN(NGHTTP2_SETTINGS_MAX_FRAME_SIZE, h2_opt_val); | |
| } | |
| h2_opt_val = gf_opts_get_int("core", "hx-iwnd"); | |
| if (h2_opt_val) { | |
| rv = nghttp2_session_set_local_window_size(sess->hmux_sess->hmux_udta, NGHTTP2_FLAG_NONE, 0 /* connection */, h2_opt_val); | |
| if (rv) { | |
| GF_LOG(GF_LOG_WARNING, GF_LOG_HTTP, ("[HTTP/2] set_local_window_size(conn) failed: %d\n", rv)); | |
| } | |
| } | |
| nghttp2_session_callbacks_del(callbacks); | |
| sess->hmux_sess->net_sess = sess; | |
| //setup function pointers | |
| sess->hmux_sess->submit_request = h2_submit_request; | |
| sess->hmux_sess->send_reply = h2_send_reply; | |
| sess->hmux_sess->write = h2_session_write; | |
| sess->hmux_sess->destroy = h2_destroy; | |
| sess->hmux_sess->stream_reset = h2_stream_reset; | |
| sess->hmux_sess->resume = h2_resume_stream; | |
| sess->hmux_sess->data_received = h2_data_received; | |
| sess->hmux_sess->setup_session = h2_setup_session; | |
| sess->hmux_sess->send_pending_data = h2_data_pending; | |
| sess->hmux_sess->async_flush = h2_async_flush; | |
| sess->hmux_sess->close_session = h2_close_session; | |
| sess->hmux_sess->connected = GF_TRUE; | |
| gf_list_add(sess->hmux_sess->sessions, sess); | |
| if (!sess->mx) { | |
| sprintf(szMXName, "http2_%p", sess->hmux_sess); | |
| sess->hmux_sess->mx = gf_mx_new(szMXName); | |
| sess->mx = sess->hmux_sess->mx; | |
| } else { | |
| sess->hmux_sess->mx = sess->mx; | |
| } | |
| sess->chunked = GF_FALSE; | |
| h2_setup_session(sess, GF_FALSE); | |
| if (sess->server_mode) { | |
| sess->hmux_stream_id = 1; | |
| if (sess->h2_upgrade_settings) { | |
| rv = nghttp2_session_upgrade2(sess->hmux_sess->hmux_udta, sess->h2_upgrade_settings, sess->h2_upgrade_settings_len, 0, sess); | |
| gf_free(sess->h2_upgrade_settings); | |
| sess->h2_upgrade_settings = NULL; | |
| if (rv) { | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| SET_LAST_ERR( (rv==NGHTTP2_ERR_NOMEM) ? GF_OUT_OF_MEM : GF_REMOTE_SERVICE_ERROR) | |
| return; | |
| } | |
| } | |
| } | |
| /* client 24 bytes magic string will be sent by nghttp2 library */ | |
| rv = nghttp2_submit_settings(sess->hmux_sess->hmux_udta, NGHTTP2_FLAG_NONE, init_settings, nb_settings); | |
| if (rv != 0) { | |
| sess->status = GF_NETIO_STATE_ERROR; | |
| SET_LAST_ERR((rv==NGHTTP2_ERR_NOMEM) ? GF_OUT_OF_MEM : GF_SERVICE_ERROR) | |
| return; | |
| } | |
| h2_session_write(sess); | |
| } | |
| void http2_set_upgrade_headers(GF_DownloadSession *sess) | |
| { | |
| GF_HTTPHeader *hdr; | |
| u8 settings[HTTP2_BUFFER_SETTINGS_SIZE]; | |
| u32 settings_len; | |
| u8 b64[100]; | |
| u32 b64len; | |
| nghttp2_settings_entry h2_settings[2] = { | |
| {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}, | |
| {NGHTTP2_SETTINGS_ENABLE_PUSH, 0} | |
| }; | |
| PUSH_HDR("Connection", "Upgrade, HTTP2-Settings") | |
| PUSH_HDR("Upgrade", "h2c") | |
| settings_len = (u32) nghttp2_pack_settings_payload(settings, HTTP2_BUFFER_SETTINGS_SIZE, h2_settings, GF_ARRAY_LENGTH(h2_settings)); | |
| b64len = gf_base64_encode(settings, settings_len, b64, 100); | |
| b64[b64len] = 0; | |
| PUSH_HDR("HTTP2-Settings", b64) | |
| sess->h2_upgrade_state = 1; | |
| } | |
| GF_Err http2_do_upgrade(GF_DownloadSession *sess, const char *body, u32 body_len) | |
| { | |
| int rv; | |
| u8 settings[HTTP2_BUFFER_SETTINGS_SIZE]; | |
| u32 settings_len; | |
| nghttp2_settings_entry h2_settings[2] = { | |
| {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}, | |
| {NGHTTP2_SETTINGS_ENABLE_PUSH, 0} | |
| }; | |
| settings_len = (u32) nghttp2_pack_settings_payload(settings, HTTP2_BUFFER_SETTINGS_SIZE, h2_settings, GF_ARRAY_LENGTH(h2_settings)); | |
| h2_initialize_session(sess); | |
| sess->hmux_stream_id = 1; | |
| rv = nghttp2_session_upgrade2(sess->hmux_sess->hmux_udta, settings, settings_len, (sess->http_read_type==1) ? 1 : 0, sess); | |
| if (rv < 0) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] nghttp2_session_upgrade2 error: %s\n", nghttp2_strerror(rv))); | |
| return GF_IP_NETWORK_FAILURE; | |
| } | |
| //push the body | |
| if (body_len) { | |
| rv = (int) nghttp2_session_mem_recv(sess->hmux_sess->hmux_udta, body, body_len); | |
| if (rv < 0) { | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[HTTP/2] nghttp2_session_mem_recv error: %s\n", nghttp2_strerror(rv))); | |
| return GF_IP_NETWORK_FAILURE; | |
| } | |
| //stay in WAIT_FOR_REPLY state and do not flush data, cache is not fully configured yet | |
| } | |
| //send pending frames | |
| GF_Err e = h2_session_write(sess); | |
| if (e) return e; | |
| sess->connection_close = GF_FALSE; | |
| sess->h2_upgrade_state = 2; | |
| GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[%s] Upgraded connection to HTTP/2\n", sess->log_name)); | |
| return GF_OK; | |
| } | |
| GF_Err http2_check_upgrade(GF_DownloadSession *sess) | |
| { | |
| GF_Err e = GF_OK; | |
| char *rsp_buf = NULL; | |
| if (!sess || !sess->server_mode) return GF_BAD_PARAM; | |
| if (!sess->h2_upgrade_settings) return GF_OK; | |
| u32 len; | |
| gf_assert(!sess->hmux_sess); | |
| gf_dynstrcat(&rsp_buf, "HTTP/1.1 101 Switching Protocols\r\n" | |
| "Connection: Upgrade\r\n" | |
| "Upgrade: h2c\r\n\r\n", NULL); | |
| len = (u32) strlen(rsp_buf); | |
| e = dm_sess_write(sess, rsp_buf, len); | |
| gf_free(rsp_buf); | |
| h2_initialize_session(sess); | |
| return e; | |
| } | |
| #endif //!defined(GPAC_DISABLE_NETWORK) && defined(GPAC_HAS_HTTP2) | |