#define HTS_BUILDING_LIBRARY
#include <config.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <pthread.h>
#ifndef _WIN32
# include <sys/select.h>
#endif
#include <assert.h>
#include "hfile_internal.h"
#ifdef ENABLE_PLUGINS
#include "version.h"
#endif
#include "htslib/hts.h"
#include "htslib/kstring.h"
#include "htslib/khash.h"
#include <curl/curl.h>
#define AUTH_REFRESH_EARLY_SECS 60
#define MIN_SEEK_FORWARD 1000000
typedef struct {
char *path;
char *token;
time_t expiry;
int failed;
pthread_mutex_t lock;
} auth_token;
KHASH_MAP_INIT_STR(auth_map, auth_token *)
typedef struct {
struct curl_slist *list;
unsigned int num;
unsigned int size;
} hdrlist;
typedef struct {
hdrlist fixed; hdrlist extra; hts_httphdr_callback callback; void *callback_data; auth_token *auth; int auth_hdr_num; redirect_callback redirect; void *redirect_data; long *http_response_ptr; int fail_on_error; } http_headers;
typedef struct {
hFILE base;
CURL *easy;
CURLM *multi;
off_t file_size;
struct {
union { char *rd; const char *wr; } ptr;
size_t len;
} buffer;
CURLcode final_result; unsigned paused : 1; unsigned closing : 1; unsigned finished : 1; unsigned perform_again : 1;
unsigned is_read : 1; unsigned can_seek : 1; unsigned is_recursive:1; unsigned tried_seek : 1; int nrunning;
http_headers headers;
off_t delayed_seek; off_t last_offset; char *preserved; size_t preserved_bytes; size_t preserved_size; } hFILE_libcurl;
static off_t libcurl_seek(hFILE *fpv, off_t offset, int whence);
static int restart_from_position(hFILE_libcurl *fp, off_t pos);
static int http_status_errno(int status)
{
if (status >= 500)
switch (status) {
case 501: return ENOSYS;
case 503: return EBUSY;
case 504: return ETIMEDOUT;
default: return EIO;
}
else if (status >= 400)
switch (status) {
case 401: return EPERM;
case 403: return EACCES;
case 404: return ENOENT;
case 405: return EROFS;
case 407: return EPERM;
case 408: return ETIMEDOUT;
case 410: return ENOENT;
default: return EINVAL;
}
else return 0;
}
static int easy_errno(CURL *easy, CURLcode err)
{
long lval;
switch (err) {
case CURLE_OK:
return 0;
case CURLE_UNSUPPORTED_PROTOCOL:
case CURLE_URL_MALFORMAT:
return EINVAL;
#if LIBCURL_VERSION_NUM >= 0x071505
case CURLE_NOT_BUILT_IN:
return ENOSYS;
#endif
case CURLE_COULDNT_RESOLVE_PROXY:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_FTP_CANT_GET_HOST:
return EDESTADDRREQ;
case CURLE_COULDNT_CONNECT:
case CURLE_SEND_ERROR:
case CURLE_RECV_ERROR:
if (curl_easy_getinfo(easy, CURLINFO_OS_ERRNO, &lval) == CURLE_OK)
return lval;
else
return ECONNABORTED;
case CURLE_REMOTE_ACCESS_DENIED:
case CURLE_LOGIN_DENIED:
case CURLE_TFTP_PERM:
return EACCES;
case CURLE_PARTIAL_FILE:
return EPIPE;
case CURLE_HTTP_RETURNED_ERROR:
if (curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &lval) == CURLE_OK)
return http_status_errno(lval);
else
return EIO;
case CURLE_OUT_OF_MEMORY:
return ENOMEM;
case CURLE_OPERATION_TIMEDOUT:
return ETIMEDOUT;
case CURLE_RANGE_ERROR:
return ESPIPE;
case CURLE_SSL_CONNECT_ERROR:
return ECONNABORTED;
case CURLE_FILE_COULDNT_READ_FILE:
case CURLE_TFTP_NOTFOUND:
return ENOENT;
case CURLE_TOO_MANY_REDIRECTS:
return ELOOP;
case CURLE_FILESIZE_EXCEEDED:
return EFBIG;
case CURLE_REMOTE_DISK_FULL:
return ENOSPC;
case CURLE_REMOTE_FILE_EXISTS:
return EEXIST;
default:
hts_log_error("Libcurl reported error %d (%s)", (int) err,
curl_easy_strerror(err));
return EIO;
}
}
static int multi_errno(CURLMcode errm)
{
switch (errm) {
case CURLM_CALL_MULTI_PERFORM:
case CURLM_OK:
return 0;
case CURLM_BAD_HANDLE:
case CURLM_BAD_EASY_HANDLE:
case CURLM_BAD_SOCKET:
return EBADF;
case CURLM_OUT_OF_MEMORY:
return ENOMEM;
default:
hts_log_error("Libcurl reported error %d (%s)", (int) errm,
curl_multi_strerror(errm));
return EIO;
}
}
static struct {
kstring_t useragent;
CURLSH *share;
char *auth_path;
khash_t(auth_map) *auth_map;
int allow_unencrypted_auth_header;
pthread_mutex_t auth_lock;
pthread_mutex_t share_lock;
} curl = { { 0, 0, NULL }, NULL, NULL, NULL, 0, PTHREAD_MUTEX_INITIALIZER,
PTHREAD_MUTEX_INITIALIZER };
static void share_lock(CURL *handle, curl_lock_data data,
curl_lock_access access, void *userptr) {
pthread_mutex_lock(&curl.share_lock);
}
static void share_unlock(CURL *handle, curl_lock_data data, void *userptr) {
pthread_mutex_unlock(&curl.share_lock);
}
static void free_auth(auth_token *tok) {
if (!tok) return;
if (pthread_mutex_destroy(&tok->lock)) abort();
free(tok->path);
free(tok->token);
free(tok);
}
static void libcurl_exit()
{
if (curl_share_cleanup(curl.share) == CURLSHE_OK)
curl.share = NULL;
free(curl.useragent.s);
curl.useragent.l = curl.useragent.m = 0; curl.useragent.s = NULL;
free(curl.auth_path);
curl.auth_path = NULL;
if (curl.auth_map) {
khiter_t i;
for (i = kh_begin(curl.auth_map); i != kh_end(curl.auth_map); ++i) {
if (kh_exist(curl.auth_map, i)) {
free_auth(kh_value(curl.auth_map, i));
kh_key(curl.auth_map, i) = NULL;
kh_value(curl.auth_map, i) = NULL;
}
}
kh_destroy(auth_map, curl.auth_map);
curl.auth_map = NULL;
}
curl_global_cleanup();
}
static int append_header(hdrlist *hdrs, const char *data, int dup) {
if (hdrs->num == hdrs->size) {
unsigned int new_sz = hdrs->size ? hdrs->size * 2 : 4, i;
struct curl_slist *new_list = realloc(hdrs->list,
new_sz * sizeof(*new_list));
if (!new_list) return -1;
hdrs->size = new_sz;
hdrs->list = new_list;
for (i = 1; i < hdrs->num; i++) hdrs->list[i-1].next = &hdrs->list[i];
}
hdrs->list[hdrs->num].data = dup ? strdup(data) : (char *) data;
if (!hdrs->list[hdrs->num].data) return -1;
if (hdrs->num > 0) hdrs->list[hdrs->num - 1].next = &hdrs->list[hdrs->num];
hdrs->list[hdrs->num].next = NULL;
hdrs->num++;
return 0;
}
static void free_headers(hdrlist *hdrs, int completely) {
unsigned int i;
for (i = 0; i < hdrs->num; i++) {
free(hdrs->list[i].data);
hdrs->list[i].data = NULL;
hdrs->list[i].next = NULL;
}
hdrs->num = 0;
if (completely) {
free(hdrs->list);
hdrs->size = 0;
hdrs->list = NULL;
}
}
static struct curl_slist * get_header_list(hFILE_libcurl *fp) {
if (fp->headers.fixed.num > 0)
return &fp->headers.fixed.list[0];
if (fp->headers.extra.num > 0)
return &fp->headers.extra.list[0];
return 0;
}
static inline int is_authorization(const char *hdr) {
return (strncasecmp("authorization:", hdr, 14) == 0);
}
static int add_callback_headers(hFILE_libcurl *fp) {
char **hdrs = NULL, **hdr;
if (!fp->headers.callback)
return 0;
if (fp->headers.callback(fp->headers.callback_data, &hdrs) != 0) {
return -1;
}
if (!hdrs) return 0;
if (fp->headers.fixed.num > 0) {
fp->headers.fixed.list[fp->headers.fixed.num - 1].next = NULL;
}
free_headers(&fp->headers.extra, 0);
if (fp->headers.auth_hdr_num > 0 || fp->headers.auth_hdr_num == -2)
fp->headers.auth_hdr_num = 0;
for (hdr = hdrs; *hdr; hdr++) {
if (append_header(&fp->headers.extra, *hdr, 0) < 0) {
goto cleanup;
}
if (is_authorization(*hdr) && !fp->headers.auth_hdr_num)
fp->headers.auth_hdr_num = -2;
}
for (hdr = hdrs; *hdr; hdr++) *hdr = NULL;
if (fp->headers.fixed.num > 0 && fp->headers.extra.num > 0) {
fp->headers.fixed.list[fp->headers.fixed.num - 1].next
= &fp->headers.extra.list[0];
}
return 0;
cleanup:
while (hdr && *hdr) {
free(*hdr);
*hdr = NULL;
}
return -1;
}
static int read_auth_json(auth_token *tok, hFILE *auth_fp) {
hts_json_token *t = hts_json_alloc_token();
kstring_t str = {0, 0, NULL};
char *token = NULL, *type = NULL, *expiry = NULL;
int ret = 'i';
if (!t) goto error;
if ((ret = hts_json_fnext(auth_fp, t, &str)) != '{') goto error;
while (hts_json_fnext(auth_fp, t, &str) != '}') {
char *key;
if (hts_json_token_type(t) != 's') {
ret = '?';
goto error;
}
key = hts_json_token_str(t);
if (!key) goto error;
if (strcmp(key, "access_token") == 0) {
if ((ret = hts_json_fnext(auth_fp, t, &str)) != 's') goto error;
token = ks_release(&str);
} else if (strcmp(key, "token_type") == 0) {
if ((ret = hts_json_fnext(auth_fp, t, &str)) != 's') goto error;
type = ks_release(&str);
} else if (strcmp(key, "expires_in") == 0) {
if ((ret = hts_json_fnext(auth_fp, t, &str)) != 'n') goto error;
expiry = ks_release(&str);
} else if (hts_json_fskip_value(auth_fp, '\0') != 'v') {
ret = '?';
goto error;
}
}
if (!token || (type && strcmp(type, "Bearer") != 0)) {
ret = 'i';
goto error;
}
ret = 'm';
str.l = 0;
if (kputs("Authorization: Bearer ", &str) < 0) goto error;
if (kputs(token, &str) < 0) goto error;
free(tok->token);
tok->token = ks_release(&str);
if (expiry) {
long exp = strtol(expiry, NULL, 10);
if (exp < 0) exp = 0;
tok->expiry = time(NULL) + exp;
} else {
tok->expiry = 0;
}
ret = 'v';
error:
free(token);
free(type);
free(expiry);
free(str.s);
hts_json_free_token(t);
return ret;
}
static int read_auth_plain(auth_token *tok, hFILE *auth_fp) {
kstring_t line = {0, 0, NULL};
kstring_t token = {0, 0, NULL};
const char *start, *end;
if (kgetline(&line, (char * (*)(char *, int, void *)) hgets, auth_fp) < 0) goto error;
if (kputc('\0', &line) < 0) goto error;
for (start = line.s; *start && isspace_c(*start); start++) {}
for (end = start; *end && !isspace_c(*end); end++) {}
if (end > start) {
if (kputs("Authorization: Bearer ", &token) < 0) goto error;
if (kputsn(start, end - start, &token) < 0) goto error;
}
free(tok->token);
tok->token = ks_release(&token);
tok->expiry = 0;
free(line.s);
return 0;
error:
free(line.s);
free(token.s);
return -1;
}
static int renew_auth_token(auth_token *tok, int *changed) {
hFILE *auth_fp = NULL;
char buffer[16];
ssize_t len;
*changed = 0;
if (tok->expiry == 0 || time(NULL) + AUTH_REFRESH_EARLY_SECS < tok->expiry)
return 0;
if (tok->failed)
return -1;
*changed = 1;
auth_fp = hopen(tok->path, "rR");
if (!auth_fp) {
if (errno != ENOENT)
goto fail;
tok->expiry = 0; free(tok->token); return 0;
}
len = hpeek(auth_fp, buffer, sizeof(buffer));
if (len < 0)
goto fail;
if (memchr(buffer, '{', len) != NULL) {
if (read_auth_json(tok, auth_fp) != 'v')
goto fail;
} else {
if (read_auth_plain(tok, auth_fp) < 0)
goto fail;
}
return hclose(auth_fp) < 0 ? -1 : 0;
fail:
tok->failed = 1;
if (auth_fp) hclose_abruptly(auth_fp);
return -1;
}
static int add_auth_header(hFILE_libcurl *fp) {
int changed = 0;
if (fp->headers.auth_hdr_num < 0)
return 0;
if (!fp->headers.auth)
return 0;
pthread_mutex_lock(&fp->headers.auth->lock);
if (renew_auth_token(fp->headers.auth, &changed) < 0)
goto unlock_fail;
if (!changed && fp->headers.auth_hdr_num > 0) {
pthread_mutex_unlock(&fp->headers.auth->lock);
return 0;
}
if (fp->headers.auth_hdr_num > 0) {
char *header = fp->headers.auth->token;
char *header_copy = header ? strdup(header) : NULL;
int idx = fp->headers.auth_hdr_num - 1;
if (header && !header_copy)
goto unlock_fail;
if (header_copy) {
free(fp->headers.extra.list[idx].data);
fp->headers.extra.list[idx].data = header_copy;
} else {
unsigned int j;
free(fp->headers.extra.list[idx].data);
for (j = idx + 1; j < fp->headers.extra.num; j++) {
fp->headers.extra.list[j - 1] = fp->headers.extra.list[j];
fp->headers.extra.list[j - 1].next = &fp->headers.extra.list[j];
}
fp->headers.extra.num--;
if (fp->headers.extra.num > 0) {
fp->headers.extra.list[fp->headers.extra.num-1].next = NULL;
} else if (fp->headers.fixed.num > 0) {
fp->headers.fixed.list[fp->headers.fixed.num - 1].next = NULL;
}
fp->headers.auth_hdr_num = 0;
}
} else if (fp->headers.auth->token) {
if (append_header(&fp->headers.extra,
fp->headers.auth->token, 1) < 0) {
goto unlock_fail;
}
fp->headers.auth_hdr_num = fp->headers.extra.num;
}
pthread_mutex_unlock(&fp->headers.auth->lock);
return 0;
unlock_fail:
pthread_mutex_unlock(&fp->headers.auth->lock);
return -1;
}
static int get_auth_token(hFILE_libcurl *fp, const char *url) {
const char *host = NULL, *p, *q;
kstring_t name = {0, 0, NULL};
size_t host_len = 0;
khiter_t idx;
auth_token *tok = NULL;
if (!curl.auth_path || fp->is_recursive || fp->headers.auth_hdr_num != 0)
return 0;
if (!curl.allow_unencrypted_auth_header && strncmp(url, "https://", 8) != 0)
return 0;
host = strstr(url, "://");
if (host) {
host += 3;
host_len = strcspn(host, "/");
}
p = curl.auth_path;
while ((q = strstr(p, "%h")) != NULL) {
if (q - p > INT_MAX || host_len > INT_MAX) goto error;
if (kputsn_(p, q - p, &name) < 0) goto error;
if (kputsn_(host, host_len, &name) < 0) goto error;
p = q + 2;
}
if (kputs(p, &name) < 0) goto error;
pthread_mutex_lock(&curl.auth_lock);
idx = kh_get(auth_map, curl.auth_map, name.s);
if (idx < kh_end(curl.auth_map)) {
tok = kh_value(curl.auth_map, idx);
} else {
tok = calloc(1, sizeof(*tok));
if (tok && pthread_mutex_init(&tok->lock, NULL) != 0) {
free(tok);
tok = NULL;
}
if (tok) {
int ret = -1;
tok->path = ks_release(&name);
tok->token = NULL;
tok->expiry = 1; idx = kh_put(auth_map, curl.auth_map, tok->path, &ret);
if (ret < 0) {
free_auth(tok);
tok = NULL;
}
kh_value(curl.auth_map, idx) = tok;
}
}
pthread_mutex_unlock(&curl.auth_lock);
fp->headers.auth = tok;
free(name.s);
return add_auth_header(fp);
error:
free(name.s);
return -1;
}
static void process_messages(hFILE_libcurl *fp)
{
CURLMsg *msg;
int remaining;
while ((msg = curl_multi_info_read(fp->multi, &remaining)) != NULL) {
switch (msg->msg) {
case CURLMSG_DONE:
fp->finished = 1;
fp->final_result = msg->data.result;
break;
default:
break;
}
}
}
static int wait_perform(hFILE_libcurl *fp)
{
fd_set rd, wr, ex;
int maxfd, nrunning;
long timeout;
CURLMcode errm;
if (!fp->perform_again) {
FD_ZERO(&rd);
FD_ZERO(&wr);
FD_ZERO(&ex);
if (curl_multi_fdset(fp->multi, &rd, &wr, &ex, &maxfd) != CURLM_OK)
maxfd = -1, timeout = 1000;
else {
if (curl_multi_timeout(fp->multi, &timeout) != CURLM_OK)
timeout = 1000;
else if (timeout < 0) {
timeout = 10000; }
}
if (maxfd < 0) {
if (timeout > 100)
timeout = 100; #ifdef _WIN32
Sleep(timeout);
timeout = 0;
#endif
}
if (timeout > 0) {
struct timeval tval;
tval.tv_sec = (timeout / 1000);
tval.tv_usec = (timeout % 1000) * 1000;
if (select(maxfd + 1, &rd, &wr, &ex, &tval) < 0) return -1;
}
}
errm = curl_multi_perform(fp->multi, &nrunning);
fp->perform_again = 0;
if (errm == CURLM_CALL_MULTI_PERFORM) fp->perform_again = 1;
else if (errm != CURLM_OK) { errno = multi_errno(errm); return -1; }
if (nrunning < fp->nrunning) process_messages(fp);
return 0;
}
static size_t recv_callback(char *ptr, size_t size, size_t nmemb, void *fpv)
{
hFILE_libcurl *fp = (hFILE_libcurl *) fpv;
size_t n = size * nmemb;
if (n > fp->buffer.len) {
fp->paused = 1;
return CURL_WRITEFUNC_PAUSE;
}
else if (n == 0) return 0;
memcpy(fp->buffer.ptr.rd, ptr, n);
fp->buffer.ptr.rd += n;
fp->buffer.len -= n;
return n;
}
static size_t header_callback(void *contents, size_t size, size_t nmemb,
void *userp)
{
size_t realsize = size * nmemb;
kstring_t *resp = (kstring_t *)userp;
if (kputsn((const char *)contents, realsize, resp) == EOF) {
return 0;
}
return realsize;
}
static ssize_t libcurl_read(hFILE *fpv, void *bufferv, size_t nbytes)
{
hFILE_libcurl *fp = (hFILE_libcurl *) fpv;
char *buffer = (char *) bufferv;
off_t to_skip = -1;
ssize_t got = 0;
CURLcode err;
if (fp->delayed_seek >= 0) {
assert(fp->base.offset == fp->delayed_seek);
if (fp->preserved
&& fp->last_offset > fp->delayed_seek
&& fp->last_offset - fp->preserved_bytes <= fp->delayed_seek) {
size_t n = fp->last_offset - fp->delayed_seek;
char *start = fp->preserved + (fp->preserved_bytes - n);
size_t bytes = n <= nbytes ? n : nbytes;
memcpy(buffer, start, bytes);
if (bytes < n) { fp->delayed_seek += bytes;
} else {
fp->last_offset = fp->delayed_seek = -1;
}
return bytes;
}
if (fp->last_offset >= 0
&& fp->delayed_seek > fp->last_offset
&& fp->delayed_seek - fp->last_offset < MIN_SEEK_FORWARD) {
to_skip = fp->delayed_seek - fp->last_offset;
} else {
if (restart_from_position(fp, fp->delayed_seek) < 0) {
return -1;
}
}
fp->delayed_seek = -1;
fp->last_offset = -1;
fp->preserved_bytes = 0;
}
do {
fp->buffer.ptr.rd = buffer;
fp->buffer.len = nbytes;
fp->paused = 0;
if (!fp->finished) {
err = curl_easy_pause(fp->easy, CURLPAUSE_CONT);
if (err != CURLE_OK) {
errno = easy_errno(fp->easy, err);
return -1;
}
}
while (! fp->paused && ! fp->finished) {
if (wait_perform(fp) < 0) return -1;
}
got = fp->buffer.ptr.rd - buffer;
if (to_skip >= 0) { if (got <= to_skip) { to_skip -= got;
} else {
got -= to_skip;
if (got > 0) { memmove(buffer, buffer + to_skip, got);
to_skip = -1;
}
}
}
} while (to_skip >= 0 && ! fp->finished);
fp->buffer.ptr.rd = NULL;
fp->buffer.len = 0;
if (fp->finished && fp->final_result != CURLE_OK) {
errno = easy_errno(fp->easy, fp->final_result);
return -1;
}
return got;
}
static size_t send_callback(char *ptr, size_t size, size_t nmemb, void *fpv)
{
hFILE_libcurl *fp = (hFILE_libcurl *) fpv;
size_t n = size * nmemb;
if (fp->buffer.len == 0) {
if (fp->closing) return 0;
else { fp->paused = 1; return CURL_READFUNC_PAUSE; }
}
if (n > fp->buffer.len) n = fp->buffer.len;
memcpy(ptr, fp->buffer.ptr.wr, n);
fp->buffer.ptr.wr += n;
fp->buffer.len -= n;
return n;
}
static ssize_t libcurl_write(hFILE *fpv, const void *bufferv, size_t nbytes)
{
hFILE_libcurl *fp = (hFILE_libcurl *) fpv;
const char *buffer = (const char *) bufferv;
CURLcode err;
fp->buffer.ptr.wr = buffer;
fp->buffer.len = nbytes;
fp->paused = 0;
err = curl_easy_pause(fp->easy, CURLPAUSE_CONT);
if (err != CURLE_OK) { errno = easy_errno(fp->easy, err); return -1; }
while (! fp->paused && ! fp->finished)
if (wait_perform(fp) < 0) return -1;
nbytes = fp->buffer.ptr.wr - buffer;
fp->buffer.ptr.wr = NULL;
fp->buffer.len = 0;
if (fp->finished && fp->final_result != CURLE_OK) {
errno = easy_errno(fp->easy, fp->final_result);
return -1;
}
return nbytes;
}
static void preserve_buffer_content(hFILE_libcurl *fp)
{
if (fp->base.begin == fp->base.end) {
fp->preserved_bytes = 0;
return;
}
if (!fp->preserved
|| fp->preserved_size < fp->base.limit - fp->base.buffer) {
fp->preserved = malloc(fp->base.limit - fp->base.buffer);
if (!fp->preserved) return;
fp->preserved_size = fp->base.limit - fp->base.buffer;
}
assert(fp->base.end - fp->base.begin <= fp->preserved_size);
memcpy(fp->preserved, fp->base.begin, fp->base.end - fp->base.begin);
fp->preserved_bytes = fp->base.end - fp->base.begin;
return;
}
static off_t libcurl_seek(hFILE *fpv, off_t offset, int whence)
{
hFILE_libcurl *fp = (hFILE_libcurl *) fpv;
off_t origin, pos;
if (!fp->is_read || !fp->can_seek) {
errno = ESPIPE;
return -1;
}
switch (whence) {
case SEEK_SET:
origin = 0;
break;
case SEEK_CUR:
errno = ENOSYS;
return -1;
case SEEK_END:
if (fp->file_size < 0) { errno = ESPIPE; return -1; }
origin = fp->file_size;
break;
default:
errno = EINVAL;
return -1;
}
if ((offset < 0)? origin + offset < 0
: (fp->file_size >= 0 && offset > fp->file_size - origin)) {
errno = EINVAL;
return -1;
}
pos = origin + offset;
if (fp->tried_seek) {
if (fp->delayed_seek < 0) {
fp->last_offset = fp->base.offset + (fp->base.end - fp->base.buffer);
preserve_buffer_content(fp);
}
fp->delayed_seek = pos;
return pos;
}
if (restart_from_position(fp, pos) < 0) {
errno = ESPIPE;
return -1;
}
fp->tried_seek = 1;
return pos;
}
static int restart_from_position(hFILE_libcurl *fp, off_t pos) {
hFILE_libcurl temp_fp;
CURLcode err;
CURLMcode errm;
int update_headers = 0;
int save_errno = 0;
if (fp->headers.callback) {
if (add_callback_headers(fp) != 0)
return -1;
update_headers = 1;
}
if (fp->headers.auth_hdr_num > 0 && fp->headers.auth) {
if (add_auth_header(fp) != 0)
return -1;
update_headers = 1;
}
if (update_headers) {
struct curl_slist *list = get_header_list(fp);
if (list) {
err = curl_easy_setopt(fp->easy, CURLOPT_HTTPHEADER, list);
if (err != CURLE_OK) {
errno = easy_errno(fp->easy,err);
return -1;
}
}
}
memcpy(&temp_fp, fp, sizeof(temp_fp));
temp_fp.buffer.len = 0;
temp_fp.buffer.ptr.rd = NULL;
temp_fp.easy = curl_easy_duphandle(fp->easy);
if (!temp_fp.easy)
goto early_error;
err = curl_easy_setopt(temp_fp.easy, CURLOPT_RESUME_FROM_LARGE,(curl_off_t)pos);
err |= curl_easy_setopt(temp_fp.easy, CURLOPT_PRIVATE, &temp_fp);
err |= curl_easy_setopt(temp_fp.easy, CURLOPT_WRITEDATA, &temp_fp);
if (err != CURLE_OK) {
save_errno = easy_errno(temp_fp.easy, err);
goto error;
}
temp_fp.buffer.len = 0; temp_fp.paused = temp_fp.finished = 0;
errm = curl_multi_add_handle(fp->multi, temp_fp.easy);
if (errm != CURLM_OK) {
save_errno = multi_errno(errm);
goto error;
}
temp_fp.nrunning = ++fp->nrunning;
while (! temp_fp.paused && ! temp_fp.finished)
if (wait_perform(&temp_fp) < 0) {
save_errno = errno;
goto error_remove;
}
if (temp_fp.finished && temp_fp.final_result != CURLE_OK) {
save_errno = easy_errno(temp_fp.easy, temp_fp.final_result);
goto error_remove;
}
errm = curl_multi_remove_handle(fp->multi, fp->easy);
if (errm != CURLM_OK) {
curl_easy_reset(temp_fp.easy);
if (curl_multi_remove_handle(fp->multi, temp_fp.easy) == CURLM_OK) {
fp->nrunning--;
curl_easy_cleanup(temp_fp.easy);
}
save_errno = multi_errno(errm);
goto early_error;
}
fp->nrunning--;
curl_easy_cleanup(fp->easy);
fp->easy = temp_fp.easy;
err = curl_easy_setopt(fp->easy, CURLOPT_WRITEDATA, fp);
err |= curl_easy_setopt(fp->easy, CURLOPT_PRIVATE, fp);
if (err != CURLE_OK) {
save_errno = easy_errno(fp->easy, err);
curl_easy_reset(fp->easy);
errno = save_errno;
return -1;
}
fp->buffer.len = 0;
fp->paused = temp_fp.paused;
fp->finished = temp_fp.finished;
fp->perform_again = temp_fp.perform_again;
fp->final_result = temp_fp.final_result;
return 0;
error_remove:
curl_easy_reset(temp_fp.easy); errm = curl_multi_remove_handle(fp->multi, temp_fp.easy);
if (errm != CURLM_OK) {
errno = multi_errno(errm);
return -1;
}
fp->nrunning--;
error:
curl_easy_cleanup(temp_fp.easy);
early_error:
fp->can_seek = 0; if (save_errno)
errno = save_errno;
return -1;
}
static int libcurl_close(hFILE *fpv)
{
hFILE_libcurl *fp = (hFILE_libcurl *) fpv;
CURLcode err;
CURLMcode errm;
int save_errno = 0;
fp->buffer.len = 0;
fp->closing = 1;
fp->paused = 0;
if (!fp->finished) {
err = curl_easy_pause(fp->easy, CURLPAUSE_CONT);
if (err != CURLE_OK) save_errno = easy_errno(fp->easy, err);
}
while (save_errno == 0 && ! fp->paused && ! fp->finished)
if (wait_perform(fp) < 0) save_errno = errno;
if (fp->finished && fp->final_result != CURLE_OK)
save_errno = easy_errno(fp->easy, fp->final_result);
errm = curl_multi_remove_handle(fp->multi, fp->easy);
if (errm != CURLM_OK && save_errno == 0) save_errno = multi_errno(errm);
fp->nrunning--;
curl_easy_cleanup(fp->easy);
curl_multi_cleanup(fp->multi);
if (fp->headers.callback) fp->headers.callback(fp->headers.callback_data, NULL);
free_headers(&fp->headers.fixed, 1);
free_headers(&fp->headers.extra, 1);
free(fp->preserved);
if (save_errno) { errno = save_errno; return -1; }
else return 0;
}
static const struct hFILE_backend libcurl_backend =
{
libcurl_read, libcurl_write, libcurl_seek, NULL, libcurl_close
};
static hFILE *
libcurl_open(const char *url, const char *modes, http_headers *headers)
{
hFILE_libcurl *fp;
struct curl_slist *list;
char mode;
const char *s;
CURLcode err;
CURLMcode errm;
int save, is_recursive;
kstring_t in_header = {0, 0, NULL};
long response;
is_recursive = strchr(modes, 'R') != NULL;
if ((s = strpbrk(modes, "rwa+")) != NULL) {
mode = *s;
if (strpbrk(&s[1], "rwa+")) mode = 'e';
}
else mode = '\0';
if (mode != 'r' && mode != 'w') { errno = EINVAL; goto early_error; }
fp = (hFILE_libcurl *) hfile_init(sizeof (hFILE_libcurl), modes, 0);
if (fp == NULL) goto early_error;
if (headers) {
fp->headers = *headers;
} else {
memset(&fp->headers, 0, sizeof(fp->headers));
fp->headers.fail_on_error = 1;
}
fp->file_size = -1;
fp->buffer.ptr.rd = NULL;
fp->buffer.len = 0;
fp->final_result = (CURLcode) -1;
fp->paused = fp->closing = fp->finished = fp->perform_again = 0;
fp->can_seek = 1;
fp->tried_seek = 0;
fp->delayed_seek = fp->last_offset = -1;
fp->preserved = NULL;
fp->preserved_bytes = fp->preserved_size = 0;
fp->is_recursive = is_recursive;
fp->nrunning = 0;
fp->easy = NULL;
fp->multi = curl_multi_init();
if (fp->multi == NULL) { errno = ENOMEM; goto error; }
fp->easy = curl_easy_init();
if (fp->easy == NULL) { errno = ENOMEM; goto error; }
err = curl_easy_setopt(fp->easy, CURLOPT_PRIVATE, fp);
err |= curl_easy_setopt(fp->easy, CURLOPT_FTP_FILEMETHOD, CURLFTPMETHOD_NOCWD);
if (mode == 'r') {
err |= curl_easy_setopt(fp->easy, CURLOPT_WRITEFUNCTION, recv_callback);
err |= curl_easy_setopt(fp->easy, CURLOPT_WRITEDATA, fp);
fp->is_read = 1;
}
else {
err |= curl_easy_setopt(fp->easy, CURLOPT_READFUNCTION, send_callback);
err |= curl_easy_setopt(fp->easy, CURLOPT_READDATA, fp);
err |= curl_easy_setopt(fp->easy, CURLOPT_UPLOAD, 1L);
if (append_header(&fp->headers.fixed,
"Transfer-Encoding: chunked", 1) < 0)
goto error;
fp->is_read = 0;
}
err |= curl_easy_setopt(fp->easy, CURLOPT_SHARE, curl.share);
err |= curl_easy_setopt(fp->easy, CURLOPT_URL, url);
{
char* env_curl_ca_bundle = getenv("CURL_CA_BUNDLE");
if (env_curl_ca_bundle) {
err |= curl_easy_setopt(fp->easy, CURLOPT_CAINFO, env_curl_ca_bundle);
}
}
err |= curl_easy_setopt(fp->easy, CURLOPT_USERAGENT, curl.useragent.s);
if (fp->headers.callback) {
if (add_callback_headers(fp) != 0) goto error;
}
if (get_auth_token(fp, url) < 0)
goto error;
if ((list = get_header_list(fp)) != NULL)
err |= curl_easy_setopt(fp->easy, CURLOPT_HTTPHEADER, list);
if (hts_verbose <= 8 && fp->headers.fail_on_error)
err |= curl_easy_setopt(fp->easy, CURLOPT_FAILONERROR, 1L);
if (hts_verbose >= 8)
err |= curl_easy_setopt(fp->easy, CURLOPT_VERBOSE, 1L);
if (fp->headers.redirect) {
err |= curl_easy_setopt(fp->easy, CURLOPT_HEADERFUNCTION, header_callback);
err |= curl_easy_setopt(fp->easy, CURLOPT_HEADERDATA, (void *)&in_header);
} else {
err |= curl_easy_setopt(fp->easy, CURLOPT_FOLLOWLOCATION, 1L);
}
if (err != 0) { errno = ENOSYS; goto error; }
errm = curl_multi_add_handle(fp->multi, fp->easy);
if (errm != CURLM_OK) { errno = multi_errno(errm); goto error; }
fp->nrunning++;
while (! fp->paused && ! fp->finished) {
if (wait_perform(fp) < 0) goto error_remove;
}
curl_easy_getinfo(fp->easy, CURLINFO_RESPONSE_CODE, &response);
if (fp->headers.http_response_ptr) {
*fp->headers.http_response_ptr = response;
}
if (fp->finished && fp->final_result != CURLE_OK) {
errno = easy_errno(fp->easy, fp->final_result);
goto error_remove;
}
if (fp->headers.redirect) {
if (response >= 300 && response < 400) { kstring_t new_url = {0, 0, NULL};
if (fp->headers.redirect(fp->headers.redirect_data, response,
&in_header, &new_url)) {
errno = ENOSYS;
goto error;
}
err |= curl_easy_setopt(fp->easy, CURLOPT_URL, new_url.s);
err |= curl_easy_setopt(fp->easy, CURLOPT_HEADERFUNCTION, NULL);
err |= curl_easy_setopt(fp->easy, CURLOPT_HEADERDATA, NULL);
free(ks_release(&in_header));
if (err != 0) { errno = ENOSYS; goto error; }
free(ks_release(&new_url));
if (restart_from_position(fp, 0) < 0) {
goto error_remove;
}
if (fp->headers.http_response_ptr) {
curl_easy_getinfo(fp->easy, CURLINFO_RESPONSE_CODE,
fp->headers.http_response_ptr);
}
if (fp->finished && fp->final_result != CURLE_OK) {
errno = easy_errno(fp->easy, fp->final_result);
goto error_remove;
}
} else {
err |= curl_easy_setopt(fp->easy, CURLOPT_HEADERFUNCTION, NULL);
err |= curl_easy_setopt(fp->easy, CURLOPT_HEADERDATA, NULL);
free(ks_release(&in_header));
if (err != 0) { errno = ENOSYS; goto error; }
}
}
if (mode == 'r') {
#if LIBCURL_VERSION_NUM >= 0x073700
curl_off_t offset;
if (curl_easy_getinfo(fp->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
&offset) == CURLE_OK && offset > 0)
fp->file_size = (off_t) offset;
#else
double dval;
if (curl_easy_getinfo(fp->easy, CURLINFO_CONTENT_LENGTH_DOWNLOAD,
&dval) == CURLE_OK && dval >= 0.0)
fp->file_size = (off_t) (dval + 0.1);
#endif
}
fp->base.backend = &libcurl_backend;
return &fp->base;
error_remove:
save = errno;
(void) curl_multi_remove_handle(fp->multi, fp->easy);
fp->nrunning--;
errno = save;
error:
if (fp->headers.redirect) free(in_header.s);
save = errno;
if (fp->easy) curl_easy_cleanup(fp->easy);
if (fp->multi) curl_multi_cleanup(fp->multi);
free_headers(&fp->headers.extra, 1);
hfile_destroy((hFILE *) fp);
errno = save;
return NULL;
early_error:
return NULL;
}
static hFILE *hopen_libcurl(const char *url, const char *modes)
{
return libcurl_open(url, modes, NULL);
}
static int parse_va_list(http_headers *headers, va_list args)
{
const char *argtype;
while ((argtype = va_arg(args, const char *)) != NULL)
if (strcmp(argtype, "httphdr:v") == 0) {
const char **hdr;
for (hdr = va_arg(args, const char **); *hdr; hdr++) {
if (append_header(&headers->fixed, *hdr, 1) < 0)
return -1;
if (is_authorization(*hdr))
headers->auth_hdr_num = -1;
}
}
else if (strcmp(argtype, "httphdr:l") == 0) {
const char *hdr;
while ((hdr = va_arg(args, const char *)) != NULL) {
if (append_header(&headers->fixed, hdr, 1) < 0)
return -1;
if (is_authorization(hdr))
headers->auth_hdr_num = -1;
}
}
else if (strcmp(argtype, "httphdr") == 0) {
const char *hdr = va_arg(args, const char *);
if (hdr) {
if (append_header(&headers->fixed, hdr, 1) < 0)
return -1;
if (is_authorization(hdr))
headers->auth_hdr_num = -1;
}
}
else if (strcmp(argtype, "httphdr_callback") == 0) {
headers->callback = va_arg(args, const hts_httphdr_callback);
}
else if (strcmp(argtype, "httphdr_callback_data") == 0) {
headers->callback_data = va_arg(args, void *);
}
else if (strcmp(argtype, "va_list") == 0) {
va_list *args2 = va_arg(args, va_list *);
if (args2) {
if (parse_va_list(headers, *args2) < 0) return -1;
}
}
else if (strcmp(argtype, "auth_token_enabled") == 0) {
const char *flag = va_arg(args, const char *);
if (strcmp(flag, "false") == 0)
headers->auth_hdr_num = -3;
}
else if (strcmp(argtype, "redirect_callback") == 0) {
headers->redirect = va_arg(args, const redirect_callback);
}
else if (strcmp(argtype, "redirect_callback_data") == 0) {
headers->redirect_data = va_arg(args, void *);
}
else if (strcmp(argtype, "http_response_ptr") == 0) {
headers->http_response_ptr = va_arg(args, long *);
}
else if (strcmp(argtype, "fail_on_error") == 0) {
headers->fail_on_error = va_arg(args, int);
}
else { errno = EINVAL; return -1; }
return 0;
}
static hFILE *vhopen_libcurl(const char *url, const char *modes, va_list args)
{
hFILE *fp = NULL;
http_headers headers = { .fail_on_error = 1 };
if (parse_va_list(&headers, args) == 0) {
fp = libcurl_open(url, modes, &headers);
}
if (!fp) {
free_headers(&headers.fixed, 1);
}
return fp;
}
int PLUGIN_GLOBAL(hfile_plugin_init,_libcurl)(struct hFILE_plugin *self)
{
static const struct hFILE_scheme_handler handler =
{ hopen_libcurl, hfile_always_remote, "libcurl",
2000 + 50,
vhopen_libcurl };
#ifdef ENABLE_PLUGINS
static const char id[] =
"@(#)hfile_libcurl plugin (htslib)\t" HTS_VERSION_TEXT;
const char *version = strchr(id, '\t')+1;
#else
const char *version = hts_version();
#endif
const curl_version_info_data *info;
const char * const *protocol;
const char *auth;
CURLcode err;
CURLSHcode errsh;
err = curl_global_init(CURL_GLOBAL_ALL);
if (err != CURLE_OK) { errno = easy_errno(NULL, err); return -1; }
curl.share = curl_share_init();
if (curl.share == NULL) { curl_global_cleanup(); errno = EIO; return -1; }
errsh = curl_share_setopt(curl.share, CURLSHOPT_LOCKFUNC, share_lock);
errsh |= curl_share_setopt(curl.share, CURLSHOPT_UNLOCKFUNC, share_unlock);
errsh |= curl_share_setopt(curl.share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
if (errsh != 0) {
curl_share_cleanup(curl.share);
curl_global_cleanup();
errno = EIO;
return -1;
}
if ((auth = getenv("HTS_AUTH_LOCATION")) != NULL) {
curl.auth_path = strdup(auth);
curl.auth_map = kh_init(auth_map);
if (!curl.auth_path || !curl.auth_map) {
int save_errno = errno;
free(curl.auth_path);
kh_destroy(auth_map, curl.auth_map);
curl_share_cleanup(curl.share);
curl_global_cleanup();
errno = save_errno;
return -1;
}
}
if ((auth = getenv("HTS_ALLOW_UNENCRYPTED_AUTHORIZATION_HEADER")) != NULL
&& strcmp(auth, "I understand the risks") == 0) {
curl.allow_unencrypted_auth_header = 1;
}
info = curl_version_info(CURLVERSION_NOW);
ksprintf(&curl.useragent, "htslib/%s libcurl/%s", version, info->version);
self->name = "libcurl";
self->destroy = libcurl_exit;
for (protocol = info->protocols; *protocol; protocol++)
hfile_add_scheme_handler(*protocol, &handler);
return 0;
}