diff --git a/README.md b/README.md index dcad07d..88ec7f5 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ - [奶牛快传 (cow)](https://cowtransfer.com/) - [ ] 登录, 上传, 下载, 分享 - [X] 直链解析 -- [移动云空间 (ec)](https://www.ecpan.cn/web) +- [移动云云空间 (ec)](https://www.ecpan.cn/web) - [ ] 登录, 上传, 下载, 分享 - [X] 直链解析 - [小飞机网盘 (fj)](https://www.feijipan.com/) @@ -67,7 +67,7 @@ your_host指的是您的域名或者IP,实际使用时替换为实际域名或 - 直链JSON: `通用接口`和`标志短链`前加上`/json` 加密分享的密码规则同上; - 网盘标识参考上面网盘支持情况 - 括号内是可选内容: 表示当带有分享密码时需要加上密码参数 -- 移动云空间,小飞机网盘的加密分享的密码可以忽略 +- 移动云云空间,小飞机网盘的加密分享的密码可以忽略 - 移动云空间分享key取分享链接中的data参数,比如`&data=xxx`的参数就是xxx 规则示例: @@ -80,7 +80,7 @@ your_host指的是您的域名或者IP,实际使用时替换为实际域名或 http://your_host/json/parser?url=分享链接(&pwd=xxx) http://your_host/json/网盘标识/分享key(@分享密码) 3. 需要特殊处理的网盘分享: - 1. 移动云空间(ec)使用parser?url= 解析时因为分享链接比较特殊(链接带有参数且含有#符号)所以要么对#进行转义%23要么直接去掉# 或者URL直接是主机名+'/'跟一个data参数 + 1. 移动云云空间(ec)使用parser?url= 解析时因为分享链接比较特殊(链接带有参数且含有#符号)所以要么对#进行转义%23要么直接去掉# 或者URL直接是主机名+'/'跟一个data参数 比如 http://your_host/parser?url=https://www.ecpan.cn/web//yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=81027a5c99af5b11ca004966c945cce6W9Bf2&isShare=1 http://your_host/parser?url=https://www.ecpan.cn/web/%23/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=81027a5c99af5b11ca004966c945cce6W9Bf2&isShare=1 http://your_host/parser?url=https://www.ecpan.cn/&data=81027a5c99af5b11ca004966c945cce6W9Bf2&isShare=1 @@ -146,17 +146,17 @@ GET http://127.0.0.1:6400/json/fc/e5079007dc31226096628870c7@QAIU # 网盘对比 -| 网盘名称 | 可直接下载分享 | 加密分享 | 初始网盘空间 | 单文件大小限制 | 登录接口 | -|------------|------------------------|----------|-----------|-----------------|------| -| 蓝奏云 | √ | √ | 不限空间 | 100M | TODO | -| 奶牛快传 | √ | X | 10G | 不限大小 | TODO | -| 移动云空间 | √ | √(密码可忽略) | 5G(个人) | 不限大小 | TODO | -| UC网盘 | 需要登录 | √ | 10G | 不限大小 | TODO | -| 小飞机网盘 | √ | √(密码可忽略) | 10G | 不限大小 | TODO | -| 360亿方云 | √(试用账号有时间限制企业版需要599续费) | √(密码可忽略) | 100G(须实名) | 不限大小 | TODO | -| 123云盘 | √ | √ | 2T | 100G(>100M需要登录) | TODO | -| 文叔叔(TODO) | √(注意有时间限制) | √ | 10G | 5GB | TODO | -| 夸克网盘(TODO) | 需要登录 | √ | 10G | 不限大小 | TODO | +| 网盘名称 | 免登陆下载分享 | 加密分享 | 初始网盘空间 | 单文件大小限制 | +|-------------|---------|----------|-----------|-----------------| +| 蓝奏云 | √ | √ | 不限空间 | 100M | +| 奶牛快传 | √ | X | 10G | 不限大小 | +| 移动云云空间(个人版) | √ | √(密码可忽略) | 5G(个人) | 不限大小 | +| 小飞机网盘 | √ | √(密码可忽略) | 10G | 不限大小 | +| 360亿方云 | √ | √(密码可忽略) | 100G(须实名) | 不限大小 | +| 123云盘 | √ | √ | 2T | 100G(>100M需要登录) | +| 文叔叔 | √ | √ | 10G | 5GB | +| 夸克网盘 | x | √ | 10G | 不限大小 | +| UC网盘 | x | √ | 10G | 不限大小 | # 打包部署 @@ -164,7 +164,7 @@ GET http://127.0.0.1:6400/json/fc/e5079007dc31226096628870c7@QAIU - [阿里jdk17(Dragonwell17-windows-x86)](https://lz.qaiu.top/ec/e957acef36ce89e1053979672a90d219n) - [阿里jdk17(Dragonwell17-linux-x86)](https://lz.qaiu.top/ec/6ebc9f2e0bbd53b4c4d5b11013f40a80NHvcYU) - [阿里jdk17(Dragonwell17-linux-aarch64)](https://lz.qaiu.top/ec/d14c2d06296f61b52a876b525265e0f8tzxTc5) -- [解析有效性测试-移动云空间-阿里jdk17-linux-x86](https://lz.qaiu.top/json/ec/6ebc9f2e0bbd53b4c4d5b11013f40a80NHvcYU) +- [解析有效性测试-移动云云空间-阿里jdk17-linux-x86](https://lz.qaiu.top/json/ec/6ebc9f2e0bbd53b4c4d5b11013f40a80NHvcYU) ## 开发和打包 diff --git a/parser/src/main/java/cn/qaiu/parser/impl/EcTool.java b/parser/src/main/java/cn/qaiu/parser/impl/EcTool.java index f84afc1..c461ff0 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/EcTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/EcTool.java @@ -13,7 +13,7 @@ import io.vertx.uritemplate.UriTemplate; */ public class EcTool extends PanBase implements IPanTool { private static final String FIRST_REQUEST_URL = "https://www.ecpan.cn/drive/fileextoverrid" + - ".do?chainUrlTemplate=https:%2F%2Fwww.ecpan" + + ".do?extractionCode={extractionCode}&chainUrlTemplate=https:%2F%2Fwww.ecpan" + ".cn%2Fweb%2F%23%2FyunpanProxy%3Fpath%3D%252F%2523%252Fdrive%252Foutside&parentId=-1&data={dataKey}"; private static final String DOWNLOAD_REQUEST_URL = "https://www.ecpan.cn/drive/sharedownload.do"; @@ -27,7 +27,11 @@ public class EcTool extends PanBase implements IPanTool { public Future parse() { String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key); // 第一次请求 获取文件信息 - client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("dataKey", dataKey).send().onSuccess(res -> { + client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)) + .setTemplateParam("dataKey", dataKey) + .setTemplateParam("extractionCode", pwd == null ? "" : pwd) + .send() + .onSuccess(res -> { JsonObject jsonObject = res.bodyAsJsonObject(); log.debug("ecPan get file info -> {}", jsonObject); JsonObject fileInfo = jsonObject @@ -37,6 +41,11 @@ public class EcTool extends PanBase implements IPanTool { fail("{} 解析失败:{} key = {}", FIRST_REQUEST_URL, fileInfo.getString("errMesg"), dataKey); return; } + if (!fileInfo.containsKey("cloudpFile")) { + fail("{} 解析失败:cloudpFile不存在 key = {}", FIRST_REQUEST_URL, dataKey); + return; + } + JsonObject cloudpFile = fileInfo.getJsonObject("cloudpFile"); JsonArray fileIdList = JsonArray.of(cloudpFile); // 构造请求JSON {"extCodeFlag":0,"isIp":0} diff --git a/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java b/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java index 97c15d2..7f95113 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/FjTool.java @@ -4,14 +4,14 @@ import cn.qaiu.parser.IPanTool; import cn.qaiu.parser.PanBase; import cn.qaiu.util.AESUtils; import cn.qaiu.util.CommonUtils; +import cn.qaiu.util.UUIDUtil; import io.vertx.core.Future; import io.vertx.core.MultiMap; +import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.client.WebClient; +import io.vertx.ext.web.client.HttpRequest; import io.vertx.uritemplate.UriTemplate; -import java.util.UUID; - /** * 小飞机网盘 * @@ -24,11 +24,19 @@ public class FjTool extends PanBase implements IPanTool { public static final String SHARE_URL_PREFIX2 = REFERER_URL + "s/"; private static final String API_URL_PREFIX = "https://api.feijipan.com/ws/"; - private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome&extra" + - "=2&shareId={shareId}&type=0&offset=1&limit=60"; + private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "recommend/list?devType=6&devModel=Chrome" + + "&uuid={uuid}&extra=2×tamp={ts}&shareId={shareId}&type=0&offset=1&limit=60"; + /// recommend/list?devType=6&devModel=Chrome&uuid={uuid}&extra=2×tamp={ts}&shareId={shareId}&type=0&offset=1 + // &limit=60 private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "file/redirect?downloadId={fidEncode}&enable=1" + - "&devType=6&uuid={uuid}×tamp={ts}&auth={auth}"; + "&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={dataKey}"; + // https://api.feijipan.com/ws/file/redirect?downloadId={fidEncode}&enable=1&devType=6&uuid={uuid}×tamp={ts}&auth={auth}&shareId={dataKey} + + + private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" + + "={uuid}&extra=2×tamp={ts}"; + // https://api.feijipan.com/ws/buy/vip/list?devType=6&devModel=Chrome&uuid=WQAl5yBy1naGudJEILBvE&extra=2×tamp=E2C53155F6D09417A27981561134CB73 public FjTool(String key, String pwd) { super(key, pwd); @@ -42,49 +50,68 @@ public class FjTool extends PanBase implements IPanTool { dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key); } - WebClient client = clientNoRedirects; String shareId = String.valueOf(AESUtils.idEncrypt(dataKey)); + long nowTs = System.currentTimeMillis(); + String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs)); + String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString() - // 第一次请求 获取文件信息 - // POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60 - client.postAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("shareId", shareId).send().onSuccess(res -> { - JsonObject resJson = res.bodyAsJsonObject(); - if (resJson.getInteger("code") != 200) { - fail(FIRST_REQUEST_URL + " 返回异常: " + resJson); - return; - } - if (resJson.getJsonArray("list").size() == 0) { - fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson); - return; - } - // 文件Id - JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0); - String fileId = fileInfo.getString("fileIds"); - String userId = fileInfo.getString("userId"); - // 其他参数 - long nowTs = System.currentTimeMillis(); - String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs)); - String uuid = UUID.randomUUID().toString(); - String fidEncode = AESUtils.encrypt2Hex(fileId + "|"); - String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs); + // 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口 + client.postAbs(UriTemplate.of(VIP_REQUEST_URL)) + .setTemplateParam("uuid", uuid) + .setTemplateParam("ts", tsEncode) + .send().onSuccess(r0 -> { // 忽略res - MultiMap headers0 = MultiMap.caseInsensitiveMultiMap(); - headers0.set("referer", REFERER_URL); - // 第二次请求 - client.getAbs(UriTemplate.of(SECOND_REQUEST_URL)) - .putHeaders(headers0) - .setTemplateParam("fidEncode", fidEncode) - .setTemplateParam("uuid", uuid) - .setTemplateParam("ts", tsEncode) - .setTemplateParam("auth", auth).send().onSuccess(res2 -> { - MultiMap headers = res2.headers(); - if (!headers.contains("Location")) { - fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res.headers()); - return; - } - promise.complete(headers.get("Location")); - }).onFailure(handleFail(SECOND_REQUEST_URL)); - }).onFailure(handleFail(FIRST_REQUEST_URL)); + long nowTs0 = System.currentTimeMillis(); + String tsEncode0 = AESUtils.encrypt2Hex(Long.toString(nowTs)); + // 第一次请求 获取文件信息 + // POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60 + client.postAbs(UriTemplate.of(FIRST_REQUEST_URL)) + .setTemplateParam("shareId", shareId) + .setTemplateParam("uuid", uuid) + .setTemplateParam("ts", tsEncode0) + .send().onSuccess(res -> { + JsonObject resJson = res.bodyAsJsonObject(); + if (resJson.getInteger("code") != 200) { + fail(FIRST_REQUEST_URL + " 返回异常: " + resJson); + return; + } + if (resJson.getJsonArray("list").size() == 0) { + fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson); + return; + } + // 文件Id + JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0); + String fileId = fileInfo.getString("fileIds"); + String userId = fileInfo.getString("userId"); + // 其他参数 + long nowTs2 = System.currentTimeMillis(); + String tsEncode2 = AESUtils.encrypt2Hex(Long.toString(nowTs2)); + String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId); + String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2); + + MultiMap headers0 = MultiMap.caseInsensitiveMultiMap(); + headers0.set("referer", REFERER_URL); + // 第二次请求 + HttpRequest httpRequest = + clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL)) + .putHeaders(headers0) + .setTemplateParam("fidEncode", fidEncode) + .setTemplateParam("uuid", uuid) + .setTemplateParam("ts", tsEncode2) + .setTemplateParam("auth", auth) + .setTemplateParam("dataKey", dataKey); + System.out.println(httpRequest.toString()); + httpRequest.send().onSuccess(res2 -> { + MultiMap headers = res2.headers(); + if (!headers.contains("Location")) { + fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res.headers()); + return; + } + promise.complete(headers.get("Location")); + }).onFailure(handleFail(SECOND_REQUEST_URL)); + }).onFailure(handleFail(FIRST_REQUEST_URL)); + + }).onFailure(handleFail(FIRST_REQUEST_URL)); return promise.future(); } diff --git a/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java b/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java index f101736..018dfdd 100644 --- a/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java +++ b/parser/src/main/java/cn/qaiu/parser/impl/IzTool.java @@ -7,7 +7,6 @@ import cn.qaiu.util.CommonUtils; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.client.WebClient; import io.vertx.uritemplate.UriTemplate; import java.util.UUID; @@ -34,12 +33,12 @@ public class IzTool extends PanBase implements IPanTool { public Future parse() { String dataKey = CommonUtils.adaptShortPaths(SHARE_URL_PREFIX, key); - WebClient client = clientNoRedirects; - String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey)); + // 24.5.12 ilanzou改规则无需计算shareId + // String shareId = String.valueOf(AESUtils.idEncryptIz(dataKey)); // 第一次请求 获取文件信息 // POST https://api.feijipan.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60 - client.postAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("shareId", shareId).send().onSuccess(res -> { + client.postAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("shareId", dataKey).send().onSuccess(res -> { JsonObject resJson = res.bodyAsJsonObject(); if (resJson.getInteger("code") != 200) { fail(FIRST_REQUEST_URL + " 返回异常: " + resJson); @@ -61,7 +60,7 @@ public class IzTool extends PanBase implements IPanTool { String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId); String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs); // 第二次请求 - client.getAbs(UriTemplate.of(SECOND_REQUEST_URL)) + clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL)) .setTemplateParam("fidEncode", fidEncode) .setTemplateParam("uuid", uuid) .setTemplateParam("ts", tsEncode) diff --git a/parser/src/main/java/cn/qaiu/util/AESUtils.java b/parser/src/main/java/cn/qaiu/util/AESUtils.java index 471d8e7..767ce10 100644 --- a/parser/src/main/java/cn/qaiu/util/AESUtils.java +++ b/parser/src/main/java/cn/qaiu/util/AESUtils.java @@ -246,6 +246,7 @@ public class AESUtils { return idEncrypt0(str, array, 2, 2); } + // ================================蓝奏优享版Id解密========================================== // public static int idEncryptIz(String str) { // idEncrypt(e) { diff --git a/parser/src/main/java/cn/qaiu/util/UUIDUtil.java b/parser/src/main/java/cn/qaiu/util/UUIDUtil.java new file mode 100644 index 0000000..f25b552 --- /dev/null +++ b/parser/src/main/java/cn/qaiu/util/UUIDUtil.java @@ -0,0 +1,35 @@ +package cn.qaiu.util; + +import java.security.SecureRandom; + +/** + * @author QAIU + * @date 2024/5/13 4:10 + */ +public class UUIDUtil { + + public static String fjUuid() { + return generateRandomString(21); + } + + public static String generateRandomString(int length) { + SecureRandom random = new SecureRandom(); + byte[] randomBytes = new byte[length]; + random.nextBytes(randomBytes); + + StringBuilder sb = new StringBuilder(); + for (byte b : randomBytes) { + int value = b & 0x3F; // 63 in hexadecimal + if (value < 36) { + sb.append(Integer.toString(value, 36)); + } else if (value < 62) { + sb.append(Character.toUpperCase(Integer.toString(value - 26, 36).charAt(0))); + } else if (value > 62) { + sb.append("-"); + } else { // value == 62 or 63 + sb.append("_"); + } + } + return sb.toString(); + } +}