最近在做一个关于在线下载种子的项目中遇到了种子文件和 magnet 地址的转换,由于后端只能接受 magnet 格式的输入,一开始我们尝试使用公共服务进行转换,但是发现容易被限制,且将用户的请求发送到站点以外的地方不太符合隐私标准。
之后我们尝试自己托管了一个 https://github.com/likebeta/torrent2magnet ,但是使用起来总感觉还是有点别扭,为此,我们研究了种子文件和 Magnet 之间的关系,并尝试直接在前端进行转换,减少对于外部资源的利用(和中间商赚差价),同时也借此机会学习一下相关的知识。
种子是什么
我们经常听到别人说下一个种子文件,但是这里的种子文件究竟是什么?一般来说我们指的种子文件是一个 .torrent 文件,其中包含了一些需要被分享的文件的元信息,比如我们从 Wikipedia 上可以知道一个 torrent 文件至少包含了以下信息:
- announce – 预设的 tracker URL
- info – 该条映射到一个字典,该字典的键将取决于共享的一个或多个文件
- files – 一个字典的列表(每个字典对应一个文件)与以下的键
- length – 文件的大小(以字节为单位)
- path – 一个对应子目录名的字符串列表,最后一项是实际的文件名称
- length – 文件的大小(以字节为单位)
- name – 建议保存到的文件和目录名称
- piece length – 每个文件块的字节数。通常为 2^8 = 256KiB = 262144B
- pieces – 每个文件块的 SHA-1 的整合 Hash。因为 SHA-1 会返回160-bit的 Hash,所以pieces 将会得到 1 个 160-bit 的整数倍的字符串。和一个 length(相当于只有一个文件正在共享)或 files(相当于当多个文件被共享)
- files – 一个字典的列表(每个字典对应一个文件)与以下的键
这些信息在 torrent 文件中是以 bencode 进行编码的,以 ubuntu-22.04-desktop-amd64.torrent 为例,通过以下简单代码即可解码 torrent 文件内容:
const fs = require('fs'); var bencode = require( 'bencode' ); const parsed = bencode.decode(fs.readFileSync('./ubuntu-22.04-desktop-amd64.torrent')); console.log(parsed);
输出结果:
{ announce: <Buffer 68 74 74 70 73 3a 2f 2f 74 6f 72 72 65 6e 74 2e 75 62 75 6e 74 75 2e 63 6f 6d 2f 61 6e 6e 6f 75 6e 63 65>, 'announce-list': [ [ <Buffer 68 74 74 70 73 3a 2f 2f 74 6f 72 72 65 6e 74 2e 75 62 75 6e 74 75 2e 63 6f 6d 2f 61 6e 6e 6f 75 6e 63 65> ], [ <Buffer 68 74 74 70 73 3a 2f 2f 69 70 76 36 2e 74 6f 72 72 65 6e 74 2e 75 62 75 6e 74 75 2e 63 6f 6d 2f 61 6e 6e 6f 75 6e 63 65> ] ], comment: <Buffer 55 62 75 6e 74 75 20 43 44 20 72 65 6c 65 61 73 65 73 2e 75 62 75 6e 74 75 2e 63 6f 6d>, 'created by': <Buffer 6d 6b 74 6f 72 72 65 6e 74 20 31 2e 31>, 'creation date': 1650550976, info: { length: 3654957056, name: <Buffer 75 62 75 6e 74 75 2d 32 32 2e 30 34 2d 64 65 73 6b 74 6f 70 2d 61 6d 64 36 34 2e 69 73 6f>, 'piece length': 262144, pieces: <Buffer bc 07 c0 6a 9d e0 6d ea 5c 2d 03 88 91 f9 7b 5e 84 67 b0 3f e5 2c 91 64 13 05 c0 8f 00 a0 cd c1 28 ea 86 f0 c6 04 ac c1 4f 70 7f f0 e7 b6 57 2f 7d 6a ... 278810 more bytes> } }
- 关于 Bencode 是什么可以参考:https://en.wikipedia.org/wiki/Bencode
Magnet 是什么
通过 Wikipedia 可得知,Magnet 是基于元数据的文档生成的一个唯一的文件识别符,在分布式数据库中,通过散列函数值来识别、搜索来下载文档。
它由一组参数组成,最常用的参数是 xt,通常是一个特定文件的内容散列函数值形成的 URN(例如magnet:?xt=urn:btih:2DAD5FF88A845EFE729FD87A26B529C5712BAFC7)
- dn(显示名称)- 文件名
- xl(绝对长度)- 文件字节数
- xt(eXact Topic)- 包含文件散列函数值的URN
- as(可接受来源) – 在线文件的网络链接
- xs(绝对资源)- P2P链接
- kt(关键字)- 用于搜索的关键字
- mt(文件列表)- 链接到一个包含磁力連結的元文件 (MAGMA – MAGnet MAnifest (页面存档备份,存于互联网档案馆))
- tr(Tracker地址)- BT下载的Tracker URL
Magnet 和种子之间的关系
通过 Magnet 的文件散列函数值,可以在分布式散列表(DHT)中唯一的定位 torrent 文件,然后通过连接 tracker 下载文件。
其中 Magnet 的 xt(eXact Topic)的计算是由 torrent 文件中整个 info(参数)信息通过 bencode 解码后,再通过 SHA1 算出来的结果。然后再通过拼接 magnet:? 和参数即可,JS 代码如下:
var fs = require('fs'); var sha1 = require('js-sha1'); var bencode = require( 'bencode'); const parsed = bencode.decode(fs.readFileSync('./ubuntu-22.04-desktop-amd64.torrent')); console.log(parsed); const infohash = sha1(bencode.encode(parsed.info)); console.log(infohash); // magnet:?xt=urn:btih:2DAD5FF88A845EFE729FD87A26B529C5712BAFC7
在 Angular 中转换
通过 reader.readAsArrayBuffer 读取上传的 torrent 文件,拿到 torrent 文件中的 info,再通过上述方法计算其对应的 infohash。
import * as buffer from 'buffer'; (window as any).Buffer = buffer.Buffer; uploadTorrent(event: any) { const file = event.target.files[0]; const reader = new FileReader(); // bencode.decode 需要的是 ArrayBuffer,所以这里需要把文件读取为 ArrayBuffer reader.readAsArrayBuffer(file); const sha1 = require('js-sha1'); const bencode = require('bencode'); reader.onload = async (file: any) => { const buffer_content = Buffer.from(file.target.result); // bencode 需要使用 Buffer,但是 Buffer 在原生的库中并不存在,所以在decode这里会报错,所以需要额外引入,并设置全局变量 const torrent = bencode.decode(buffer_content); const infohash = sha1(bencode.encode(torrent.info)).toUpperCase(); // 2DAD5FF88A845EFE729FD87A26B529C5712BAFC7 } }
希望可以帮助到有类似需求的同学们,Have Fun!🥳