• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * IPFS and IPNS protocol support through IPFS Gateway.
3  * Copyright (c) 2022 Mark Gaiser
4  *
5  * This file is part of FFmpeg.
6  *
7  * FFmpeg is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * FFmpeg is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with FFmpeg; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20  */
21 
22 #include "libavutil/avstring.h"
23 #include "libavutil/getenv_utf8.h"
24 #include "libavutil/opt.h"
25 #include <sys/stat.h>
26 #include "os_support.h"
27 #include "url.h"
28 
29 // Define the posix PATH_MAX if not there already.
30 // This fixes a compile issue for MSVC.
31 #ifndef PATH_MAX
32 #define PATH_MAX 4096
33 #endif
34 
35 typedef struct IPFSGatewayContext {
36     AVClass *class;
37     URLContext *inner;
38     // Is filled by the -gateway argument and not changed after.
39     char *gateway;
40     // If the above gateway is non null, it will be copied into this buffer.
41     // Else this buffer will contain the auto detected gateway.
42     // In either case, the gateway to use will be in this buffer.
43     char gateway_buffer[PATH_MAX];
44 } IPFSGatewayContext;
45 
46 // A best-effort way to find the IPFS gateway.
47 // Only the most appropiate gateway is set. It's not actually requested
48 // (http call) to prevent a potential slowdown in startup. A potential timeout
49 // is handled by the HTTP protocol.
populate_ipfs_gateway(URLContext * h)50 static int populate_ipfs_gateway(URLContext *h)
51 {
52     IPFSGatewayContext *c = h->priv_data;
53     char ipfs_full_data_folder[PATH_MAX];
54     char ipfs_gateway_file[PATH_MAX];
55     struct stat st;
56     int stat_ret = 0;
57     int ret = AVERROR(EINVAL);
58     FILE *gateway_file = NULL;
59     char *env_ipfs_gateway, *env_ipfs_path;
60 
61     // Test $IPFS_GATEWAY.
62     env_ipfs_gateway = getenv_utf8("IPFS_GATEWAY");
63     if (env_ipfs_gateway != NULL) {
64         int printed = snprintf(c->gateway_buffer, sizeof(c->gateway_buffer),
65                                "%s", env_ipfs_gateway);
66         freeenv_utf8(env_ipfs_gateway);
67         if (printed >= sizeof(c->gateway_buffer)) {
68             av_log(h, AV_LOG_WARNING,
69                    "The IPFS_GATEWAY environment variable "
70                    "exceeds the maximum length. "
71                    "We allow a max of %zu characters\n",
72                    sizeof(c->gateway_buffer));
73             ret = AVERROR(EINVAL);
74             goto err;
75         }
76 
77         ret = 1;
78         goto err;
79     } else
80         av_log(h, AV_LOG_DEBUG, "$IPFS_GATEWAY is empty.\n");
81 
82     // We need to know the IPFS folder to - eventually - read the contents of
83     // the "gateway" file which would tell us the gateway to use.
84     env_ipfs_path = getenv_utf8("IPFS_PATH");
85     if (env_ipfs_path == NULL) {
86         int printed;
87         char *env_home = getenv_utf8("HOME");
88 
89         av_log(h, AV_LOG_DEBUG, "$IPFS_PATH is empty.\n");
90 
91         // Try via the home folder.
92         if (env_home == NULL) {
93             av_log(h, AV_LOG_WARNING, "$HOME appears to be empty.\n");
94             ret = AVERROR(EINVAL);
95             goto err;
96         }
97 
98         // Verify the composed path fits.
99         printed = snprintf(
100             ipfs_full_data_folder, sizeof(ipfs_full_data_folder),
101             "%s/.ipfs/", env_home);
102         freeenv_utf8(env_home);
103         if (printed >= sizeof(ipfs_full_data_folder)) {
104             av_log(h, AV_LOG_WARNING,
105                    "The IPFS data path exceeds the "
106                    "max path length (%zu)\n",
107                    sizeof(ipfs_full_data_folder));
108             ret = AVERROR(EINVAL);
109             goto err;
110         }
111 
112         // Stat the folder.
113         // It should exist in a default IPFS setup when run as local user.
114         stat_ret = stat(ipfs_full_data_folder, &st);
115 
116         if (stat_ret < 0) {
117             av_log(h, AV_LOG_INFO,
118                    "Unable to find IPFS folder. We tried:\n"
119                    "- $IPFS_PATH, which was empty.\n"
120                    "- $HOME/.ipfs (full uri: %s) which doesn't exist.\n",
121                    ipfs_full_data_folder);
122             ret = AVERROR(ENOENT);
123             goto err;
124         }
125     } else {
126         int printed = snprintf(
127             ipfs_full_data_folder, sizeof(ipfs_full_data_folder),
128             "%s", env_ipfs_path);
129         freeenv_utf8(env_ipfs_path);
130         if (printed >= sizeof(ipfs_full_data_folder)) {
131             av_log(h, AV_LOG_WARNING,
132                    "The IPFS_PATH environment variable "
133                    "exceeds the maximum length. "
134                    "We allow a max of %zu characters\n",
135                    sizeof(c->gateway_buffer));
136             ret = AVERROR(EINVAL);
137             goto err;
138         }
139     }
140 
141     // Copy the fully composed gateway path into ipfs_gateway_file.
142     if (snprintf(ipfs_gateway_file, sizeof(ipfs_gateway_file), "%sgateway",
143                  ipfs_full_data_folder)
144         >= sizeof(ipfs_gateway_file)) {
145         av_log(h, AV_LOG_WARNING,
146                "The IPFS gateway file path exceeds "
147                "the max path length (%zu)\n",
148                sizeof(ipfs_gateway_file));
149         ret = AVERROR(ENOENT);
150         goto err;
151     }
152 
153     // Get the contents of the gateway file.
154     gateway_file = avpriv_fopen_utf8(ipfs_gateway_file, "r");
155     if (!gateway_file) {
156         av_log(h, AV_LOG_WARNING,
157                "The IPFS gateway file (full uri: %s) doesn't exist. "
158                "Is the gateway enabled?\n",
159                ipfs_gateway_file);
160         ret = AVERROR(ENOENT);
161         goto err;
162     }
163 
164     // Read a single line (fgets stops at new line mark).
165     if (!fgets(c->gateway_buffer, sizeof(c->gateway_buffer) - 1, gateway_file)) {
166         av_log(h, AV_LOG_WARNING, "Unable to read from file (full uri: %s).\n",
167                ipfs_gateway_file);
168         ret = AVERROR(ENOENT);
169         goto err;
170     }
171 
172     // Replace first occurence of end of line with \0
173     c->gateway_buffer[strcspn(c->gateway_buffer, "\r\n")] = 0;
174 
175     // If strlen finds anything longer then 0 characters then we have a
176     // potential gateway url.
177     if (*c->gateway_buffer == '\0') {
178         av_log(h, AV_LOG_WARNING,
179                "The IPFS gateway file (full uri: %s) appears to be empty. "
180                "Is the gateway started?\n",
181                ipfs_gateway_file);
182         ret = AVERROR(EILSEQ);
183         goto err;
184     } else {
185         // We're done, the c->gateway_buffer has something that looks valid.
186         ret = 1;
187         goto err;
188     }
189 
190 err:
191     if (gateway_file)
192         fclose(gateway_file);
193 
194     return ret;
195 }
196 
translate_ipfs_to_http(URLContext * h,const char * uri,int flags,AVDictionary ** options)197 static int translate_ipfs_to_http(URLContext *h, const char *uri, int flags, AVDictionary **options)
198 {
199     const char *ipfs_cid;
200     char *fulluri = NULL;
201     int ret;
202     IPFSGatewayContext *c = h->priv_data;
203 
204     // Test for ipfs://, ipfs:, ipns:// and ipns:. This prefix is stripped from
205     // the string leaving just the CID in ipfs_cid.
206     int is_ipfs = av_stristart(uri, "ipfs://", &ipfs_cid);
207     int is_ipns = av_stristart(uri, "ipns://", &ipfs_cid);
208 
209     // We must have either ipns or ipfs.
210     if (!is_ipfs && !is_ipns) {
211         ret = AVERROR(EINVAL);
212         av_log(h, AV_LOG_WARNING, "Unsupported url %s\n", uri);
213         goto err;
214     }
215 
216     // If the CID has a length greater then 0 then we assume we have a proper working one.
217     // It could still be wrong but in that case the gateway should save us and
218     // ruturn a 403 error. The http protocol handles this.
219     if (strlen(ipfs_cid) < 1) {
220         av_log(h, AV_LOG_WARNING, "A CID must be provided.\n");
221         ret = AVERROR(EILSEQ);
222         goto err;
223     }
224 
225     // Populate c->gateway_buffer with whatever is in c->gateway
226     if (c->gateway != NULL) {
227         if (snprintf(c->gateway_buffer, sizeof(c->gateway_buffer), "%s",
228                      c->gateway)
229             >= sizeof(c->gateway_buffer)) {
230             av_log(h, AV_LOG_WARNING,
231                    "The -gateway parameter is too long. "
232                    "We allow a max of %zu characters\n",
233                    sizeof(c->gateway_buffer));
234             ret = AVERROR(EINVAL);
235             goto err;
236         }
237     } else {
238         // Populate the IPFS gateway if we have any.
239         // If not, inform the user how to properly set one.
240         ret = populate_ipfs_gateway(h);
241 
242         if (ret < 1) {
243             av_log(h, AV_LOG_ERROR,
244                    "IPFS does not appear to be running.\n\n"
245                    "Installing IPFS locally is recommended to "
246                    "improve performance and reliability, "
247                    "and not share all your activity with a single IPFS gateway.\n"
248                    "There are multiple options to define this gateway.\n"
249                    "1. Call ffmpeg with a gateway param, "
250                    "without a trailing slash: -gateway <url>.\n"
251                    "2. Define an $IPFS_GATEWAY environment variable with the "
252                    "full HTTP URL to the gateway "
253                    "without trailing forward slash.\n"
254                    "3. Define an $IPFS_PATH environment variable "
255                    "and point it to the IPFS data path "
256                    "- this is typically ~/.ipfs\n");
257             ret = AVERROR(EINVAL);
258             goto err;
259         }
260     }
261 
262     // Test if the gateway starts with either http:// or https://
263     if (av_stristart(c->gateway_buffer, "http://", NULL) == 0
264         && av_stristart(c->gateway_buffer, "https://", NULL) == 0) {
265         av_log(h, AV_LOG_WARNING,
266                "The gateway URL didn't start with http:// or "
267                "https:// and is therefore invalid.\n");
268         ret = AVERROR(EILSEQ);
269         goto err;
270     }
271 
272     // Concatenate the url.
273     // This ends up with something like: http://localhost:8080/ipfs/Qm.....
274     // The format of "%s%s%s%s" is the following:
275     // 1st %s = The gateway.
276     // 2nd %s = If the gateway didn't end in a slash, add a "/". Otherwise it's an empty string
277     // 3rd %s = Either ipns/ or ipfs/.
278     // 4th %s = The IPFS CID (Qm..., bafy..., ...).
279     fulluri = av_asprintf("%s%s%s%s",
280                           c->gateway_buffer,
281                           (c->gateway_buffer[strlen(c->gateway_buffer) - 1] == '/') ? "" : "/",
282                           (is_ipns) ? "ipns/" : "ipfs/",
283                           ipfs_cid);
284 
285     if (!fulluri) {
286         av_log(h, AV_LOG_ERROR, "Failed to compose the URL\n");
287         ret = AVERROR(ENOMEM);
288         goto err;
289     }
290 
291     // Pass the URL back to FFMpeg's protocol handler.
292     ret = ffurl_open_whitelist(&c->inner, fulluri, flags,
293                                &h->interrupt_callback, options,
294                                h->protocol_whitelist,
295                                h->protocol_blacklist, h);
296     if (ret < 0) {
297         av_log(h, AV_LOG_WARNING, "Unable to open resource: %s\n", fulluri);
298         goto err;
299     }
300 
301 err:
302     av_free(fulluri);
303     return ret;
304 }
305 
ipfs_read(URLContext * h,unsigned char * buf,int size)306 static int ipfs_read(URLContext *h, unsigned char *buf, int size)
307 {
308     IPFSGatewayContext *c = h->priv_data;
309     return ffurl_read(c->inner, buf, size);
310 }
311 
ipfs_seek(URLContext * h,int64_t pos,int whence)312 static int64_t ipfs_seek(URLContext *h, int64_t pos, int whence)
313 {
314     IPFSGatewayContext *c = h->priv_data;
315     return ffurl_seek(c->inner, pos, whence);
316 }
317 
ipfs_close(URLContext * h)318 static int ipfs_close(URLContext *h)
319 {
320     IPFSGatewayContext *c = h->priv_data;
321     return ffurl_closep(&c->inner);
322 }
323 
324 #define OFFSET(x) offsetof(IPFSGatewayContext, x)
325 
326 static const AVOption options[] = {
327     {"gateway", "The gateway to ask for IPFS data.", OFFSET(gateway), AV_OPT_TYPE_STRING, {.str = NULL}, 0, 0, AV_OPT_FLAG_DECODING_PARAM},
328     {NULL},
329 };
330 
331 static const AVClass ipfs_context_class = {
332     .class_name     = "IPFS",
333     .item_name      = av_default_item_name,
334     .option         = options,
335     .version        = LIBAVUTIL_VERSION_INT,
336 };
337 
338 const URLProtocol ff_ipfs_protocol = {
339     .name               = "ipfs",
340     .url_open2          = translate_ipfs_to_http,
341     .url_read           = ipfs_read,
342     .url_seek           = ipfs_seek,
343     .url_close          = ipfs_close,
344     .priv_data_size     = sizeof(IPFSGatewayContext),
345     .priv_data_class    = &ipfs_context_class,
346 };
347 
348 const URLProtocol ff_ipns_protocol = {
349     .name               = "ipns",
350     .url_open2          = translate_ipfs_to_http,
351     .url_read           = ipfs_read,
352     .url_seek           = ipfs_seek,
353     .url_close          = ipfs_close,
354     .priv_data_size     = sizeof(IPFSGatewayContext),
355     .priv_data_class    = &ipfs_context_class,
356 };
357