mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2025-12-16 20:33:03 +00:00
Compare commits
215 Commits
0.1.8.bate
...
v0.1.9b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef65d0e095 | ||
|
|
6438505f4a | ||
|
|
1be5030dd1 | ||
|
|
421b2f4a42 | ||
|
|
a66bf84381 | ||
|
|
0c4d366d6d | ||
|
|
a1d0a921fa | ||
|
|
2092230a61 | ||
|
|
6e5ae6eff3 | ||
|
|
4f8259d772 | ||
|
|
8b987d9824 | ||
|
|
e8ba451d18 | ||
|
|
77758db463 | ||
|
|
6c58598a8e | ||
|
|
3ac35230a3 | ||
|
|
ca91302d28 | ||
|
|
e07272a5dc | ||
|
|
461305e1df | ||
|
|
8e8ab10a0f | ||
|
|
e754326925 | ||
|
|
4c92994c6f | ||
|
|
66c57f47ac | ||
|
|
ec689eadd8 | ||
|
|
c1e15709a7 | ||
|
|
2848937ce7 | ||
|
|
42ff0c21b2 | ||
|
|
3ed7e547e6 | ||
|
|
fad8e688df | ||
|
|
b2f2dcac4c | ||
|
|
fcba78e977 | ||
|
|
77c9d777a1 | ||
|
|
4460659210 | ||
|
|
8631524107 | ||
|
|
0579588814 | ||
|
|
df2bfb6ac7 | ||
|
|
517b6f8910 | ||
|
|
94a46d2833 | ||
|
|
1631a0faa1 | ||
|
|
06d5943cb6 | ||
|
|
3095e13676 | ||
|
|
482cbce7e8 | ||
|
|
ef2fc3ab98 | ||
|
|
5b57b05eae | ||
|
|
093579c6f5 | ||
|
|
c2d4990d7f | ||
|
|
40e8380738 | ||
|
|
b716e1e861 | ||
|
|
8432d4952c | ||
|
|
dd8f085f63 | ||
|
|
161ff8d8a3 | ||
|
|
1390cd0104 | ||
|
|
7a02b1e97f | ||
|
|
036f107c90 | ||
|
|
5652383450 | ||
|
|
9a047a5da0 | ||
|
|
8975743a37 | ||
|
|
0e30eafe49 | ||
|
|
7facb62f21 | ||
|
|
30d43cb961 | ||
|
|
c505b17e35 | ||
|
|
080c4c753d | ||
|
|
ade0d34d91 | ||
|
|
56d082eb0b | ||
|
|
795c4529ba | ||
|
|
0f5cfe22ea | ||
|
|
925ad2c3a5 | ||
|
|
f3e96907fe | ||
|
|
75a1e58a7d | ||
|
|
379e889f71 | ||
|
|
40c06f397b | ||
|
|
9e9302436e | ||
|
|
6d816d4193 | ||
|
|
438eda9c08 | ||
|
|
ace39e4633 | ||
|
|
7712391f29 | ||
|
|
65f08dcb02 | ||
|
|
1d332aa6f4 | ||
|
|
ba81641517 | ||
|
|
fb30bdb879 | ||
|
|
fc451d3b41 | ||
|
|
ffee1f3462 | ||
|
|
f30027dd13 | ||
|
|
8b6aad17f4 | ||
|
|
b77930adfb | ||
|
|
aff8f88076 | ||
|
|
4e6582e24c | ||
|
|
fa9acaccfd | ||
|
|
0414f85f12 | ||
|
|
527dd0eeb4 | ||
|
|
74ed7475c9 | ||
|
|
54dc3dba96 | ||
|
|
9980159090 | ||
|
|
0b193ebb00 | ||
|
|
f5fc9843b2 | ||
|
|
df1f67dd26 | ||
|
|
b069a5f576 | ||
|
|
7686763a03 | ||
|
|
635a6eac37 | ||
|
|
877edc535f | ||
|
|
01d59e3c1e | ||
|
|
fece2799e3 | ||
|
|
de9756ee86 | ||
|
|
51f047a51b | ||
|
|
04b66e82b7 | ||
|
|
df89253647 | ||
|
|
45dbca794e | ||
|
|
857bf28f99 | ||
|
|
e07ce15228 | ||
|
|
0637bcfd8e | ||
|
|
23db0563ac | ||
|
|
ccba71aa4e | ||
|
|
fee4bf2ad6 | ||
|
|
5052fea9ef | ||
|
|
e85215fca1 | ||
|
|
e42fe45329 | ||
|
|
4240815bd1 | ||
|
|
6f0c5305e2 | ||
|
|
757005cad8 | ||
|
|
81651ad97c | ||
|
|
f3763b6058 | ||
|
|
82478dc485 | ||
|
|
703fd05d43 | ||
|
|
ff868b6e2a | ||
|
|
051a74b37b | ||
|
|
a0a1085623 | ||
|
|
2612d3919c | ||
|
|
6f123a236f | ||
|
|
71e57e6a08 | ||
|
|
7cb18d8186 | ||
|
|
cdbf670ece | ||
|
|
e0dafee617 | ||
|
|
c37bce1563 | ||
|
|
0b3c77d644 | ||
|
|
2cf85caf86 | ||
|
|
594010ba88 | ||
|
|
d91460d2e2 | ||
|
|
89713e6ac9 | ||
|
|
17c9b2538c | ||
|
|
d337b003cb | ||
|
|
8f1485656b | ||
|
|
f0c4ec3031 | ||
|
|
458be84aca | ||
|
|
c7716aad34 | ||
|
|
4a3e734408 | ||
|
|
54cc212753 | ||
|
|
f4ae1eaa51 | ||
|
|
d2537282c9 | ||
|
|
87527688c3 | ||
|
|
2be0b6505a | ||
|
|
672f100c7c | ||
|
|
5af402c0c5 | ||
|
|
693a4f0f63 | ||
|
|
f8d2426ff6 | ||
|
|
973a9bedcd | ||
|
|
a583733400 | ||
|
|
78eb51b3ca | ||
|
|
a2606be9d8 | ||
|
|
a4975c72ce | ||
|
|
58f96822a4 | ||
|
|
96b0d94986 | ||
|
|
70b38db8c5 | ||
|
|
b6a9c2d3a0 | ||
|
|
a01df6c7db | ||
|
|
4455bee570 | ||
|
|
cd0adef2ed | ||
|
|
4aa24a65fb | ||
|
|
760dca8772 | ||
|
|
8269673619 | ||
|
|
82ec586554 | ||
|
|
ca98cc8708 | ||
|
|
f07800985d | ||
|
|
b042df93b7 | ||
|
|
ecf4441946 | ||
|
|
39b2612840 | ||
|
|
218f486e6b | ||
|
|
cfcc25f175 | ||
|
|
155e88223c | ||
|
|
05039ece51 | ||
|
|
1c673f2b46 | ||
|
|
2232a70228 | ||
|
|
e661b1d817 | ||
|
|
5a6a65f580 | ||
|
|
5cdd3bcd30 | ||
|
|
1233a885b8 | ||
|
|
adf56cd768 | ||
|
|
cd4b208be9 | ||
|
|
502de1a5d0 | ||
|
|
4158f869a3 | ||
|
|
ff569d339c | ||
|
|
10eec323dd | ||
|
|
0a3db51c7d | ||
|
|
229aee0b30 | ||
|
|
44714aa981 | ||
|
|
2b6138a889 | ||
|
|
5e424f7bf4 | ||
|
|
294e47deed | ||
|
|
dc42547b73 | ||
|
|
7ef7f0706b | ||
|
|
a59b98a7c9 | ||
|
|
088fee9a4d | ||
|
|
d8666acfe8 | ||
|
|
209e9c2866 | ||
|
|
6c3195dea4 | ||
|
|
7d774a7433 | ||
|
|
f1ec4433cf | ||
|
|
1f825db261 | ||
|
|
1019f24f1d | ||
|
|
f5c5b99579 | ||
|
|
e002d19f1b | ||
|
|
0d5c9651f0 | ||
|
|
53fc13b95c | ||
|
|
694c3b0ddc | ||
|
|
9b3d4577cc | ||
|
|
77783915dd | ||
|
|
b67ac21a79 |
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 文本文件使用 LF 换行符,适用于 Linux 和 macOS
|
||||
*.sh text eol=lf
|
||||
*.service text eol=lf
|
||||
|
||||
# Windows 执行的文件使用 CRLF 换行符
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
bin/nfd-service-template.xml text eol=crlf
|
||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: https://blog.qaiu.top/archives/da-shang-zhuan-yong # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
60
.github/workflows/maven.yml
vendored
60
.github/workflows/maven.yml
vendored
@@ -11,12 +11,25 @@ name: Java CI with Maven
|
||||
# The API requires write permission on the repository to submit dependencies
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags:
|
||||
- '*' # 只有推送tag时才会触发构建
|
||||
branches-ignore:
|
||||
- '*' # 排除所有分支的提交
|
||||
paths-ignore:
|
||||
- 'bin/**'
|
||||
- '.github/**'
|
||||
- '.mvn/**'
|
||||
- '.run/**'
|
||||
- '.vscode/**'
|
||||
- '*.txt'
|
||||
- '*.md'
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -25,21 +38,64 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
|
||||
- name: Build Frontend
|
||||
run: cd web-front && yarn install && yarn run build
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn -B package --file pom.xml
|
||||
|
||||
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
|
||||
- name: Update dependency graph
|
||||
uses: advanced-security/maven-dependency-submission-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
ignore-maven-wrapper: true
|
||||
|
||||
# - uses: release-drafter/release-drafter@v5
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: web-service/target/netdisk-fast-download-bin.zip
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract git tag
|
||||
id: tag
|
||||
run: |
|
||||
GIT_TAG=$(git tag --points-at HEAD | head -n 1)
|
||||
echo "tag=$GIT_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: |
|
||||
ghcr.io/qaiu/netdisk-fast-download:${{ steps.tag.outputs.tag }}
|
||||
ghcr.io/qaiu/netdisk-fast-download:latest
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,3 +39,4 @@ gradlew.bat
|
||||
unused.txt
|
||||
/web-service/src/main/generated/
|
||||
/db
|
||||
/webroot/nfd-front/
|
||||
|
||||
15
.run/AppMain.run.xml
Normal file
15
.run/AppMain.run.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="AppMain" type="Application" factoryName="Application" nameIsGenerated="true">
|
||||
<option name="MAIN_CLASS_NAME" value="cn.qaiu.lz.AppMain" />
|
||||
<module name="web-service" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="cn.qaiu.lz.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM eclipse-temurin:17-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./web-service/target/netdisk-fast-download-bin.zip .
|
||||
|
||||
RUN unzip netdisk-fast-download-bin.zip && \
|
||||
mv netdisk-fast-download/* ./ && \
|
||||
rm netdisk-fast-download-bin.zip && \
|
||||
chmod +x run.sh
|
||||
|
||||
EXPOSE 6400 6401
|
||||
|
||||
ENTRYPOINT ["sh", "run.sh"]
|
||||
316
README.md
316
README.md
@@ -1,67 +1,125 @@
|
||||
云盘解析服务 (nfd云解析)
|
||||
预览地址 https://lz.qaiu.top
|
||||
<p align="center">
|
||||
<img src="https://github.com/user-attachments/assets/87401aae-b0b6-4ffb-bbeb-44756404d26f" alt="项目预览图" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?branch=v0.1.9b8a&style=flat"></a>
|
||||
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
|
||||
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.6-blue?style=flat"></a>
|
||||
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
|
||||
<a href="https://github.com/qaiu/netdisk-fast-download/releases/"><img src="https://img.shields.io/github/v/release/qaiu/netdisk-fast-download?style=flat"></a>
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
# netdisk-fast-download 网盘分享链接云解析服务
|
||||
QQ群:1017480890
|
||||
|
||||
netdisk-fast-download网盘直链云解析(nfd云解析)能把网盘分享下载链接转化为直链,支持多款云盘,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享,以及部分网盘文件夹分享。
|
||||
|
||||
## 快速开始
|
||||
命令行下载分享文件:
|
||||
```shell
|
||||
curl -LOJ "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
|
||||
```
|
||||
或者使用wget:
|
||||
```shell
|
||||
wget -O bilibili.mp4 "https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234"
|
||||
```
|
||||
或者使用浏览器[直接访问](https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4):
|
||||
```
|
||||
### 调用演示站下载:
|
||||
https://lz.qaiu.top/parser?url=https://share.feijipan.com/s/nQOaNRPW&pwd=1234
|
||||
### 调用演示站预览:
|
||||
https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.top%2Fparser%3Furl%3Dhttps%3A%2F%2Fshare.feijipan.com%2Fs%2FnQOaNRPW&name=bilibili.mp4&ext=mp4
|
||||
|
||||
```
|
||||
|
||||
## 预览地址
|
||||
[预览地址1](https://lz.qaiu.top)
|
||||
[预览地址2](http://www.722shop.top:6401)
|
||||
[天翼云盘大文件解析限时开放](https://189.qaiu.top)
|
||||
|
||||
main分支依赖JDK17, 提供了JDK11分支[main-jdk11](https://github.com/qaiu/netdisk-fast-download/tree/main-jdk11)
|
||||
**注意: 请不要过度依赖lz.qaiu.top预览地址服务,建议本地搭建或者云服务器自行搭建。
|
||||
解析次数过多IP会被部分网盘厂商限制,不推荐做公共解析。**
|
||||
|
||||
[](https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml)
|
||||
[](https://www.oracle.com/cn/java/technologies/downloads/)
|
||||
[](https://vertx-china.github.io/)
|
||||
[](https://github.com/qaiu/netdisk-fast-download/releases/tag/0.1.6-releases)
|
||||
|
||||
## 项目介绍
|
||||
网盘直链解析工具能把网盘分享下载链接转化为直链,已支持蓝奏云/蓝奏云优享/奶牛快传/移动云云空间/小飞机盘/亿方云/123云盘/Cloudreve等,支持加密分享。
|
||||
|
||||
|
||||
*重要声明:本项目仅供学习参考;请不要将此项目用于任何商业用途,否则可能带来严重的后果。转发/分享该项目请注明来源*
|
||||
**0.1.8及以上版本json接口格式有调整 参考json返回数据格式示例**
|
||||
**小飞机解析有IP限制,多数云服务商的大陆IP会被拦截(可以自行配置代理),和本程序无关**
|
||||
**注意: 请不要过度依赖lz.qaiu.top预览地址服务,建议本地搭建或者云服务器自行搭建。解析次数过多IP会被部分网盘厂商限制,不推荐做公共解析。**
|
||||
|
||||
## 网盘支持情况:
|
||||
> 20230905 奶牛云直链做了防盗链,需加入请求头:Referer: https://cowtransfer.com/
|
||||
> 20230824 123云盘解析大文件(>100MB)失效,需要登录
|
||||
> 20230722 UC网盘解析失效,需要登录
|
||||
|
||||
`网盘名称(网盘标识):`
|
||||
网盘名称-网盘标识:
|
||||
|
||||
- [蓝奏云 (lz)](https://pc.woozooo.com/)
|
||||
- [蓝奏云优享 (iz)](https://www.ilanzou.com/)
|
||||
- [奶牛快传 (cow)](https://cowtransfer.com/)
|
||||
- [移动云云空间 (ec)](https://www.ecpan.cn/web)
|
||||
- [小飞机网盘 (fj)](https://www.feijipan.com/)
|
||||
- [亿方云 (fc)](https://www.fangcloud.com/)
|
||||
- [123云盘 (ye)](https://www.123pan.com/)
|
||||
- [文叔叔 (ws)](https://www.wenshushu.cn/)
|
||||
- [联想乐云 (le)](https://lecloud.lenovo.com/)
|
||||
- [QQ邮箱文件中转站 (qq)](https://mail.qq.com/)
|
||||
- [超星网盘-开发中 (cx)](https://passport2.chaoxing.com/login?newversion=true&refer=https%3A%2F%2Fpan-yz.chaoxing.com%2F)
|
||||
- [Cloudreve自建网盘--正在优化 (ce)](https://github.com/cloudreve/Cloudreve)
|
||||
**TODO:**
|
||||
- 登录接口, 文件上传/下载/分享后端接口
|
||||
- 短地址服务
|
||||
- 前端界面(建设中...)
|
||||
- [蓝奏云-lz](https://pc.woozooo.com/)
|
||||
- [蓝奏云优享-iz](https://www.ilanzou.com/)
|
||||
- [奶牛快传-cow](https://cowtransfer.com/)
|
||||
- [移动云云空间-ec](https://www.ecpan.cn/web)
|
||||
- [小飞机网盘-fj](https://www.feijipan.com/)
|
||||
- [亿方云-fc](https://www.fangcloud.com/)
|
||||
- [123云盘-ye](https://www.123pan.com/)
|
||||
- ~[115网盘(失效)-p115](https://115.com/)~
|
||||
- [118网盘(已停服)-p118](https://www.118pan.com/)
|
||||
- [文叔叔-ws](https://www.wenshushu.cn/)
|
||||
- [联想乐云-le](https://lecloud.lenovo.com/)
|
||||
- [QQ邮箱云盘-qqw](https://mail.qq.com/)
|
||||
- [QQ闪传-qqsc](https://nutty.qq.com/nutty/ssr/26797.html)
|
||||
- [城通网盘-ct](https://www.ctfile.com)
|
||||
- [网易云音乐分享链接-mnes](https://music.163.com)
|
||||
- [酷狗音乐分享链接-mkgs](https://www.kugou.com)
|
||||
- [酷我音乐分享链接-mkws](https://kuwo.cn)
|
||||
- [QQ音乐分享链接-mqqs](https://y.qq.com)
|
||||
- 咪咕音乐分享链接(开发中)
|
||||
- [Cloudreve自建网盘-ce](https://github.com/cloudreve/Cloudreve)
|
||||
- ~[微雨云存储-pvvy](https://www.vyuyun.com/)~
|
||||
- [超星云盘(需要referer: https://pan-yz.chaoxing.com)-pcx](https://pan-yz.chaoxing.com)
|
||||
- Google云盘-pgd
|
||||
- Onedrive-pod
|
||||
- Dropbox-pdp
|
||||
- iCloud-pic
|
||||
### 仅专属版提供
|
||||
- [移动云盘-p139](https://yun.139.com/)
|
||||
- [联通云盘-pwo](https://pan.wo.cn/)
|
||||
- [天翼云盘-p189](https://cloud.189.cn/)
|
||||
|
||||
### API接口说明
|
||||
your_host指的是您的域名或者IP,实际使用时替换为实际域名或者IP,端口默认6400,可以使用nginx代理来做域名访问。
|
||||
解析方式分为两种类型直接跳转下载文件和获取下载链接,每一种都提供了两种接口形式parser和网盘标志/分享key拼接的短地址(标志短链),所有规则参考示例。
|
||||
- 通用接口: `/parser?url=分享链接`,加密分享需要加上参数pwd=密码;
|
||||
- 标志短链: `/网盘标识/分享key` 在分享Key后面加上@密码;
|
||||
- 直链JSON: `通用接口`和`标志短链`前加上`/json` 加密分享的密码规则同上;
|
||||
## API接口说明
|
||||
your_host指的是您的域名或者IP,实际使用时替换为实际域名或者IP,端口默认6400,可以使用nginx代理来做域名访问。
|
||||
解析方式分为两种类型直接跳转下载文件和获取下载链接,
|
||||
每一种都提供了两种接口形式: `通用接口parser?url=`和`网盘标志/分享key拼接的短地址(标志短链)`,所有规则参考示例。
|
||||
- 通用接口: `/parser?url=分享链接&pwd=密码` 没有分享密码去掉&pwd参数;
|
||||
- 标志短链: `/d/网盘标识/分享key@密码` 没有分享密码去掉@密码;
|
||||
- 直链JSON: `/json/网盘标识/分享key@密码`和`/json/parser?url=分享链接&pwd=密码`
|
||||
- 网盘标识参考上面网盘支持情况
|
||||
- 当带有分享密码时需要加上密码参数(pwd)
|
||||
- 移动云云空间,小飞机网盘的加密分享的密码可以忽略
|
||||
- 移动云空间分享key取分享链接中的data参数,比如`&data=xxx`的参数就是xxx
|
||||
|
||||
API规则:
|
||||
```
|
||||
> 建议使用UrlEncode编码分享链接
|
||||
|
||||
1. 解析并自动302跳转 :
|
||||
1. 解析并自动302跳转
|
||||
http://your_host/parser?url=分享链接&pwd=xxx
|
||||
http://your_host/网盘标识/分享key@分享密码
|
||||
http://your_host/parser?url=UrlEncode(分享链接)&pwd=xxx
|
||||
http://your_host/d/网盘标识/分享key@分享密码
|
||||
2. 获取解析后的直链--JSON格式
|
||||
http://your_host/json/parser?url=分享链接&pwd=xxx
|
||||
http://your_host/json/网盘标识/分享key@分享密码
|
||||
3. 文件夹解析v0.1.8fixed3新增
|
||||
http://your_host/json/getFileList?url=分享链接&pwd=xxx
|
||||
|
||||
|
||||
### json接口说明
|
||||
|
||||
#### 1. 文件解析:/json/parser?url=分享链接&pwd=xxx
|
||||
|
||||
```
|
||||
json返回数据格式示例:
|
||||
`shareKey`: 全局分享key
|
||||
`directLink`: 下载链接
|
||||
`cacheHit`: 是否为缓存链接
|
||||
`expires`: 缓存到期时间
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
@@ -78,6 +136,79 @@ json返回数据格式示例:
|
||||
"timestamp": 1726637151902
|
||||
}
|
||||
```
|
||||
#### 2. 分享链接详情接口 /v2/linkInfo?url=分享链接
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"success": true,
|
||||
"count": 0,
|
||||
"data": {
|
||||
"downLink": "https://lz.qaiu.top/d/fj/xx",
|
||||
"apiLink": "https://lz.qaiu.top/json/fj/xx",
|
||||
"cacheHitTotal": 5,
|
||||
"parserTotal": 2,
|
||||
"sumTotal": 7,
|
||||
"shareLinkInfo": {
|
||||
"shareKey": "xx",
|
||||
"panName": "小飞机网盘",
|
||||
"type": "fj",
|
||||
"sharePassword": "",
|
||||
"shareUrl": "https://share.feijipan.com/s/xx",
|
||||
"standardUrl": "https://www.feijix.com/s/xx",
|
||||
"otherParam": {
|
||||
"UA": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0"
|
||||
},
|
||||
"cacheKey": "fj:xx"
|
||||
}
|
||||
},
|
||||
"timestamp": 1736489219402
|
||||
}
|
||||
```
|
||||
#### 3. 文件夹解析(仅支持蓝奏云/蓝奏优享/小飞机网盘)
|
||||
/v2/getFileList?url=分享链接&pwd=分享密码
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"fileName": "xxx",
|
||||
"fileId": "xxx",
|
||||
"fileIcon": null,
|
||||
"size": 999,
|
||||
"sizeStr": "999 M",
|
||||
"fileType": "file/folder",
|
||||
"filePath": null,
|
||||
"createTime": "17 小时前",
|
||||
"updateTime": null,
|
||||
"createBy": null,
|
||||
"description": null,
|
||||
"downloadCount": "下载次数",
|
||||
"panType": "lz",
|
||||
"parserUrl": "下载链接/文件夹链接",
|
||||
"extParameters": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
#### 4. 解析次数统计接口 /v2/statisticsInfo
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"success": true,
|
||||
"count": 0,
|
||||
"data": {
|
||||
"parserTotal": 320508,
|
||||
"cacheTotal": 5957910,
|
||||
"total": 6278418
|
||||
},
|
||||
"timestamp": 1736489378770
|
||||
}
|
||||
```
|
||||
|
||||
IDEA HttpClient示例:
|
||||
|
||||
@@ -148,6 +279,58 @@ mvn package
|
||||
```
|
||||
打包好的文件位于 web-service/target/netdisk-fast-download-bin.zip
|
||||
## Linux服务部署
|
||||
|
||||
### Docker 部署(Main分支)
|
||||
|
||||
#### 海外服务器Docker部署
|
||||
```shell
|
||||
# 创建目录
|
||||
mkdir -p netdisk-fast-download
|
||||
cd netdisk-fast-download
|
||||
|
||||
# 拉取镜像
|
||||
docker pull ghcr.io/qaiu/netdisk-fast-download:latest
|
||||
|
||||
# 复制配置文件(或下载仓库web-service\src\main\resources)
|
||||
docker create --name netdisk-fast-download ghcr.io/qaiu/netdisk-fast-download:latest
|
||||
docker cp netdisk-fast-download:/app/resources ./resources
|
||||
docker rm netdisk-fast-download
|
||||
|
||||
# 启动容器
|
||||
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.io/qaiu/netdisk-fast-download:latest
|
||||
|
||||
# 反代6401端口
|
||||
|
||||
# 升级容器
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
|
||||
```
|
||||
|
||||
#### 国内Docker部署
|
||||
```shell
|
||||
# 创建目录
|
||||
mkdir -p netdisk-fast-download
|
||||
cd netdisk-fast-download
|
||||
|
||||
# 拉取镜像
|
||||
docker pull ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
|
||||
|
||||
# 复制配置文件(或下载仓库web-service\src\main\resources)
|
||||
docker create --name netdisk-fast-download ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
|
||||
docker cp netdisk-fast-download:/app/resources ./resources
|
||||
docker rm netdisk-fast-download
|
||||
|
||||
# 启动容器
|
||||
docker run -d -it --name netdisk-fast-download -p 6401:6401 --restart unless-stopped -e TZ=Asia/Shanghai -v ./resources:/app/resources -v ./db:/app/db -v ./logs:/app/logs ghcr.nju.edu.cn/qaiu/netdisk-fast-download:latest
|
||||
|
||||
# 反代6401端口
|
||||
|
||||
# 升级容器
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --run-once netdisk-fast-download
|
||||
```
|
||||
|
||||
### 宝塔部署指引 -> [点击进入宝塔部署教程](https://blog.qaiu.top/archives/netdisk-fast-download-bao-ta-an-zhuang-jiao-cheng)
|
||||
|
||||
### Linux命令行部署
|
||||
> 注意: netdisk-fast-download.service中的ExecStart的路径改为实际路径
|
||||
```shell
|
||||
cd ~
|
||||
@@ -183,15 +366,36 @@ bash service-install.sh
|
||||
如果不想使用服务运行可以直接运行run.bat
|
||||
> 注意: 如果jdk环境变量的java版本不是17请修改nfd-service-template.xml中的java命令的路径改为实际路径
|
||||
|
||||
## 0.1.8 开发计划
|
||||
- Docker部署
|
||||
- 联想乐云解析 √
|
||||
- 直链缓存 √
|
||||
- 日志优化 √
|
||||
## 相关配置说明
|
||||
|
||||
resources目录下包含服务端配置文件 配置文件自带说明,具体请查看配置文件内容,
|
||||
app-dev.yml 可以配置解析服务相关信息, 包括端口,域名,缓存时长等
|
||||
server-proxy.yml 可以配置代理服务运行的相关信息, 包括前端反向代理端口,路径等
|
||||
|
||||
### ip代理配置说明
|
||||
有时候解析量很大,IP容易被ban,这时候可以使用其他服务器搭建nfd-proxy代理服务。
|
||||
|
||||
修改配置文件:
|
||||
app-dev.yml
|
||||
|
||||
```yaml
|
||||
proxy:
|
||||
- panTypes: pgd,pdb,pod # 网盘标识
|
||||
type: http # 支持http/socks4/socks5
|
||||
host: 127.0.0.1 # 代理IP
|
||||
port: 7890 # 端口
|
||||
username: # 用户名
|
||||
password: # 密码
|
||||
```
|
||||
nfd-proxy搭建http代理服务器
|
||||
参考https://github.com/nfd-parser/nfd-proxy
|
||||
|
||||
## 0.1.9 开发计划
|
||||
- 目录解析(专属版)
|
||||
- 带cookie/token参数解析大文件(专属版)
|
||||
|
||||
**技术栈:**
|
||||
Jdk17+Vert.x4.4.1
|
||||
Jdk17+Vert.x4
|
||||
Core模块集成Vert.x实现类似spring的注解式路由API
|
||||
|
||||
|
||||
@@ -199,12 +403,26 @@ Core模块集成Vert.x实现类似spring的注解式路由API
|
||||
|
||||
[](https://star-history.com/#qaiu/netdisk-fast-download&Date)
|
||||
|
||||
## **免责声明**
|
||||
- 用户在使用本项目时,应自行承担风险,并确保其行为符合当地法律法规及网盘服务提供商的使用条款。
|
||||
- 开发者不对用户因使用本项目而导致的任何后果负责,包括但不限于数据丢失、隐私泄露、账号封禁或其他任何形式的损害。
|
||||
|
||||
## 支持该项目
|
||||
本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
|
||||
开源不易,用爱发电,本项目长期维护如果觉得有帮助, 可以请作者喝杯咖啡, 感谢支持
|
||||
|
||||
|
||||
### 关于专属版
|
||||
99元, 提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘,移动云盘,联调云盘的解析支持
|
||||
199元, 包含部署服务和首页定制, 需提供宝塔环境
|
||||
可以提供功能定制开发, 加v价格详谈:
|
||||
<p>qq: 197575894</p>
|
||||
<p>wechat: imcoding_</p>
|
||||
|
||||
<!--
|
||||

|
||||
|
||||
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
rem 获取当前 Java 版本信息并搜索是否包含 "17."
|
||||
java -version 2>&1 | find "17." >nul
|
||||
|
||||
rem 如果找不到 JDK 17.x,则下载并安装
|
||||
if errorlevel 1 (
|
||||
echo JDK 17.x not found. Downloading and installing...
|
||||
|
||||
REM 这里添加下载和安装 JDK 的代码
|
||||
|
||||
rem 验证安装
|
||||
java -version
|
||||
|
||||
echo JDK 17.x installation complete.
|
||||
) else (
|
||||
echo JDK 17.x is already installed.
|
||||
)
|
||||
|
||||
endlocal
|
||||
pause
|
||||
86
bin/nfd-install.sh
Normal file
86
bin/nfd-install.sh
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# ----------- 配置区域 ------------
|
||||
# JRE 下载目录
|
||||
JRE_DIR="/opt/custom-jre17"
|
||||
# 使用阿里云镜像下载 JRE(OpenJDK 17)
|
||||
JRE_TARBALL_URL="https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jre/x64/linux/OpenJDK17U-jre_x64_linux_hotspot_17.0.15_6.tar.gz"
|
||||
|
||||
# ZIP 文件下载相关
|
||||
ZIP_URL="http://www.722shop.top:6401/parser?url="
|
||||
ZIP_DEST_DIR="/opt/target-zip"
|
||||
ZIP_FILE_NAME="nfd.zip"
|
||||
# --------------------------------
|
||||
|
||||
# 创建目录
|
||||
mkdir -p "$JRE_DIR"
|
||||
mkdir -p "$ZIP_DEST_DIR"
|
||||
|
||||
# -------- 检查 unzip 是否存在 --------
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
echo "unzip 未安装,正在安装..."
|
||||
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update && apt-get install -y unzip
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
yum install -y unzip
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
dnf install -y unzip
|
||||
else
|
||||
echo "不支持的包管理器,无法自动安装 unzip,请手动安装后重试。"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "unzip 已安装"
|
||||
fi
|
||||
|
||||
# -------- 下载并解压 JRE --------
|
||||
echo "下载 JRE 17 到 $JRE_DIR..."
|
||||
curl -L "$JRE_TARBALL_URL" -o "$JRE_DIR/jre17.tar.gz"
|
||||
|
||||
echo "解压 JRE..."
|
||||
tar -xzf "$JRE_DIR/jre17.tar.gz" -C "$JRE_DIR" --strip-components=1
|
||||
rm "$JRE_DIR/jre17.tar.gz"
|
||||
echo "JRE 解压完成"
|
||||
|
||||
# -------- 下载 ZIP 文件 --------
|
||||
ZIP_PATH="$ZIP_DEST_DIR/$ZIP_FILE_NAME"
|
||||
echo "下载 ZIP 文件到 $ZIP_PATH..."
|
||||
curl -L "$ZIP_URL" -o "$ZIP_PATH"
|
||||
|
||||
# -------- 解压 ZIP 文件 --------
|
||||
echo "解压 ZIP 文件到 $ZIP_DEST_DIR..."
|
||||
unzip -o "$ZIP_PATH" -d "$ZIP_DEST_DIR"
|
||||
echo "解压完成"
|
||||
|
||||
# -------- 启动 JAR 程序 --------
|
||||
echo "进入 JAR 目录并后台运行程序..."
|
||||
|
||||
JAR_DIR="/opt/target-zip/netdisk-fast-download"
|
||||
JAR_FILE="netdisk-fast-download.jar"
|
||||
JAVA_BIN="$JRE_DIR/bin/java"
|
||||
LOG_FILE="$JAR_DIR/app.log"
|
||||
|
||||
if [ ! -d "$JAR_DIR" ]; then
|
||||
echo "[错误] 找不到 JAR 目录: $JAR_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$JAR_DIR"
|
||||
|
||||
if [ ! -f "$JAR_FILE" ]; then
|
||||
echo "[错误] 找不到 JAR 文件: $JAR_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVA_BIN" ]; then
|
||||
echo "[错误] 找不到可执行的 java: $JAVA_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 后台运行,日志记录
|
||||
nohup "$JAVA_BIN" -jar "$JAR_FILE" > "$LOG_FILE" 2>&1 &
|
||||
|
||||
echo "程序已在后台启动 ✅"
|
||||
echo "日志路径: $LOG_FILE"
|
||||
@@ -4,7 +4,7 @@
|
||||
<name>netdisk-fast-download</name>
|
||||
<description>netdisk fast download service</description>
|
||||
<executable>java</executable>
|
||||
<arguments> -server -Xmx128m -jar ${jar} </arguments>
|
||||
<arguments> -server -Xmx128m -Dfile.encoding=utf8 -jar ${jar} </arguments>
|
||||
<logpath>${dd}\logs</logpath>
|
||||
<log mode="roll-by-time">
|
||||
<pattern>yyyyMMdd</pattern>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
<version>3.18.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
@@ -59,6 +59,18 @@
|
||||
<artifactId>vertx-jdbc-client</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.2.0</version>
|
||||
</dependency>
|
||||
<!-- PG驱动-->
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<version>42.7.3</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -21,7 +21,6 @@ public @interface Constraint {
|
||||
boolean notNull() default false;
|
||||
|
||||
/**
|
||||
* 唯一键约束 TODO 待实现
|
||||
* @return 唯一键约束
|
||||
*/
|
||||
String uniqueKey() default "";
|
||||
@@ -32,7 +31,7 @@ public @interface Constraint {
|
||||
*/
|
||||
String defaultValue() default "";
|
||||
/**
|
||||
* 默认值是否是函数
|
||||
* 默认值是否是函数 like value=NOW()
|
||||
* @return false 不是函数
|
||||
*/
|
||||
boolean defaultValueIsFunction() default false;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package cn.qaiu.db.ddl;
|
||||
|
||||
import cn.qaiu.db.pool.JDBCPoolInit;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CreateDatabase {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
||||
|
||||
/**
|
||||
* 解析数据库URL,获取数据库名
|
||||
* @param url 数据库URL
|
||||
* @return 数据库名
|
||||
*/
|
||||
public static String getDatabaseName(String url) {
|
||||
// 正则表达式匹配数据库名
|
||||
String regex = "jdbc:mysql://[^/]+/(\\w+)(\\?.*)?";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(url);
|
||||
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid database URL: " + url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用JDBC原生方法创建数据库
|
||||
* @param url 数据库连接URL
|
||||
* @param user 数据库用户名
|
||||
* @param password 数据库密码
|
||||
*/
|
||||
public static void createDatabase(String url, String user, String password) {
|
||||
String dbName = getDatabaseName(url);
|
||||
LOGGER.info(">>>>>>>>>>> 创建数据库:'{}' <<<<<<<<<<<< ", dbName);
|
||||
|
||||
// 去掉数据库名,构建不带数据库名的URL
|
||||
String baseUrl = url.substring(0, url.lastIndexOf("/") + 1) + "?characterEncoding=UTF-8&useUnicode=true";
|
||||
|
||||
try (Connection conn = DriverManager.getConnection(baseUrl, user, password);
|
||||
Statement stmt = conn.createStatement()) {
|
||||
// 创建数据库
|
||||
stmt.executeUpdate("CREATE DATABASE IF NOT EXISTS " + dbName + " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
LOGGER.info(">>>>>>>>>>> 数据库'{}'创建成功 <<<<<<<<<<<<", dbName);
|
||||
} catch (SQLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void createDatabase(JsonObject dbConfig) {
|
||||
createDatabase(
|
||||
dbConfig.getString("jdbcUrl"),
|
||||
dbConfig.getString("username"),
|
||||
dbConfig.getString("password")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import io.vertx.codegen.format.CamelCase;
|
||||
import io.vertx.codegen.format.Case;
|
||||
import io.vertx.codegen.format.LowerCamelCase;
|
||||
import io.vertx.codegen.format.SnakeCase;
|
||||
import io.vertx.jdbcclient.JDBCPool;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.sqlclient.Pool;
|
||||
import io.vertx.sqlclient.templates.annotations.Column;
|
||||
import io.vertx.sqlclient.templates.annotations.RowMapped;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -14,9 +16,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 创建表
|
||||
@@ -24,154 +24,312 @@ import java.util.Set;
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
public class CreateTable {
|
||||
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>();
|
||||
public static Map<Class<?>, String> javaProperty2SqlColumnMap = new HashMap<>() {{
|
||||
// Java类型到SQL类型的映射
|
||||
put(Integer.class, "INT");
|
||||
put(Short.class, "SMALLINT");
|
||||
put(Byte.class, "TINYINT");
|
||||
put(Long.class, "BIGINT");
|
||||
put(java.math.BigDecimal.class, "DECIMAL");
|
||||
put(Double.class, "DOUBLE");
|
||||
put(Float.class, "REAL");
|
||||
put(Boolean.class, "BOOLEAN");
|
||||
put(String.class, "VARCHAR");
|
||||
put(Date.class, "TIMESTAMP");
|
||||
put(java.time.LocalDateTime.class, "TIMESTAMP");
|
||||
put(java.sql.Timestamp.class, "TIMESTAMP");
|
||||
put(java.sql.Date.class, "DATE");
|
||||
put(java.sql.Time.class, "TIME");
|
||||
|
||||
// 基本数据类型
|
||||
put(int.class, "INT");
|
||||
put(short.class, "SMALLINT");
|
||||
put(byte.class, "TINYINT");
|
||||
put(long.class, "BIGINT");
|
||||
put(double.class, "DOUBLE");
|
||||
put(float.class, "REAL");
|
||||
put(boolean.class, "BOOLEAN");
|
||||
}};
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CreateTable.class);
|
||||
|
||||
static {
|
||||
javaProperty2SqlColumnMap.put(Integer.class, "INT");
|
||||
javaProperty2SqlColumnMap.put(Short.class, "SMALLINT");
|
||||
javaProperty2SqlColumnMap.put(Byte.class, "TINYINT");
|
||||
javaProperty2SqlColumnMap.put(Long.class, "BIGINT");
|
||||
javaProperty2SqlColumnMap.put(java.math.BigDecimal.class, "DECIMAL");
|
||||
javaProperty2SqlColumnMap.put(Double.class, "DOUBLE");
|
||||
javaProperty2SqlColumnMap.put(Float.class, "REAL");
|
||||
javaProperty2SqlColumnMap.put(Boolean.class, "BOOLEAN");
|
||||
javaProperty2SqlColumnMap.put(String.class, "VARCHAR");
|
||||
javaProperty2SqlColumnMap.put(java.util.Date.class, "TIMESTAMP");
|
||||
javaProperty2SqlColumnMap.put(java.time.LocalDateTime.class, "TIMESTAMP");
|
||||
javaProperty2SqlColumnMap.put(java.sql.Timestamp.class, "TIMESTAMP");
|
||||
javaProperty2SqlColumnMap.put(java.sql.Date.class, "DATE");
|
||||
javaProperty2SqlColumnMap.put(java.sql.Time.class, "TIME");
|
||||
|
||||
javaProperty2SqlColumnMap.put(int.class, "INT");
|
||||
javaProperty2SqlColumnMap.put(short.class, "SMALLINT");
|
||||
javaProperty2SqlColumnMap.put(byte.class, "TINYINT");
|
||||
javaProperty2SqlColumnMap.put(long.class, "BIGINT");
|
||||
javaProperty2SqlColumnMap.put(double.class, "DOUBLE");
|
||||
javaProperty2SqlColumnMap.put(float.class, "REAL");
|
||||
javaProperty2SqlColumnMap.put(boolean.class, "BOOLEAN");
|
||||
}
|
||||
public static String UNIQUE_PREFIX = "idx_";
|
||||
|
||||
private static Case getCase(Class<?> clz) {
|
||||
switch (clz.getName()) {
|
||||
case "io.vertx.codegen.format.CamelCase":
|
||||
return CamelCase.INSTANCE;
|
||||
case "io.vertx.codegen.format.SnakeCase":
|
||||
return SnakeCase.INSTANCE;
|
||||
case "io.vertx.codegen.format.LowerCamelCase":
|
||||
return LowerCamelCase.INSTANCE;
|
||||
default:
|
||||
throw new UnsupportedOperationException();
|
||||
return switch (clz.getName()) {
|
||||
case "io.vertx.codegen.format.CamelCase" -> CamelCase.INSTANCE;
|
||||
case "io.vertx.codegen.format.SnakeCase" -> SnakeCase.INSTANCE;
|
||||
case "io.vertx.codegen.format.LowerCamelCase" -> LowerCamelCase.INSTANCE;
|
||||
default -> throw new UnsupportedOperationException();
|
||||
};
|
||||
}
|
||||
|
||||
public static List<String> getCreateTableSQL(Class<?> clz, JDBCType type) {
|
||||
// 获取表名和主键
|
||||
TableInfo tableInfo = extractTableInfo(clz, type);
|
||||
|
||||
// 构建表的SQL语句
|
||||
List<String> sqlList = new ArrayList<>();
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
sb.append("CREATE TABLE IF NOT EXISTS ")
|
||||
.append(tableInfo.quotationMarks).append(tableInfo.tableName).append(tableInfo.quotationMarks)
|
||||
.append(" ( \r\n ");
|
||||
|
||||
// 处理字段并生成列定义
|
||||
List<String> indexSQLs = new ArrayList<>();
|
||||
processFields(clz, tableInfo, sb, indexSQLs);
|
||||
|
||||
// 去掉最后一个逗号并添加表尾部信息
|
||||
String tableSQL = sb.substring(0, sb.lastIndexOf(",")) + tableInfo.endStr;
|
||||
sqlList.add(tableSQL);
|
||||
|
||||
// 添加索引SQL
|
||||
sqlList.addAll(indexSQLs);
|
||||
|
||||
return sqlList;
|
||||
}
|
||||
|
||||
|
||||
// 修改extractTableInfo方法,处理没有Table注解时默认使用id字段作为主键
|
||||
private static TableInfo extractTableInfo(Class<?> clz, JDBCType type) {
|
||||
String quotationMarks;
|
||||
String endStr;
|
||||
if (type == JDBCType.MySQL) {
|
||||
quotationMarks = "`";
|
||||
endStr = ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
||||
} else {
|
||||
quotationMarks = "\"";
|
||||
endStr = ");";
|
||||
}
|
||||
|
||||
String primaryKey = null;
|
||||
String tableName = null;
|
||||
Case caseFormat = SnakeCase.INSTANCE;
|
||||
|
||||
// 判断类上是否有RowMapped注解
|
||||
if (clz.isAnnotationPresent(RowMapped.class)) {
|
||||
RowMapped annotation = clz.getAnnotation(RowMapped.class);
|
||||
caseFormat = getCase(annotation.formatter());
|
||||
}
|
||||
|
||||
// 判断类上是否有Table注解
|
||||
if (clz.isAnnotationPresent(Table.class)) {
|
||||
Table annotation = clz.getAnnotation(Table.class);
|
||||
tableName = StringUtils.isNotEmpty(annotation.value())
|
||||
? annotation.value()
|
||||
: LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
|
||||
primaryKey = annotation.keyFields();
|
||||
}
|
||||
|
||||
// 如果表名仍为null,使用类名转下划线命名作为表名
|
||||
if (StringUtils.isEmpty(tableName)) {
|
||||
tableName = LowerCamelCase.INSTANCE.to(SnakeCase.INSTANCE, clz.getSimpleName());
|
||||
}
|
||||
|
||||
// 如果主键为空,默认使用id字段作为主键
|
||||
if (StringUtils.isEmpty(primaryKey)) {
|
||||
try {
|
||||
clz.getDeclaredField("id");
|
||||
primaryKey = "id";
|
||||
} catch (NoSuchFieldException e) {
|
||||
// 如果没有id字段,不设置主键
|
||||
primaryKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
return new TableInfo(tableName, quotationMarks, endStr, primaryKey, caseFormat, type);
|
||||
}
|
||||
|
||||
// 修改processFields方法,处理索引
|
||||
private static void processFields(Class<?> clz, TableInfo tableInfo, StringBuilder sb, List<String> indexSQLs) {
|
||||
Field[] fields = clz.getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
// 跳过无效字段
|
||||
if (isIgnoredField(field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取字段名和SQL类型
|
||||
String column = LowerCamelCase.INSTANCE.to(tableInfo.caseFormat, field.getName());
|
||||
String sqlType = javaProperty2SqlColumnMap.get(field.getType());
|
||||
|
||||
// 处理字段注解
|
||||
column = processColumnAnnotation(field, column);
|
||||
int[] decimalSize = {22, 2};
|
||||
int varcharSize = 255;
|
||||
if (field.isAnnotationPresent(Length.class)) {
|
||||
Length length = field.getAnnotation(Length.class);
|
||||
decimalSize = length.decimalSize();
|
||||
varcharSize = length.varcharSize();
|
||||
}
|
||||
|
||||
// 构建列定义
|
||||
sb.append(tableInfo.quotationMarks).append(column).append(tableInfo.quotationMarks)
|
||||
.append(" ").append(sqlType);
|
||||
appendTypeLength(sqlType, sb, decimalSize, varcharSize);
|
||||
appendConstraints(field, sb, tableInfo);
|
||||
appendPrimaryKey(tableInfo.primaryKey, column, sb);
|
||||
|
||||
// 添加索引
|
||||
appendIndex(tableInfo, indexSQLs, field);
|
||||
|
||||
sb.append(",\n ");
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCreateTableSQL(Class<?> clz, JDBCType type) {
|
||||
String quotationMarks = type == JDBCType.H2DB ? "\"" : "`";
|
||||
String endStr = type == JDBCType.H2DB ? ");" : ")ENGINE=InnoDB DEFAULT CHARSET=utf8;";
|
||||
// 判断类上是否有次注解
|
||||
String primaryKey = null; // 主键
|
||||
String tableName = null; // 表名
|
||||
Case caseFormat = SnakeCase.INSTANCE;
|
||||
if (clz.isAnnotationPresent(RowMapped.class)) {
|
||||
RowMapped annotation = clz.getAnnotation(RowMapped.class);
|
||||
Class<? extends Case> formatter = annotation.formatter();
|
||||
caseFormat = getCase(formatter);
|
||||
}
|
||||
// 判断是否忽略字段
|
||||
private static boolean isIgnoredField(Field field) {
|
||||
return field.getName().equals("serialVersionUID")
|
||||
|| StringUtils.isEmpty(javaProperty2SqlColumnMap.get(field.getType()))
|
||||
|| field.isAnnotationPresent(TableGenIgnore.class);
|
||||
}
|
||||
|
||||
if (clz.isAnnotationPresent(Table.class)) {
|
||||
// 获取类上的注解
|
||||
Table annotation = clz.getAnnotation(Table.class);
|
||||
// 输出注解上的类名
|
||||
String tableNameAnnotation = annotation.value();
|
||||
if (StringUtils.isNotEmpty(tableNameAnnotation)) {
|
||||
tableName = tableNameAnnotation;
|
||||
} else {
|
||||
tableName = LowerCamelCase.INSTANCE.to(caseFormat, clz.getSimpleName());
|
||||
// 处理Column注解
|
||||
private static String processColumnAnnotation(Field field, String column) {
|
||||
if (field.isAnnotationPresent(Column.class)) {
|
||||
Column columnAnnotation = field.getAnnotation(Column.class);
|
||||
if (StringUtils.isNotBlank(columnAnnotation.name())) {
|
||||
column = columnAnnotation.name();
|
||||
}
|
||||
primaryKey = annotation.keyFields();
|
||||
}
|
||||
Field[] fields = clz.getDeclaredFields();
|
||||
String column;
|
||||
int[] decimalSize = {22, 2};
|
||||
int varcharSize = 255;
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
sb.append("CREATE TABLE IF NOT EXISTS ").append(quotationMarks).append(tableName).append(quotationMarks).append(" ( \r\n ");
|
||||
boolean firstId = true;
|
||||
for (Field f : fields) {
|
||||
Class<?> paramType = f.getType();
|
||||
String sqlType = javaProperty2SqlColumnMap.get(paramType);
|
||||
if (f.getName().equals("serialVersionUID") || StringUtils.isEmpty(sqlType) || f.isAnnotationPresent(TableGenIgnore.class)) {
|
||||
continue;
|
||||
return column;
|
||||
}
|
||||
|
||||
// 添加类型长度
|
||||
private static void appendTypeLength(String sqlType, StringBuilder sb, int[] decimalSize, int varcharSize) {
|
||||
if ("DECIMAL".equals(sqlType)) {
|
||||
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
|
||||
} else if ("VARCHAR".equals(sqlType)) {
|
||||
sb.append("(").append(varcharSize).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加约束
|
||||
private static void appendConstraints(Field field, StringBuilder sb, TableInfo tableInfo) {
|
||||
JDBCType type = tableInfo.dbType;
|
||||
|
||||
if (field.isAnnotationPresent(Constraint.class)) {
|
||||
Constraint constraint = field.getAnnotation(Constraint.class);
|
||||
if (constraint.notNull()) {
|
||||
sb.append(" NOT NULL");
|
||||
}
|
||||
column = LowerCamelCase.INSTANCE.to(caseFormat, f.getName());
|
||||
if (f.isAnnotationPresent(Column.class)) {
|
||||
Column columnAnnotation = f.getAnnotation(Column.class);
|
||||
//输出注解属性
|
||||
if (StringUtils.isNotBlank(columnAnnotation.name())) {
|
||||
column = columnAnnotation.name();
|
||||
}
|
||||
String apostrophe = constraint.defaultValueIsFunction() ? "" : "'";
|
||||
if (StringUtils.isNotEmpty(constraint.defaultValue())) {
|
||||
sb.append(" DEFAULT ").append(apostrophe).append(constraint.defaultValue()).append(apostrophe);
|
||||
}
|
||||
if (f.isAnnotationPresent(Length.class)) {
|
||||
Length fieldAnnotation = f.getAnnotation(Length.class);
|
||||
decimalSize = fieldAnnotation.decimalSize();
|
||||
varcharSize = fieldAnnotation.varcharSize();
|
||||
}
|
||||
sb.append(quotationMarks).append(column).append(quotationMarks);
|
||||
sb.append(" ").append(sqlType);
|
||||
// 添加类型长度
|
||||
if (sqlType.equals("DECIMAL")) {
|
||||
sb.append("(").append(decimalSize[0]).append(",").append(decimalSize[1]).append(")");
|
||||
}
|
||||
if (sqlType.equals("VARCHAR")) {
|
||||
sb.append("(").append(varcharSize).append(")");
|
||||
}
|
||||
if (f.isAnnotationPresent(Constraint.class)) {
|
||||
Constraint constraintAnnotation = f.getAnnotation(Constraint.class);
|
||||
if (constraintAnnotation.notNull()) {
|
||||
//非空约束
|
||||
sb.append(" NOT NULL");
|
||||
}
|
||||
String apostrophe = constraintAnnotation.defaultValueIsFunction() ? "" : "'";
|
||||
if (StringUtils.isNotEmpty(constraintAnnotation.defaultValue())) {
|
||||
//默认值约束
|
||||
sb.append(" DEFAULT ").append(apostrophe).append(constraintAnnotation.defaultValue()).append(apostrophe);
|
||||
}
|
||||
if (constraintAnnotation.autoIncrement() && paramType.equals(Integer.class) || paramType.equals(Long.class)) {
|
||||
////自增
|
||||
if (constraint.autoIncrement()) {
|
||||
if (type == JDBCType.PostgreSQL) {
|
||||
// 需要移除字段类型(最后一个单词)
|
||||
if (field.getType().equals(Integer.class)) {
|
||||
sb.delete(sb.lastIndexOf(" "), sb.length());
|
||||
sb.append(" SERIAL");
|
||||
} else if (field.getType().equals(Long.class)) {
|
||||
sb.delete(sb.lastIndexOf(" "), sb.length());
|
||||
sb.append(" BIGSERIAL");
|
||||
}
|
||||
} else if (field.getType().equals(Integer.class) || field.getType().equals(Long.class)) {
|
||||
sb.append(" AUTO_INCREMENT");
|
||||
}
|
||||
}
|
||||
if (StringUtils.isEmpty(primaryKey)) {
|
||||
if (firstId) {//类型转换
|
||||
sb.append(" PRIMARY KEY");
|
||||
firstId = false;
|
||||
}
|
||||
} else {
|
||||
if (primaryKey.equals(column.toLowerCase())) {
|
||||
sb.append(" PRIMARY KEY");
|
||||
}
|
||||
}
|
||||
|
||||
// 添加主键
|
||||
private static void appendPrimaryKey(String primaryKey, String column, StringBuilder sb) {
|
||||
if (StringUtils.isEmpty(primaryKey)) {
|
||||
return;
|
||||
}
|
||||
if (primaryKey.equalsIgnoreCase(column)) {
|
||||
sb.append(" PRIMARY KEY");
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendIndex(TableInfo tableInfo, List<String> indexSQLs, Field field) {
|
||||
if (!field.isAnnotationPresent(Constraint.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Constraint constraint = field.getAnnotation(Constraint.class);
|
||||
if (StringUtils.isEmpty(constraint.uniqueKey())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String indexName = UNIQUE_PREFIX + tableInfo.tableName + "_" + constraint.uniqueKey();
|
||||
String columnName = field.getName();
|
||||
|
||||
// 检查是否已有相同索引名称的索引
|
||||
Optional<String> existingIndex = indexSQLs.stream()
|
||||
.filter(sql -> sql.contains(tableInfo.quotationMarks + indexName + tableInfo.quotationMarks))
|
||||
.findFirst();
|
||||
|
||||
if (existingIndex.isPresent()) {
|
||||
// 如果存在相同索引名称,追加字段到索引定义中
|
||||
String updatedIndex = existingIndex.get().replaceFirst(
|
||||
"\\(([^)]+)\\)", // 匹配索引字段列表
|
||||
"($1, " + tableInfo.quotationMarks + columnName + tableInfo.quotationMarks + ")"
|
||||
);
|
||||
indexSQLs.remove(existingIndex.get());
|
||||
indexSQLs.add(updatedIndex);
|
||||
} else {
|
||||
// 如果不存在相同索引名称,创建新的索引
|
||||
String indexSQL = String.format(
|
||||
"CREATE UNIQUE INDEX %s %s%s%s ON %s%s%s (%s%s%s);",
|
||||
tableInfo.dbType == JDBCType.MySQL ? "" : "IF NOT EXISTS",
|
||||
tableInfo.quotationMarks, indexName, tableInfo.quotationMarks,
|
||||
tableInfo.quotationMarks, tableInfo.tableName, tableInfo.quotationMarks,
|
||||
tableInfo.quotationMarks, columnName, tableInfo.quotationMarks
|
||||
);
|
||||
indexSQLs.add(indexSQL);
|
||||
}
|
||||
}
|
||||
|
||||
// 表信息类
|
||||
private record TableInfo(
|
||||
String tableName, // 表名
|
||||
String quotationMarks, // 引号或反引号
|
||||
String endStr, // 表尾部信息
|
||||
String primaryKey, // 主键字段
|
||||
Case caseFormat, // 命名格式
|
||||
JDBCType dbType // 数据库类型
|
||||
) {
|
||||
}
|
||||
|
||||
public static Future<Void> createTable(Pool pool, JDBCType type) {
|
||||
Promise<Void> promise = Promise.promise();
|
||||
Set<Class<?>> tableClasses = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
|
||||
|
||||
if (tableClasses.isEmpty()) {
|
||||
LOGGER.warn("Table model class not found");
|
||||
promise.complete();
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
List<Future<Object>> futures = new ArrayList<>();
|
||||
|
||||
for (Class<?> clazz : tableClasses) {
|
||||
List<String> sqlList = getCreateTableSQL(clazz, type);
|
||||
LOGGER.info("Class `{}` auto-generate table", clazz.getName());
|
||||
|
||||
for (String sql : sqlList) {
|
||||
try {
|
||||
pool.query(sql).execute().toCompletionStage().toCompletableFuture().join();
|
||||
futures.add(Future.succeededFuture());
|
||||
LOGGER.debug("Executed SQL:\n{}", sql);
|
||||
} catch (Exception e) {
|
||||
String message = e.getMessage();
|
||||
if (message != null && message.contains("Duplicate key name")) {
|
||||
LOGGER.warn("Ignoring duplicate key error: {}", message);
|
||||
futures.add(Future.succeededFuture());
|
||||
} else {
|
||||
LOGGER.error("SQL Error: {}\nSQL: {}", message, sql);
|
||||
futures.add(Future.failedFuture(e));
|
||||
throw new RuntimeException(e); // Stop execution for other exceptions
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append(",\n ");
|
||||
}
|
||||
String sql = sb.toString();
|
||||
//去掉最后一个逗号
|
||||
int lastIndex = sql.lastIndexOf(",");
|
||||
sql = sql.substring(0, lastIndex) + sql.substring(lastIndex + 1);
|
||||
return sql.substring(0, sql.length() - 1) + endStr;
|
||||
|
||||
Future.all(futures).onSuccess(r -> promise.complete()).onFailure(promise::fail);
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public static void createTable(JDBCPool pool, JDBCType type) {
|
||||
Set<Class<?>> tableClassList = ReflectionUtil.getReflections().getTypesAnnotatedWith(Table.class);
|
||||
if (tableClassList.isEmpty()) LOGGER.info("Table model class not fount");
|
||||
tableClassList.forEach(clazz -> {
|
||||
String createTableSQL = getCreateTableSQL(clazz, type);
|
||||
|
||||
pool.query(createTableSQL).execute().onSuccess(
|
||||
rs -> LOGGER.info("table auto generate:\n" + createTableSQL)
|
||||
).onFailure(e -> {
|
||||
LOGGER.error(e.getMessage() + " SQL: \n" + createTableSQL);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package cn.qaiu.db.pool;
|
||||
|
||||
import cn.qaiu.db.ddl.CreateTable;
|
||||
import cn.qaiu.db.ddl.CreateDatabase;
|
||||
import cn.qaiu.vx.core.util.VertxHolder;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.jdbcclient.JDBCPool;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -17,18 +20,29 @@ import org.slf4j.LoggerFactory;
|
||||
public class JDBCPoolInit {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JDBCPoolInit.class);
|
||||
|
||||
private static final String providerClass = io.vertx.ext.jdbc.spi.impl.HikariCPDataSourceProvider.class.getName();
|
||||
|
||||
private JDBCPool pool = null;
|
||||
JsonObject dbConfig;
|
||||
Vertx vertx = VertxHolder.getVertxInstance();
|
||||
String url;
|
||||
private JDBCType type;
|
||||
|
||||
private final JDBCType type;
|
||||
|
||||
private static JDBCPoolInit instance;
|
||||
|
||||
public JDBCType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public JDBCPoolInit(Builder builder) {
|
||||
this.dbConfig = builder.dbConfig;
|
||||
this.url = builder.url;
|
||||
this.type = builder.type;
|
||||
this.type = JDBCType.getJDBCTypeByURL(builder.url);
|
||||
if (StringUtils.isBlank(builder.dbConfig.getString("provider_class"))) {
|
||||
builder.dbConfig.put("provider_class", providerClass);
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -42,12 +56,10 @@ public class JDBCPoolInit {
|
||||
public static class Builder {
|
||||
private JsonObject dbConfig;
|
||||
private String url;
|
||||
private JDBCType type;
|
||||
|
||||
public Builder config(JsonObject dbConfig) {
|
||||
this.dbConfig = dbConfig;
|
||||
this.url = dbConfig.getString("jdbcUrl");
|
||||
this.type = JDBCUtil.getJDBCType(dbConfig.getString("driverClassName"));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -59,35 +71,34 @@ public class JDBCPoolInit {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* init h2db<br>
|
||||
* 这个方法只允许调用一次
|
||||
*/
|
||||
public void initPool() {
|
||||
synchronized public Future<Void> initPool() {
|
||||
if (pool != null) {
|
||||
LOGGER.error("pool 重复初始化");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 初始化数据库连接
|
||||
vertx.createSharedWorkerExecutor("sql-pool-init")
|
||||
.executeBlocking(() -> {
|
||||
// 初始化连接池
|
||||
pool = JDBCPool.pool(vertx, dbConfig);
|
||||
CreateTable.createTable(pool, type);
|
||||
return "数据库连接初始化: URL=" + url;
|
||||
})
|
||||
.onSuccess(LOGGER::info)
|
||||
.onFailure(Throwable::printStackTrace);
|
||||
// 初始化连接池
|
||||
if (type == JDBCType.MySQL) {
|
||||
CreateDatabase.createDatabase(dbConfig);
|
||||
}
|
||||
pool = JDBCPool.pool(vertx, dbConfig);
|
||||
LOGGER.info("数据库连接初始化: URL=" + url);
|
||||
return CreateTable.createTable(pool, type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取连接池
|
||||
*
|
||||
* @return pool
|
||||
*/
|
||||
public JDBCPool getPool() {
|
||||
synchronized public JDBCPool getPool() {
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
package cn.qaiu.db.pool;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/10/10 14:06
|
||||
* @since 2023/10/10 14:06
|
||||
*/
|
||||
public enum JDBCType {
|
||||
MySQL, H2DB
|
||||
// 添加驱动类型字段
|
||||
MySQL("jdbc:mysql:"),
|
||||
H2DB("jdbc:h2:"),
|
||||
PostgreSQL("jdbc:postgresql:");
|
||||
private final String urlPrefix; // JDBC URL 前缀
|
||||
|
||||
// 构造函数
|
||||
JDBCType(String urlPrefix) {
|
||||
this.urlPrefix = urlPrefix;
|
||||
}
|
||||
|
||||
// 获取 JDBC URL 前缀
|
||||
public String getUrlPrefix() {
|
||||
return urlPrefix;
|
||||
}
|
||||
|
||||
// 根据 JDBC URL 获取 JDBC 类型
|
||||
public static JDBCType getJDBCTypeByURL(String jdbcURL) {
|
||||
for (JDBCType jdbcType : values()) {
|
||||
if (StringUtils.startsWithIgnoreCase(jdbcURL, jdbcType.getUrlPrefix())) {
|
||||
return jdbcType;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("不支持的SQL类型: " + jdbcURL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package cn.qaiu.db.pool;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/10/10 14:05
|
||||
*/
|
||||
public class JDBCUtil {
|
||||
public static JDBCType getJDBCType(String deviceName) {
|
||||
switch (deviceName) {
|
||||
case "com.mysql.cj.jdbc.Driver":
|
||||
case "com.mysql.jdbc.Driver":
|
||||
return JDBCType.MySQL;
|
||||
case "org.h2.Driver":
|
||||
return JDBCType.H2DB;
|
||||
}
|
||||
throw new RuntimeException("不支持的SQL驱动类型: " + deviceName);
|
||||
}
|
||||
}
|
||||
20
core/pom.xml
20
core/pom.xml
@@ -12,6 +12,7 @@
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.build.timestamp.format>yyMMdd_HHmm</maven.build.timestamp.format>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -60,12 +61,6 @@
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.melloware/commons-beanutils2 -->
|
||||
<dependency>
|
||||
<groupId>com.melloware</groupId>
|
||||
<artifactId>commons-beanutils2</artifactId>
|
||||
<version>${commons-beanutils2.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
@@ -82,6 +77,12 @@
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
@@ -90,6 +91,13 @@
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<!-- 代码生成器 -->
|
||||
<annotationProcessors>
|
||||
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
|
||||
</annotationProcessors>
|
||||
<generatedSourcesDirectory>
|
||||
${project.basedir}/src/main/generated
|
||||
</generatedSourcesDirectory>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package cn.qaiu.vx.core;
|
||||
|
||||
import cn.qaiu.vx.core.util.CommonUtil;
|
||||
import cn.qaiu.vx.core.util.ConfigUtil;
|
||||
import cn.qaiu.vx.core.util.VertxHolder;
|
||||
import cn.qaiu.vx.core.verticle.ReverseProxyVerticle;
|
||||
import cn.qaiu.vx.core.verticle.RouterVerticle;
|
||||
import cn.qaiu.vx.core.verticle.ServiceVerticle;
|
||||
import io.vertx.core.*;
|
||||
import io.vertx.core.dns.AddressResolverOptions;
|
||||
import io.vertx.core.impl.launcher.commands.VersionCommand;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.shareddata.LocalMap;
|
||||
@@ -52,9 +54,9 @@ public final class Deploy {
|
||||
public void start(String[] args, Handler<JsonObject> handle) {
|
||||
this.mainThread = Thread.currentThread();
|
||||
this.handle = handle;
|
||||
if (args.length > 0) {
|
||||
if (args.length > 0 && args[0].startsWith("app-")) {
|
||||
// 启动参数dev或者prod
|
||||
path.append("-").append(args[0]);
|
||||
path.append("-").append(args[0].replace("app-",""));
|
||||
}
|
||||
|
||||
// 读取yml配置
|
||||
@@ -101,7 +103,7 @@ public final class Deploy {
|
||||
""";
|
||||
|
||||
System.out.printf(logoTemplate,
|
||||
conf.getString("version_app"),
|
||||
CommonUtil.getAppVersion(),
|
||||
VersionCommand.getVersion(),
|
||||
conf.getString("copyright"),
|
||||
year
|
||||
@@ -121,6 +123,12 @@ public final class Deploy {
|
||||
var vertxOptions = vertxConfigELPS == 0 ?
|
||||
new VertxOptions() : new VertxOptions(vertxConfig);
|
||||
|
||||
vertxOptions.setAddressResolverOptions(
|
||||
new AddressResolverOptions().
|
||||
addServer("114.114.114.114").
|
||||
addServer("114.114.115.115").
|
||||
addServer("8.8.8.8").
|
||||
addServer("8.8.4.4"));
|
||||
LOGGER.info("vertxConfigEventLoopPoolSize: {}, eventLoopPoolSize: {}, workerPoolSize: {}", vertxConfigELPS,
|
||||
vertxOptions.getEventLoopPoolSize(),
|
||||
vertxOptions.getWorkerPoolSize());
|
||||
@@ -132,19 +140,23 @@ public final class Deploy {
|
||||
localMap.put(GLOBAL_CONFIG, globalConfig);
|
||||
localMap.put(CUSTOM_CONFIG, customConfig);
|
||||
localMap.put(SERVER, globalConfig.getJsonObject(SERVER));
|
||||
var future0 = vertx.createSharedWorkerExecutor("other-handle").executeBlocking(bch -> {
|
||||
handle.handle(globalConfig);
|
||||
bch.complete("other handle complete");
|
||||
});
|
||||
var future0 = vertx.createSharedWorkerExecutor("other-handle")
|
||||
.executeBlocking(() -> {
|
||||
handle.handle(globalConfig);
|
||||
return "Other handle complete";
|
||||
});
|
||||
|
||||
// 部署 路由、异步service、反向代理 服务
|
||||
var future1 = vertx.deployVerticle(RouterVerticle.class, getWorkDeploymentOptions("Router"));
|
||||
var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service"));
|
||||
var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy"));
|
||||
future0.onSuccess(res -> {
|
||||
LOGGER.info(res);
|
||||
// 部署 路由、异步service、反向代理 服务
|
||||
var future1 = vertx.deployVerticle(RouterVerticle.class, getWorkDeploymentOptions("Router"));
|
||||
var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service"));
|
||||
var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy"));
|
||||
|
||||
CompositeFuture.all(future1, future2, future3, future0)
|
||||
.onSuccess(this::deployWorkVerticalSuccess)
|
||||
.onFailure(this::deployVerticalFailed);
|
||||
Future.all(future1, future2, future3)
|
||||
.onSuccess(this::deployWorkVerticalSuccess)
|
||||
.onFailure(this::deployVerticalFailed);
|
||||
}).onFailure(e -> LOGGER.error("Other handle error", e));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +193,7 @@ public final class Deploy {
|
||||
private DeploymentOptions getWorkDeploymentOptions(String name, int ins) {
|
||||
return new DeploymentOptions()
|
||||
.setWorkerPoolName(name)
|
||||
.setWorker(true)
|
||||
.setThreadingModel(ThreadingModel.WORKER)
|
||||
.setInstances(ins);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static cn.qaiu.vx.core.util.ConfigConstant.ROUTE_TIME_OUT;
|
||||
import static cn.qaiu.vx.core.verticle.ReverseProxyVerticle.REROUTE_PATH_PREFIX;
|
||||
import static io.vertx.core.http.HttpHeaders.*;
|
||||
import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
@@ -72,20 +73,15 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
public Router createRouter() {
|
||||
// 主路由
|
||||
Router mainRouter = Router.router(VertxHolder.getVertxInstance());
|
||||
|
||||
// 静态资源
|
||||
String path = SharedDataUtil.getJsonConfig("server")
|
||||
.getString("staticResourcePath");
|
||||
if (!StringUtils.isEmpty(path)) {
|
||||
// 静态资源
|
||||
mainRouter.route("/*").handler(StaticHandler
|
||||
.create(path)
|
||||
.setCachingEnabled(true)
|
||||
.setDefaultContentEncoding("UTF-8"));
|
||||
}
|
||||
|
||||
|
||||
mainRouter.route().handler(ctx -> {
|
||||
String realPath = ctx.request().uri();;
|
||||
if (realPath.startsWith(REROUTE_PATH_PREFIX)) {
|
||||
// vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
|
||||
String rePath = realPath.substring(REROUTE_PATH_PREFIX.length());
|
||||
ctx.reroute(rePath);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("The HTTP service request address information ===>path:{}, uri:{}, method:{}",
|
||||
ctx.request().path(), ctx.request().absoluteURI(), ctx.request().method());
|
||||
ctx.response().headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
@@ -179,8 +175,13 @@ public class RouterHandlerFactory implements BaseHttpApi {
|
||||
route.handler(ResponseTimeHandler.create());
|
||||
route.handler(ctx -> handlerMethod(instance, method, ctx)).failureHandler(ctx -> {
|
||||
if (ctx.response().ended()) return;
|
||||
ctx.failure().printStackTrace();
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500));
|
||||
// 超时处理器状态码503
|
||||
if (ctx.statusCode() == 503 || ctx.failure() == null) {
|
||||
doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员", 500));
|
||||
} else {
|
||||
ctx.failure().printStackTrace();
|
||||
doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500));
|
||||
}
|
||||
});
|
||||
} else if (method.isAnnotationPresent(SockRouteMapper.class)) {
|
||||
// websocket 基于sockJs
|
||||
|
||||
@@ -16,17 +16,24 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
|
||||
default Handler<RoutingContext> doHandle() {
|
||||
|
||||
return ctx -> {
|
||||
ctx.put(IS_NEXT, false);
|
||||
BeforeInterceptor.this.handle(ctx);
|
||||
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
|
||||
sendError(ctx, 403);
|
||||
// 加同步锁
|
||||
synchronized (BeforeInterceptor.class) {
|
||||
ctx.put(IS_NEXT, false);
|
||||
BeforeInterceptor.this.handle(ctx);
|
||||
if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
|
||||
sendError(ctx, 403);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
default void doNext(RoutingContext context) {
|
||||
context.put(IS_NEXT, true);
|
||||
context.next();
|
||||
// 设置上下文状态为可以继续执行
|
||||
// 添加同步锁保障多线程下执行时序
|
||||
synchronized (BeforeInterceptor.class) {
|
||||
context.put(IS_NEXT, true);
|
||||
context.next();
|
||||
}
|
||||
}
|
||||
|
||||
void handle(RoutingContext context);
|
||||
|
||||
@@ -30,12 +30,10 @@ public class JsonResult<T> implements Serializable {
|
||||
|
||||
private int code = SUCCESS_CODE;//状态码
|
||||
|
||||
private String msg = SUCCESS_MESSAGE;//消息
|
||||
private String msg = SUCCESS_MESSAGE; //消息
|
||||
|
||||
private boolean success = true; //是否成功
|
||||
|
||||
private int count;
|
||||
|
||||
private T data;
|
||||
|
||||
private long timestamp = System.currentTimeMillis(); //时间戳
|
||||
@@ -54,20 +52,6 @@ public class JsonResult<T> implements Serializable {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public JsonResult(int code, String msg, boolean success, T data, int count) {
|
||||
this(code, msg, success, data);
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public JsonResult<T> setCount(int count) {
|
||||
this.count = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
@@ -136,20 +120,9 @@ public class JsonResult<T> implements Serializable {
|
||||
return new JsonResult<>(SUCCESS_CODE, msg, true, data);
|
||||
}
|
||||
|
||||
// 响应成功消息和数据实体
|
||||
public static <T> JsonResult<T> data(String msg, T data, int count) {
|
||||
if (StringUtils.isEmpty(msg)) msg = SUCCESS_MESSAGE;
|
||||
return new JsonResult<>(SUCCESS_CODE, msg, true, data, count);
|
||||
}
|
||||
|
||||
// 响应数据实体
|
||||
public static <T> JsonResult<T> data(T data) {
|
||||
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data, 0);
|
||||
}
|
||||
|
||||
// 响应数据实体
|
||||
public static <T> JsonResult<T> data(T data, int count) {
|
||||
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data, count);
|
||||
return new JsonResult<>(SUCCESS_CODE, SUCCESS_MESSAGE, true, data);
|
||||
}
|
||||
|
||||
// 响应成功消息
|
||||
|
||||
7
core/src/main/java/cn/qaiu/vx/core/package-info.java
Normal file
7
core/src/main/java/cn/qaiu/vx/core/package-info.java
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* ModuleGen cn.qaiu.vx.core
|
||||
*/
|
||||
@ModuleGen(name = "vertx-http-proxy", groupPackage = "cn.qaiu.vx.core", useFutures = true)
|
||||
package cn.qaiu.vx.core;
|
||||
|
||||
import io.vertx.codegen.annotations.ModuleGen;
|
||||
@@ -3,8 +3,6 @@ package cn.qaiu.vx.core.util;
|
||||
import cn.qaiu.vx.core.annotaions.HandleSortFilter;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.apache.commons.beanutils2.ConvertUtils;
|
||||
import org.apache.commons.beanutils2.Converter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -16,6 +14,7 @@ import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -99,23 +98,6 @@ public class CommonUtil {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册枚举转换器
|
||||
*
|
||||
* @param enums 枚举类
|
||||
*/
|
||||
@SafeVarargs
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public static void enumConvert(Class<? extends Enum>... enums) {
|
||||
for (Class<? extends Enum> anEnum : enums) {
|
||||
ConvertUtils.register(new Converter() {
|
||||
public Object convert(Class type, Object value) {
|
||||
return Enum.valueOf(anEnum, (String) value);
|
||||
}
|
||||
}, anEnum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理其他配置
|
||||
*
|
||||
@@ -158,4 +140,21 @@ public class CommonUtil {
|
||||
}
|
||||
}).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static String appVersion;
|
||||
|
||||
public static String getAppVersion() {
|
||||
if (null == appVersion) {
|
||||
Properties properties = new Properties();
|
||||
try {
|
||||
properties.load(CommonUtil.class.getClassLoader().getResourceAsStream("app.properties"));
|
||||
if (!properties.isEmpty()) {
|
||||
appVersion = properties.getProperty("app.version") + "build" + properties.getProperty("build");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return appVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ public interface ConfigConstant {
|
||||
String LOCAL = "local";
|
||||
String SERVER = "server";
|
||||
String CACHE = "cache";
|
||||
|
||||
String PROXY_SERVER = "proxy-server";
|
||||
|
||||
String PROXY = "proxy";
|
||||
|
||||
String AUTHS = "auths";
|
||||
String GLOBAL_CONFIG = "globalConfig";
|
||||
String CUSTOM_CONFIG = "customConfig";
|
||||
String ASYNC_SERVICE_INSTANCES = "asyncServiceInstances";
|
||||
|
||||
46
core/src/main/java/cn/qaiu/vx/core/util/JacksonConfig.java
Normal file
46
core/src/main/java/cn/qaiu/vx/core/util/JacksonConfig.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package cn.qaiu.vx.core.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import io.vertx.core.json.jackson.DatabindCodec;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/10/14 9:07
|
||||
*/
|
||||
public class JacksonConfig {
|
||||
|
||||
static {
|
||||
// 通过该方法对mapper对象进行设置,所有序列化的对象都将按改规则进行系列化
|
||||
// Include.Include.ALWAYS 默认
|
||||
// Include.NON_DEFAULT 属性为默认值不序列化
|
||||
// Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,则返回的json是没有这个字段的。这样对移动端会更省流量
|
||||
// Include.NON_NULL 属性为NULL 不序列化,就是为null的字段不参加序列化
|
||||
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
ObjectMapper objectMapper = DatabindCodec.mapper();
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
javaTimeModule.addDeserializer(LocalDateTime.class,
|
||||
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
javaTimeModule.addDeserializer(LocalDate.class,
|
||||
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||
javaTimeModule.addDeserializer(LocalTime.class,
|
||||
new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
|
||||
objectMapper.registerModule(javaTimeModule);
|
||||
LoggerFactory.getLogger(JacksonConfig.class).info("Global JacksonConfig complete.");
|
||||
}
|
||||
|
||||
public static void nothing() {}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package cn.qaiu.vx.core.util;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
import org.apache.commons.beanutils2.BeanUtils;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -18,29 +17,21 @@ import java.util.Map;
|
||||
public final class ParamUtil {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ParamUtil.class);
|
||||
|
||||
public static Map<String, String> multiMapToMap(MultiMap multiMap) {
|
||||
public static Map<String, Object> multiMapToMap(MultiMap multiMap) {
|
||||
if (multiMap == null) return null;
|
||||
Map<String, String> map = new HashMap<>();
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
for (Map.Entry<String, String> entry : multiMap.entries()) {
|
||||
map.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public static <T> T multiMapToEntity(MultiMap multiMap, Class<T> tClass) throws NoSuchMethodException {
|
||||
Map<String, String> map = multiMapToMap(multiMap);
|
||||
T obj = null;
|
||||
try {
|
||||
obj = tClass.getDeclaredConstructor().newInstance();
|
||||
BeanUtils.populate(obj, map);
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("实例化异常");
|
||||
} catch (InvocationTargetException e2) {
|
||||
e2.printStackTrace();
|
||||
LOGGER.error("map2bean转换异常");
|
||||
public static <T> T multiMapToEntity(MultiMap multiMap, Class<T> tClass) {
|
||||
Map<String, Object> map = multiMapToMap(multiMap);
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
return obj;
|
||||
return new JsonObject(map).mapTo(tClass);
|
||||
}
|
||||
|
||||
public static MultiMap paramsToMap(String paramString) {
|
||||
|
||||
@@ -12,7 +12,8 @@ import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE;
|
||||
public class ResponseUtil {
|
||||
|
||||
public static void redirect(HttpServerResponse response, String url) {
|
||||
response.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
|
||||
response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||
.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
|
||||
}
|
||||
|
||||
public static void redirect(HttpServerResponse response, String url, Promise<?> promise) {
|
||||
@@ -26,10 +27,20 @@ public class ResponseUtil {
|
||||
.end(jsonObject.encode());
|
||||
}
|
||||
|
||||
public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject) {
|
||||
ctx.putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.setStatusCode(200)
|
||||
.end(jsonObject.encode());
|
||||
}
|
||||
|
||||
public static <T> void fireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult) {
|
||||
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
|
||||
}
|
||||
|
||||
public static <T> void fireJsonResultResponse(HttpServerResponse ctx, JsonResult<T> jsonResult) {
|
||||
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
|
||||
}
|
||||
|
||||
public static void fireTextResponse(RoutingContext ctx, String text) {
|
||||
ctx.response().putHeader(CONTENT_TYPE, "text/html; charset=utf-8").end(text);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
package cn.qaiu.vx.core.verticle;
|
||||
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.VertxOptions;
|
||||
import io.vertx.core.dns.AddressResolverOptions;
|
||||
import io.vertx.core.http.*;
|
||||
import io.vertx.core.net.NetClient;
|
||||
import io.vertx.core.net.NetClientOptions;
|
||||
import io.vertx.core.net.NetSocket;
|
||||
import io.vertx.core.net.ProxyOptions;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public class HttpProxyVerticle extends AbstractVerticle {
|
||||
|
||||
private HttpClient httpClient;
|
||||
private NetClient netClient;
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
ProxyOptions proxyOptions = new ProxyOptions().setHost("127.0.0.1").setPort(7890);
|
||||
// 初始化 HTTP 客户端,用于向目标服务器发送 HTTP 请求
|
||||
HttpClientOptions httpClientOptions = new HttpClientOptions();
|
||||
httpClient = vertx.createHttpClient(httpClientOptions.setProxyOptions(proxyOptions));
|
||||
|
||||
// 创建并启动 HTTP 代理服务器,监听指定端口
|
||||
HttpServer server = vertx.createHttpServer(new HttpServerOptions().setClientAuth(ClientAuth.REQUIRED));
|
||||
server.requestHandler(this::handleClientRequest);
|
||||
|
||||
// 初始化 NetClient,用于在 CONNECT 请求中建立 TCP 连接隧道
|
||||
netClient = vertx.createNetClient(new NetClientOptions()
|
||||
.setProxyOptions(proxyOptions)
|
||||
.setConnectTimeout(15000)
|
||||
.setTrustAll(true));
|
||||
|
||||
// 启动 HTTP 代理服务器
|
||||
server.listen(7891, ar -> {
|
||||
if (ar.succeeded()) {
|
||||
System.out.println("HTTP Proxy server started on port 7891");
|
||||
} else {
|
||||
System.err.println("Failed to start HTTP Proxy server: " + ar.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理 HTTP CONNECT 请求,用于代理 HTTPS 流量
|
||||
private void handleConnectRequest(HttpServerRequest clientRequest) {
|
||||
String[] uriParts = clientRequest.uri().split(":");
|
||||
if (uriParts.length != 2) {
|
||||
clientRequest.response().setStatusCode(400).end("Bad Request: Invalid URI format");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析目标主机和端口
|
||||
String targetHost = uriParts[0];
|
||||
int targetPort;
|
||||
try {
|
||||
targetPort = Integer.parseInt(uriParts[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
clientRequest.response().setStatusCode(400).end("Bad Request: Invalid port");
|
||||
return;
|
||||
}
|
||||
clientRequest.pause();
|
||||
// 通过 NetClient 连接目标服务器并创建隧道
|
||||
netClient.connect(targetPort, targetHost, connectionAttempt -> {
|
||||
if (connectionAttempt.succeeded()) {
|
||||
NetSocket targetSocket = connectionAttempt.result();
|
||||
|
||||
// 升级客户端连接到 NetSocket 并实现双向数据流
|
||||
clientRequest.toNetSocket().onComplete(clientSocketAttempt -> {
|
||||
if (clientSocketAttempt.succeeded()) {
|
||||
NetSocket clientSocket = clientSocketAttempt.result();
|
||||
|
||||
// 设置双向数据流转发
|
||||
clientSocket.handler(targetSocket::write);
|
||||
targetSocket.handler(clientSocket::write);
|
||||
|
||||
// 关闭其中一方时关闭另一方
|
||||
clientSocket.closeHandler(v -> targetSocket.close());
|
||||
targetSocket.closeHandler(v -> clientSocket.close());
|
||||
} else {
|
||||
System.err.println("Failed to upgrade client connection to socket: " + clientSocketAttempt.cause().getMessage());
|
||||
targetSocket.close();
|
||||
clientRequest.response().setStatusCode(500).end("Internal Server Error");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
System.err.println("Failed to connect to target: " + connectionAttempt.cause().getMessage());
|
||||
clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to connect to target");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理客户端的 HTTP 请求
|
||||
private void handleClientRequest(HttpServerRequest clientRequest) {
|
||||
String s = clientRequest.headers().get("Proxy-Authorization");
|
||||
if (s == null) {
|
||||
clientRequest.response().setStatusCode(403).end();
|
||||
return;
|
||||
}
|
||||
String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
|
||||
if (split.length > 1) {
|
||||
System.out.println(split[0]);
|
||||
System.out.println(split[1]);
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
||||
if (clientRequest.method() == HttpMethod.CONNECT) {
|
||||
// 处理 CONNECT 请求
|
||||
handleConnectRequest(clientRequest);
|
||||
} else {
|
||||
// 处理普通的 HTTP 请求
|
||||
handleHttpRequest(clientRequest);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 HTTP 请求,转发至目标服务器并返回响应
|
||||
private void handleHttpRequest(HttpServerRequest clientRequest) {
|
||||
// 获取目标主机
|
||||
String hostHeader = clientRequest.getHeader("Host");
|
||||
if (hostHeader == null) {
|
||||
clientRequest.response().setStatusCode(400).end("Host header is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
String targetHost = hostHeader.split(":")[0];
|
||||
int targetPort = 80; // 默认为 HTTP 的端口
|
||||
clientRequest.pause(); // 暂停客户端请求的读取,避免数据丢失
|
||||
|
||||
httpClient.request(clientRequest.method(), targetPort, targetHost, clientRequest.uri())
|
||||
.onSuccess(request -> {
|
||||
clientRequest.resume(); // 恢复客户端请求的读取
|
||||
|
||||
// 逐个设置请求头
|
||||
clientRequest.headers().forEach(header -> request.putHeader(header.getKey(), header.getValue()));
|
||||
|
||||
// 将客户端请求的 body 转发给目标服务器
|
||||
clientRequest.bodyHandler(body -> request.send(body, ar -> {
|
||||
if (ar.succeeded()) {
|
||||
var response = ar.result();
|
||||
clientRequest.response().setStatusCode(response.statusCode());
|
||||
clientRequest.response().headers().setAll(response.headers());
|
||||
response.body().onSuccess(b-> clientRequest.response().end(b));
|
||||
} else {
|
||||
clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to reach target");
|
||||
}
|
||||
}));
|
||||
})
|
||||
.onFailure(err -> {
|
||||
err.printStackTrace();
|
||||
clientRequest.response().setStatusCode(502).end("Bad Gateway: Request failed");
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
// 停止 HTTP 客户端以释放资源
|
||||
if (httpClient != null) {
|
||||
httpClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO add Deploy
|
||||
* @param args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// 配置 DNS 解析器,使用多个 DNS 服务器来提升解析速度
|
||||
Vertx vertx = Vertx.vertx(new VertxOptions()
|
||||
.setAddressResolverOptions(new AddressResolverOptions()
|
||||
.addServer("114.114.114.114")
|
||||
.addServer("114.114.115.115")
|
||||
.addServer("8.8.8.8")
|
||||
.addServer("8.8.4.4")));
|
||||
|
||||
// 部署 Verticle 并启动动态 HTTP 代理服务器
|
||||
vertx.deployVerticle(new HttpProxyVerticle());
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,13 @@ import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.http.HttpClient;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.core.http.WebSocket;
|
||||
import io.vertx.core.http.HttpServerOptions;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.net.PemKeyCertOptions;
|
||||
import io.vertx.ext.web.Route;
|
||||
import io.vertx.ext.web.Router;
|
||||
import io.vertx.ext.web.handler.StaticHandler;
|
||||
import io.vertx.ext.web.handler.sockjs.SockJSHandler;
|
||||
import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions;
|
||||
import io.vertx.ext.web.proxy.handler.ProxyHandler;
|
||||
import io.vertx.httpproxy.HttpProxy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -40,14 +40,17 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
.getJsonConfig(ConfigConstant.GLOBAL_CONFIG)
|
||||
.getString("proxyConf");
|
||||
private static final Future<JsonObject> CONFIG = ConfigUtil.readYamlConfig(PATH_PROXY_CONFIG);
|
||||
private static final String DEFAULT_PATH_404 = "webroot/err/404.html";
|
||||
private static final String DEFAULT_PATH_404 = "webroot/err/page404.html";
|
||||
|
||||
private static String serverName = "Vert.x-proxy-server"; //Server name in Http response header
|
||||
|
||||
public static String REROUTE_PATH_PREFIX = "/__rrvpspp"; //re_route_vert_proxy_server_path_prefix 硬编码
|
||||
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> startPromise) throws Exception {
|
||||
public void start(Promise<Void> startPromise) {
|
||||
CONFIG.onSuccess(this::handleProxyConfList);
|
||||
// createFileListener
|
||||
startPromise.complete();
|
||||
}
|
||||
|
||||
@@ -74,22 +77,24 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
* @param proxyConf 代理配置
|
||||
*/
|
||||
private void handleProxyConf(JsonObject proxyConf) {
|
||||
// 404 path
|
||||
if (proxyConf.containsKey("404")) {
|
||||
// page404 path
|
||||
if (proxyConf.containsKey(
|
||||
|
||||
"page404")) {
|
||||
System.getProperty("user.dir");
|
||||
String path = proxyConf.getString("404");
|
||||
String path = proxyConf.getString("page404");
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
proxyConf.put("404", DEFAULT_PATH_404);
|
||||
proxyConf.put("page404", DEFAULT_PATH_404);
|
||||
} else {
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
if (!new File(System.getProperty("user.dir") + path).exists()) {
|
||||
proxyConf.put("404", DEFAULT_PATH_404);
|
||||
proxyConf.put("page404", DEFAULT_PATH_404);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proxyConf.put("404", DEFAULT_PATH_404);
|
||||
proxyConf.put("page404", DEFAULT_PATH_404);
|
||||
}
|
||||
|
||||
final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient();
|
||||
@@ -111,17 +116,10 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
handleStatic(proxyConf.getJsonObject("static"), proxyRouter);
|
||||
}
|
||||
|
||||
// static server
|
||||
if (proxyConf.containsKey("sock")) {
|
||||
handleSock(proxyConf.getJsonArray("sock"), httpClient, proxyRouter);
|
||||
}
|
||||
// Send page404 page
|
||||
proxyRouter.errorHandler(404, ctx -> ctx.response().sendFile(proxyConf.getString("page404")));
|
||||
|
||||
// Send 404 page
|
||||
proxyRouter.errorHandler(404, ctx -> {
|
||||
ctx.response().sendFile(proxyConf.getString("404"));
|
||||
});
|
||||
|
||||
HttpServer server = vertx.createHttpServer();
|
||||
HttpServer server = getHttpsServer(proxyConf);
|
||||
server.requestHandler(proxyRouter);
|
||||
|
||||
Integer port = proxyConf.getInteger("listen");
|
||||
@@ -129,6 +127,38 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
server.listen(port);
|
||||
}
|
||||
|
||||
private HttpServer getHttpsServer(JsonObject proxyConf) {
|
||||
HttpServerOptions httpServerOptions = new HttpServerOptions();
|
||||
if (proxyConf.containsKey("ssl")) {
|
||||
JsonObject sslConfig = proxyConf.getJsonObject("ssl");
|
||||
|
||||
URL sslUrl = this.getClass().getClassLoader().getResource("");
|
||||
if (sslUrl == null) {
|
||||
throw new RuntimeException("SSL url not exist...");
|
||||
}
|
||||
if (sslConfig.containsKey("enable") && sslConfig.getBoolean("enable")) {
|
||||
String sslCertificatePath = sslUrl.getPath() + sslConfig.getString("ssl_certificate");
|
||||
String sslCertificateKeyPath = sslUrl.getPath() + sslConfig.getString("ssl_certificate_key");
|
||||
LOGGER.info("enable ssl config. ");
|
||||
httpServerOptions
|
||||
.setSsl(true)
|
||||
.setKeyCertOptions(
|
||||
new PemKeyCertOptions()
|
||||
.setKeyPath(sslCertificateKeyPath)
|
||||
.setCertPath(sslCertificatePath)
|
||||
).addEnabledSecureTransportProtocol(sslConfig.getString("ssl_protocols"));
|
||||
String sslCiphers = sslConfig.getString("ssl_ciphers");
|
||||
if (sslCiphers != null && !sslCiphers.isEmpty()) {
|
||||
for (String s : sslCiphers.split(":")) {
|
||||
httpServerOptions.addEnabledCipherSuite(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return vertx.createHttpServer(httpServerOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理静态资源配置
|
||||
*
|
||||
@@ -145,9 +175,12 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
ctx.next();
|
||||
});
|
||||
|
||||
final StaticHandler staticHandler = StaticHandler.create();
|
||||
|
||||
StaticHandler staticHandler;
|
||||
if (staticConf.containsKey("root")) {
|
||||
staticHandler.setWebRoot(staticConf.getString("root"));
|
||||
staticHandler = StaticHandler.create(staticConf.getString("root"));
|
||||
} else {
|
||||
staticHandler = StaticHandler.create();
|
||||
}
|
||||
if (staticConf.containsKey("directory-listing")) {
|
||||
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
|
||||
@@ -178,7 +211,7 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
port = 80;
|
||||
}
|
||||
String originPath = url.getPath();
|
||||
LOGGER.debug("Conf(path, originPath, host, port) ----> {},{},{},{}", path, originPath, host, port);
|
||||
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
|
||||
|
||||
// 注意这里不能origin多个代理地址, 一个实例只能代理一个origin
|
||||
final HttpProxy httpProxy = HttpProxy.reverseProxy(httpClient);
|
||||
@@ -189,14 +222,21 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
|
||||
// 代理目标路径为空 就像nginx一样路径穿透 (相对路径)
|
||||
if (StringUtils.isEmpty(originPath) || path.equals(originPath)) {
|
||||
proxyRouter.route(path + "*").handler(ProxyHandler.create(httpProxy));
|
||||
Route route = path.startsWith("~") ? proxyRouter.routeWithRegex(path.substring(1))
|
||||
: proxyRouter.route(path);
|
||||
route.handler(ProxyHandler.create(httpProxy));
|
||||
} else {
|
||||
proxyRouter.route(originPath + "*").handler(ProxyHandler.create(httpProxy));
|
||||
proxyRouter.route(path + "*").handler(ctx -> {
|
||||
String realPath = ctx.request().path();
|
||||
if (realPath.startsWith(path)) {
|
||||
// 配置 /api/, / => 请求 /api/test 代理后 /test
|
||||
// 配置 /api/, /xxx => 请求 /api/test 代理后 /xxx/test
|
||||
final String path0 = path;
|
||||
final String originPath0 = REROUTE_PATH_PREFIX + originPath;
|
||||
|
||||
proxyRouter.route(originPath0 + "*").handler(ProxyHandler.create(httpProxy));
|
||||
proxyRouter.route(path0 + "*").handler(ctx -> {
|
||||
String realPath = ctx.request().uri();
|
||||
if (realPath.startsWith(path0)) {
|
||||
// vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
|
||||
String rePath = realPath.replaceAll("^" + path, originPath);
|
||||
String rePath = realPath.replaceAll("^" + path0, originPath0);
|
||||
ctx.reroute(rePath);
|
||||
} else {
|
||||
ctx.next();
|
||||
@@ -210,54 +250,4 @@ public class ReverseProxyVerticle extends AbstractVerticle {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理websocket
|
||||
*
|
||||
* @param confList sock配置
|
||||
* @param httpClient 客户端
|
||||
* @param proxyRouter 代理路由
|
||||
*/
|
||||
private void handleSock(JsonArray confList, HttpClient httpClient, Router proxyRouter) {
|
||||
// 代理规则
|
||||
confList.stream().map(e -> (JsonObject) e).forEach(conf -> {
|
||||
|
||||
String origin = conf.getString("origin");
|
||||
String path = conf.getString("path");
|
||||
LOGGER.info("websocket proxy: {}, {}",origin,path);
|
||||
|
||||
SockJSHandlerOptions options = new SockJSHandlerOptions()
|
||||
.setHeartbeatInterval(2000)
|
||||
.setRegisterWriteHandler(true);
|
||||
|
||||
SockJSHandler sockJSHandler = SockJSHandler.create(VertxHolder.getVertxInstance(), options);
|
||||
if (!path.startsWith("/")) {
|
||||
path = "/" + path;
|
||||
}
|
||||
|
||||
|
||||
Router route = sockJSHandler.socketHandler(sock -> {
|
||||
sock.handler(buffer -> {
|
||||
Future<WebSocket> webSocketFuture = httpClient.webSocket(8086,"127.0.0.1",sock.uri());
|
||||
webSocketFuture.onSuccess(s -> {
|
||||
System.out.println(buffer.toString());
|
||||
s.write(buffer).onSuccess(v -> {
|
||||
s.handler(w->{
|
||||
System.out.println("--------"+w.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
sock.endHandler(v -> {
|
||||
|
||||
});
|
||||
sock.closeHandler(v -> {
|
||||
|
||||
});
|
||||
});
|
||||
proxyRouter.mountSubRouter("/real/serverApi/test", route);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.qaiu.vx.core.verticle;
|
||||
|
||||
import cn.qaiu.vx.core.handlerfactory.RouterHandlerFactory;
|
||||
import cn.qaiu.vx.core.util.CommonUtil;
|
||||
import cn.qaiu.vx.core.util.JacksonConfig;
|
||||
import cn.qaiu.vx.core.util.SharedDataUtil;
|
||||
import io.vertx.core.AbstractVerticle;
|
||||
import io.vertx.core.Promise;
|
||||
@@ -30,6 +31,8 @@ public class RouterVerticle extends AbstractVerticle {
|
||||
private HttpServer server;
|
||||
|
||||
static {
|
||||
LOGGER.info(JacksonConfig.class.getSimpleName() + " >> ");
|
||||
JacksonConfig.nothing();
|
||||
LOGGER.info("To start listening to port {} ......", port);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package cn.qaiu.vx.core.verticle.conf;
|
||||
|
||||
import io.vertx.codegen.annotations.DataObject;
|
||||
import io.vertx.codegen.json.annotations.JsonGen;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.net.ProxyOptions;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@DataObject
|
||||
@JsonGen(publicConverter = false)
|
||||
public class HttpProxyConf {
|
||||
|
||||
public static final String DEFAULT_USERNAME = UUID.randomUUID().toString();
|
||||
|
||||
public static final String DEFAULT_PASSWORD = UUID.randomUUID().toString();
|
||||
|
||||
public static final Integer DEFAULT_PORT = 6402;
|
||||
|
||||
public static final Integer DEFAULT_TIMEOUT = 15000;
|
||||
|
||||
Integer timeout;
|
||||
|
||||
String username;
|
||||
|
||||
String password;
|
||||
|
||||
Integer port;
|
||||
|
||||
ProxyOptions preProxyOptions;
|
||||
|
||||
public HttpProxyConf() {
|
||||
this.username = DEFAULT_USERNAME;
|
||||
this.password = DEFAULT_PASSWORD;
|
||||
this.timeout = DEFAULT_PORT;
|
||||
this.timeout = DEFAULT_TIMEOUT;
|
||||
this.preProxyOptions = new ProxyOptions();
|
||||
}
|
||||
|
||||
public HttpProxyConf(JsonObject json) {
|
||||
this();
|
||||
}
|
||||
|
||||
|
||||
public Integer getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
public HttpProxyConf setTimeout(Integer timeout) {
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public HttpProxyConf setUsername(String username) {
|
||||
this.username = username;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public HttpProxyConf setPassword(String password) {
|
||||
this.password = password;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public HttpProxyConf setPort(Integer port) {
|
||||
this.port = port;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProxyOptions getPreProxyOptions() {
|
||||
return preProxyOptions;
|
||||
}
|
||||
|
||||
public HttpProxyConf setPreProxyOptions(ProxyOptions preProxyOptions) {
|
||||
this.preProxyOptions = preProxyOptions;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
2
core/src/main/resources/app.properties
Normal file
2
core/src/main/resources/app.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
app.version=${project.version}
|
||||
build=${maven.build.timestamp}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<!-- 日志自定义颜色 -->
|
||||
<!-- https://logback.qos.ch/manual/layouts.html#coloring -->
|
||||
|
||||
<!--日志文件主目录:这里${user.home}为当前服务器用户主目录-->
|
||||
<property name="LOG_HOME" value="logs"/>
|
||||
|
||||
<property name="LOGBACK_DEFAULT" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
|
||||
<property name="CUSTOMER_PATTERN2" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) -> %magenta([%15.15thread]) %cyan(%-40.40logger{39}) : %msg%n"/>
|
||||
|
||||
<!--配置日志文件(File)-->
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<!--设置策略-->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!--日志文件路径:这里%d{yyyyMMdd}表示按天分类日志-->
|
||||
<FileNamePattern>${LOG_HOME}/%d{yyyyMMdd}/run.log</FileNamePattern>
|
||||
<!--日志保留天数-->
|
||||
<MaxHistory>15</MaxHistory>
|
||||
</rollingPolicy>
|
||||
<!--设置格式-->
|
||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
|
||||
<!-- 或者使用默认配置 -->
|
||||
<!--<pattern>${FILE_LOG_PATTERN}</pattern>-->
|
||||
<charset>utf8</charset>
|
||||
</encoder>
|
||||
<!--日志文件最大的大小-->
|
||||
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
|
||||
<MaxFileSize>100MB</MaxFileSize>
|
||||
</triggeringPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- 将文件输出设置成异步输出 -->
|
||||
<appender name="ASYNC-FILE" class="ch.qos.logback.classic.AsyncAppender">
|
||||
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
|
||||
<discardingThreshold>0</discardingThreshold>
|
||||
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
|
||||
<queueSize>256</queueSize>
|
||||
<!-- 添加附加的appender,最多只能添加一个 -->
|
||||
<appender-ref ref="FILE"/>
|
||||
</appender>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
|
||||
<pattern>${CUSTOMER_PATTERN2}</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<logger name="io.netty" level="warn"/>
|
||||
<logger name="io.vertx" level="info"/>
|
||||
<root level="debug">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
</configuration>
|
||||
52
note.txt
52
note.txt
@@ -20,5 +20,55 @@ Cloudreve自建网盘 (ce) {origin}/s/{shareKey}
|
||||
缓存key -> 下载URL
|
||||
分享链接 -> add 网盘类型 pwd origin(私有化) -> 直链
|
||||
|
||||
开源版 TODO
|
||||
1. 缓存优化, 配置自动重载
|
||||
2. 缓存删除接口(后台功能)
|
||||
3. JS脚本引擎 自定义解析
|
||||
|
||||
|
||||
|
||||
专属版 功能设计
|
||||
1. 支持绑定域名, 后台管理-账号管理, token管理, 账号解析次数限制
|
||||
2. 流量统计, 文件分享信息, 目录解析, 文件云下载
|
||||
3. IP代理池
|
||||
|
||||
网页跳转 防盗链
|
||||
可禁用parser接口
|
||||
|
||||
标志短链 鉴权后 生成混淆链接
|
||||
|
||||
|
||||
短链算法:
|
||||
1. 基于Hash映射 hash(type:key:pwd) = h/xxxxx
|
||||
|
||||
鉴权实现:
|
||||
auth-jdbc
|
||||
|
||||
// 基于标准SQL语法
|
||||
|
||||
支持H2, MySQL
|
||||
|
||||
用户:
|
||||
jwt鉴权用户
|
||||
|
||||
角色:
|
||||
超级管理员
|
||||
注册用户
|
||||
|
||||
定义操作(权限):
|
||||
用户的创建/删除/查询/修改, 生成短链/删除短链/修改解析次数和有效期/查询短链信息(
|
||||
文件信息: 文件/文件夹, 文件数量, 文件大小, 文件类型; 链接信息: 解析次数, 缓存次数等)
|
||||
|
||||
微服务设计:
|
||||
TODO
|
||||
|
||||
后台管理:
|
||||
菜单:
|
||||
网盘管理: token配置, 启用/禁用
|
||||
短链管理: 短链列表, 新增, 删除
|
||||
解析统计: 下载次数统计, 下载流量统计, 详细解析列表
|
||||
状态监视: 服务请求并发数; 来源IP列表: 拉黑, 限制次数; Nginx
|
||||
系统配置: 管理员账户, 系统参数: 域名配置, 预览URL,
|
||||
|
||||
|
||||
|
||||
https://f.ws59.cn/f/e3peohu6192
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
</parent>
|
||||
<artifactId>parser</artifactId>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
<name>cn.qaiu:parser</name>
|
||||
<description>NFD parser</description>
|
||||
<url>https://qaiu.top</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
@@ -46,10 +51,39 @@
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.13.2</version>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://opensource.org/license/mit</url>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<name>qaiu</name>
|
||||
<email>qaiu00@gmail.com</email>
|
||||
<organization>https://qaiu.top</organization>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git@github.com:qaiu/netdisk-fast-download.git</connection>
|
||||
<developerConnection>scm:git@github.com:qaiu/netdisk-fast-download.git</developerConnection>
|
||||
<url>git@github.com:qaiu/netdisk-fast-download.git</url>
|
||||
</scm>
|
||||
<distributionManagement>
|
||||
<snapshotRepository>
|
||||
<id>sonatype</id>
|
||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</snapshotRepository>
|
||||
<repository>
|
||||
<id>sonatype</id>
|
||||
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
@@ -62,6 +96,49 @@
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.3.1</version>
|
||||
<configuration>
|
||||
<attach>true</attach>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-site-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
</plugin>
|
||||
<!-- Gpg Signature -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>1.6</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.6.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>central</publishingServerId>
|
||||
<autoPublish>true</autoPublish>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
250
parser/src/main/java/cn/qaiu/entity/FileInfo.java
Normal file
250
parser/src/main/java/cn/qaiu/entity/FileInfo.java
Normal file
@@ -0,0 +1,250 @@
|
||||
package cn.qaiu.entity;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class FileInfo {
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件ID
|
||||
*/
|
||||
private String fileId;
|
||||
|
||||
private String fileIcon;
|
||||
|
||||
/**
|
||||
* 文件大小(byte)
|
||||
*/
|
||||
private Long size;
|
||||
|
||||
private String sizeStr;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private String fileType;
|
||||
|
||||
/**
|
||||
* 文件路径
|
||||
*/
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 创建(上传)时间 yyyy-MM-dd HH:mm:ss格式
|
||||
*/
|
||||
private String createTime;
|
||||
|
||||
/**
|
||||
* 上次修改时间
|
||||
*/
|
||||
private String updateTime;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 文件描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 下载次数
|
||||
*/
|
||||
private Integer downloadCount;
|
||||
|
||||
/**
|
||||
* 网盘标识
|
||||
*/
|
||||
private String panType;
|
||||
|
||||
/**
|
||||
* nfd下载链接(可能获取不到)
|
||||
* note: 不是下载直链
|
||||
*/
|
||||
private String parserUrl;
|
||||
|
||||
//预览地址
|
||||
private String previewUrl;
|
||||
|
||||
// 文件hash默认类型为md5
|
||||
private String hash;
|
||||
|
||||
/**
|
||||
* 扩展参数
|
||||
*/
|
||||
private Map<String, Object> extParameters;
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public FileInfo setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFileId() {
|
||||
return fileId;
|
||||
}
|
||||
|
||||
public FileInfo setFileId(String fileId) {
|
||||
this.fileId = fileId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFileIcon() {
|
||||
return fileIcon;
|
||||
}
|
||||
|
||||
public FileInfo setFileIcon(String fileIcon) {
|
||||
this.fileIcon = fileIcon;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public FileInfo setSize(Long size) {
|
||||
this.size = size;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getSizeStr() {
|
||||
return sizeStr;
|
||||
}
|
||||
|
||||
public FileInfo setSizeStr(String sizeStr) {
|
||||
this.sizeStr = sizeStr;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
public FileInfo setFileType(String fileType) {
|
||||
this.fileType = fileType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getFilePath() {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
public FileInfo setFilePath(String filePath) {
|
||||
this.filePath = filePath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public FileInfo setCreateTime(String createTime) {
|
||||
this.createTime = createTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUpdateTime() {
|
||||
return updateTime;
|
||||
}
|
||||
|
||||
public FileInfo setUpdateTime(String updateTime) {
|
||||
this.updateTime = updateTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCreateBy() {
|
||||
return createBy;
|
||||
}
|
||||
|
||||
public FileInfo setCreateBy(String createBy) {
|
||||
this.createBy = createBy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public FileInfo setDescription(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getDownloadCount() {
|
||||
return downloadCount;
|
||||
}
|
||||
|
||||
public FileInfo setDownloadCount(Integer downloadCount) {
|
||||
this.downloadCount = downloadCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getPanType() {
|
||||
return panType;
|
||||
}
|
||||
|
||||
public FileInfo setPanType(String panType) {
|
||||
this.panType = panType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getParserUrl() {
|
||||
return parserUrl;
|
||||
}
|
||||
|
||||
public FileInfo setParserUrl(String parserUrl) {
|
||||
this.parserUrl = parserUrl;
|
||||
return this;
|
||||
}
|
||||
public String getPreviewUrl() {
|
||||
return previewUrl;
|
||||
}
|
||||
public FileInfo setPreviewUrl(String previewUrl) {
|
||||
this.previewUrl = previewUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public FileInfo setHash(String hash) {
|
||||
this.hash = hash;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getExtParameters() {
|
||||
return extParameters;
|
||||
}
|
||||
|
||||
public FileInfo setExtParameters(Map<String, Object> extParameters) {
|
||||
this.extParameters = extParameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FileInfo{" +
|
||||
"fileName='" + fileName + '\'' +
|
||||
", fileId='" + fileId + '\'' +
|
||||
", size=" + size +
|
||||
", fileType='" + fileType + '\'' +
|
||||
", filePath='" + filePath + '\'' +
|
||||
", createTime='" + createTime + '\'' +
|
||||
", updateTime='" + updateTime + '\'' +
|
||||
", createBy='" + createBy + '\'' +
|
||||
", description='" + description + '\'' +
|
||||
", downloadCount=" + downloadCount +
|
||||
", extParameters=" + extParameters +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,35 @@
|
||||
package cn.qaiu.entity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ShareLinkInfo {
|
||||
|
||||
private String shareKey; // 分享键
|
||||
|
||||
private String panName; // 网盘名称
|
||||
private String type; // 分享类型
|
||||
private String sharePassword; // 分享密码(如果存在)
|
||||
private String shareUrl; // 原始分享链接
|
||||
private String standardUrl; // 规范化的标准链接
|
||||
|
||||
/**
|
||||
* 其他参数预定义
|
||||
* dirId: 目录ID 传入
|
||||
* auths: 认证相关 传入
|
||||
* UA: 浏览器请求头 传入
|
||||
* fileInfo: 解析成功的文件信息对象 传出
|
||||
*/
|
||||
private Map<String, Object> otherParam;
|
||||
|
||||
private ShareLinkInfo(Builder builder) {
|
||||
this.shareKey = builder.shareKey;
|
||||
this.panName = builder.panName;
|
||||
this.type = builder.type;
|
||||
this.sharePassword = builder.sharePassword;
|
||||
this.shareUrl = builder.shareUrl;
|
||||
this.standardUrl = builder.standardUrl;
|
||||
this.otherParam = builder.otherParam;
|
||||
}
|
||||
|
||||
// Getter和Setter方法
|
||||
@@ -22,10 +38,18 @@ public class ShareLinkInfo {
|
||||
return shareKey;
|
||||
}
|
||||
|
||||
public String getPanName() {
|
||||
return panName;
|
||||
}
|
||||
|
||||
public void setShareKey(String shareKey) {
|
||||
this.shareKey = shareKey;
|
||||
}
|
||||
|
||||
public void setPanName(String panName) {
|
||||
this.panName = panName;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
@@ -58,6 +82,24 @@ public class ShareLinkInfo {
|
||||
this.standardUrl = standardUrl;
|
||||
}
|
||||
|
||||
public String getCacheKey() {
|
||||
// 将type和shareKey组合成一个字符串作为缓存key
|
||||
String key = type + ":" + shareKey;
|
||||
if (type.equals("p115")) {
|
||||
key += ("_" + otherParam.get("UA").toString().hashCode());
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
public ShareLinkInfo setOtherParam(Map<String, Object> otherParam) {
|
||||
this.otherParam = otherParam;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> getOtherParam() {
|
||||
return otherParam;
|
||||
}
|
||||
|
||||
// 静态方法创建建造者对象
|
||||
public static ShareLinkInfo.Builder newBuilder() {
|
||||
return new ShareLinkInfo.Builder();
|
||||
@@ -65,17 +107,24 @@ public class ShareLinkInfo {
|
||||
|
||||
// 建造者类
|
||||
public static class Builder {
|
||||
public String panName; // 分享网盘名称
|
||||
private String shareKey; // 分享键
|
||||
private String type; // 分享类型 (网盘模板枚举的小写)
|
||||
private String sharePassword = ""; // 分享密码(如果存在)
|
||||
private String shareUrl; // 原始分享链接
|
||||
private String standardUrl; // 规范化的标准链接
|
||||
private Map<String, Object> otherParam = new HashMap<>(); // 其他参数
|
||||
|
||||
public Builder shareKey(String shareKey) {
|
||||
this.shareKey = shareKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder panName(String panName) {
|
||||
this.panName = panName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
@@ -96,6 +145,11 @@ public class ShareLinkInfo {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder otherParam(Map<String, Object> otherParam) {
|
||||
this.otherParam = otherParam;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShareLinkInfo build() {
|
||||
return new ShareLinkInfo(this);
|
||||
}
|
||||
@@ -105,10 +159,12 @@ public class ShareLinkInfo {
|
||||
public String toString() {
|
||||
return "ShareLinkInfo{" +
|
||||
"shareKey='" + shareKey + '\'' +
|
||||
", panName='" + panName + '\'' +
|
||||
", type='" + type + '\'' +
|
||||
", sharePassword='" + sharePassword + '\'' +
|
||||
", shareUrl='" + shareUrl + '\'' +
|
||||
", standardUrl='" + standardUrl + '\'' +
|
||||
", otherParam='" + otherParam + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IPanTool {
|
||||
Future<String> parse();
|
||||
|
||||
default String parseSync() {
|
||||
return parse().toCompletionStage().toCompletableFuture().join();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件列表
|
||||
* @return List
|
||||
*/
|
||||
default Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
promise.fail("Not implemented yet");
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件ID获取下载链接
|
||||
* @return url
|
||||
*/
|
||||
default Future<String> parseById() {
|
||||
Promise<String> promise = Promise.promise();
|
||||
promise.complete("Not implemented yet");
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,38 @@ package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.WebClientVertxInit;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Handler;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.net.ProxyOptions;
|
||||
import io.vertx.core.net.ProxyType;
|
||||
import io.vertx.ext.web.client.HttpResponse;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientOptions;
|
||||
import io.vertx.ext.web.client.WebClientSession;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* 解析器抽象类包含promise, HTTP Client, 默认失败方法等;
|
||||
* 新增网盘解析器需要继承该类.
|
||||
* 新增网盘解析器需要继承该类. <br>
|
||||
* <h2>实现类命名规则: </h2>
|
||||
* <p>{网盘标识}Tool, 网盘标识不超过5个字符, 可以取网盘名称首字母缩写或拼音首字母, <br>
|
||||
* 音乐类型的解析以M开头, 例如网易云音乐Mne</p>
|
||||
*/
|
||||
public abstract class PanBase {
|
||||
public abstract class PanBase implements IPanTool {
|
||||
protected Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
protected Promise<String> promise = Promise.promise();
|
||||
@@ -22,7 +41,8 @@ public abstract class PanBase {
|
||||
/**
|
||||
* Http client
|
||||
*/
|
||||
protected WebClient client = WebClient.create(WebClientVertxInit.get());
|
||||
protected WebClient client = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions());
|
||||
|
||||
/**
|
||||
* Http client session (会话管理, 带cookie请求)
|
||||
@@ -46,11 +66,47 @@ public abstract class PanBase {
|
||||
* }
|
||||
* </pre></blockquote>
|
||||
*
|
||||
* @param shareLinkInfo 分享链接信息
|
||||
*/
|
||||
public PanBase(ShareLinkInfo shareLinkInfo) {
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
if (shareLinkInfo.getOtherParam().containsKey("proxy")) {
|
||||
JsonObject proxy = (JsonObject) shareLinkInfo.getOtherParam().get("proxy");
|
||||
ProxyOptions proxyOptions = new ProxyOptions()
|
||||
.setType(ProxyType.valueOf(proxy.getString("type").toUpperCase()))
|
||||
.setHost(proxy.getString("host"))
|
||||
.setPort(proxy.getInteger("port"));
|
||||
if (StringUtils.isNotEmpty(proxy.getString("username"))) {
|
||||
proxyOptions.setUsername(proxy.getString("username"));
|
||||
}
|
||||
if (StringUtils.isNotEmpty(proxy.getString("password"))) {
|
||||
proxyOptions.setPassword(proxy.getString("password"));
|
||||
}
|
||||
this.client = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions()
|
||||
.setUserAgentEnabled(false)
|
||||
.setProxyOptions(proxyOptions));
|
||||
|
||||
this.clientSession = WebClientSession.create(client);
|
||||
this.clientNoRedirects = WebClient.create(WebClientVertxInit.get(),
|
||||
new WebClientOptions().setFollowRedirects(false)
|
||||
.setUserAgentEnabled(false)
|
||||
.setProxyOptions(proxyOptions));
|
||||
}
|
||||
}
|
||||
|
||||
protected PanBase() {
|
||||
}
|
||||
|
||||
protected String baseMsg() {
|
||||
if (shareLinkInfo.getShareUrl() != null) {
|
||||
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": url=" + shareLinkInfo.getShareUrl();
|
||||
}
|
||||
return shareLinkInfo.getPanName() + "-" + shareLinkInfo.getType() + ": key=" + shareLinkInfo.getShareKey() +
|
||||
";pwd=" + shareLinkInfo.getSharePassword();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 失败时生成异常消息
|
||||
*
|
||||
@@ -60,13 +116,22 @@ public abstract class PanBase {
|
||||
*/
|
||||
protected void fail(Throwable t, String errorMsg, Object... args) {
|
||||
try {
|
||||
// 判断是否已经完成
|
||||
if (promise.future().isComplete()) {
|
||||
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
|
||||
return;
|
||||
}
|
||||
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
|
||||
log.error("解析异常: " + s, t.fillInStackTrace());
|
||||
promise.fail(this.getClass().getSimpleName() + ": 解析异常: " + s + " -> " + t);
|
||||
promise.fail(baseMsg() + ": 解析异常: " + s + " -> " + t);
|
||||
} catch (Exception e) {
|
||||
log.error("ErrorMsg format fail. The parameter has been discarded", e);
|
||||
log.error("解析异常: " + errorMsg, t.fillInStackTrace());
|
||||
promise.fail(this.getClass().getSimpleName() + ": 解析异常: " + errorMsg + " -> " + t);
|
||||
if (promise.future().isComplete()) {
|
||||
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
|
||||
return;
|
||||
}
|
||||
promise.fail(baseMsg() + ": 解析异常: " + errorMsg + " -> " + t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,16 +143,27 @@ public abstract class PanBase {
|
||||
*/
|
||||
protected void fail(String errorMsg, Object... args) {
|
||||
try {
|
||||
// 判断是否已经完成
|
||||
if (promise.future().isComplete()) {
|
||||
log.warn("Promise 已经完成, 无法再次失败: {}, {}", errorMsg, promise.future().cause());
|
||||
return;
|
||||
}
|
||||
String s = String.format(errorMsg.replaceAll("\\{}", "%s"), args);
|
||||
log.error("解析异常: " + s);
|
||||
promise.fail(this.getClass().getSimpleName() + " - 解析异常: " + s);
|
||||
promise.fail(baseMsg() + " - 解析异常: " + s);
|
||||
} catch (Exception e) {
|
||||
if (promise.future().isComplete()) {
|
||||
log.warn("ErrorMsg format. Promise 已经完成, 无法再次失败: {}", errorMsg);
|
||||
return;
|
||||
}
|
||||
log.error("ErrorMsg format fail. The parameter has been discarded", e);
|
||||
log.error("解析异常: " + errorMsg);
|
||||
promise.fail(this.getClass().getSimpleName() + " - 解析异常: " + errorMsg);
|
||||
promise.fail(baseMsg() + " - 解析异常: " + errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
protected void fail() {
|
||||
fail("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成失败Future的处理器
|
||||
*
|
||||
@@ -95,7 +171,133 @@ public abstract class PanBase {
|
||||
* @return Handler
|
||||
*/
|
||||
protected Handler<Throwable> handleFail(String errorMsg) {
|
||||
return t -> fail(this.getClass().getSimpleName() + " - 请求异常 {}: -> {}", errorMsg, t.fillInStackTrace());
|
||||
return t -> fail(baseMsg() + " - 请求异常 {}: -> {}", errorMsg, t.fillInStackTrace());
|
||||
}
|
||||
|
||||
protected Handler<Throwable> handleFail() {
|
||||
return handleFail("");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* bodyAsJsonObject的封装, 会自动处理异常
|
||||
*
|
||||
* @param res HttpResponse
|
||||
* @return JsonObject
|
||||
*/
|
||||
protected JsonObject asJson(HttpResponse<?> res) {
|
||||
// 检查响应头中的Content-Encoding是否为gzip
|
||||
String contentEncoding = res.getHeader("Content-Encoding");
|
||||
try {
|
||||
if ("gzip".equalsIgnoreCase(contentEncoding)) {
|
||||
// 如果是gzip压缩的响应体,解压
|
||||
return new JsonObject(decompressGzip((Buffer) res.body()));
|
||||
} else {
|
||||
return res.bodyAsJsonObject();
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
if ("gzip".equalsIgnoreCase(contentEncoding)) {
|
||||
// 如果是gzip压缩的响应体,解压
|
||||
try {
|
||||
log.error(decompressGzip((Buffer) res.body()));
|
||||
fail(decompressGzip((Buffer) res.body()));
|
||||
//throw new RuntimeException("响应不是JSON格式");
|
||||
} catch (IOException ex) {
|
||||
log.error("响应gzip解压失败");
|
||||
fail("响应gzip解压失败: {}", ex.getMessage());
|
||||
//throw new RuntimeException("响应gzip解压失败", ex);
|
||||
}
|
||||
} else {
|
||||
log.error("解析失败: json格式异常: {}", res.bodyAsString());
|
||||
fail("解析失败: json格式异常: {}", res.bodyAsString());
|
||||
//throw new RuntimeException("解析失败: json格式异常");
|
||||
}
|
||||
return JsonObject.of();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* body To text的封装, 会自动处理异常, 会自动解压gzip
|
||||
* @param res HttpResponse
|
||||
* @return String
|
||||
*/
|
||||
protected String asText(HttpResponse<?> res) {
|
||||
// 检查响应头中的Content-Encoding是否为gzip
|
||||
String contentEncoding = res.getHeader("Content-Encoding");
|
||||
try {
|
||||
if ("gzip".equalsIgnoreCase(contentEncoding)) {
|
||||
// 如果是gzip压缩的响应体,解压
|
||||
return decompressGzip((Buffer) res.body());
|
||||
} else {
|
||||
return res.bodyAsString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
fail("解析失败: res格式异常");
|
||||
//throw new RuntimeException("解析失败: res格式异常");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void complete(String url) {
|
||||
promise.complete(url);
|
||||
}
|
||||
|
||||
protected Future<String> future() {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用下一个解析器, 通用域名解析
|
||||
*/
|
||||
protected void nextParser() {
|
||||
Iterator<PanDomainTemplate> iterator = Arrays.asList(PanDomainTemplate.values()).iterator();
|
||||
while (iterator.hasNext()) {
|
||||
if (iterator.next().name().equalsIgnoreCase(shareLinkInfo.getType())) {
|
||||
if (iterator.hasNext()) {
|
||||
PanDomainTemplate next = iterator.next();
|
||||
log.debug("规则不匹配, 处理解析器转发: {} -> {}", shareLinkInfo.getPanName(), next.getDisplayName());
|
||||
ParserCreate.fromType(next.name())
|
||||
.fromAnyShareUrl(shareLinkInfo.getShareUrl())
|
||||
.createTool()
|
||||
.parse()
|
||||
.onComplete(promise);
|
||||
} else {
|
||||
fail("error: 没有下一个解析处理器");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解压gzip数据
|
||||
* @param compressedData compressedData
|
||||
* @return String
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
private String decompressGzip(Buffer compressedData) throws IOException {
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(compressedData.getBytes());
|
||||
GZIPInputStream gzis = new GZIPInputStream(bais);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(gzis,
|
||||
StandardCharsets.UTF_8))) {
|
||||
|
||||
// 用于存储解压后的字符串
|
||||
StringBuilder decompressedData = new StringBuilder();
|
||||
|
||||
// 逐行读取解压后的数据
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
decompressedData.append(line);
|
||||
}
|
||||
|
||||
// 此时decompressedData.toString()包含了解压后的字符串
|
||||
return decompressedData.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected String getDomainName(){
|
||||
return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.parser.impl.*;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.regex.Pattern.compile;
|
||||
|
||||
/**
|
||||
* 枚举类 PanDomainTemplate 定义了不同网盘服务的模板信息,包括:
|
||||
* <ul>
|
||||
@@ -16,94 +22,371 @@ import cn.qaiu.parser.impl.*;
|
||||
*/
|
||||
public enum PanDomainTemplate {
|
||||
|
||||
|
||||
// https://www.ilanzou.com/s/
|
||||
IZ("蓝奏云优享",
|
||||
compile("https://www\\.ilanzou\\.com/s/(?<KEY>.+)"),
|
||||
"https://www.ilanzou.com/s/{shareKey}",
|
||||
IzTool.class),
|
||||
// 网盘定义
|
||||
/*
|
||||
lanzoul.com
|
||||
lanzouh.com
|
||||
lanosso.com
|
||||
lanpv.com
|
||||
bakstotre.com
|
||||
lanzouo.com
|
||||
lanzov.com
|
||||
lanpw.com
|
||||
ulanzou.com
|
||||
lanzouf.com
|
||||
lanzn.com
|
||||
lanzouj.com
|
||||
lanzouk.com
|
||||
lanzouq.com
|
||||
lanzouv.com
|
||||
lanzoue.com
|
||||
lanzouw.com
|
||||
lanzoub.com
|
||||
lanzouu.com
|
||||
lanwp.com
|
||||
lanzouy.com
|
||||
lanzoup.com
|
||||
woozooo.com
|
||||
lanzv.com
|
||||
dmpdmp.com
|
||||
lanrar.com
|
||||
webgetstore.com
|
||||
lanzb.com
|
||||
lanzoux.com
|
||||
lanzout.com
|
||||
lanzouc.com
|
||||
ilanzou.com
|
||||
lanzoui.com
|
||||
lanzoug.com
|
||||
lanzoum.com
|
||||
t-is.cn
|
||||
*/
|
||||
LZ("蓝奏云",
|
||||
"https://([a-z]+)?\\.?lanzou[a-z]\\.com/(.+/)?(.+)",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?(" +
|
||||
"lanzoul|" +
|
||||
"lanzouh|" +
|
||||
"lanosso|" +
|
||||
"lanpv|" +
|
||||
"bakstotre|" +
|
||||
"lanzouo|" +
|
||||
"lanzov|" +
|
||||
"lanpw|" +
|
||||
"ulanzou|" +
|
||||
"lanzouf|" +
|
||||
"lanzn|" +
|
||||
"lanzouj|" +
|
||||
"lanzouk|" +
|
||||
"lanzouq|" +
|
||||
"lanzouv|" +
|
||||
"lanzoue|" +
|
||||
"lanzouw|" +
|
||||
"lanzoub|" +
|
||||
"lanzouu|" +
|
||||
"lanwp|" +
|
||||
"lanzouy|" +
|
||||
"lanzoup|" +
|
||||
"woozooo|" +
|
||||
"lanzv|" +
|
||||
"dmpdmp|" +
|
||||
"lanrar|" +
|
||||
"webgetstore|" +
|
||||
"lanzb|" +
|
||||
"lanzoux|" +
|
||||
"lanzout|" +
|
||||
"lanzouc|" +
|
||||
"lanzoui|" +
|
||||
"lanzoug|" +
|
||||
"lanzoum" +
|
||||
")\\.com/(.+/)?(?<KEY>.+)"),
|
||||
"https://lanzoux.com/{shareKey}",
|
||||
LzTool.class),
|
||||
|
||||
// https://www.feijix.com/s/
|
||||
// https://share.feijipan.com/s/
|
||||
FJ("小飞机网盘",
|
||||
"https://(share\\.feijipan\\.com|www\\.feijix\\.com)/s/(.+)",
|
||||
compile("https://(share\\.feijipan\\.com|www\\.feijix\\.com)/s/(?<KEY>.+)"),
|
||||
"https://www.feijix.com/s/{shareKey}",
|
||||
FjTool.class),
|
||||
|
||||
// https://lecloud.lenovo.com/share/
|
||||
LE("联想乐云",
|
||||
"https://lecloud?\\.lenovo\\.com/share/(.+)",
|
||||
compile("https://lecloud?\\.lenovo\\.com/share/(?<KEY>.+)"),
|
||||
"https://lecloud.lenovo.com/share/{shareKey}",
|
||||
LeTool.class),
|
||||
|
||||
// https://v2.fangcloud.com/s/
|
||||
FC("亿方云",
|
||||
"https://v2\\.fangcloud\\.(com|cn)/(s|sharing)/([^/]+)",
|
||||
compile("https://v2\\.fangcloud\\.(com|cn)/(s|sharing)/(?<KEY>.+)"),
|
||||
"https://v2.fangcloud.com/s/{shareKey}",
|
||||
"https://www.fangcloud.com/",
|
||||
FcTool.class),
|
||||
// https://www.ilanzou.com/s/
|
||||
IZ("蓝奏云优享",
|
||||
"https://www\\.ilanzou\\.com/s/(.+)",
|
||||
"https://www.ilanzou.com/s/{shareKey}",
|
||||
IzTool.class),
|
||||
// https://wx.mail.qq.com/ftn/download?
|
||||
QQ("QQ邮箱中转站",
|
||||
"https://i?wx\\.mail\\.qq\\.com/ftn/download\\?(.+)",
|
||||
compile("https://i?wx\\.mail\\.qq\\.com/ftn/download\\?(?<KEY>.+)"),
|
||||
"https://iwx.mail.qq.com/ftn/download/{shareKey}",
|
||||
"https://mail.qq.com",
|
||||
QQTool.class),
|
||||
// https://wx.mail.qq.com/s?k=uAG9JR42Rqgt010mFp
|
||||
QQW("QQ邮箱云盘",
|
||||
compile("https://i?wx\\.mail\\.qq\\.com/s\\?k=(?<KEY>.+)"),
|
||||
"https://wx.mail.qq.com/s?k={shareKey}",
|
||||
"https://mail.qq.com",
|
||||
QQwTool.class),
|
||||
// https://qfile.qq.com/q/xxx
|
||||
QQSC("QQ闪传",
|
||||
compile("https://qfile\\.qq\\.com/q/(?<KEY>.+)"),
|
||||
"https://qfile.qq.com/q/{shareKey}",
|
||||
QQscTool.class),
|
||||
// https://f.ws59.cn/f/或者https://www.wenshushu.cn/f/
|
||||
WS("文叔叔",
|
||||
"https://(f\\.ws59\\.cn|www\\.wenshushu\\.cn)/f/(.+)",
|
||||
"https://f.ws59.cn/f/{shareKey}",
|
||||
compile("https://(f\\.ws(\\d{2})\\.cn|www\\.wenshushu\\.cn)/f/(?<KEY>.+)"),
|
||||
"https://www.wenshushu.cn/f/{shareKey}",
|
||||
WsTool.class),
|
||||
// https://www.123pan.com/s/
|
||||
/*
|
||||
123254.com
|
||||
123957.com
|
||||
123295.com
|
||||
123panpay.com
|
||||
123860.com
|
||||
123pan.com
|
||||
123245.com
|
||||
123278.com
|
||||
123842.com
|
||||
123294.com
|
||||
123865.com
|
||||
123773.com
|
||||
123624.com
|
||||
123684.com
|
||||
123641.com
|
||||
123259.com
|
||||
123912.com
|
||||
123952.com
|
||||
123652.com
|
||||
123pan.cn
|
||||
123635.com
|
||||
123242.com
|
||||
123795.com
|
||||
*/
|
||||
YE("123网盘",
|
||||
"https://www\\.123pan\\.com/s/(.+)",
|
||||
compile("https://www\\.(" +
|
||||
"123254\\.com|" +
|
||||
"123957\\.com|" +
|
||||
"123295\\.com|" +
|
||||
"123panpay\\.com|" +
|
||||
"123860\\.com|" +
|
||||
"123pan\\.com|" +
|
||||
"123245\\.com|" +
|
||||
"123278\\.com|" +
|
||||
"123842\\.com|" +
|
||||
"123294\\.com|" +
|
||||
"123865\\.com|" +
|
||||
"123773\\.com|" +
|
||||
"123624\\.com|" +
|
||||
"123684\\.com|" +
|
||||
"123641\\.com|" +
|
||||
"123259\\.com|" +
|
||||
"123912\\.com|" +
|
||||
"123952\\.com|" +
|
||||
"123652\\.com|" +
|
||||
"123pan\\.cn|" +
|
||||
"123635\\.com|" +
|
||||
"123242\\.com|" +
|
||||
"123795\\.com" +
|
||||
")/s/(?<KEY>.+)(.html)?"),
|
||||
"https://www.123pan.com/s/{shareKey}",
|
||||
YeTool.class),
|
||||
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={code}&isShare=1
|
||||
EC("移动云空间",
|
||||
"https://www\\.ecpan\\.cn/web(/%23|/#)?/yunpanProxy\\?path=.*&data=" +
|
||||
"([^&]+)&isShare=1",
|
||||
compile("https://www\\.ecpan\\.cn/web(/%23|/#)?/yunpanProxy\\?path=.*&data=" +
|
||||
"(?<KEY>[^&]+)&isShare=1"),
|
||||
"https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data={shareKey}&isShare=1",
|
||||
EcTool.class),
|
||||
// https://cowtransfer.com/s/
|
||||
COW("奶牛快传",
|
||||
"https://(.*)cowtransfer\\.com/s/(.+)",
|
||||
compile("https://(.*)cowtransfer\\.com/s/(?<KEY>.+)"),
|
||||
"https://cowtransfer.com/s/{shareKey}",
|
||||
CowTool.class),
|
||||
// https://pan.huang1111.cn/s/
|
||||
CE("huang1111",
|
||||
"https://pan\\.huang1111\\.cn/s/(.+)",
|
||||
"https://pan.huang1111.cn/s/{shareKey}",
|
||||
CeTool.class);
|
||||
CT("城通网盘",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" +
|
||||
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
|
||||
"https://474b.com/file/{shareKey}",
|
||||
CtTool.class),
|
||||
// https://xxx.118pan.com/bxxx
|
||||
P118("118网盘",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?118pan\\.com/b(?<KEY>.+)"),
|
||||
"https://qaiu.118pan.com/b{shareKey}",
|
||||
P118Tool.class),
|
||||
// https://www.vyuyun.com/s/QMa6ie?password=I4KG7H
|
||||
// https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H
|
||||
PVYY("微雨云存储",
|
||||
compile("https://www\\.vyuyun\\.com/s/(?<KEY>[a-zA-Z\\d-]+)(/file)?(\\?password=(?<PWD>\\w+))?"),
|
||||
"https://www.vyuyun.com/s/{shareKey}?password={pwd}",
|
||||
PvyyTool.class),
|
||||
// https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp
|
||||
// https://1drv.ms/u/c/abfd0a26e47d3458/EdYACWvPq85Et797YmvL5LgBruUKoNxqIFATXhIv1PI2_Q?e=z4ffNJ
|
||||
POD("OneDrive",
|
||||
compile("https://1drv\\.ms/(?<KEY>.+)"),
|
||||
"https://1drv\\.ms/{shareKey}",
|
||||
"https://onedrive.live.com/",
|
||||
PodTool.class),
|
||||
// 404网盘 https://drive.google.com/file/d/xxx/view?usp=sharing
|
||||
PGD("GoogleDrive",
|
||||
compile("https://drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?"),
|
||||
"https://drive.google.com/file/d/{shareKey}/view?usp=sharing",
|
||||
PgdTool.class),
|
||||
// iCloud https://www.icloud.com.cn/iclouddrive/xxx#fonts
|
||||
PIC("iCloud",
|
||||
compile("https://www\\.icloud\\.com(\\.cn)?/iclouddrive/(?<KEY>[a-z_A-Z\\d-=]+)(#(.+))?"),
|
||||
"https://www.icloud.com.cn/iclouddrive/{shareKey}",
|
||||
PicTool.class),
|
||||
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
|
||||
PDB("dropbox",
|
||||
compile("https://www.dropbox.com/scl/fi/(?<KEY>\\w+)/.+?rlkey=(?<PWD>\\w+).*"),
|
||||
"https://www.dropbox.com/scl/fi/{shareKey}/?rlkey={pwd}&dl=0",
|
||||
PdbTool.class),
|
||||
P115("115网盘",
|
||||
compile("https://(115|anxia).com/s/(?<KEY>\\w+)(\\?password=(?<PWD>\\w+))?([&#].*)?"),
|
||||
"https://115.com/s/{shareKey}?password={pwd}",
|
||||
P115Tool.class),
|
||||
// 链接:https://www.yunpan.com/surl_yD7wz4VgU9v(提取码:fc70)
|
||||
// P360("360云盘(需要referer头)",
|
||||
// compile("https://www\\.yunpan\\.com/(?<KEY>\\w+)"),
|
||||
// "https://www.yunpan.com/{shareKey}",
|
||||
// P360Tool.class),
|
||||
|
||||
// https://pan-yz.cldisk.com/external/m/file/953658049102462976
|
||||
Pcx("超星云盘(需要referer头)",
|
||||
compile("https://pan-yz\\.cldisk\\.com/external/m/file/(?<KEY>\\w+)"),
|
||||
"https://pan-yz.cldisk.com/external/m/file/{shareKey}",
|
||||
PcxTool.class),
|
||||
// =====================音乐类解析 分享链接标志->MxxS (单歌曲/普通音质)==========================
|
||||
// http://163cn.tv/xxx
|
||||
MNES("网易云音乐分享",
|
||||
compile("http(s)?://163cn\\.tv/(?<KEY>.+)"),
|
||||
"https://163cn.tv/{shareKey}",
|
||||
MnesTool.class),
|
||||
// https://music.163.com/#/song?id=xxx
|
||||
MNE("网易云音乐歌曲详情",
|
||||
compile("https://(y.)?music\\.163\\.com/(#|m/)?song\\?id=(?<KEY>.+)(&.*)?"),
|
||||
"https://music.163.com/#/song?id={shareKey}",
|
||||
MnesTool.MneTool.class),
|
||||
// https://c6.y.qq.com/base/fcgi-bin/u?__=xxx
|
||||
MQQS("QQ音乐分享",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?y\\.qq\\.com/base/fcgi-bin/u\\?__=(?<KEY>.+)"),
|
||||
"https://c6.y.qq.com/base/fcgi-bin/u?__={shareKey}",
|
||||
MqqsTool.class),
|
||||
// https://y.qq.com/n/ryqq/songDetail/000XjcLg0fbRjv?songtype=0
|
||||
MQQ("QQ音乐歌曲详情",
|
||||
compile("https://y\\.qq\\.com/n/ryqq/songDetail/(?<KEY>.+)(\\?.*)?"),
|
||||
"https://y.qq.com/n/ryqq/songDetail/{shareKey}",
|
||||
MqqsTool.MqqTool.class),
|
||||
|
||||
// https://t1.kugou.com/song.html?id=xxx
|
||||
MKGS("酷狗音乐分享",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/song\\.html\\?id=(?<KEY>.+)"),
|
||||
"https://t1.kugou.com/song.html?id={shareKey}",
|
||||
MkgsTool.class),
|
||||
// https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4"
|
||||
MKGS2("酷狗音乐分享2",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/share/(?<KEY>.+).html.*"),
|
||||
"https://www.kugou.com/share/{shareKey}.html",
|
||||
MkgsTool.Mkgs2Tool.class),
|
||||
// https://www.kugou.com/mixsong/2bi8Fe9CSV3
|
||||
MKG("酷狗音乐歌曲详情",
|
||||
compile("https://(?:[a-zA-Z\\d-]+\\.)?kugou\\.com/mixsong/(?<KEY>.+)\\.html.*"),
|
||||
"https://www.kugou.com/mixsong/{shareKey}.html",
|
||||
MkgsTool.MkgTool.class),
|
||||
// https://kuwo.cn/play_detail/395500809
|
||||
// https://m.kuwo.cn/newh5app/play_detail/318448522
|
||||
MKWS("酷我音乐分享*",
|
||||
compile("https://(m\\.)?kuwo\\.cn/(newh5app/)?play_detail/(?<KEY>.+)"),
|
||||
"https://kuwo.cn/play_detail/{shareKey}",
|
||||
MkwTool.class),
|
||||
// https://music.migu.cn/v3/music/song/6326951FKBJ?channelId=001002H
|
||||
MMGS("咪咕音乐分享",
|
||||
compile("https://music\\.migu\\.cn/v3/music/song/(?<KEY>.+)(\\?.*)?"),
|
||||
"https://music.migu.cn/v3/music/song/{shareKey}",
|
||||
MmgTool.class),
|
||||
// =====================私有盘解析==========================
|
||||
|
||||
// Cloudreve自定义域名解析, 解析器CeTool兜底策略, 即任意域名如果匹配不到对应的规则, 则由CeTool统一处理,
|
||||
// 如果不属于Cloudreve盘 则调用下一个自定义域名解析器, 若都处理不了则抛出异常, 这种匹配模式类似责任链
|
||||
// https://pan.huang1111.cn/s/xxx
|
||||
// 通用域名([a-z\\d]+(-[a-z\\d]+)*\.)+[a-z]{2,}
|
||||
CE("Cloudreve",
|
||||
compile("http(s)?://([a-zA-Z\\d]+(-[a-zA-Z\\d]+)*\\.)+[a-zA-Z]{2,}(/s)?/(?<KEY>.+)"),
|
||||
"https://{any}/s/{shareKey}",
|
||||
"https://cloudreve.org/",
|
||||
CeTool.class),
|
||||
// 可道云自定义域名解析
|
||||
KD("可道云",
|
||||
compile("http(s)?://([a-zA-Z\\d]+(-[a-zA-Z\\d]+)*\\.)+[a-zA-Z]{2,}(/#s)?/(?<KEY>.+)"),
|
||||
"https://{any}/#s/{shareKey}",
|
||||
"https://kodcloud.com/",
|
||||
KdTool.class),
|
||||
// 其他自定义域名解析
|
||||
OTHER("其他网盘",
|
||||
compile("http(s)?://([a-zA-Z\\d]+(-[a-zA-Z\\d]+)*\\.)+[a-zA-Z]{2,}/(?<KEY>.+)"),
|
||||
"https://{any}/{shareKey}",
|
||||
OtherTool.class);
|
||||
|
||||
public static final String KEY = "KEY";
|
||||
|
||||
public static final String PWD = "PWD";
|
||||
|
||||
// 网盘的显示名称,用于用户界面显示
|
||||
private final String displayName;
|
||||
|
||||
// 用于匹配和解析分享链接的正则表达式,保证最后一个捕捉组能匹配到分享key
|
||||
private final String regexPattern;
|
||||
private final Pattern pattern;
|
||||
|
||||
private final String regex;
|
||||
|
||||
// 网盘的标准链接模板,不含占位符,用于规范化分享链接
|
||||
private final String standardUrlTemplate;
|
||||
|
||||
// 网盘的域名, 如果在分享链接里能提取到, 则可不写
|
||||
private String panDomain;
|
||||
|
||||
// 指向解析工具IPanTool实现类
|
||||
private final Class<? extends IPanTool> toolClass;
|
||||
|
||||
PanDomainTemplate(String displayName, String regexPattern, String standardUrlTemplate,
|
||||
PanDomainTemplate(String displayName, Pattern pattern, String standardUrlTemplate,
|
||||
Class<? extends IPanTool> toolClass) {
|
||||
this.displayName = displayName;
|
||||
this.regexPattern = regexPattern;
|
||||
this.pattern = pattern;
|
||||
this.regex = pattern.pattern();
|
||||
this.standardUrlTemplate = standardUrlTemplate;
|
||||
this.toolClass = toolClass;
|
||||
}
|
||||
|
||||
PanDomainTemplate(String displayName, Pattern pattern, String standardUrlTemplate, String panDomain,
|
||||
Class<? extends IPanTool> toolClass) {
|
||||
this.displayName = displayName;
|
||||
this.pattern = pattern;
|
||||
this.regex = pattern.pattern();
|
||||
this.standardUrlTemplate = standardUrlTemplate;
|
||||
this.panDomain = panDomain;
|
||||
this.toolClass = toolClass;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getRegexPattern() {
|
||||
return regexPattern;
|
||||
public Pattern getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
public String getRegex() {
|
||||
return regex;
|
||||
}
|
||||
|
||||
public String getStandardUrlTemplate() {
|
||||
@@ -113,4 +396,18 @@ public enum PanDomainTemplate {
|
||||
public Class<? extends IPanTool> getToolClass() {
|
||||
return toolClass;
|
||||
}
|
||||
|
||||
public String getPanDomain() {
|
||||
if (panDomain == null) {
|
||||
String url = standardUrlTemplate
|
||||
.replace("{shareKey}", "");
|
||||
URL panDomainUrl = null;
|
||||
try {
|
||||
panDomainUrl = new URL(url);
|
||||
} catch (MalformedURLException ignored) {}
|
||||
|
||||
return panDomainUrl != null ? (panDomainUrl.getProtocol() + "://" + panDomainUrl.getHost()) : "无";
|
||||
}
|
||||
return panDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/6/13 4:26
|
||||
*/
|
||||
public enum PanType {
|
||||
LZ("lz"),
|
||||
COW("cow");
|
||||
|
||||
PanType(String type) {
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,12 @@ package cn.qaiu.parser;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static cn.qaiu.parser.PanDomainTemplate.KEY;
|
||||
import static cn.qaiu.parser.PanDomainTemplate.PWD;
|
||||
|
||||
|
||||
/**
|
||||
@@ -18,9 +22,12 @@ public class ParserCreate {
|
||||
private final PanDomainTemplate panDomainTemplate;
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
|
||||
private String standardUrl;
|
||||
|
||||
public ParserCreate(PanDomainTemplate panDomainTemplate, ShareLinkInfo shareLinkInfo) {
|
||||
this.panDomainTemplate = panDomainTemplate;
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
this.standardUrl = panDomainTemplate.getStandardUrlTemplate();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,15 +41,28 @@ public class ParserCreate {
|
||||
if (StringUtils.isEmpty(shareUrl)) {
|
||||
throw new IllegalArgumentException("ShareLinkInfo shareUrl is empty");
|
||||
}
|
||||
Pattern pattern = Pattern.compile(this.panDomainTemplate.getRegexPattern());
|
||||
Matcher matcher = pattern.matcher(shareUrl);
|
||||
Matcher matcher = this.panDomainTemplate.getPattern().matcher(shareUrl);
|
||||
if (matcher.find()) {
|
||||
String shareKey = matcher.group(matcher.groupCount());
|
||||
String k0 = matcher.group(KEY);
|
||||
String shareKey = URLEncoder.encode(k0, StandardCharsets.UTF_8);
|
||||
|
||||
// 返回规范化的标准链接
|
||||
String standardUrl = getStandardUrlTemplate().replace("{shareKey}", shareKey);
|
||||
standardUrl = getStandardUrlTemplate()
|
||||
.replace("{shareKey}", k0);
|
||||
|
||||
try {
|
||||
String pwd = matcher.group(PWD);
|
||||
if (StringUtils.isNotEmpty(pwd)) {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
}
|
||||
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
shareLinkInfo.setShareUrl(shareUrl);
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
if (!(panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal())) {
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
throw new IllegalArgumentException("Invalid share URL for " + this.panDomainTemplate.getDisplayName());
|
||||
@@ -66,8 +86,28 @@ public class ParserCreate {
|
||||
|
||||
// set share key
|
||||
public ParserCreate shareKey(String shareKey) {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
shareLinkInfo.setStandardUrl(panDomainTemplate.getStandardUrlTemplate().replace("{shareKey}", shareKey));
|
||||
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
|
||||
// 处理Cloudreve(ce)类: pan.huang1111.cn_s_wDz5TK _ -> /
|
||||
String[] s = shareKey.split("_");
|
||||
String standardUrl = "https://" + String.join("/", s);
|
||||
shareLinkInfo.setShareKey(s[s.length - 1]);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
shareLinkInfo.setShareUrl(standardUrl);
|
||||
} else {
|
||||
shareLinkInfo.setShareKey(shareKey);
|
||||
standardUrl = standardUrl.replace("{shareKey}", shareKey);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
}
|
||||
if (StringUtils.isEmpty(shareLinkInfo.getShareUrl())) {
|
||||
shareLinkInfo.setShareUrl(standardUrl);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// set any share url
|
||||
public ParserCreate fromAnyShareUrl(String url) {
|
||||
shareLinkInfo.setStandardUrl(url);
|
||||
shareLinkInfo.setShareUrl(url);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -80,17 +120,28 @@ public class ParserCreate {
|
||||
}
|
||||
|
||||
public ParserCreate setShareLinkInfoPwd(String pwd) {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
if (pwd != null) {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
if (shareLinkInfo.getShareUrl().contains("{pwd}")) {
|
||||
shareLinkInfo.setShareUrl(standardUrl);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// 根据分享链接获取PanDomainTemplate实例
|
||||
public synchronized static ParserCreate fromShareUrl(String shareUrl) {
|
||||
for (PanDomainTemplate panDomainTemplate : PanDomainTemplate.values()) {
|
||||
if (shareUrl.matches(panDomainTemplate.getRegexPattern())) {
|
||||
if (panDomainTemplate.getPattern().matcher(shareUrl).matches()) {
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type(panDomainTemplate.name().toLowerCase())
|
||||
.panName(panDomainTemplate.getDisplayName())
|
||||
.shareUrl(shareUrl).build();
|
||||
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
|
||||
shareLinkInfo.setStandardUrl(shareUrl);
|
||||
}
|
||||
ParserCreate parserCreate = new ParserCreate(panDomainTemplate, shareLinkInfo);
|
||||
return parserCreate.normalizeShareLink();
|
||||
}
|
||||
@@ -104,10 +155,28 @@ public class ParserCreate {
|
||||
PanDomainTemplate panDomainTemplate = Enum.valueOf(PanDomainTemplate.class, type.toUpperCase());
|
||||
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
|
||||
.type(type.toLowerCase()).build();
|
||||
shareLinkInfo.setPanName(panDomainTemplate.getDisplayName());
|
||||
return new ParserCreate(panDomainTemplate, shareLinkInfo);
|
||||
} catch (IllegalArgumentException ignore) {
|
||||
// 如果没有找到对应的枚举实例,抛出异常
|
||||
throw new IllegalArgumentException("No enum constant for type name: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
// 生成parser短链path(不包含domainName)
|
||||
public String genPathSuffix() {
|
||||
|
||||
String path;
|
||||
if (panDomainTemplate.ordinal() >= PanDomainTemplate.CE.ordinal()) {
|
||||
// 处理Cloudreve(ce)类: pan.huang1111.cn_s_wDz5TK _ -> /
|
||||
path = this.shareLinkInfo.getType() + "/"
|
||||
+ this.shareLinkInfo.getShareUrl()
|
||||
.substring("https://".length()).replace("/", "_");
|
||||
} else {
|
||||
path = this.shareLinkInfo.getType() + "/" + this.shareLinkInfo.getShareKey();
|
||||
}
|
||||
String sharePassword = this.shareLinkInfo.getSharePassword();
|
||||
return path + (StringUtils.isBlank(sharePassword) ? "" : ("@" + sharePassword));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.parser.PanDomainTemplate;
|
||||
import cn.qaiu.parser.ParserCreate;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* <a href="https://github.com/cloudreve/Cloudreve">Cloudreve网盘解析</a> <br>
|
||||
* <a href="https://github.com/cloudreve/Cloudreve">Cloudreve自建网盘解析</a> <br>
|
||||
* <a href="https://pan.xiaomuxi.cn">暮希云盘</a> <br>
|
||||
* <a href="https://pan.huang1111.cn">huang1111</a> <br>
|
||||
* <a href="https://pan.seeoss.com">看见存储</a> <br>
|
||||
* <a href="https://dav.yiandrive.com">亿安云盘</a> <br>
|
||||
*/
|
||||
public class CeTool extends PanBase implements IPanTool {
|
||||
public class CeTool extends PanBase {
|
||||
|
||||
private static final String DOWNLOAD_API_PATH = "https://pan.huang1111.cn/api/v3/share/download/";
|
||||
private static final String DOWNLOAD_API_PATH = "/api/v3/share/download/";
|
||||
|
||||
// api/v3/share/info/g31PcQ?password=qaiu
|
||||
private static final String SHARE_API_PATH = "https://pan.huang1111.cn/api/v3/share/info/";
|
||||
private static final String SHARE_API_PATH = "/api/v3/share/info/";
|
||||
|
||||
public CeTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
@@ -31,47 +38,47 @@ public class CeTool extends PanBase implements IPanTool {
|
||||
// https://pan.huang1111.cn/s/wDz5TK
|
||||
// https://pan.huang1111.cn/s/y12bI6 -> https://pan.huang1111
|
||||
// .cn/api/v3/share/download/y12bI6?path=undefined%2Fundefined;
|
||||
// 类型解析 -> /ce/https_pan.huang1111.cn_s_wDz5TK
|
||||
// 类型解析 -> /ce/pan.huang1111.cn_s_wDz5TK
|
||||
// parser接口 -> /parser?url=https://pan.huang1111.cn/s/wDz5TK
|
||||
try {
|
||||
// if (key.startsWith("https_") || key.startsWith("http_")) {
|
||||
// key = key.replace("https_", "https://")
|
||||
// .replace("http_", "http://")
|
||||
// .replace("_", "/");
|
||||
// }
|
||||
// // 处理URL
|
||||
// URL url = new URL(key);
|
||||
// String path = url.getPath();
|
||||
// String shareKey = path.substring(3);
|
||||
// String downloadApiUrl = url.getProtocol() + "://" + url.getHost() + DOWNLOAD_API_PATH + shareKey + "?path" +
|
||||
// "=undefined/undefined;";
|
||||
// String shareApiUrl = url.getProtocol() + "://" + url.getHost() + SHARE_API_PATH + shareKey;
|
||||
|
||||
var shareApiUrl = SHARE_API_PATH;
|
||||
var downloadApiUrl = DOWNLOAD_API_PATH;
|
||||
|
||||
URL url = new URL(shareLinkInfo.getShareUrl());
|
||||
String downloadApiUrl = url.getProtocol() + "://" + url.getHost() + DOWNLOAD_API_PATH + key + "?path" +
|
||||
"=undefined/undefined;";
|
||||
String shareApiUrl = url.getProtocol() + "://" + url.getHost() + SHARE_API_PATH + key;
|
||||
// 设置cookie
|
||||
HttpRequest<Buffer> httpRequest = clientSession.getAbs(shareApiUrl);
|
||||
if (pwd != null) {
|
||||
httpRequest.addQueryParam("password", pwd);
|
||||
}
|
||||
// 获取下载链接
|
||||
httpRequest.send().onSuccess(res -> getDownURL(downloadApiUrl)).onFailure(handleFail(shareApiUrl));
|
||||
httpRequest.send().onSuccess(res -> {
|
||||
try {
|
||||
if (res.statusCode() == 200 && res.bodyAsJsonObject().containsKey("code")) {
|
||||
getDownURL(downloadApiUrl);
|
||||
} else {
|
||||
nextParser();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
nextParser();
|
||||
}
|
||||
}).onFailure(handleFail(shareApiUrl));
|
||||
} catch (Exception e) {
|
||||
fail(e, "URL解析错误");
|
||||
}
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
private void getDownURL(String apiUrl) {
|
||||
clientSession.putAbs(apiUrl).send().onSuccess(res -> {
|
||||
JsonObject jsonObject = res.bodyAsJsonObject();
|
||||
|
||||
private void getDownURL(String shareApiUrl) {
|
||||
clientSession.putAbs(shareApiUrl).send().onSuccess(res -> {
|
||||
JsonObject jsonObject = asJson(res);
|
||||
System.out.println(jsonObject.encodePrettily());
|
||||
if (jsonObject.containsKey("code") && jsonObject.getInteger("code") == 0) {
|
||||
promise.complete(jsonObject.getString("data"));
|
||||
} else {
|
||||
fail("JSON解析失败: {}", jsonObject.encodePrettily());
|
||||
}
|
||||
}).onFailure(handleFail(apiUrl));
|
||||
}).onFailure(handleFail(shareApiUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
@@ -13,7 +12,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/4/21 21:19
|
||||
*/
|
||||
public class CowTool extends PanBase implements IPanTool {
|
||||
public class CowTool extends PanBase {
|
||||
|
||||
private static final String API_REQUEST_URL = "https://cowtransfer.com/core/api/transfer/share";
|
||||
|
||||
@@ -26,7 +25,7 @@ public class CowTool extends PanBase implements IPanTool {
|
||||
final String key = shareLinkInfo.getShareKey();
|
||||
String url = API_REQUEST_URL + "?uniqueUrl=" + key;
|
||||
client.getAbs(url).send().onSuccess(res -> {
|
||||
JsonObject resJson = res.bodyAsJsonObject();
|
||||
JsonObject resJson = asJson(res);
|
||||
if ("success".equals(resJson.getString("message")) && resJson.containsKey("data")) {
|
||||
JsonObject dataJson = resJson.getJsonObject("data");
|
||||
String guid = dataJson.getString("guid");
|
||||
@@ -41,7 +40,7 @@ public class CowTool extends PanBase implements IPanTool {
|
||||
}
|
||||
String url2 = url2Build.toString();
|
||||
client.getAbs(url2).send().onSuccess(res2 -> {
|
||||
JsonObject res2Json = res2.bodyAsJsonObject();
|
||||
JsonObject res2Json = asJson(res2);
|
||||
if ("success".equals(res2Json.getString("message")) && res2Json.containsKey("data")) {
|
||||
JsonObject data2 = res2Json.getJsonObject("data");
|
||||
String downloadUrl = data2.getString("downloadUrl");
|
||||
|
||||
105
parser/src/main/java/cn/qaiu/parser/impl/CtTool.java
Normal file
105
parser/src/main/java/cn/qaiu/parser/impl/CtTool.java
Normal file
@@ -0,0 +1,105 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.RandomStringGenerator;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* <a href="https://www.ctfile.com">诚通网盘</a>
|
||||
*/
|
||||
public class CtTool extends PanBase {
|
||||
private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
|
||||
|
||||
// https://webapi.ctfile.com/getfile.php?path=f&f=55050874-1246660795-6464f6&
|
||||
// passcode=7548&token=30wiijxs1fzhb6brw0p9m6&r=0.5885881231735761&
|
||||
// ref=&url=https%3A%2F%2F474b.com%2Ff%2F55050874-1246660795-6464f6%3Fp%3D7548
|
||||
private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" +
|
||||
"&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref=";
|
||||
|
||||
//https://webapi.ctfile.com/get_file_url.php?uid=55050874&fid=1246660795&folder_id=0&
|
||||
// file_chk=054bc20461f5c63ff82015b9e69fb7fc&mb=1&token=30wiijxs1fzhb6brw0p9m6&app=0&
|
||||
// acheck=1&verifycode=&rd=0.965929071503574
|
||||
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" +
|
||||
"uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" +
|
||||
"&rd={rand}";
|
||||
|
||||
|
||||
/**
|
||||
* 子类重写此构造方法不需要添加额外逻辑
|
||||
* 如:
|
||||
* <blockquote><pre>
|
||||
* public XxTool(String key, String pwd) {
|
||||
* super(key, pwd);
|
||||
* }
|
||||
* </pre></blockquote>
|
||||
*
|
||||
* @param shareLinkInfo 分享链接信息
|
||||
*/
|
||||
public CtTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
final String shareKey = shareLinkInfo.getShareKey();
|
||||
if (shareKey.indexOf('-') == -1) {
|
||||
fail("shareKey格式不正确找不到'-': {}", shareKey);
|
||||
return promise.future();
|
||||
}
|
||||
String[] split = shareKey.split("-");
|
||||
String uid = split[0], fid = split[1];
|
||||
String token = RandomStringGenerator.generateRandomString();
|
||||
// 获取url path
|
||||
int i1 = shareLinkInfo.getShareUrl().indexOf("com/");
|
||||
int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/");
|
||||
String path = shareLinkInfo.getShareUrl().substring(i1 + 4, i2);
|
||||
|
||||
HttpRequest<Buffer> bufferHttpRequest1 = clientSession.getAbs(UriTemplate.of(API1))
|
||||
.setTemplateParam("path", path)
|
||||
.setTemplateParam("shareKey", shareKey)
|
||||
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||
.setTemplateParam("token", token)
|
||||
.setTemplateParam("r", Math.random() + "");
|
||||
|
||||
bufferHttpRequest1
|
||||
.send().onSuccess(res -> {
|
||||
var resJson = asJson(res);
|
||||
if (resJson.containsKey("file")) {
|
||||
var fileJson = resJson.getJsonObject("file");
|
||||
if (fileJson.containsKey("file_chk")) {
|
||||
var file_chk = fileJson.getString("file_chk");
|
||||
HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2))
|
||||
.setTemplateParam("uid", uid)
|
||||
.setTemplateParam("fid", fid)
|
||||
.setTemplateParam("file_chk", file_chk)
|
||||
.setTemplateParam("token", token)
|
||||
.setTemplateParam("rd", Math.random() + "");
|
||||
bufferHttpRequest2
|
||||
.send().onSuccess(res2 -> {
|
||||
JsonObject resJson2 = asJson(res2);
|
||||
if (resJson2.containsKey("downurl")) {
|
||||
promise.complete(resJson2.getString("downurl"));
|
||||
} else {
|
||||
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl");
|
||||
}
|
||||
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
|
||||
} else {
|
||||
fail("解析失败, file_chk找不到, 可能分享已失效或者分享密码不对: {}", fileJson);
|
||||
}
|
||||
} else {
|
||||
fail("解析失败, 文件信息为空, 可能分享已失效");
|
||||
}
|
||||
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
@@ -11,7 +10,7 @@ import io.vertx.uritemplate.UriTemplate;
|
||||
/**
|
||||
* 移动云空间解析
|
||||
*/
|
||||
public class EcTool extends PanBase implements IPanTool {
|
||||
public class EcTool extends PanBase {
|
||||
// https://www.ecpan.cn/web/#/yunpanProxy?path=%2F%23%2Fdrive%2Foutside&data=4b3d786755688b85c6eb0c04b9124f4dalzdaJpXHx&isShare=1
|
||||
private static final String FIRST_REQUEST_URL = "https://www.ecpan.cn/drive/fileextoverrid" +
|
||||
".do?extractionCode={extractionCode}&chainUrlTemplate=https:%2F%2Fwww.ecpan" +
|
||||
@@ -34,7 +33,7 @@ public class EcTool extends PanBase implements IPanTool {
|
||||
.setTemplateParam("extractionCode", pwd == null ? "" : pwd)
|
||||
.send()
|
||||
.onSuccess(res -> {
|
||||
JsonObject jsonObject = res.bodyAsJsonObject();
|
||||
JsonObject jsonObject = asJson(res);
|
||||
log.debug("ecPan get file info -> {}", jsonObject);
|
||||
JsonObject fileInfo = jsonObject
|
||||
.getJsonObject("var")
|
||||
@@ -59,7 +58,7 @@ public class EcTool extends PanBase implements IPanTool {
|
||||
|
||||
// 第二次请求 获取下载链接
|
||||
client.postAbs(DOWNLOAD_REQUEST_URL).sendJsonObject(requestBodyJson).onSuccess(res2 -> {
|
||||
JsonObject jsonRes = res2.bodyAsJsonObject();
|
||||
JsonObject jsonRes = asJson(res2);
|
||||
log.debug("ecPan get download url -> {}", res2.body().toString());
|
||||
promise.complete(jsonRes.getJsonObject("var").getString("downloadUrl"));
|
||||
}).onFailure(handleFail(""));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
@@ -19,7 +18,7 @@ import java.util.regex.Pattern;
|
||||
/**
|
||||
* 360亿方云
|
||||
*/
|
||||
public class FcTool extends PanBase implements IPanTool {
|
||||
public class FcTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://v2.fangcloud.com/sharing/";
|
||||
public static final String SHARE_URL_PREFIX2 = "https://v2.fangcloud.cn/sharing/";
|
||||
@@ -88,7 +87,7 @@ public class FcTool extends PanBase implements IPanTool {
|
||||
.setTemplateParam("unique_name", dataKey).send().onSuccess(res2 -> {
|
||||
JsonObject resJson;
|
||||
try {
|
||||
resJson = res2.bodyAsJsonObject();
|
||||
resJson = asJson(res2);
|
||||
} catch (Exception e) {
|
||||
fail(e, DOWN_REQUEST_URL + " 第二次请求没有返回JSON, 可能下载受限");
|
||||
return;
|
||||
|
||||
@@ -1,30 +1,39 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import cn.qaiu.util.FileSizeConverter;
|
||||
import cn.qaiu.util.UUIDUtil;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 小飞机网盘
|
||||
*
|
||||
* @version V016_230609
|
||||
*/
|
||||
public class FjTool extends PanBase implements IPanTool {
|
||||
public class FjTool extends PanBase {
|
||||
public static final String REFERER_URL = "https://share.feijipan.com/";
|
||||
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" +
|
||||
"&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
|
||||
/// 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=JoUTkZYj&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}&shareId={dataKey}";
|
||||
@@ -35,45 +44,94 @@ public class FjTool extends PanBase implements IPanTool {
|
||||
"={uuid}&extra=2×tamp={ts}";
|
||||
// https://api.feijipan.com/ws/buy/vip/list?devType=6&devModel=Chrome&uuid=WQAl5yBy1naGudJEILBvE&extra=2×tamp=E2C53155F6D09417A27981561134CB73
|
||||
|
||||
// https://api.feijipan.com/ws/share/list?devType=6&devModel=Chrome&uuid=pwRWqwbk1J-KMTlRZowrn&extra=2×tamp=C5F8A68C53121AB21FA35BA3529E8758&shareId=fmAuOh3m&folderId=28986333&offset=1&limit=60
|
||||
|
||||
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
|
||||
"={uuid}&extra=2×tamp={ts}&shareId={shareId}&folderId" +
|
||||
"={folderId}&offset=1&limit=60";
|
||||
|
||||
private static final MultiMap header;
|
||||
|
||||
|
||||
long nowTs = System.currentTimeMillis();
|
||||
String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs));
|
||||
String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString()
|
||||
|
||||
static {
|
||||
header = MultiMap.caseInsensitiveMultiMap();
|
||||
header.set("Accept", "application/json, text/plain, */*");
|
||||
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
|
||||
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||
header.set("Cache-Control", "no-cache");
|
||||
header.set("Connection", "keep-alive");
|
||||
header.set("Content-Length", "0");
|
||||
header.set("DNT", "1");
|
||||
header.set("Host", "api.feijipan.com");
|
||||
header.set("Origin", "https://www.feijix.com");
|
||||
header.set("Pragma", "no-cache");
|
||||
header.set("Referer", "https://www.feijix.com/");
|
||||
header.set("Sec-Fetch-Dest", "empty");
|
||||
header.set("Sec-Fetch-Mode", "cors");
|
||||
header.set("Sec-Fetch-Site", "cross-site");
|
||||
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
|
||||
header.set("sec-ch-ua-mobile", "?0");
|
||||
header.set("sec-ch-ua-platform", "\"Windows\"");
|
||||
}
|
||||
|
||||
public FjTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
final String dataKey = shareLinkInfo.getShareKey();
|
||||
|
||||
// 240530 此处shareId又改为了原始的shareId
|
||||
String shareId = dataKey; // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||
long nowTs = System.currentTimeMillis();
|
||||
String tsEncode = AESUtils.encrypt2Hex(Long.toString(nowTs));
|
||||
String uuid = UUIDUtil.fjUuid(); // 也可以使用 UUID.randomUUID().toString()
|
||||
// String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||
final String shareId = shareLinkInfo.getShareKey();
|
||||
|
||||
// 24.5.12 飞机盘 规则修改 需要固定UUID先请求会员接口, 再请求后续接口
|
||||
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
||||
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
||||
|
||||
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.send().onSuccess(r0 -> { // 忽略res
|
||||
|
||||
// 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))
|
||||
client.postAbs(UriTemplate.of(url))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("shareId", shareId)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode0)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.send().onSuccess(res -> {
|
||||
JsonObject resJson = res.bodyAsJsonObject();
|
||||
JsonObject resJson = asJson(res);
|
||||
if (resJson.getInteger("code") != 200) {
|
||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||
return;
|
||||
}
|
||||
if (resJson.getJsonArray("list").size() == 0) {
|
||||
if (resJson.getJsonArray("list").isEmpty()) {
|
||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||
return;
|
||||
}
|
||||
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||
return;
|
||||
}
|
||||
// 文件Id
|
||||
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
||||
// 如果是目录返回目录ID
|
||||
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
|
||||
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
|
||||
return;
|
||||
}
|
||||
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
|
||||
if (fileList.getInteger("fileType") == 2) {
|
||||
promise.complete(fileList.getInteger("folderId").toString());
|
||||
return;
|
||||
}
|
||||
|
||||
String fileId = fileInfo.getString("fileIds");
|
||||
String userId = fileInfo.getString("userId");
|
||||
// 其他参数
|
||||
@@ -82,18 +140,16 @@ public class FjTool extends PanBase implements IPanTool {
|
||||
String fidEncode = AESUtils.encrypt2Hex(fileId + "|" + userId);
|
||||
String auth = AESUtils.encrypt2Hex(fileId + "|" + nowTs2);
|
||||
|
||||
MultiMap headers0 = MultiMap.caseInsensitiveMultiMap();
|
||||
headers0.set("referer", REFERER_URL);
|
||||
// 第二次请求
|
||||
HttpRequest<Buffer> httpRequest =
|
||||
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.putHeaders(headers0)
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode2)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("dataKey", dataKey);
|
||||
System.out.println(httpRequest.toString());
|
||||
.setTemplateParam("dataKey", shareId);
|
||||
// System.out.println(httpRequest.toString());
|
||||
httpRequest.send().onSuccess(res2 -> {
|
||||
MultiMap headers = res2.headers();
|
||||
if (!headers.contains("Location")) {
|
||||
@@ -108,4 +164,145 @@ public class FjTool extends PanBase implements IPanTool {
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
|
||||
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||
|
||||
// 如果参数里的目录ID不为空,则直接解析目录
|
||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||
if (dirId != null && !dirId.isEmpty()) {
|
||||
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
|
||||
parserDir(dirId, shareId, promise);
|
||||
return promise.future();
|
||||
}
|
||||
parse().onSuccess(id -> {
|
||||
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
|
||||
parserDir(id, shareId, promise);
|
||||
} else {
|
||||
promise.fail("解析目录ID失败");
|
||||
}
|
||||
}).onFailure(failRes -> {
|
||||
log.error("解析目录失败: {}", failRes.getMessage());
|
||||
promise.fail(failRes);
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
||||
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
||||
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
|
||||
// 拿到目录ID
|
||||
client.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("shareId", shareId)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.setTemplateParam("folderId", id)
|
||||
.send().onSuccess(res -> {
|
||||
JsonObject jsonObject;
|
||||
try {
|
||||
jsonObject = asJson(res);
|
||||
} catch (Exception e) {
|
||||
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
|
||||
return;
|
||||
}
|
||||
// System.out.println(jsonObject.encodePrettily());
|
||||
JsonArray list = jsonObject.getJsonArray("list");
|
||||
ArrayList<FileInfo> result = new ArrayList<>();
|
||||
list.forEach(item->{
|
||||
JsonObject fileJson = (JsonObject) item;
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
|
||||
// 映射已知字段fileInfo
|
||||
String fileId = fileJson.getString("fileId");
|
||||
String userId = fileJson.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);
|
||||
|
||||
// 回传用到的参数
|
||||
//"fidEncode", paramJson.getString("fidEncode"))
|
||||
//"uuid", paramJson.getString("uuid"))
|
||||
//"ts", paramJson.getString("ts"))
|
||||
//"auth", paramJson.getString("auth"))
|
||||
//"shareId", paramJson.getString("shareId"))
|
||||
JsonObject entries = JsonObject.of(
|
||||
"fidEncode", fidEncode,
|
||||
"uuid", uuid,
|
||||
"ts", tsEncode2,
|
||||
"auth", auth,
|
||||
"shareId", shareId);
|
||||
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
|
||||
String param = new String(encode);
|
||||
|
||||
if (fileJson.getInteger("fileType") == 2) {
|
||||
// 如果是目录
|
||||
fileInfo.setFileName(fileJson.getString("name"))
|
||||
.setFileId(fileJson.getString("folderId"))
|
||||
.setCreateTime(fileJson.getString("updTime"))
|
||||
.setFileType("folder")
|
||||
.setSize(0L)
|
||||
.setSizeStr("0B")
|
||||
.setCreateBy(fileJson.getLong("userId").toString())
|
||||
.setDownloadCount(fileJson.getInteger("fileDownloads"))
|
||||
.setCreateTime(fileJson.getString("updTime"))
|
||||
.setFileIcon(fileJson.getString("fileIcon"))
|
||||
.setPanType(shareLinkInfo.getType())
|
||||
// 设置目录解析的URL
|
||||
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
|
||||
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
|
||||
result.add(fileInfo);
|
||||
return;
|
||||
}
|
||||
long fileSize = fileJson.getLong("fileSize") * 1024;
|
||||
fileInfo.setFileName(fileJson.getString("fileName"))
|
||||
.setFileId(fileJson.getString("fileId"))
|
||||
.setCreateTime(fileJson.getString("createTime"))
|
||||
.setFileType("file")
|
||||
.setSize(fileSize)
|
||||
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
|
||||
.setCreateBy(fileJson.getLong("userId").toString())
|
||||
.setDownloadCount(fileJson.getInteger("fileDownloads"))
|
||||
.setCreateTime(fileJson.getString("updTime"))
|
||||
.setFileIcon(fileJson.getString("fileIcon"))
|
||||
.setPanType(shareLinkInfo.getType())
|
||||
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), param))
|
||||
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), param));
|
||||
result.add(fileInfo);
|
||||
});
|
||||
promise.complete(result);
|
||||
}).onFailure(failRes -> {
|
||||
log.error("解析目录请求失败: {}", failRes.getMessage());
|
||||
promise.fail(failRes);
|
||||
});;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parseById() {
|
||||
// 第二次请求
|
||||
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
|
||||
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||
.setTemplateParam("ts", paramJson.getString("ts"))
|
||||
.setTemplateParam("auth", paramJson.getString("auth"))
|
||||
.setTemplateParam("dataKey", paramJson.getString("shareId"))
|
||||
.putHeaders(header).send().onSuccess(res2 -> {
|
||||
MultiMap headers = res2.headers();
|
||||
if (!headers.contains("Location")) {
|
||||
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
|
||||
return;
|
||||
}
|
||||
promise.complete(headers.get("Location"));
|
||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
132
parser/src/main/java/cn/qaiu/parser/impl/GenShortUrl.java
Normal file
132
parser/src/main/java/cn/qaiu/parser/impl/GenShortUrl.java
Normal file
@@ -0,0 +1,132 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.netty.handler.codec.http.cookie.Cookie;
|
||||
import io.netty.handler.codec.http.cookie.DefaultCookie;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.impl.headers.HeadersMultiMap;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* t.cn 短链生成 //
|
||||
*/
|
||||
public class GenShortUrl extends PanBase {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GenShortUrl.class);
|
||||
|
||||
private static final String COMMENT_URL = "https://www.weibo.com/aj/v6/comment/add";
|
||||
private static final String DELETE_COMMENT_URL = "https://www.weibo.com/aj/comment/del";
|
||||
|
||||
private static final String WRAPPER_URL = "https://www.so.com/link?m=ewgUSYiFWXIoTybC3fJH8YoJy8y10iRquo6cazgINwWjTn3HvVJ92TrCJu0PmMUR0RMDfOAucP3wa4G8j64SrhNH9Z0Cr0PEyn9ASuvpkUGmAjjUEGJkO5%2BIDGWVrEkPHsL7UsoKO6%2BlT%2BD6r&ccc=";
|
||||
private static final String MID = "5095144728824883"; // 微博的mid
|
||||
|
||||
private static final MultiMap HEADER = HeadersMultiMap.headers()
|
||||
.add("Content-Type", "application/x-www-form-urlencoded")
|
||||
.add("Referer", "https://www.weibo.com")
|
||||
.add("Content-Type", "application/x-www-form-urlencoded")
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36");
|
||||
|
||||
Cookie cookie = new DefaultCookie("SUB", "_2A25KJE5vDeRhGeRJ6lsR9SjJzDuIHXVpWM-nrDV8PUJbkNAbLVPlkW1NUmJm3GjYtRHBsHdMUKafkdTL_YheMEmu");
|
||||
public GenShortUrl(ShareLinkInfo build) {
|
||||
super(build);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
String longUrl = shareLinkInfo.getShareUrl();
|
||||
|
||||
String url = WRAPPER_URL + Base64.getEncoder().encodeToString(longUrl.getBytes());
|
||||
|
||||
String payload = "mid=" + MID + "&content=" + URLEncoder.encode(url, StandardCharsets.UTF_8);
|
||||
clientSession.cookieStore().put(cookie);
|
||||
|
||||
clientSession.postAbs(COMMENT_URL)
|
||||
.putHeaders(HEADER)
|
||||
.sendBuffer(Buffer.buffer(payload))
|
||||
.onSuccess(res -> {
|
||||
JsonObject data = asJson(res).getJsonObject("data");
|
||||
if (data.isEmpty()) {
|
||||
fail(asJson(res).getString("msg"));
|
||||
return;
|
||||
}
|
||||
String comment = data.getString("comment");
|
||||
String shortUrl = extractShortUrl(comment);
|
||||
if (shortUrl != null) {
|
||||
log.info("生成的短链:{}", shortUrl);
|
||||
String commentId = extractCommentId(comment);
|
||||
if (commentId != null) {
|
||||
deleteComment(commentId);
|
||||
} else {
|
||||
promise.fail("未能提取评论ID");
|
||||
}
|
||||
} else {
|
||||
promise.fail("未能生成短链");
|
||||
}
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("添加评论失败", err);
|
||||
promise.fail(err);
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
|
||||
private void deleteComment(String commentId) {
|
||||
String payload = "mid=" + MID + "&cid=" + commentId;
|
||||
|
||||
clientSession.postAbs(DELETE_COMMENT_URL)
|
||||
.putHeaders(HEADER)
|
||||
.sendBuffer(Buffer.buffer(payload))
|
||||
.onSuccess(res -> {
|
||||
JsonObject responseJson = res.bodyAsJsonObject();
|
||||
if (responseJson.getString("code").equals("100000")) {
|
||||
log.info("评论已删除: {}", commentId);
|
||||
} else {
|
||||
log.error("删除评论失败: {}", responseJson.encode());
|
||||
}
|
||||
})
|
||||
.onFailure(err -> {
|
||||
log.error("删除评论失败", err);
|
||||
});
|
||||
}
|
||||
|
||||
private String extractShortUrl(String comment) {
|
||||
Pattern pattern = Pattern.compile("(https?)://t.cn/\\w+");
|
||||
Matcher matcher = pattern.matcher(comment);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String extractCommentId(String comment) {
|
||||
Pattern pattern = Pattern.compile("comment_id=\"(\\d+)\"");
|
||||
Matcher matcher = pattern.matcher(comment);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// http://t.cn/A6nfZn86
|
||||
// http://t.cn/A6nfZn86
|
||||
|
||||
new GenShortUrl(ShareLinkInfo.newBuilder().shareUrl("https://qaiu.top/sdfsdf").build()).parse().onSuccess(
|
||||
System.out::println
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +1,283 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import cn.qaiu.util.FileSizeConverter;
|
||||
import cn.qaiu.util.UUIDUtil;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 蓝奏云优享
|
||||
*
|
||||
*/
|
||||
public class IzTool extends PanBase implements IPanTool {
|
||||
public class IzTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://www.ilanzou.com/s/";
|
||||
private static final String API_URL_PREFIX = "https://api.ilanzou.com/unproved/";
|
||||
|
||||
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";
|
||||
|
||||
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}";
|
||||
// downloadId=x&enable=1&devType=6&uuid=x×tamp=x&auth=x&shareId=lGFndCM
|
||||
|
||||
private static final String VIP_REQUEST_URL = API_URL_PREFIX + "/buy/vip/list?devType=6&devModel=Chrome&uuid" +
|
||||
"={uuid}&extra=2×tamp={ts}";
|
||||
|
||||
private static final String FILE_LIST_URL = API_URL_PREFIX + "/share/list?devType=6&devModel=Chrome&uuid" +
|
||||
"={uuid}&extra=2×tamp={ts}&shareId={shareId}&folderId" +
|
||||
"={folderId}&offset=1&limit=60";
|
||||
|
||||
long nowTs = System.currentTimeMillis();
|
||||
String tsEncode = AESUtils.encrypt2HexIz(Long.toString(nowTs));
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
|
||||
private static final MultiMap header;
|
||||
|
||||
static {
|
||||
header = MultiMap.caseInsensitiveMultiMap();
|
||||
header.set("Accept", "application/json, text/plain, */*");
|
||||
header.set("Accept-Encoding", "gzip, deflate, br, zstd");
|
||||
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||
header.set("Cache-Control", "no-cache");
|
||||
header.set("Connection", "keep-alive");
|
||||
header.set("Content-Length", "0");
|
||||
header.set("DNT", "1");
|
||||
header.set("Host", "api.ilanzou.com");
|
||||
header.set("Origin", "https://www.ilanzou.com/");
|
||||
header.set("Pragma", "no-cache");
|
||||
header.set("Referer", "https://www.ilanzou.com/");
|
||||
header.set("Sec-Fetch-Dest", "empty");
|
||||
header.set("Sec-Fetch-Mode", "cors");
|
||||
header.set("Sec-Fetch-Site", "cross-site");
|
||||
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
||||
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
|
||||
header.set("sec-ch-ua-mobile", "?0");
|
||||
header.set("sec-ch-ua-platform", "\"Windows\"");
|
||||
}
|
||||
|
||||
public IzTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String dataKey = shareLinkInfo.getShareKey();
|
||||
String shareId = shareLinkInfo.getShareKey();
|
||||
|
||||
// 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", dataKey).send().onSuccess(res -> {
|
||||
JsonObject resJson = res.bodyAsJsonObject();
|
||||
if (resJson.getInteger("code") != 200) {
|
||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||
return;
|
||||
// POST https://api.ilanzou.com/ws/recommend/list?devType=6&devModel=Chrome&extra=2&shareId=146731&type=0&offset=1&limit=60
|
||||
String url = StringUtils.isBlank(shareLinkInfo.getSharePassword()) ? FIRST_REQUEST_URL
|
||||
: (FIRST_REQUEST_URL + "&code=" + shareLinkInfo.getSharePassword());
|
||||
client.postAbs(UriTemplate.of(VIP_REQUEST_URL))
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.send().onSuccess(r0 -> { // 忽略res
|
||||
// 第一次请求 获取文件信息
|
||||
// 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(url))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("shareId", shareId)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.send().onSuccess(res -> {
|
||||
JsonObject resJson = asJson(res);
|
||||
if (resJson.getInteger("code") != 200) {
|
||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||
return;
|
||||
}
|
||||
if (resJson.getJsonArray("list").isEmpty()) {
|
||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||
return;
|
||||
}
|
||||
if (!resJson.containsKey("list") || resJson.getJsonArray("list").isEmpty()) {
|
||||
fail(FIRST_REQUEST_URL + " 解析文件列表为空: " + resJson);
|
||||
return;
|
||||
}
|
||||
// 文件Id
|
||||
JsonObject fileInfo = resJson.getJsonArray("list").getJsonObject(0);
|
||||
// 如果是目录返回目录ID
|
||||
if (!fileInfo.containsKey("fileList") || fileInfo.getJsonArray("fileList").isEmpty()) {
|
||||
fail(FIRST_REQUEST_URL + " 文件列表为空: " + fileInfo);
|
||||
return;
|
||||
}
|
||||
JsonObject fileList = fileInfo.getJsonArray("fileList").getJsonObject(0);
|
||||
if (fileList.getInteger("fileType") == 2) {
|
||||
promise.complete(fileList.getInteger("folderId").toString());
|
||||
return;
|
||||
}
|
||||
|
||||
String fileId = fileInfo.getString("fileIds");
|
||||
String userId = fileInfo.getString("userId");
|
||||
// 其他参数
|
||||
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
|
||||
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
||||
// 第二次请求
|
||||
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.setTemplateParam("fidEncode", fidEncode)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.setTemplateParam("auth", auth)
|
||||
.setTemplateParam("shareId", shareId)
|
||||
.putHeaders(header).send().onSuccess(res2 -> {
|
||||
MultiMap headers = res2.headers();
|
||||
if (!headers.contains("Location")) {
|
||||
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + headers);
|
||||
return;
|
||||
}
|
||||
promise.complete(headers.get("Location"));
|
||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
|
||||
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||
|
||||
// 如果参数里的目录ID不为空,则直接解析目录
|
||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||
if (dirId != null && !dirId.isEmpty()) {
|
||||
uuid = shareLinkInfo.getOtherParam().get("uuid").toString();
|
||||
parserDir(dirId, shareId, promise);
|
||||
return promise.future();
|
||||
}
|
||||
parse().onSuccess(id -> {
|
||||
if (id != null && id.matches("^[a-zA-Z0-9]+$")) {
|
||||
parserDir(id, shareId, promise);
|
||||
} else {
|
||||
promise.fail("解析目录ID失败");
|
||||
}
|
||||
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.encrypt2HexIz(Long.toString(nowTs));
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
// String fidEncode = AESUtils.encrypt2HexIz(fileId + "|");
|
||||
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
||||
// 第二次请求
|
||||
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.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());
|
||||
}).onFailure(failRes -> {
|
||||
log.error("解析目录失败: {}", failRes.getMessage());
|
||||
promise.fail(failRes);
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
private void parserDir(String id, String shareId, Promise<List<FileInfo>> promise) {
|
||||
log.debug("开始解析目录: {}, shareId: {}, uuid: {}, ts: {}", id, shareId, uuid, tsEncode);
|
||||
// 开始解析目录: 164312216, shareId: bPMsbg5K, uuid: 0fmVWTx2Ea4zFwkpd7KXf, ts: 20865d7b7f00828279f437cd1f097860
|
||||
// 拿到目录ID
|
||||
client.postAbs(UriTemplate.of(FILE_LIST_URL))
|
||||
.putHeaders(header)
|
||||
.setTemplateParam("shareId", shareId)
|
||||
.setTemplateParam("uuid", uuid)
|
||||
.setTemplateParam("ts", tsEncode)
|
||||
.setTemplateParam("folderId", id)
|
||||
.send().onSuccess(res -> {
|
||||
JsonObject jsonObject;
|
||||
try {
|
||||
jsonObject = asJson(res);
|
||||
} catch (Exception e) {
|
||||
promise.fail(FIRST_REQUEST_URL + " 解析JSON失败: " + res.bodyAsString());
|
||||
return;
|
||||
}
|
||||
// System.out.println(jsonObject.encodePrettily());
|
||||
JsonArray list = jsonObject.getJsonArray("list");
|
||||
ArrayList<FileInfo> result = new ArrayList<>();
|
||||
list.forEach(item->{
|
||||
JsonObject fileJson = (JsonObject) item;
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
|
||||
// 映射已知字段
|
||||
String fileId = fileJson.getString("fileId");
|
||||
String userId = fileJson.getString("userId");
|
||||
|
||||
// 回传用到的参数
|
||||
//"fidEncode", paramJson.getString("fidEncode"))
|
||||
//"uuid", paramJson.getString("uuid"))
|
||||
//"ts", paramJson.getString("ts"))
|
||||
//"auth", paramJson.getString("auth"))
|
||||
//"shareId", paramJson.getString("shareId"))
|
||||
String fidEncode = AESUtils.encrypt2HexIz(fileId + "|" + userId);
|
||||
String auth = AESUtils.encrypt2HexIz(fileId + "|" + nowTs);
|
||||
JsonObject entries = JsonObject.of(
|
||||
"fidEncode", fidEncode,
|
||||
"uuid", uuid,
|
||||
"ts", tsEncode,
|
||||
"auth", auth,
|
||||
"shareId", shareId);
|
||||
byte[] encode = Base64.getEncoder().encode(entries.encode().getBytes());
|
||||
String param = new String(encode);
|
||||
|
||||
if (fileJson.getInteger("fileType") == 2) {
|
||||
// 如果是目录
|
||||
fileInfo.setFileName(fileJson.getString("name"))
|
||||
.setFileId(fileJson.getString("folderId"))
|
||||
.setCreateTime(fileJson.getString("updTime"))
|
||||
.setFileType("folder")
|
||||
.setSize(0L)
|
||||
.setSizeStr("0B")
|
||||
.setCreateBy(fileJson.getLong("userId").toString())
|
||||
.setDownloadCount(fileJson.getInteger("fileDownloads"))
|
||||
.setCreateTime(fileJson.getString("updTime"))
|
||||
.setFileIcon(fileJson.getString("fileIcon"))
|
||||
.setPanType(shareLinkInfo.getType())
|
||||
// 设置目录解析的URL
|
||||
.setParserUrl(String.format("%s/v2/getFileList?url=%s&dirId=%s&uuid=%s", getDomainName(),
|
||||
shareLinkInfo.getShareUrl(), fileJson.getString("folderId"), uuid));
|
||||
result.add(fileInfo);
|
||||
return;
|
||||
}
|
||||
promise.complete(headers.get("Location"));
|
||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
||||
long fileSize = fileJson.getLong("fileSize") * 1024;
|
||||
fileInfo.setFileName(fileJson.getString("fileName"))
|
||||
.setFileId(fileId)
|
||||
.setCreateTime(fileJson.getString("createTime"))
|
||||
.setFileType("file")
|
||||
.setSize(fileSize)
|
||||
.setSizeStr(FileSizeConverter.convertToReadableSize(fileSize))
|
||||
.setCreateBy(fileJson.getLong("userId").toString())
|
||||
.setDownloadCount(fileJson.getInteger("fileDownloads"))
|
||||
.setCreateTime(fileJson.getString("updTime"))
|
||||
.setFileIcon(fileJson.getString("fileIcon"))
|
||||
.setPanType(shareLinkInfo.getType())
|
||||
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), param))
|
||||
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), param));
|
||||
result.add(fileInfo);
|
||||
});
|
||||
promise.complete(result);
|
||||
}).onFailure(failRes -> {
|
||||
log.error("解析目录请求失败: {}", failRes.getMessage());
|
||||
promise.fail(failRes);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parseById() {
|
||||
// 第二次请求
|
||||
JsonObject paramJson = (JsonObject)shareLinkInfo.getOtherParam().get("paramJson");
|
||||
clientNoRedirects.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.setTemplateParam("fidEncode", paramJson.getString("fidEncode"))
|
||||
.setTemplateParam("uuid", paramJson.getString("uuid"))
|
||||
.setTemplateParam("ts", paramJson.getString("ts"))
|
||||
.setTemplateParam("auth", paramJson.getString("auth"))
|
||||
.setTemplateParam("shareId", paramJson.getString("shareId"))
|
||||
.putHeaders(header).send().onSuccess(res2 -> {
|
||||
MultiMap headers = res2.headers();
|
||||
if (!headers.contains("Location")) {
|
||||
fail(SECOND_REQUEST_URL + " 未找到重定向URL: \n" + res2.headers());
|
||||
return;
|
||||
}
|
||||
promise.complete(headers.get("Location"));
|
||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
26
parser/src/main/java/cn/qaiu/parser/impl/KdTool.java
Normal file
26
parser/src/main/java/cn/qaiu/parser/impl/KdTool.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* <a href="https://kodcloud.com/">可道云</a>
|
||||
*/
|
||||
public class KdTool extends PanBase {
|
||||
private static final String API_URL_PREFIX = "";
|
||||
|
||||
public KdTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
nextParser();
|
||||
// TODO
|
||||
return future();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
@@ -12,7 +11,7 @@ import java.util.UUID;
|
||||
/**
|
||||
* <a href="https://lecloud.lenovo.com/">联想乐云</a>
|
||||
*/
|
||||
public class LeTool extends PanBase implements IPanTool {
|
||||
public class LeTool extends PanBase {
|
||||
private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/share/api/clouddiskapi/share/public/v1/";
|
||||
|
||||
public LeTool(ShareLinkInfo shareLinkInfo) {
|
||||
@@ -27,7 +26,7 @@ public class LeTool extends PanBase implements IPanTool {
|
||||
client.postAbs(apiUrl1)
|
||||
.sendJsonObject(JsonObject.of("shareId", dataKey, "password", pwd, "directoryId", -1))
|
||||
.onSuccess(res -> {
|
||||
JsonObject resJson = res.bodyAsJsonObject();
|
||||
JsonObject resJson = asJson(res);
|
||||
if (resJson.containsKey("result")) {
|
||||
if (resJson.getBoolean("result")) {
|
||||
JsonObject dataJson = resJson.getJsonObject("data");
|
||||
@@ -68,7 +67,7 @@ public class LeTool extends PanBase implements IPanTool {
|
||||
client.postAbs(apiUrl2)
|
||||
.sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", key, "browserId", uuid))
|
||||
.onSuccess(res -> {
|
||||
JsonObject resJson = res.bodyAsJsonObject();
|
||||
JsonObject resJson = asJson(res);
|
||||
if (resJson.containsKey("result")) {
|
||||
if (resJson.getBoolean("result")) {
|
||||
JsonObject dataJson = resJson.getJsonObject("data");
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.CastUtil;
|
||||
import cn.qaiu.util.FileSizeConverter;
|
||||
import cn.qaiu.util.HeaderUtils;
|
||||
import cn.qaiu.util.JsExecUtils;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||
|
||||
import javax.script.ScriptException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
@@ -20,16 +26,15 @@ import java.util.regex.Pattern;
|
||||
*
|
||||
* @author QAIU
|
||||
*/
|
||||
public class LzTool extends PanBase implements IPanTool {
|
||||
public class LzTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://wwwa.lanzoux.com";
|
||||
public static final String SHARE_URL_PREFIX = "https://wwww.lanzoup.com";
|
||||
|
||||
|
||||
public LzTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Future<String> parse() {
|
||||
String sUrl = shareLinkInfo.getStandardUrl();
|
||||
String pwd = shareLinkInfo.getSharePassword();
|
||||
@@ -42,23 +47,11 @@ public class LzTool extends PanBase implements IPanTool {
|
||||
Matcher matcher = compile.matcher(html);
|
||||
// 没有Iframe说明是加密分享, 匹配sign通过密码请求下载页面
|
||||
if (!matcher.find()) {
|
||||
// 处理一下JS
|
||||
String jsText = getJsText(html);
|
||||
|
||||
if (jsText == null) {
|
||||
fail(SHARE_URL_PREFIX + " -> " + sUrl + ": js脚本匹配失败, 可能分享已失效");
|
||||
return;
|
||||
}
|
||||
|
||||
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
|
||||
int i = jsText.indexOf("document.getElementById('rpt')");
|
||||
if (i > 0) {
|
||||
jsText = jsText.substring(0, i);
|
||||
}
|
||||
try {
|
||||
String jsText = getJsByPwd(pwd, html, "document.getElementById('rpt')");
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "down_p");
|
||||
getDownURL(sUrl, client, (Map<String, String>) scriptObjectMirror.get("data"));
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
getDownURL(sUrl, client, scriptObjectMirror);
|
||||
} catch (Exception e) {
|
||||
fail(e, "js引擎执行失败");
|
||||
}
|
||||
} else {
|
||||
@@ -76,7 +69,7 @@ public class LzTool extends PanBase implements IPanTool {
|
||||
}
|
||||
try {
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, null);
|
||||
getDownURL(sUrl, client, (Map<String, String>) scriptObjectMirror.get("data"));
|
||||
getDownURL(sUrl, client, scriptObjectMirror);
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
fail(e, "js引擎执行失败");
|
||||
}
|
||||
@@ -86,6 +79,20 @@ public class LzTool extends PanBase implements IPanTool {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
private String getJsByPwd(String pwd, String html, String subText) {
|
||||
String jsText = getJsText(html);
|
||||
|
||||
if (jsText == null) {
|
||||
throw new RuntimeException("js脚本匹配失败, 可能分享已失效");
|
||||
}
|
||||
jsText = jsText.replace("document.getElementById('pwd').value", "\"" + pwd + "\"");
|
||||
int i = jsText.indexOf(subText);
|
||||
if (i > 0) {
|
||||
jsText = jsText.substring(0, i);
|
||||
}
|
||||
return jsText;
|
||||
}
|
||||
|
||||
private String getJsText(String html) {
|
||||
String jsTagStart = "<script type=\"text/javascript\">";
|
||||
String jsTagEnd = "</script>";
|
||||
@@ -95,14 +102,60 @@ public class LzTool extends PanBase implements IPanTool {
|
||||
}
|
||||
int startPos = index + jsTagStart.length();
|
||||
int endPos = html.indexOf(jsTagEnd, startPos);
|
||||
return html.substring(startPos, endPos);
|
||||
return html.substring(startPos, endPos).replaceAll("<!--.*-->", "");
|
||||
}
|
||||
|
||||
private void getDownURL(String key, WebClient client, Map<String, ?> signMap) {
|
||||
private void getDownURL(String key, WebClient client, Map<String, ?> obj) {
|
||||
if (obj == null) {
|
||||
fail("需要访问密码");
|
||||
return;
|
||||
}
|
||||
Map<?, ?> signMap = (Map<?, ?>)obj.get("data");
|
||||
String url0 = obj.get("url").toString();
|
||||
MultiMap map = MultiMap.caseInsensitiveMultiMap();
|
||||
signMap.forEach((k, v) -> {
|
||||
map.set(k, v.toString());
|
||||
map.add((String) k, v.toString());
|
||||
});
|
||||
MultiMap headers = HeaderUtils.parseHeaders("""
|
||||
Accept: application/json, text/javascript, */*
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
|
||||
Cache-Control: no-cache
|
||||
Connection: keep-alive
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Pragma: no-cache
|
||||
Sec-Fetch-Dest: empty
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Site: same-origin
|
||||
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36 Edg/134.0.0.0
|
||||
X-Requested-With: XMLHttpRequest
|
||||
sec-ch-ua: "Chromium";v="134", "Not:A-Brand";v="24", "Microsoft Edge";v="134"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "Windows"
|
||||
""");
|
||||
|
||||
headers.set("referer", key);
|
||||
// action=downprocess&signs=%3Fctdf&websignkey=I5gl&sign=BWMGOF1sBTRWXwI9BjZdYVA7BDhfNAIyUG9UawJtUGMIPlAhACkCa1UyUTAAYFxvUj5XY1E7UGFXaFVq&websign=&kd=1&ves=1
|
||||
String url = SHARE_URL_PREFIX + url0;
|
||||
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
||||
try {
|
||||
JsonObject urlJson = asJson(res2);
|
||||
if (urlJson.getInteger("zt") != 1) {
|
||||
fail(urlJson.getString("inf"));
|
||||
return;
|
||||
}
|
||||
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
|
||||
headers.remove("Referer");
|
||||
client.getAbs(downUrl).putHeaders(headers).send()
|
||||
.onSuccess(res3 -> promise.complete(res3.headers().get("Location")))
|
||||
.onFailure(handleFail(downUrl));
|
||||
} catch (Exception e) {
|
||||
fail("解析异常");
|
||||
}
|
||||
}).onFailure(handleFail(url));
|
||||
}
|
||||
|
||||
private static MultiMap getHeaders(String key) {
|
||||
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||
var userAgent2 = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, " +
|
||||
"like " +
|
||||
@@ -112,18 +165,75 @@ public class LzTool extends PanBase implements IPanTool {
|
||||
headers.set("sec-ch-ua-platform", "Android");
|
||||
headers.set("Accept-Language", "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2");
|
||||
headers.set("sec-ch-ua-mobile", "sec-ch-ua-mobile");
|
||||
return headers;
|
||||
}
|
||||
|
||||
String url = SHARE_URL_PREFIX + "/ajaxm.php";
|
||||
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
||||
JsonObject urlJson = res2.bodyAsJsonObject();
|
||||
if (urlJson.getInteger("zt") != 1) {
|
||||
fail(urlJson.getString("inf"));
|
||||
return;
|
||||
|
||||
@Override
|
||||
public Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
|
||||
String sUrl = shareLinkInfo.getShareUrl();
|
||||
String pwd = shareLinkInfo.getSharePassword();
|
||||
|
||||
WebClient client = clientNoRedirects;
|
||||
client.getAbs(sUrl).send().onSuccess(res -> {
|
||||
String html = res.bodyAsString();
|
||||
try {
|
||||
String jsText = getJsByPwd(pwd, html, "var urls =window.location.href");
|
||||
ScriptObjectMirror scriptObjectMirror = JsExecUtils.executeDynamicJs(jsText, "file");
|
||||
Map<String, Object> data = CastUtil.cast(scriptObjectMirror.get("data"));
|
||||
MultiMap map = MultiMap.caseInsensitiveMultiMap();
|
||||
data.forEach((k, v) -> map.set(k, v.toString()));
|
||||
log.debug("解析参数: {}", map);
|
||||
MultiMap headers = getHeaders(sUrl);
|
||||
|
||||
String url = SHARE_URL_PREFIX + "/filemoreajax.php?file=" + data.get("fid");
|
||||
client.postAbs(url).putHeaders(headers).sendForm(map).onSuccess(res2 -> {
|
||||
JsonObject fileListJson = asJson(res2);
|
||||
if (fileListJson.getInteger("zt") != 1) {
|
||||
promise.fail(baseMsg() + fileListJson.getString("info"));
|
||||
return;
|
||||
}
|
||||
List<FileInfo> list = new ArrayList<>();
|
||||
fileListJson.getJsonArray("text").forEach(item -> {
|
||||
/*
|
||||
{
|
||||
"icon": "apk",
|
||||
"t": 0,
|
||||
"id": "iULV2n4361c",
|
||||
"name_all": "xx.apk",
|
||||
"size": "49.8 M",
|
||||
"time": "2021-03-19",
|
||||
"duan": "in4361",
|
||||
"p_ico": 0
|
||||
}
|
||||
*/
|
||||
JsonObject fileJson = (JsonObject) item;
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
String size = fileJson.getString("size");
|
||||
Long sizeNum = FileSizeConverter.convertToBytes(size);
|
||||
String panType = shareLinkInfo.getType();
|
||||
String id = fileJson.getString("id");
|
||||
fileInfo.setFileName(fileJson.getString("name_all"))
|
||||
.setFileId(id)
|
||||
.setCreateTime(fileJson.getString("time"))
|
||||
.setFileType(fileJson.getString("icon"))
|
||||
.setSizeStr(fileJson.getString("size"))
|
||||
.setSize(sizeNum)
|
||||
.setPanType(panType)
|
||||
.setParserUrl(getDomainName() + "/d/" + panType + "/" + id)
|
||||
.setPreviewUrl(String.format("%s/v2/view/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), id));
|
||||
log.debug("文件信息: {}", fileInfo);
|
||||
list.add(fileInfo);
|
||||
});
|
||||
promise.complete(list);
|
||||
});
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
promise.fail(e);
|
||||
}
|
||||
String downUrl = urlJson.getString("dom") + "/file/" + urlJson.getString("url");
|
||||
client.getAbs(downUrl).putHeaders(headers).send()
|
||||
.onSuccess(res3 -> promise.complete(res3.headers().get("Location")))
|
||||
.onFailure(handleFail(downUrl));
|
||||
}).onFailure(handleFail(url));
|
||||
});
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
125
parser/src/main/java/cn/qaiu/parser/impl/MkgsTool.java
Normal file
125
parser/src/main/java/cn/qaiu/parser/impl/MkgsTool.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 酷狗音乐分享
|
||||
* <a href="https://t1.kugou.com/song.html?id=2bi8Fe9CSV3">分享链接1</a>
|
||||
* <a href="https://www.kugou.com/share/2bi8Fe9CSV3.html?id=2bi8Fe9CSV3#6ed9gna4">分享链接2</a>
|
||||
* <a href="https://www.kugou.com/share/2bi8Fe9CSV3.html">分享链接3</a>
|
||||
* <a href="https://www.kugou.com/mixsong/8odv4ef8.html">歌曲链接1</a>
|
||||
*/
|
||||
public class MkgsTool extends PanBase {
|
||||
|
||||
public static final String API_URL = "https://m.kugou.com/app/i/getSongInfo.php?cmd=playInfo&hash={hash}";
|
||||
|
||||
private static final MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||
static {
|
||||
// 设置 User-Agent
|
||||
headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0");
|
||||
|
||||
// 设置 Accept
|
||||
headers.set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
|
||||
|
||||
// 设置 If-Modified-Since
|
||||
headers.set("If-Modified-Since", "Mon, 21 Oct 2024 08:45:50 GMT");
|
||||
|
||||
// 设置 Priority
|
||||
headers.set("Priority", "u=0, i");
|
||||
|
||||
// 设置 Sec-CH-UA
|
||||
headers.set("Sec-CH-UA", "\"Microsoft Edge\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"");
|
||||
|
||||
// 设置 Sec-CH-UA-Mobile
|
||||
headers.set("Sec-CH-UA-Mobile", "?0");
|
||||
|
||||
// 设置 Sec-CH-UA-Platform
|
||||
headers.set("Sec-CH-UA-Platform", "\"Windows\"");
|
||||
|
||||
// 设置 Sec-Fetch-Dest
|
||||
headers.set("Sec-Fetch-Dest", "document");
|
||||
|
||||
// 设置 Sec-Fetch-Mode
|
||||
headers.set("Sec-Fetch-Mode", "navigate");
|
||||
|
||||
// 设置 Sec-Fetch-Site
|
||||
headers.set("Sec-Fetch-Site", "none");
|
||||
|
||||
// 设置 Sec-Fetch-User
|
||||
headers.set("Sec-Fetch-User", "?1");
|
||||
|
||||
// 设置 Upgrade-Insecure-Requests
|
||||
headers.set("Upgrade-Insecure-Requests", "1");
|
||||
};
|
||||
|
||||
public MkgsTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String shareUrl = shareLinkInfo.getStandardUrl();
|
||||
// String shareUrl = "https://t1.kugou.com/song.html?id=2bi8Fe9CSV3";
|
||||
clientNoRedirects.getAbs(shareUrl).send().onSuccess(res -> {
|
||||
String locationURL = res.headers().get("Location");
|
||||
downUrl(locationURL);
|
||||
}).onFailure(handleFail(shareUrl));
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
protected void downUrl(String locationURL) {
|
||||
client.getAbs(locationURL).putHeaders(headers).send().onSuccess(res2->{
|
||||
String body = res2.bodyAsString();
|
||||
// 正则表达式匹配 hash 字段
|
||||
String regex = "\"hash\"\s*:\s*\"([A-F0-9]+)\"";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(body);
|
||||
|
||||
// 查找并输出 hash 字段的值
|
||||
if (matcher.find()) {
|
||||
String hashValue = matcher.group(1); // 获取第一个捕获组
|
||||
System.out.println(hashValue);
|
||||
client.getAbs(UriTemplate.of(API_URL)).setTemplateParam("hash", hashValue).send().onSuccess(res3 -> {
|
||||
JsonObject jsonObject = asJson(res3);
|
||||
System.out.println(jsonObject.encodePrettily());
|
||||
if (jsonObject.containsKey("url")) {
|
||||
promise.complete(jsonObject.getString("url"));
|
||||
} else {
|
||||
fail("下载链接不存在");
|
||||
}
|
||||
}).onFailure(handleFail(API_URL.replace("{hash}", hashValue)));
|
||||
} else {
|
||||
fail("歌曲hash匹配失败, 可能分享已失效");
|
||||
}
|
||||
}).onFailure(handleFail(locationURL));
|
||||
}
|
||||
|
||||
public static class MkgTool extends MkgsTool {
|
||||
|
||||
public MkgTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
downUrl(shareLinkInfo.getStandardUrl());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
;
|
||||
}
|
||||
|
||||
public static class Mkgs2Tool extends MkgTool {
|
||||
public Mkgs2Tool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
parser/src/main/java/cn/qaiu/parser/impl/MkwTool.java
Normal file
68
parser/src/main/java/cn/qaiu/parser/impl/MkwTool.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.JsExecUtils;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 酷我音乐分享
|
||||
* <a href="https://kuwo.cn/play_detail/395500809">分享示例</a>
|
||||
* <a href="https://m.kuwo.cn/newh5app/play_detail/318448522">分享示例</a>
|
||||
*/
|
||||
public class MkwTool extends PanBase {
|
||||
|
||||
public static final String API_URL = "https://www.kuwo.cn/api/v1/www/music/playUrl?mid={mid}&type=music&httpsStatus=1&reqId=&plat=web_www&from=";
|
||||
|
||||
|
||||
public MkwTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String shareUrl = shareLinkInfo.getStandardUrl();
|
||||
clientSession.getAbs(shareUrl).send().onSuccess(result -> {
|
||||
String cookie = result.headers().get("set-cookie");
|
||||
|
||||
if (!cookie.isEmpty()) {
|
||||
|
||||
String regex = "([A-Za-z0-9_]+)=([A-Za-z0-9]+)";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(cookie);
|
||||
if (matcher.find()) {
|
||||
System.out.println(matcher.group(1));
|
||||
System.out.println(matcher.group(2));
|
||||
|
||||
var key = matcher.group(1);
|
||||
var token = matcher.group(2);
|
||||
String sign = JsExecUtils.getKwSign(token, key);
|
||||
System.out.println(sign);
|
||||
clientSession.getAbs(UriTemplate.of(API_URL)).setTemplateParam("mid", shareLinkInfo.getShareKey())
|
||||
.putHeader("Secret", sign).send().onSuccess(res -> {
|
||||
JsonObject json = asJson(res);
|
||||
log.debug(json.encodePrettily());
|
||||
try {
|
||||
if (json.getInteger("code") == 200) {
|
||||
complete(json.getJsonObject("data").getString("url"));
|
||||
} else {
|
||||
fail("链接已失效/需要VIP");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
fail("解析失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
26
parser/src/main/java/cn/qaiu/parser/impl/MmgTool.java
Normal file
26
parser/src/main/java/cn/qaiu/parser/impl/MmgTool.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
|
||||
/**
|
||||
* 咪咕音乐分享
|
||||
*/
|
||||
public class MmgTool extends PanBase {
|
||||
|
||||
public static final String API_URL = "";
|
||||
|
||||
|
||||
public MmgTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String shareUrl = shareLinkInfo.getStandardUrl();
|
||||
|
||||
// TODO
|
||||
promise.complete("暂未实现, 敬请期待");
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
59
parser/src/main/java/cn/qaiu/parser/impl/MnesTool.java
Normal file
59
parser/src/main/java/cn/qaiu/parser/impl/MnesTool.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.URLUtil;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
/**
|
||||
* 网易云音乐, 单歌曲直链解析
|
||||
* <a href="http://163cn.tv/ykLZJJT">示例分享1</a>
|
||||
* <a href="https://music.163.com/#/song?id=472194327">示例分享2</a>
|
||||
*/
|
||||
public class MnesTool extends PanBase {
|
||||
|
||||
|
||||
public static final String API_URL = "https://music.163.com/song/media/outer/url?id={id}";
|
||||
|
||||
|
||||
public MnesTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String shareUrl = shareLinkInfo.getStandardUrl();
|
||||
clientNoRedirects.getAbs(shareUrl).send().onSuccess(res -> {
|
||||
String locationURL = res.headers().get("Location");
|
||||
downUrl(locationURL);
|
||||
}).onFailure(handleFail(shareUrl));
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
protected void downUrl(String locationURL) {
|
||||
String id = URLUtil.from(locationURL).getParam("id");
|
||||
clientNoRedirects.getAbs(UriTemplate.of(API_URL)).setTemplateParam("id", id).send()
|
||||
.onSuccess(res2 -> {
|
||||
String location = res2.headers().get("Location");
|
||||
if (location.endsWith("/404")) {
|
||||
fail("链接已失效: id={}", id);
|
||||
} else {
|
||||
promise.complete(location);
|
||||
}
|
||||
}).onFailure(handleFail(API_URL.replace("{id}", id)));
|
||||
}
|
||||
|
||||
|
||||
public static class MneTool extends MnesTool{
|
||||
|
||||
public MneTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
downUrl(shareLinkInfo.getStandardUrl());
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
}
|
||||
78
parser/src/main/java/cn/qaiu/parser/impl/MqqsTool.java
Normal file
78
parser/src/main/java/cn/qaiu/parser/impl/MqqsTool.java
Normal file
@@ -0,0 +1,78 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.URLUtil;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* QQ音乐分享解析
|
||||
* <a href="https://c6.y.qq.com/base/fcgi-bin/u?__=w3lqEpOHACLO">分享示例</a>
|
||||
* <a href="https://y.qq.com/n/ryqq/songDetail/000XjcLg0fbRjv?songtype=0">详情页</a>
|
||||
*/
|
||||
public class MqqsTool extends PanBase {
|
||||
|
||||
public static final String API_URL = "https://u.y.qq.com/cgi-bin/musicu" +
|
||||
".fcg?-=getplaysongvkey2682247447678878&g_tk=5381&loginUin=956581739&hostUin=0&format=json&inCharset=utf8" +
|
||||
"&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data=%7B%22req_0%22%3A%7B%22module%22%3A" +
|
||||
"%22vkey.GetVkeyServer%22%2C%22method%22%3A%22CgiGetVkey%22%2C%22param%22%3A%7B%22guid%22%3A%222796982635" +
|
||||
"%22%2C%22songmid%22%3A%5B%22{songmid}%22%5D%2C%22songtype%22%3A%5B1%5D%2C%22uin%22%3A%22956581739%22" +
|
||||
"%2C%22loginflag%22%3A1%2C%22platform%22%3A%2220%22%7D%7D%2C%22comm%22%3A%7B%22uin%22%3A956581739%2C" +
|
||||
"%22format%22%3A%22json%22%2C%22ct%22%3A24%2C%22cv%22%3A0%7D%7D";
|
||||
|
||||
public MqqsTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String shareUrl = shareLinkInfo.getStandardUrl();
|
||||
// https://c6.y.qq.com/base/fcgi-bin/u?__=uXXtsB
|
||||
// String shareUrl = "https://c6.y.qq.com/base/fcgi-bin/u?__=k8gafY6HAQ5Y";
|
||||
|
||||
clientNoRedirects.getAbs(shareUrl).send().onSuccess(res -> {
|
||||
String locationURL = res.headers().get("Location");
|
||||
String id = URLUtil.from(locationURL).getParam("songmid");
|
||||
downUrl(id);
|
||||
}).onFailure(handleFail(shareUrl));
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
protected void downUrl(String id) {
|
||||
clientNoRedirects.getAbs(UriTemplate.of(API_URL)).setTemplateParam("songmid", id).send().onSuccess(res2 -> {
|
||||
JsonObject jsonObject = asJson(res2);
|
||||
log.debug(jsonObject.encodePrettily());
|
||||
try {
|
||||
JsonObject data = jsonObject.getJsonObject("req_0").getJsonObject("data");
|
||||
String path = data.getJsonArray("midurlinfo").getJsonObject(0).getString("purl");
|
||||
if (path.isEmpty()) {
|
||||
fail("暂不支持VIP音乐");
|
||||
return;
|
||||
}
|
||||
String downURL = data.getJsonArray("sip").getString(0)
|
||||
.replace("http://", "https://") + path;
|
||||
promise.complete(downURL);
|
||||
} catch (Exception e) {
|
||||
fail("获取失败");
|
||||
}
|
||||
}).onFailure(handleFail(API_URL.replace("{id}", id)));
|
||||
}
|
||||
|
||||
|
||||
public static class MqqTool extends MqqsTool{
|
||||
|
||||
public MqqTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
downUrl(shareLinkInfo.getShareKey());
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
}
|
||||
22
parser/src/main/java/cn/qaiu/parser/impl/OtherTool.java
Normal file
22
parser/src/main/java/cn/qaiu/parser/impl/OtherTool.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
|
||||
/**
|
||||
* 其他网盘解析
|
||||
*/
|
||||
public class OtherTool extends PanBase {
|
||||
private static final String API_URL_PREFIX = "";
|
||||
|
||||
public OtherTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
// TODO
|
||||
fail("暂未实现, 敬请期待");
|
||||
return future();
|
||||
}
|
||||
}
|
||||
90
parser/src/main/java/cn/qaiu/parser/impl/P115Tool.java
Normal file
90
parser/src/main/java/cn/qaiu/parser/impl/P115Tool.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
/**
|
||||
* 115网盘
|
||||
*
|
||||
* 需要请求API的UA和请求下载链接的UA保持一致,安卓Chrome需要访问电脑版才能下载
|
||||
*/
|
||||
public class P115Tool extends PanBase {
|
||||
private static final String API_URL_PREFIX = "https://115cdn.com/webapi/";
|
||||
|
||||
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "share/snap?share_code={dataKey}&offset=0" +
|
||||
"&limit=20&receive_code={dataPwd}&cid=";
|
||||
|
||||
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "share/skip_login_downurl";
|
||||
|
||||
|
||||
private static final MultiMap header;
|
||||
|
||||
static {
|
||||
header = MultiMap.caseInsensitiveMultiMap();
|
||||
header.set("Accept", "application/json, text/plain, */*");
|
||||
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||
header.set("Cache-Control", "no-cache");
|
||||
header.set("Connection", "keep-alive");
|
||||
header.set("Content-Length", "0");
|
||||
header.set("DNT", "1");
|
||||
header.set("Host", "115cdn.com");
|
||||
header.set("Origin", "https://115cdn.com");
|
||||
header.set("Pragma", "no-cache");
|
||||
header.set("Referer", "https://115cdn.com");
|
||||
header.set("Sec-Fetch-Dest", "empty");
|
||||
header.set("Sec-Fetch-Mode", "cors");
|
||||
header.set("Sec-Fetch-Site", "cross-site");
|
||||
header.set("sec-ch-ua", "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
|
||||
header.set("sec-ch-ua-mobile", "?0");
|
||||
header.set("sec-ch-ua-platform", "\"Windows\"");
|
||||
}
|
||||
|
||||
public P115Tool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
// 第一次请求 获取文件信息
|
||||
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL))
|
||||
.putHeaders(header)
|
||||
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString())
|
||||
.setTemplateParam("dataKey", shareLinkInfo.getShareKey())
|
||||
.setTemplateParam("dataPwd", shareLinkInfo.getSharePassword())
|
||||
.send().onSuccess(res -> {
|
||||
JsonObject resJson = asJson(res);
|
||||
if (!resJson.getBoolean("state")) {
|
||||
fail(FIRST_REQUEST_URL + " 解析错误: " + resJson);
|
||||
return;
|
||||
}
|
||||
// 文件Id: data.list[0].fid
|
||||
JsonObject fileInfo = resJson.getJsonObject("data").getJsonArray("list").getJsonObject(0);
|
||||
String fileId = fileInfo.getString("fid");
|
||||
|
||||
// 第二次请求
|
||||
// share_code={dataKey}&receive_code={dataPwd}&file_id={file_id}
|
||||
client.postAbs(SECOND_REQUEST_URL)
|
||||
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.putHeader("User-Agent", shareLinkInfo.getOtherParam().get("UA").toString())
|
||||
.sendForm(MultiMap.caseInsensitiveMultiMap()
|
||||
.set("share_code", shareLinkInfo.getShareKey())
|
||||
.set("receive_code", shareLinkInfo.getSharePassword())
|
||||
.set("file_id", fileId))
|
||||
.onSuccess(res2 -> {
|
||||
JsonObject resJson2 = asJson(res2);
|
||||
if (!resJson2.getBoolean("state")) {
|
||||
fail(FIRST_REQUEST_URL + " 解析错误: " + resJson2);
|
||||
return;
|
||||
}
|
||||
// data.url.url
|
||||
promise.complete(resJson2.getJsonObject("data").getJsonObject("url").getString("url"));
|
||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||
}).onFailure(handleFail(FIRST_REQUEST_URL));
|
||||
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
50
parser/src/main/java/cn/qaiu/parser/impl/P118Tool.java
Normal file
50
parser/src/main/java/cn/qaiu/parser/impl/P118Tool.java
Normal file
@@ -0,0 +1,50 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 118网盘解析
|
||||
*/
|
||||
public class P118Tool extends PanBase {
|
||||
|
||||
private static final String API_URL_PREFIX = "https://qaiu.118pan.com/ajax.php";
|
||||
|
||||
// private static final String
|
||||
|
||||
public P118Tool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
|
||||
client.postAbs(API_URL_PREFIX)
|
||||
.putHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.sendBuffer(Buffer.buffer("action=load_down_addr1&file_id=" + shareLinkInfo.getShareKey()))
|
||||
.onSuccess(res -> {
|
||||
System.out.println(res.headers());
|
||||
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
|
||||
Matcher matcher = compile.matcher(res.bodyAsString());
|
||||
if (matcher.find()) {
|
||||
//c: 0x63
|
||||
//o: 0x6F
|
||||
//m: 0x6D
|
||||
//1: 0x31
|
||||
///: 0x2F
|
||||
char[] chars1 = new char[]{99, 111, 109, 49, 47};
|
||||
char[] chars2 = new char[]{99, 111, 109, 47};
|
||||
String group = matcher.group(1).replace(String.valueOf(chars1), String.valueOf(chars2));
|
||||
System.out.println(group);
|
||||
complete(group);
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}).onFailure(handleFail(""));
|
||||
return future();
|
||||
}
|
||||
}
|
||||
109
parser/src/main/java/cn/qaiu/parser/impl/P360Tool.java
Normal file
109
parser/src/main/java/cn/qaiu/parser/impl/P360Tool.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* <a href="https://yunpan.360.cn">360AI云盘</a>
|
||||
* 360AI云盘解析
|
||||
* 下载链接需要Referer: https://link.yunpan.com/
|
||||
*/
|
||||
public class P360Tool extends PanBase {
|
||||
public P360Tool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
// https://www.yunpan.com/surl_yD7jK9d4W6D 获取302跳转地址
|
||||
clientNoRedirects.getAbs(shareLinkInfo.getShareUrl()).send()
|
||||
.onSuccess(res -> {
|
||||
String location = res.getHeader("Location");
|
||||
if (location != null) {
|
||||
down302(location);
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}).onFailure(handleFail(""));
|
||||
|
||||
|
||||
return future();
|
||||
}
|
||||
|
||||
private void down302(String url) {
|
||||
// 获取URL前缀 https://214004.link.yunpan.com/lk/surl_yD7ZU4WreR8 -> https://214004.link.yunpan.com/
|
||||
String urlPrefix = url.substring(0, url.indexOf("/", 8));
|
||||
|
||||
clientSession.getAbs(url)
|
||||
.send()
|
||||
.onSuccess(res -> {
|
||||
// find "nid": "17402043311959599"
|
||||
Pattern compile = Pattern.compile("\"nid\": \"([^\"]+)\"");
|
||||
Matcher matcher = compile.matcher(res.bodyAsString());
|
||||
AtomicReference<String> nid = new AtomicReference<>();
|
||||
if (matcher.find()) {
|
||||
nid.set(matcher.group(1));
|
||||
} else {
|
||||
// 需要验证密码
|
||||
/*
|
||||
* POST https://4aec17.link.yunpan.com/share/verifyPassword
|
||||
* Content-type: application/x-www-form-urlencoded UTF-8
|
||||
* Referer: https://4aec17.link.yunpan.com/lk/surl_yD7jK9d4W6D
|
||||
*
|
||||
* shorturl=surl_yD7jK9d4W6D&linkpassword=d969
|
||||
*/
|
||||
clientSession.postAbs(urlPrefix + "/share/verifyPassword")
|
||||
.putHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.putHeader("Referer", urlPrefix)
|
||||
.sendBuffer(Buffer.buffer("shorturl=" + shareLinkInfo.getShareKey() + "&linkpassword" +
|
||||
"=" + shareLinkInfo.getSharePassword()))
|
||||
.onSuccess(res2 -> {
|
||||
JsonObject entries = asJson(res2);
|
||||
if (entries.getInteger("errno") == 0) {
|
||||
clientSession.getAbs(url)
|
||||
.send()
|
||||
.onSuccess(res3 -> {
|
||||
Matcher matcher1 = compile.matcher(res3.bodyAsString());
|
||||
if (matcher1.find()) {
|
||||
nid.set(matcher1.group(1));
|
||||
} else {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
down(urlPrefix, nid.get());
|
||||
}).onFailure(handleFail(""));
|
||||
} else {
|
||||
fail(entries.encode());
|
||||
}
|
||||
}).onFailure(handleFail(""));
|
||||
return;
|
||||
}
|
||||
down(urlPrefix, nid.get());
|
||||
}).onFailure(handleFail(""));
|
||||
}
|
||||
|
||||
private void down(String urlPrefix, String nid) {
|
||||
clientSession.postAbs(urlPrefix + "/share/downloadfile")
|
||||
.putHeader("Content-Type", "application/x-www-form-urlencoded")
|
||||
.putHeader("Referer", urlPrefix)
|
||||
.sendBuffer(Buffer.buffer("shorturl=" + shareLinkInfo.getShareKey() + "&nid=" + nid))
|
||||
.onSuccess(res2 -> {
|
||||
JsonObject entries = asJson(res2);
|
||||
String downloadurl = entries.getJsonObject("data").getString("downloadurl");
|
||||
complete(downloadurl);
|
||||
}).onFailure(handleFail(""));
|
||||
}
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// String s = new P360Tool(ShareLinkInfo.newBuilder().shareUrl("https://www.yunpan.com/surl_yD7ZU4WreR8")
|
||||
// .shareKey("surl_yD7ZU4WreR8")
|
||||
// .build()).parseSync();
|
||||
// System.out.println(s);
|
||||
// }
|
||||
}
|
||||
47
parser/src/main/java/cn/qaiu/parser/impl/PcxTool.java
Normal file
47
parser/src/main/java/cn/qaiu/parser/impl/PcxTool.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
/**
|
||||
* <a href="https://passport2.chaoxing.com">超星云盘</a>
|
||||
*/
|
||||
public class PcxTool extends PanBase {
|
||||
|
||||
public PcxTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
client.getAbs(shareLinkInfo.getShareUrl())
|
||||
.send().onSuccess(res -> {
|
||||
// 'download': 'https://d0.ananas.chaoxing.com/download/de08dcf546e4dd88a17bead86ff6338d?at_=1740211698795&ak_=d62a3acbd5ce43e1e8565b67990691e4&ad_=8c4ef22e980ee0dd9532ec3757ab19f8&fn=33.c'
|
||||
String body = res.bodyAsString();
|
||||
// 获取download
|
||||
String str = "var fileinfo = {";
|
||||
String fileInfo = res.bodyAsString().substring(res.bodyAsString().indexOf(str) + str.length() - 1
|
||||
, res.bodyAsString().indexOf("};") + 1);
|
||||
fileInfo = fileInfo.replace("'", "\"");
|
||||
JsonObject jsonObject = new JsonObject(fileInfo);
|
||||
String download = jsonObject.getString("download");
|
||||
if (download.contains("fn=")) {
|
||||
complete(download);
|
||||
} else {
|
||||
fail("获取下载链接失败: 不支持的文件类型: {}", jsonObject.getString("suffix"));
|
||||
}
|
||||
}).onFailure(handleFail(shareLinkInfo.getShareUrl()));
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
|
||||
// public static void main(String[] args) {
|
||||
// String s = new PcxTool(ShareLinkInfo.newBuilder().shareUrl("https://pan-yz.cldisk.com/external/m/file/953658049102462976")
|
||||
// .shareKey("953658049102462976")
|
||||
// .build()).parseSync();
|
||||
// System.out.println(s);
|
||||
// }
|
||||
}
|
||||
95
parser/src/main/java/cn/qaiu/parser/impl/PdbTool.java
Normal file
95
parser/src/main/java/cn/qaiu/parser/impl/PdbTool.java
Normal file
@@ -0,0 +1,95 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.URLUtil;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.ProxyOptions;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
import io.vertx.ext.web.client.HttpResponse;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <a href="https://www.dropbox.com/">dropbox</a>
|
||||
* Dropbox网盘--不支持大陆地区
|
||||
*/
|
||||
public class PdbTool extends PanBase implements IPanTool {
|
||||
|
||||
private static final String API_URL =
|
||||
"https://www.dropbox.com/sharing/fetch_user_content_link";
|
||||
static final String COOKIE_KEY = "__Host-js_csrf=";
|
||||
|
||||
public PdbTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/emqx-5.0.26-el7-amd64.tar.gz?rlkey=3uoi4bxz5mv93jmlaws0nlol1&e=8&st=fe0lclc2&dl=0
|
||||
// https://www.dropbox.com/scl/fi/()/Get-Started-with-Dropbox.pdf?rlkey=yrddd6s9gxsq967pmbgtzvfl3&st=2trcc1f3&dl=0
|
||||
//
|
||||
clientSession.getAbs(shareLinkInfo.getShareUrl())
|
||||
.send()
|
||||
.onSuccess(res->{
|
||||
List<String> collect =
|
||||
res.cookies().stream().filter(key -> key.contains(COOKIE_KEY)).toList();
|
||||
if (collect.isEmpty()) {
|
||||
fail("cookie未找到");
|
||||
return;
|
||||
}
|
||||
Matcher matcher = Pattern.compile(COOKIE_KEY + "([\\w-]+);").matcher(collect.get(0));
|
||||
String _t;
|
||||
if (matcher.find()) {
|
||||
_t = matcher.group(1);
|
||||
} else {
|
||||
fail("cookie未找到");
|
||||
return;
|
||||
}
|
||||
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||
headers.set("accept", "*/*");
|
||||
headers.set("accept-language", "zh-CN,zh;q=0.9");
|
||||
headers.set("cache-control", "no-cache");
|
||||
headers.set("dnt", "1");
|
||||
headers.set("origin", "https://www.dropbox.com");
|
||||
headers.set("pragma", "no-cache");
|
||||
headers.set("priority", "u=1, i");
|
||||
headers.set("referer", shareLinkInfo.getShareUrl());
|
||||
headers.set("sec-ch-ua", "\"Chromium\";v=\"130\", \"Microsoft Edge\";v=\"130\", \"Not?A_Brand\";v=\"99\"");
|
||||
headers.set("sec-ch-ua-mobile", "?0");
|
||||
headers.set("sec-ch-ua-platform", "\"Windows\"");
|
||||
headers.set("sec-fetch-dest", "empty");
|
||||
headers.set("sec-fetch-mode", "cors");
|
||||
headers.set("sec-fetch-site", "same-origin");
|
||||
headers.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0");
|
||||
headers.set("x-dropbox-client-yaps-attribution", "edison_atlasservlet.file_viewer-edison:prod");
|
||||
headers.set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
try {
|
||||
URL url = new URL(shareLinkInfo.getShareUrl());
|
||||
// https://www.dropbox.com/scl/fi/cwnbms1yn8u6rcatzyta7/xxx?rlkey=xx&dl=0
|
||||
String u0 = URLEncoder.encode((url.getProtocol() + "://" + url.getHost() + url.getPath() + "?rlkey=%s&dl=0")
|
||||
.formatted(URLUtil.from(shareLinkInfo.getShareUrl()).getParam("rlkey")), StandardCharsets.UTF_8);
|
||||
clientSession.postAbs(API_URL)
|
||||
.sendBuffer(Buffer.buffer("is_xhr=true&t=%s&url=%s&origin=PREVIEW_PAGE".formatted(_t, u0)))
|
||||
.onSuccess(res2 -> {
|
||||
complete(res2.bodyAsString());
|
||||
})
|
||||
.onFailure(handleFail());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
})
|
||||
.onFailure(handleFail("请求下载链接失败"));
|
||||
return future();
|
||||
}
|
||||
}
|
||||
98
parser/src/main/java/cn/qaiu/parser/impl/PgdTool.java
Normal file
98
parser/src/main/java/cn/qaiu/parser/impl/PgdTool.java
Normal file
@@ -0,0 +1,98 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.net.ProxyOptions;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
import io.vertx.ext.web.client.HttpResponse;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* <a href="https://drive.google.com/">GoogleDrive</a>
|
||||
* Google Drive文件解析工具.
|
||||
*/
|
||||
public class PgdTool extends PanBase implements IPanTool {
|
||||
|
||||
private static final String DOWN_URL_TEMPLATE =
|
||||
"https://drive.usercontent.google.com/download";
|
||||
|
||||
private String downloadUrl;
|
||||
|
||||
public PgdTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
downloadUrl = DOWN_URL_TEMPLATE + "?id=" + shareLinkInfo.getShareKey() + "&export=download";
|
||||
|
||||
if (shareLinkInfo.getOtherParam().containsKey("proxy")) {
|
||||
// if (shareLinkInfo.getOtherParam().containsKey("bypassCheck")
|
||||
// && "true".equalsIgnoreCase(shareLinkInfo.getOtherParam().get("bypassCheck").toString())) {
|
||||
// 发起请求但不真正下载文件, 只检查响应头
|
||||
client.headAbs(downloadUrl).send()
|
||||
.onSuccess(this::handleResponse)
|
||||
.onFailure(handleFail("请求下载链接失败"));
|
||||
return future();
|
||||
}
|
||||
complete(downloadUrl);
|
||||
return future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理下载链接的响应.
|
||||
*/
|
||||
private void handleResponse(HttpResponse<Buffer> response) {
|
||||
String contentType = response.getHeader("Content-Type");
|
||||
if (contentType != null && !contentType.contains("text/html")) {
|
||||
complete(downloadUrl);
|
||||
} else {
|
||||
// 如果不是文件流类型,从 HTML 中解析出真实下载链接
|
||||
client.getAbs(downloadUrl)
|
||||
.send()
|
||||
.onSuccess(res0 -> {
|
||||
parseHtmlForRealLink(res0.bodyAsString());
|
||||
})
|
||||
.onFailure(handleFail("请求下载链接失败"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从HTML内容中解析真实下载链接.
|
||||
*/
|
||||
private void parseHtmlForRealLink(String html) {
|
||||
// 使用正则表达式匹配 id、export、confirm、uuid、at 等参数
|
||||
String id = extractHiddenInputValue(html, "id");
|
||||
String confirm = extractHiddenInputValue(html, "confirm");
|
||||
String uuid = extractHiddenInputValue(html, "uuid");
|
||||
String at = extractHiddenInputValue(html, "at");
|
||||
|
||||
if (id != null && confirm != null && uuid != null) {
|
||||
String realDownloadLink = DOWN_URL_TEMPLATE +
|
||||
"?id=" + id +
|
||||
"&export=download" +
|
||||
"&confirm=" + confirm +
|
||||
"&uuid=" + uuid;
|
||||
if (at != null) {
|
||||
realDownloadLink += ( "&at=" + at);
|
||||
}
|
||||
complete(realDownloadLink);
|
||||
} else {
|
||||
fail("无法找到完整的下载链接参数");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法: 从HTML中提取指定name的input隐藏字段的value
|
||||
*/
|
||||
private String extractHiddenInputValue(String html, String name) {
|
||||
Pattern pattern = Pattern.compile("<input[^>]*name=\"" + name + "\"[^>]*value=\"([^\"]*)\"");
|
||||
Matcher matcher = pattern.matcher(html);
|
||||
return matcher.find() ? matcher.group(1) : null;
|
||||
}
|
||||
}
|
||||
58
parser/src/main/java/cn/qaiu/parser/impl/PicTool.java
Normal file
58
parser/src/main/java/cn/qaiu/parser/impl/PicTool.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
|
||||
/**
|
||||
* <a href="https://www.icloud.com.cn/">iCloud云盘(pic)</a>
|
||||
*/
|
||||
public class PicTool extends PanBase {
|
||||
|
||||
|
||||
private static final String api = "https://ckdatabasews.icloud.com.cn/database/1/com.apple.cloudkit/production/public/records/resolve";
|
||||
|
||||
public PicTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
// {"shortGUIDs":[{"value":"xxx"}]}
|
||||
JsonObject jsonObject =
|
||||
new JsonObject("{\"shortGUIDs\":[{\"value\":\"%s\"}]}".formatted(shareLinkInfo.getShareKey()));
|
||||
|
||||
client.postAbs(api).sendJsonObject(jsonObject).onSuccess(res -> {
|
||||
// results->rootRecord->fields->fileContent->value->downloadURL // ${f}->fileName
|
||||
// fileName: results->share->fields->cloudkit.title->value + "." + results->rootRecord->fields->extension->value
|
||||
JsonObject json = asJson(res);
|
||||
try {
|
||||
JsonObject result = json.getJsonArray("results").getJsonObject(0);
|
||||
JsonObject fileInfo = result
|
||||
.getJsonObject("rootRecord")
|
||||
.getJsonObject("fields");
|
||||
|
||||
String downURL = fileInfo
|
||||
.getJsonObject("fileContent")
|
||||
.getJsonObject("value")
|
||||
.getString("downloadURL");
|
||||
|
||||
String extension = fileInfo
|
||||
.getJsonObject("extension")
|
||||
.getString("value");
|
||||
|
||||
String fileTitle = result
|
||||
.getJsonObject("share")
|
||||
.getJsonObject("fields")
|
||||
.getJsonObject("cloudkit.title")
|
||||
.getString("value");
|
||||
complete(downURL.replace("${f}", fileTitle + "." + extension));
|
||||
} catch (Exception e) {
|
||||
fail(e, "json解析失败");
|
||||
}
|
||||
|
||||
}).onFailure(handleFail());
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
231
parser/src/main/java/cn/qaiu/parser/impl/PodTool.java
Normal file
231
parser/src/main/java/cn/qaiu/parser/impl/PodTool.java
Normal file
@@ -0,0 +1,231 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.WebClientVertxInit;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.WorkerExecutor;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* <a href="https://onedrive.live.com/">onedrive分享(od)</a>
|
||||
*/
|
||||
public class PodTool extends PanBase {
|
||||
|
||||
/*
|
||||
* https://1drv.ms/w/s!Alg0feQmCv2rnRFd60DQOmMa-Oh_?e=buaRtp --302->
|
||||
* https://api.onedrive.com/v1.0/drives/abfd0a26e47d3458/items/ABFD0A26E47D3458!3729?authkey=!AF3rQNA6Yxr46H8
|
||||
* https://onedrive.live.com/redir?resid=(?<cid>)!(?<cid2>)&authkey=(?<authkey>)&e=hV98W1
|
||||
* cid: abfd0a26e47d3458, cid2: ABFD0A26E47D3458!3729 authkey: !AF3rQNA6Yxr46H8
|
||||
* -> @content.downloadUrl
|
||||
*/
|
||||
|
||||
|
||||
// https://onedrive.live.com/redir?resid=ABFD0A26E47D3458!4699&e=OggA4s&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBbGcwZmVRbUN2MnJwRnZ1NDQ0aGc1eVZxRGNLP2U9T2dnQTRz
|
||||
private static final String API_TEMPLATE = "https://onedrive.live.com/embed" +
|
||||
"?id={resid}&resid={resid1}" +
|
||||
"&cid={cid}" +
|
||||
"&redeem={redeem}" +
|
||||
"&migratedtospo=true&embed=1";
|
||||
|
||||
private static final String TOKEN_API = "https://api-badgerp.svc.ms/v1.0/token";
|
||||
|
||||
|
||||
private static final Pattern redirectUrlRegex =
|
||||
Pattern.compile("resid=(?<cid1>[^!]+)!(?<cid2>[^&]+).+&redeem=(?<redeem>.+).*");
|
||||
|
||||
public PodTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
|
||||
|
||||
/*
|
||||
* POST https://api-badgerp.svc.ms/v1.0/token
|
||||
* Content-Type: application/json
|
||||
*
|
||||
* {
|
||||
* "appid": "00000000-0000-0000-0000-0000481710a4"
|
||||
* }
|
||||
*/
|
||||
// https://my.microsoftpersonalcontent.com/_api/v2.0/shares/u!aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBbGcwZmVRbUN2MnJwRnZ1NDQ0aGc1eVZxRGNLP2U9T2dnQTRz/driveitem?%24select=*%2Cocr%2CwebDavUrl
|
||||
// https://onedrive.live.com/embed?id=ABFD0A26E47D3458!4698&resid=ABFD0A26E47D3458!4698&cid=abfd0a26e47d3458&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvYy9hYmZkMGEyNmU0N2QzNDU4L0lRUllOSDNrSmdyOUlJQ3JXaElBQUFBQUFTWGlubWZ2WmNxYUQyMXJUQjIxVmg4&migratedtospo=true&embed=1
|
||||
|
||||
|
||||
clientNoRedirects.getAbs(shareLinkInfo.getShareUrl() == null ? shareLinkInfo.getStandardUrl() :
|
||||
shareLinkInfo.getShareUrl()).send().onSuccess(r0 -> {
|
||||
String location = r0.getHeader("Location");
|
||||
Matcher matcher = redirectUrlRegex.matcher(location);
|
||||
if (!matcher.find()) {
|
||||
fail("Location格式错误");
|
||||
return;
|
||||
}
|
||||
String redeem = matcher.group("redeem");
|
||||
String cid1 = matcher.group("cid1");
|
||||
String cid2 = cid1 + "!" + matcher.group("cid2");
|
||||
|
||||
clientNoRedirects.getAbs(UriTemplate.of(API_TEMPLATE))
|
||||
.setTemplateParam("resid", cid2)
|
||||
.setTemplateParam("resid1", cid2)
|
||||
.setTemplateParam("cid", cid1.toLowerCase())
|
||||
.setTemplateParam("redeem", redeem)
|
||||
.send()
|
||||
.onSuccess(r1 -> {
|
||||
String auth =
|
||||
r1.cookies().stream().filter(c -> c.startsWith("BadgerAuth=")).findFirst().orElse("");
|
||||
if (auth.isEmpty()) {
|
||||
fail("Error BadgerAuth not fount");
|
||||
return;
|
||||
}
|
||||
String token = auth.split(";")[0].split("=")[1];
|
||||
|
||||
try {
|
||||
|
||||
String url = matcherUrl(r1.bodyAsString());
|
||||
|
||||
sendHttpRequest(url, token).onSuccess(body -> {
|
||||
Matcher matcher1 =
|
||||
Pattern.compile("\"downloadUrl\":\"(?<url>https?://[^\s\"]+)").matcher(body);
|
||||
if (matcher1.find()) {
|
||||
complete(matcher1.group("url"));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}).onFailure(handleFail());
|
||||
} catch (Exception ignored) {
|
||||
sendHttpRequest2(token, redeem).onSuccess(res -> {
|
||||
try {
|
||||
complete(new JsonObject(res).getString("@content.downloadUrl"));
|
||||
} catch (Exception ignored1) {
|
||||
fail();
|
||||
}
|
||||
}).onFailure(handleFail());
|
||||
}
|
||||
|
||||
}).onFailure(handleFail());
|
||||
}).onFailure(handleFail());
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
private String matcherUrl(String html) {
|
||||
|
||||
// 正则表达式来匹配 URL
|
||||
String urlRegex = "'action'.+(?<url>https://.+)'\\)";
|
||||
Pattern urlPattern = Pattern.compile(urlRegex);
|
||||
Matcher urlMatcher = urlPattern.matcher(html);
|
||||
|
||||
if (urlMatcher.find()) {
|
||||
String url = urlMatcher.group("url");
|
||||
System.out.println("URL: " + url);
|
||||
return url;
|
||||
}
|
||||
throw new RuntimeException("URL匹配失败");
|
||||
}
|
||||
|
||||
|
||||
private String matcherToken(String html) {
|
||||
// 正则表达式来匹配 inputElem.value 中的 Token
|
||||
String tokenRegex = "inputElem\\.value\\s*=\\s*'([^']+)'";
|
||||
Pattern tokenPattern = Pattern.compile(tokenRegex);
|
||||
Matcher tokenMatcher = tokenPattern.matcher(html);
|
||||
|
||||
if (tokenMatcher.find()) {
|
||||
String token = tokenMatcher.group(1);
|
||||
System.out.println("Token: " + token);
|
||||
return token;
|
||||
}
|
||||
throw new RuntimeException("token匹配失败");
|
||||
}
|
||||
|
||||
public Future<String> sendHttpRequest2(String token, String redeem) {
|
||||
Promise<String> promise = Promise.promise();
|
||||
// 构造 HttpClient
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
// 构造请求的 URI 和头部信息
|
||||
// https://onedrive.live.com/redir?cid=abfd0a26e47d3458&resid=ABFD0A26E47D3458!4465&ithint=file%2cxlsx&e=Ao2uSU&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3gvYy9hYmZkMGEyNmU0N2QzNDU4L0VWZzBmZVFtQ3YwZ2dLdHhFUUFBQUFBQlRQRWVDMTZfZk1EYk5FTjhEdTRta1E_ZT1BbzJ1U1U
|
||||
String url = ("https://my.microsoftpersonalcontent.com/_api/v2.0/shares/u!%s/driveItem?$select=content" +
|
||||
".downloadUrl").formatted(redeem);
|
||||
String authorizationHeader = "Badger " + token;
|
||||
|
||||
// 构建请求
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.header("Authorization", authorizationHeader)
|
||||
.build();
|
||||
|
||||
// 发送请求并处理响应
|
||||
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||
.thenApply(response -> {
|
||||
System.out.println("Response Status Code: " + response.statusCode());
|
||||
System.out.println("Response Body: " + response.body());
|
||||
promise.complete(response.body());
|
||||
return null;
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public Future<String> sendHttpRequest(String url, String token) {
|
||||
// 创建一个 WorkerExecutor 用于异步执行阻塞的 HTTP 请求
|
||||
WorkerExecutor executor = WebClientVertxInit.get().createSharedWorkerExecutor("http-client-worker");
|
||||
|
||||
Promise<String> promise = Promise.promise();
|
||||
executor.executeBlocking(() -> {
|
||||
HttpClient client = HttpClient.newHttpClient();
|
||||
HttpRequest request = null;
|
||||
|
||||
try {
|
||||
// 构造请求
|
||||
request = HttpRequest.newBuilder()
|
||||
.uri(new URI(url))
|
||||
.header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9," +
|
||||
"image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;" +
|
||||
"v=b3;q=0.7")
|
||||
.header("accept-language", "zh-CN,zh;q=0.9")
|
||||
.header("cache-control", "no-cache")
|
||||
.header("content-type", "application/x-www-form-urlencoded")
|
||||
.header("dnt", "1")
|
||||
.header("origin", "https://onedrive.live.com")
|
||||
.header("pragma", "no-cache")
|
||||
.header("priority", "u=0, i")
|
||||
.header("referer", "https://onedrive.live.com/")
|
||||
.header("sec-ch-ua", "\"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", " +
|
||||
"\"Not?A_Brand\";v=\"99\"")
|
||||
.header("sec-ch-ua-mobile", "?0")
|
||||
.header("sec-ch-ua-platform", "\"Windows\"")
|
||||
.header("sec-fetch-dest", "iframe")
|
||||
.header("sec-fetch-mode", "navigate")
|
||||
.header("sec-fetch-site", "cross-site")
|
||||
.header("upgrade-insecure-requests", "1")
|
||||
.header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537" +
|
||||
".36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("badger_token=" + token))
|
||||
.build();
|
||||
|
||||
// 发起请求并获取响应
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
// 返回响应体
|
||||
promise.complete(response.body());
|
||||
return null;
|
||||
} catch (URISyntaxException | IOException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
221
parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java
Normal file
221
parser/src/main/java/cn/qaiu/parser/impl/PvyyTool.java
Normal file
@@ -0,0 +1,221 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.WebClientVertxInit;
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.FileSizeConverter;
|
||||
import cn.qaiu.util.HeaderUtils;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.json.pointer.JsonPointer;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 微雨云
|
||||
*/
|
||||
public class PvyyTool extends PanBase {
|
||||
private static final String API_URL_PREFIX1 = "https://www.vyuyun.com/apiv1/share/file/{key}?password={pwd}";
|
||||
private static final String API_URL_PREFIX2 = "https://www.vyuyun.com/apiv1/share/getShareDownUrl/{key}/{id}?password={pwd}";
|
||||
|
||||
byte[] hexArray = {
|
||||
0x68, 0x74, 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x31, 0x31, 0x36, 0x2e, 0x32, 0x30, 0x35, 0x2e,
|
||||
0x39, 0x36, 0x2e, 0x31, 0x39, 0x38, 0x3a, 0x33, 0x30, 0x30, 0x30, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x2f
|
||||
};
|
||||
|
||||
private static final MultiMap header = HeaderUtils.parseHeaders("""
|
||||
accept-language: zh-CN,zh;q=0.9,en;q=0.8
|
||||
cache-control: no-cache
|
||||
dnt: 1
|
||||
origin: https://www.vyuyun.com
|
||||
pragma: no-cache
|
||||
priority: u=1, i
|
||||
referer: https://www.vyuyun.com/
|
||||
sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "Windows"
|
||||
sec-fetch-dest: empty
|
||||
sec-fetch-mode: cors
|
||||
sec-fetch-site: same-site
|
||||
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36
|
||||
""");
|
||||
|
||||
private final String api;
|
||||
public PvyyTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
api = new String(hexArray);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
// 请求downcode
|
||||
WebClient.create(WebClientVertxInit.get())
|
||||
.getAbs(api + shareLinkInfo.getShareKey())
|
||||
.send()
|
||||
.onSuccess(res -> {
|
||||
if (res.statusCode() == 200) {
|
||||
String code = res.bodyAsString();
|
||||
log.info("vyy url:{}, code:{}", shareLinkInfo.getStandardUrl(), code);
|
||||
String downApi = API_URL_PREFIX2 + "&downcode=" + code;
|
||||
getDownUrl(downApi);
|
||||
} else {
|
||||
fail("code获取失败");
|
||||
}
|
||||
}).onFailure(handleFail("code服务异常"));
|
||||
return future();
|
||||
}
|
||||
|
||||
private void getDownUrl(String apiUrl) {
|
||||
client.getAbs(UriTemplate.of(API_URL_PREFIX1))
|
||||
.setTemplateParam("key", shareLinkInfo.getShareKey())
|
||||
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||
.putHeaders(header)
|
||||
.send().onSuccess(res -> {
|
||||
try {
|
||||
JsonObject resJson = asJson(res);
|
||||
if (!resJson.containsKey("code") || resJson.getInteger("code") != 0) {
|
||||
fail("获取文件信息失败: " + resJson.getString("message"));
|
||||
return;
|
||||
}
|
||||
JsonObject fileData = resJson.getJsonObject("data").getJsonObject("data");
|
||||
if (fileData == null) {
|
||||
fail("文件数据为空");
|
||||
return;
|
||||
}
|
||||
setFileInfo(fileData);
|
||||
String id = fileData.getString("id");
|
||||
|
||||
client.getAbs(UriTemplate.of(apiUrl))
|
||||
.setTemplateParam("key", shareLinkInfo.getShareKey())
|
||||
.setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||
.setTemplateParam("id", id)
|
||||
.putHeaders(header).send().onSuccess(res2 -> {
|
||||
try {
|
||||
// data->downInfo->url
|
||||
String url =
|
||||
asJson(res2).getJsonObject("data").getJsonObject("downInfo").getString("url");
|
||||
complete(url);
|
||||
} catch (Exception ignored) {
|
||||
fail(asJson(res2).encodePrettily());
|
||||
}
|
||||
});
|
||||
} catch (Exception ignored) {
|
||||
fail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setFileInfo(JsonObject fileData) {
|
||||
JsonObject attributes = fileData.getJsonObject("attributes");
|
||||
JsonObject user = (JsonObject)(JsonPointer.from("/relationships/user/data").queryJson(fileData));
|
||||
int downCount = (Integer)(JsonPointer.from("/relationships/shared/data/attributes/down").queryJson(fileData));
|
||||
String filesize = attributes.getString("filesize");
|
||||
FileInfo fileInfo = new FileInfo()
|
||||
.setFileId(fileData.getString("id"))
|
||||
.setFileName(attributes.getString("basename"))
|
||||
.setFileType(attributes.getString("mimetype"))
|
||||
.setPanType(shareLinkInfo.getType())
|
||||
.setCreateBy(user.getString("email"))
|
||||
.setDownloadCount(downCount)
|
||||
.setSize(FileSizeConverter.convertToBytes(filesize))
|
||||
.setSizeStr(filesize);
|
||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||
}
|
||||
|
||||
private static final String DIR_API = "https://www.vyuyun.com/apiv1/share/folders/809Pt6/bMjnUg?sort=created_at&direction=DESC&password={pwd}";
|
||||
private static final String SHARE_TYPE_API = "https://www.vyuyun.com/apiv1/share/info/{key}?password={pwd}";
|
||||
//
|
||||
// @Override
|
||||
// public Future<List<FileInfo>> parseFileList() {
|
||||
// Promise<List<FileInfo>> promise = Promise.promise();
|
||||
// client.getAbs(UriTemplate.of(SHARE_TYPE_API))
|
||||
// .setTemplateParam("key", shareLinkInfo.getShareKey())
|
||||
// .setTemplateParam("pwd", shareLinkInfo.getSharePassword()).send().onSuccess(res -> {
|
||||
// // "data" -> "attributes"->type
|
||||
// String type = asJson(res).getJsonObject("data").getJsonObject("attributes").getString("type");
|
||||
// if ("folder".equals(type)) {
|
||||
// // 文件夹
|
||||
// client.getAbs(UriTemplate.of(DIR_API))
|
||||
// .setTemplateParam("key", shareLinkInfo.getShareKey())
|
||||
// .setTemplateParam("pwd", shareLinkInfo.getSharePassword())
|
||||
// .send().onSuccess(res2 -> { try {
|
||||
//
|
||||
// try {
|
||||
// // 新的解析逻辑
|
||||
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
|
||||
// List<FileInfo> list = arr.stream().map(o -> {
|
||||
// FileInfo fileInfo = new FileInfo();
|
||||
// var jo = ((io.vertx.core.json.JsonObject) o).getJsonObject("data");
|
||||
// String fileType = jo.getString("type");
|
||||
// fileInfo.setFileId(jo.getString("id"));
|
||||
// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name"));
|
||||
// // 文件大小可能为null或字符串
|
||||
// Object sizeObj = jo.getJsonObject("attributes").getValue("filesize");
|
||||
// if (sizeObj instanceof Number) {
|
||||
// fileInfo.setSize(((Number) sizeObj).longValue());
|
||||
// } else if (sizeObj instanceof String sizeStr) {
|
||||
// try {
|
||||
// getSize(fileInfo, sizeStr);
|
||||
// } catch (Exception e) {
|
||||
// fileInfo.setSize(0L);
|
||||
// }
|
||||
// } else {
|
||||
// fileInfo.setSize(0L);
|
||||
// }
|
||||
// fileInfo.setFileType("folder".equals(fileType) ? "folder" : "file");
|
||||
// return fileInfo;
|
||||
// }).toList();
|
||||
// promise.complete(list);
|
||||
// } catch (Exception ignored) {
|
||||
// promise.fail(asJson(res2).encodePrettily());
|
||||
// }
|
||||
// }).onFailure(t->{
|
||||
// promise.fail("获取文件夹内容失败: " + t.getMessage());
|
||||
// });
|
||||
// } else if ("file".equals(type)) {
|
||||
// // 单文件
|
||||
// FileInfo fileInfo = new FileInfo();
|
||||
// var jo = asJson(res).getJsonObject("data").getJsonObject("attributes");
|
||||
// fileInfo.setFileId(asJson(res).getJsonObject("data").getString("id"));
|
||||
// fileInfo.setFileName(jo.getString("name"));
|
||||
// Object sizeObj = jo.getValue("filesize");
|
||||
// if (sizeObj instanceof Number) {
|
||||
// fileInfo.setSize(((Number) sizeObj).longValue());
|
||||
// } else if (sizeObj instanceof String sizeStr) {
|
||||
// try {
|
||||
// getSize(fileInfo, sizeStr);
|
||||
// } catch (Exception e) {
|
||||
// fileInfo.setSize(0L);
|
||||
// }
|
||||
// } else {
|
||||
// fileInfo.setSize(0L);
|
||||
// }
|
||||
// fileInfo.setFileType("file");
|
||||
// promise.complete(List.of(fileInfo));
|
||||
// } else {
|
||||
// promise.fail("未知的分享类型");
|
||||
// }
|
||||
// });
|
||||
// return promise.future();
|
||||
// }
|
||||
//
|
||||
// private void getSize(FileInfo fileInfo, String sizeStr) {
|
||||
// if (sizeStr.endsWith("KB")) {
|
||||
// fileInfo.setSize(Long.parseLong(sizeStr.replace("KB", "").trim()) * 1024);
|
||||
// } else if (sizeStr.endsWith("MB")) {
|
||||
// fileInfo.setSize(Long.parseLong(sizeStr.replace("MB", "").trim()) * 1024 * 1024);
|
||||
// } else {
|
||||
// fileInfo.setSize(Long.parseLong(sizeStr));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Future<String> parseById() {
|
||||
// return super.parseById();
|
||||
// }
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.StringUtils;
|
||||
import io.netty.handler.codec.http.QueryStringDecoder;
|
||||
@@ -15,7 +14,7 @@ import java.util.Map;
|
||||
/**
|
||||
* <a href="https://wx.mail.qq.com/">QQ邮箱</a>
|
||||
*/
|
||||
public class QQTool extends PanBase implements IPanTool {
|
||||
public class QQTool extends PanBase {
|
||||
|
||||
public static final String REDIRECT_URL_TEMP = "https://iwx.mail.qq.com/ftn/download?func=4&key={key}&code={code}";
|
||||
|
||||
|
||||
171
parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
Normal file
171
parser/src/main/java/cn/qaiu/parser/impl/QQscTool.java
Normal file
@@ -0,0 +1,171 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.HeaderUtils;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* QQ闪传 <br>
|
||||
* 只能客户端上传 支持Android QQ 9.2.5, MACOS QQ 6.9.78,可生成分享链接,通过浏览器下载,支持超大文件,有效期默认7天(暂时没找到续期方法)。<br>
|
||||
*/
|
||||
public class QQscTool extends PanBase {
|
||||
|
||||
Logger LOG = LoggerFactory.getLogger(QQscTool.class);
|
||||
|
||||
private static final String API_URL = "https://qfile.qq.com/http2rpc/gotrpc/noauth/trpc.qqntv2.richmedia.InnerProxy/BatchDownload";
|
||||
|
||||
private static final MultiMap HEADERS = HeaderUtils.parseHeaders("""
|
||||
Accept-Encoding: gzip, deflate
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Connection: keep-alive
|
||||
Cookie: uin=9000002; p_uin=9000002
|
||||
DNT: 1
|
||||
Origin: https://qfile.qq.com
|
||||
Referer: https://qfile.qq.com/q/Xolxtv5b4O
|
||||
Sec-Fetch-Dest: empty
|
||||
Sec-Fetch-Mode: cors
|
||||
Sec-Fetch-Site: same-origin
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0
|
||||
accept: application/json
|
||||
content-type: application/json
|
||||
sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"
|
||||
sec-ch-ua-mobile: ?0
|
||||
sec-ch-ua-platform: "macOS"
|
||||
x-oidb: {"uint32_command":"0x9248", "uint32_service_type":"4"}
|
||||
""");
|
||||
|
||||
public QQscTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
String jsonTemplate = """
|
||||
{"req_head":{"agent":8},"download_info":[{"batch_id":"%s","scene":{"business_type":4,"app_type":22,"scene_type":5},"index_node":{"file_uuid":"%s"},"url_type":2,"download_scene":0}],"scene_type":103}
|
||||
""";
|
||||
|
||||
client.getAbs(shareLinkInfo.getShareUrl()).send(result -> {
|
||||
if (result.succeeded()) {
|
||||
String htmlJs = result.result().bodyAsString();
|
||||
LOG.debug("获取到的HTML内容: {}", htmlJs);
|
||||
String fileUUID = getFileUUID(htmlJs);
|
||||
String fileName = extractFileNameFromTitle(htmlJs);
|
||||
if (fileName != null) {
|
||||
LOG.info("提取到的文件名: {}", fileName);
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setFileName(fileName);
|
||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||
} else {
|
||||
LOG.warn("未能提取到文件名");
|
||||
}
|
||||
if (fileUUID != null) {
|
||||
LOG.info("提取到的文件UUID: {}", fileUUID);
|
||||
String formatted = jsonTemplate.formatted(fileUUID, fileUUID);
|
||||
JsonObject entries = new JsonObject(formatted);
|
||||
client.postAbs(API_URL)
|
||||
.putHeaders(HEADERS)
|
||||
.sendJsonObject(entries)
|
||||
.onSuccess(result2 -> {
|
||||
if (result2.statusCode() == 200) {
|
||||
JsonObject body = asJson(result2);
|
||||
LOG.debug("API响应内容: {}", body.encodePrettily());
|
||||
// {
|
||||
// "retcode": 0,
|
||||
// "cost": 132,
|
||||
// "message": "",
|
||||
// "error": {
|
||||
// "message": "",
|
||||
// "code": 0
|
||||
// },
|
||||
// "data": {
|
||||
// "download_rsp": [{
|
||||
|
||||
// 取 download_rsp
|
||||
if (!body.containsKey("retcode") || body.getInteger("retcode") != 0) {
|
||||
promise.fail("API请求失败,错误信息: " + body.encodePrettily());
|
||||
return;
|
||||
}
|
||||
JsonArray downloadRsp = body.getJsonObject("data").getJsonArray("download_rsp");
|
||||
if (downloadRsp != null && !downloadRsp.isEmpty()) {
|
||||
String url = downloadRsp.getJsonObject(0).getString("url");
|
||||
if (fileName != null) {
|
||||
url = url + "&filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8);
|
||||
}
|
||||
promise.complete(url);
|
||||
} else {
|
||||
promise.fail("API响应中缺少 download_rsp");
|
||||
}
|
||||
} else {
|
||||
promise.fail("API请求失败,状态码: " + result2.statusCode());
|
||||
}
|
||||
}).onFailure(e -> {
|
||||
LOG.error("API请求异常", e);
|
||||
promise.fail(e);
|
||||
});
|
||||
} else {
|
||||
LOG.error("未能提取到文件UUID");
|
||||
promise.fail("未能提取到文件UUID");
|
||||
}
|
||||
} else {
|
||||
LOG.error("请求失败: {}", result.cause().getMessage());
|
||||
promise.fail(result.cause());
|
||||
}
|
||||
});
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
String getFileUUID(String htmlJs) {
|
||||
String keyword = "\"download_limit_status\"";
|
||||
String marker = "},\"";
|
||||
|
||||
int startIndex = htmlJs.indexOf(keyword);
|
||||
if (startIndex != -1) {
|
||||
int markerIndex = htmlJs.indexOf(marker, startIndex);
|
||||
if (markerIndex != -1) {
|
||||
int quoteStart = markerIndex + marker.length();
|
||||
int quoteEnd = htmlJs.indexOf("\"", quoteStart);
|
||||
if (quoteEnd != -1) {
|
||||
String extracted = htmlJs.substring(quoteStart, quoteEnd);
|
||||
LOG.debug("提取结果: {}", extracted);
|
||||
return extracted;
|
||||
} else {
|
||||
LOG.error("未找到结束引号: {}", marker);
|
||||
}
|
||||
} else {
|
||||
LOG.error("未找到标记: {} 在关键字: {} 之后", marker, keyword);
|
||||
}
|
||||
} else {
|
||||
LOG.error("未找到关键字: {}", keyword);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String extractFileNameFromTitle(String content) {
|
||||
// 匹配<title>和</title>之间的内容
|
||||
Pattern pattern = Pattern.compile("<title>(.*?)</title>");
|
||||
Matcher matcher = pattern.matcher(content);
|
||||
if (matcher.find()) {
|
||||
String fullTitle = matcher.group(1);
|
||||
// 按 "|" 分割,取前半部分
|
||||
int sepIndex = fullTitle.indexOf("|");
|
||||
if (sepIndex != -1) {
|
||||
return fullTitle.substring(0, sepIndex);
|
||||
}
|
||||
return fullTitle; // 如果没有分隔符,就返回全部
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
69
parser/src/main/java/cn/qaiu/parser/impl/QQwTool.java
Normal file
69
parser/src/main/java/cn/qaiu/parser/impl/QQwTool.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import io.vertx.core.Future;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class QQwTool extends QQTool {
|
||||
|
||||
public QQwTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
client.getAbs(shareLinkInfo.getShareUrl()).send().onSuccess(res -> {
|
||||
String html = res.bodyAsString();
|
||||
Map<String, String> stringStringMap = extractVariables(html);
|
||||
String url = stringStringMap.get("url");
|
||||
String fn = stringStringMap.get("filename");
|
||||
String size = stringStringMap.get("filesize");
|
||||
String createBy = stringStringMap.get("nick");
|
||||
FileInfo fileInfo = new FileInfo().setFileName(fn).setSize(Long.parseLong(size)).setCreateBy(createBy);
|
||||
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
|
||||
if (url != null) {
|
||||
String url302 = url.replace("\\x26", "&");
|
||||
promise.complete(url302);
|
||||
|
||||
/*
|
||||
clientNoRedirects.getAbs(url302).send().onSuccess(res2 -> {
|
||||
MultiMap headers = res2.headers();
|
||||
if (headers.contains("Location")) {
|
||||
promise.complete(headers.get("Location"));
|
||||
} else {
|
||||
fail("找不到重定向URL");
|
||||
}
|
||||
|
||||
}).onFailure(handleFail());
|
||||
*/
|
||||
} else {
|
||||
fail("分享链接解析失败, 可能是链接失效");
|
||||
}
|
||||
}).onFailure(handleFail());
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
|
||||
private Map<String, String> extractVariables(String jsCode) {
|
||||
Map<String, String> variables = new HashMap<>();
|
||||
// 正则表达式匹配 var 变量定义
|
||||
String regex = "\\s+var\\s+(\\w+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^;\\r\\n]*))";
|
||||
Pattern p = Pattern.compile(regex);
|
||||
Matcher m = p.matcher(jsCode);
|
||||
|
||||
while (m.find()) {
|
||||
String name = m.group(1);
|
||||
String value = m.group(2) != null ? m.group(2)
|
||||
: m.group(3) != null ? m.group(3)
|
||||
: m.group(4);
|
||||
variables.put(name, value);
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class QkTool extends PanBase implements IPanTool {
|
||||
public class QkTool extends PanBase {
|
||||
|
||||
public QkTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
|
||||
122
parser/src/main/java/cn/qaiu/parser/impl/UcTool.java
Normal file
122
parser/src/main/java/cn/qaiu/parser/impl/UcTool.java
Normal file
@@ -0,0 +1,122 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
|
||||
/**
|
||||
* UC网盘解析
|
||||
*/
|
||||
public class UcTool extends PanBase {
|
||||
private static final String API_URL_PREFIX = "https://pc-api.uc.cn/1/clouddrive/";
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://fast.uc.cn/s/";
|
||||
|
||||
private static final String FIRST_REQUEST_URL = API_URL_PREFIX + "share/sharepage/token?entry=ft&fr=pc&pr" +
|
||||
"=UCBrowser";
|
||||
|
||||
private static final String SECOND_REQUEST_URL = API_URL_PREFIX + "transfer_share/detail?pwd_id={pwd_id}&passcode" +
|
||||
"={passcode}&stoken={stoken}";
|
||||
|
||||
private static final String THIRD_REQUEST_URL = API_URL_PREFIX + "file/download?entry=ft&fr=pc&pr=UCBrowser";
|
||||
|
||||
public UcTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
var dataKey = shareLinkInfo.getShareKey();
|
||||
var passcode = shareLinkInfo.getSharePassword();
|
||||
|
||||
var jsonObject = JsonObject.of("share_for_transfer", true);
|
||||
jsonObject.put("pwd_id", dataKey);
|
||||
jsonObject.put("passcode", passcode);
|
||||
// 第一次请求 获取文件信息
|
||||
client.postAbs(FIRST_REQUEST_URL).sendJsonObject(jsonObject).onSuccess(res -> {
|
||||
log.debug("第一阶段 {}", res.body());
|
||||
var resJson = res.bodyAsJsonObject();
|
||||
if (resJson.getInteger("code") != 0) {
|
||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson);
|
||||
return;
|
||||
}
|
||||
var stoken = resJson.getJsonObject("data").getString("stoken");
|
||||
// 第二次请求
|
||||
client.getAbs(UriTemplate.of(SECOND_REQUEST_URL))
|
||||
.setTemplateParam("pwd_id", dataKey)
|
||||
.setTemplateParam("passcode", passcode)
|
||||
.setTemplateParam("stoken", stoken)
|
||||
.send().onSuccess(res2 -> {
|
||||
log.debug("第二阶段 {}", res2.body());
|
||||
JsonObject resJson2 = res2.bodyAsJsonObject();
|
||||
if (resJson2.getInteger("code") != 0) {
|
||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson2);
|
||||
return;
|
||||
}
|
||||
// 文件信息
|
||||
var info = resJson2.getJsonObject("data").getJsonArray("list").getJsonObject(0);
|
||||
// 第二次请求
|
||||
var bodyJson = JsonObject.of()
|
||||
.put("fids", JsonArray.of(info.getString("fid")))
|
||||
.put("pwd_id", dataKey)
|
||||
.put("stoken", stoken)
|
||||
.put("fids_token", JsonArray.of(info.getString("share_fid_token")));
|
||||
client.postAbs(THIRD_REQUEST_URL).sendJsonObject(bodyJson)
|
||||
.onSuccess(res3 -> {
|
||||
log.debug("第三阶段 {}", res3.body());
|
||||
var resJson3 = res3.bodyAsJsonObject();
|
||||
if (resJson3.getInteger("code") != 0) {
|
||||
fail(FIRST_REQUEST_URL + " 返回异常: " + resJson2);
|
||||
return;
|
||||
}
|
||||
promise.complete(resJson3.getJsonArray("data").getJsonObject(0).getString("download_url"));
|
||||
}).onFailure(handleFail(THIRD_REQUEST_URL));
|
||||
|
||||
}).onFailure(handleFail(SECOND_REQUEST_URL));
|
||||
}
|
||||
).onFailure(handleFail(FIRST_REQUEST_URL));
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
// https://dl-uf-zb.pds.uc.cn/l3PNAKfz/64623447/
|
||||
// 646b0de6e9f13000c9b14ba182b805312795a82a/
|
||||
// 646b0de6717e1bfa5bb44dd2a456f103c5177850?
|
||||
// Expires=1737784900&OSSAccessKeyId=LTAI5tJJpWQEfrcKHnd1LqsZ&
|
||||
// Signature=oBVV3anhv3tBKanHUcEIsktkB%2BM%3D&x-oss-traffic-limit=503316480
|
||||
// &response-content-disposition=attachment%3B%20filename%3DC%2523%2520Shell%2520%2528C%2523%2520Offline%2520Compiler%2529_2.5.16.apks
|
||||
// %3Bfilename%2A%3Dutf-8%27%27C%2523%2520Shell%2520%2528C%2523%2520Offline%2520Compiler%2529_2.5.16.apks
|
||||
|
||||
//eyJ4OmF1IjoiLSIsIng6dWQiOiI0LU4tNS0wLTYtTi0zLWZ0LTAtMi1OLU4iLCJ4OnNwIjoiMTAwIiwieDp0b2tlbiI6IjQtZjY0ZmMxMDFjZmQxZGVkNTRkMGM0NmMzYzliMzkyOWYtNS03LTE1MzYxMS1kYWNiMzY2NWJiYWE0ZjVlOWQzNzgwMGVjNjQwMzE2MC0wLTAtMC0wLTQ5YzUzNTE3OGIxOTY0YzhjYzUwYzRlMDk5MTZmYWRhIiwieDp0dGwiOiIxMDgwMCJ9
|
||||
//eyJjYWxsYmFja0JvZHlUeXBlIjoiYXBwbGljYXRpb24vanNvbiIsImNhbGxiYWNrU3RhZ2UiOiJiZWZvcmUtZXhlY3V0ZSIsImNhbGxiYWNrRmFpbHVyZUFjdGlvbiI6Imlnbm9yZSIsImNhbGxiYWNrVXJsIjoiaHR0cHM6Ly9hdXRoLWNkbi51Yy5jbi9vdXRlci9vc3MvY2hlY2twbGF5IiwiY2FsbGJhY2tCb2R5Ijoie1wiaG9zdFwiOiR7aHR0cEhlYWRlci5ob3N0fSxcInNpemVcIjoke3NpemV9LFwicmFuZ2VcIjoke2h0dHBIZWFkZXIucmFuZ2V9LFwicmVmZXJlclwiOiR7aHR0cEhlYWRlci5yZWZlcmVyfSxcImNvb2tpZVwiOiR7aHR0cEhlYWRlci5jb29raWV9LFwibWV0aG9kXCI6JHtodHRwSGVhZGVyLm1ldGhvZH0sXCJpcFwiOiR7Y2xpZW50SXB9LFwicG9ydFwiOiR7Y2xpZW50UG9ydH0sXCJvYmplY3RcIjoke29iamVjdH0sXCJzcFwiOiR7eDpzcH0sXCJ1ZFwiOiR7eDp1ZH0sXCJ0b2tlblwiOiR7eDp0b2tlbn0sXCJhdVwiOiR7eDphdX0sXCJ0dGxcIjoke3g6dHRsfSxcImR0X3NwXCI6JHt4OmR0X3NwfSxcImhzcFwiOiR7eDpoc3B9LFwiY2xpZW50X3Rva2VuXCI6JHtxdWVyeVN0cmluZy5jbGllbnRfdG9rZW59fSJ9
|
||||
//callback-var {"x:au":"-","x:ud":"4-N-5-0-6-N-3-ft-0-2-N-N","x:sp":"100","x:token":"4-f64fc101cfd1ded54d0c46c3c9b3929f-5-7-153611-dacb3665bbaa4f5e9d37800ec6403160-0-0-0-0-49c535178b1964c8cc50c4e09916fada","x:ttl":"10800"}
|
||||
//callback {"callbackBodyType":"application/json","callbackStage":"before-execute","callbackFailureAction":"ignore","callbackUrl":"https://auth-cdn.uc.cn/outer/oss/checkplay","callbackBody":"{\"host\":${httpHeader.host},\"size\":${size},\"range\":${httpHeader.range},\"referer\":${httpHeader.referer},\"cookie\":${httpHeader.cookie},\"method\":${httpHeader.method},\"ip\":${clientIp},\"port\":${clientPort},\"object\":${object},\"sp\":${x:sp},\"ud\":${x:ud},\"token\":${x:token},\"au\":${x:au},\"ttl\":${x:ttl},\"dt_sp\":${x:dt_sp},\"hsp\":${x:hsp},\"client_token\":${queryString.client_token}}"}
|
||||
|
||||
/*
|
||||
// callback-var
|
||||
{
|
||||
"x:au": "-",
|
||||
"x:ud": "4-N-5-0-6-N-3-ft-0-2-N-N",
|
||||
"x:sp": "100",
|
||||
"x:token": "4-f64fc101cfd1ded54d0c46c3c9b3929f-5-7-153611-dacb3665bbaa4f5e9d37800ec6403160-0-0-0-0-49c535178b1964c8cc50c4e09916fada",
|
||||
"x:ttl": "10800"
|
||||
}
|
||||
|
||||
// callback
|
||||
{
|
||||
"callbackBodyType": "application/json",
|
||||
"callbackStage": "before-execute",
|
||||
"callbackFailureAction": "ignore",
|
||||
"callbackUrl": "https://auth-cdn.uc.cn/outer/oss/checkplay",
|
||||
"callbackBody": "{\"host\":${httpHeader.host},\"size\":${size},\"range\":${httpHeader.range},\"referer\":${httpHeader.referer},\"cookie\":${httpHeader.cookie},\"method\":${httpHeader.method},\"ip\":${clientIp},\"port\":${clientPort},\"object\":${object},\"sp\":${x:sp},\"ud\":${x:ud},\"token\":${x:token},\"au\":${x:au},\"ttl\":${x:ttl},\"dt_sp\":${x:dt_sp},\"hsp\":${x:hsp},\"client_token\":${queryString.client_token}}"
|
||||
}
|
||||
*/
|
||||
|
||||
new UcTool(ShareLinkInfo.newBuilder().shareUrl("https://fast.uc.cn/s/33197dd53ace4").shareKey("33197dd53ace4").build()).parse().onSuccess(
|
||||
System.out::println
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
@@ -15,7 +14,7 @@ import java.nio.charset.StandardCharsets;
|
||||
/**
|
||||
* <a href="https://www.wenshushu.cn/">文叔叔</a>
|
||||
*/
|
||||
public class WsTool extends PanBase implements IPanTool {
|
||||
public class WsTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "www.wenshushu.cn/f/";
|
||||
public static final String SHARE_URL_API = "https://www.wenshushu.cn/ap/";
|
||||
@@ -50,7 +49,7 @@ public class WsTool extends PanBase implements IPanTool {
|
||||
if (res.statusCode() == 200) {
|
||||
try {
|
||||
// 设置匿名登录token
|
||||
String token = res.bodyAsJsonObject().getJsonObject("data").getString("token");
|
||||
String token = asJson(res).getJsonObject("data").getString("token");
|
||||
headers.set("X-Token", token);
|
||||
|
||||
// 获取文件夹信息
|
||||
@@ -63,10 +62,10 @@ public class WsTool extends PanBase implements IPanTool {
|
||||
if (res2.statusCode() == 200) {
|
||||
try {
|
||||
// 获取文件夹信息
|
||||
String filetime = res2.bodyAsJsonObject().getJsonObject("data").getString("expire"); // 文件夹剩余时间
|
||||
String filesize = res2.bodyAsJsonObject().getJsonObject("data").getString("file_size"); // 文件夹大小
|
||||
String filepid = res2.bodyAsJsonObject().getJsonObject("data").getString("ufileid"); // 文件夹pid
|
||||
String filebid = res2.bodyAsJsonObject().getJsonObject("data").getString("boxid"); // 文件夹bid
|
||||
String filetime = asJson(res2).getJsonObject("data").getString("expire"); // 文件夹剩余时间
|
||||
String filesize = asJson(res2).getJsonObject("data").getString("file_size"); // 文件夹大小
|
||||
String filepid = asJson(res2).getJsonObject("data").getString("ufileid"); // 文件夹pid
|
||||
String filebid = asJson(res2).getJsonObject("data").getString("boxid"); // 文件夹bid
|
||||
|
||||
// 调试输出文件夹信息
|
||||
System.out.println("文件夹期限: " + filetime);
|
||||
@@ -93,9 +92,9 @@ public class WsTool extends PanBase implements IPanTool {
|
||||
if (res3.statusCode() == 200) {
|
||||
try {
|
||||
// 获取文件信息
|
||||
String filename = res3.bodyAsJsonObject().getJsonObject("data")
|
||||
String filename = asJson(res3).getJsonObject("data")
|
||||
.getJsonArray("fileList").getJsonObject(0).getString("fname"); // 文件名称
|
||||
String filefid = res3.bodyAsJsonObject().getJsonObject("data")
|
||||
String filefid = asJson(res3).getJsonObject("data")
|
||||
.getJsonArray("fileList").getJsonObject(0).getString("fid"); // 文件fid
|
||||
|
||||
// 调试输出文件信息
|
||||
@@ -113,7 +112,7 @@ public class WsTool extends PanBase implements IPanTool {
|
||||
if (res4.statusCode() == 200) {
|
||||
try {
|
||||
// 获取直链
|
||||
String fileurl = res4.bodyAsJsonObject().getJsonObject("data").getString("url");
|
||||
String fileurl = asJson(res4).getJsonObject("data").getString("url");
|
||||
|
||||
// 调试输出文件直链
|
||||
System.out.println("文件直链: " + fileurl);
|
||||
|
||||
@@ -1,38 +1,67 @@
|
||||
package cn.qaiu.parser.impl;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.IPanTool;
|
||||
import cn.qaiu.parser.PanBase;
|
||||
import cn.qaiu.util.CommonUtils;
|
||||
import cn.qaiu.util.FileSizeConverter;
|
||||
import cn.qaiu.util.JsExecUtils;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.json.JsonArray;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.core.json.pointer.JsonPointer;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.uritemplate.UriTemplate;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static cn.qaiu.util.RandomStringGenerator.gen36String;
|
||||
|
||||
/**
|
||||
* 123网盘
|
||||
*/
|
||||
public class YeTool extends PanBase implements IPanTool {
|
||||
public class YeTool extends PanBase {
|
||||
|
||||
public static final String SHARE_URL_PREFIX = "https://www.123pan.com/s/";
|
||||
public static final String FIRST_REQUEST_URL = SHARE_URL_PREFIX + "{key}.html";
|
||||
|
||||
private static final String GET_FILE_INFO_URL = "https://www.123pan.com/a/api/share/get?limit=100&next=1&orderBy" +
|
||||
"=file_name&orderDirection=asc" +
|
||||
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId=0&Page=1&event=homeListFile&operateType=1";
|
||||
"&shareKey={shareKey}&SharePwd={pwd}&ParentFileId={ParentFileId}&Page=1&event=homeListFile&operateType=1";
|
||||
private static final String DOWNLOAD_API_URL = "https://www.123pan.com/a/api/share/download/info?{authK}={authV}";
|
||||
|
||||
private static final String BATCH_DOWNLOAD_API_URL = "https://www.123pan.com/b/api/file/batch_download_share_info?{authK}={authV}";
|
||||
private final MultiMap header = MultiMap.caseInsensitiveMultiMap();
|
||||
|
||||
public YeTool(ShareLinkInfo shareLinkInfo) {
|
||||
super(shareLinkInfo);
|
||||
header.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
|
||||
header.set("App-Version", "3");
|
||||
header.set("Cache-Control", "no-cache");
|
||||
header.set("Connection", "keep-alive");
|
||||
//header.set("DNT", "1");
|
||||
//header.set("Host", "www.123pan.com");
|
||||
header.set("LoginUuid", gen36String());
|
||||
header.set("Pragma", "no-cache");
|
||||
header.set("Referer", shareLinkInfo.getStandardUrl());
|
||||
header.set("Sec-Fetch-Dest", "empty");
|
||||
header.set("Sec-Fetch-Mode", "cors");
|
||||
header.set("Sec-Fetch-Site", "same-origin");
|
||||
header.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0");
|
||||
header.set("platform", "web");
|
||||
header.set("sec-ch-ua", "\"Not)A;Brand\";v=\"99\", \"Microsoft Edge\";v=\"127\", \"Chromium\";v=\"127\"");
|
||||
header.set("sec-ch-ua-mobile", "?0");
|
||||
header.set("sec-ch-ua-platform", "Windows");
|
||||
}
|
||||
|
||||
public Future<String> parse() {
|
||||
@@ -43,11 +72,17 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
client.getAbs(UriTemplate.of(FIRST_REQUEST_URL)).setTemplateParam("key", dataKey).send().onSuccess(res -> {
|
||||
|
||||
String html = res.bodyAsString();
|
||||
// 判断分享是否已经失效
|
||||
if (html.contains("分享链接已失效")) {
|
||||
fail("该分享已失效({})已失效", shareLinkInfo.getShareUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
Pattern compile = Pattern.compile("window.g_initialProps\\s*=\\s*(.*);");
|
||||
Matcher matcher = compile.matcher(html);
|
||||
|
||||
if (!matcher.find()) {
|
||||
fail(html + "\n Ye: " + dataKey + " 正则匹配失败");
|
||||
fail("该分享({})文件信息找不到, 可能分享已失效", shareLinkInfo.getShareUrl());
|
||||
return;
|
||||
}
|
||||
String fileInfoString = matcher.group(1);
|
||||
@@ -60,24 +95,40 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
return;
|
||||
}
|
||||
String shareKey = resJson.getJsonObject("data").getString("ShareKey");
|
||||
|
||||
if (resListJson == null || resListJson.getInteger("code") != 0) {
|
||||
// 加密分享
|
||||
if (StringUtils.isNotEmpty(pwd)) {
|
||||
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
|
||||
.setTemplateParam("shareKey", shareKey)
|
||||
.setTemplateParam("pwd", pwd)
|
||||
.setTemplateParam("ParentFileId", "0")
|
||||
// .setTemplateParam("authKey", AESUtils.getAuthKey("/a/api/share/get"))
|
||||
.putHeader("Platform", "web")
|
||||
.putHeader("App-Version", "3")
|
||||
.send().onSuccess(res2 -> {
|
||||
JsonObject infoJson = res2.bodyAsJsonObject();
|
||||
JsonObject infoJson = asJson(res2);
|
||||
if (infoJson.getInteger("code") != 0) {
|
||||
fail("{} 状态码异常 {}", dataKey, infoJson);
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject getFileInfoJson =
|
||||
infoJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
|
||||
getFileInfoJson.put("ShareKey", shareKey);
|
||||
|
||||
// 判断是否为文件夹: data->InfoList->0->Type: 1为文件夹, 0为文件
|
||||
try {
|
||||
int type = (Integer)JsonPointer.from("/data/InfoList/0/Type").queryJson(infoJson);
|
||||
if (type == 1) {
|
||||
getZipDownUrl(client, getFileInfoJson);
|
||||
return;
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
fail("该分享[{}]解析异常: {}", dataKey, exception.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
getDownUrl(client, getFileInfoJson);
|
||||
}).onFailure(this.handleFail(GET_FILE_INFO_URL));
|
||||
} else {
|
||||
@@ -88,6 +139,11 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
|
||||
JsonObject reqBodyJson = resListJson.getJsonObject("data").getJsonArray("InfoList").getJsonObject(0);
|
||||
reqBodyJson.put("ShareKey", shareKey);
|
||||
if (reqBodyJson.getInteger("Type") == 1) {
|
||||
// 文件夹
|
||||
getZipDownUrl(client, reqBodyJson);
|
||||
return;
|
||||
}
|
||||
getDownUrl(client, reqBodyJson);
|
||||
}).onFailure(this.handleFail(FIRST_REQUEST_URL));
|
||||
|
||||
@@ -106,6 +162,21 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
jsonObject.put("Etag", reqBodyJson.getString("Etag"));
|
||||
|
||||
// 调用JS文件获取签名
|
||||
down(client, jsonObject, DOWNLOAD_API_URL);
|
||||
}
|
||||
|
||||
|
||||
private void getZipDownUrl(WebClient client, JsonObject reqBodyJson) {
|
||||
log.info(reqBodyJson.encodePrettily());
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
// {"ShareKey":"LH3rTd-1ENed","fileIdList":[{"fileId":17525952}]}
|
||||
jsonObject.put("ShareKey", reqBodyJson.getString("ShareKey"));
|
||||
jsonObject.put("fileIdList", new JsonArray().add(JsonObject.of("fileId", reqBodyJson.getInteger("FileId"))));
|
||||
// 调用JS文件获取签名
|
||||
down(client, jsonObject, BATCH_DOWNLOAD_API_URL);
|
||||
}
|
||||
|
||||
private void down(WebClient client, JsonObject jsonObject, String api) {
|
||||
ScriptObjectMirror getSign;
|
||||
try {
|
||||
getSign = JsExecUtils.executeJs("getSign", "/a/api/share/download/info");
|
||||
@@ -115,13 +186,13 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
}
|
||||
log.info("ye getSign: {}={}", getSign.get("0").toString(), getSign.get("1").toString());
|
||||
|
||||
client.postAbs(UriTemplate.of(DOWNLOAD_API_URL))
|
||||
client.postAbs(UriTemplate.of(api))
|
||||
.setTemplateParam("authK", getSign.get("0").toString())
|
||||
.setTemplateParam("authV", getSign.get("1").toString())
|
||||
.putHeader("Platform", "web")
|
||||
.putHeader("App-Version", "3")
|
||||
.sendJsonObject(jsonObject).onSuccess(res2 -> {
|
||||
JsonObject downURLJson = res2.bodyAsJsonObject();
|
||||
JsonObject downURLJson = asJson(res2);
|
||||
|
||||
try {
|
||||
if (downURLJson.getInteger("code") != 0) {
|
||||
@@ -132,7 +203,8 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
fail("Ye: downURLJson格式异常->" + downURLJson);
|
||||
return;
|
||||
}
|
||||
String downURL = downURLJson.getJsonObject("data").getString("DownloadURL");
|
||||
String downURL = downURLJson.getJsonObject("data")
|
||||
.getString(api.contains("batch_download_share_info")? "DownloadUrl" : "DownloadURL");
|
||||
try {
|
||||
Map<String, String> urlParams = CommonUtils.getURLParams(downURL);
|
||||
String params = urlParams.get("params");
|
||||
@@ -141,7 +213,7 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
|
||||
// 获取直链
|
||||
client.getAbs(downUrl2).send().onSuccess(res3 -> {
|
||||
JsonObject res3Json = res3.bodyAsJsonObject();
|
||||
JsonObject res3Json = asJson(res3);
|
||||
try {
|
||||
if (res3Json.getInteger("code") != 0) {
|
||||
fail("Ye: downUrl2返回值异常->" + res3Json);
|
||||
@@ -161,4 +233,121 @@ public class YeTool extends PanBase implements IPanTool {
|
||||
}
|
||||
}).onFailure(this.handleFail(DOWNLOAD_API_URL));
|
||||
}
|
||||
|
||||
|
||||
// dir parser
|
||||
@Override
|
||||
public Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
|
||||
String shareKey = shareLinkInfo.getShareKey(); // 分享链接的唯一标识
|
||||
String pwd = shareLinkInfo.getSharePassword(); // 分享密码
|
||||
String parentFileId = "0"; // 根目录的文件ID
|
||||
String shareId = shareLinkInfo.getShareKey(); // String.valueOf(AESUtils.idEncrypt(dataKey));
|
||||
|
||||
// 如果参数里的目录ID不为空,则直接解析目录
|
||||
String dirId = (String) shareLinkInfo.getOtherParam().get("dirId");
|
||||
if (StringUtils.isNotBlank(dirId)) {
|
||||
parentFileId = dirId;
|
||||
}
|
||||
|
||||
|
||||
// 构造文件列表接口的URL
|
||||
client.getAbs(UriTemplate.of(GET_FILE_INFO_URL))
|
||||
.setTemplateParam("shareKey", shareKey)
|
||||
.setTemplateParam("pwd", pwd)
|
||||
.setTemplateParam("ParentFileId", parentFileId)
|
||||
.putHeaders(header)
|
||||
.send().onSuccess(res -> {
|
||||
JsonObject response = asJson(res);
|
||||
if (response.getInteger("code") != 0) {
|
||||
promise.fail("API错误: " + response.getString("message"));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray infoList = response.getJsonObject("data").getJsonArray("InfoList");
|
||||
List<FileInfo> result = new ArrayList<>();
|
||||
|
||||
// 遍历返回的文件和目录信息
|
||||
for (int i = 0; i < infoList.size(); i++) {
|
||||
JsonObject item = infoList.getJsonObject(i);
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
// "FileId": 16603582,
|
||||
// "FileName": "pdf",
|
||||
// "Type": 1,
|
||||
// "Size": 0,
|
||||
// "ContentType": "0",
|
||||
// "S3KeyFlag": "",
|
||||
// "CreateAt": "2025-07-09T06:56:20+08:00",
|
||||
// "UpdateAt": "2025-07-09T06:56:20+08:00",
|
||||
// "Etag": "",
|
||||
// "DownloadUrl": "",
|
||||
// "Status": 0,
|
||||
// "ParentFileId": 16603579,
|
||||
// "Category": 0,
|
||||
// "PunishFlag": 0,
|
||||
// "StorageNode": "m0",
|
||||
// "PreviewType": 0
|
||||
|
||||
// =>
|
||||
// {
|
||||
// "ShareKey":"iaKtVv-FTaCd",
|
||||
// "FileID":16604189,
|
||||
// "S3keyFlag":"1815268665-0",
|
||||
// "Size":425929,
|
||||
// "Etag":"70049de67075ab2b269c62d690424601",
|
||||
// "OrderId":""}
|
||||
JsonObject postData = JsonObject.of()
|
||||
.put("ShareKey", shareKey)
|
||||
.put("FileID", item.getInteger("FileId"))
|
||||
.put("S3keyFlag", item.getString("S3KeyFlag"))
|
||||
.put("Size", item.getLong("Size"))
|
||||
.put("Etag", item.getString("Etag"));
|
||||
|
||||
byte[] encode = Base64.getEncoder().encode(postData.encode().getBytes());
|
||||
String param = new String(encode);
|
||||
|
||||
if (item.getInteger("Type") == 0) { // 文件
|
||||
fileInfo.setFileName(item.getString("FileName"))
|
||||
.setFileId(item.getString("FileId"))
|
||||
.setFileType("file")
|
||||
.setSize(item.getLong("Size"))
|
||||
.setCreateTime(item.getString("CreateAt"))
|
||||
.setUpdateTime(item.getString("UpdateAt"))
|
||||
.setSizeStr(FileSizeConverter.convertToReadableSize(item.getLong("Size")))
|
||||
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), param))
|
||||
.setPreviewUrl(String.format("%s/v2/viewUrl/%s/%s", getDomainName(),
|
||||
shareLinkInfo.getType(), param));
|
||||
result.add(fileInfo);
|
||||
} else if (item.getInteger("Type") == 1) { // 目录
|
||||
fileInfo.setFileName(item.getString("FileName"))
|
||||
.setFileId(item.getString("FileId"))
|
||||
.setCreateTime(item.getString("CreateAt"))
|
||||
.setUpdateTime(item.getString("UpdateAt"))
|
||||
.setSize(0L)
|
||||
.setFileType("folder")
|
||||
.setParserUrl(
|
||||
String.format("%s/v2/getFileList?url=%s&dirId=%s&pwd=%s",
|
||||
getDomainName(),
|
||||
shareLinkInfo.getShareUrl(),
|
||||
item.getString("FileId"),
|
||||
pwd)
|
||||
);
|
||||
result.add(fileInfo);
|
||||
}
|
||||
}
|
||||
promise.complete(result);
|
||||
}).onFailure(promise::fail);
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parseById() {
|
||||
JsonObject paramJson = (JsonObject) shareLinkInfo.getOtherParam().get("paramJson");
|
||||
// 调用下载接口获取直链
|
||||
down(client, paramJson, DOWNLOAD_API_URL);
|
||||
return promise.future();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import io.vertx.core.net.impl.URIDecoder;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.crypto.*;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.*;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Random;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* AES加解密工具类
|
||||
@@ -39,6 +42,12 @@ public class AESUtils {
|
||||
public static final String CIPHER_AES0;
|
||||
public static final String CIPHER_AES0_IZ;
|
||||
|
||||
|
||||
public static final String MG_PKEY2 = "D8jg+H2iNX94zvHhRLnSM3oy59dH2QQjxQ0GgKJSL+mJclbCcItjV3AmkPY6WcbV4hNQk5+hN2J1eTrxPQqF4p28e3FTsGRCXVN80CLS+XqpFNY/9xuyf2bvbeq5JJU1IBCXgSZmEo8zu0/VGS3YNeDsHtjg92QSrRY8i4A+shihZBSz0/0KOL1VPd/K4tAYvsI9YjVFOI7z9mJJ8Ek8rVUplurJyGkjevRfvReN7xQ67PR+yZovk72yTZKlHDz5jVpLGLOy2iwTTSTbTvtnOi9TSE6sSPtRHv16cxZYZQY=";
|
||||
public static final String MG_PKEY;
|
||||
|
||||
public static final String MG_KEY = "4ea5c508a6566e76240543f8feb06fd457777be39549c4016436afda65d2330e";
|
||||
|
||||
/**
|
||||
* 秘钥长度
|
||||
*/
|
||||
@@ -63,6 +72,7 @@ public class AESUtils {
|
||||
try {
|
||||
CIPHER_AES0 = decryptByBase64AES(CIPHER_AES2, CIPHER_AES);
|
||||
CIPHER_AES0_IZ = decryptByBase64AES(CIPHER_AES2_IZ, CIPHER_AES);
|
||||
MG_PKEY = decryptByBase64AES(MG_PKEY2, CIPHER_AES2);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException | NoSuchPaddingException | NoSuchAlgorithmException |
|
||||
InvalidKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -329,4 +339,73 @@ public class AESUtils {
|
||||
return _0x53928f + "-" + _0x430930 + "-" + _0x49ec94;
|
||||
}
|
||||
|
||||
|
||||
public static String encrypt(String str, String pwd) {
|
||||
if (pwd == null || pwd.length() <= 0) {
|
||||
throw new IllegalArgumentException("Please enter a password with which to encrypt the message.");
|
||||
}
|
||||
|
||||
// 生成 prand 值
|
||||
StringBuilder prand = new StringBuilder();
|
||||
for (int i = 0; i < pwd.length(); i++) {
|
||||
prand.append((int) pwd.charAt(i));
|
||||
}
|
||||
|
||||
// 计算 sPos, mult, incr, modu
|
||||
int sPos = prand.length() / 5;
|
||||
long mult = Long.parseLong(prand.substring(sPos, sPos + 1) +
|
||||
prand.substring(sPos * 2, sPos * 2 + 1) +
|
||||
prand.substring(sPos * 3, sPos * 3 + 1) +
|
||||
prand.substring(sPos * 4, sPos * 4 + 1) +
|
||||
prand.substring(sPos * 5, sPos * 5 + 1));
|
||||
int incr = (int) Math.ceil(pwd.length() / 2.0);
|
||||
long modu = (long) Math.pow(2, 31) - 1;
|
||||
|
||||
if (mult < 2) {
|
||||
throw new IllegalArgumentException("Algorithm cannot find a suitable hash. Please choose a different password.");
|
||||
}
|
||||
|
||||
// 生成 salt 并加到 prand 上
|
||||
long salt = Math.round(Math.random() * 1000000000) % 100000000;
|
||||
prand.append(salt);
|
||||
|
||||
// 使用 BigInteger 处理超过 Long 范围的 prand 值
|
||||
BigInteger prandValue = new BigInteger(prand.toString());
|
||||
while (prandValue.toString().length() > 10) {
|
||||
prandValue = prandValue.divide(BigInteger.TEN).add(prandValue.remainder(BigInteger.TEN));
|
||||
}
|
||||
|
||||
// 将 prand 转换为 long 进行后续处理
|
||||
prandValue = prandValue.multiply(BigInteger.valueOf(mult)).add(BigInteger.valueOf(incr)).mod(BigInteger.valueOf(modu));
|
||||
|
||||
// 加密过程
|
||||
StringBuilder encStr = new StringBuilder();
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
// 将字符和异或的结果强制转换为 int
|
||||
int encChr = (int) (str.charAt(i) ^ (int) ((prandValue.doubleValue() / modu) * 255));
|
||||
String hexStr = Integer.toHexString(encChr);
|
||||
if (hexStr.length() < 2) {
|
||||
encStr.append("0");
|
||||
}
|
||||
encStr.append(hexStr);
|
||||
|
||||
prandValue = prandValue.multiply(BigInteger.valueOf(mult)).add(BigInteger.valueOf(incr)).mod(BigInteger.valueOf(modu));
|
||||
}
|
||||
|
||||
// 将 salt 转换为 16 进制并追加到加密结果
|
||||
String saltHex = Long.toHexString(salt);
|
||||
while (saltHex.length() < 8) {
|
||||
saltHex = "0" + saltHex;
|
||||
}
|
||||
encStr.append(saltHex);
|
||||
|
||||
return encStr.toString();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
// 示例
|
||||
String encrypted = encrypt("HelloWorld", "password123");
|
||||
System.out.println("Encrypted String: " + encrypted);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
18
parser/src/main/java/cn/qaiu/util/CastUtil.java
Normal file
18
parser/src/main/java/cn/qaiu/util/CastUtil.java
Normal file
@@ -0,0 +1,18 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
/**
|
||||
* 转换为任意类型 旨在消除泛型转换时的异常
|
||||
*/
|
||||
public interface CastUtil {
|
||||
|
||||
/**
|
||||
* 泛型转换
|
||||
* @param object 要转换的object
|
||||
* @param <T> T
|
||||
* @return T
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> T cast(Object object) {
|
||||
return (T) object;
|
||||
}
|
||||
}
|
||||
44
parser/src/main/java/cn/qaiu/util/FileSizeConverter.java
Normal file
44
parser/src/main/java/cn/qaiu/util/FileSizeConverter.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
public class FileSizeConverter {
|
||||
|
||||
public static long convertToBytes(String sizeStr) {
|
||||
if (sizeStr == null || sizeStr.isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid file size string");
|
||||
}
|
||||
|
||||
sizeStr = sizeStr.replace(",","").trim().toUpperCase();
|
||||
// 判断是2位单位还是1位单位
|
||||
// 判断单位是否为2位
|
||||
int unitIndex = sizeStr.length() - 1;
|
||||
char unit = sizeStr.charAt(unitIndex);
|
||||
if (Character.isLetter(sizeStr.charAt(unitIndex - 1))) {
|
||||
unit = sizeStr.charAt(unitIndex - 1);
|
||||
sizeStr = sizeStr.substring(0, unitIndex - 1);
|
||||
} else {
|
||||
sizeStr = sizeStr.substring(0, unitIndex);
|
||||
}
|
||||
double size = Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
|
||||
return switch (unit) {
|
||||
case 'B' -> (long) size;
|
||||
case 'K' -> (long) (size * 1024);
|
||||
case 'M' -> (long) (size * 1024 * 1024);
|
||||
case 'G' -> (long) (size * 1024 * 1024 * 1024);
|
||||
default -> throw new IllegalArgumentException("Unknown file size unit: " + unit);
|
||||
};
|
||||
}
|
||||
|
||||
public static String convertToReadableSize(long bytes) {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return String.format("%.1f K", bytes / 1024.0);
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return String.format("%.1f M", bytes / (1024.0 * 1024));
|
||||
} else {
|
||||
return String.format("%.1f G", bytes / (1024.0 * 1024 * 1024));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
35
parser/src/main/java/cn/qaiu/util/HeaderUtils.java
Normal file
35
parser/src/main/java/cn/qaiu/util/HeaderUtils.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
|
||||
public class HeaderUtils {
|
||||
|
||||
/**
|
||||
* 将请求头字符串转换为Vert.x的MultiMap对象
|
||||
*
|
||||
* @param headerString 请求头字符串
|
||||
* @return MultiMap对象
|
||||
*/
|
||||
public static MultiMap parseHeaders(String headerString) {
|
||||
MultiMap headers = MultiMap.caseInsensitiveMultiMap();
|
||||
|
||||
if (headerString == null || headerString.isEmpty()) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
// 按行分割字符串
|
||||
String[] lines = headerString.split("\n");
|
||||
|
||||
for (String line : lines) {
|
||||
// 按冒号分割键值对
|
||||
String[] parts = line.split(":", 2);
|
||||
if (parts.length == 2) {
|
||||
String key = parts[0].trim();
|
||||
String value = parts[1].trim();
|
||||
headers.add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
80
parser/src/main/java/cn/qaiu/util/IpExtractor.java
Normal file
80
parser/src/main/java/cn/qaiu/util/IpExtractor.java
Normal file
@@ -0,0 +1,80 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.impl.headers.HeadersMultiMap;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientSession;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IpExtractor {
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
|
||||
|
||||
// 创建请求头Map
|
||||
MultiMap headers = new HeadersMultiMap();
|
||||
headers.add("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
|
||||
headers.add("accept-language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||
headers.add("cache-control", "no-cache");
|
||||
headers.add("dnt", "1");
|
||||
headers.add("origin", "https://ip.ihuan.me");
|
||||
headers.add("pragma", "no-cache");
|
||||
headers.add("priority", "u=0, i");
|
||||
headers.add("referer", "https://ip.ihuan.me/ti.html");
|
||||
headers.add("sec-ch-ua", "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"");
|
||||
headers.add("sec-ch-ua-mobile", "?0");
|
||||
headers.add("sec-ch-ua-platform", "\"Windows\"");
|
||||
headers.add("sec-fetch-dest", "document");
|
||||
headers.add("sec-fetch-mode", "navigate");
|
||||
headers.add("sec-fetch-site", "same-origin");
|
||||
headers.add("sec-fetch-user", "?1");
|
||||
headers.add("upgrade-insecure-requests", "1");
|
||||
headers.add("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36");
|
||||
headers.add("Content-Type", "application/x-www-form-urlencoded");
|
||||
|
||||
WebClient client = WebClient.create(Vertx.vertx());
|
||||
WebClientSession webClientSession = WebClientSession.create(client);
|
||||
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res->{
|
||||
System.out.println(res.toString());
|
||||
webClientSession.getAbs("https://ip.ihuan.me").putHeaders(headers).send().onSuccess(res2->{
|
||||
System.out.println(res2.toString());
|
||||
|
||||
});
|
||||
});
|
||||
//
|
||||
// String htmlContent = "<div class=\"panel-heading\">提取结果</div>\n" +
|
||||
// "<div class=\"panel-body\">111.1.27.85:80<br>42.63.65.46:80<br>118.195.242.20:8080<br>42.63.65.119:80<br>117.50.108.90:7890<br>116.62.50.250:7890<br>114.231.8.177:8089<br>190.43.92.90:999<br>221.178.85.68:8080<br><br>61.160.202.86:80<br>42.63.65.86:80<br>42.63.65.7:80<br>42.63.65.41:80<br>159.226.227.119:80<br>61.160.202.52:80<br>42.63.65.15:80<br>112.17.16.204:80<br>61.160.202.53:80<br>42.63.65.9:80<br>42.63.65.60:80<br>42.63.65.18:80<br>203.190.115.177:8071<br>42.63.65.38:80<br>42.63.65.31:80<br>91.185.3.126:8080<br>139.9.119.20:80<br>1.15.47.213:443<br>183.164.243.108:8089<br>165.225.208.177:80<br>194.163.132.232:3128<br>91.235.220.122:80<br>39.100.120.200:7890<br>141.147.33.121:80<br>183.164.243.138:8089<br>104.129.205.94:54321<br>117.160.250.138:81<br>180.120.213.208:8089<br>61.130.9.37:443<br>182.34.18.206:9999<br>117.86.12.150:8089<br>27.192.173.108:9000<br>183.164.243.11:8089<br>114.231.41.205:8089<br>103.104.233.78:8080<br>183.164.243.174:8089<br>36.6.144.230:8089<br>111.224.213.239:8089<br>182.92.73.106:80<br>36.6.145.81:8089<br>117.69.232.125:8089<br>36.6.144.64:8089<br>117.57.92.20:8089<br>47.100.69.29:8888<br>117.57.93.246:8089<br>120.234.203.171:9002<br>114.231.46.231:8089<br>183.164.242.199:8089<br>117.69.237.179:8089<br>182.44.32.239:7890<br>47.100.91.57:8080<br>117.69.236.127:8089<br>114.231.8.18:8089<br>117.69.232.183:8089<br>117.69.237.29:8089<br>183.164.242.67:8089<br>183.164.242.35:8089<br>183.164.243.71:8089<br>113.223.214.155:8089<br>36.6.145.132:8089<br>182.106.220.252:9091<br>113.223.212.176:8089<br>62.152.53.186:8909<br>117.57.92.16:8089<br>183.164.243.186:8089<br>36.6.144.210:8089<br>183.164.242.189:8089<br>213.178.39.170:8080<br>121.52.145.163:8080<br>36.6.144.240:8089<br>60.188.5.234:80<br>113.223.213.48:8089<br>183.164.243.149:8089<br>200.58.87.195:8080<br>36.6.144.153:8089<br>36.6.144.67:8089<br>36.6.145.182:8089<br>117.57.93.226:8089<br>42.112.24.127:8888<br>43.138.20.156:80<br>117.57.92.79:8089<br>65.109.111.238:3128<br>183.166.137.201:41122<br>113.223.213.150:8089<br>36.6.145.154:8089<br>185.5.209.101:80<br>36.6.144.17:8089<br>114.231.8.244:8089<br>117.69.237.24:8089<br>117.69.236.232:8089<br>117.69.236.127:8089<br>114.231.8.18:8089<br>117.69.232.183:8089<br>117.69.237.29:8089<br>183.164.242.67:8089<br>183.164.242.35:8089<br>183.164.243.71:8089<br>113.223.214.155:8089<br>36.6.145.132:8089<br>182.106.220.252:9091<br>113.223.212.176:8089<br>62.152.53.186:8909<br>117.57.92.16:8089<br>183.164.243.186:8089<br>36.6.144.210:8089<br>183.164.242.189:8089<br>213.178.39.170:8080<br>121.52.145.163:8080<br>36.6.144.240:8089<br>60.188.5.234:80<br>113.223.213.48:8089<br>183.164.243.149:8089<br>200.58.87.195:8080<br>36.6.144.153:8089<br>36.6.144.67:8089<br>36.6.145.182:8089<br>117.57.93.226:8089<br>42.112.24.127:8888<br>43.138.20.156:80<br>117.57.92.79:8089<br>65.109.111.238:3128<br>183.166.137.201:41122<br>113.223.213.150:8089<br>36.6.145.154:8089<br>185.5.209.101:80<br>36.6.144.17:8089<br>114.231.8.244:8089<br>117.69.237.24:8089<br>117.69.236.232:8089</div>";
|
||||
//
|
||||
// // 正则表达式匹配提取结果关键字下面的IP地址
|
||||
// Pattern pattern = Pattern.compile("<div class=\"panel-heading\">提取结果</div>\\s*<div class=\"panel-body\">([\\s\\S]*?)(?=</div>)", Pattern.DOTALL);
|
||||
// Matcher matcher = pattern.matcher(htmlContent);
|
||||
//
|
||||
// if (matcher.find()) {
|
||||
// String ipText = matcher.group(1); // 获取匹配的IP地址部分
|
||||
// Pattern ipPattern = Pattern.compile("(?:[0-9]{1,3}\\.){3}[0-9]{1,3}(?::\\d+)?");
|
||||
// Matcher ipMatcher = ipPattern.matcher(ipText);
|
||||
//
|
||||
// List<String> ips = new ArrayList<>();
|
||||
// while (ipMatcher.find()) {
|
||||
// ips.add(ipMatcher.group());
|
||||
// }
|
||||
//
|
||||
// System.out.println("提取到的IP地址:");
|
||||
// for (String ip : ips) {
|
||||
//// System.out.println(ip);
|
||||
// }
|
||||
// } else {
|
||||
// System.out.println("没有找到匹配的IP地址");
|
||||
// }
|
||||
//
|
||||
// TimeUnit.SECONDS.sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,10 @@ public interface JsContent {
|
||||
},
|
||||
ajax: function (obj) {
|
||||
signObj = obj
|
||||
}
|
||||
},
|
||||
val: function(a) {
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
@@ -134,7 +137,6 @@ public interface JsContent {
|
||||
jQuery.fn.init.prototype = jQuery.fn;
|
||||
|
||||
|
||||
// 伪装jquery.ajax函数获取关键数据
|
||||
$.ajax = function (obj) {
|
||||
signObj = obj
|
||||
}
|
||||
@@ -142,11 +144,921 @@ public interface JsContent {
|
||||
var document = {
|
||||
getElementById: function (v) {
|
||||
return {
|
||||
value: 'v'
|
||||
value: 'v',
|
||||
style: {
|
||||
display: ''
|
||||
},
|
||||
addEventListener: function() {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var window = {location: {}}
|
||||
""";
|
||||
|
||||
String kwSignString = """
|
||||
function encrypt(str, pwd) {
|
||||
if (pwd == null || pwd.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
var prand = "";
|
||||
for (var i = 0; i < pwd.length; i++) {
|
||||
prand += pwd.charCodeAt(i).toString();
|
||||
}
|
||||
var sPos = Math.floor(prand.length / 5);
|
||||
var mult = parseInt(prand.charAt(sPos) + prand.charAt(sPos * 2) + prand.charAt(sPos * 3) + prand.charAt(sPos * 4) + prand.charAt(sPos * 5));
|
||||
var incr = Math.ceil(pwd.length / 2);
|
||||
var modu = Math.pow(2, 31) - 1;
|
||||
if (mult < 2) {
|
||||
return null;
|
||||
}
|
||||
var salt = Math.round(Math.random() * 1000000000) % 100000000;
|
||||
prand += salt;
|
||||
var flag = 1;
|
||||
while (prand.length > 10) {
|
||||
prand = (parseInt(prand.substring(0, 10)) + (flag ?parseFloat(prand.substring(10, prand.length)) : parseInt(prand.substring(10, prand.length)) )).toString();
|
||||
flag = 0;
|
||||
}
|
||||
prand = (mult * prand + incr) % modu;
|
||||
var enc_chr = "";
|
||||
var enc_str = "";
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
enc_chr = parseInt(str.charCodeAt(i) ^ Math.floor((prand / modu) * 255));
|
||||
if (enc_chr < 16) {
|
||||
enc_str += "0" + enc_chr.toString(16);
|
||||
} else enc_str += enc_chr.toString(16);
|
||||
prand = (mult * prand + incr) % modu;
|
||||
}
|
||||
salt = salt.toString(16);
|
||||
while (salt.length < 8) salt = "0" + salt;
|
||||
enc_str += salt;
|
||||
return enc_str;
|
||||
}
|
||||
""";
|
||||
|
||||
String mgJS = """
|
||||
|
||||
|
||||
var XO, qO = {
|
||||
exports: {}
|
||||
}, JO = {
|
||||
exports: {}
|
||||
};
|
||||
|
||||
var qb = globalThis = this
|
||||
|
||||
JO.exports = (XO = XO || function (e, t) {
|
||||
var n;
|
||||
if ("undefined" != typeof window && window.crypto && (n = window.crypto),
|
||||
"undefined" != typeof self && self.crypto && (n = self.crypto),
|
||||
"undefined" != typeof globalThis && globalThis.crypto && (n = globalThis.crypto),
|
||||
!n && "undefined" != typeof window && window.msCrypto && (n = window.msCrypto),
|
||||
!n && void 0 !== qb && qb.crypto && (n = qb.crypto),
|
||||
!n)
|
||||
try {
|
||||
n = require("crypto")
|
||||
} catch (g) {
|
||||
}
|
||||
var r = function () {
|
||||
|
||||
return Math.floor(Math.random() * 0xFFFFFFFF + 0)
|
||||
}
|
||||
, o = Object.create || function () {
|
||||
function e() {
|
||||
}
|
||||
|
||||
return function (t) {
|
||||
var n;
|
||||
return e.prototype = t,
|
||||
n = new e,
|
||||
e.prototype = null,
|
||||
n
|
||||
}
|
||||
}()
|
||||
, a = {}
|
||||
, i = a.lib = {}
|
||||
, l = i.Base = {
|
||||
extend: function (e) {
|
||||
var t = o(this);
|
||||
return e && t.mixIn(e),
|
||||
t.hasOwnProperty("init") && this.init !== t.init || (t.init = function () {
|
||||
t.$super.init.apply(this, arguments)
|
||||
}
|
||||
),
|
||||
t.init.prototype = t,
|
||||
t.$super = this,
|
||||
t
|
||||
},
|
||||
create: function () {
|
||||
var e = this.extend();
|
||||
return e.init.apply(e, arguments),
|
||||
e
|
||||
},
|
||||
init: function () {
|
||||
},
|
||||
mixIn: function (e) {
|
||||
for (var t in e)
|
||||
e.hasOwnProperty(t) && (this[t] = e[t]);
|
||||
e.hasOwnProperty("toString") && (this.toString = e.toString)
|
||||
},
|
||||
clone: function () {
|
||||
return this.init.prototype.extend(this)
|
||||
}
|
||||
}
|
||||
, s = i.WordArray = l.extend({
|
||||
init: function (e, n) {
|
||||
e = this.words = e || [],
|
||||
this.sigBytes = n != t ? n : 4 * e.length
|
||||
},
|
||||
toString: function (e) {
|
||||
return (e || c).stringify(this)
|
||||
},
|
||||
concat: function (e) {
|
||||
var t = this.words
|
||||
, n = e.words
|
||||
, r = this.sigBytes
|
||||
, o = e.sigBytes;
|
||||
if (this.clamp(),
|
||||
r % 4)
|
||||
for (var a = 0; a < o; a++) {
|
||||
var i = n[a >>> 2] >>> 24 - a % 4 * 8 & 255;
|
||||
t[r + a >>> 2] |= i << 24 - (r + a) % 4 * 8
|
||||
}
|
||||
else
|
||||
for (var l = 0; l < o; l += 4)
|
||||
t[r + l >>> 2] = n[l >>> 2];
|
||||
return this.sigBytes += o,
|
||||
this
|
||||
},
|
||||
clamp: function () {
|
||||
var t = this.words
|
||||
, n = this.sigBytes;
|
||||
t[n >>> 2] &= 4294967295 << 32 - n % 4 * 8,
|
||||
t.length = e.ceil(n / 4)
|
||||
},
|
||||
clone: function () {
|
||||
var e = l.clone.call(this);
|
||||
return e.words = this.words.slice(0),
|
||||
e
|
||||
},
|
||||
random: function (e) {
|
||||
for (var t = [], n = 0; n < e; n += 4)
|
||||
t.push(r());
|
||||
return new s.init(t, e)
|
||||
}
|
||||
})
|
||||
, u = a.enc = {}
|
||||
, c = u.Hex = {
|
||||
stringify: function (e) {
|
||||
for (var t = e.words, n = e.sigBytes, r = [], o = 0; o < n; o++) {
|
||||
var a = t[o >>> 2] >>> 24 - o % 4 * 8 & 255;
|
||||
r.push((a >>> 4).toString(16)),
|
||||
r.push((15 & a).toString(16))
|
||||
}
|
||||
return r.join("")
|
||||
},
|
||||
parse: function (e) {
|
||||
for (var t = e.length, n = [], r = 0; r < t; r += 2)
|
||||
n[r >>> 3] |= parseInt(e.substr(r, 2), 16) << 24 - r % 8 * 4;
|
||||
return new s.init(n, t / 2)
|
||||
}
|
||||
}
|
||||
, d = u.Latin1 = {
|
||||
stringify: function (e) {
|
||||
for (var t = e.words, n = e.sigBytes, r = [], o = 0; o < n; o++) {
|
||||
var a = t[o >>> 2] >>> 24 - o % 4 * 8 & 255;
|
||||
r.push(String.fromCharCode(a))
|
||||
}
|
||||
return r.join("")
|
||||
},
|
||||
parse: function (e) {
|
||||
for (var t = e.length, n = [], r = 0; r < t; r++)
|
||||
n[r >>> 2] |= (255 & e.charCodeAt(r)) << 24 - r % 4 * 8;
|
||||
return new s.init(n, t)
|
||||
}
|
||||
}
|
||||
, p = u.Utf8 = {
|
||||
stringify: function (e) {
|
||||
try {
|
||||
return decodeURIComponent(escape(d.stringify(e)))
|
||||
} catch (RE) {
|
||||
throw new Error("Malformed UTF-8 data")
|
||||
}
|
||||
},
|
||||
parse: function (e) {
|
||||
return d.parse(unescape(encodeURIComponent(e)))
|
||||
}
|
||||
}
|
||||
, f = i.BufferedBlockAlgorithm = l.extend({
|
||||
reset: function () {
|
||||
this._data = new s.init,
|
||||
this._nDataBytes = 0
|
||||
},
|
||||
_append: function (e) {
|
||||
"string" == typeof e && (e = p.parse(e)),
|
||||
this._data.concat(e),
|
||||
this._nDataBytes += e.sigBytes
|
||||
},
|
||||
_process: function (t) {
|
||||
var n, r = this._data, o = r.words, a = r.sigBytes, i = this.blockSize, l = a / (4 * i),
|
||||
u = (l = t ? e.ceil(l) : e.max((0 | l) - this._minBufferSize, 0)) * i, c = e.min(4 * u, a);
|
||||
if (u) {
|
||||
for (var d = 0; d < u; d += i)
|
||||
this._doProcessBlock(o, d);
|
||||
n = o.splice(0, u),
|
||||
r.sigBytes -= c
|
||||
}
|
||||
return new s.init(n, c)
|
||||
},
|
||||
clone: function () {
|
||||
var e = l.clone.call(this);
|
||||
return e._data = this._data.clone(),
|
||||
e
|
||||
},
|
||||
_minBufferSize: 0
|
||||
});
|
||||
i.Hasher = f.extend({
|
||||
cfg: l.extend(),
|
||||
init: function (e) {
|
||||
this.cfg = this.cfg.extend(e),
|
||||
this.reset()
|
||||
},
|
||||
reset: function () {
|
||||
f.reset.call(this),
|
||||
this._doReset()
|
||||
},
|
||||
update: function (e) {
|
||||
return this._append(e),
|
||||
this._process(),
|
||||
this
|
||||
},
|
||||
finalize: function (e) {
|
||||
return e && this._append(e),
|
||||
this._doFinalize()
|
||||
},
|
||||
blockSize: 16,
|
||||
_createHelper: function (e) {
|
||||
return function (t, n) {
|
||||
return new e.init(n).finalize(t)
|
||||
}
|
||||
},
|
||||
_createHmacHelper: function (e) {
|
||||
return function (t, n) {
|
||||
return new h.HMAC.init(e, n).finalize(t)
|
||||
}
|
||||
}
|
||||
});
|
||||
var h = a.algo = {};
|
||||
return a
|
||||
}(Math), XO);
|
||||
|
||||
var QO = {
|
||||
exports: {}
|
||||
};
|
||||
QO.exports = function (e) {
|
||||
return r = (n = e).lib,
|
||||
o = r.Base,
|
||||
a = r.WordArray,
|
||||
(i = n.x64 = {}).Word = o.extend({
|
||||
init: function (e, t) {
|
||||
this.high = e,
|
||||
this.low = t
|
||||
}
|
||||
}),
|
||||
i.WordArray = o.extend({
|
||||
init: function (e, n) {
|
||||
e = this.words = e || [],
|
||||
this.sigBytes = n != t ? n : 8 * e.length
|
||||
},
|
||||
toX32: function () {
|
||||
for (var e = this.words, t = e.length, n = [], r = 0; r < t; r++) {
|
||||
var o = e[r];
|
||||
n.push(o.high),
|
||||
n.push(o.low)
|
||||
}
|
||||
return a.create(n, this.sigBytes)
|
||||
},
|
||||
clone: function () {
|
||||
for (var e = o.clone.call(this), t = e.words = this.words.slice(0), n = t.length, r = 0; r < n; r++)
|
||||
t[r] = t[r].clone();
|
||||
return e
|
||||
}
|
||||
}),
|
||||
e;
|
||||
var t, n, r, o, a, i
|
||||
}(JO.exports);
|
||||
|
||||
var nM = {
|
||||
exports: {}
|
||||
};
|
||||
nM.exports = function (e) {
|
||||
return function () {
|
||||
var t = e
|
||||
, n = t.lib.WordArray;
|
||||
|
||||
function r(e, t, r) {
|
||||
for (var o = [], a = 0, i = 0; i < t; i++)
|
||||
if (i % 4) {
|
||||
var l = r[e.charCodeAt(i - 1)] << i % 4 * 2 | r[e.charCodeAt(i)] >>> 6 - i % 4 * 2;
|
||||
o[a >>> 2] |= l << 24 - a % 4 * 8,
|
||||
a++
|
||||
}
|
||||
return n.create(o, a)
|
||||
}
|
||||
|
||||
t.enc.Base64 = {
|
||||
stringify: function (e) {
|
||||
var t = e.words
|
||||
, n = e.sigBytes
|
||||
, r = this._map;
|
||||
e.clamp();
|
||||
for (var o = [], a = 0; a < n; a += 3)
|
||||
for (var i = (t[a >>> 2] >>> 24 - a % 4 * 8 & 255) << 16 | (t[a + 1 >>> 2] >>> 24 - (a + 1) % 4 * 8 & 255) << 8 | t[a + 2 >>> 2] >>> 24 - (a + 2) % 4 * 8 & 255, l = 0; l < 4 && a + .75 * l < n; l++)
|
||||
o.push(r.charAt(i >>> 6 * (3 - l) & 63));
|
||||
var s = r.charAt(64);
|
||||
if (s)
|
||||
for (; o.length % 4;)
|
||||
o.push(s);
|
||||
return o.join("")
|
||||
},
|
||||
parse: function (e) {
|
||||
var t = e.length
|
||||
, n = this._map
|
||||
, o = this._reverseMap;
|
||||
if (!o) {
|
||||
o = this._reverseMap = [];
|
||||
for (var a = 0; a < n.length; a++)
|
||||
o[n.charCodeAt(a)] = a
|
||||
}
|
||||
var i = n.charAt(64);
|
||||
if (i) {
|
||||
var l = e.indexOf(i);
|
||||
-1 !== l && (t = l)
|
||||
}
|
||||
return r(e, t, o)
|
||||
},
|
||||
_map: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
|
||||
}
|
||||
}(),
|
||||
e.enc.Base64
|
||||
}(JO.exports);
|
||||
|
||||
var oM = {
|
||||
exports: {}
|
||||
};
|
||||
oM.exports = function (e) {
|
||||
return function (t) {
|
||||
var n = e
|
||||
, r = n.lib
|
||||
, o = r.WordArray
|
||||
, a = r.Hasher
|
||||
, i = n.algo
|
||||
, l = [];
|
||||
!function () {
|
||||
for (var e = 0; e < 64; e++)
|
||||
l[e] = 4294967296 * t.abs(t.sin(e + 1)) | 0
|
||||
}();
|
||||
var s = i.MD5 = a.extend({
|
||||
_doReset: function () {
|
||||
this._hash = new o.init([1732584193, 4023233417, 2562383102, 271733878])
|
||||
},
|
||||
_doProcessBlock: function (e, t) {
|
||||
for (var n = 0; n < 16; n++) {
|
||||
var r = t + n
|
||||
, o = e[r];
|
||||
e[r] = 16711935 & (o << 8 | o >>> 24) | 4278255360 & (o << 24 | o >>> 8)
|
||||
}
|
||||
var a = this._hash.words
|
||||
, i = e[t + 0]
|
||||
, s = e[t + 1]
|
||||
, f = e[t + 2]
|
||||
, h = e[t + 3]
|
||||
, g = e[t + 4]
|
||||
, v = e[t + 5]
|
||||
, m = e[t + 6]
|
||||
, y = e[t + 7]
|
||||
, b = e[t + 8]
|
||||
, C = e[t + 9]
|
||||
, _ = e[t + 10]
|
||||
, E = e[t + 11]
|
||||
, F = e[t + 12]
|
||||
, A = e[t + 13]
|
||||
, w = e[t + 14]
|
||||
, D = e[t + 15]
|
||||
, x = a[0]
|
||||
, S = a[1]
|
||||
, B = a[2]
|
||||
, k = a[3];
|
||||
x = u(x, S, B, k, i, 7, l[0]),
|
||||
k = u(k, x, S, B, s, 12, l[1]),
|
||||
B = u(B, k, x, S, f, 17, l[2]),
|
||||
S = u(S, B, k, x, h, 22, l[3]),
|
||||
x = u(x, S, B, k, g, 7, l[4]),
|
||||
k = u(k, x, S, B, v, 12, l[5]),
|
||||
B = u(B, k, x, S, m, 17, l[6]),
|
||||
S = u(S, B, k, x, y, 22, l[7]),
|
||||
x = u(x, S, B, k, b, 7, l[8]),
|
||||
k = u(k, x, S, B, C, 12, l[9]),
|
||||
B = u(B, k, x, S, _, 17, l[10]),
|
||||
S = u(S, B, k, x, E, 22, l[11]),
|
||||
x = u(x, S, B, k, F, 7, l[12]),
|
||||
k = u(k, x, S, B, A, 12, l[13]),
|
||||
B = u(B, k, x, S, w, 17, l[14]),
|
||||
x = c(x, S = u(S, B, k, x, D, 22, l[15]), B, k, s, 5, l[16]),
|
||||
k = c(k, x, S, B, m, 9, l[17]),
|
||||
B = c(B, k, x, S, E, 14, l[18]),
|
||||
S = c(S, B, k, x, i, 20, l[19]),
|
||||
x = c(x, S, B, k, v, 5, l[20]),
|
||||
k = c(k, x, S, B, _, 9, l[21]),
|
||||
B = c(B, k, x, S, D, 14, l[22]),
|
||||
S = c(S, B, k, x, g, 20, l[23]),
|
||||
x = c(x, S, B, k, C, 5, l[24]),
|
||||
k = c(k, x, S, B, w, 9, l[25]),
|
||||
B = c(B, k, x, S, h, 14, l[26]),
|
||||
S = c(S, B, k, x, b, 20, l[27]),
|
||||
x = c(x, S, B, k, A, 5, l[28]),
|
||||
k = c(k, x, S, B, f, 9, l[29]),
|
||||
B = c(B, k, x, S, y, 14, l[30]),
|
||||
x = d(x, S = c(S, B, k, x, F, 20, l[31]), B, k, v, 4, l[32]),
|
||||
k = d(k, x, S, B, b, 11, l[33]),
|
||||
B = d(B, k, x, S, E, 16, l[34]),
|
||||
S = d(S, B, k, x, w, 23, l[35]),
|
||||
x = d(x, S, B, k, s, 4, l[36]),
|
||||
k = d(k, x, S, B, g, 11, l[37]),
|
||||
B = d(B, k, x, S, y, 16, l[38]),
|
||||
S = d(S, B, k, x, _, 23, l[39]),
|
||||
x = d(x, S, B, k, A, 4, l[40]),
|
||||
k = d(k, x, S, B, i, 11, l[41]),
|
||||
B = d(B, k, x, S, h, 16, l[42]),
|
||||
S = d(S, B, k, x, m, 23, l[43]),
|
||||
x = d(x, S, B, k, C, 4, l[44]),
|
||||
k = d(k, x, S, B, F, 11, l[45]),
|
||||
B = d(B, k, x, S, D, 16, l[46]),
|
||||
x = p(x, S = d(S, B, k, x, f, 23, l[47]), B, k, i, 6, l[48]),
|
||||
k = p(k, x, S, B, y, 10, l[49]),
|
||||
B = p(B, k, x, S, w, 15, l[50]),
|
||||
S = p(S, B, k, x, v, 21, l[51]),
|
||||
x = p(x, S, B, k, F, 6, l[52]),
|
||||
k = p(k, x, S, B, h, 10, l[53]),
|
||||
B = p(B, k, x, S, _, 15, l[54]),
|
||||
S = p(S, B, k, x, s, 21, l[55]),
|
||||
x = p(x, S, B, k, b, 6, l[56]),
|
||||
k = p(k, x, S, B, D, 10, l[57]),
|
||||
B = p(B, k, x, S, m, 15, l[58]),
|
||||
S = p(S, B, k, x, A, 21, l[59]),
|
||||
x = p(x, S, B, k, g, 6, l[60]),
|
||||
k = p(k, x, S, B, E, 10, l[61]),
|
||||
B = p(B, k, x, S, f, 15, l[62]),
|
||||
S = p(S, B, k, x, C, 21, l[63]),
|
||||
a[0] = a[0] + x | 0,
|
||||
a[1] = a[1] + S | 0,
|
||||
a[2] = a[2] + B | 0,
|
||||
a[3] = a[3] + k | 0
|
||||
},
|
||||
_doFinalize: function () {
|
||||
var e = this._data
|
||||
, n = e.words
|
||||
, r = 8 * this._nDataBytes
|
||||
, o = 8 * e.sigBytes;
|
||||
n[o >>> 5] |= 128 << 24 - o % 32;
|
||||
var a = t.floor(r / 4294967296)
|
||||
, i = r;
|
||||
n[15 + (o + 64 >>> 9 << 4)] = 16711935 & (a << 8 | a >>> 24) | 4278255360 & (a << 24 | a >>> 8),
|
||||
n[14 + (o + 64 >>> 9 << 4)] = 16711935 & (i << 8 | i >>> 24) | 4278255360 & (i << 24 | i >>> 8),
|
||||
e.sigBytes = 4 * (n.length + 1),
|
||||
this._process();
|
||||
for (var l = this._hash, s = l.words, u = 0; u < 4; u++) {
|
||||
var c = s[u];
|
||||
s[u] = 16711935 & (c << 8 | c >>> 24) | 4278255360 & (c << 24 | c >>> 8)
|
||||
}
|
||||
return l
|
||||
},
|
||||
clone: function () {
|
||||
var e = a.clone.call(this);
|
||||
return e._hash = this._hash.clone(),
|
||||
e
|
||||
}
|
||||
});
|
||||
|
||||
function u(e, t, n, r, o, a, i) {
|
||||
var l = e + (t & n | ~t & r) + o + i;
|
||||
return (l << a | l >>> 32 - a) + t
|
||||
}
|
||||
|
||||
function c(e, t, n, r, o, a, i) {
|
||||
var l = e + (t & r | n & ~r) + o + i;
|
||||
return (l << a | l >>> 32 - a) + t
|
||||
}
|
||||
|
||||
function d(e, t, n, r, o, a, i) {
|
||||
var l = e + (t ^ n ^ r) + o + i;
|
||||
return (l << a | l >>> 32 - a) + t
|
||||
}
|
||||
|
||||
function p(e, t, n, r, o, a, i) {
|
||||
var l = e + (n ^ (t | ~r)) + o + i;
|
||||
return (l << a | l >>> 32 - a) + t
|
||||
}
|
||||
|
||||
n.MD5 = a._createHelper(s),
|
||||
n.HmacMD5 = a._createHmacHelper(s)
|
||||
}(Math),
|
||||
e.MD5
|
||||
}(JO.exports);
|
||||
|
||||
var hM = {
|
||||
exports: {}
|
||||
};
|
||||
hM.exports = function (e) {
|
||||
return n = (t = e).lib,
|
||||
r = n.Base,
|
||||
o = n.WordArray,
|
||||
a = t.algo,
|
||||
i = a.MD5,
|
||||
l = a.EvpKDF = r.extend({
|
||||
cfg: r.extend({
|
||||
keySize: 4,
|
||||
hasher: i,
|
||||
iterations: 1
|
||||
}),
|
||||
init: function (e) {
|
||||
this.cfg = this.cfg.extend(e)
|
||||
},
|
||||
compute: function (e, t) {
|
||||
for (var n, r = this.cfg, a = r.hasher.create(), i = o.create(), l = i.words, s = r.keySize, u = r.iterations; l.length < s;) {
|
||||
n && a.update(n),
|
||||
n = a.update(e).finalize(t),
|
||||
a.reset();
|
||||
for (var c = 1; c < u; c++)
|
||||
n = a.finalize(n),
|
||||
a.reset();
|
||||
i.concat(n)
|
||||
}
|
||||
return i.sigBytes = 4 * s,
|
||||
i
|
||||
}
|
||||
}),
|
||||
t.EvpKDF = function (e, t, n) {
|
||||
return l.create(n).compute(e, t)
|
||||
}
|
||||
,
|
||||
e.EvpKDF;
|
||||
var t, n, r, o, a, i, l
|
||||
}(JO.exports);
|
||||
|
||||
var gM = {
|
||||
exports: {}
|
||||
};
|
||||
gM.exports = function (e) {
|
||||
e.lib.Cipher || function (t) {
|
||||
var n = e
|
||||
, r = n.lib
|
||||
, o = r.Base
|
||||
, a = r.WordArray
|
||||
, i = r.BufferedBlockAlgorithm
|
||||
, l = n.enc;
|
||||
l.Utf8;
|
||||
var s = l.Base64
|
||||
, u = n.algo.EvpKDF
|
||||
, c = r.Cipher = i.extend({
|
||||
cfg: o.extend(),
|
||||
createEncryptor: function (e, t) {
|
||||
return this.create(this._ENC_XFORM_MODE, e, t)
|
||||
},
|
||||
createDecryptor: function (e, t) {
|
||||
return this.create(this._DEC_XFORM_MODE, e, t)
|
||||
},
|
||||
init: function (e, t, n) {
|
||||
this.cfg = this.cfg.extend(n),
|
||||
this._xformMode = e,
|
||||
this._key = t,
|
||||
this.reset()
|
||||
},
|
||||
reset: function () {
|
||||
i.reset.call(this),
|
||||
this._doReset()
|
||||
},
|
||||
process: function (e) {
|
||||
return this._append(e),
|
||||
this._process()
|
||||
},
|
||||
finalize: function (e) {
|
||||
return e && this._append(e),
|
||||
this._doFinalize()
|
||||
},
|
||||
keySize: 4,
|
||||
ivSize: 4,
|
||||
_ENC_XFORM_MODE: 1,
|
||||
_DEC_XFORM_MODE: 2,
|
||||
_createHelper: function () {
|
||||
function e(e) {
|
||||
return "string" == typeof e ? b : m
|
||||
}
|
||||
|
||||
return function (t) {
|
||||
return {
|
||||
encrypt: function (n, r, o) {
|
||||
return e(r).encrypt(t, n, r, o)
|
||||
},
|
||||
decrypt: function (n, r, o) {
|
||||
return e(r).decrypt(t, n, r, o)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
});
|
||||
|
||||
r.StreamCipher = c.extend({
|
||||
_doFinalize: function () {
|
||||
return this._process(!0)
|
||||
},
|
||||
blockSize: 1
|
||||
});
|
||||
var d = n.mode = {}
|
||||
, p = r.BlockCipherMode = o.extend({
|
||||
createEncryptor: function (e, t) {
|
||||
return this.Encryptor.create(e, t)
|
||||
},
|
||||
createDecryptor: function (e, t) {
|
||||
return this.Decryptor.create(e, t)
|
||||
},
|
||||
init: function (e, t) {
|
||||
this._cipher = e,
|
||||
this._iv = t
|
||||
}
|
||||
})
|
||||
, f = d.CBC = function () {
|
||||
var e = p.extend();
|
||||
|
||||
function n(e, n, r) {
|
||||
var o, a = this._iv;
|
||||
a ? (o = a,
|
||||
this._iv = t) : o = this._prevBlock;
|
||||
for (var i = 0; i < r; i++)
|
||||
e[n + i] ^= o[i]
|
||||
}
|
||||
|
||||
return e.Encryptor = e.extend({
|
||||
processBlock: function (e, t) {
|
||||
var r = this._cipher
|
||||
, o = r.blockSize;
|
||||
n.call(this, e, t, o),
|
||||
r.encryptBlock(e, t),
|
||||
this._prevBlock = e.slice(t, t + o)
|
||||
}
|
||||
}),
|
||||
e.Decryptor = e.extend({
|
||||
processBlock: function (e, t) {
|
||||
var r = this._cipher
|
||||
, o = r.blockSize
|
||||
, a = e.slice(t, t + o);
|
||||
r.decryptBlock(e, t),
|
||||
n.call(this, e, t, o),
|
||||
this._prevBlock = a
|
||||
}
|
||||
}),
|
||||
e
|
||||
}()
|
||||
, h = (n.pad = {}).Pkcs7 = {
|
||||
pad: function (e, t) {
|
||||
for (var n = 4 * t, r = n - e.sigBytes % n, o = r << 24 | r << 16 | r << 8 | r, i = [], l = 0; l < r; l += 4)
|
||||
i.push(o);
|
||||
var s = a.create(i, r);
|
||||
e.concat(s)
|
||||
},
|
||||
unpad: function (e) {
|
||||
var t = 255 & e.words[e.sigBytes - 1 >>> 2];
|
||||
e.sigBytes -= t
|
||||
}
|
||||
};
|
||||
r.BlockCipher = c.extend({
|
||||
cfg: c.cfg.extend({
|
||||
mode: f,
|
||||
padding: h
|
||||
}),
|
||||
reset: function () {
|
||||
var e;
|
||||
c.reset.call(this);
|
||||
var t = this.cfg
|
||||
, n = t.iv
|
||||
, r = t.mode;
|
||||
this._xformMode == this._ENC_XFORM_MODE ? e = r.createEncryptor : (e = r.createDecryptor,
|
||||
this._minBufferSize = 1),
|
||||
this._mode && this._mode.__creator == e ? this._mode.init(this, n && n.words) : (this._mode = e.call(r, this, n && n.words),
|
||||
this._mode.__creator = e)
|
||||
},
|
||||
_doProcessBlock: function (e, t) {
|
||||
this._mode.processBlock(e, t)
|
||||
},
|
||||
_doFinalize: function () {
|
||||
var e, t = this.cfg.padding;
|
||||
return this._xformMode == this._ENC_XFORM_MODE ? (t.pad(this._data, this.blockSize),
|
||||
e = this._process(!0)) : (e = this._process(!0),
|
||||
t.unpad(e)),
|
||||
e
|
||||
},
|
||||
blockSize: 4
|
||||
});
|
||||
var g = r.CipherParams = o.extend({
|
||||
init: function (e) {
|
||||
this.mixIn(e)
|
||||
},
|
||||
toString: function (e) {
|
||||
return (e || this.formatter).stringify(this)
|
||||
}
|
||||
})
|
||||
, v = (n.format = {}).OpenSSL = {
|
||||
stringify: function (e) {
|
||||
var t = e.ciphertext
|
||||
, n = e.salt;
|
||||
return (n ? a.create([1398893684, 1701076831]).concat(n).concat(t) : t).toString(s)
|
||||
},
|
||||
parse: function (e) {
|
||||
var t, n = s.parse(e), r = n.words;
|
||||
return 1398893684 == r[0] && 1701076831 == r[1] && (t = a.create(r.slice(2, 4)),
|
||||
r.splice(0, 4),
|
||||
n.sigBytes -= 16),
|
||||
g.create({
|
||||
ciphertext: n,
|
||||
salt: t
|
||||
})
|
||||
}
|
||||
}
|
||||
, m = r.SerializableCipher = o.extend({
|
||||
cfg: o.extend({
|
||||
format: v
|
||||
}),
|
||||
encrypt: function (e, t, n, r) {
|
||||
r = this.cfg.extend(r);
|
||||
var o = e.createEncryptor(n, r)
|
||||
, a = o.finalize(t)
|
||||
, i = o.cfg;
|
||||
return g.create({
|
||||
ciphertext: a,
|
||||
key: n,
|
||||
iv: i.iv,
|
||||
algorithm: e,
|
||||
mode: i.mode,
|
||||
padding: i.padding,
|
||||
blockSize: e.blockSize,
|
||||
formatter: r.format
|
||||
})
|
||||
},
|
||||
decrypt: function (e, t, n, r) {
|
||||
return r = this.cfg.extend(r),
|
||||
t = this._parse(t, r.format),
|
||||
e.createDecryptor(n, r).finalize(t.ciphertext)
|
||||
},
|
||||
_parse: function (e, t) {
|
||||
return "string" == typeof e ? t.parse(e, this) : e
|
||||
}
|
||||
})
|
||||
, y = (n.kdf = {}).OpenSSL = {
|
||||
execute: function (e, t, n, r) {
|
||||
r || (r = a.random(8));
|
||||
var o = u.create({
|
||||
keySize: t + n
|
||||
}).compute(e, r)
|
||||
, i = a.create(o.words.slice(t), 4 * n);
|
||||
return o.sigBytes = 4 * t,
|
||||
g.create({
|
||||
key: o,
|
||||
iv: i,
|
||||
salt: r
|
||||
})
|
||||
}
|
||||
}
|
||||
, b = r.PasswordBasedCipher = m.extend({
|
||||
cfg: m.cfg.extend({
|
||||
kdf: y
|
||||
}),
|
||||
encrypt: function (e, t, n, r) {
|
||||
var o = (r = this.cfg.extend(r)).kdf.execute(n, e.keySize, e.ivSize);
|
||||
r.iv = o.iv;
|
||||
var a = m.encrypt.call(this, e, t, o.key, r);
|
||||
return a.mixIn(o),
|
||||
a
|
||||
},
|
||||
decrypt: function (e, t, n, r) {
|
||||
r = this.cfg.extend(r),
|
||||
t = this._parse(t, r.format);
|
||||
var o = r.kdf.execute(n, e.keySize, e.ivSize, t.salt);
|
||||
return r.iv = o.iv,
|
||||
m.decrypt.call(this, e, t, o.key, r)
|
||||
}
|
||||
})
|
||||
}()
|
||||
}(JO.exports);
|
||||
|
||||
var xM = {
|
||||
exports: {}
|
||||
};
|
||||
xM.exports = function (e) {
|
||||
return function () {
|
||||
var t = e
|
||||
, n = t.lib.BlockCipher
|
||||
, r = t.algo
|
||||
, o = []
|
||||
, a = []
|
||||
, i = []
|
||||
, l = []
|
||||
, s = []
|
||||
, u = []
|
||||
, c = []
|
||||
, d = []
|
||||
, p = []
|
||||
, f = [];
|
||||
!function () {
|
||||
for (var e = [], t = 0; t < 256; t++)
|
||||
e[t] = t < 128 ? t << 1 : t << 1 ^ 283;
|
||||
var n = 0
|
||||
, r = 0;
|
||||
for (t = 0; t < 256; t++) {
|
||||
var h = r ^ r << 1 ^ r << 2 ^ r << 3 ^ r << 4;
|
||||
h = h >>> 8 ^ 255 & h ^ 99,
|
||||
o[n] = h,
|
||||
a[h] = n;
|
||||
var g = e[n]
|
||||
, v = e[g]
|
||||
, m = e[v]
|
||||
, y = 257 * e[h] ^ 16843008 * h;
|
||||
i[n] = y << 24 | y >>> 8,
|
||||
l[n] = y << 16 | y >>> 16,
|
||||
s[n] = y << 8 | y >>> 24,
|
||||
u[n] = y,
|
||||
y = 16843009 * m ^ 65537 * v ^ 257 * g ^ 16843008 * n,
|
||||
c[h] = y << 24 | y >>> 8,
|
||||
d[h] = y << 16 | y >>> 16,
|
||||
p[h] = y << 8 | y >>> 24,
|
||||
f[h] = y,
|
||||
n ? (n = g ^ e[e[e[m ^ g]]],
|
||||
r ^= e[e[r]]) : n = r = 1
|
||||
}
|
||||
}();
|
||||
var h = [0, 1, 2, 4, 8, 16, 32, 64, 128, 27, 54]
|
||||
, g = r.AES = n.extend({
|
||||
_doReset: function () {
|
||||
if (!this._nRounds || this._keyPriorReset !== this._key) {
|
||||
for (var e = this._keyPriorReset = this._key, t = e.words, n = e.sigBytes / 4, r = 4 * ((this._nRounds = n + 6) + 1), a = this._keySchedule = [], i = 0; i < r; i++)
|
||||
i < n ? a[i] = t[i] : (u = a[i - 1],
|
||||
i % n ? n > 6 && i % n == 4 && (u = o[u >>> 24] << 24 | o[u >>> 16 & 255] << 16 | o[u >>> 8 & 255] << 8 | o[255 & u]) : (u = o[(u = u << 8 | u >>> 24) >>> 24] << 24 | o[u >>> 16 & 255] << 16 | o[u >>> 8 & 255] << 8 | o[255 & u],
|
||||
u ^= h[i / n | 0] << 24),
|
||||
a[i] = a[i - n] ^ u);
|
||||
for (var l = this._invKeySchedule = [], s = 0; s < r; s++) {
|
||||
if (i = r - s,
|
||||
s % 4)
|
||||
var u = a[i];
|
||||
else
|
||||
u = a[i - 4];
|
||||
l[s] = s < 4 || i <= 4 ? u : c[o[u >>> 24]] ^ d[o[u >>> 16 & 255]] ^ p[o[u >>> 8 & 255]] ^ f[o[255 & u]]
|
||||
}
|
||||
}
|
||||
},
|
||||
encryptBlock: function (e, t) {
|
||||
this._doCryptBlock(e, t, this._keySchedule, i, l, s, u, o)
|
||||
},
|
||||
decryptBlock: function (e, t) {
|
||||
var n = e[t + 1];
|
||||
e[t + 1] = e[t + 3],
|
||||
e[t + 3] = n,
|
||||
this._doCryptBlock(e, t, this._invKeySchedule, c, d, p, f, a),
|
||||
n = e[t + 1],
|
||||
e[t + 1] = e[t + 3],
|
||||
e[t + 3] = n
|
||||
},
|
||||
_doCryptBlock: function (e, t, n, r, o, a, i, l) {
|
||||
for (var s = this._nRounds, u = e[t] ^ n[0], c = e[t + 1] ^ n[1], d = e[t + 2] ^ n[2], p = e[t + 3] ^ n[3], f = 4, h = 1; h < s; h++) {
|
||||
var g = r[u >>> 24] ^ o[c >>> 16 & 255] ^ a[d >>> 8 & 255] ^ i[255 & p] ^ n[f++]
|
||||
, v = r[c >>> 24] ^ o[d >>> 16 & 255] ^ a[p >>> 8 & 255] ^ i[255 & u] ^ n[f++]
|
||||
, m = r[d >>> 24] ^ o[p >>> 16 & 255] ^ a[u >>> 8 & 255] ^ i[255 & c] ^ n[f++]
|
||||
, y = r[p >>> 24] ^ o[u >>> 16 & 255] ^ a[c >>> 8 & 255] ^ i[255 & d] ^ n[f++];
|
||||
u = g,
|
||||
c = v,
|
||||
d = m,
|
||||
p = y
|
||||
}
|
||||
g = (l[u >>> 24] << 24 | l[c >>> 16 & 255] << 16 | l[d >>> 8 & 255] << 8 | l[255 & p]) ^ n[f++],
|
||||
v = (l[c >>> 24] << 24 | l[d >>> 16 & 255] << 16 | l[p >>> 8 & 255] << 8 | l[255 & u]) ^ n[f++],
|
||||
m = (l[d >>> 24] << 24 | l[p >>> 16 & 255] << 16 | l[u >>> 8 & 255] << 8 | l[255 & c]) ^ n[f++],
|
||||
y = (l[p >>> 24] << 24 | l[u >>> 16 & 255] << 16 | l[c >>> 8 & 255] << 8 | l[255 & d]) ^ n[f++],
|
||||
e[t] = g,
|
||||
e[t + 1] = v,
|
||||
e[t + 2] = m,
|
||||
e[t + 3] = y
|
||||
},
|
||||
keySize: 8
|
||||
});
|
||||
t.AES = n._createHelper(g)
|
||||
}(),
|
||||
e.AES
|
||||
}(JO.exports);
|
||||
|
||||
|
||||
var OM = qO.exports = JO.exports;
|
||||
|
||||
function enc(data, key) {
|
||||
return OM.AES.encrypt(data, key).toString()
|
||||
}
|
||||
|
||||
""";
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static cn.qaiu.util.AESUtils.encrypt;
|
||||
|
||||
/**
|
||||
* 执行Js脚本
|
||||
*
|
||||
@@ -57,4 +62,49 @@ public class JsExecUtils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调用执行蓝奏云js文件
|
||||
*/
|
||||
public static Object executeOtherJs(String jsText, String funName, Object ... args) throws ScriptException,
|
||||
NoSuchMethodException {
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByName("JavaScript"); // 得到脚本引擎
|
||||
engine.eval(jsText);
|
||||
Invocable inv = (Invocable) engine;
|
||||
//调用js中的函数
|
||||
if (StringUtils.isNotEmpty(funName)) {
|
||||
return inv.invokeFunction(funName, args);
|
||||
}
|
||||
throw new ScriptException("funName is null");
|
||||
}
|
||||
|
||||
public static String getKwSign(String s, String pwd) {
|
||||
try {
|
||||
return executeOtherJs(JsContent.kwSignString, "encrypt", s, pwd).toString();
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String mgEncData(String data, String key) {
|
||||
try {
|
||||
return executeOtherJs(JsContent.mgJS, "enc", data, key).toString();
|
||||
} catch (ScriptException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// return OM.AES.encrypt('{"copyrightId":"6326951FKBL","type":1,"auditionsFlag":0}',
|
||||
// '4ea5c508a6566e76240543f8feb06fd457777be39549c4016436afda65d2330e').toString()
|
||||
//
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(URLEncoder
|
||||
.encode(mgEncData("{\"copyrightId\":\"6326951FKBL\",\"type\":1,\"auditionsFlag\":0}", AESUtils.MG_KEY), StandardCharsets.UTF_8));
|
||||
|
||||
// U2FsdGVkX1/UiZC91ImQvQY7qDBSEbTykAcVoARiquibPCZhWSs3kWQw3j2PNme5wNLqt2oau498ni1hgjGFuxwORnkk6x9rzk/X0arElUo=
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
12
parser/src/main/java/cn/qaiu/util/PanExceptionUtils.java
Normal file
12
parser/src/main/java/cn/qaiu/util/PanExceptionUtils.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* @date 2023/7/16 1:53
|
||||
*/
|
||||
public class PanExceptionUtils {
|
||||
|
||||
public static RuntimeException fillRunTimeException(String name, String dataKey, Throwable t) {
|
||||
return new RuntimeException(name + ": 请求异常: key = " + dataKey, t.fillInStackTrace());
|
||||
}
|
||||
}
|
||||
29
parser/src/main/java/cn/qaiu/util/RandomStringGenerator.java
Normal file
29
parser/src/main/java/cn/qaiu/util/RandomStringGenerator.java
Normal file
@@ -0,0 +1,29 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RandomStringGenerator {
|
||||
private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
private static final int LENGTH = 13; // 每段长度为13
|
||||
|
||||
public static String generateRandomString() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < 2; i++) { // 生成两段
|
||||
for (int j = 0; j < LENGTH; j++) {
|
||||
int index = random.nextInt(CHARACTERS.length());
|
||||
sb.append(CHARACTERS.charAt(index));
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static String gen36String() {
|
||||
String uuid = UUID.randomUUID().toString().toLowerCase();
|
||||
// 移除短横线
|
||||
return uuid.replace("-", "");
|
||||
}
|
||||
}
|
||||
74
parser/src/main/java/cn/qaiu/util/ReqIpUtil.java
Normal file
74
parser/src/main/java/cn/qaiu/util/ReqIpUtil.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import io.vertx.core.AsyncResult;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.http.impl.headers.HeadersMultiMap;
|
||||
import io.vertx.ext.web.client.HttpResponse;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientSession;
|
||||
|
||||
public class ReqIpUtil {
|
||||
public static String BASE_URL = "https://ip.ihuan.me";
|
||||
public static String BASE_URL_TEMPLATE = BASE_URL + "/{path}";
|
||||
|
||||
// GET https://ip.ihuan.me/mouse.do -> $("input[name='key']").val("30b4975b5547fed806bd2b9caa18485a");
|
||||
public static String PATH1 = "mouse.do";
|
||||
|
||||
public static String PATH2 = "tqdl.html";
|
||||
|
||||
// 创建请求头Map
|
||||
static MultiMap headers = new HeadersMultiMap();
|
||||
|
||||
static {
|
||||
|
||||
headers.set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7");
|
||||
headers.set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8");
|
||||
headers.set("cache-control", "no-cache");
|
||||
headers.set("dnt", "1");
|
||||
headers.set("origin", "https://ip.ihuan.me");
|
||||
headers.set("pragma", "no-cache");
|
||||
headers.set("priority", "u=0, i");
|
||||
headers.set("referer", "https://ip.ihuan.me");
|
||||
headers.set("sec-ch-ua", "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"");
|
||||
headers.set("sec-ch-ua-mobile", "?0");
|
||||
headers.set("sec-ch-ua-platform", "\"Windows\"");
|
||||
headers.set("sec-fetch-dest", "document");
|
||||
headers.set("sec-fetch-mode", "navigate");
|
||||
headers.set("sec-fetch-site", "same-origin");
|
||||
headers.set("sec-fetch-user", "?1");
|
||||
headers.set("upgrade-insecure-requests", "1");
|
||||
// headers.set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36");
|
||||
|
||||
}
|
||||
|
||||
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClient webClient = WebClient.create(vertx);
|
||||
// 发送GET请求
|
||||
WebClientSession webClientSession = WebClientSession.create(webClient);
|
||||
|
||||
|
||||
public void exec() {
|
||||
webClientSession.getAbs(BASE_URL)
|
||||
.putHeaders(headers) // 将请求头Map添加到请求中
|
||||
.send(this::next);
|
||||
}
|
||||
|
||||
void next(AsyncResult<HttpResponse<Buffer>> response) {
|
||||
if (response.failed()) {
|
||||
response.cause().printStackTrace();
|
||||
} else {
|
||||
HttpResponse<Buffer> res = response.result();
|
||||
System.out.println("Received response with status code " + res.statusCode());
|
||||
System.out.println("Body: " + res.body());
|
||||
webClientSession.getAbs(BASE_URL_TEMPLATE).setTemplateParam("path", PATH1)
|
||||
.putHeaders(headers) // 将请求头Map添加到请求中
|
||||
.send(response2 -> {
|
||||
System.out.println(response2.result().bodyAsString());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
47
parser/src/main/java/cn/qaiu/util/URLUtil.java
Normal file
47
parser/src/main/java/cn/qaiu/util/URLUtil.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class URLUtil {
|
||||
|
||||
private final Map<String, String> queryParams = new HashMap<>();
|
||||
|
||||
// 构造函数,传入URL并解析参数
|
||||
private URLUtil(String url) {
|
||||
try {
|
||||
URL parsedUrl = new URL(url);
|
||||
String ref = parsedUrl.getRef();
|
||||
if (StringUtils.isNotEmpty(ref)) {
|
||||
parsedUrl = new URL(parsedUrl.getProtocol() + "://" + parsedUrl.getHost() + ref);
|
||||
}
|
||||
String query = parsedUrl.getQuery();
|
||||
if (query != null) {
|
||||
String[] pairs = query.split("&");
|
||||
for (String pair : pairs) {
|
||||
String[] keyValue = pair.split("=");
|
||||
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
|
||||
String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : "";
|
||||
queryParams.put(key, value);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 静态方法,用于创建UrlUtil实例
|
||||
public static URLUtil from(String url) {
|
||||
return new URLUtil(url);
|
||||
}
|
||||
|
||||
// 获取参数的方法
|
||||
public String getParam(String param) {
|
||||
return queryParams.get(param);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@
|
||||
<logger name="io.netty" level="warn"/>
|
||||
<logger name="io.vertx" level="info"/>
|
||||
<logger name="com.zaxxer.hikari" level="info"/>
|
||||
<logger name="cn.qaiu" level="debug"/>
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
<!-- <appender-ref ref="FILE"/>-->
|
||||
|
||||
64
parser/src/main/resources/py/urltool.py
Normal file
64
parser/src/main/resources/py/urltool.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import requests
|
||||
import urllib.parse
|
||||
import re
|
||||
import base64
|
||||
|
||||
|
||||
"""
|
||||
https://github.com/chenhal/short_url
|
||||
"""
|
||||
headers = {
|
||||
'Cookie': 'SUB=',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
|
||||
'Referer': 'https://www.weibo.com',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
|
||||
def get_short_url(long_url):
|
||||
url = "https://www.weibo.com/aj/v6/comment/add"
|
||||
|
||||
payload = urllib.parse.urlencode({
|
||||
'mid': '5094736413852129',
|
||||
'content': long_url
|
||||
})
|
||||
response = requests.post(url, headers=headers, data=payload)
|
||||
|
||||
try:
|
||||
# print(response.json())
|
||||
data = response.json()['data']['comment']
|
||||
short_url = re.search(r'(https?)://t.cn/\w+', data).group(0)
|
||||
comment_id = re.findall(r'comment_id="(.+\d)"', data)[-1] # 评论id
|
||||
print('微博短链:' + short_url)
|
||||
del_comment(comment_id) # 需要删除评论,可以取消该行注释
|
||||
except:
|
||||
print('失败')
|
||||
pass
|
||||
|
||||
|
||||
# 删除评论
|
||||
def del_comment(comment_id):
|
||||
url = 'https://www.weibo.com/aj/comment/del'
|
||||
|
||||
payload = urllib.parse.urlencode({
|
||||
'mid': '微博mid',
|
||||
'cid': comment_id # 评论id
|
||||
})
|
||||
response = requests.post(url, headers=headers, data=payload)
|
||||
try:
|
||||
if response.json()['code'] == '100000':
|
||||
print('评论已删除')
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# https://so.toutiao.com/search/jump?url=https%3A%2F%2Fblog.qaiu.top%2Farchives%2Fpydroidall&aid=4916&jtoken=297f06127cb010274213422b1967bdc2ae8469b627205941dc287173b58a2a8439ea0d813d24ada8780047d33f37d7e82c6a620760de1ca37640c1dc143b4e01
|
||||
# https://so.toutiao.com/search/jump?url=https%3A%2F%2Fblog.qaiu.top%2Farchives%2Fpydroidall1&aid=4916&jtoken=297f06127cb010274213422b1967bdc2ae8469b627205941dc287173b58a2a8439ea0d813d24ada8780047d33f37d7e82c6a620760de1ca37640c1dc143b4e01
|
||||
|
||||
# 原始 URL
|
||||
url = "https://lz.qaiu.top/json/lz/i5vOm0xho2cj"
|
||||
|
||||
# 将 URL 编码为 Base64
|
||||
encoded_url = base64.b64encode(url.encode("utf-8")).decode("utf-8")
|
||||
get_short_url('https://www.so.com/link?m=ewgUSYiFWXIoTybC3fJH8YoJy8y10iRquo6cazgINwWjTn3HvVJ92TrCJu0PmMUR0RMDfOAucP3wa4G8j64SrhNH9Z0Cr0PEyn9ASuvpkUGmAjjUEGJkO5%2BIDGWVrEkPHsL7UsoKO6%2BlT%2BD6r&ccc=' + encoded_url)
|
||||
@@ -3,8 +3,13 @@ package cn.qaiu.parser;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.regex.Pattern.compile;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
@@ -70,4 +75,32 @@ public class PanDomainTemplateTest {
|
||||
// new Scanner(System.in).nextLine();
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void verifyDuplicates() {
|
||||
|
||||
Matcher matcher = compile("https://(?:[a-zA-Z\\d-]+\\.)?drive\\.google\\.com/file/d/(?<KEY>.+)/view(\\?usp=(sharing|drive_link))?")
|
||||
.matcher("https://adsd.drive.google.com/file/d/151bR-nk-tOBm9QAFaozJIVt2WYyCMkoz/view");
|
||||
if (matcher.find()) {
|
||||
System.out.println(matcher.group());
|
||||
System.out.println(matcher.group("KEY"));
|
||||
}
|
||||
// 校验重复
|
||||
Set<String> collect =
|
||||
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getRegex).collect(Collectors.toSet());
|
||||
if (collect.size()<PanDomainTemplate.values().length) {
|
||||
System.out.println("有重复枚举正则");
|
||||
} else {
|
||||
System.out.println("正则无重复");
|
||||
}
|
||||
|
||||
Set<String> collect2 =
|
||||
Arrays.stream(PanDomainTemplate.values()).map(PanDomainTemplate::getStandardUrlTemplate).collect(Collectors.toSet());
|
||||
if (collect2.size()<PanDomainTemplate.values().length) {
|
||||
System.out.println("有重复枚举标准链接");
|
||||
} else {
|
||||
System.out.println("标准链接无重复");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
28
parser/src/test/java/cn/qaiu/parser/ParserUrlOut.java
Normal file
28
parser/src/test/java/cn/qaiu/parser/ParserUrlOut.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import cn.qaiu.parser.impl.PodTool;
|
||||
|
||||
public class ParserUrlOut {
|
||||
|
||||
//https://onedrive.live.com/redir?resid=ABFD0A26E47D3458!4699&e=OggA4s&migratedtospo=true&redeem=aHR0cHM6Ly8xZHJ2Lm1zL3UvcyFBbGcwZmVRbUN2MnJwRnZ1NDQ0aGc1eVZxRGNLP2U9T2dnQTRz
|
||||
public static void main(String[] args) {
|
||||
// Matcher matcher = redirectUrlRegex.matcher("https://onedrive.live.com/redir?resid=ABFD0A26E47D3458!4698" +
|
||||
// "&authkey=!ACpvXghP5xhG_cg&e=hV98W1");
|
||||
// if (matcher.find()) {
|
||||
// System.out.println(matcher.group("cid"));
|
||||
// System.out.println(matcher.group("cid2"));
|
||||
// System.out.println(matcher.group("authkey"));
|
||||
// }
|
||||
// appid 5cbed6ac-a083-4e14-b191-b4ba07653de2 5cbed6ac-a083-4e14-b191-b4ba07653de2
|
||||
// https://my.microsoftpersonalcontent.com/personal/abfd0a26e47d3458/_layouts/15/embed.aspx?UniqueId=e47d3458-0a26-20fd-80ab-5b1200000000&Translate=false&ApiVersion=2.0
|
||||
// https://my.microsoftpersonalcontent.com/personal/abfd0a26e47d3458/_layouts/15/embed.aspx?UniqueId=6b0900d6-abcf-44ce-b7bf-7b626bcbe4b8&Translate=false&ApiVersion=2.0
|
||||
// https://1drv.ms/u/s!Alg0feQmCv2rpFvu444hg5yVqDcK?e=OggA4s
|
||||
// https://1drv.ms/u/c/abfd0a26e47d3458/EVg0feQmCv0ggKtbEgAAAAABqGv8K6HmOwLRsvokyV5fUg?e=iqoRc0
|
||||
// https://1drv.ms/u/c/abfd0a26e47d3458/EVg0feQmCv0ggKtaEgAAAAAB-lF1qjkfv5OqdrT9VSMDMw
|
||||
new PodTool(ShareLinkInfo.newBuilder().shareUrl("https://1drv.ms/u/c/abfd0a26e47d3458/EdYACWvPq85Et797YmvL5LgBruUKoNxqIFATXhIv1PI2_Q")
|
||||
.build())
|
||||
.parse().onSuccess(System.out::println);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package qaiu.web.test;
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Vertx;
|
||||
@@ -1,4 +1,4 @@
|
||||
package qaiu.web.test;
|
||||
package cn.qaiu.util;
|
||||
|
||||
import cn.qaiu.util.AESUtils;
|
||||
import org.junit.Assert;
|
||||
@@ -18,7 +18,11 @@ public class TestAESUtil {
|
||||
@Test
|
||||
public void decode() throws NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException,
|
||||
BadPaddingException, InvalidKeyException {
|
||||
String hex = AESUtils.encryptHexByAES("1686215935703", AESUtils.CIPHER_AES2);
|
||||
String hex = AESUtils.encryptBase64ByAES("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8asrfSaoOb4je+DSmKdriQJKW\n" +
|
||||
"VJ2oDZrs3wi5W67m3LwTB9QVR+cE3XWU21Nx+YBxS0yun8wDcjgQvYt625ZCcgin\n" +
|
||||
"2ro/eOkNyUOTBIbuj9CvMnhUYiR61lC1f1IGbrSYYimqBVSjpifVufxtx/I3exRe\n" +
|
||||
"ZosTByYp4Xwpb1+WAQIDAQAB", AESUtils.CIPHER_AES2);
|
||||
System.out.println(hex);
|
||||
Assert.assertEquals("B4C5B9833113ACA41F16AABADE17349C", hex.toUpperCase());
|
||||
}
|
||||
|
||||
32
parser/src/test/java/cn/qaiu/util/TestRegex.java
Normal file
32
parser/src/test/java/cn/qaiu/util/TestRegex.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TestRegex {
|
||||
|
||||
@Test
|
||||
public void regexYFC() {
|
||||
String html = """
|
||||
true|
|
||||
<div class="load" id="go"><a
|
||||
href="https://nsub2d3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
|
||||
onclick="down_process2('1228264');" target="_blank"><span class="txt">极速下载</span></a><a
|
||||
href="https://nsub2t3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
|
||||
onclick="down_process2('1228264');" target="_blank"><span class="txt txtc">电信下载</span></a><a
|
||||
href="https://nsub2u3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
|
||||
onclick="down_process2('1228264');" target="_blank"><span class="txt">联通下载</span></a><a
|
||||
href="https://nsub2d3.118pan.com/dl.php?YzljNGFnOHFabWpsdzVXNUhyOXNiQ0ZYMHEzUjFoQ0R6MExCckdKZmRQL1RXbERPSTUwS0xlZkUvdmN5dGNqTHhCNDJVZURFekthV0tROHNINXdNRkZYUXB5aGQ5TEdXeTgrNkc1TkZwOWhzYVdpak83dEZleWxpYnloc0c1Ky9wbVBSZ0xiSHo5aTBnVVpCRmpyMGFBRERjcldtc0FnbS9vMnd3ZGRHWU4zYTNzNncwbk56UEtOVWhFeGIvWlM1aWZLQjlaTjB3b2V0cGVrb2lpQ0x3U0dtQkJqd2plWk1yeU1mZG1oNXpRYUJpYmFhZVgvWFZScThsTFhmU1ZKRW9UT3haVHFMVGVRTUhR"
|
||||
onclick="down_process2('1228264');" target="_blank"><span class="txt txtc">普通下载</span></a></div>
|
||||
""";
|
||||
|
||||
Pattern compile = Pattern.compile("href=\"([^\"]+)\"");
|
||||
Matcher matcher = compile.matcher(html);
|
||||
if (matcher.find()) {
|
||||
// System.out.println(matcher.group(0));
|
||||
System.out.println(matcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
19
parser/src/test/java/cn/qaiu/util/URLUtilTest.java
Normal file
19
parser/src/test/java/cn/qaiu/util/URLUtilTest.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package cn.qaiu.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class URLUtilTest {
|
||||
|
||||
@Test
|
||||
public void getParam() {
|
||||
|
||||
URLUtil util = URLUtil.from("https://i.y.qq.com/v8/playsong.html?ADTAG=cbshare&_wv=1&appshare=android_qq" +
|
||||
"&appsongtype=1&appversion=13100008&channelId=10036163&hosteuin=7iosow-s7enz&openinqqmusic=1&platform" +
|
||||
"=11&songmid=000XjcLg0fbRjv&type=0");
|
||||
Assert.assertEquals(util.getParam("songmid"), "000XjcLg0fbRjv");
|
||||
Assert.assertEquals(util.getParam("type"), "0");
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package qaiu.web.test;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TestRegex {
|
||||
|
||||
@Test
|
||||
public void regexYFC() {
|
||||
String html = """
|
||||
<input type="hidden" id="typed_id" value="file_559003251828">
|
||||
<input type="hidden" id="share_link_token" value="9cbe4b73521ba4d65a8cd38a8c">
|
||||
""";
|
||||
|
||||
Pattern compile = Pattern.compile("id=\"typed_id\"\\s+value=\"file_(\\d+)\"");
|
||||
Matcher matcher = compile.matcher(html);
|
||||
if (matcher.find()) {
|
||||
System.out.println(matcher.group(0));
|
||||
System.out.println(matcher.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package qaiu.web.test;
|
||||
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TestWebClient2 {
|
||||
|
||||
@Test
|
||||
public void matcherHtml() {
|
||||
|
||||
Pattern compile = Pattern.compile("class=\"ifr2\" name=.+src=\"(/fn\\?[a-zA-Z0-9_+/=]{16,})\"");
|
||||
var text = """
|
||||
<div class="ifr"><!--<iframe class="ifr2" name="1" src="/fn?v2" frameborder="0" scrolling="no"></iframe>-->
|
||||
<iframe class="ifr2" name="1685001208" src="/fn?UzUBa1oxBmUAYgNsUDUFNVI6BjJfJlchV21TZFU_aVWwANVQzXTBXMlUxUTcLZ1dwUn8DYwQ5AHFVOwdmBjRUPlM2AS9aOgY3AGIDMFA2" frameborder="0" scrolling="no"></iframe>""";
|
||||
System.out.println(text);
|
||||
Matcher matcher = compile.matcher(text);
|
||||
if (matcher.find()) {
|
||||
System.out.println(matcher.group(0));
|
||||
System.out.println(matcher.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lzClient() {
|
||||
Vertx vertx = Vertx.vertx();
|
||||
WebClient client = WebClient.create(vertx);
|
||||
MultiMap form = MultiMap.caseInsensitiveMultiMap();
|
||||
|
||||
|
||||
// action=downprocess&sign=AGYHOQk4BjdTWgQ7BzcGOlU_bATVSNQMxBDFQZgZoBj4HMFEgWnMOZ1I1A2NWOgUxB20HMlM_aUGoLOgQz&p=e4k4
|
||||
form.set("action", "downprocess");
|
||||
form.set("sign", "VzFWaA4_aBzYIAQI9ADBUaARvATVRNlNhUGUBNwBuATkDNFEgXHVVPAZhB2dTP1ZiVD5UYQBpV2EBPwA3");
|
||||
form.set("p", "e4k4");
|
||||
client.postAbs("https://wwsd.lanzoue.com/ajaxm.php")
|
||||
.putHeader("referer", "https://wwsd.lanzoue.com/iFhd00x8k0kh")
|
||||
.sendForm(form).onSuccess(res -> {
|
||||
JsonObject jsonObject = res.bodyAsJsonObject();
|
||||
System.out.println(jsonObject);
|
||||
|
||||
vertx.close();
|
||||
});
|
||||
|
||||
//
|
||||
// https://developer.lanzoug.com/file/?VTMBPwEwU2IGD1dvV2ICblBvU2sENQZhVSZSMAA1WihSOVYsDTZTZlN2UXMFfAZjBzJXJQAxAG5XP1UyXGQGKlVlAXgBbVMpBmNXLFdhAmpQZFN4BCEGbVUiUnIAOloyUj5WZA0PU25TYVE6BWAGNgdlV2IAbQAyV2JValw3BiFVMwElAWFTNgZmVzBXMwIyUDpTYARrBiJVIlIkAGFaaVJiVjMNY1MoUzVRMgV+BjUHaFd9ADwAMVdlVTFcOAYyVWcBYgFqUz4GaVdlVzMCNFBrUzcEOAZgVWJSZQA/WmJSM1Y2DWhTNFMzUTEFYgY3B2VXZgBxAHhXOVUjXCYGclUmATMBLlNuBjRXPFcyAjNQP1NvBG8GPVVqUnIAKFoyUj9WZA02UzpTNFExBWkGMgdtV2IAaQAxV2FVYFwuBilVcwEwATBTcAZtVzBXNQI7UD9TZgRrBjFVY1JlAGlafVInVnENJ1M6UzRRMQVpBjIHbVdiAG0AMldgVWRcJgZyVTwBJgFhUzYGYVczVy0CMVA5U2QEdQY1VWZSYgByWmxSag==
|
||||
// https://developer.lanzoug.com/file/?B2FWaA4/BDVTWgc/UWRVOQQ7BT1VZFYxUSJUNgE0UiAEbwJ4CDMOOwInU3EKc1w5ATRSIAIzUz1ROVcwATkDLwc3Vi8OYgR+UzYHfFFnVT0EMAUuVXBWPVEmVHQBO1I6BGgCMAgKDjMCMFM4Cm9cbAFjUmcCb1NhUWRXaAFqAyQHYVZyDm4EYVMzB2BRNVVlBG4FNlU6VnJRJlQiAWBSYQQ0AmcIZg51AmRTMApxXG8BblJ4Aj5TYlFjVzMBZQM3BzVWNQ5lBGlTPAc1UTVVYwQ/BWFVaVYwUWZUYwE+UmoEZQJiCG0OaQJiUzMKbVxtAWNSYwJzUytRP1chAXsDdwd0VmQOIQQ5U2EHbFE0VWQEawU5VT5WbVFuVHQBKVI6BGkCMAgzDmcCZVMzCmZcaAFrUmcCaFNlUWNXZwFzAywHIVZnDj8EJ1M4B2BRM1VsBGsFMFU6VmZRb1RgAW5SdQRxAiUIIg5nAmVTMwpmXGgBa1JnAm9TYVFmV2YBewN3B25WcQ5uBGFTNAdjUStVZgRtBTJVJFZlUWJUZAFzUmQEPA==
|
||||
// https://developer.lanzoug.com/file/?CW9WaAk4BzZUXVRsCz5cMAE+Bj5UZVM0USJUNlRhA3FUPwJ4CTJUYQInASMHflI3ATQGdFdmAW9ROQFmVGwEKAk5Vi8JZQd9VDFULws9XDQBNQYtVHFTOFEmVHRUbgNrVDgCMAkLVGkCMAFqB2JSYgFjBjNXOgEzUWQBPlQ/BCMJb1ZyCWkHYlQ0VDMLb1xsAWsGNVQ7U3dRJlQiVDUDMFRkAmcJZ1QvAmQBYgd8UmEBbgYsV2sBMFFjAWVUMAQwCTtWNQliB2pUO1RmC29cagE6BmJUaFM1UWZUY1RrAztUNQJiCWxUMwJiAWEHYFJjAWMGN1cmAXlRPwF3VC4EcAl6VmQJJgc6VGZUPwtuXG0BbgY6VD9TaFFuVHRUfANrVDkCMAkyVD0CZQFhB2tSZgFrBjNXPgE6UWABM1QmBCsJL1ZnCTgHJFQ/VDMLaVxlAW4GM1Q7U2RRb1RmVDgDJFQhAiUJI1Q9AmUBYQdrUmYBawYzVzoBM1FmATBULgRwCWBWcQlpB2JUM1QwC3FcbwFoBjFUJVNgUWJUZFQmAzVUbA==
|
||||
}
|
||||
}
|
||||
8
pom.xml
8
pom.xml
@@ -25,15 +25,15 @@
|
||||
|
||||
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
|
||||
|
||||
<slf4j.version>2.0.5</slf4j.version>
|
||||
<vertx.version>4.5.6</vertx.version>
|
||||
<org.reflections.version>0.10.2</org.reflections.version>
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
<slf4j.version>2.0.5</slf4j.version>
|
||||
<commons-lang3.version>3.12.0</commons-lang3.version>
|
||||
<commons-lang3.version>3.18.0</commons-lang3.version>
|
||||
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
|
||||
<jackson.version>2.14.2</jackson.version>
|
||||
<logback.version>1.4.12</logback.version>
|
||||
<logback.version>1.5.8</logback.version>
|
||||
<junit.version>4.13.2</junit.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
2
web-front/.gitignore
vendored
2
web-front/.gitignore
vendored
@@ -23,3 +23,5 @@ pnpm-debug.log*
|
||||
*.sw?
|
||||
/nfd-front.zip
|
||||
/nfd-front
|
||||
/package-lock.json
|
||||
/yarn.lock
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
# nfd-web
|
||||
当前页面修改自开源项目 https://github.com/HurryBy/CloudDiskAnalysis
|
||||
使用vue3+element-plus打造
|
||||
解析服务的前端页面, 提供API测试, 统计查询, 二维码生成等;
|
||||
20241101 支持剪切板链接自动识别解析, 一键生成短链
|
||||
20241111 vue框架升级为3.0
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 关于如何将前端项目和java一块打包:
|
||||
1. 先打包前端模块
|
||||
2. 运行`npm run build`
|
||||
3. 项目部署后演示页面的代理端口是6401默认使用http, 如需https可以加nginx代理, 也可以使用本项目自带的代理服务和配置证书路径
|
||||
|
||||
## nginx配置
|
||||
```nginx
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6401;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
## Project setup
|
||||
```
|
||||
npm install
|
||||
@@ -24,3 +45,8 @@ npm run lint
|
||||
|
||||
### Customize configuration
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
||||
|
||||
|
||||
## 参考项目
|
||||
- https://github.com/HurryBy/CloudDiskAnalysis
|
||||
- https://github.com/syhyz1990/panAI
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user