在这节及之后的PT架构书写过程中,我将使用ThinkPHP 5作为MVC框架,rchouinard/bencode 作为Bencode编码库,实现一个演示性质的Private Tracker。在此,我将默认你已经对前面的内容有所理解,并对PHP以及composer有了解运用。

该项目代码见: https://github.com/Rhilip/Simple-Private-Tracker ,仅供学习无法运行~

请注意,本文所列方法,仅表示本人的一种实现。实际只要符合BEP 0003以及BEP 0027的都是可行的实现~
请注意,本处所列代码并不一定是最新的,仅代表思考逻辑过程,具体代码请看repo

相关commit:c2c37e668a3f63722b6d4d736e957c8cda76b2a8

基础准备

首先,我们需要准备好PHP环境(建议为7.x)以及数据库,因为学习,所以缓存暂时使用文件缓存。并使用composer安装 ThinkPHP5以及bencode ,其命令分别如下:

1
2
3
composer create-project topthink/think=5.0.* tp5  --prefer-dist
cd pt
composer require rych/bencode

准备相关数据表,分别用来存储 Torrent(种子信息)、User(用户信息)、Peer(做种人信息)、Snatch(做种完成情况),此处为了方便(偷懒)直接使用NP的建表语句(-> 见 NexusPHP/_db/dbstructure.sql 相关)就行(实际很多字段不需要)。顺带也方面后续兼容~

而文件夹方面,依次添加以下文件:

1
2
3
4
├─application
│ ├─tracker
│ │ ├─controllers
│ │ │ └─Index.php

并在路由(route\route.php)中注册两个控制器

1
2
Route::get('tracker/scrape','tracker/Index/scrape');
Route::get('tracker/announce','tracker/Index/announce');

并在设置中开启你的debug模式以及应用trace,准备postman或其他作为调试工具。

方法准备

我们先要为TrackerController 准备一些公用方法,分别用于构造响应信息(包括正常的以及错误)、禁用浏览器访问。修改app\Http\Controllers\TrackerController.php为以下信息:

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
<?php

namespace app\tracker\controller;

use think\Controller;
use think\Db;
use think\facade\Cache;
use think\Request;
use think\Response;
use think\Validate;

use Rych\Bencode\Bencode;

class Index extends Controller
{

private $errormsg = [
// Error message about requests params
// Error message about Bittorrent Client
// Error message about User Account
// Error message about Torrent
// Error message about Server
];

private $announce_param = []; // Announce Param HERE~

public function announce(Request $request)
{
}

public function scrape(Request $request)
{
}

private function block_browser()
{
$judge = false;
$request = Request();
if (preg_match("/Mozilla|Opera|Links|Lynx/", $request->header("user_agent"))) {
$judge = true;
}
if (!$request->isSSl()) {
if (
(null !== $request->header("Cookie", null)) ||
(null !== $request->header("Accept-Language", null)) ||
(null !== $request->header("Accept-Charset", null))
) {
$judge = true;
}
}
return $judge;
}

private function sendErrorMsg($code = 999, $msg = null)
{
if ($code && !$msg) {
$msg = $this->makeErrorMsg($code);
}

return $this->bencResp([
"failure reason" => $msg,
]);
}

private function makeErrorMsg($code)
{
return "$code - " . $this->errormsg[$code];
}

private function bencResp($obj)
{
$rep_benc = Bencode::encode($obj);
return response($rep_benc)
->header("Content-Type", "text/plain; charset=utf-8")
->header("Pragma", "no-cache");
}
}

我们将在$errormsg中定义错误信息,并在announce以及scrape这两个公开方法中定义具体逻辑。而所有的响应应该使用bencResp构造。

构建Scrape

相比较为复杂的Announce逻辑,我们先来处理较为简单的Scrape逻辑:

  1. 从请求头中获取所有info_hash信息,
  2. 从数据库中匹配出来对应的做种内容,
  3. 构造返回或错误信息。

下面我们开始写Scrape的具体逻辑。首先我们先禁用掉 非GET请求 以及 浏览器及非BT客户端请求。代码如下,但实际上,因为已经设置的路由关系,我们其实已经禁止了非GET请求,这里需要不需要都无所谓了

1
2
3
4
// 1. Block NON-GET requests, (though it should be block in Router)
if (!$request->isGet()) return $this->sendErrorMsg(100);
// 2. Block Browser, Crawler or Cheater
if ($this->block_browser()) return $this->sendErrorMsg(101);

然后我们从请求头中获取info_hash信息,并检查其是否存在(这里附加对各个info_hash的字节数进行检查也行)。当其不存在时,返回错误信息。

注意,根据BEP0048规定 ,info_hash在url中是以info_hash=xxxxx&info_hash=yyyyy的形式存在的,故本人之前的写法是错误的(之前写法见Archive.org的备份,只能匹配info_hash[]=xxxx的情况)

应为:

1
2
preg_match_all('/info_hash=([^&]*)/i', urldecode(Request::server('query_string')), $info_hash_match);
$info_hash = $info_hash_match[1];

针对info_hash未找到的情况进行处理。

1
2
3
4
if (count($info_hash) < 1) {
return $this->sendErrorMsg(null,
str_replace(':attribute', 'info_hash', $this->makeErrorMsg(102)));
}

使用查询构造器生成SQL语句并查询,并对查询结果进行检查;当数据库中未检查到该种子时,返回错误信息。

1
2
3
4
5
6
7
$res = Db::table("torrents")
->field(['info_hash', 'times_completed', 'seeders', 'leechers'])
->where('info_hash', "IN", $info_hash)
->select();
if (count($res) < 1) {
return $this->sendErrorMsg(131);
}

如果没有任何问题,我们需要把原来数据库中查询的结果(如下)进行转换

1
2
3
4
5
6
7
8
array(1) {
[0] => array(4) {
["info_hash"] => string(20) "aaaaaaaaaaaaaaaaaaaaa"
["times_completed"] => int(0)
["seeders"] => int(0)
["leechers"] => int(0)
}
}

方法如下,并最后使用bencResp($obj)的方式进行编码并发送

1
2
3
4
5
6
7
8
9
10
11
12
$rep = [
'files' => array_map(function ($item) {
return array (
$item["info_hash"] => [
"complete" => $item["seeders"],
"downloaded" => $item["times_completed"],
"incomplete" => $item["leechers"],
]
);
},$res)
];
return $this->bencResp($rep);

scrape_resp.jpg