Magnet 和种子之间的关系——在前端(Angular)实现种子到 Magnet 转换的实践

English article:https://tuki.moe/angular-magnet-and-torrent-en/

最近在做一个关于在线下载种子的项目中遇到了种子文件和 magnet 地址的转换的问题,由于后端只能接受 magnet 格式的输入,一开始我们尝试使用公共服务进行转换,但是发现容易被限制,且将用户的请求发送到站点以外的地方不太符合隐私标准。

之后我们尝试自己托管了一个 https://github.com/likebeta/torrent2magnet ,但是使用起来总感觉还是有点别扭,为此,我们研究了种子文件和 Magnet 之间的关系,并尝试直接在前端进行转换,减少对于外部资源的利用(和中间商赚差价),同时也借此机会学习一下相关的知识。

种子是什么

我们经常听到别人说下一个种子文件,但是这里的种子文件究竟是什么?一般来说我们指的种子文件是一个 .torrent 文件,其中包含了一些需要被分享的文件的元信息,比如我们从 Wikipedia 上可以知道一个 torrent 文件至少包含了以下信息(但实际上可能会有更多信息):

  • announce – 预设的 tracker URL(主Tracker的URL,用于Peers和Seeders之间的交互)
  • 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(相当于当多个文件被共享)

这些信息在 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>
  }
}
  • announce-list – Tracker URL 的列表,表示备选的 Tracker
  • comment – 可选的文本字段,通常包含关于资源的一般评论或说明
  • created by – 创建此Torrent文件的软件或工具的名称
  • creation date – 文件创建或Torrent文件创建的时间戳

一个种子由 infohash 唯一标识,infohash 是根据 Bencode 形式的信息字典内容计算的 SHA-1 哈希值。如果一个种子文件本身就带有 infohash 参数,那么就可以直接使用它作为 magnet 的 xt(eXact Topic),如果没有,那就需要根据 info 来计算。

关于 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

magnet 与 torrent 参数的转换关系:

  • dn:info.name
  • xl: info.length
  • tr: 可以直接使用 announce,也可以将 announce-list 所有的 url 拼接

在 Angular 中转换

通过 reader.readAsArrayBuffer 读取上传的 torrent 文件,拿到 torrent 文件中的 info,再通过上述方法计算其对应的 infohash。

<input 
  type="file"
  accept=".torrent"
  (change)="uploadTorrent($event)"
/>
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这里会报错,所以需要额外引入,并设置全局变量(polyfills.ts)
      const torrent = bencode.decode(buffer_content);
      const infohash = sha1(bencode.encode(torrent.info)).toUpperCase(); // 2DAD5FF88A845EFE729FD87A26B529C5712BAFC7
    }
 }
// polyfills.ts
import * as buffer from "buffer";
(window as any).Buffer = buffer.Buffer;

torrent2magnet-js

根据项目中总结的经验,尝试写了一个转换的小工具,发布到了 npm 上。

https://github.com/tukideng/torrent2magnet-js

通过接受一个 torrent 文件的 buffer,输出其中包含的某些信息以及 magnet uri

以及一个在线转换工具

https://t2m.tuki.moe/

希望可以帮助到有类似需求的同学们,Have Fun!🥳

参考链接

Leave a Reply

Your email address will not be published. Required fields are marked *