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