最近有点忙,实在抽不出时间更新,而且可以预见的短期内也没有充足的时间去找素材,写文章。如果有朋友在等更新,还请见谅。
什么是aria2
aria2是一个轻量级的,可以支持多种协议(不支持ed2k)的下载软件,支持多种操作系统,同时也提供了RPC调用的能力。
部署方法很简单:
1. 下载,解压,下载地址: https://github.com/aria2/aria2/。
2. 网上抄一份配置文件再根据自己情况稍微改一改,带上指定配置文件路径的参数–conf-path=/smbdata/soft/aria2/aria2.conf运行aria2c程序;如果想无人值守运行,也可以用supervisor来跑这个任务;官方参考手册地址:https://aria2.github.io/manual/en/html/aria2c.html。
3. 下载一个前端管理项目进行任务管理和配置管理,比如AriaNG,https://github.com/mayswind/AriaNg(可以放到本地,直接打开网页文件,也可以放到服务器上发布为一个web站点),这一步在日常使用场景里是必需的,但是在本文所述场景里用不到,因为我们要做的是和这个前端管理项目一样的功能。
关于第3步,一般这种项目都是靠rpc访问服务器(运行aria2程序的主机),第一次访问前端项目的时候需要指定服务器地址,端口,rpc路径,token(与配置文件里的rpc-secret参数一致)等关键信息,填写完毕,刷新页面即可生效。
下图的配置将http://192.168.3.4:6800/jsonrpc设置为rpc服务地址。
dir=/smbdata/download
log=/smbdata/soft/aria2/aria2.log
input-file=/smbdata/soft/aria2/aria2.session
save-session=/smbdata/soft/aria2/aria2.session
save-session-interval=60
force-save=true
log-level=error
max-concurrent-downloads=5
continue=true
max-overall-download-limit=0
max-overall-upload-limit=50K
max-upload-limit=20
connect-timeout=120
lowest-speed-limit=10K
max-connection-per-server=10
max-file-not-found=2
min-split-size=1M
split=5
check-certificate=false
http-no-cache=true
bt-enable-lpd=true
#bt-max-peers=55
follow-torrent=true
enable-dht6=false
bt-seed-unverified
rpc-save-upload-metadata=true
bt-hash-check-seed
bt-remove-unselected-file
bt-request-peer-speed-limit=100K
seed-ratio=0.0
enable-rpc=true
pause=false
rpc-allow-origin-all=true
rpc-listen-all=true
rpc-save-upload-metadata=true
rpc-secure=false
rpc-secret=*******#这儿自己随便设置一个token
daemon=true
disable-ipv6=true
enable-mmap=true
file-allocation=falloc
max-download-result=120
#no-file-allocation-limit=32M
force-sequential=true
parameterized-uri=true
[program:aria2]
command=/smbdata/soft/aria2/aria2c --conf-path=/smbdata/soft/aria2/aria2.conf
directory=/smbdata/soft/aria2
autostart=true
autorestart=true
stderr_logfile=/smbdata/soft/aria2/daemon.err
stdout_logfile=/smbdata/soft/aria2/daemon.log
log_stderr=true
log_stdout=true
user=root
如何远程调用
配置文件中有一个参数 enable-rpc=true 用于打开rpc功能,rpc-secret参数用于设置鉴权所用的token。其它几个相关参数见下图:
官方手册对rpc请求有详细的介绍:https://aria2.github.io/manual/en/html/aria2c.html#json-rpc-using-http-get。
当然也可以直接用万能的F12方法分析一下几个场景的网络请求。
1. 修改参数Max Concurrent Downloads:
2. 获取服务器状态getGlobalStat
3. 获取当前正在下载的任务tellActive
从上图可以看出,所有请求都需要被构造为一个请求对象,并将这个对象以json形式向 http://192.168.3.4:6800/jsonrpc 发送。基本结构有
- jsonrpc: “2.0”, #固定的版本号
- method: “” #所请求的方法
- params: [] #描述详细请求内容的数组,比如:
- 使用getGlobalStat方法,获取服务器详情的时候,这个数组只包含一个”token”:”xxxx”参数;
- 使用changeGlobalOption方法,修改参数Max Concurrent Downloads,需要用到token参数,以及一个键值对{“max-concurrent-downloads”:”6″}
另外,jsonrpc既可以用post请求也可以用get请求。如果使用get请求,method和id可以直接传UTF8明文,params需要用base64进行编码。
如何集成到公众号
公众号打开开发模式后,所有信息(用户消息、系统消息)都将由微信服务器发到开发者指定的监听服务地址,由用户自行处理。要和公众号集成,无非就是针对具体的某一种特征的用户消息类型进行特殊处理。用户在使用的时候将种子文件或者下载链接发到公众号,再由公众号服务器调用aria2的jsonrpc添加相应的任务。
但是这样部署会存在一定的安全隐患,aria2的jsonrpc服务直接暴露在了公网,比较安全的做法是写一个前置代理对请求进行鉴权和过滤,再将合法的请求转给aria2的rpc服务,或者使用白名单模式对jsonrpc服务的请求进行管控(因为公众号服务器的地址是固定的,所以这个场景对白名单方案特别友善)。
需要实现哪些功能
首先,需要实现查看当前服务器包括正在下载、等待下载、下载中断/停止在内的所有任务。
其次,需要可以添加常见下载类型的任务,包括http, ftp, magnet。
由于公众号不支持file类型的文件,任何非媒体文件(包括torrent)上传以后,服务器无法获取到文件内容,因此torrent类型的下载功能暂时无法直接在公众号对话窗口实现,但是在小程序或者H5应用里不会有这个问题。
aria2不支持ed2k。
aria2不支持ed2k。
aria2不支持ed2k。
public static class DownloadHelper
{
const string SERVER = "http://……:6800/jsonrpc";
/// <summary>
/// URL类型的链接,包括http, https, ftp, 有/无前缀的magnet(32位和40位)
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string DownloadLink(string url)
{
//去掉magnet文件名参数,有些特殊字符会造成下载失败
url = Regex.Replace(url, "&dn=[^&]+(?=(&|$))", "");
using WebClient client = new WebClient();
client.Headers["Content-Type"] = "application/json;charset=UTF-8";
var res = client.UploadData(SERVER,
Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(new RPCReq("addUri", new List<object> { url }))
));
string strRes = Encoding.UTF8.GetString(res);
return JsonConvert.DeserializeObject<DownRes>(strRes).result;
}
/// <summary>
/// torrent类型的资源,将torrent文件base64编码后作为参数的一部分上传
/// </summary>
/// <param name="file"></param>
/// <returns></returns>
public static string DownloadTorrent(string file)
{
using WebClient client = new WebClient();
client.Headers["Content-Type"] = "application/json;charset=UTF-8";
var res = client.UploadData(SERVER,
Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
new RPCReq("addTorrent", new List<object> {
Convert.ToBase64String(Encoding.UTF8.GetBytes(file)),
new List<string>(),
new object() }))));
string strRes = Encoding.UTF8.GetString(res);
return JsonConvert.DeserializeObject<DownRes>(strRes).result;
}
/// <summary>
/// 接受本地文件torrent路径,
/// 带协议前缀的http, https, ftp, magnet(32位和40位)链接,
/// 不带协议前缀的32位和40位的磁力链接,并根据类型调用下载接口
/// </summary>
/// <param name="input"></param>
public static string Download(string input)
{ //32位base32格式的磁力,或者40位Hex格式的磁力,不带协议前缀
//带协议前缀的http,https,ftp,magnet
if (Regex.IsMatch(input, "([a-fA-F0-9]{40}&?|^[a-zA-Z2-7]{32}&?)") ||
Regex.IsMatch(input, "(http(s?)|ftp|magnet):"))
{
DownloadLink(input);
}
else if (Regex.IsMatch(input, @"(^/|^[C-Zc-z]:\\)"))
{//本地文件路径
DownloadTorrent(input);
}
Thread.Sleep(2000);
return GetFullStatus();
}
/// <summary>
/// 获取服务器状态,以及每个任务的状态和进度
/// </summary>
/// <returns></returns>
public static string GetFullStatus()
{
string result = "";
List<object> columns =
new List<object> { "gid", "files", "totalLength", "bittorrent", "completedLength" };
using WebClient client = new WebClient();
foreach (var method in new List<string> { "Active", "Waiting", "Stopped" })
{
client.Headers["Content-Type"] = "application/json;charset=UTF-8";
var res = client.UploadData(SERVER,
Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
new RPCReq(
$"tell{method}", columns)
)
));
string strRes = Encoding.UTF8.GetString(res);
result += $"【{method}】\r\n" + string.Join("\r\n",
JsonConvert.DeserializeObject<Tasks>(strRes).result.Select(t =>
$"[{t.Name}]\r\n {t.totalLength >> 20} MB, " +
$"{t.completedLength * 1.0 / t.totalLength:P2}")) + "\r\n\r\n";
}
client.Headers["Content-Type"] = "application/json;charset=UTF-8";
var stat = Encoding.UTF8.GetString(client.UploadData(SERVER,
Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(
new RPCReq($"getGlobalStat", new List<object>())))));
var statobj = JsonConvert.DeserializeObject<ServerStat>(stat).result;
return $"下载:{statobj.numActive}, 等待:{statobj.numWaiting},停止:{statobj.numStopped}\r\n{result}";
}
}
public class RPCReq
{
public string id { get; set; }
public const string jsonrpc = "2.0";
public string method { get; set; }
public List<object> @params =
new List<object> { "token:c4c……9b" };
/// <summary>
///
/// </summary>
/// <param name="_method"></param>
/// <param name="_params"></param>
/// <param name="append"></param>
public RPCReq(string _method, List<object> _params)
{
id = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));
method = $"aria2.{_method}";
if (_method.ToUpper() == "ADDURI")
{
@params.Add(_params);
@params.Add(new object());
}
else
{
switch (_method.ToUpper())
{
case "TELLSTOPPED":
@params.Add(0);
@params.Add(1000);
break;
case "TELLWAITING":
@params.Add(-1);
@params.Add(1000);
break;
default:
break;
}
@params.Add(_params);
}
}
}
public class DownRes
{
/// <summary>
/// 返回任务ID,其它不重要的字段,比如id, jsonrpc就不处理了。
/// </summary>
public string result { get; set; }
}
public class Tasks { public List<DownTask> result { get; set; } }
public class ServerStat
{
public ServerStatResult result { get; set; }
}
public class ServerStatResult
{
public int numActive { get; set; }
public int numStopped { get; set; }
public int numWaiting { get; set; }
}
public class DownTask
{
public TorrentObj bittorrent { get; set; }
public long completedLength { get; set; }
public long totalLength { get; set; }
public List<TaskFileInfo> files { get; set; }
public string Name => bittorrent?.info is null ?
new FileInfo(files[0].path).Name :
new FileInfo(bittorrent.info.name).Name;
}
public class TorrentObj { public TorrentObjInfo info { get; set; } }
public class TorrentObjInfo { public string name { get; set; } }
public class TaskFileInfo { public string path { get; set; } }