mirror of https://github.com/FFmpeg/FFmpeg.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
12 KiB
347 lines
12 KiB
/* |
|
* 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/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; |
|
|
|
// Test $IPFS_GATEWAY. |
|
if (getenv("IPFS_GATEWAY") != NULL) { |
|
if (snprintf(c->gateway_buffer, sizeof(c->gateway_buffer), "%s", |
|
getenv("IPFS_GATEWAY")) |
|
>= 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. |
|
if (getenv("IPFS_PATH") == NULL) { |
|
av_log(h, AV_LOG_DEBUG, "$IPFS_PATH is empty.\n"); |
|
|
|
// Try via the home folder. |
|
if (getenv("HOME") == NULL) { |
|
av_log(h, AV_LOG_WARNING, "$HOME appears to be empty.\n"); |
|
ret = AVERROR(EINVAL); |
|
goto err; |
|
} |
|
|
|
// Verify the composed path fits. |
|
if (snprintf(ipfs_full_data_folder, sizeof(ipfs_full_data_folder), |
|
"%s/.ipfs/", getenv("HOME")) |
|
>= 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 { |
|
if (snprintf(ipfs_full_data_folder, sizeof(ipfs_full_data_folder), "%s", |
|
getenv("IPFS_PATH")) |
|
>= 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) { |
|
// We fallback on dweb.link (managed by Protocol Labs). |
|
snprintf(c->gateway_buffer, sizeof(c->gateway_buffer), "https://dweb.link"); |
|
|
|
av_log(h, AV_LOG_WARNING, |
|
"IPFS does not appear to be running. " |
|
"You’re now using the public gateway at dweb.link.\n"); |
|
av_log(h, AV_LOG_INFO, |
|
"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"); |
|
} |
|
} |
|
|
|
// 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, |
|
};
|
|
|