2.10测试版

ipv6地址也会使用::结尾,代表后面全是0缩写,不一定是::1 ::2

目前 IP 过滤还是只能在 GUI 上面操作吗,是否有 WebAPI 可用了呢?

目前先把自动订阅和IP压缩的部分完善一下
webui的接口应是手动屏蔽的一部分

还有个303状态码下载错误,其它任何一款下载软件都工作正常,就比特彗星不行

有了ip过滤器,整个世界都清净了,每次打开bitcomet都有近10mb/s的上传速度,一开始我还以为这是正常现象,直到有一个bitcomet贴吧的大佬告诉我gopeed dev是放血客户端,然后我就禁掉了gopeed dev,ban掉了两个ip,然后速度还是很快,自从我使用了ip规则,发现有十几个ip被ban,我才知道gopeed dev只是冰山一角,用其他的客户端隐藏得更深。不是gopeed dev是吸血客户端,而是gopeed dev用户少,相比之下放血ip的占比就高,所以容易发现,那些伪装成bitcomet火或qbittorrent的ip不用PBH还真没办法对付它。

还行吧,主要吸血的就qbittorrent比较多,只下载不上传

0923版本统计信息,本地IP获取为空,不知什么原因。绿灯处不显示内网IP


现在也有伪装成qb的异常客户端了
Gopeed dev它们已经算不上吸血了,纯放血,吸血的拿了数据至少有用,这帮子软件都不往硬盘里进的

beta5已发布,欢迎试用

3個讚

新的功能效果不错

Web UI 中的端口监听设置已经可用了
可以方便的修改监听端口 可以补全之前STUN 穿透中最后一块不能自动化的部分

建议可以学习qb添加一个对本地地址或指定地址段免除身份验证的选项
这样可以极大的简化自动更新参数时的复杂程度

手动的IP提交也已经可用了
不过关闭窗口后再打开 在编辑框内看不见之前提交的内容
建议将自动订阅的IP列表和手动提交的拆分开来

而且目前该编辑框右键无法弹出菜单 粘贴内容
只能使用快捷键

远程访问这边 可以在webUI中开关远程访问功能
感觉这样容易出现误操作 导致失联 远程访问功能应保持强制勾选


自动语言识别功能好像又坏了 默认又变成了英文
之前已经修正的部分翻译显示问题又出现了


提示改进意见

文件列表 追踪器 用户列表 目前在没有选中任务的时候 依然显示的是 no data available
可以参考摘要中的提示进行修改

文件列表》请选择一个任务以查看文件列表
追踪器》请选择一个任务以查看追踪器列表
用户列表》请选择一个任务以查看用户列表


该测试版的端口检测似乎也存在一些问题
即使端口已经开放 其也总是显示 阻塞

使用外部工具对端口进行扫描 使其可以接收到主动传入连接
也不能改变显示的端口状态

来自B站用户的错误报告 在写入文件时遇到 “执行页内操作时的错误”

看了一下,还需要 Peer 的上传大小和下载大小,GUI 上是有现成数据的,能考虑提取到 WebUI 吗。没有这个数据的话,启发式检测功能无法正常工作。

2個讚

旧版webUI是有的 新版也加一下吧
@wxhere15
image

有人知道新版 WebUI 这个 AES 加密是怎么搞的吗?

JS 被压缩混淆之后毫无可读性,勉强靠着搜索能看出来用的 AES/CBC/PKCS7Padding。
我试了一下用 AES/CBC/PKCS7Padding 对着 {"username": "test", "password": "test"} 加密,key 是 clientId (通过 UUID 随机生成),但扔给 BitComet 好像不认,返回错误:{"error_code":"INVALID_TOKEN","version":"2.10"}

然后我又把 JS 丢给 GPT,但生成的代码好像也不对:

生成的 Java AES 处理代码
package com.ghostchu.peerbanhelper.downloader.impl.bitcomet.crypto;

import org.json.JSONObject;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;

public class BCAESTool {
    private static final String AES_ALGORITHM = "AES";
    private static final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
    private static final String HMAC_ALGORITHM = "HmacSHA256";
    private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
    private static final int PBKDF2_ITERATIONS = 10000;
    private static final int KEY_SIZE = 256; // 256 bits
    private static final int SALT_LENGTH = 8; // 64 bits
    private static final int IV_LENGTH = 16; // 128 bits
    private static final int HMAC_LENGTH = 32; // 256 bits

    // Generate random bytes
    private static byte[] generateRandomBytes(int length) {
        byte[] randomBytes = new byte[length];
        new SecureRandom().nextBytes(randomBytes);
        return randomBytes;
    }

    // Derive key using PBKDF2
    private static byte[] deriveKey(String password, byte[] salt, int keyLength) throws Exception {
        KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, keyLength);
        SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return factory.generateSecret(spec).getEncoded();
    }
    

    // AES encryption
    public static String encrypt(String plaintext, String password) throws Exception {
        byte[] salt1 = generateRandomBytes(SALT_LENGTH); // First salt for AES key
        byte[] aesKey = deriveKey(password, salt1, KEY_SIZE);

        byte[] salt2 = generateRandomBytes(SALT_LENGTH); // Second salt for HMAC
        byte[] hmacKey = deriveKey(password, salt2, KEY_SIZE);

        byte[] iv = generateRandomBytes(IV_LENGTH); // Initialization vector
        SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, AES_ALGORITHM);
        Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));
        byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

        // Concatenate version, salts, iv, ciphertext
        byte[] version = {0x03, 0x01}; // Example version bytes
        byte[] combinedData = new byte[version.length + salt1.length + salt2.length + iv.length + encrypted.length];
        System.arraycopy(version, 0, combinedData, 0, version.length);
        System.arraycopy(salt1, 0, combinedData, version.length, salt1.length);
        System.arraycopy(salt2, 0, combinedData, version.length + salt1.length, salt2.length);
        System.arraycopy(iv, 0, combinedData, version.length + salt1.length + salt2.length, iv.length);
        System.arraycopy(encrypted, 0, combinedData, version.length + salt1.length + salt2.length + iv.length, encrypted.length);

        // Generate HMAC
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(new SecretKeySpec(hmacKey, HMAC_ALGORITHM));
        byte[] hmac = mac.doFinal(combinedData);

        // Append HMAC to the data
        byte[] finalData = new byte[combinedData.length + HMAC_LENGTH];
        System.arraycopy(combinedData, 0, finalData, 0, combinedData.length);
        System.arraycopy(hmac, 0, finalData, combinedData.length, HMAC_LENGTH);

        // Return as Base64 encoded string
        return Base64.getEncoder().encodeToString(finalData);
    }
}

这个是调用代码:

...
        Map<String, String> loginAttemptCred = new HashMap<>();
        loginAttemptCred.put("username", config.getUsername());
        loginAttemptCred.put("password", config.getPassword());
        var aesEncrypted = BCAESTool.encrypt(JsonUtil.standard().toJson(loginAttemptCred), clientId.toString());
        Map<String, String> loginJsonObject = new HashMap<>();
        loginJsonObject.put("authentication", aesEncrypted);
        loginJsonObject.put("client_id", clientId.toString());
        try {
            HttpResponse<String> request = httpClient.send(MutableRequest.POST(apiEndpoint + BCEndpoint.USER_LOGIN.getEndpoint(),
                            HttpRequest.BodyPublishers.ofString(JsonUtil.standard().toJson(loginJsonObject)))
                    , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
            var loginResponse = JsonUtil.standard().fromJson(request.body(), BCLoginResponse.class);
...

感觉除了 AES 加密以外,估计还有一段自订的复杂逻辑,但是实在是推进不下去了。
有点头大了。

直接post登录信息或者取cookie不是更好吗?

直接post登录信息

现在就是在 POST 登录信息去登陆了,POST 的信息是加密的,必须现在本地进行加密运算

以下是用户名和密码都为 test 时,通过 WebUI 登陆时发送的数据:

POST http://192.168.2.17:23253/api/webui/login

{
	"client_id": "50388af6-30ce-4205-8c65-5e30abb81023",
	"authentication": "AwHAWpnEwpqAQylvsoRpZW+xdKYDVqNFi9Co9VRCEpFngIFZs4QA6uoVAnvgf/LZJNXuNbMDfnXtsqksiH62HY7p+FDjd1xH6tzU23e/ZPb1IWcZubsopyQkned5r93+zk8AuqFzZizQQvBh2rlLinU2"
}

或者取cookie不是更好吗

不管是新版 BitComet WebUI 还是旧版 BitComet WebUI,都没用过 cookie (所以不存在抓 cookie 这一说),而是通过 Authorization 请求头,带着 Bearer <Device Token> 去鉴权。而目前模拟 POST 也是为了得到这个 Device Token。

为什么不使用旧版 WebUI 登陆了再请求新 API

  • 我测试了一下,使用旧版 WebUI 的凭据,无法请求新版 API,只能用新版的 WebUI 登录给的凭据
  • 旧版 WebUI 在未来肯定会被删除,我不想去兼容一个马上就要淘汰的东西。如果在未来某一天因为这个原因出现中断,会导致用户不愿意升级 BitComet,我想这是大家都不愿看到的。要支持肯定用新的。
2個讚

已经建议像qb那样 可对指定地址免除身份验证


或者可以增加一个令牌机制 像许多其他的API那样

免除身份验证不是最好的解决方案,因为实践中,是有通过互联网同时添加多个下载器控制的情况。
只能说是能够缓解问题,但无法解决问题(因为还是无法完成登录流程)。

更新:刚刚群友已经找到了正确的算法。我去学习一下

2個讚

新版有使用cookie 其可被用于记忆语言选项和保持登录状态

没有用到哦,实际上用的是 localStorage,和 cookies 有点像,但不是 cookies。

1個讚