|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "libavutil/avstring.h" |
|
#include "libavutil/file_open.h" |
|
#include "libavutil/getenv_utf8.h" |
|
#include "libavutil/opt.h" |
|
#include <sys/stat.h> |
|
#include "os_support.h" |
|
#include "url.h" |
|
|
|
|
|
|
|
#ifndef PATH_MAX |
|
#define PATH_MAX 4096 |
|
#endif |
|
|
|
typedef struct IPFSGatewayContext { |
|
AVClass *class; |
|
URLContext *inner; |
|
|
|
char *gateway; |
|
|
|
|
|
|
|
char gateway_buffer[PATH_MAX]; |
|
} IPFSGatewayContext; |
|
|
|
|
|
|
|
|
|
|
|
static int populate_ipfs_gateway(URLContext *h) |
|
{ |
|
IPFSGatewayContext *c = h->priv_data; |
|
char ipfs_full_data_folder[PATH_MAX]; |
|
char ipfs_gateway_file[PATH_MAX]; |
|
struct stat st; |
|
int stat_ret = 0; |
|
int ret = AVERROR(EINVAL); |
|
FILE *gateway_file = NULL; |
|
char *env_ipfs_gateway, *env_ipfs_path; |
|
|
|
|
|
env_ipfs_gateway = getenv_utf8("IPFS_GATEWAY"); |
|
if (env_ipfs_gateway != NULL) { |
|
int printed = snprintf(c->gateway_buffer, sizeof(c->gateway_buffer), |
|
"%s", env_ipfs_gateway); |
|
freeenv_utf8(env_ipfs_gateway); |
|
if (printed >= sizeof(c->gateway_buffer)) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The IPFS_GATEWAY environment variable " |
|
"exceeds the maximum length. " |
|
"We allow a max of %zu characters\n", |
|
sizeof(c->gateway_buffer)); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
|
|
ret = 1; |
|
goto err; |
|
} else |
|
av_log(h, AV_LOG_DEBUG, "$IPFS_GATEWAY is empty.\n"); |
|
|
|
|
|
|
|
env_ipfs_path = getenv_utf8("IPFS_PATH"); |
|
if (env_ipfs_path == NULL) { |
|
int printed; |
|
char *env_home = getenv_utf8("HOME"); |
|
|
|
av_log(h, AV_LOG_DEBUG, "$IPFS_PATH is empty.\n"); |
|
|
|
|
|
if (env_home == NULL) { |
|
av_log(h, AV_LOG_WARNING, "$HOME appears to be empty.\n"); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
|
|
|
|
printed = snprintf( |
|
ipfs_full_data_folder, sizeof(ipfs_full_data_folder), |
|
"%s/.ipfs/", env_home); |
|
freeenv_utf8(env_home); |
|
if (printed >= sizeof(ipfs_full_data_folder)) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The IPFS data path exceeds the " |
|
"max path length (%zu)\n", |
|
sizeof(ipfs_full_data_folder)); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
|
|
|
|
|
|
stat_ret = stat(ipfs_full_data_folder, &st); |
|
|
|
if (stat_ret < 0) { |
|
av_log(h, AV_LOG_INFO, |
|
"Unable to find IPFS folder. We tried:\n" |
|
"- $IPFS_PATH, which was empty.\n" |
|
"- $HOME/.ipfs (full uri: %s) which doesn't exist.\n", |
|
ipfs_full_data_folder); |
|
ret = AVERROR(ENOENT); |
|
goto err; |
|
} |
|
} else { |
|
int printed = snprintf( |
|
ipfs_full_data_folder, sizeof(ipfs_full_data_folder), |
|
"%s", env_ipfs_path); |
|
freeenv_utf8(env_ipfs_path); |
|
if (printed >= sizeof(ipfs_full_data_folder)) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The IPFS_PATH environment variable " |
|
"exceeds the maximum length. " |
|
"We allow a max of %zu characters\n", |
|
sizeof(c->gateway_buffer)); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
} |
|
|
|
|
|
if (snprintf(ipfs_gateway_file, sizeof(ipfs_gateway_file), "%sgateway", |
|
ipfs_full_data_folder) |
|
>= sizeof(ipfs_gateway_file)) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The IPFS gateway file path exceeds " |
|
"the max path length (%zu)\n", |
|
sizeof(ipfs_gateway_file)); |
|
ret = AVERROR(ENOENT); |
|
goto err; |
|
} |
|
|
|
|
|
gateway_file = avpriv_fopen_utf8(ipfs_gateway_file, "r"); |
|
if (!gateway_file) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The IPFS gateway file (full uri: %s) doesn't exist. " |
|
"Is the gateway enabled?\n", |
|
ipfs_gateway_file); |
|
ret = AVERROR(ENOENT); |
|
goto err; |
|
} |
|
|
|
|
|
if (!fgets(c->gateway_buffer, sizeof(c->gateway_buffer) - 1, gateway_file)) { |
|
av_log(h, AV_LOG_WARNING, "Unable to read from file (full uri: %s).\n", |
|
ipfs_gateway_file); |
|
ret = AVERROR(ENOENT); |
|
goto err; |
|
} |
|
|
|
|
|
c->gateway_buffer[strcspn(c->gateway_buffer, "\r\n")] = 0; |
|
|
|
|
|
|
|
if (*c->gateway_buffer == '\0') { |
|
av_log(h, AV_LOG_WARNING, |
|
"The IPFS gateway file (full uri: %s) appears to be empty. " |
|
"Is the gateway started?\n", |
|
ipfs_gateway_file); |
|
ret = AVERROR(EILSEQ); |
|
goto err; |
|
} else { |
|
|
|
ret = 1; |
|
goto err; |
|
} |
|
|
|
err: |
|
if (gateway_file) |
|
fclose(gateway_file); |
|
|
|
return ret; |
|
} |
|
|
|
static int translate_ipfs_to_http(URLContext *h, const char *uri, int flags, AVDictionary **options) |
|
{ |
|
const char *ipfs_cid; |
|
char *fulluri = NULL; |
|
int ret; |
|
IPFSGatewayContext *c = h->priv_data; |
|
|
|
|
|
|
|
int is_ipfs = av_stristart(uri, "ipfs://", &ipfs_cid); |
|
int is_ipns = av_stristart(uri, "ipns://", &ipfs_cid); |
|
|
|
|
|
if (!is_ipfs && !is_ipns) { |
|
ret = AVERROR(EINVAL); |
|
av_log(h, AV_LOG_WARNING, "Unsupported url %s\n", uri); |
|
goto err; |
|
} |
|
|
|
|
|
|
|
|
|
if (strlen(ipfs_cid) < 1) { |
|
av_log(h, AV_LOG_WARNING, "A CID must be provided.\n"); |
|
ret = AVERROR(EILSEQ); |
|
goto err; |
|
} |
|
|
|
|
|
if (c->gateway != NULL) { |
|
if (snprintf(c->gateway_buffer, sizeof(c->gateway_buffer), "%s", |
|
c->gateway) |
|
>= sizeof(c->gateway_buffer)) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The -gateway parameter is too long. " |
|
"We allow a max of %zu characters\n", |
|
sizeof(c->gateway_buffer)); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
} else { |
|
|
|
|
|
ret = populate_ipfs_gateway(h); |
|
|
|
if (ret < 1) { |
|
av_log(h, AV_LOG_ERROR, |
|
"IPFS does not appear to be running.\n\n" |
|
"Installing IPFS locally is recommended to " |
|
"improve performance and reliability, " |
|
"and not share all your activity with a single IPFS gateway.\n" |
|
"There are multiple options to define this gateway.\n" |
|
"1. Call ffmpeg with a gateway param, " |
|
"without a trailing slash: -gateway <url>.\n" |
|
"2. Define an $IPFS_GATEWAY environment variable with the " |
|
"full HTTP URL to the gateway " |
|
"without trailing forward slash.\n" |
|
"3. Define an $IPFS_PATH environment variable " |
|
"and point it to the IPFS data path " |
|
"- this is typically ~/.ipfs\n"); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
} |
|
|
|
|
|
if (av_stristart(c->gateway_buffer, "http://", NULL) == 0 |
|
&& av_stristart(c->gateway_buffer, "https://", NULL) == 0) { |
|
av_log(h, AV_LOG_WARNING, |
|
"The gateway URL didn't start with http:// or " |
|
"https:// and is therefore invalid.\n"); |
|
ret = AVERROR(EILSEQ); |
|
goto err; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fulluri = av_asprintf("%s%s%s%s", |
|
c->gateway_buffer, |
|
(c->gateway_buffer[strlen(c->gateway_buffer) - 1] == '/') ? "" : "/", |
|
(is_ipns) ? "ipns/" : "ipfs/", |
|
ipfs_cid); |
|
|
|
if (!fulluri) { |
|
av_log(h, AV_LOG_ERROR, "Failed to compose the URL\n"); |
|
ret = AVERROR(ENOMEM); |
|
goto err; |
|
} |
|
|
|
|
|
ret = ffurl_open_whitelist(&c->inner, fulluri, flags, |
|
&h->interrupt_callback, options, |
|
h->protocol_whitelist, |
|
h->protocol_blacklist, h); |
|
if (ret < 0) { |
|
av_log(h, AV_LOG_WARNING, "Unable to open resource: %s\n", fulluri); |
|
goto err; |
|
} |
|
|
|
err: |
|
av_free(fulluri); |
|
return ret; |
|
} |
|
|
|
static int ipfs_read(URLContext *h, unsigned char *buf, int size) |
|
{ |
|
IPFSGatewayContext *c = h->priv_data; |
|
return ffurl_read(c->inner, buf, size); |
|
} |
|
|
|
static int64_t ipfs_seek(URLContext *h, int64_t pos, int whence) |
|
{ |
|
IPFSGatewayContext *c = h->priv_data; |
|
return ffurl_seek(c->inner, pos, whence); |
|
} |
|
|
|
static int ipfs_close(URLContext *h) |
|
{ |
|
IPFSGatewayContext *c = h->priv_data; |
|
return ffurl_closep(&c->inner); |
|
} |
|
|
|
#define OFFSET(x) offsetof(IPFSGatewayContext, x) |
|
|
|
static const AVOption options[] = { |
|
{"gateway", "The gateway to ask for IPFS data.", OFFSET(gateway), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, AV_OPT_FLAG_DECODING_PARAM}, |
|
{NULL}, |
|
}; |
|
|
|
static const AVClass ipfs_gateway_context_class = { |
|
.class_name = "IPFS Gateway", |
|
.item_name = av_default_item_name, |
|
.option = options, |
|
.version = LIBAVUTIL_VERSION_INT, |
|
}; |
|
|
|
const URLProtocol ff_ipfs_gateway_protocol = { |
|
.name = "ipfs", |
|
.url_open2 = translate_ipfs_to_http, |
|
.url_read = ipfs_read, |
|
.url_seek = ipfs_seek, |
|
.url_close = ipfs_close, |
|
.priv_data_size = sizeof(IPFSGatewayContext), |
|
.priv_data_class = &ipfs_gateway_context_class, |
|
}; |
|
|
|
const URLProtocol ff_ipns_gateway_protocol = { |
|
.name = "ipns", |
|
.url_open2 = translate_ipfs_to_http, |
|
.url_read = ipfs_read, |
|
.url_seek = ipfs_seek, |
|
.url_close = ipfs_close, |
|
.priv_data_size = sizeof(IPFSGatewayContext), |
|
.priv_data_class = &ipfs_gateway_context_class, |
|
}; |
|
|