aboutsummaryrefslogtreecommitdiffstats
path: root/libavformat/ipfsgateway.c
blob: ce69d9055a5d161ec7416fcfe2c6f9c8f84fc35c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*
 * 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/file_open.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,
};