From b6dc646e567a2d0107c2e7e3bc05e13cacd6e0bc Mon Sep 17 00:00:00 2001 From: Mark Gaiser Date: Sat, 7 Oct 2023 17:19:39 +0200 Subject: [PATCH 1/9] Blog post for IPFS URI support in CURL --- src/_blog/ipfs-uri-support-in-curl.md | 120 ++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/_blog/ipfs-uri-support-in-curl.md diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md new file mode 100644 index 00000000..9119974c --- /dev/null +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -0,0 +1,120 @@ +--- +title: IPFS URI support in CURL +description: 'CURL 8.4.0 shipped with built-in support for ipfs:// and ipns:// addresses.' +author: Mark Gaiser +date: 2023-10-7 +permalink: '/ipfs-uri-support-in-curl/' +header_image: '/ipfs-calendar/ipfs-calendar-cover.png' +tags: + - 'community' +--- + +# IPFS URI support in CURL + +[CURL 8.4.0](https://github.com/curl/curl/releases/tag/curl-8_4_0) shipped with built-in support for ipfs:// and ipns:// addresses. It implements conventions from [IPIP-280](https://github.com/ipfs/specs/pull/280) and will respect user's gateway choice made via `IPFS_GATEWAY` environment variable or presence of `gateway` file. + +In this blog post we'll go over a brief history of implementing support for IPFS URIs in CURL. Why this is an important feature to have and what you can do with it. We'll see how distributed data access can be clean and we'll find out when we can use this. + +## A brief history +Supporting IPFS in CURL has been attempted [before](https://github.com/curl/curl/pull/8468) as a CURL library feature. Some discussions lead to a belief that this should be implemented in the CURL tool itself, not it's library. A renewed [implementation attempt](https://github.com/curl/curl/pull/8805) took the tool-side approach which ultimately was accepted and is available right now in CURL 8.4.0! + +The support of IPFS in CURL is effectively consisting of two implementation details. + +1. CURL tries to find a locally installed or [configured gateway](#how-does-curl-find-an-ipfs-gateway). +2. It then rewrites an `ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` to a gateway URL. This is how curl handles it internally, you see nothing of this URL rewriting. + +If you have IPFS installed locally then running: +`curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` +will just work. + +## Why ipfs:// URI support is so important? + +This question keeps coming up. Why-o-why do we find it so important to have IPFS support in CURL, even if it's just a fancy URL rewriter? + +Why isn't `https://ipfs.io/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` equally acceptable? Or why isn't a local url like `http://localhost:8080/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` fine? + +I'll have to repeat one of the core concepts of IPFS here. IPFS is a distributed network in which you access content. It shouldn't matter where that content is. The "where" part should not be provided. If you do proovide this where part (a gateway is a central point of entry, the where part) then you limit your access to IPFS through that one point of entry. + +If we pull the URL apart we see: + +![](https://hackmd.io/_uploads/Bk2MV-9ea.png) + +As a user of IPFS you should not care about the **where** part because you're now limited to however available that central access point is. + +An example of a direct limitation. There are gateways out there that don't allow playback of video through their gateway or that bandwidth-throttle such content. This isn't a limitation of IPFS but is purely a limitation a gateway has set through custom configuration. You have no such limitation if you were to be using your own local node with a local gateway. + +Another example. Say you use `ipfs.io` as gateway. Now if that gateway starts throttling users, or gets sensored by ISPs then to you it looks like "IPFS isn't working" while the data might be well accessible, just not momentarily via that gateway. Even though gateways like `ipfs.io` and `dweb.link` are themselves connected to vast numbers of peers, it's worth nothing if the http gateway endpoint has a momentarily hiccup. Beyond the connectivity aspect, a gateway is hosted by someone. If you use a gateway you use their resources and bandwidth! + +This is why running a local node (and therefore a local gateway, it's part of a node) is so important. Even though you still effectively use `http://localhost:8080` as gateway, it's hosted by you locally backed by the many peers your node is connected with. Your experience in using IPFS is going to be best and fastest with a local node. Even when your local gateway isn't working it's easy for you to restart your node and get that gateway back and running. You can't do that on public gateways that you don't control. + +One of the many reasons why we're putting in the effort to make applications recognize IPFS URIs (like [ffmpeg](https://blog.ipfs.tech/2022-08-01-ipfs-and-ffmpeg/)) `ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` is to let the application in the background find that gateway you're running and giving you the freedom of being truly distributed! This also allows url's to be shared as IPFS url's (like `ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi`) without any trace of a (central) gateway and bring us one step closer to a distributed world where it doesn't matter anymore where that data is located. + +## How does CURL find an IPFS Gateway? + +Any IPFS implementation that has support for [IPIP-280](https://github.com/ipfs/specs/pull/280) exposes an IPFS gateway that CURL (and [ffmpeg](https://blog.ipfs.tech/2022-08-01-ipfs-and-ffmpeg/)) can use. At the moment of writing that's just [Kubo](https://github.com/ipfs/kubo/releases). + +CURL 8.4.0 and greater looks for a gateway in the following order: + +1. `IPFS_GATEWAY`, if set it's used. +2. The `--ipfs-gateway` argument. +3. The `~/.ipfs/gateway` file where it reads the first line. + +If a gateway is found at any of those places, and if that is a valid http(s) URL then CURL will use it. If not then you'll be getting an arror message pointing to the [CURL documentation related to IPFS](https://curl.se/docs/ipfs.html) to help you further. + +Note that you can just specify any gateway in any of these places, it's highly recommended to use a local gateway! + +## Malicious gateways and data integrity? + +Technically it's possible that a gateway is malicious. Or that a known well used gateway is hacked and changed to give you data that is malicious. How do you protect against that? + +The easiest fix - by far - is to run your own local node that has support for [IPIP-280](https://github.com/ipfs/specs/pull/280). Every block of data you get though your own local node is verified internally in Kubo. Therefore, if you want the most flexibility and security, run that node! + +Alternativly if you want to use a gateway that you can't trust, use CAR archives instead. CAR allows you to locally verify if the data you get is the data you expect. How to use CAR and verify your data is beyond the scope of this blog, but [here's](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval) a good starting point. + +## CURL Examples + +Technically nothing is truly new here as everything is already possible with full URL's. +There's one caveat to keep in mind with CURL usage. The implementation in curl assumes the [path-style](https://docs.ipfs.tech/concepts/ipfs-gateway/#path) gateway. If your gateway redirects to [subdomain-style](https://docs.ipfs.tech/concepts/ipfs-gateway/#subdomain) (like `https://dweb.link` and the gateway provided by your local Kubo node) then you have to pass `-L` in the curl command to follow that redirect. + +### Playing Big Buck Bunny (CURL + ffplay) +``` +curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | ffplay - +``` + +### Downloading a file from IPFS with CURL +``` +curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi -o bbb.webm +``` + +### Explicitly specifying a gateway +``` +IPFS_GATEWAY=http://localhost:8080 curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi +``` + +### Handling redirects +You have to explicitly tell curl to handle redirects for security reasons. +``` +curl -L ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi +``` + +### Piping data and follow redirect +In this case the data is piped to the `file` utility in linux. We need to tell curl to be silent `-s` and to follow the redirects `-L`: +``` +IPFS_GATEWAY=https://dweb.link curl -s -L ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | file - +``` + +Which tells us: +``` +/dev/stdin: WebM +``` + +## What's next? + +integration. Everyone can integrate `ipfs://` and `ipns://` URI support into their application. See [IPIP-280](https://github.com/ipfs/specs/pull/280) for details. We are [tracking potential project](https://github.com/ipfs/integrations/issues) where an integration makes sense! If you feel up to the challenge, don't hesitate to drop a comment in one of the [potential projects](https://github.com/ipfs/integrations/issues) for IPFS URI integration or find us on: + +* [Slack](https://filecoin.io/slack) +* [Discord](https://discord.com/invite/ipfs) +* [Forum](https://discuss.ipfs.tech/) + +Or one of the other many places where the [IPFS comunity](https://docs.ipfs.tech/community/) is active. + From afa256da3bab1797b9311916feea1976ae603979 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 9 Oct 2023 00:49:59 +0200 Subject: [PATCH 2/9] curl: improve summary and malicious gw sections first stab at making initial summary more engaging and describing best practices around verifiable responses --- src/_blog/ipfs-uri-support-in-curl.md | 43 +++++++++++++++++++-------- 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 9119974c..6141f906 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -11,11 +11,24 @@ tags: # IPFS URI support in CURL -[CURL 8.4.0](https://github.com/curl/curl/releases/tag/curl-8_4_0) shipped with built-in support for ipfs:// and ipns:// addresses. It implements conventions from [IPIP-280](https://github.com/ipfs/specs/pull/280) and will respect user's gateway choice made via `IPFS_GATEWAY` environment variable or presence of `gateway` file. +[CURL 8.4.0](https://github.com/curl/curl/releases/tag/curl-8_4_0) shipped with built-in support for `ipfs://` and `ipns://` addresses. -In this blog post we'll go over a brief history of implementing support for IPFS URIs in CURL. Why this is an important feature to have and what you can do with it. We'll see how distributed data access can be clean and we'll find out when we can use this. +This enables `curl` to seamlessly integrate with the user's preferred [IPFS gateway](https://docs.ipfs.tech/reference/http/gateway/) through the `IPFS_GATEWAY` environment variable or a `gateway` file. Best of all, these capabilities are available for immediate use today: + +```bash +$ export IPFS_GATEWAY="http://127.0.0.1:8080" # local gateway provided by ipfs daemon like Kubo +$ curl ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey -s -L +hello from IPFS +``` + +In this blog post, we will: +- explore the journey of implementing IPFS URI support in CURL, +- delve into the mechanics of [how CURL locates an IPFS gateway](#how-does-curl-find-an-ipfs-gateway), +- learn how to be immune to [malicious gateways](#malicious-gateways-and-data-integrity), +- and finally, provide [practical CURL examples](#curl-examples) for leveraging IPFS URIs for either deserialized or verifiable responses. ## A brief history + Supporting IPFS in CURL has been attempted [before](https://github.com/curl/curl/pull/8468) as a CURL library feature. Some discussions lead to a belief that this should be implemented in the CURL tool itself, not it's library. A renewed [implementation attempt](https://github.com/curl/curl/pull/8805) took the tool-side approach which ultimately was accepted and is available right now in CURL 8.4.0! The support of IPFS in CURL is effectively consisting of two implementation details. @@ -30,7 +43,7 @@ will just work. ## Why ipfs:// URI support is so important? This question keeps coming up. Why-o-why do we find it so important to have IPFS support in CURL, even if it's just a fancy URL rewriter? - + Why isn't `https://ipfs.io/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` equally acceptable? Or why isn't a local url like `http://localhost:8080/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` fine? I'll have to repeat one of the core concepts of IPFS here. IPFS is a distributed network in which you access content. It shouldn't matter where that content is. The "where" part should not be provided. If you do proovide this where part (a gateway is a central point of entry, the where part) then you limit your access to IPFS through that one point of entry. @@ -65,16 +78,22 @@ Note that you can just specify any gateway in any of these places, it's highly r ## Malicious gateways and data integrity? -Technically it's possible that a gateway is malicious. Or that a known well used gateway is hacked and changed to give you data that is malicious. How do you protect against that? +Requesting deserialized responses and delegating hash verification to a third-party gateway comes with risks. It is possible that a public gateway is malicious. Or, that a well-known and respected gateway gets hacked and changed to return payload that does not match requested CID. How can one protect themselves against that? -The easiest fix - by far - is to run your own local node that has support for [IPIP-280](https://github.com/ipfs/specs/pull/280). Every block of data you get though your own local node is verified internally in Kubo. Therefore, if you want the most flexibility and security, run that node! +If deserialized responses are necessary, one should run own gateway in a local, controlled environment. Every block of data retrieved though self-hosted IPFS gateway is verified to match the hash from CID. For the maximum flexibility and security, find implementation that provides the gateway endpoint (i.e. [Kubo](https://docs.ipfs.tech/install/command-line/)) and run it yourself! -Alternativly if you want to use a gateway that you can't trust, use CAR archives instead. CAR allows you to locally verify if the data you get is the data you expect. How to use CAR and verify your data is beyond the scope of this blog, but [here's](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval) a good starting point. +When using a third-party gateway that one can't fully trust, the only secure option is to [request verifiable response types](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval) such as [application/vnd.ipld.raw](https://www.iana.org/assignments/media-types/application/vnd.ipld.raw) (a single block) or [application/vnd.ipld.car](https://www.iana.org/assignments/media-types/application/vnd.ipld.car) (multiple blocks in CAR archive). Both allow to locally verify if the data returned by gateway match the requested CID, removing the surface for [Man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). ## CURL Examples -Technically nothing is truly new here as everything is already possible with full URL's. -There's one caveat to keep in mind with CURL usage. The implementation in curl assumes the [path-style](https://docs.ipfs.tech/concepts/ipfs-gateway/#path) gateway. If your gateway redirects to [subdomain-style](https://docs.ipfs.tech/concepts/ipfs-gateway/#subdomain) (like `https://dweb.link` and the gateway provided by your local Kubo node) then you have to pass `-L` in the curl command to follow that redirect. +::: callout + +**NOTE on HTTP redirects** + +The URI resolution in `curl` does not follow redirects by default and assumes the endpoint implements deserializing [path gateway](https://specs.ipfs.tech/http-gateways/path-gateway/) or at the very least, the [trustless gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/). +When pointing `curl` at a [subdomain gateway](https://specs.ipfs.tech/http-gateways/subdomain-gateway) (like `https://dweb.link` or the `http://localhost:8080` provided by a local Kubo node) one has to pass `-L` in the curl command to follow the redirect. + +::: ### Playing Big Buck Bunny (CURL + ffplay) ``` @@ -82,24 +101,24 @@ curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | ffplay ``` ### Downloading a file from IPFS with CURL -``` +```bash curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi -o bbb.webm ``` ### Explicitly specifying a gateway -``` +```bash IPFS_GATEWAY=http://localhost:8080 curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi ``` ### Handling redirects You have to explicitly tell curl to handle redirects for security reasons. -``` +```bash curl -L ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi ``` ### Piping data and follow redirect In this case the data is piped to the `file` utility in linux. We need to tell curl to be silent `-s` and to follow the redirects `-L`: -``` +```bash IPFS_GATEWAY=https://dweb.link curl -s -L ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | file - ``` From 5d4507831d2fa010a27f1606a68e83b31aad4ec4 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Thu, 12 Oct 2023 17:50:13 +0200 Subject: [PATCH 3/9] chore: cosmetics --- src/_blog/ipfs-uri-support-in-curl.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 6141f906..136abfdd 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -7,6 +7,9 @@ permalink: '/ipfs-uri-support-in-curl/' header_image: '/ipfs-calendar/ipfs-calendar-cover.png' tags: - 'community' + - 'URI' + - 'HTTP' + - 'curl' --- # IPFS URI support in CURL @@ -69,12 +72,12 @@ Any IPFS implementation that has support for [IPIP-280](https://github.com/ipfs/ CURL 8.4.0 and greater looks for a gateway in the following order: 1. `IPFS_GATEWAY`, if set it's used. -2. The `--ipfs-gateway` argument. -3. The `~/.ipfs/gateway` file where it reads the first line. +2. The `--ipfs-gateway` CLI argument. +3. The `~/.ipfs/gateway` file, where it reads the first line. -If a gateway is found at any of those places, and if that is a valid http(s) URL then CURL will use it. If not then you'll be getting an arror message pointing to the [CURL documentation related to IPFS](https://curl.se/docs/ipfs.html) to help you further. +If a gateway hint is found at any of those places, and if that is a valid HTTP URL then CURL will use it. If not then you'll be getting an error message pointing to the [CURL documentation related to IPFS](https://curl.se/docs/ipfs.html) to help you further. -Note that you can just specify any gateway in any of these places, it's highly recommended to use a local gateway! +One can specify any IPFS gateway that is in compliance with [Gateway Specifications](https://specs.ipfs.tech/http-gateways/). It is highly recommended using a local gateway, as it provides the best security guarantees. ## Malicious gateways and data integrity? @@ -129,11 +132,10 @@ Which tells us: ## What's next? -integration. Everyone can integrate `ipfs://` and `ipns://` URI support into their application. See [IPIP-280](https://github.com/ipfs/specs/pull/280) for details. We are [tracking potential project](https://github.com/ipfs/integrations/issues) where an integration makes sense! If you feel up to the challenge, don't hesitate to drop a comment in one of the [potential projects](https://github.com/ipfs/integrations/issues) for IPFS URI integration or find us on: +More places supporting IPFS addresses. Everyone can integrate `ipfs://` and `ipns://` URI support into their application. See specifications proposed in [IPIP-280](https://github.com/ipfs/specs/pull/280) for technical details. We are [tracking potential project](https://github.com/ipfs/integrations/issues) where an integration makes sense! If you feel up to the challenge, don't hesitate to drop a comment in one of the [potential projects](https://github.com/ipfs/integrations/issues) for IPFS URI integration or find us on: -* [Slack](https://filecoin.io/slack) -* [Discord](https://discord.com/invite/ipfs) -* [Forum](https://discuss.ipfs.tech/) +* [Matrix](https://matrix.to/#/#ipfs-space:ipfs.io), [Discord](https://discord.com/invite/ipfs) or [Slack](https://filecoin.io/slack) +* [Discussion Forum](https://discuss.ipfs.tech/) Or one of the other many places where the [IPFS comunity](https://docs.ipfs.tech/community/) is active. From 3af0e7180bfcb413d78ab042e34f8076482318e2 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Fri, 13 Oct 2023 17:35:21 +0200 Subject: [PATCH 4/9] improvde URI section --- src/_blog/ipfs-uri-support-in-curl.md | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 136abfdd..9c7bd495 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -12,14 +12,14 @@ tags: - 'curl' --- -# IPFS URI support in CURL +# `ipfs://` URI support in `curl` [CURL 8.4.0](https://github.com/curl/curl/releases/tag/curl-8_4_0) shipped with built-in support for `ipfs://` and `ipns://` addresses. This enables `curl` to seamlessly integrate with the user's preferred [IPFS gateway](https://docs.ipfs.tech/reference/http/gateway/) through the `IPFS_GATEWAY` environment variable or a `gateway` file. Best of all, these capabilities are available for immediate use today: ```bash -$ export IPFS_GATEWAY="http://127.0.0.1:8080" # local gateway provided by ipfs daemon like Kubo +$ export IPFS_GATEWAY="http://127.0.0.1:8080" # local (trusted) gateway provided by ipfs daemon like Kubo $ curl ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey -s -L hello from IPFS ``` @@ -39,32 +39,35 @@ The support of IPFS in CURL is effectively consisting of two implementation deta 1. CURL tries to find a locally installed or [configured gateway](#how-does-curl-find-an-ipfs-gateway). 2. It then rewrites an `ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` to a gateway URL. This is how curl handles it internally, you see nothing of this URL rewriting. -If you have IPFS installed locally then running: -`curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` -will just work. +If you have IPFS installed locally then running `curl ipfs://` will Just Work™. If not, CURL will return an error with details how to set up the gateway preference. This ensures the user agency is respected, no third-party gateway is used as implicit default. -## Why ipfs:// URI support is so important? +## Why `ipfs://` URI support is so important? -This question keeps coming up. Why-o-why do we find it so important to have IPFS support in CURL, even if it's just a fancy URL rewriter? - -Why isn't `https://ipfs.io/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` equally acceptable? Or why isn't a local url like `http://localhost:8080/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` fine? +Why isn't `https://ipfs.io/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` equally acceptable? +Or why isn't a local URL `http://localhost:8080/ipfs/bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` fine? -I'll have to repeat one of the core concepts of IPFS here. IPFS is a distributed network in which you access content. It shouldn't matter where that content is. The "where" part should not be provided. If you do proovide this where part (a gateway is a central point of entry, the where part) then you limit your access to IPFS through that one point of entry. +Both addresses are tied to a specific _location_. + +IPFS is a modular suite of protocols purpose built for the organization and transfer of [content-addressed](https://docs.ipfs.tech/concepts/content-addressing) data. It shouldn't matter where the content is. Content Identifier ([CID](https://docs.ipfs.tech/concepts/glossary/#cid)) is all that is required. The "where" part is implementation detail an IPFS system takes care of. Hardcoding a location in addition to a CID (like a specific HTTP gateway) limits end user to IPFS resources available through that one specific, centralized point of entry. If we pull the URL apart we see: ![](https://hackmd.io/_uploads/Bk2MV-9ea.png) -As a user of IPFS you should not care about the **where** part because you're now limited to however available that central access point is. +Users of IPFS system should not care about the _where_ part, nor be coerced to use a specific, hard-coded entry point into the system. -An example of a direct limitation. There are gateways out there that don't allow playback of video through their gateway or that bandwidth-throttle such content. This isn't a limitation of IPFS but is purely a limitation a gateway has set through custom configuration. You have no such limitation if you were to be using your own local node with a local gateway. +Public gateways like `ipfs.io` are always owned by some entity and could get censored or shut down at any time. Many gateways will not allow playback of deserialized videos or only respond to CIDs from allowlists to reduce costs. Other gateways will block specific CIDs from resolving in specific jurisdictions for legal reasons. Community-run public gateways will have limits and throttle usage. -Another example. Say you use `ipfs.io` as gateway. Now if that gateway starts throttling users, or gets sensored by ISPs then to you it looks like "IPFS isn't working" while the data might be well accessible, just not momentarily via that gateway. Even though gateways like `ipfs.io` and `dweb.link` are themselves connected to vast numbers of peers, it's worth nothing if the http gateway endpoint has a momentarily hiccup. Beyond the connectivity aspect, a gateway is hosted by someone. If you use a gateway you use their resources and bandwidth! +These are not limitations of IPFS but purely a limitation a specific gateway has set through custom configuration. IPFS user should always have ability to avoid such limitations if they choose to self-host and [run their own IPFS node with a local gateway](https://docs.ipfs.tech/install/). + + + ## How does CURL find an IPFS Gateway? Any IPFS implementation that has support for [IPIP-280](https://github.com/ipfs/specs/pull/280) exposes an IPFS gateway that CURL (and [ffmpeg](https://blog.ipfs.tech/2022-08-01-ipfs-and-ffmpeg/)) can use. At the moment of writing that's just [Kubo](https://github.com/ipfs/kubo/releases). @@ -93,8 +96,8 @@ When using a third-party gateway that one can't fully trust, the only secure opt **NOTE on HTTP redirects** -The URI resolution in `curl` does not follow redirects by default and assumes the endpoint implements deserializing [path gateway](https://specs.ipfs.tech/http-gateways/path-gateway/) or at the very least, the [trustless gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/). -When pointing `curl` at a [subdomain gateway](https://specs.ipfs.tech/http-gateways/subdomain-gateway) (like `https://dweb.link` or the `http://localhost:8080` provided by a local Kubo node) one has to pass `-L` in the curl command to follow the redirect. +By default, the URI resolution in `curl` does not follow HTTP redirects and assumes the endpoint implements deserializing [path gateway](https://specs.ipfs.tech/http-gateways/path-gateway/), or at the very least, the [trustless gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/). +When pointing `curl` at a [subdomain gateway](https://specs.ipfs.tech/http-gateways/subdomain-gateway) (like `https://dweb.link` or the `http://localhost:8080` provided by a [local Kubo node](https://docs.ipfs.tech/how-to/command-line-quick-start/)) one has to pass `-L` in the curl command to follow the redirect. ::: From 41e0c2051998c6f23c73cc526977400bdab1a41b Mon Sep 17 00:00:00 2001 From: "dame.eth" <110121581+damedoteth@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:52:37 -0400 Subject: [PATCH 5/9] Update ipfs-uri-support-in-curl.md Minor syntax and grammar changes --- src/_blog/ipfs-uri-support-in-curl.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 9c7bd495..65a191dd 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -32,14 +32,14 @@ In this blog post, we will: ## A brief history -Supporting IPFS in CURL has been attempted [before](https://github.com/curl/curl/pull/8468) as a CURL library feature. Some discussions lead to a belief that this should be implemented in the CURL tool itself, not it's library. A renewed [implementation attempt](https://github.com/curl/curl/pull/8805) took the tool-side approach which ultimately was accepted and is available right now in CURL 8.4.0! +Supporting IPFS in CURL has been attempted [before](https://github.com/curl/curl/pull/8468) as a CURL library feature. Some discussions lead to a belief that this should be implemented in the CURL tool itself, not its library. A renewed [implementation attempt](https://github.com/curl/curl/pull/8805) took the tool-side approach which ultimately was accepted and is available right now in CURL 8.4.0! The support of IPFS in CURL is effectively consisting of two implementation details. 1. CURL tries to find a locally installed or [configured gateway](#how-does-curl-find-an-ipfs-gateway). 2. It then rewrites an `ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi` to a gateway URL. This is how curl handles it internally, you see nothing of this URL rewriting. -If you have IPFS installed locally then running `curl ipfs://` will Just Work™. If not, CURL will return an error with details how to set up the gateway preference. This ensures the user agency is respected, no third-party gateway is used as implicit default. +If you have IPFS installed locally then running `curl ipfs://` will Just Work™. If not, CURL will return an error with details about how to set up the gateway preference. This ensures the user agency is respected, no third-party gateway is used as implicit default. ## Why `ipfs://` URI support is so important? @@ -48,15 +48,15 @@ Or why isn't a local URL `http://localhost:8080/ipfs/bafybeigagd5nmnn2iys2f3doro Both addresses are tied to a specific _location_. -IPFS is a modular suite of protocols purpose built for the organization and transfer of [content-addressed](https://docs.ipfs.tech/concepts/content-addressing) data. It shouldn't matter where the content is. Content Identifier ([CID](https://docs.ipfs.tech/concepts/glossary/#cid)) is all that is required. The "where" part is implementation detail an IPFS system takes care of. Hardcoding a location in addition to a CID (like a specific HTTP gateway) limits end user to IPFS resources available through that one specific, centralized point of entry. +IPFS is a modular suite of protocols purpose built for the organization and transfer of [content-addressed](https://docs.ipfs.tech/concepts/content-addressing) data. It shouldn't matter where the content is. Content Identifier ([CID](https://docs.ipfs.tech/concepts/glossary/#cid)) is all that is required. The "where" part is implementation detail an IPFS system takes care of. Hardcoding a location in addition to a CID (like a specific HTTP gateway) limits end users to IPFS resources available through that one specific, centralized point of entry. If we pull the URL apart we see: ![](https://hackmd.io/_uploads/Bk2MV-9ea.png) -Users of IPFS system should not care about the _where_ part, nor be coerced to use a specific, hard-coded entry point into the system. +Users of the IPFS system should not care about the _where_ part, nor be coerced to use a specific, hard-coded entry point into the system. -Public gateways like `ipfs.io` are always owned by some entity and could get censored or shut down at any time. Many gateways will not allow playback of deserialized videos or only respond to CIDs from allowlists to reduce costs. Other gateways will block specific CIDs from resolving in specific jurisdictions for legal reasons. Community-run public gateways will have limits and throttle usage. +Public gateways like `ipfs.io` are always owned by some entity and could get censored or shut down at any time. Many gateways will not allow playback of deserialized videos or only respond to CIDs from allowlists to reduce costs. Other gateways will block specific CIDs from resolving in specific jurisdictions for legal reasons. Community-run public gateways will have limits and throttle usage. These are not limitations of IPFS but purely a limitation a specific gateway has set through custom configuration. IPFS user should always have ability to avoid such limitations if they choose to self-host and [run their own IPFS node with a local gateway](https://docs.ipfs.tech/install/). @@ -78,15 +78,15 @@ CURL 8.4.0 and greater looks for a gateway in the following order: 2. The `--ipfs-gateway` CLI argument. 3. The `~/.ipfs/gateway` file, where it reads the first line. -If a gateway hint is found at any of those places, and if that is a valid HTTP URL then CURL will use it. If not then you'll be getting an error message pointing to the [CURL documentation related to IPFS](https://curl.se/docs/ipfs.html) to help you further. +If a gateway hint is found at any of those places, and if that is a valid HTTP URL, then CURL will use it. If not, then you'll be getting an error message pointing to the [CURL documentation related to IPFS](https://curl.se/docs/ipfs.html) to help you further. -One can specify any IPFS gateway that is in compliance with [Gateway Specifications](https://specs.ipfs.tech/http-gateways/). It is highly recommended using a local gateway, as it provides the best security guarantees. +One can specify any IPFS gateway that is in compliance with [Gateway Specifications](https://specs.ipfs.tech/http-gateways/). It is highly recommended to use a local gateway, as it provides the best security guarantees. ## Malicious gateways and data integrity? Requesting deserialized responses and delegating hash verification to a third-party gateway comes with risks. It is possible that a public gateway is malicious. Or, that a well-known and respected gateway gets hacked and changed to return payload that does not match requested CID. How can one protect themselves against that? -If deserialized responses are necessary, one should run own gateway in a local, controlled environment. Every block of data retrieved though self-hosted IPFS gateway is verified to match the hash from CID. For the maximum flexibility and security, find implementation that provides the gateway endpoint (i.e. [Kubo](https://docs.ipfs.tech/install/command-line/)) and run it yourself! +If deserialized responses are necessary, one should run their own gateway in a local, controlled environment. Every block of data retrieved though self-hosted IPFS gateway is verified to match the hash from CID. For the maximum flexibility and security, find an implementation that provides the gateway endpoint (i.e. [Kubo](https://docs.ipfs.tech/install/command-line/)) and run it yourself! When using a third-party gateway that one can't fully trust, the only secure option is to [request verifiable response types](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval) such as [application/vnd.ipld.raw](https://www.iana.org/assignments/media-types/application/vnd.ipld.raw) (a single block) or [application/vnd.ipld.car](https://www.iana.org/assignments/media-types/application/vnd.ipld.car) (multiple blocks in CAR archive). Both allow to locally verify if the data returned by gateway match the requested CID, removing the surface for [Man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack). From ca4c13fae57476d9678d8fa111f54a80153e8ccc Mon Sep 17 00:00:00 2001 From: "dame.eth" <110121581+damedoteth@users.noreply.github.com> Date: Fri, 13 Oct 2023 13:35:17 -0400 Subject: [PATCH 6/9] Update ipfs-uri-support-in-curl.md add cover image --- src/_blog/ipfs-uri-support-in-curl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 65a191dd..861c7488 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -4,7 +4,7 @@ description: 'CURL 8.4.0 shipped with built-in support for ipfs:// and ipns:// a author: Mark Gaiser date: 2023-10-7 permalink: '/ipfs-uri-support-in-curl/' -header_image: '/ipfs-calendar/ipfs-calendar-cover.png' +header_image: '/curl.png' tags: - 'community' - 'URI' From 57d7276a9fa20ec882db7cb0fef55c670aa51134 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 16 Oct 2023 02:46:47 +0200 Subject: [PATCH 7/9] curl: add cli examples --- package-lock.json | 16 +++-- src/_blog/ipfs-uri-support-in-curl.md | 92 +++++++++++++++++++++------ 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index d1d1fee3..a709c247 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6364,9 +6364,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001470", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001470.tgz", - "integrity": "sha512-065uNwY6QtHCBOExzbV6m236DDhYCCtPmQUCoQtwkVqzud8v5QPidoMr6CoMkC2nfp6nksjttqWQRRh75LqUmA==", + "version": "1.0.30001549", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz", + "integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==", "dev": true, "funding": [ { @@ -6376,6 +6376,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -30067,9 +30071,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001470", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001470.tgz", - "integrity": "sha512-065uNwY6QtHCBOExzbV6m236DDhYCCtPmQUCoQtwkVqzud8v5QPidoMr6CoMkC2nfp6nksjttqWQRRh75LqUmA==", + "version": "1.0.30001549", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001549.tgz", + "integrity": "sha512-qRp48dPYSCYaP+KurZLhDYdVE+yEyht/3NlmcJgVQ2VMGt6JL36ndQ/7rgspdZsJuxDPFIo/OzBT2+GmIJ53BA==", "dev": true }, "caseless": { diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 861c7488..0de759b4 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -20,7 +20,7 @@ This enables `curl` to seamlessly integrate with the user's preferred [IPFS gate ```bash $ export IPFS_GATEWAY="http://127.0.0.1:8080" # local (trusted) gateway provided by ipfs daemon like Kubo -$ curl ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey -s -L +$ curl ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey hello from IPFS ``` @@ -92,45 +92,95 @@ When using a third-party gateway that one can't fully trust, the only secure opt ## CURL Examples +### Deserialized responses + ::: callout -**NOTE on HTTP redirects** +By default, a trusted local gateway acts as a bridge between traditional HTTP clients and IPFS. + +It performs necessary hash verification, UnixFS _deserialization_ and return reassembled files to the client, as if they were stored in a traditional HTTP server. This means all validation happens on the gateway, and clients trust that the gateway is correctly validating content-addressed data before returning it to them. + +::: + +#### Downloading a file from IPFS with CURL + +```bash +$ curl ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey -o out.txt +``` + +If curl responds with `curl: IPFS automatic gateway detection failure`, make sure `IPFS_GATEWAY` is set (see examples below). + +#### Explicitly specifying a gateway + +To use local gateway on custom port 48080: + +```bash +$ export IPFS_GATEWAY=http://127.0.0.1:48080 +$ curl ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey +hello from IPFS +``` + +When setting environment variable is not feasible, one can use `--ipfs-gateway` instead: + +```bash +$ curl --ipfs-gateway http://127.0.0.1:48080 ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey +hello from IPFS +``` + +#### Following subdomain redirects + +::: callout By default, the URI resolution in `curl` does not follow HTTP redirects and assumes the endpoint implements deserializing [path gateway](https://specs.ipfs.tech/http-gateways/path-gateway/), or at the very least, the [trustless gateway](https://specs.ipfs.tech/http-gateways/trustless-gateway/). When pointing `curl` at a [subdomain gateway](https://specs.ipfs.tech/http-gateways/subdomain-gateway) (like `https://dweb.link` or the `http://localhost:8080` provided by a [local Kubo node](https://docs.ipfs.tech/how-to/command-line-quick-start/)) one has to pass `-L` in the curl command to follow the redirect. ::: -### Playing Big Buck Bunny (CURL + ffplay) -``` -curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | ffplay - -``` - -### Downloading a file from IPFS with CURL ```bash -curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi -o bbb.webm +$ IPFS_GATEWAY=https://localhost:8080 curl -s -L ipfs://bafkreih3wifdszgljcae7eu2qtpbgaedfkcvgnh4liq7rturr2crqlsuey +hello from IPFS ``` -### Explicitly specifying a gateway +#### Piping and streaming responses + +Deserialized response returned by CURL can be piped directly to a video player: + +``` +$ curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | ffplay - +``` + +### Verifiable responses + +::: callout + +By explicitly requesting [application/vnd.ipld.raw](https://www.iana.org/assignments/media-types/application/vnd.ipld.raw) (a block) or [application/vnd.ipld.car](https://www.iana.org/assignments/media-types/application/vnd.ipld.car) (a stream of blocks) responses, by means defined in [Trustless Gateway Specification](https://specs.ipfs.tech/http-gateways/trustless-gateway/), the user is able to fetch raw content-addressed data and [perform hash verification themselves](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval). + +::: + +#### Fetching and verifying a directory from an untrusted gateway + +Requesting [trustless and verifiable](https://docs.ipfs.tech/reference/http/gateway/#trustless-verifiable-retrieval) CAR response via `Accept` HTTP header: + ```bash -IPFS_GATEWAY=http://localhost:8080 curl ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi +$ export IPFS_GATEWAY="https://ipfs.io" # using untrusted public gateway +$ curl -H "Accept: application/vnd.ipld.car" "ipfs://bafybeiakou6e7hnx4ms2yangplzl6viapsoyo6phlee6bwrg4j2xt37m3q" > dag.car ``` -### Handling redirects -You have to explicitly tell curl to handle redirects for security reasons. +Then, CAR can be moved around and imported into some other IPFS node: + ```bash -curl -L ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi +$ ipfs dag import dag.car ``` -### Piping data and follow redirect -In this case the data is piped to the `file` utility in linux. We need to tell curl to be silent `-s` and to follow the redirects `-L`: -```bash -IPFS_GATEWAY=https://dweb.link curl -s -L ipfs://bafybeigagd5nmnn2iys2f3doro7ydrevyr2mzarwidgadawmamiteydbzi | file - -``` +or verified and unpacked locally, without having to run a full IPFS node, with tools like [go-car](https://github.com/ipld/go-car/tree/master/cmd/car#readme) or [ipfs-car](https://www.npmjs.com/package/ipfs-car): -Which tells us: ``` -/dev/stdin: WebM +$ npm i -g ipfs-car +$ ipfs-car unpack dag.car --output dag.out +$ ls dag.out +1007 - Sustainable - alt.txt +1007 - Sustainable - transcript.txt +1007 - Sustainable.png ``` ## What's next? From 25d0b51b25fb347845fda4c0a4214ef865539ec0 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Mon, 16 Oct 2023 02:49:11 +0200 Subject: [PATCH 8/9] fix typo --- src/_blog/ipfs-uri-support-in-curl.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 0de759b4..529a07db 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -190,5 +190,5 @@ More places supporting IPFS addresses. Everyone can integrate `ipfs://` and `ipn * [Matrix](https://matrix.to/#/#ipfs-space:ipfs.io), [Discord](https://discord.com/invite/ipfs) or [Slack](https://filecoin.io/slack) * [Discussion Forum](https://discuss.ipfs.tech/) -Or one of the other many places where the [IPFS comunity](https://docs.ipfs.tech/community/) is active. +Or one of the other many places where the [IPFS community](https://docs.ipfs.tech/community/) is active. From 0e9afaeb743d65214040f9fe759681376940166e Mon Sep 17 00:00:00 2001 From: Mark Gaiser Date: Mon, 16 Oct 2023 16:38:30 +0200 Subject: [PATCH 9/9] Fix image --- src/_blog/ipfs-uri-support-in-curl.md | 2 +- src/assets/ipfs_uri_where_protocol_what.png | Bin 0 -> 20099 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/assets/ipfs_uri_where_protocol_what.png diff --git a/src/_blog/ipfs-uri-support-in-curl.md b/src/_blog/ipfs-uri-support-in-curl.md index 529a07db..f690a6a5 100644 --- a/src/_blog/ipfs-uri-support-in-curl.md +++ b/src/_blog/ipfs-uri-support-in-curl.md @@ -52,7 +52,7 @@ IPFS is a modular suite of protocols purpose built for the organization and tran If we pull the URL apart we see: -![](https://hackmd.io/_uploads/Bk2MV-9ea.png) +![](../assets/ipfs_uri_where_protocol_what.png) Users of the IPFS system should not care about the _where_ part, nor be coerced to use a specific, hard-coded entry point into the system. diff --git a/src/assets/ipfs_uri_where_protocol_what.png b/src/assets/ipfs_uri_where_protocol_what.png new file mode 100644 index 0000000000000000000000000000000000000000..87d0c5e012ffc9512c896e8f2e167b02c76777c6 GIT binary patch literal 20099 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 ZNIN2zk8$Co7?}yUt{Ir?zt(k(`WIY!r;z{v literal 0 HcmV?d00001