blob: 5def9d4b48d40155415f5c5c22d7d2de329643fb [file] [edit]
/*
* GPAC Multimedia Framework
*
* Authors: Jean Le Feuvre
* Copyright (c) Telecom ParisTech 2005-2025
* 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_CURL)
#include <curl/curl.h>
//we need multi API
#if CURL_AT_LEAST_VERSION(7,80,0)
#ifndef CURLPIPE_MULTIPLEX
#define CURLPIPE_MULTIPLEX 0
#endif
#if !defined(__GNUC__)
# if defined(_WIN32_WCE) || defined (WIN32)
#pragma comment(lib, "libcurl")
# endif
#endif
#ifndef GPAC_DISABLE_LOG
static int curl_trace(CURL *handle, curl_infotype type, char *data, size_t size, void *clientp)
{
(void)clientp;
(void)handle;
switch(type) {
case CURLINFO_TEXT:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Info: %s", data));
return 0;
case CURLINFO_HEADER_OUT:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Send header %s\n", data));
return 0;
case CURLINFO_DATA_OUT:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Send data\n"));
return 0;
case CURLINFO_SSL_DATA_OUT:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Send SSL data\n"));
return 0;
case CURLINFO_HEADER_IN:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Recv header %s\n", data));
return 0;
case CURLINFO_DATA_IN:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Recv data %d byes\n", size));
return 0;
case CURLINFO_SSL_DATA_IN:
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] Recv SSL data\n"));
return 0;
default:
return 0;
}
return 0;
}
#endif
static int curl_on_connect(void *clientp, char *conn_primary_ip, char *conn_local_ip, int conn_primary_port, int conn_local_port)
{
GF_DownloadSession *sess = clientp;
if (sess->status == GF_NETIO_SETUP) {
sess->status = GF_NETIO_CONNECTED;
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] connected\n"));
gf_dm_sess_notify_state(sess, GF_NETIO_CONNECTED, GF_OK);
gf_dm_configure_cache(sess);
gf_mx_p(sess->dm->curl_mx);
curl_multi_remove_handle(sess->dm->curl_multi, sess->curl_hnd);
sess->curl_hnd_registered = GF_FALSE;
gf_mx_v(sess->dm->curl_mx);
}
return CURL_PREREQFUNC_OK;
}
static int curl_on_socket_close(void *clientp, curl_socket_t item)
{
GF_DownloadManager *dm = clientp;
dm->curl_has_close = GF_TRUE;
return 0;
}
static size_t curl_on_header(char *buffer, size_t size, size_t nitems, void *clientp)
{
GF_DownloadSession *sess = clientp;
if ((buffer[0]=='\r') || (buffer[0]=='\n'))
return nitems * size;
if (sess->status==GF_NETIO_WAIT_FOR_REPLY) {
//push headers
char *sep = memchr(buffer, ':', nitems*size);
if (sep) {
sep[0] = 0;
GF_HTTPHeader *hdrp;
GF_SAFEALLOC(hdrp, GF_HTTPHeader);
if (hdrp) {
u32 len = (u32) strlen(buffer) + 2;
hdrp->name = gf_strdup(buffer);
hdrp->value = gf_malloc(nitems * size - len + 1);
memcpy(hdrp->value, buffer+len, nitems * size - len);
hdrp->value[nitems * size - len] = 0;
gf_list_add(sess->headers, hdrp);
}
}
if (!sess->reply_time)
sess->reply_time = (u32) (gf_sys_clock_high_res() - sess->request_start_time);
}
return nitems * size;
}
static size_t curl_on_data(char *ptr, size_t size, size_t nmemb, void *clientp)
{
GF_DownloadSession *sess = clientp;
u32 len = (u32) (size*nmemb);
if (sess->local_buf_len + len > sess->local_buf_alloc) {
sess->local_buf_alloc = sess->local_buf_len + len;
sess->local_buf = gf_realloc(sess->local_buf, sizeof(u8) * sess->local_buf_alloc);
if (!sess->local_buf) return 0;
}
memcpy(sess->local_buf + sess->local_buf_len, ptr, len);
sess->local_buf_len += (u32) len;
sess->last_fetch_time = gf_sys_clock_high_res();
//dirty hack: libcurl may use blocking calls for some protocols (ftp)
//if we know the total size and have all bytes, force a timeout and tach it in curl_flush
if (sess->total_size && (sess->bytes_done + sess->local_buf_len == sess->total_size)) {
curl_easy_setopt(sess->curl_hnd, CURLOPT_TIMEOUT_MS, 1);
}
//GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] sess %s received %d bytes (%d/%d total)\n", sess->orig_url, len, sess->bytes_done, sess->total_size));
return len;
}
#endif //GPAC_DISABLE_LOG
GF_Err curl_setup_session(GF_DownloadSession* sess)
{
CURLcode res;
sess->curl_hnd = curl_easy_init();
res = curl_easy_setopt(sess->curl_hnd, CURLOPT_PRIVATE, sess);
res = curl_easy_setopt(sess->curl_hnd, CURLOPT_URL, sess->orig_url);
#ifndef GPAC_DISABLE_LOG
if (gf_opts_get_bool("curl", "trace") && gf_log_tool_level_on(GF_LOG_HTTP, GF_LOG_DEBUG)) {
curl_easy_setopt(sess->curl_hnd, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(sess->curl_hnd, CURLOPT_DEBUGFUNCTION, curl_trace);
curl_easy_setopt(sess->curl_hnd, CURLOPT_DEBUGDATA, sess);
}
#endif
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_BUFFERSIZE, sess->dm->read_buf_size);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_FOLLOWLOCATION, 0L);
#if (CURLPIPE_MULTIPLEX > 0)
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_PIPEWAIT, 1L);
#endif
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_PREREQFUNCTION, curl_on_connect);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_PREREQDATA, sess);
//set close callback at DM level since a same socket can be used by several sessions (H2/H3)
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_CLOSESOCKETFUNCTION, curl_on_socket_close);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_CLOSESOCKETDATA, sess->dm);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_HEADERFUNCTION, curl_on_header);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_HEADERDATA, sess);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_WRITEFUNCTION, curl_on_data);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_WRITEDATA, sess);
//DO NOT SET CURLOPT_TIMEOUT as we have no clue how long the session will be running
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_CONNECTTIMEOUT_MS, sess->conn_timeout);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_ACCEPTTIMEOUT_MS, sess->conn_timeout);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_NOSIGNAL, 1);
if (gf_opts_get_bool("core", "broken-cert")) {
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_SSL_VERIFYPEER, 0);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_SSL_VERIFYHOST, 0);
}
else {
const char* ca_bundle = gf_opts_get_key("core", "ca-bundle");
if (ca_bundle && gf_file_exists(ca_bundle)) {
curl_easy_setopt(sess->curl_hnd, CURLOPT_CAINFO, ca_bundle);
curl_easy_setopt(sess->curl_hnd, CURLOPT_CAPATH, NULL);
GF_LOG(GF_LOG_DEBUG, GF_LOG_CORE, ("[CURL] Using CA bundle at %s\n", ca_bundle));
}
#if CURL_AT_LEAST_VERSION(7,84,0)
else {
const char* curl_bundle = NULL;
curl_easy_getinfo(sess->curl_hnd, CURLINFO_CAINFO, &curl_bundle);
GF_LOG(GF_LOG_DEBUG, GF_LOG_CORE, ("[CURL] libcurl CA bundle location: %s\n", curl_bundle));
if (!curl_bundle || !gf_file_exists(curl_bundle)) {
const char* ca_bundle_default = gf_opts_get_key("core", "ca-bundle-default");
if (ca_bundle_default) {
curl_easy_setopt(sess->curl_hnd, CURLOPT_CAINFO, ca_bundle_default);
GF_LOG(GF_LOG_DEBUG, GF_LOG_CORE, ("[CURL] using default CA bundle at %s\n", ca_bundle_default));
}
}
}
#endif
}
//set HTTP version
if (!res) {
curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW);
res = CURLE_UNSUPPORTED_PROTOCOL;
//try H3
if ((ver->features & CURL_VERSION_HTTP3) && sess->dm && (sess->dm->h3_mode!=H3_MODE_NO)) {
#ifdef CURL_HTTP_VERSION_3ONLY
if (sess->dm->h3_mode==H3_MODE_ONLY)
res = curl_easy_setopt(sess->curl_hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_3ONLY);
else
#endif
res = curl_easy_setopt(sess->curl_hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_3);
}
//try H2
if (res && (ver->features & CURL_VERSION_HTTP2) && !gf_opts_get_bool("core", "no-h2")) {
res = curl_easy_setopt(sess->curl_hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2_0);
}
//fallback 1.1 - we could use CURL_HTTP_VERSION_NONE
if (res==CURLE_UNSUPPORTED_PROTOCOL)
res = curl_easy_setopt(sess->curl_hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_1_1);
}
const char *proxy = (sess->flags & GF_NETIO_SESSION_NO_PROXY) ? NULL : gf_opts_get_key("core", "proxy");
if (proxy) {
sess->proxy_enabled = 1;
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_PROXY, proxy);
}
sess->curl_not_http = GF_FALSE;
if (!sess->do_requests) {
sess->curl_not_http = GF_TRUE;
sess->do_requests = http_do_requests;
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_FOLLOWLOCATION, 1L);
if (!res) res = curl_easy_setopt(sess->curl_hnd, CURLOPT_TCP_KEEPALIVE, 1L);
if (sess->log_name) {
gf_free(sess->log_name);
sess->log_name = NULL;
}
sess->log_name = gf_strdup("CURL");
}
//set user-defined options
const struct curl_easyoption *opt = curl_easy_option_next(NULL);
while (opt) {
u64 llvalue;
s32 value;
const char *val = gf_opts_get_key("curl", opt->name);
if (!val) {
opt = curl_easy_option_next(opt);
continue;
}
switch (opt->type) {
case CURLOT_LONG:
case CURLOT_VALUES:
sscanf(val, "%d", &value);
if (!res) res = curl_easy_setopt(sess->curl_hnd, opt->id, value);
break;
case CURLOT_OFF_T:
sscanf(val, LLU, &llvalue);
if (!res) res = curl_easy_setopt(sess->curl_hnd, opt->id, llvalue);
break;
case CURLOT_STRING:
if (!res) res = curl_easy_setopt(sess->curl_hnd, opt->id, val);
break;
case CURLOT_OBJECT:
case CURLOT_SLIST:
case CURLOT_CBPTR:
case CURLOT_BLOB:
case CURLOT_FUNCTION:
break;
}
opt = curl_easy_option_next(opt);
}
if (res) {
GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[CURL] Failed to setup session: %s\n", curl_easy_strerror(res)));
return GF_SERVICE_ERROR;
}
sess->status = GF_NETIO_SETUP;
sess->last_error = GF_OK;
//reset number of retry and start time
sess->num_retry = SESSION_RETRY_COUNT;
sess->start_time = 0;
sess->reply_time = 0;
sess->local_buf_len = 0;
return GF_OK;
}
void curl_flush(GF_DownloadSession *sess)
{
gf_mx_p(sess->dm->curl_mx);
if (!sess->curl_hnd_registered) {
curl_multi_add_handle(sess->dm->curl_multi, sess->curl_hnd);
sess->curl_hnd_registered = GF_TRUE;
}
int still_running = 0;
CURLMcode mres = curl_multi_perform(sess->dm->curl_multi, &still_running);
gf_mx_v(sess->dm->curl_mx);
if (mres != CURLM_OK) {
GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[CURL] curl_multi_perform() failed: %s\n", curl_multi_strerror(mres) ));
}
//if no close detected and session has total size set nothing to do
//otherwise check for EOS detection, either error (close) or OK (chunk transfer)
if (!sess->dm->curl_has_close && sess->total_size) return;
//process close messages
sess->dm->curl_has_close = GF_FALSE;
CURLMsg *m=NULL;
int nb_msgs=0;
while (1) {
m = curl_multi_info_read(sess->dm->curl_multi, &nb_msgs);
if (!m) break;
if (m->msg != CURLMSG_DONE) continue;
GF_DownloadSession *a_sess = NULL;
CURLcode res = curl_easy_getinfo(m->easy_handle, CURLINFO_PRIVATE, &a_sess);
if (!a_sess) {
GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[CURL] failed to retrieve private pointer: %s\n", curl_easy_strerror(res) ));
continue;
}
if (a_sess->curl_hnd_registered) {
curl_multi_remove_handle(sess->dm->curl_multi, a_sess->curl_hnd);
a_sess->curl_hnd_registered = GF_FALSE;
}
if (m->data.result==CURLE_COULDNT_CONNECT)
a_sess->curl_closed = GF_IP_CONNECTION_FAILURE;
else if (m->data.result==CURLE_COULDNT_RESOLVE_HOST)
a_sess->curl_closed = GF_IP_ADDRESS_NOT_FOUND;
else if (m->data.result==CURLE_PARTIAL_FILE)
a_sess->curl_closed = GF_IP_CONNECTION_CLOSED;
else if (m->data.result==CURLE_REMOTE_FILE_NOT_FOUND)
a_sess->curl_closed = GF_URL_ERROR;
else if (m->data.result==CURLE_OK)
a_sess->curl_closed = GF_EOS;
else if (m->data.result==CURLE_REMOTE_ACCESS_DENIED) {
if (a_sess->num_retry) {
a_sess->curl_fake_hcode = 401;
a_sess->num_retry--;
a_sess->last_error = GF_OK;
a_sess->status = GF_NETIO_WAIT_FOR_REPLY;
a_sess->curl_closed = GF_OK;
} else {
a_sess->curl_closed = GF_AUTHENTICATION_FAILURE;
}
}
else if (m->data.result==CURLE_OPERATION_TIMEDOUT) {
//timeout was forced
if (a_sess->total_size && (a_sess->total_size == a_sess->local_buf_len + a_sess->bytes_done))
a_sess->curl_closed = GF_EOS;
else
a_sess->curl_closed = GF_IP_CONNECTION_FAILURE;
}
else
a_sess->curl_closed = GF_IP_NETWORK_FAILURE;
}
}
void curl_destroy(GF_DownloadSession *sess)
{
GF_LOG(GF_LOG_DEBUG, GF_LOG_HTTP, ("[CURL] closing session\n"));
if (sess->curl_hnd_registered) {
gf_mx_p(sess->dm->curl_mx);
curl_multi_remove_handle(sess->dm->curl_multi, sess->curl_hnd);
gf_mx_v(sess->dm->curl_mx);
}
curl_easy_cleanup(sess->curl_hnd);
sess->curl_hnd = NULL;
sess->curl_hnd_registered = GF_FALSE;
if (sess->curl_hdrs) curl_slist_free_all(sess->curl_hdrs);
sess->curl_hdrs = NULL;
sess->curl_closed = GF_OK;
}
Bool curl_can_handle_url(const char *url)
{
curl_version_info_data *ver = curl_version_info(CURLVERSION_NOW);
char *prot_sep = strstr(url, "://");
if (!prot_sep) return GF_FALSE;
u32 i=0, len = (u32) (prot_sep - url);
while (ver && ver->protocols[i]) {
if (!strnicmp(ver->protocols[i], url, len))
return GF_TRUE;
i++;
}
return GF_FALSE;
}
GF_Err curl_read_data(GF_DownloadSession *sess, char *data, u32 data_size, u32 *out_read)
{
if (!sess->curl_hnd) return GF_BAD_PARAM;
if (!sess->local_buf_len)
curl_flush(sess);
if (!sess->local_buf_len) {
*out_read = 0;
if (sess->curl_closed==GF_EOS) {
//for chunk transfer
if (!sess->total_size)
sess->total_size = sess->bytes_done;
//reutrn OK so that data_received is processed
return GF_OK;
}
else if (sess->curl_closed) {
sess->status = GF_NETIO_STATE_ERROR;
SET_LAST_ERR(GF_IP_CONNECTION_CLOSED);
return sess->curl_closed;
}
return GF_IP_NETWORK_EMPTY;
}
if (sess->curl_not_http && !sess->total_size) {
long cl=0;
curl_easy_getinfo(sess->curl_hnd, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &cl);
if (cl>=0)
sess->total_size = cl;
}
u32 nb_copy = MIN(data_size, sess->local_buf_len);
memcpy(data, sess->local_buf, nb_copy);
sess->local_buf_len -= nb_copy;
memmove(sess->local_buf, sess->local_buf + nb_copy, sess->local_buf_len);
*out_read = nb_copy;
return GF_OK;
}
void curl_connect(GF_DownloadSession*sess)
{
curl_flush(sess);
if (sess->curl_closed) {
if (sess->cache_entry && sess->cached_file) return;
sess->status = GF_NETIO_STATE_ERROR;
SET_LAST_ERR(GF_IP_CONNECTION_FAILURE)
gf_dm_sess_notify_state(sess, sess->status, GF_IP_CONNECTION_FAILURE);
}
}
GF_Err curl_process_reply(GF_DownloadSession *sess, u32 *ContentLength)
{
if (!sess->reply_time) {
curl_flush(sess);
if (!sess->reply_time) return GF_IP_NETWORK_EMPTY;
}
if (sess->curl_closed<0) {
gf_dm_disconnect(sess, HTTP_RESET_CONN);
sess->num_retry--;
if (sess->num_retry) {
GF_LOG(GF_LOG_INFO, GF_LOG_HTTP, ("[%s] Connection closed by server when processing %s - retrying\n", sess->log_name, sess->remote_path));
sess->status = GF_NETIO_SETUP;
} else {
GF_LOG(GF_LOG_ERROR, GF_LOG_HTTP, ("[%s] Connection closed by server when processing %s - aborting\n", sess->log_name, sess->remote_path));
SET_LAST_ERR(GF_IP_CONNECTION_CLOSED)
sess->status = GF_NETIO_STATE_ERROR;
}
return GF_IP_CONNECTION_CLOSED;
}
if (sess->curl_not_http) {
//if not HTTP we have no guarantee that the return code for the URL is known when receiving server first bytes
//eg FTP may need extra round trips before we get OK/ not found.
//We assume by default OK and will throw an error in curl_flush
// curl_fake_hcode can be set in curl_flush to force reprocessing the reply (eg on authentication failure)
sess->rsp_code = sess->curl_fake_hcode ? sess->curl_fake_hcode : 200;
sess->curl_fake_hcode = 0;
long cl=0;
curl_easy_getinfo(sess->curl_hnd, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &cl);
if (cl>=0)
*ContentLength = cl;
} else {
long rsp=505;
curl_easy_getinfo(sess->curl_hnd, CURLINFO_RESPONSE_CODE, &rsp);
sess->rsp_code = rsp;
if (gf_log_tool_level_on(GF_LOG_HTTP, GF_LOG_INFO)) {
long http_version;
const char *version="unknown";
curl_easy_getinfo(sess->curl_hnd, CURLINFO_HTTP_VERSION, &http_version);
switch (http_version) {
case CURL_HTTP_VERSION_1_0: version = "1.0"; break;
case CURL_HTTP_VERSION_1_1: version = "1.1"; break;
case CURL_HTTP_VERSION_2_0: version = "2"; break;
case CURL_HTTP_VERSION_3: version = "3"; break;
}
GF_LOG(GF_LOG_INFO, GF_LOG_HTTP, ("[CURL] Using HTTP %s\n", version));
}
}
sess->start_time = gf_sys_clock_high_res();
sess->start_time_utc = gf_net_get_utc();
return GF_OK;
}
#endif //GPAC_DISABLE_NETWORK && HAS_CURL