ffmpeg/libavformat/ipfsgateway.c

357 lines
12 KiB
C

/*
* IPFS and IPNS protocol support through IPFS Gateway.
* Copyright (c) 2022 Mark Gaiser
*
* This file is part of FFmpeg.
*
* FFmpeg 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.1 of the License, or (at your option) any later version.
*
* FFmpeg 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 FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "libavutil/avstring.h"
#include "libavutil/getenv_utf8.h"
#include "libavutil/opt.h"
#include <sys/stat.h>
#include "os_support.h"
#include "url.h"
// Define the posix PATH_MAX if not there already.
// This fixes a compile issue for MSVC.
#ifndef PATH_MAX
#define PATH_MAX 4096
#endif
typedef struct IPFSGatewayContext {
AVClass *class;
URLContext *inner;
// Is filled by the -gateway argument and not changed after.
char *gateway;
// If the above gateway is non null, it will be copied into this buffer.
// Else this buffer will contain the auto detected gateway.
// In either case, the gateway to use will be in this buffer.
char gateway_buffer[PATH_MAX];
} IPFSGatewayContext;
// A best-effort way to find the IPFS gateway.
// Only the most appropiate gateway is set. It's not actually requested
// (http call) to prevent a potential slowdown in startup. A potential timeout
// is handled by the HTTP protocol.
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;
// Test $IPFS_GATEWAY.
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");
// We need to know the IPFS folder to - eventually - read the contents of
// the "gateway" file which would tell us the gateway to use.
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");
// Try via the home folder.
if (env_home == NULL) {
av_log(h, AV_LOG_WARNING, "$HOME appears to be empty.\n");
ret = AVERROR(EINVAL);
goto err;
}
// Verify the composed path fits.
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 the folder.
// It should exist in a default IPFS setup when run as local user.
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;
}
}
// Copy the fully composed gateway path into ipfs_gateway_file.
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;
}
// Get the contents of the gateway file.
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;
}
// Read a single line (fgets stops at new line mark).
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;
}
// Replace first occurence of end of line with \0
c->gateway_buffer[strcspn(c->gateway_buffer, "\r\n")] = 0;
// If strlen finds anything longer then 0 characters then we have a
// potential gateway url.
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 {
// We're done, the c->gateway_buffer has something that looks valid.
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;
// Test for ipfs://, ipfs:, ipns:// and ipns:. This prefix is stripped from
// the string leaving just the CID in ipfs_cid.
int is_ipfs = av_stristart(uri, "ipfs://", &ipfs_cid);
int is_ipns = av_stristart(uri, "ipns://", &ipfs_cid);
// We must have either ipns or ipfs.
if (!is_ipfs && !is_ipns) {
ret = AVERROR(EINVAL);
av_log(h, AV_LOG_WARNING, "Unsupported url %s\n", uri);
goto err;
}
// If the CID has a length greater then 0 then we assume we have a proper working one.
// It could still be wrong but in that case the gateway should save us and
// ruturn a 403 error. The http protocol handles this.
if (strlen(ipfs_cid) < 1) {
av_log(h, AV_LOG_WARNING, "A CID must be provided.\n");
ret = AVERROR(EILSEQ);
goto err;
}
// Populate c->gateway_buffer with whatever is in c->gateway
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 {
// Populate the IPFS gateway if we have any.
// If not, inform the user how to properly set one.
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;
}
}
// Test if the gateway starts with either http:// or https://
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;
}
// Concatenate the url.
// This ends up with something like: http://localhost:8080/ipfs/Qm.....
// The format of "%s%s%s%s" is the following:
// 1st %s = The gateway.
// 2nd %s = If the gateway didn't end in a slash, add a "/". Otherwise it's an empty string
// 3rd %s = Either ipns/ or ipfs/.
// 4th %s = The IPFS CID (Qm..., bafy..., ...).
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;
}
// Pass the URL back to FFMpeg's protocol handler.
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_context_class = {
.class_name = "IPFS",
.item_name = av_default_item_name,
.option = options,
.version = LIBAVUTIL_VERSION_INT,
};
const URLProtocol ff_ipfs_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_context_class,
};
const URLProtocol ff_ipns_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_context_class,
};