XiaO

IPFS 构建去中心化网站

XiaO / 2021-01-23


IPFS 简介

IPFS,又称“星际文件系统”。简单点说,它是一个点对点的分布式文件系统(和比特币技术一样),通过底层协议,可以让存储在 IPFS 系统上的文件,在全世界任何一个地方快速获取,且不受防火墙的影响(无需网络代理)。

现在所使用的互联网协议被称作——超文本协议 HTTP,是一种超中心化特性。当从互联网上下载文件或者是浏览网页,一次只能从一个数据中心获取你所需要的资料。如果这个数据中心出现故障,或者被限制或是攻击,就会出现文件丢失或者网页无法打开的问题。

HTTP 的痛点

HTTP 协议已经用了 20 年的历史,从 HTTP 1.0 到现在的 HTTP5,网页的展示越来越美观丰富,但它背后的 Browser/Server 模式是从来没有改变过。

IPFS 特质

IPFS 像一个分布式存储网络,任何存储在系统里的资源,包括文字、图片、声音、视频,以及网站代码,通过 IPFS 进行哈希运算后,都会生成唯一的地址。今后,你只要通过这个地址就可以打开它们。并且这个地址是可以被分享的。

而由于加密算法的保护,该地址具备了不可篡改和删除的特性(在某种意义上,如果破解密码还是有可能被篡改或删除,但概率极低)。所以,一旦数据存储在 IPFS 网络中,它就会是永久性的。即便我们把该站点撤销,只要存储该站点信息的网络依然存在,该网页就可以被正常访问。存储站点的分布式网络越多,它的可靠性也就越强。

IPFS 存储的一般是公共信息,普通大众都可以获得的。有一种说法认为,如果 IPFS 完全取代 HTTP,那么此后,人类历史将会被永久保存,且不会被篡改。这也就意味着,人类所做的每一件事情都会被记录,不管是正确的、抑或是错误的。

IPFS 会把存储文件,做一次哈希计算,只字不差的两个文件哈希值相同。所以,用户只需要使用相同的哈希值,就可以访问那个文件,这个哈希值就是文件的地址。只要获取这个地址,就可以共享资源了。

基于上面的永久存储特性,再也不用担心某个电影找不到了,也不用备份,因为全球电脑上只要有那么几个人存储着,你就能拿到它,并且不依赖于中心网络。

IPFS 的网络上运行着一条区块链,即用来存储互联网文件的哈希值表,每次有网络访问,即要在链上查询该内容(文件)的地址。

文件(内容)具有存在的唯一性,一个文件加入了IPFS的网络,将基于计算对内容赋予一个唯一加密的哈希值。这将改变我们使用域名访问网络的习惯。

提供文件的历史版本控制器(类似Git),并且让多节点使用保存不同版本的文件。

通过使用代币(FileCoin)的激励作用,让各节点有动力去存储数据。 Filecoin 是一个由加密货币驱动的存储网络。矿工通过为网络提供开放的硬盘空间获得 Filecoin,而用户则用 Filecoin 来支付在去中心化网络中储存加密文件的费用

IPFS 工作原理

IPFS 配置

1. 下载安装

下载 Mac 版 IPFS 安装文件 go-ipfs_v0.7.0_darwin-amd64.tar.gz,解压缩该文件,得到 go-ipfs 文件夹,包含如下文件:

├── LICENSE
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── install.sh
└── ipfs

安装 IPFS

cd go-ipfs
install.sh
> Moved ./ipfs to /usr/local/bin

ipfs --version                                        
> ipfs version 0.7.0 # 出现版本信息,表示安装成功

升级 IPFS,下载 ipfs-update 工具,解压缩

cd ipfs-update
install.sh

> Moved ./ipfs-update to /usr/local/bin

ipfs-update --version # 出现版本信息,表示安装成功

2. IPFS 本地部署

ipfs init # 创建新节点,所生成的新文件夹`.ipfs` 所含文件如下
.
├── blocks
├── config
├── datastore
├── datastore_spec
├── keystore
└── version

fs-repo-migrations -revert-ok -to 10 # 将现有节点迁移到指定版本,比如 10
{
  "API": {
    "HTTPHeaders": {
      "Server": [
        "go-ipfs/0.4.14"
      ]
    }
  },
  "Addresses": {
    "API": "/ip4/127.0.0.1/tcp/5001",
    "Announce": [],
    "Gateway": "/ip4/127.0.0.1/tcp/8080",
    "NoAnnounce": [],
    "Swarm": [
      "/ip4/0.0.0.0/tcp/4001",
      "/ip6/::/tcp/4001",
      "/ip4/0.0.0.0/udp/4001/quic",
      "/ip6/::/udp/4001/quic"
    ]
  },
  "Bootstrap": [
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
    "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
    "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
    "/ip4/104.236.179.241/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
    "/ip4/128.199.219.111/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu",
    "/ip4/104.236.76.40/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64",
    "/ip4/178.62.158.247/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd",
    "/ip6/2604:a880:1:20::203:d001/tcp/4001/p2p/QmSoLPppuBtQSGwKDZT2M73ULpjvfd3aZ6ha4oFGL1KrGM",
    "/ip6/2400:6180:0:d0::151:6001/tcp/4001/p2p/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu",
    "/ip6/2604:a880:800:10::4a:5001/tcp/4001/p2p/QmSoLV4Bbm51jM9C4gDYZQ9Cy3U6aXMJDAbzgu2fzaDs64",
    "/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd",
    "/ip4/104.131.131.82/udp/4001/quic/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
  ],
  "Datastore": {
    "BloomFilterSize": 0,
    "GCPeriod": "1h",
    "HashOnRead": false,
    "Spec": {
      "mounts": [
        {
          "child": {
            "path": "blocks",
            "shardFunc": "/repo/flatfs/shard/v1/next-to-last/2",
            "sync": true,
            "type": "flatfs"
          },
          "mountpoint": "/blocks",
          "prefix": "flatfs.datastore",
          "type": "measure"
        },
        {
          "child": {
            "compression": "none",
            "path": "datastore",
            "type": "levelds"
          },
          "mountpoint": "/",
          "prefix": "leveldb.datastore",
          "type": "measure"
        }
      ],
      "type": "mount"
    },
    "StorageGCWatermark": 90,
    "StorageMax": "2GB"
  },
  "Discovery": {
    "MDNS": {
      "Enabled": true,
      "Interval": 10
    }
  },
  "Experimental": {
    "FilestoreEnabled": false,
    "Libp2pStreamMounting": false,
    "ShardingEnabled": false
  },
  "Gateway": {
    "HTTPHeaders": {
      "Access-Control-Allow-Headers": [
        "X-Requested-With",
        "Range"
      ],
      "Access-Control-Allow-Methods": [
        "GET"
      ],
      "Access-Control-Allow-Origin": [
        "*"
      ]
    },
    "PathPrefixes": [],
    "RootRedirect": "",
    "Writable": false
  },
  "Identity": {
    "PeerID": "QmXXXXXXXXXXXXXXX",
    "PrivKey": "XXXXXXXXXXXX"
  },
  "Ipns": {
    "RecordLifetime": "",
    "RepublishPeriod": "",
    "ResolveCacheSize": 128
  },
  "Mounts": {
    "FuseAllowOther": false,
    "IPFS": "/ipfs",
    "IPNS": "/ipns"
  },
  "Reprovider": {
    "Interval": "12h",
    "Strategy": "all"
  },
  "Swarm": {
    "AddrFilters": null,
    "ConnMgr": {
      "GracePeriod": "20s",
      "HighWater": 900,
      "LowWater": 600,
      "Type": "basic"
    },
    "DisableBandwidthMetrics": false,
    "DisableNatPortMap": false,
    "DisableRelay": false,
    "EnableRelayHop": false
  }
}

每个节点都会存在一个唯一标识。

ipfs id # 查看节点 ID

{
	"ID": "QmXXXXXXXXXXXXXXX",
	"PublicKey": "XXXXXXXXXXXXXXX",
	"Addresses": null,
	"AgentVersion": "go-ipfs/0.7.0/",
	"ProtocolVersion": "ipfs/0.1.0",
	"Protocols": null
}
ipfs daemon # 启动节点

Initializing daemon...
go-ipfs version: 0.7.0
Repo version: 10
System version: amd64/darwin
Golang version: go1.14.4
Swarm listening on /ip4/127.0.0.1/tcp/4001
Swarm listening on /ip4/127.0.0.1/udp/4001/quic
Swarm listening on /ip4/192.168.0.102/tcp/4001
Swarm listening on /ip4/192.168.0.102/udp/4001/quic
Swarm listening on /ip6/::1/tcp/4001
Swarm listening on /ip6/::1/udp/4001/quic
Swarm listening on /p2p-circuit
Swarm announcing /ip4/127.0.0.1/tcp/4001
Swarm announcing /ip4/127.0.0.1/udp/4001/quic
Swarm announcing /ip4/192.168.0.102/tcp/4001
Swarm announcing /ip4/192.168.0.102/udp/4001/quic
Swarm announcing /ip6/::1/tcp/4001
Swarm announcing /ip6/::1/udp/4001/quic
API server listening on /ip4/127.0.0.1/tcp/5001
WebUI: http://127.0.0.1:5001/webui
Gateway (readonly) server listening on /ip4/127.0.0.1/tcp/8080
Daemon is ready

执行命令后,最后出现 Daemon is ready,就代表节点启动成功了,并已经作为节点加入了 IPFS 网络了。

检查本机的种子节点命令:ipfs bootstrap 检查本机加入的集群信息:ipfs swarm peers

GATEWAY: http://localhost:8080
API: http://127.0.0.1:5001

注意,使用带有 -r 参数的 ipfs add 命令,即 ipfs add -r,以便将文件夹下所有文件递归地添加到 IPFS。

ipfs add -r Path/to/webpage/Folder

ipfs add -r ~/Downloads/me     
added QmdNsncGJ35Uy247gSW9JG2oqvUjNwfQNcLC9BzGKMtZP7 me/assets/css/me.css
added QmSGwLaWfAF8VURTUdjLrYJLuCfeSM4DNkouPH5622v6rP me/assets/sass/00-reset.scss
added QmPHZrugucjRVuRQVHVh47cBZ47mC3ch2ktJP7wFDUNPQv me/assets/sass/01-content.scss
added QmdmeTQHUXbjhhfKVYsYutRPwTve7xqaFQx1Jzp7KFUzKc me/assets/sass/me.scss
added QmVhTyfjPvPjVm9Qr2HzHhEcPHuUTdwyFoGSbrun26eqge me/images/background.jpg
added QmS9GrnuFH7316vxbygdTWjsQidTheWbLq9W8ESyGY715H me/images/favicon.png
added QmarPeQ5brfhdstyJFnY6PmK6TZFDkfbktwCwt4eHcaTkV me/images/logo.png
added QmcML7GAxYfbfQoHUFWpYDEBXfoagVkGE4enWiWCJymwBa me/images/social.jpg
added QmVEJXxaPnGqaXH6wEmLonKNzbBi6svU5b8CcokKLUQyPA me/index-static.html
added QmeSGXLwY9Nc3Zwtrv2JiCSS5hDhFfwDwZaJKekdwdqVn7 me/index-youtube.html
added QmNZWBqCrhNinmfq8yUxxPteyubAGmMfzL6CubKgeeeT59 me/index.html
added QmTWxfA4jaSujHPVhLu1VVY888kLgbxnzVjcXp369Ge3eS me/readme.md
added QmWadsLVLLCyrMxJ6owQQRGpunQHcjeVpF3Z4rbFanm8it me/readme.txt
added QmZQZSEA5Cgyn3iCW4qTtDAjt2f25RRqFyPDPyeBJPCcNF me/videos/background.mp4
added QmV29PcMMYZ5SVe1SWJWw8VdwDj6xmBKVJmF4pykviDz5t me/assets/css
added QmeES6u2o7LpCdkh72KreSQ8Cea16e6LdvSHhV1K5KoNBE me/assets/sass
added QmbshznhgJn5MMi1nLzSyAeKb9VUtD9HLP57LoPgXPQurd me/assets
added QmcvWkbm2nUFNSfaWyo1Rc4UQvndSgeHbJd6rrFs4QRrXM me/images
added QmaZ8B21dkpTVAMpdkcYQsaqfetsotKQxeb6ug7xmJ4CFP me/videos
added QmNWsJM4c2ESKmwhVvz3nh1h87jrtxfvTVXViUSzE7m9dV me
 1.34 MiB / 1.34 MiB [=====================================================] 100.00%

我们需要一种服务,将内容放入其中之后,它可以保证内容一直在线, 供他人访问或复制。 这种服务并不需要承担所有访问流量, 但它需要承诺持续在线。 在 IPFS 世界中, 这种将文件“钉”在网络中的服务被称为"pin”。

虽然也可以自己搭建 IPFS 节点来完成上述任务,但对于个人网站这种简单场景, 更好的选择是使用现成的"pin"服务。Pinata 就是这样一个服务。 使用简便, 而且1G以下的存储容量是完全免费的。

注册 Pinata 之后, 访问 (https://pinata.cloud/pinataupload) 即可执行上传操作。 选择 “Upload Directory”后,可将包含网站的文件夹整体上传。上传成功以后将得到 CID (即此文件夹在 IPFS 中的内容 ID),也可以在 (https://pinata.cloud/pinexplorer) 管理已上传的文件或文件夹。

https://ipfs.io/ipfs/your-webpage-hash # 使用 `IPFS hash` 访问站点;

IPFS 中,网站的哈希值和网站的内容是一一对应的,网页中任何的改变都将导致其生成新的哈希值。所以,每次更新网站之后,网站的哈希值都会变,旧的链接不能访问到新的内容。

通过 IPNS,将网站的 IPFS 哈希值与 Peer ID 绑定,完后更新完网站,可通过 IPNS 使用 Peer ID 来访问站点,而每个用户的 Peer ID 可保持不变。

ipfs name publish your-webpage-hash # 将网站的 IPFS 哈希值发布到 IPNS

ipfs name publish QmNWsJM4c2ESKmwhVvz3nh1h87jrtxfvTVXViUSzE7m9dV # 将上述网站发布到 IPNS
Published to k2k4r8ot14v7ncfnxhvq4hx86thrxkn2sghvmjcrksas8fyqojwmnrq7: /ipfs/QmNWsJM4c2ESKmwhVvz3nh1h87jrtxfvTVXViUSzE7m9dV #  将 IPFS 哈希值发布到 IPNS,生成了一个新的 IPNS 哈希值;

ipfs name resolve your-peer-id # 验证 Peer ID 和网站的哈希已经绑定
/ipfs/your-webpage-hash

ipfs name resolve QmctjMf6xLZGiiXSoK9JAmvVdXwgpLBGLcShPE9fGpDbxr
/ipfs/QmNWsJM4c2ESKmwhVvz3nh1h87jrtxfvTVXViUSzE7m9dV
https://ipfs.io/ipns/your-peer-id
https://ipfs.io/ipns/QmctjMf6xLZGiiXSoK9JAmvVdXwgpLBGLcShPE9fGpDbxr

我们已经有一个域名sub.your.domain,可创建一个 DNS TXT 记录(DNSLink),这条记录的键(name 或者 key)为 _dnslink.sub.your.domain,值(data)为 dnslink=/ipns/$SITE_CID,其中 $SITE_CID 为站点的 Peer ID,将 Peer ID 与域名绑定。

TXT _dnslink.www.urz.one dnslink=/ipns/QmctjMf6xLZGiiXSoK9JAmvVdXwgpLBGLcShPE9fGpDbxr

在域名解析里面建立一条 CNAME 记录,将 sub.your.domain 解析指向 cloudflare-ipfs.com.

CNAME www.urz.one cloudflare-ipfs.com

访问 Cloudflare IPFS Gateway,在页面最底部文本框中输入想要保护的域名(在本例中即 www.urz.one), 等待几分钟, 就会提示 “Certificate is live”。

至此,我们就可以通过sub.your.domain 直接访问 ipfs 网站了。

网站本地更新后,将内容托管到 pinata.cloud,将 pinata.cloud 生成的内容 CID,发布到 IPNS ipfs name publish site-hash 即可。

nslookup cloudflare-ipfs.com. # 查询服务节点的 IP
Server:		8.8.8.8
Address:	8.8.8.8#53
Non-authoritative answer:
Name:	cloudflare-ipfs.com
Address: 104.244.46.5

dig +noall +answer TXT www.urz.one         
www.urz.one.		21599	IN	CNAME	cloudflare-ipfs.com.