扩展FastRoute使其支持Middleware

很久之前在写RidPT的时候,我就在考虑使用社区中更为优质的组件来替换原 MixPHP 中自带的一些组件。而路由部分中 symfony/routing 可能是我之前最想尝试的,因为目前RidPT中使用Symfony/HttpFoundation构建了请求和响应组件(甚至有段时间我觉得我在另外构建一个symfony,而且还没别人官方的好,其对swoole异步/协程的支持也均未测试)。然而接触文档之后,发现配置起来相当麻烦,远不如我在symfony应用中使用Annotations方法来的简便(因为杂活都让框架给做了)。

此外,对照文档,我们需要首先根据Request构造新的RequestContext对象,然后传入UrlMatcher并调用其match方法。虽然swoole常驻模式可以减少前面路由规则生成部分,但是后续对请求处理的部分,依然需要构造新的对象,造成无端的时间和空间浪费。且Symfony中并没有Middleware的概念,而是使用Event Dispatch的整体流程对请求-响应进行调度,为此再引入多个symfony组件似乎得不偿失。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use App\Controller\BlogController;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$route = new Route('/blog/{slug}', ['_controller' => BlogController::class]);
$routes = new RouteCollection();
$routes->add('blog_show', $route);

$context = new RequestContext();

// Routing can match routes with incoming requests
$matcher = new UrlMatcher($routes, $context);
$parameters = $matcher->match('/blog/lorem-ipsum');
// $parameters = [
// '_controller' => 'App\Controller\BlogController',
// 'slug' => 'lorem-ipsum',
// '_route' => 'blog_show'
// ]

之后我便一直在关注 nikic/FastRoute 的东西。但一开始FastRoute本身不太了解,认为很难将FastRoute和目前应用中中间件部分相结合,另一方面就是之前有些排斥直接使用函数返回的形式对路由进行定义(真香)。于是找了一些基于FastRoute的扩展repo来学习,例如:

然而league/route的实现基于PSR-7 HTTP Message,已有的Symfony\Component\HttpFoundation\{Request,Response}都不是相关继承实现,而且内部较为复杂,一时难以理解。而其他swoole框架的虽稍好理解,但也很难直接的拿来主义。就这样搁置了一阵子。


这段时间正好有些空闲,对原有依赖注入部分进行了替换(改成PHP-DI),便重新拾起了这一部分,看看能不能有更好的方法将FastRoute与项目进行联系。

回到FastRoute的官方示例中,

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
require '/path/to/vendor/autoload.php';

$dispatcher = FastRoute\simpleDispatcher(function(FastRoute\RouteCollector $r) {
$r->addRoute('GET', '/users', 'get_all_users_handler');
// {id} must be a number (\d+)
$r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler');
// The /{title} suffix is optional
$r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler');
});

// Fetch method and URI from somewhere
$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];

// Strip query string (?foo=bar) and decode URI
if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
switch ($routeInfo[0]) {
case FastRoute\Dispatcher::NOT_FOUND:
// ... 404 Not Found
break;
case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
// ... 405 Method Not Allowed
break;
case FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// ... call $handler with $vars
break;
}

并结合一些issue,我才知道,我对handler本身的理解存在偏差,一开始我以为handler只能存放类似 [AbstractController::class,'action']的动作,但是其实并不是这样的。

FastRoute在返回的$routeInfo[1]中会将定义路由时 addRoute($method,$path,$handler)的第三个参数原模原样返回。在其issue的 #186#147 中,我觉得我找到了相应添加middleware的方法。

Nevraxe/Cervo在其Router对象中,对FastRoute进行了进一步封装,但我觉得稍有过度。直接对原 FastRoute\RouteCollector 进行继承扩展似乎更为合适。最终结果如下:

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

namespace Rid\Http\Route;

use FastRoute\RouteCollector as FastRouteCollector;

/**
* Extend FastRoute\RouteCollector, So we can support middleware.
*
* Class RouteCollector
* @package Rid\Http\Route
*/
class RouteCollector extends FastRouteCollector
{
/** @var array List of middlewares called using the addMiddleware() method. */
private array $currentMiddlewares = [];

/**
* Encapsulate all the routes that are added from $func(Router) with this middleware.
*
* If the return value of the middleware is false, throws a RouteMiddlewareFailedException.
*
* @param string|string[] $middlewareClass The middleware to use
* @param callable $func
*/
public function addMiddleware($middlewareClass, callable $func): void
{
array_push($this->currentMiddlewares, ...(array)$middlewareClass);
$func($this);
array_pop($this->currentMiddlewares);
}

public function addRoute($httpMethod, $route, $handler)
{
$handler['middlewares'] = $this->currentMiddlewares;
parent::addRoute($httpMethod, $route, $handler); // TODO: Change the autogenerated stub
}
}

从中可以看到,我增加了一个addMiddleware方法,并依此扩展了原addRoute方法。在此基础上即可生成对应的可返回中间件的FastRoute对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$dispatcher = \FastRoute\simpleDispatcher($route_def, [
'routeCollector' => \Rid\Http\Route\RouteCollector::class,
]);

$dispatcher->dispatch($method, $path);
switch ($routeInfo[0]) {
case \FastRoute\Dispatcher::FOUND:
$handler = $routeInfo[1];
$vars = $routeInfo[2];
// 执行中间件和控制器,并返回结果
return $this->runWithMiddleware([$handler[0], $handler[1]], $handler['middlewares']);
case \FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
$allowedMethods = $routeInfo[1];
.....
break;
case \FastRoute\Dispatcher::NOT_FOUND:
default:
.....
break;
}

而对应路由配置文件如下:

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

use Rid\Http\Route\RouteCollector;

return function (RouteCollector $r) {
$r->addMiddleware([
// 增加全局Middleware
], function (RouteCollector $r) {
// 一些不需要中间件保护的路由
$r->get('/maintenance', [\App\Controllers\MaintenanceController::class, 'index']);

// 需要额外中间件保护的路由
$r->addMiddleware(\App\Middleware\AuthMiddleware::class, function (RouteCollector $r) {
// 增加单个路由规则
$r->get('/test', [\App\Controllers\TestController::class, 'index']);

// 增加路由组规则
$r->addGroup('/links', function (RouteCollector $r) {
$r->addRoute(['GET', 'POST'], '/apply', [\App\Controllers\LinksController::class, 'apply']);
});

// 增加路由组规则
$r->addGroup('/api', function (RouteCollector $r) {
// 增加路由组内路由组规则
$r->addGroup('/v1', function (RouteCollector $r) {
// 增加组内中间件保护
$r->addMiddleware([
\App\Middleware\AuthMiddleware::class,
\App\Middleware\ApiMiddleware::class
], function (RouteCollector $r) {
$r->addGroup('/torrent', function (RouteCollector $r) {
$r->post('/bookmark', [\App\Controllers\Api\v1\TorrentController::class, 'bookmark']);
});
});
});
});
});
};

使用用户脚本/Redirector插件自动进行VPN访问域名替换

其实自从 豆瓣下载大师 之后,本人就很少写Userscript了。

正值疫情在家科研阶段,访问论文全文数据库均需要使用学校的VPN。但因为我们学校使用的是深信服的VPN服务,不是全局代理的形式,所以就出现访问知网或者Web of Science需要通过EasyConnect的面板进入,实属麻烦,且面板中没有我经常使用的ScienceDirect。

加之,本人对论文检索通常是以Google Scholars作为入口的,所以造成了一定的不便。

image-20200419203720306.png

通过观察url地址变化,可以发现知网或者其他通过VPN访问的地址变成了如下形式

1
2
3
4
5
6
7
8
9
10
11
# 知网
https://www.cnki.net/xxxxxxx
https://www-cnki-net-s.vpn.xxxxxx.edu.cn:8118/xxxxx

# 万方
http://g.wanfangdata.com.cn/index.html
https://g-wanfangdata-com-cn.vpn.xxxxxx.edu.cn:8118/index.html

# 校内地址(示例)
http://10.10.100.100/index.html
https://10-10-100-100-p.vpn.xxxxxx.edu.cn:8118/index.html

也就是说把原始域名中的点改为-,如果使用https访问则加上-s,然后后面附加.vpn.xxxxxx.edu.cn:8118。而如果访问ip地址形式的域名,则需要先加上 -s,然后再附加 .vpn.xxxxxx.edu.cn:8118 参数。
但实际测试中发现,不加上 -s,会以302形式的跳转。

值得注意的是,VPN服务强制以https协议进行访问,所以如果之前是通过http协议访问的论文数据库,需要将schema参数改为https:

image-20200419204243264.png

综上,我们可以通过UserScript的形式,将windows.location.href参数进行替换,达到网页自动切换到VPN中的要求。

初版代码如下,但目前未处理ip地址形式的域名(这种更建议直接通过资源列表进入):

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
// ==UserScript==
// @name 对学校VPN资源进行链接转换(深信服)
// @namespace https://blog.rhilip.info/
// @version 0.1
// @author Rhilip
// @match *://*.sciencedirect.com/*
// @match *://*.cnki.net/*
// @match *://*.wanfangdata.com.cn/*
// @match *://falvmen.com.cn/*
// @match *://webofknowledge.com/*
// @match *://*.engineeringvillage.com/*
// @match *://*.springer.com/*
// @match *://*.springerlink.com/*
// @run-at document-start
// ==/UserScript==

let host_suffix = '.vpn.xxxxxx.edu.cn:8118'; // 请替换成自己学校的参数

(function() {
'use strict';
let new_href = location.href.replace(location.host, location.host.replace(/\./g,'-') + host_suffix);
if (location.protocol === 'http:') {
new_href = new_href.replace(location.protocol, 'https:');
}

location.href = new_href;
})();

如果后续要添加新的网站,也可以仿照目前@match的形式进行添加。


由于userscript注入是在 document-start,此时页面主体部分已经请求完成。其瀑布图如下。

image-20200419210118340.png

如果我们换种形式,使用Redirector插件,并作如下配置

image-20200419210653994.png

我们可以看到Network面板的请求变成如下形式,对CNKI的访问以307 Internal Redirect的形式被直接重定向到了我们学校的VPN域名上,而不是通过Userscript的得到的200响应。这比使用userscript的形式响应更快,但由于该插件的设计,导致将.替换成-的步骤较难实现。

image-20200419210608356.png

综合考虑后,使用userscript的形式更为简便23333

基于Cloudflare的NPHP站点保护

周所周知,国内多数基于NPHP构建的PT站点都是使用Cloudflare作为CDN,隐藏起自身服务器IP,防止直接面对IP的DDOS攻击。但部分攻击者同样可以使用CC的形式,恶意消耗服务器请求。(毕竟NPHP一上来就dbconn(),数据库可能撑不住)

本文通过综合运用Cloudflare Firewall规则以及Nginx规则,以达到阻拦大部分面对NPHP无脑CC的目的。

使用PowerShell脚本批量清理OneDrive历史记录以释放空间

由于Rclone在复制/移动文件到OneDrive过程中存在一些问题(特别是一些比较老的Rclone版本),容易导致部分文件出现大量历史记录。因为OneDrive对于历史记录同样计算占用空间,用户侧无法禁用该feature,所以产生了大量浪费。

image-20200311220632223.png

rclone size One: --json 的结果进行检查,如果 total - used - free - trashed 所得结果超过约 1710000 (应该是SharePoint保留空间) 的部分则基本可以代表历史记录所占空间

但Rclone不支持相关操作,且OneDrive历史记录在网页上处理相当麻烦(需要在 Site settings - Storage Metrics中对一个个文件的历史记录进行处理),在文件数众多的情况下难以有效处理。故很久之前,面对历史记录,我一般就直接放置处理(然后换用一个OD帐号)。

然而在开始使用Microsoft Graph API对OneDrive进行操作时,我就开始有意识的找一些脚本,然而很有意思的是,Graph API在目前无法对DriveItemVersion类型资源进行删除处理,此外也没有对OneDrive回收站以及二层回收站进行删除处理的相关方法。

image-20200311220214195.png

皇天不负有心人,我终于找到一篇文章 Delete old document versions from OneDrive for Business | I learned it. I share it. ,它介绍了通过SharePoint形式,对OneDrive进行操作的方式,并提供了两个PowerShell脚本和一个该文作者的C#项目 https://github.com/balassy/OneDriveVersionCleaner。

结果一开始我使用PowerShell脚本时,就出现如上文中一样的因为文件数过多的错误,在尝试balassy/OneDriveVersionCleaner 时,又因为该项目不支持Recursive,而且我对PowerShell以及C#不熟悉便草草放弃了折腾。

今天晚上忙里偷闲,翻看了了其上面两个脚本以及C#项目的实现,发现对Powershell脚本稍作修改便可以正常使用。并将其上传至GitHub上分享:

项目地址 : https://github.com/Rhilip/OneDrive_VersionHistoryCleaner

PT作弊与反作弊

在去年年底(2019年12月),我曾经公开了一个Github仓库 Rhilip/awesome_ptcheater 收集了绝大多数用于PT作弊的软件,并谋划着这篇文章。但是由于原仓库使用git-lfs的方式占用了并不多的1G空间,所以于前段时间重新整理仓库,并重建仓库以及着手这篇文章

所以此文就主要介绍这些PT作弊的软件以及比较常用的反作弊思想。

特请注意:本文不提倡在任何PT站点作弊!毕竟只要怀疑,查起来非常容易2333

部分名词解释:

  • fake seeding: 由于某peer本地文件错误,导致从该peer从获得的区块文件hash错误,该peer称为fake seeding。

PT作弊方法及作弊软件

  • 同机/内网 多客户端/多端口

顾名思义,就是在同一台及其上通过多开BT软件,利用本地或者内部高速网络进行上传和下载。最低级的作弊方法,实际消耗了对应的带宽和存储空间,只是没有FAKE,且将流量传给了一个未被private tracker知道的peer。

  • 基于直接伪装announce思想实现

由于BT协议是完全信任的协议,所以在服务器端就无法通过字符串校验等形式确认用户是否存在伪造做种信息。如以下请求(使用python requests简单示例),如果对应站点存在该info_hash的种子,这以下代码执行的结果将会在1分钟内为你的账户增加1G的上传流量。

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
import time
import requests

# 基本信息 (在整个announce阶段都不会变化)
announce_url = 'http://nexusphp.local/tracker/announce'
info_hash = '%22%31%b1%de%32%2f%4d%0e%43%fe%14%79%d1%e6%68%f3%89%02%6b%23'
peer_id = '-UT2210-%16bh%b6K3%ca%cc%ce%c7T%96'
key = 'D22EF0E5'
user_agent = 'uTorrent/2040(22967)'

params = {
# 确定种子,peer身份,开放端口
'info_hash' : info_hash, 'peer_id' : peer_id, 'port' : 58140,
# 客户端上传、下载、剩余、出错字节
'uploaded' : 80, 'downloaded' : 0, 'left' : 10923445644, 'corrupt' : 0,
'key' : key, 'event' : '', 'numwant': 200, 'compact' : 1, 'no_peer_id' : 1,
# ... 其他可能还有一些字段
}

# 注意这里使用requests自带的params方法,实际多数btclient使用的都是字符串拼接方法
r = requests.get(announce_url, params=params, headers = {'User-Agent' : user_agent}) # 第一次请求

time.sleep(60) # 休眠一段时间

params['uploaded'] = params['uploaded'] + 1 * pow(1024,3) # 伪装增加1G上传,其他不变
r = requests.get(r'%s',params=params, headers = {'User-Agent' : user_agent}) # 第二次请求

基于直接伪装announce思想实现的PT作弊软件较多,如国内早些有点名气的ptliarptliar2,以及RatioMaster系列、mRatio等都是这类的代表。关于这些软件的简单介绍可以见更早大佬们的文章:

PT流量作弊工具之mRatio
PT流量作弊工具之PTLiarPT流量作弊工具之PTLiar2
PTMaster,新的PT流量作弊工具?

如果你稍对python有了解,那么就可以发现 PTLiarPTLiar2 此类软件的实现实质就是对上述示例代码的进一步包装,并增加额外的请求、相应处理。以RatioMaster Plus为例(这款软件是),我们可以看到打开该软件后,可以对UA头信息进行任意的伪装,之后只需要添加种子并启动,程序会自动进行作弊行为。

image-20200309193011514.png

此外可以对单种做高级设置,设定更为相似正常客户端的行为。

image-20200309193749336.png

然而这些软件基本都很稳定,也就是说基本都没有更新了,软件的一些特点也比较明显,容易被管理员直接发现。

而最近发现新出现的 Joal-Server ,相比上面提到的几款在一些参数FAKE上更具有优势,也比较接近,有兴趣可以自己在小站进行测试。

  • 基于中间人代理方式实现

上面说的直接announce还需要对进行客户端模拟,那么我直接使用中间人代理是否可行?毕竟announce请求的实质是一次web请求, 同样存在 “HTTP 请求 -> HTTP 响应”的过程,也就是说有 http_connect、request、response事件。以python的mitmproxy 做示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import re
from mitmproxy import ctx
import mitmproxy.http

class DoubleUpload:

def request(self, flow: mitmproxy.http.HTTPFlow):
"""
The full HTTP request has been read.
"""
path = flow.request.path

upload_search = re.search(r'uploaded=(\d+)',path)
if upload_search:
old_upload = int(upload_search.group(1))
fix_upload = int(old_upload * 2)
ctx.log.info("Old uploaded is %s, fix it to %s" % (old_upload, fix_upload))
flow.request.path = path.replace(upload_search.group(0),'uploaded=%s' % (fix_upload,))


addons = [
DoubleUpload()
]

该脚本将会充当中间人的角色,将每次汇报的上传量变成原先的两倍。使用mitmdump或mitmproxy命令启动后,将BT软件代理地址改为127.0.0.1:8080。通过对请求字段进行分析,我们可以看到通过mitmproxy 实际发送给tracker的上传数量确实如我们预期变成了原先的两倍了。

image-20200309203536890.png

基于这种中间人代理实现的有 UNI-Leech,[Ratio Faker](https://github.com/Rhilip/awesome_ptcheater/tree/master/Ratio Faker/release),GiveMeTorrentGreedyTorrent,[Torrent Proxy](https://github.com/Rhilip/awesome_ptcheater/tree/master/Torrent Proxy/release/TorrentProxy 1.0)等,有兴趣可以自己试试。下图展示了Ratio Faker的工作界面。从上面可以看出可以将汇报的uploaded值改为实际upload、download的相关倍数,开启软件后将bittorrent的代理设置改为对应端口127.0.0.1:8282,即可作弊,且主要有两个高级工作模式:

1. 伪装成seeder,且只汇报upload,不汇报download。
	2. 不汇报upload以及download数据。

image-20200309213859178.png

基于中间人代理模式的作弊软件,相对只进行announce FAKE的软件相对更难以识别。因为他本身就是正常的客户端行为,只是将announce请求字段进行修改。

  • 直接修改客户端源码,使其汇报上传下载不符合正常情况

此条和上一条基于中间人代理在一些表现上相同,但不如中间人代理灵活,且修改客户端基本都算严重作弊。故不叙述。

  • 基于云存储挂载方式实现

关于云挂载做种算不算作弊,不同站点的sysop或者admin态度不同。有些觉得算作弊,查到就ban号;有些觉得不算作弊,且在一定程度上能够有速度,使部分种子保持活种状态。因为本人认为这算作弊行为(骗取seed_bonus,恶意限速,且十分容易出现fake seeding的情况),所以在此列出。

一般都是找一个无限空间的Google云盘(部分使用OneDrive盘),然后使用rclone mount挂载到本地目录(RaiDrive也可以)。做种软件添加种子直接跳校验辅种(注意,一般不直接下载到云挂载目录),且通过限低速和mount cache的形式防止爆API请求。

PT反作弊思路

  • 核验单种子总上传以及总下载数据

PT网络(BT网络)其实是一个对等网络,在单一种子上所有peer的总上传和总下载数据一定能核验上。但实际情况下由于汇报间隔以及可能存在fake seeding,核算总上传和总下载实际并不可能均为正好的1。

将某站的snatched表导出,分析其单种总上传和总下载及比值,共计有效个案数48752。完整统计频率数据如下表:

统计项 数值 统计项 数值
个案数(有效) 48752 峰度 41886.118
平均值 1.0646 峰度标准误差 .022
平均值标准误差 .02133 范围 1001.58
中位数 1.0080 最小值 .00
众数 1.00 最大值 1001.58
标准 偏差 4.70902 百分位数(25) 1.0005
方差 22.175 百分位数(50) 1.0080
偏度 199.025 百分位数(75) 1.0279
偏度标准误差 .011

通过判读描述信息,我们可以看出数据分布存在右偏现象,且分布的峰态十分陡峭。然而很有意思的是,对样本以及自然对数转换后的样本进行Kolmogorov-Smirnov正态检验,结果均为拒绝原假设(笑,很不符合预期)。

原假设 检验 显著性 决策
ratio 的分布为正态分布,平均值为 1.06,标准差为 4.70902。 单样本柯尔莫戈洛夫-斯米诺夫检验 .000a 拒绝原假设。
lgratio 的分布为正态分布,平均值为 .01,标准差为 .23705。 单样本柯尔莫戈洛夫-斯米诺夫检验 .000a 拒绝原假设。

然而从P-P图上看,在图左右侧出现明显的符合情况,但图主体部分满足正态分布的要求。故以95%置信区间进行单样本正态统计(后验),得到的置信区间为 1.0228-1.1064。

综合考虑后,比较建议选择1-1.1作为实际判断过程中选取的置信范围,对远超该范围的种子进行检查。(这个结论不用分析也能知道23333)

OUTPUT.png

  • 流量异常测试(上传速度限制)

很老套,目前看作用并不大,但是因为NPHP以及其他一些PT架构中使用,故做介绍。如下为NPHP默认的规则:

  • 上传量超过1GB,而且上传速度超过100 MB/s,无疑是作弊(会立刻禁用)
  • 上传量超过1GB,而且上传速度超过10 MB/s,可能是作弊(列入怀疑名单)
  • 上传量超过1GB,而且上传速度超过1 MB/s,而且此时下载人数小于2人,可能是作弊(列入怀疑名单)
  • 上传量超过10MB,而且上传速度超过100 KB/s,而且此时没有人在下载,可能是作弊(列入怀疑名单)

NPHP使用100MB/s限速ban有其历史背景,毕竟NPHP开发年代,网速并没有如今这么快,故设定100 MB/s作为超速线,并添加一些额外的判断,非常简陋。往往只需要低速长时间上传就可以绕过该流量异常测试。且往往需要管理者根据自身经验进行额外判断。(曾经某admin在线下聚会时,说他瞄一眼NPHP的作弊列表的信息,就能看出来那些人做没作弊)

  • 做种、作弊软件特征

一些比较明显的做种、作弊软件特征一般也会成为辅助管理员进行判断的标准。例如我在 Pt站点禁用Aria2客户端方法分析 一文中就是用Aria2其明显与正常BT软件不同两个特征进行判断。此外零零散散的一些特征有:

1.  uTorrent 2.x 版本不能添加1T以上种子。
	2.  作弊软件常用伪装版本号以及请求字段中的KEY信息等,例如 ptliar2 默认伪装的UA就是 uTorrent/2210(25130)。
	3. ......... (怎么可能全部告诉你)
  • 基于peer间协议

这个反作弊成本有点高,类似BT网络的蜜罐技术。通常我们知道,一般垃圾的作弊软件只管与Tracker之间annonce,而没有关注过peer之间的TCP请求。一般可以采用以下两种方式:

  1. 公网蜜罐创建socket server,tracker往peerlist中插入蜜罐信息,peer应与蜜罐进行连接测试。
  2. 蜜罐通过tracker主动获取peerlist,并对每一个peer(应具备公网可连接性)进行连接测试。

此处对第二种方式进行示例,

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import errno
import socket
import logging
import hashlib
import binascii
import bencode

from struct import pack, unpack

# HandShake - String identifier of the protocol for BitTorrent V1
HANDSHAKE_PSTR_V1 = b"BitTorrent protocol"
HANDSHAKE_PSTR_LEN = len(HANDSHAKE_PSTR_V1)


class Message:
def to_bytes(self):
raise NotImplementedError()

@classmethod
def from_bytes(cls, payload):
raise NotImplementedError()


class Handshake(Message):
"""
Handshake = <pstrlen><pstr><reserved><info_hash><peer_id>
- pstrlen = length of pstr (1 byte)
- pstr = string identifier of the protocol: "BitTorrent protocol" (19 bytes)
- reserved = 8 reserved bytes indicating extensions to the protocol (8 bytes)
- info_hash = hash of the value of the 'info' key of the torrent file (20 bytes)
- peer_id = unique identifier of the Peer (20 bytes)

Total length = payload length = 49 + len(pstr) = 68 bytes (for BitTorrent v1)
"""
payload_length = 68
total_length = payload_length

def __init__(self, info_hash, peer_id=b'-ZZ0007-000000000000'):
super(Handshake, self).__init__()

assert len(info_hash) == 20
assert len(peer_id) < 255
self.peer_id = peer_id
self.info_hash = info_hash

def to_bytes(self):
reserved = b'\x00' * 8
handshake = pack(">B{}s8s20s20s".format(HANDSHAKE_PSTR_LEN),
HANDSHAKE_PSTR_LEN,
HANDSHAKE_PSTR_V1,
reserved,
self.info_hash,
self.peer_id)

return handshake

@classmethod
def from_bytes(cls, payload):
pstrlen, = unpack(">B", payload[:1])
pstr, reserved, info_hash, peer_id = unpack(">{}s8s20s20s".format(pstrlen), payload[1:cls.total_length])

if pstr != HANDSHAKE_PSTR_V1:
raise ValueError("Invalid string identifier of the protocol")

return Handshake(info_hash, peer_id)


def read_from_socket(sock):
data = b''

while True:
try:
buff = sock.recv(4096)
if len(buff) <= 0:
break

data += buff
except socket.error as e:
err = e.args[0]
if err != errno.EAGAIN or err != errno.EWOULDBLOCK:
logging.debug("Wrong errno {}".format(err))
break
except Exception:
logging.exception("Recv failed")
break

return data


if __name__ == '__main__':
# 打开一个info_hash为 b'91fb97619ef887d439a2142a2f9530b080cfbfd0' 的种子
torrent_file = r'D:\Downloads\1.torrent'

with open (torrent_file,'rb') as f:
torrent_dict = bencode.bread(f)

raw_info_hash = bencode.bencode(torrent_dict['info'])
info_hash = hashlib.sha1(raw_info_hash).digest()
hex_info_hash = binascii.hexlify(info_hash)

assert b'91fb97619ef887d439a2142a2f9530b080cfbfd0' == hex_info_hash

# 通过请求tracker获得peer_list,这里直接假定获得的ip+port以及peer_id prefix
peer_ip = '127.0.0.1'
peer_port = 8299
peer_id_prefix = b'-qB4210-'

# 尝试连接peer并获取Handshake信息
try:
peer_socket = socket.create_connection((peer_ip,peer_port),timeout=2) # 尝试连接
#peer_socket.setblocking(False)
peer_socket.send(Handshake(info_hash).to_bytes()) # 发送握手信息
raw_data = read_from_socket(peer_socket) # 从套接字中获取peer返回的握手信息
if raw_data == b'':
raise ValueError('Handshake Failed')

parsed_handshake = Handshake.from_bytes(raw_data) # 解析peer返回的握手信息
if parsed_handshake.peer_id.find(peer_id_prefix) == -1:
raise ValueError('The Peer Id (%s) from Handshake not what we wan\'t ' % (parsed_handshake.peer_id))

print('This peer is exist and pass the handshake check')
except Exception as e:
print("Failed to Handshake with peer (ip: %s - port: %s - %s)" % (peer_ip, peer_port, e.__str__()))

通过类似脚本,我们可以主动探测一个peer是否运行的BT客户端。如果该用户使用BT软件做种,则通过BT握手检查;如果该用户未使用BT软件做种,或者通过握手检查返回的peer_id非我们需要检查的peer_id,则会报错。且如果你未关闭python运行终端,你还可以从被检测的peer处看到以下蜜罐信息。

image-20200310151954214.png

此外,你还可以在对BT协议有者更深入的理解上,构造request等其他信息来进一步探测peer情况。

Github Action 尝试报告

近期,我为个人的三个仓库分别添加了 GitHub Action 作为CI,此前我也使用过 Travis CI作为CI服务(见 Rhilip/pt-gen-cfworker),但此次尝试仍有部分地方觉得很有意思,便于此记录。这三个仓库及其使用Action的目的分别如下:

R酱的资源收纳库(Symfony 5+Vue)

** 网址: https://share.rhilip.info/#/ **

update 2020.07.20: 因OneDrive于2020年7月初大量杀号,所以两个站点均已关闭。
update 2020.08.04: 使用备用的OneDrive域开始恢复,之前的分享只剩下一个账号还活着,其他看情况,能补就补吧。。资源仓库( https://archive.rhilip.info/ )应该是不再开了,也没精力再做整理了。

在2019年初,随着接触到OneDrive和Google Drive后,我开始使用这两个在线服务存储发种姬发布过的种子资源。并在之后使用过 donwa/OneIndex 搭建过在线目录程序,当时的网址是 https://seedbox.rhilip.info/oneindex ,因为经常性出现白屏,于19年中旬就关闭了。(说起来也比较有意思,虽然该域名连DNS解析都已经停了,但目前在Google给我发送的搜索结果表现中仍然存在且高居榜首)

也正如我在 R酱の资源仓库 中的说明一样,我依次尝试 PyOne、CuteOne、OLAINDEX 之后,开始采用OneDrive分享链接的形式进行资源分享。这种方法很好,通过 脚本自动生成分享链接(见前文 如何批量生成OneDrive分享链接 )+git自动同步 的形式,我可以很方便的将最新的资源通过OneDrive形式进行分享。

image-20200223185600543.png

这样也存在一些不足,比如说,分享更新不及时,往往都是塞满一个OD盘之后才开始建立分享,然后进行链接整理;管理起来也略显麻烦,有些不是很适合归类的难以进行发布。

所以,前段时间,我觉得需要另写一套工具,来实现整个 资源下载+OD或GD上传备份+OD分享+资源展示 链条。那么结果就是一个新的网站 R酱的资源收纳库。其整个技术栈如下:

  • 前端 Vue + Vue Route , 前端项目开源在 Rhilip/od_share_frontend 并使用Github Action进行自动构建
  • 后端 API : Symfony 5 , transfer: Python Scirpt + Rclone

NexusPHP 建站优化 (3) 升级NPHP到PHP 7

因为NexusPHP较早就停止维护,所以官方源码基本只能停留在PHP5.3-5.6版本使用,无法使用PHP7,然而随着PHP5.x(甚至PHP7.0)已经完全停止维护,势必有必要将NPHP推进到PHP7.x。然而主要阻碍这种推进的原因是因为:

  1. Mysql库在PHP7中不存在,必须更换到 Mysqli库。
  2. Memcache库在PHP7出现兼容性问题,需要调整连接代码,或更换到 Memcached库 或者 Redis库。
  3. Github或其他开源代码库中没有PHP7版本的NexusPHP。

基于以上原因,本文给出相关方法实现:

  • 使用psr-4相关方法,加载/classes目录中库文件。
  • 替换Cache组件的后端为Redis。
  • 使用单例模式的Mysqli wrapper组件替换原Mysql库相关方法,将Mysql连接改成只有第一次执行query或相关方法时才连接,避免NPHP遭受CC攻击时,大量连接请求直接拖垮Mysql导致服务不可用;并提供stmt方法支持,可以通过更换写法的形式组件将NPHP的real_query实现改成stmt实现,防止SQL注入。
  • 移除PHP 7中不存在的相关方法,例如 get_magic_quotes_gpc 等。

以下讲解和代码patch均基于本人fork的官方源码 Rhilip/NexusPHP(v1.5.beta5.20120707),不提供除本文外的任何形式的说明以及免费讲解

具体请见:Rhilip/NexusPHP#2

image-20200207133455442.png

请注意:

  1. 可以使用该库 MySQL wrapper for MySQLi ,快速提供mysqli库在mysql方法名下的支持,但因为其过于简单,且无法使用到stmt特性,故本文不做额外说明。
  2. 部分小细节,比如 $arr[id] ,PHP 4 style constructors 等未在此步骤中体现。你应该参照PHP官方升级教程 Migrating from PHP 5.6.x to PHP 7.0.x 或使用 PHPStan 等静态工具进行进一步的检查。

NexusPHP 建站优化 (2) 替换Bencode库

我曾在 PHP 下 Bencode 库差异及性能对比 一文中,通过对比指出NPHP在解析多文件(>1k)种子时,因为原解析库的低效率问题,导致性能过差的问题,并给出了相关解决方法。

但是随着TJUPT代码库变成private状态(示例没了),以及 Rhilip/Bencode 以基础库形式发布在 https://packagist.org/ 上并维护。势必有必要重新写一个commit来说明如何替换Bencode库。

以下讲解和代码patch均基于本人fork的官方源码 Rhilip/NexusPHP(v1.5.beta5.20120707),不提供除本文外的任何形式的说明以及免费讲解

具体请见: Rhilip/NexusPHP#1

image-20200206120615167.png

include/benc.php 调用分析

通过对 NPHP 原使用的benc.php 文件中函数调用关系进行分析,我们可以知道在以下文件中调用了NPHP原benc库相关方法并进行替换。

image-20200206104748443.png

image-20200206104819933.png

相关修改步骤

  1. 使用Composer进行rhilip/bencode库加载(注意,rhilip/bencode 库要求PHP大于5.6),并在include/core.php中添加autoload。 (@888b2107)

    1
    composer require rhilip/bencode
  2. 替换一系列和benc相关的文件,涉及列表如下 (@a24393a2)

    image-20200206115358707.png

  3. 移除原benc文件 (@c5903c39)

Python下载国自然结题报告 + 初尝Vue项目构建

前段时间,我导师布置任务,让我根据一些关键词主题以及接下去的工作任务查找国自然的一些项目,看看其他人的科研经验。

然而假期嘛~ 所以直到前几天老师打电话催问的时候,我才想起来做。

为了体现工作量,我认真找了下相关课题,并准备把 科学基金共享服务网(科技成果信息系统) 上其结题报告下载了下来。在此期间,从Google、GitHub等处均搜索了相关方法,感觉都不是很好,所以自己写了个脚本。

其实本文章原本是想介绍本人写的 Rhilip/NSFC_conclusion_downloader 仓库,顺带解释下我前段时间摸鱼的原因。(然而这种脚本就是随手写写的,所以是真的摸鱼了

但是昨天不知道想了些什么,突然觉得是不是可以把这个做成一个网页工具,供给其他人员在线使用。毕竟整体逻辑特别简单,于是便开始编写,顺带又突然想试试Vue,就上手了开发,现两个项目地址分别如下:

然而Vue版就最终完成的程度来看(连Vue入门加测试写了一天半),实际效果并不如我的想象,所以在写完并测试完最后一个component之后就扔到GitHub上存档了。主要的原因是浏览器的CORS策略影响太大,后面我会在Vue版开发过程介绍中详细说明。