diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 17d19dcba4..49fef9e88d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ -name: '错误报告' -description: '报告与 PeerBanHelper 有关的程序错误' +name: '错误报告 - Bug Report' +description: '报告与 PeerBanHelper 有关的程序错误 - Report the errors that related to PeerBanHelper' title: '[BUG] ' labels: @@ -8,42 +8,49 @@ body: - type: 'markdown' attributes: value: |- - ## 请注意 + ## 请注意 - Caution + This form only used for bug report, for any other cases, please [click here](https://github.com/PBH-BTN/PeerBanHelper/issues/new) 此表单**仅用于反馈错误**,如果是其它类型的反馈,请[点击这里](https://github.com/PBH-BTN/PeerBanHelper/issues/new)。 - + If you think the error is related PBH WebUI, please [report to here](https://github.com/PBH-BTN/pbh-fe) + 如果你认为此错误是一个 PBH WebUI,请[在此反馈](https://github.com/PBH-BTN/pbh-fe)。 请尽可能完整且详细地填写所有表单项,以便我们以最高效率并准确的排查故障和诊断问题 - type: 'textarea' attributes: - label: '版本号' + label: '版本号 - Version' description: |- + Enter the PBH version that display on WebUI footer or GUI window title. 输入您正在使用 PeerBanHelper 的版本号,通常可在窗口标题或者 WebUI 页面的底部找到 placeholder: 'vX.X.X' validations: required: true - type: 'textarea' attributes: - label: '操作系统平台和系统架构' + label: '操作系统平台和系统架构 - OS and CPU Arch' description: |- + Enter the OS version/arch that PBH running on (not downloader), E.g Windows, Debian, iStoreOS. 输入 PBH 所在的操作系统平台(不是下载器),例如:Windows、Debian、iStoreOS 等 + And please also enter the system arch if you know it, E.g x86, arm64 此外,您还需要输入系统架构。如果是 x86 设备,则通常为 x64;如果是 arm 设备,则通常为 arm64。请根据实际情况填写。如果不知道,也可以不写系统架构类型。 placeholder: '操作系统平台名称……' validations: required: true - type: 'textarea' attributes: - label: '部署方式' + label: '部署方式 - Deploy method' description: |- + Enter the deploy method that you're using: 输入您部署 PeerBanHelper 方式,官方支持的有如下几种方式: - * Windows 安装程序(通过 .exe 安装) - * Windows 绿色懒人包(解压即用的 .zip 文件) - * Docker 镜像 + * Windows 安装程序(通过 .exe 安装) (Windows .EXE Installer) + * Windows 绿色懒人包(解压即用的 .zip 文件) (Windows .ZIP Portable) + * Docker 镜像 (Docker Container) placeholder: '部署方式……' validations: required: true - type: 'textarea' attributes: - label: '关联的下载器类型' + label: '关联的下载器类型 - Downloader Type' description: |- + Enter the downloader type that you trying to connecting/connected to PBH (E.g): 输入您的 PBH 关联的下载器类型,例如: * qBittorrent * Transmission @@ -54,15 +61,18 @@ body: required: true - type: 'textarea' attributes: - label: '问题描述' + label: '问题描述 - Issue Description' description: |- + Describe the problem you encounted. 在此详细的描述你所遇到的问题 validations: required: true - type: 'textarea' attributes: - label: '复现步骤' - description: '如果你清楚如何复现此故障,也欢迎告诉我们,帮助我们更快的复现它。如果它是一个偶尔才会出现的错误,请告诉我们它通常可能会在什么情况下出现。' + label: '复现步骤 - Reproduce steps' + description: |- + If you know how to reproduce the error, please type it in this text area. + 如果你清楚如何复现此故障,也欢迎告诉我们,帮助我们更快的复现它。如果它是一个偶尔才会出现的错误,请告诉我们它通常可能会在什么情况下出现。 placeholder: |- 1. 第一步 2. ... @@ -71,31 +81,36 @@ body: required: true - type: 'textarea' attributes: - label: '截图/日志文件' - description: '如果你有一些截图或者日志能够更好的解释你所提出的问题,你可以在这里上传。' + label: '截图/日志文件 - Screenshot / Logs ' + description: |- + If you have some screenshot or logs file can help us, please upload them here. + 如果你有一些截图或者日志能够更好的解释你所提出的问题,你可以在这里上传。 placeholder: '<截图文件>' validations: required: false - type: 'textarea' attributes: - label: '额外信息' - description: '如果你还有其他觉得可能对排查和解决此问题有帮助的更多信息,可以在这里告诉我们' + label: '额外信息 - Addition Information' + description: |- + If you have any related informations, please insert them into this text area. + 如果你还有其他觉得可能对排查和解决此问题有帮助的更多信息,可以在这里告诉我们 placeholder: '在此填写可能有用的额外信息...' - type: checkboxes id: check-list attributes: - label: 检查清单 - description: 请检查并勾选下面的所有的复选框,如果您没有这样做,我们可能会直接关闭这个 Issue + label: 检查清单 - Check list + description: |- + Check and tick checkboxes that listed below options: - - label: "我确定正在运行 Github Releases 中的最新的正式版本 PeerBanHelper" + - label: "我确定正在运行 Github Releases 中的最新的正式版本 PeerBanHelper (I'm running the latest version of PBH that can be found in Github Relases)" required: false - - label: "我确定我所添加的下载器已满足 README 中的前置要求(如版本号和插件)" + - label: "我确定我所添加的下载器已满足 README 中的前置要求(如版本号和插件)(The downloaders that I've added already satisfied the requirements (E.g install plugins/adapters))" required: false - - label: "我确定我所提到的问题,均未在 README 和 WIKI 中有所解答" + - label: "我确定我所提到的问题,均未在 README 和 WIKI 中有所解答 (This not a question/or the question that not listed in README's FAQ or WIKI)" required: false - - label: "我确定我没有检查这个检查清单,只是闭眼选中了所有的复选框" + - label: "我确定我没有检查这个检查清单,只是闭眼选中了所有的复选框 (I have not read these checkboxes and therefore I just ticked them all)" required: false - - label: "我确定这不是一个与安全有关的安全漏洞,它可以被安全的公开报告" + - label: "我确定这不是一个与安全有关的安全漏洞,它可以被安全的公开报告 (This not a security related issue, can be safe report in public)" required: false - - label: "我确定我已知悉,如果我没有正确地填写问题报告表单,则 Issue 可能会被关闭" + - label: "我确定我已知悉,如果我没有正确地填写问题报告表单,则 Issue 可能会被关闭 (I know this issue may closed without any warnings if I didn't fill the form correctly)" required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..0cbc137cbd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +blank_issues_enabled: true +contact_links: + - name: 'WebUI 错误报告 - WebUI Bug Report' + about: |- + 报告与 PBH WebUI 有关的错误 + Report the errors that related to PBH WebUI. + url: 'https://github.com/PBH-BTN/pbh-fe/issues/new' diff --git a/.github/workflows/jvm-ci.yml b/.github/workflows/jvm-ci.yml index 5e16718258..5f9ef06936 100644 --- a/.github/workflows/jvm-ci.yml +++ b/.github/workflows/jvm-ci.yml @@ -10,9 +10,9 @@ name: Java CI on: push: - branches: [ "master", "release" ] + branches: [ "master", "major-refactor", "release" ] pull_request: - branches: [ "master", "release" ] + branches: [ "master", "major-refactor", "release" ] workflow_dispatch: release: types: @@ -31,8 +31,13 @@ jobs: distribution: "temurin" java-version: "21" cache: "maven" + - uses: luangong/setup-install4j@v1 + name: Setup Install4j + with: + version: 10.0.8 + license: ${{ secrets.INSTALL4J_LICENSE }} - name: Build with Maven - run: mvn -B clean package --file pom.xml + run: mvn -B clean package --file pom.xml -P install4j-ci,thin-sqlite-packaging - name: Upload build artifacts uses: actions/upload-artifact@v4 with: @@ -40,6 +45,7 @@ jobs: path: | target/*.jar target/peerbanhelper-binary + target/media/*.exe id: project - name: Set Up QEMU uses: docker/setup-qemu-action@v3 @@ -65,7 +71,7 @@ jobs: type=raw,ci type=sha - name: Build and push Docker image - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v6.4.0 with: context: . file: ./Dockerfile-CI @@ -75,3 +81,4 @@ jobs: linux/arm64/v8 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }}-jvm-universal + diff --git a/.github/workflows/jvm-release.yml b/.github/workflows/jvm-release.yml index c49c40dbb9..8a69ae444e 100644 --- a/.github/workflows/jvm-release.yml +++ b/.github/workflows/jvm-release.yml @@ -15,6 +15,15 @@ on: - published jobs: build: + permissions: + contents: write + checks: write + actions: read + issues: read + packages: write + pull-requests: read + repository-projects: read + statuses: read runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -27,8 +36,13 @@ jobs: distribution: 'temurin' java-version: '21' cache: 'maven' + - uses: luangong/setup-install4j@v1 + name: Setup Install4j + with: + version: 10.0.8 + license: ${{ secrets.INSTALL4J_LICENSE }} - name: Build with Maven - run: mvn -B clean package --file pom.xml + run: mvn -B clean package --file pom.xml -P install4j-ci,thin-sqlite-packaging - name: Upload build artifacts uses: actions/upload-artifact@v4 with: @@ -36,6 +50,13 @@ jobs: path: | target/*.jar target/peerbanhelper-binary + target/media/*.exe + - name: Upload release binaries + uses: alexellis/upload-assets@0.4.1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + asset_paths: '["target/PeerBanHelper.jar", "target/media/PeerBanHelper_*"]' id: project - name: Set Up QEMU uses: docker/setup-qemu-action@v3 @@ -61,7 +82,7 @@ jobs: type=raw,latest type=sha - name: Build and push Docker image - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v6.4.0 with: context: . file: ./Dockerfile @@ -93,7 +114,7 @@ jobs: type=raw,latest type=sha - name: Build and push Aliyun ACR - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v6.4.0 with: context: . file: ./Dockerfile @@ -102,4 +123,4 @@ jobs: linux/amd64 linux/arm64/v8 tags: ${{ steps.meta-acr.outputs.tags }} - labels: ${{ steps.meta-acr.outputs.labels }}-jvm-universal \ No newline at end of file + labels: ${{ steps.meta-acr.outputs.labels }}-jvm-universal diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 3263bb5f22..6a62beb9c4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v5 + - uses: actions/stale@v9 with: # issues only-issue-labels: "waiting-reply" diff --git a/Dockerfile b/Dockerfile index d198b31238..7d24cd8058 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM docker.io/maven:3.9.6-eclipse-temurin-21 as build +FROM --platform=$BUILDPLATFORM docker.io/maven:3.9.8-eclipse-temurin-21 as build COPY . /build WORKDIR /build @@ -12,4 +12,4 @@ WORKDIR /app VOLUME /tmp COPY --from=build build/target/PeerBanHelper.jar /app/PeerBanHelper.jar ENV PATH "${JAVA_HOME}/bin:${PATH}" -ENTRYPOINT ["java","-Xmx256M","-XX:+UseG1GC", "-XX:+UseStringDeduplication","-XX:+ShrinkHeapInSteps","-jar","PeerBanHelper.jar"] \ No newline at end of file +ENTRYPOINT ["java","-Xmx386M","-XX:+UseG1GC", "-XX:+UseStringDeduplication","-XX:+ShrinkHeapInSteps","-jar","PeerBanHelper.jar"] \ No newline at end of file diff --git a/Dockerfile-CI b/Dockerfile-CI index 28f96f9e21..d08ff845a1 100644 --- a/Dockerfile-CI +++ b/Dockerfile-CI @@ -6,4 +6,4 @@ ENV TZ=UTC WORKDIR /app VOLUME /tmp ENV PATH "${JAVA_HOME}/bin:${PATH}" -ENTRYPOINT ["java","-Xmx256M","-XX:+UseG1GC", "-XX:+UseStringDeduplication","-XX:+ShrinkHeapInSteps","-jar","PeerBanHelper.jar"] \ No newline at end of file +ENTRYPOINT ["java","-Xmx386M","-XX:+UseG1GC", "-XX:+UseStringDeduplication","-XX:+ShrinkHeapInSteps","-jar","PeerBanHelper.jar"] \ No newline at end of file diff --git a/README.md b/README.md index 6693cfa2bb..249c077193 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ * Transmission **(3.00-20 或更高版本)** * BiglyBT(需要安装[插件](https://github.com/PBH-BTN/PBH-Adapter-BiglyBT)) * Deluge(需要安装[插件](https://github.com/PBH-BTN/PBH-Adapter-Deluge)) +* Azureus(Vuze)(需要安装[插件](https://github.com/PBH-BTN/PBH-Adapter-Azureus)) ## 功能介绍 @@ -36,11 +37,14 @@ PeerBanHelper 主要由以下几个功能模块组成: * Client Name 黑名单 * IP 黑名单 * 虚假进度检查器(提供启发式客户端检测功能)(Transmission不支持过量下载检测) -* 主动探测 * 自动 IP 段封禁 * 多拨追猎 * Peer ID/Client Name 伪装检查 -* WebUI (目前支持:活跃封禁名单查看,历史封禁查询,封禁最频繁的 Top 50 IP) +* WebUI (目前支持:活跃封禁名单查看,历史封禁查询,封禁最频繁的 Top 50 IP,规则订阅管理,图表查看,Peer 列表查看) + +如果配置了 Maxmind IP 库,则还支持以下内容: + +* 在封禁列表中查看 IP 归属地 ### PeerID 黑名单 diff --git a/docker-compose.yml b/docker-compose.yml index 33fe8507cb..193d45f678 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,4 +11,5 @@ services: environment: - PUID=0 - PGID=0 - - TZ=UTC \ No newline at end of file + - TZ=UTC + stop_grace_period: 30s \ No newline at end of file diff --git a/install4j/icon.ico b/install4j/icon.ico new file mode 100644 index 0000000000..7530b24998 Binary files /dev/null and b/install4j/icon.ico differ diff --git a/install4j/icon.png b/install4j/icon.png new file mode 100644 index 0000000000..b9033555c5 Binary files /dev/null and b/install4j/icon.png differ diff --git a/install4j/lang/custom.utf8 b/install4j/lang/custom.utf8 new file mode 100644 index 0000000000..b23bd54813 --- /dev/null +++ b/install4j/lang/custom.utf8 @@ -0,0 +1,6 @@ +launcher.peerbanhelper.gui=PeerBanHelper +launcher.peerbanhelper.gui.swing=PeerBanHelper(兼容模式) +launcher.peerbanhelper.nogui=PeerBanHelper(无GUI, 控制台) +launcher.peerbanhelper.service=PeerBanHelper(服务) +checkbox.followsystemstartup=登录时自动启动到系统托盘 +peerbanhelper.description=PeerBanHelper \ No newline at end of file diff --git a/install4j/lang/en-US.utf8 b/install4j/lang/en-US.utf8 new file mode 100644 index 0000000000..d9be99bcba --- /dev/null +++ b/install4j/lang/en-US.utf8 @@ -0,0 +1,6 @@ +launcher.peerbanhelper.gui=PeerBanHelper +launcher.peerbanhelper.gui.swing=PeerBanHelper(Compatibility Mode) +launcher.peerbanhelper.nogui=PeerBanHelper(NoGUI, Console) +launcher.peerbanhelper.service=PeerBanHelper(Service) +checkbox.followsystemstartup=Boot automatically to the system tray when logged in +peerbanhelper.description=PeerBanHelper \ No newline at end of file diff --git a/install4j/lang/zh-CN.utf8 b/install4j/lang/zh-CN.utf8 new file mode 100644 index 0000000000..6206ed964d --- /dev/null +++ b/install4j/lang/zh-CN.utf8 @@ -0,0 +1,6 @@ +launcher.peerbanhelper.gui=PeerBanHelper +launcher.peerbanhelper.gui.swing=PeerBanHelper(兼容模式) +launcher.peerbanhelper.nogui=PeerBanHelper(无GUI, 控制台) +launcher.peerbanhelper.service=PeerBanHelper(服务) +checkbox.followsystemstartup=登录到桌面时自动启动 +peerbanhelper.description=一个能够自动封禁不受欢迎、吸血和异常的 Peers,并支持自定义规则的 BT 客户端辅助工具 \ No newline at end of file diff --git a/install4j/project.install4j b/install4j/project.install4j new file mode 100644 index 0000000000..3a0e174d2b --- /dev/null +++ b/install4j/project.install4j @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sys.installationDir + + + context.getBooleanVariable("sys.confirmedUpdateInstallation") + + + + + + ${form:welcomeMessage} + + !context.isConsole() + + + + + + String message = context.getMessage("ConsoleWelcomeLabel", context.getApplicationName()); +return console.askOkCancel(message, true); + + + + + + + + updateCheck + + + + + ${i18n:ClickNext} + + + + + + !context.getBooleanVariable("sys.confirmedUpdateInstallation") + + + + + sys.installationDir + + + context.getVariable("sys.responseFile") == null + + + + + + ${i18n:SelectDirLabel(${compiler:sys.fullName})} + + + + + + + + suggestAppDir + validateApplicationId + existingDirWarning + checkWritable + manualEntryAllowed + checkFreeSpace + showRequiredDiskSpace + showFreeDiskSpace + allowSpacesOnUnix + validationScript + standardValidation + + + + + + + + + ${i18n:SelectComponentsLabel2} + + !context.isConsole() + + + + + + + selectionChangedScript + + + + + + + + + ${form:confirmationMessage} + + !context.isConsole() + + + + ${i18n:CreateDesktopIcon} + createDesktopLinkAction + + + + + ${i18n:checkbox.followsystemstartup} + startupWhenLoggedIn + + + + + + + + + + ${i18n:UninstallerMenuEntry(${compiler:sys.fullName})} + + !context.getBooleanVariable("sys.programGroupDisabled") + + + + + + + PeerBanHelper-GUI + + + ${compiler:sys.fullName} + + + . + + + + + ./icon.png + + + + + ./icon.ico + + + + context.getBooleanVariable("createDesktopLinkAction") + + + + + + PeerBanHelper-GUI-Silent + + + PeerBanHelper + + context.getBooleanVariable("startupWhenLoggedIn") + + + + ${compiler:sys.fullName} ${compiler:sys.version} + + + + + + + ${i18n:WizardPreparing} + + + + + + + + + ${form:finishedMessage} + + + + + + + + + ${i18n:UninstallerMenuEntry(${compiler:sys.fullName})} + + + + + + + + + + + + + + + + ${form:welcomeMessage} + + !context.isConsole() + + + + + + String message = context.getMessage("ConfirmUninstall", context.getApplicationName()); +return console.askYesNo(message, true); + + + + + + + + + + + + + + + ${i18n:UninstallerPreparing} + + + + + + + + + + ${form:successMessage} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 5fcc768899..d8eb2b65e1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.ghostchu.peerbanhelper peerbanhelper - 4.4.0 + 5.0.0 takari-jar PeerBanHelper @@ -17,20 +17,24 @@ com.ghostchu.peerbanhelper.MainJumpLoader yyyyMMdd-HHmmss 3.4.1 - 5.8.27 22.0.1 provided - 126.2.0 - 126.2.0 - + /opt/install4j + 6.1 + + + ej-technologies + https://maven.ej-technologies.com/repository + + org.apache.maven.plugins maven-compiler-plugin - 3.12.1 + 3.13.0 true true @@ -41,7 +45,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.0 org.apache.logging.log4j @@ -78,17 +82,18 @@ + *:* - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - META-INF/*.kotlin_module - META-INF/*.txt - META-INF/proguard/* - META-INF/services/* - META-INF/versions/9/* - *License* - *LICENSE* + + + + + + + + + + @@ -103,7 +108,7 @@ io.github.git-commit-id git-commit-id-maven-plugin - 5.0.0 + 5.0.1 get-the-git-infos @@ -137,7 +142,7 @@ io.takari.maven.plugins takari-lifecycle-plugin - 2.1.5 + 2.1.6 true proc @@ -155,7 +160,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.5.3 + 3.6.0 @@ -187,7 +192,7 @@ io.github.git-commit-id git-commit-id-maven-plugin - 5.0.0 + 5.0.1 get-the-git-infos @@ -220,6 +225,21 @@ io.takari.maven.plugins takari-lifecycle-plugin + + maven-dependency-plugin + 3.7.1 + + + generate-sources + + build-classpath + + + pbh.classpath + + + + @@ -233,6 +253,9 @@ src/main/resources/static + + .git/ + static false @@ -306,6 +329,114 @@ compile + + install4j-ci + + + + com.install4j + install4j-maven + 10.0.8 + + + compile-installers + package + + compile + + + ${install4j.home} + ${project.basedir}/install4j/project.install4j + + ${project.basedir}/target/PeerBanHelper.jar + + + + + + + + + + install4j-dev + + + + com.install4j + install4j-maven + 10.0.8 + + + compile-installers + package + + compile + + + ${install4j.home} + ${project.basedir}/install4j/project.install4j + + ${project.basedir}/target/PeerBanHelper.jar + + + + + + + + + C:\Program Files\install4j10 + + + + + thin-sqlite-packaging + + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.1.0 + + + + + package + + shade + + + + + *:* + + org/sqlite/native/Linux-Android/** + org/sqlite/native/Linux/x86/** + org/sqlite/native/Linux/arm/** + org/sqlite/native/Linux/armv7/** + org/sqlite/native/Linux/armv6/** + org/sqlite/native/Windows/armv7/** + org/sqlite/native/Windows/x86/** + org/sqlite/native/Linux-Musl/x86/** + org/sqlite/native/FreeBSD/x86/** + + + + + + + + + + @@ -337,25 +468,25 @@ org.bspfsystems yamlconfiguration - 2.0.1 + 2.0.2 compile org.projectlombok lombok - 1.18.30 + 1.18.34 provided com.google.code.gson gson - 2.10.1 + 2.11.0 com.google.guava guava - 33.0.0-jre + 33.2.1-jre org.jetbrains @@ -367,17 +498,17 @@ com.github.seancfoley ipaddress - 5.4.2 + 5.5.0 org.slf4j slf4j-api - 2.0.12 + 2.0.13 org.xerial sqlite-jdbc - 3.45.2.0 + 3.46.0.0 com.zaxxer @@ -408,7 +539,7 @@ org.junit.jupiter junit-jupiter-api - 5.10.2 + 5.10.3 test @@ -425,7 +556,7 @@ io.javalin javalin - 6.1.3 + 6.1.6 @@ -470,6 +601,11 @@ ${javafx.version} ${javafx.scope} + + + + + com.googlecode.aviator aviator @@ -491,5 +627,53 @@ json 20240303 + + + org.springframework + spring-context + 6.1.11 + + + + com.j256.ormlite + ormlite-core + ${ormlite.version} + + + + com.j256.ormlite + ormlite-jdbc + ${ormlite.version} + + + fr.turri + aXMLRPC + 1.14.0 + + + com.ghostchu + simplereloadlib + 1.1.2 + + + + + + + + + + + + + + + + + + + + + diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000000..5db72dd6a9 --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/AppConfig.java b/src/main/java/com/ghostchu/peerbanhelper/AppConfig.java new file mode 100644 index 0000000000..ff7f4b471e --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/AppConfig.java @@ -0,0 +1,33 @@ +package com.ghostchu.peerbanhelper; + +import com.ghostchu.simplereloadlib.ReloadManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import java.io.File; + +@Configuration +@ComponentScan("com.ghostchu.peerbanhelper") +@Slf4j +public class AppConfig { + @Bean + public BuildMeta buildMeta() { + return Main.getMeta(); + } + + @Bean("banListFile") + public File banListFile() { + return new File(Main.getDataDirectory(), "banlist.dump"); + } + + @Bean("userAgent") + public String userAgent() { + return Main.getUserAgent(); + } + + public ReloadManager reloadManager() { + return Main.getReloadManager(); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/BuildMeta.java b/src/main/java/com/ghostchu/peerbanhelper/BuildMeta.java index fae8902535..00bd5df0d4 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/BuildMeta.java +++ b/src/main/java/com/ghostchu/peerbanhelper/BuildMeta.java @@ -8,7 +8,7 @@ @Data @NoArgsConstructor @AllArgsConstructor -public class BuildMeta { +public final class BuildMeta { private String version = "unknown"; private String os; private String branch; diff --git a/src/main/java/com/ghostchu/peerbanhelper/Main.java b/src/main/java/com/ghostchu/peerbanhelper/Main.java index a49c4b14b2..8e60b484e9 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/Main.java +++ b/src/main/java/com/ghostchu/peerbanhelper/Main.java @@ -10,9 +10,9 @@ import com.ghostchu.peerbanhelper.gui.impl.console.ConsoleGuiImpl; import com.ghostchu.peerbanhelper.gui.impl.javafx.JavaFxImpl; import com.ghostchu.peerbanhelper.gui.impl.swing.SwingGuiImpl; -import com.ghostchu.peerbanhelper.text.Lang; import com.ghostchu.peerbanhelper.util.PBHLibrariesLoader; import com.ghostchu.peerbanhelper.util.Slf4jLogAppender; +import com.ghostchu.simplereloadlib.ReloadManager; import com.google.common.eventbus.EventBus; import com.google.common.io.ByteStreams; import lombok.Getter; @@ -20,6 +20,11 @@ import org.apache.logging.log4j.core.config.plugins.util.PluginManager; import org.bspfsystems.yamlconfiguration.configuration.InvalidConfigurationException; import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.awt.*; import java.io.File; @@ -29,11 +34,17 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.Locale; import java.util.Map; import java.util.logging.Level; @Slf4j public class Main { + @Getter + private static final EventBus eventBus = new EventBus(); + @Getter + private static final ReloadManager reloadManager = new ReloadManager(); + public static String DEF_LOCALE = Locale.getDefault().toLanguageTag(); @Getter private static File dataDirectory; @Getter @@ -43,14 +54,10 @@ public class Main { private static File pluginDirectory; private static File libraryDirectory; @Getter - private static BuildMeta meta = new BuildMeta(); - @Getter private static PeerBanHelperServer server; @Getter private static PBHGuiManager guiManager; @Getter - private static final EventBus eventBus = new EventBus(); - @Getter private static File mainConfigFile; @Getter private static File profileConfigFile; @@ -58,34 +65,60 @@ public class Main { private static LibraryManager libraryManager; @Getter private static PBHLibrariesLoader librariesLoader; + @Getter + private static AnnotationConfigApplicationContext applicationContext; + @Getter + private static String pbhServerAddress; + @Getter + private static YamlConfiguration mainConfig; + @Getter + private static YamlConfiguration profileConfig; + @Getter + private static BuildMeta meta; + @Getter + private static String[] startupArgs; public static void main(String[] args) { + startupArgs = args; setupConfDirectory(args); setupLog4j2(); - setupProxySettings(); + Path librariesPath = dataDirectory.toPath().toAbsolutePath().resolve("libraries"); libraryManager = new PBHLibraryManager( new Slf4jLogAppender(), - dataDirectory.toPath(), "libraries" + Main.getDataDirectory().toPath(), "libraries" ); - Path librariesPath = dataDirectory.toPath().toAbsolutePath().resolve("libraries"); libraryManager.setLogLevel(LogLevel.ERROR); librariesLoader = new PBHLibrariesLoader(libraryManager, librariesPath); - initBuildMeta(); - initGUI(args); + meta = buildMeta(); setupConfiguration(); - guiManager.createMainWindow(); - mainConfigFile = new File(configDirectory, "config.yml"); - YamlConfiguration mainConfig = loadConfiguration(mainConfigFile); - new PBHConfigUpdater(mainConfigFile, mainConfig).update(new MainConfigUpdateScript(mainConfig)); + mainConfig = loadConfiguration(mainConfigFile); + new PBHConfigUpdater(mainConfigFile, mainConfig, Main.class.getResourceAsStream("/config.yml")).update(new MainConfigUpdateScript(mainConfig)); profileConfigFile = new File(configDirectory, "profile.yml"); - YamlConfiguration profileConfig = loadConfiguration(profileConfigFile); - new PBHConfigUpdater(profileConfigFile, profileConfig).update(new ProfileUpdateScript(profileConfig)); - String pbhServerAddress = mainConfig.getString("server.prefix", "http://127.0.0.1:" + mainConfig.getInt("server.http")); + profileConfig = loadConfiguration(profileConfigFile); + new PBHConfigUpdater(profileConfigFile, profileConfig, Main.class.getResourceAsStream("/profile.yml")).update(new ProfileUpdateScript(profileConfig)); + log.info("Current system language tag: {}", Locale.getDefault().toLanguageTag()); + DEF_LOCALE = mainConfig.getString("language"); + if (DEF_LOCALE == null || DEF_LOCALE.equalsIgnoreCase("default")) { + DEF_LOCALE = Locale.getDefault().toLanguageTag(); + } + DEF_LOCALE = DEF_LOCALE.toLowerCase(Locale.ROOT).replace("-", "_"); + initGUI(args); + guiManager.createMainWindow(); + pbhServerAddress = mainConfig.getString("server.prefix", "http://127.0.0.1:" + mainConfig.getInt("server.http")); + setupProxySettings(); try { - server = new PeerBanHelperServer(pbhServerAddress, profileConfig, mainConfig); + applicationContext = new AnnotationConfigApplicationContext(); + applicationContext.register(AppConfig.class); + applicationContext.refresh(); + registerBean(File.class, mainConfigFile, "mainConfigFile"); + registerBean(File.class, profileConfigFile, "profileConfigFile"); + registerBean(YamlConfiguration.class, mainConfig, "mainConfig"); + registerBean(YamlConfiguration.class, profileConfig, "profileConfig"); + server = applicationContext.getBean(PeerBanHelperServer.class); + server.start(); } catch (Exception e) { - log.error(Lang.BOOTSTRAP_FAILED, e); + log.error("Failed to startup PeerBanHelper, FATAL ERROR", e); throw new RuntimeException(e); } guiManager.onPBHFullyStarted(server); @@ -94,8 +127,23 @@ public static void main(String[] args) { } private static void setupProxySettings() { - if (System.getenv("http_proxy") != null || System.getenv("HTTP_PROXY") != null) { - log.warn(Lang.ALERT_INCORRECT_PROXY_SETTING); + var proxySection = mainConfig.getConfigurationSection("proxy"); + if (proxySection == null) return; + String host = proxySection.getString("host"); + String port = String.valueOf(proxySection.getInt("port")); + switch (proxySection.getInt("setting")) { + case 1 -> System.setProperty("java.net.useSystemProxies", "true"); + case 2 -> { + System.setProperty("http.proxyHost", host); + System.setProperty("http.proxyPort", port); + System.setProperty("https.proxyHost", host); + System.setProperty("https.proxyPort", port); + } + case 3 -> { + System.setProperty("socksProxyHost", host); + System.setProperty("socksProxyPort", port); + } + default -> System.setProperty("java.net.useSystemProxies", "false"); } } @@ -106,7 +154,7 @@ private static void setupConfDirectory(String[] args) { if (osName.contains("Windows")) { root = new File(System.getenv("LOCALAPPDATA"), "PeerBanHelper").getAbsolutePath(); } else { - root = new File(System.getProperty("user.home"), ".config/PeerBanHelper").getAbsolutePath(); + root = new File(new File(System.getProperty("user.home"), ".config"), "PeerBanHelper").getAbsolutePath(); } } if (System.getProperty("pbh.datadir") != null) { @@ -131,30 +179,46 @@ private static YamlConfiguration loadConfiguration(File file) { try { configuration.load(file); } catch (IOException | InvalidConfigurationException e) { - log.error(Lang.CONFIGURATION_INVALID, file); - guiManager.createDialog(Level.SEVERE, Lang.CONFIGURATION_INVALID_TITLE, String.format(Lang.CONFIGURATION_INVALID_DESCRIPTION, file)); + log.error("Unable to load configuration: invalid YAML configuration // 无法加载配置文件:无效的 YAML 配置,请检查是否有语法错误", e); + guiManager.createDialog(Level.SEVERE, "Invalid YAML configuration | 无效 YAML 配置文件", String.format("Failed to read configuration: %s", file)); System.exit(1); } return configuration; } private static void setupConfiguration() { - log.info(Lang.LOADING_CONFIG); + log.info("Loading configuration..."); try { - if (!initConfiguration()) { - guiManager.showConfigurationSetupDialog(); - System.exit(0); - } + initConfiguration(); + //guiManager.showConfigurationSetupDialog(); + //System.exit(0); } catch (IOException e) { - log.error(Lang.ERR_SETUP_CONFIGURATION, e); + log.error("Unable to load configuration, something went wrong!", e); System.exit(0); } } + private static BuildMeta buildMeta() { + var meta = new BuildMeta(); + try (InputStream stream = Main.class.getResourceAsStream("/build-info.yml")) { + if (stream == null) { + log.error("Error: Unable to load build metadata from JAR/build-info.yml: Bundled resources not exists"); + } else { + String str = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + YamlConfiguration configuration = new YamlConfiguration(); + configuration.loadFromString(str); + meta.loadBuildMeta(configuration); + } + } catch (IOException | InvalidConfigurationException e) { + log.error("Error: Unable to load build metadata from JAR/build-info.yml", e); + } + return meta; + } + private static void setupShutdownHook() { Thread shutdownThread = new Thread(() -> { try { - log.info(Lang.PBH_SHUTTING_DOWN); + log.info("Shutting down..."); eventBus.post(new PBHShutdownEvent()); server.shutdown(); guiManager.close(); @@ -180,7 +244,7 @@ private static void initGUI(String[] args) { guiType = "swing"; } } catch (IOException e) { - log.warn("Failed to load JavaFx dependencies", e); + log.error("Failed to load JavaFx dependencies", e); guiType = "swing"; } } @@ -192,6 +256,10 @@ private static void initGUI(String[] args) { guiManager.setup(); } + public static String getUserAgent() { + return "PeerBanHelper/" + meta.getVersion() + " BTN-Protocol/0.0.0-dev"; + } + private static boolean loadJavaFxDependencies() throws IOException { try (var is = Main.class.getResourceAsStream("/libraries/javafx.maven")) { String str = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8); @@ -204,32 +272,17 @@ private static boolean loadJavaFxDependencies() throws IOException { sysArch = "mac"; } try { - librariesLoader.loadLibraries(Arrays.stream(libraries).toList(), Map.of("system.platform", sysArch, "javafx.version", meta.getJavafx())); + librariesLoader.loadLibraries(Arrays.stream(libraries).toList(), + Map.of("system.platform", sysArch, "javafx.version", + Main.getMeta().getJavafx())); return true; } catch (Exception e) { - log.warn("Unable to load JavaFx dependencies", e); + log.error("Unable to load JavaFx dependencies", e); return false; } } - } - private static void initBuildMeta() { - meta = new BuildMeta(); - try (InputStream stream = Main.class.getResourceAsStream("/build-info.yml")) { - if (stream == null) { - log.error(Lang.ERR_BUILD_NO_INFO_FILE); - } else { - String str = new String(stream.readAllBytes(), StandardCharsets.UTF_8); - YamlConfiguration configuration = new YamlConfiguration(); - configuration.loadFromString(str); - meta.loadBuildMeta(configuration); - } - } catch (IOException | InvalidConfigurationException e) { - log.error(Lang.ERR_CANNOT_LOAD_BUILD_INFO, e); - } - log.info(Lang.MOTD, meta.getVersion()); - } private static void handleCommand(String input) { @@ -244,7 +297,7 @@ private static boolean initConfiguration() throws IOException { configDirectory.mkdirs(); } if (!configDirectory.isDirectory()) { - throw new IllegalStateException(Lang.ERR_CONFIG_DIRECTORY_INCORRECT); + throw new IllegalStateException("The path " + configDirectory.getAbsolutePath() + " should be a directory but found a file."); } if (!pluginDirectory.exists()) { pluginDirectory.mkdirs(); @@ -263,9 +316,59 @@ private static boolean initConfiguration() throws IOException { return exists; } + public static String decapitalize(String name) { + if (name == null || name.isEmpty()) { + return name; + } + if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && + Character.isUpperCase(name.charAt(0))) { + return name; + } + char chars[] = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } + + public static void registerBean(Class clazz, @Nullable String beanName) { + if (beanName == null) { + beanName = decapitalize(clazz.getSimpleName()); + } + if (applicationContext.containsBean(beanName)) { + return; + } else { + String bn = decapitalize(clazz.getSimpleName()); + if (applicationContext.containsBean(bn)) { + return; + } + } + ConfigurableApplicationContext configurableApplicationContext = applicationContext; + DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz); + defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition()); + } + + public static void registerBean(Class clazz, T instance, @Nullable String beanName) { + if (beanName == null) { + beanName = decapitalize(clazz.getSimpleName()); + } + if (applicationContext.containsBean(beanName)) { + return; + } else { + String bn = decapitalize(clazz.getSimpleName()); + if (applicationContext.containsBean(bn)) { + return; + } + } + DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory(); + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> instance); + defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition()); + } + + public static void unregisterBean(String beanName) { + ConfigurableApplicationContext configurableApplicationContext = applicationContext; + DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); + defaultListableBeanFactory.removeBeanDefinition(beanName); - public static String getUserAgent() { - return "PeerBanHelper/" + meta.getVersion() + " BTN-Protocol/0.0.0-dev"; } } \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/MainJavaFx.java b/src/main/java/com/ghostchu/peerbanhelper/MainJavaFx.java index 24162770e1..e1d86f767e 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/MainJavaFx.java +++ b/src/main/java/com/ghostchu/peerbanhelper/MainJavaFx.java @@ -1,7 +1,6 @@ package com.ghostchu.peerbanhelper; import com.ghostchu.peerbanhelper.gui.impl.javafx.mainwindow.JFXWindowController; -import com.ghostchu.peerbanhelper.text.Lang; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Scene; @@ -36,7 +35,7 @@ public void start(Stage st) throws Exception { stage = st; FXMLLoader fxmlLoader = new FXMLLoader(MainJavaFx.class.getResource("/javafx/main_window.fxml")); Scene scene = new Scene(fxmlLoader.load(), 600, 400); - st.setTitle(String.format(Lang.GUI_TITLE_LOADING, "JavaFx")); + st.setTitle(String.format("PeerBanHelper (JavaFx) - Loading...")); st.setScene(scene); st.setWidth(1000); st.setHeight(600); diff --git a/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java b/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java index 2dad0134b2..7568b7dcb2 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/PeerBanHelperServer.java @@ -1,9 +1,9 @@ package com.ghostchu.peerbanhelper; import com.ghostchu.peerbanhelper.alert.AlertManager; -import com.ghostchu.peerbanhelper.btn.BtnNetwork; +import com.ghostchu.peerbanhelper.database.Database; import com.ghostchu.peerbanhelper.database.DatabaseHelper; -import com.ghostchu.peerbanhelper.database.DatabaseManager; +import com.ghostchu.peerbanhelper.database.dao.impl.BanListDao; import com.ghostchu.peerbanhelper.downloader.Downloader; import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; import com.ghostchu.peerbanhelper.downloader.impl.biglybt.BiglyBT; @@ -18,14 +18,15 @@ import com.ghostchu.peerbanhelper.invoker.impl.CommandExec; import com.ghostchu.peerbanhelper.invoker.impl.IPFilterInvoker; import com.ghostchu.peerbanhelper.ipdb.IPDB; +import com.ghostchu.peerbanhelper.ipdb.IPGeoData; import com.ghostchu.peerbanhelper.metric.BasicMetrics; import com.ghostchu.peerbanhelper.metric.HitRateMetric; -import com.ghostchu.peerbanhelper.metric.impl.persist.PersistMetrics; import com.ghostchu.peerbanhelper.module.*; import com.ghostchu.peerbanhelper.module.impl.rule.*; import com.ghostchu.peerbanhelper.module.impl.webapi.*; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.*; import com.ghostchu.peerbanhelper.util.rule.ModuleMatchCache; @@ -38,13 +39,7 @@ import com.ghostchu.peerbanhelper.wrapper.TorrentWrapper; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; -import com.maxmind.geoip2.exception.GeoIp2Exception; -import com.maxmind.geoip2.model.AsnResponse; -import com.maxmind.geoip2.model.CityResponse; import inet.ipaddr.IPAddress; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -53,112 +48,128 @@ import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.sql.SQLException; import java.util.*; import java.util.concurrent.*; import java.util.logging.Level; +import static com.ghostchu.peerbanhelper.Main.DEF_LOCALE; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j +@Component public class PeerBanHelperServer { + private static final long BANLIST_SAVE_INTERVAL = 60 * 60 * 1000; + private final CheckResult NO_MATCHES_CHECK_RESULT = new CheckResult(getClass(), PeerAction.NO_ACTION, 0, new TranslationComponent("No Matches"), new TranslationComponent("No Matches")); private final Map BAN_LIST = new ConcurrentHashMap<>(); - private final YamlConfiguration profile; - private final List downloaders = new ArrayList<>(); - @Getter - private final long banDuration; - @Getter - private final int httpdPort; - @Getter - private final boolean hideFinishLogs; + private final Deque scheduledBanListOperations = new ConcurrentLinkedDeque<>(); + private final List downloaders = new CopyOnWriteArrayList<>(); @Getter private final List ignoreAddresses = new ArrayList<>(); - @Getter - private final YamlConfiguration mainConfig; - private final ModuleMatchCache moduleMatchCache; - private final File banListFile; private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); @Getter - private final HitRateMetric hitRateMetric = new HitRateMetric(); - @Getter private final List banListInvoker = new ArrayList<>(); private final String pbhServerAddress; - private ScheduledExecutorService BAN_WAVE_SERVICE; + private final ScheduledExecutorService GENERAL_SCHEDULER = Executors.newScheduledThreadPool(8, Thread.ofVirtual().factory()); @Getter - private ImmutableMap LIVE_PEERS = ImmutableMap.of(); + private YamlConfiguration profileConfig; @Getter - private BtnNetwork btnNetwork; + private long banDuration; @Getter - private BasicMetrics metrics; - private DatabaseManager databaseManager; + private int httpdPort; @Getter - private DatabaseHelper databaseHelper; + private boolean hideFinishLogs; + @Getter + private YamlConfiguration mainConfig; + @Autowired + private ModuleMatchCache moduleMatchCache; + @Autowired + @Qualifier("banListFile") + private File banListFile; + private ScheduledExecutorService BAN_WAVE_SERVICE; @Getter + private Map LIVE_PEERS = new HashMap<>(); + @Autowired + @Qualifier("persistMetrics") + private BasicMetrics metrics; + @Autowired + private Database databaseManager; + @Autowired private ModuleManager moduleManager; @Getter @Nullable private IPDB ipdb = null; private WatchDog banWaveWatchDog; - @Getter + @Autowired private JavalinWebContainer webContainer; - @Getter + @Autowired private AlertManager alertManager; private Cache geoIpCache = CacheBuilder.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .maximumSize(3000) .softValues() .build(); - - - public PeerBanHelperServer(String pbhServerAddress, YamlConfiguration profile, YamlConfiguration mainConfig) throws SQLException { - this.pbhServerAddress = pbhServerAddress; - this.profile = profile; - this.banDuration = profile.getLong("ban-duration"); - this.mainConfig = mainConfig; + @Getter + private HitRateMetric hitRateMetric = new HitRateMetric(); + @Autowired + private DatabaseHelper databaseHelper; + @Autowired + private BanListDao banListDao; + + public PeerBanHelperServer() { + this.pbhServerAddress = Main.getPbhServerAddress(); + this.profileConfig = Main.getProfileConfig(); + this.banDuration = profileConfig.getLong("ban-duration"); + this.mainConfig = Main.getMainConfig(); this.httpdPort = mainConfig.getInt("server.http"); this.hideFinishLogs = mainConfig.getBoolean("logger.hide-finish-log"); - profile.getStringList("ignore-peers-from-addresses").forEach(ip -> { + profileConfig.getStringList("ignore-peers-from-addresses").forEach(ip -> { IPAddress ignored = IPAddressUtil.getIPAddress(ip); ignoreAddresses.add(ignored); }); - this.banListFile = new File(Main.getDataDirectory(), "banlist.dump"); + } + + public void start() throws SQLException { loadDownloaders(); - this.moduleMatchCache = new ModuleMatchCache(); + registerBanListInvokers(); + registerModules(); registerHttpServer(); - this.moduleManager = new ModuleManager(); setupIPDB(); - setupBtn(); - try { - prepareDatabase(); - } catch (Exception e) { - log.error(Lang.DATABASE_FAILURE, e); - throw e; - } - registerMetrics(); - registerModules(); resetKnownDownloaders(); - registerBanListInvokers(); loadBanListToMemory(); registerTimer(); banListInvoker.forEach(BanListInvoker::reset); + GENERAL_SCHEDULER.scheduleWithFixedDelay(this::saveBanList, 10 * 1000, BANLIST_SAVE_INTERVAL, TimeUnit.MILLISECONDS); Main.getEventBus().post(new PBHServerStartedEvent(this)); + if (downloaders.isEmpty()) { + for (int i = 0; i < 50; i++) { + log.error(tlUI(Lang.NEW_SETUP_NO_DOWNLOADERS, getWebContainer() == null ? "ERROR" : getWebContainer().getToken())); + } + } } public void loadDownloaders() { this.downloaders.clear(); ConfigurationSection clientSection = mainConfig.getConfigurationSection("client"); + if (clientSection == null) { + return; + } for (String client : clientSection.getKeys(false)) { ConfigurationSection downloaderSection = clientSection.getConfigurationSection(client); String endpoint = downloaderSection.getString("endpoint"); Downloader downloader = createDownloader(client, downloaderSection); registerDownloader(downloader); - log.info(Lang.DISCOVER_NEW_CLIENT, downloader.getType(), client, endpoint); + log.info(tlUI(Lang.DISCOVER_NEW_CLIENT, downloader.getType(), client, endpoint)); } } @@ -170,6 +181,7 @@ public Downloader createDownloader(String client, ConfigurationSection downloade downloader = Transmission.loadFromConfig(client, pbhServerAddress, downloaderSection); case "biglybt" -> downloader = BiglyBT.loadFromConfig(client, downloaderSection); case "deluge" -> downloader = Deluge.loadFromConfig(client, downloaderSection); + //case "rtorrent" -> downloader = RTorrent.loadFromConfig(client, downloaderSection); } return downloader; @@ -183,6 +195,7 @@ public Downloader createDownloader(String client, JsonObject downloaderSection) downloader = Transmission.loadFromConfig(client, pbhServerAddress, downloaderSection); case "biglybt" -> downloader = BiglyBT.loadFromConfig(client, downloaderSection); case "deluge" -> downloader = Deluge.loadFromConfig(client, downloaderSection); + //case "rtorrent" -> downloader = RTorrent.loadFromConfig(client, downloaderSection); } return downloader; @@ -217,12 +230,13 @@ private void setupIPDB() { String databaseASN = mainConfig.getString("ip-database.database-asn", ""); boolean autoUpdate = mainConfig.getBoolean("ip-database.auto-update"); if (accountId.isEmpty() || licenseKey.isEmpty() || databaseCity.isEmpty() || databaseASN.isEmpty()) { - log.warn(Lang.IPDB_NEED_CONFIG); + log.warn(tlUI(Lang.IPDB_NEED_CONFIG)); return; } - this.ipdb = new IPDB(new File(Main.getDataDirectory(), "ipdb"), accountId, licenseKey, databaseCity, databaseASN, autoUpdate); + this.ipdb = new IPDB(new File(Main.getDataDirectory(), "ipdb"), accountId, licenseKey, + databaseCity, databaseASN, autoUpdate, Main.getUserAgent()); } catch (Exception e) { - log.info(Lang.IPDB_INVALID, e); + log.info(tlUI(Lang.IPDB_INVALID, e)); } } @@ -233,25 +247,10 @@ private void resetKnownDownloaders() { downloader.setBanList(Collections.emptyList(), null, null); } } catch (Exception e) { - log.warn(Lang.RESET_DOWNLOADER_FAILED, e); + log.error(tlUI(Lang.RESET_DOWNLOADER_FAILED), e); } } - public void setupBtn() { - if (this.btnNetwork != null) { - this.btnNetwork.close(); - } - BtnNetwork btnm; - try { - log.info(Lang.BTN_NETWORK_CONNECTING); - btnm = new BtnNetwork(this, mainConfig.getConfigurationSection("btn")); - log.info(Lang.BTN_NETWORK_ENABLED); - } catch (IllegalStateException e) { - btnm = null; - log.info(Lang.BTN_NETWORK_NOT_ENABLED); - } - this.btnNetwork = btnm; - } private void registerBanListInvokers() { banListInvoker.add(new IPFilterInvoker(this)); @@ -260,14 +259,14 @@ private void registerBanListInvokers() { public void shutdown() { // place some clean code here - dumpBanListToFile(); - log.info(Lang.SHUTDOWN_CLOSE_METRICS); + saveBanList(); + log.info(tlUI(Lang.SHUTDOWN_CLOSE_METRICS)); this.metrics.close(); - log.info(Lang.SHUTDOWN_UNREGISTER_MODULES); + log.info(tlUI(Lang.SHUTDOWN_UNREGISTER_MODULES)); this.moduleManager.unregisterAll(); - log.info(Lang.SHUTDOWN_CLOSE_DATABASE); + log.info(tlUI(Lang.SHUTDOWN_CLOSE_DATABASE)); this.databaseManager.close(); - log.info(Lang.SHUTDOWN_CLEANUP_RESOURCES); + log.info(tlUI(Lang.SHUTDOWN_CLEANUP_RESOURCES)); this.moduleMatchCache.close(); if (this.ipdb != null) { this.ipdb.close(); @@ -279,7 +278,7 @@ public void shutdown() { log.error("Failed to close download {}", d.getName(), e); } }); - log.info(Lang.SHUTDOWN_DONE); + log.info(tlUI(Lang.SHUTDOWN_DONE)); } private void loadBanListToMemory() { @@ -287,17 +286,9 @@ private void loadBanListToMemory() { return; } try { - if (!banListFile.exists()) { - return; - } - String json = Files.readString(banListFile.toPath(), StandardCharsets.UTF_8); - Map data = JsonUtil.getGson().fromJson(json, new TypeToken>() { - }.getType()); - if (data == null) { - return; - } + Map data = banListDao.readBanList(); this.BAN_LIST.putAll(data); - log.info(Lang.LOAD_BANLIST_FROM_FILE, data.size()); + log.info(tlUI(Lang.LOAD_BANLIST_FROM_FILE, data.size())); downloaders.forEach(downloader -> { downloader.login(); downloader.setBanList(BAN_LIST.keySet(), null, null); @@ -305,36 +296,22 @@ private void loadBanListToMemory() { Collection relaunch = data.values().stream().map(BanMetadata::getTorrent).toList(); downloaders.forEach(downloader -> downloader.relaunchTorrentIfNeededByTorrentWrapper(relaunch)); } catch (Exception e) { - log.error(Lang.LOAD_BANLIST_FAIL, e); - } finally { - banListFile.delete(); + log.error(tlUI(Lang.ERR_UPDATE_BAN_LIST), e); } } - private void dumpBanListToFile() { + private void saveBanList() { if (!mainConfig.getBoolean("persist.banlist")) { return; } - log.info(Lang.SHUTDOWN_SAVE_BANLIST); try { - if (!banListFile.exists()) { - banListFile.createNewFile(); - } - Files.writeString(banListFile.toPath(), JsonUtil.getGson().toJson(BAN_LIST)); - } catch (IOException e) { - log.error(Lang.SHUTDOWN_SAVE_BANLIST_FAILED); + int count = banListDao.saveBanList(BAN_LIST); + log.info(tlUI(Lang.SAVED_BANLIST, count)); + } catch (Exception e) { + log.error(tlUI(Lang.SAVE_BANLIST_FAILED), e); } } - private void prepareDatabase() throws SQLException { - this.databaseManager = new DatabaseManager(); - this.databaseHelper = new DatabaseHelper(this, databaseManager); - } - - private void registerMetrics() { - this.metrics = new PersistMetrics(databaseHelper); - } - private void registerHttpServer() { String token = System.getenv("PBH_API_TOKEN"); if (token == null) { @@ -344,21 +321,21 @@ private void registerHttpServer() { token = getMainConfig().getString("server.token"); } String host = getMainConfig().getString("server.address"); - if (host.equals("0.0.0.0") || host.equals("::")) { + if (host.equals("0.0.0.0") || host.equals("::") || host.equals("localhost")) { host = null; } - this.webContainer = new JavalinWebContainer(host, httpdPort, token); + webContainer.start(host, httpdPort, token); } private void registerTimer() { - this.banWaveWatchDog = new WatchDog("BanWave Thread", profile.getLong("check-interval", 5000) + (1000 * 60), this::watchDogHungry, null); + this.banWaveWatchDog = new WatchDog("BanWave Thread", profileConfig.getLong("check-interval", 5000) + (1000 * 60), this::watchDogHungry, null); registerBanWaveTimer(); this.banWaveWatchDog.start(); } private void registerBanWaveTimer() { if (BAN_WAVE_SERVICE != null && (!BAN_WAVE_SERVICE.isShutdown() || !BAN_WAVE_SERVICE.isTerminated())) { - BAN_WAVE_SERVICE.shutdownNow().forEach(r -> log.warn("Unfinished runnable: {}", r)); + BAN_WAVE_SERVICE.shutdownNow().forEach(r -> log.error("Unfinished runnable: {}", r)); } BAN_WAVE_SERVICE = Executors.newScheduledThreadPool(1, r -> { Thread thread = new Thread(r); @@ -366,8 +343,8 @@ private void registerBanWaveTimer() { thread.setDaemon(true); return thread; }); - log.info(Lang.PBH_BAN_WAVE_STARTED); - BAN_WAVE_SERVICE.scheduleAtFixedRate(this::banWave, 1, profile.getLong("check-interval", 5000), TimeUnit.MILLISECONDS); + log.info(tlUI(Lang.PBH_BAN_WAVE_STARTED)); + BAN_WAVE_SERVICE.scheduleAtFixedRate(this::banWave, 1, profileConfig.getLong("check-interval", 5000), TimeUnit.MILLISECONDS); } @@ -379,7 +356,7 @@ private void watchDogHungry() { } log.info(threadDump.toString()); registerBanWaveTimer(); - Main.getGuiManager().createNotification(Level.WARNING, Lang.BAN_WAVE_WATCH_DOG_TITLE, Lang.BAN_WAVE_WATCH_DOG_DESCRIPTION); + Main.getGuiManager().createNotification(Level.WARNING, tlUI(Lang.BAN_WAVE_WATCH_DOG_TITLE), tlUI(Lang.BAN_WAVE_WATCH_DOG_DESCRIPTION)); } @@ -387,6 +364,7 @@ private void watchDogHungry() { * 启动新的一轮封禁序列 */ public void banWave() { + banWaveWatchDog.setLastOperation("Ban wave - start"); long startTimer = System.currentTimeMillis(); try { @@ -410,14 +388,39 @@ public void banWave() { Map> downloaderBanDetailMap = new ConcurrentHashMap<>(); banWaveWatchDog.setLastOperation("Check Bans"); try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.CHECK_BANS.getTimeout(), (t) -> { - log.warn(Lang.TIMING_CHECK_BANS); + log.error(tlUI(Lang.TIMING_CHECK_BANS)); })) { downloaders.forEach(downloader -> protect.getService().submit(() -> downloaderBanDetailMap.put(downloader, checkBans(peers.get(downloader))))); } + + + // 处理计划操作 + int scheduled = 0; + while (!scheduledBanListOperations.isEmpty()) { + ScheduledBanListOperation ops = scheduledBanListOperations.poll(); + scheduled++; + if (ops.ban()) { + ScheduledPeerBanning banning = (ScheduledPeerBanning) ops.object(); + List banDetails = downloaderBanDetailMap.getOrDefault(banning.downloader(), new CopyOnWriteArrayList<>()); + banDetails.add(banning.detail()); + downloaderBanDetailMap.put(banning.downloader(), banDetails); + } else { + PeerAddress address = (PeerAddress) ops.object(); + BanMetadata banMetadata = BAN_LIST.get(address); + if (banMetadata != null) { + unbannedPeers.add(banMetadata); + } + } + } + + if (scheduled > 0) { + log.info(tlUI(Lang.SCHEDULED_OPERATIONS, scheduled)); + } + // 添加被封禁的 Peers 到封禁列表中 banWaveWatchDog.setLastOperation("Add banned peers into banlist"); try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.ADD_BAN_ENTRY.getTimeout(), (t) -> { - log.warn(Lang.TIMING_ADD_BANS); + log.error(tlUI(Lang.TIMING_ADD_BANS)); })) { downloaderBanDetailMap.forEach((downloader, details) -> { try { @@ -425,13 +428,17 @@ public void banWave() { details.forEach(detail -> { protect.getService().submit(() -> { if (detail.result().action() == PeerAction.BAN) { - BanMetadata banMetadata = new BanMetadata(detail.result().moduleContext() == null ? "Unknown" : detail.result().moduleContext().getClass().getName(), downloader.getName(), - System.currentTimeMillis(), System.currentTimeMillis() + banDuration, + long actualBanDuration = banDuration; + if (detail.banDuration() > 0) { + actualBanDuration = detail.banDuration(); + } + BanMetadata banMetadata = new BanMetadata(detail.result().moduleContext().getName(), downloader.getName(), + System.currentTimeMillis(), System.currentTimeMillis() + actualBanDuration, detail.torrent(), detail.peer(), detail.result().rule(), detail.result().reason()); bannedPeers.add(banMetadata); relaunch.add(detail.torrent()); banPeer(banMetadata, detail.torrent(), detail.peer()); - log.warn(Lang.BAN_PEER, detail.peer().getPeerAddress(), detail.peer().getPeerId(), detail.peer().getClientName(), detail.peer().getProgress(), detail.peer().getUploaded(), detail.peer().getDownloaded(), detail.torrent().getName(), detail.result().reason()); + log.warn(tlUI(Lang.BAN_PEER, detail.peer().getPeerAddress(), detail.peer().getPeerId(), detail.peer().getClientName(), detail.peer().getProgress(), detail.peer().getUploaded(), detail.peer().getDownloaded(), detail.torrent().getName(), tl(DEF_LOCALE, detail.result().reason()))); } }); }); @@ -445,17 +452,18 @@ public void banWave() { banWaveWatchDog.setLastOperation("Apply banlist"); // 如果需要,则应用更改封禁列表到下载器 try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.APPLY_BANLIST.getTimeout(), (t) -> { - log.warn(Lang.TIMING_APPLY_BAN_LIST); + log.error(tlUI(Lang.TIMING_APPLY_BAN_LIST)); })) { - downloaders.forEach(downloader -> protect.getService().submit(() -> updateDownloader(downloader, !bannedPeers.isEmpty() || !unbannedPeers.isEmpty(), - needRelaunched.getOrDefault(downloader, Collections.emptyList()), - bannedPeers, unbannedPeers))); + downloaders.forEach(downloader -> protect.getService().submit(() -> + updateDownloader(downloader, !bannedPeers.isEmpty() || !unbannedPeers.isEmpty(), + needRelaunched.getOrDefault(downloader, Collections.emptyList()), + bannedPeers, unbannedPeers))); } - if (!hideFinishLogs) { + if (!hideFinishLogs && !downloaders.isEmpty()) { long downloadersCount = peers.keySet().size(); long torrentsCount = peers.values().stream().mapToLong(e -> e.keySet().size()).sum(); long peersCount = peers.values().stream().flatMap(e -> e.values().stream()).mapToLong(List::size).sum(); - log.info(Lang.BAN_WAVE_CHECK_COMPLETED, downloadersCount, torrentsCount, peersCount, bannedPeers.size(), unbannedPeers.size(), System.currentTimeMillis() - startTimer); + log.info(tlUI(Lang.BAN_WAVE_CHECK_COMPLETED, downloadersCount, torrentsCount, peersCount, bannedPeers.size(), unbannedPeers.size(), System.currentTimeMillis() - startTimer)); } banWaveWatchDog.setLastOperation("Completed"); } finally { @@ -466,15 +474,13 @@ public void banWave() { private List checkBans(Map> provided) { List details = Collections.synchronizedList(new ArrayList<>()); - try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.CHECK_BANS.getTimeout(), (t) -> { - log.warn(Lang.TIMING_CHECK_BANS); - })) { + try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.CHECK_BANS.getTimeout(), (t) -> log.error(tlUI(Lang.TIMING_CHECK_BANS)))) { for (Torrent torrent : provided.keySet()) { List peers = provided.get(torrent); for (Peer peer : peers) { protect.getService().submit(() -> { - BanResult banResult = checkBan(torrent, peer); - details.add(new BanDetail(torrent, peer, banResult)); + CheckResult checkResult = checkBan(torrent, peer); + details.add(new BanDetail(torrent, peer, checkResult, checkResult.duration())); }); } } @@ -483,20 +489,18 @@ private List checkBans(Map> provided) { } private void updateLivePeers(Map>> peers) { - Map livePeers = new ConcurrentHashMap<>(); - try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.UPDATE_LIVE_PEERS.getTimeout(), (t) -> { - })) { - peers.forEach((downloader, tasks) -> - tasks.forEach((torrent, peer) -> - peer.forEach(p -> protect.getService().submit(() -> { - PeerAddress address = p.getPeerAddress(); - PeerMetadata metadata = new PeerMetadata( - downloader.getName(), - torrent, p); - livePeers.put(address, metadata); - })))); - } - LIVE_PEERS = ImmutableMap.copyOf(livePeers); + Map livePeers = new HashMap<>(128); + peers.forEach((downloader, tasks) -> + tasks.forEach((torrent, peer) -> + peer.forEach(p -> { + PeerAddress address = p.getPeerAddress(); + PeerMetadata metadata = new PeerMetadata( + downloader.getName(), + torrent, p); + livePeers.put(address, metadata); + } + ))); + LIVE_PEERS = Map.copyOf(livePeers); Main.getEventBus().post(new LivePeersUpdatedEvent(LIVE_PEERS)); } @@ -511,18 +515,19 @@ private void updateLivePeers(Map>> peers) { public void updateDownloader(@NotNull Downloader downloader, boolean updateBanList, @NotNull Collection needToRelaunch, @Nullable Collection added, @Nullable Collection removed) { if (!updateBanList && needToRelaunch.isEmpty()) return; try { - if (!downloader.login()) { - log.warn(Lang.ERR_CLIENT_LOGIN_FAILURE_SKIP, downloader.getName(), downloader.getEndpoint()); - downloader.setLastStatus(DownloaderLastStatus.ERROR, Lang.STATUS_TEXT_LOGIN_FAILED); + var loginResult = downloader.login(); + if (!loginResult.success()) { + log.error(tlUI(Lang.ERR_CLIENT_LOGIN_FAILURE_SKIP, downloader.getName(), downloader.getEndpoint(), tlUI(loginResult.getMessage()))); + downloader.setLastStatus(DownloaderLastStatus.ERROR, loginResult.getMessage()); return; } else { - downloader.setLastStatus(DownloaderLastStatus.HEALTHY, Lang.STATUS_TEXT_OK); + downloader.setLastStatus(DownloaderLastStatus.HEALTHY, loginResult.getMessage()); } downloader.setBanList(BAN_LIST.keySet(), added, removed); downloader.relaunchTorrentIfNeeded(needToRelaunch); } catch (Throwable th) { - log.warn(Lang.ERR_UPDATE_BAN_LIST, downloader.getName(), downloader.getEndpoint(), th); - downloader.setLastStatus(DownloaderLastStatus.ERROR, Lang.STATUS_TEXT_EXCEPTION); + log.error(tlUI(Lang.ERR_UPDATE_BAN_LIST, downloader.getName(), downloader.getEndpoint()), th); + downloader.setLastStatus(DownloaderLastStatus.ERROR, new TranslationComponent(Lang.STATUS_TEXT_EXCEPTION, th.getClass().getName() + ": " + th.getMessage())); } } @@ -542,7 +547,7 @@ public Collection removeExpiredBans() { } removeBan.forEach(this::unbanPeer); if (!removeBan.isEmpty()) { - log.info(Lang.PEER_UNBAN_WAVE, removeBan.size()); + log.info(tlUI(Lang.PEER_UNBAN_WAVE, removeBan.size())); } return metadata; } @@ -551,25 +556,25 @@ public Collection removeExpiredBans() { * 注册 Modules */ private void registerModules() { - log.info(Lang.WAIT_FOR_MODULES_STARTUP); - moduleManager.register(new IPBlackList(this, profile)); - moduleManager.register(new PeerIdBlacklist(this, profile)); - moduleManager.register(new ClientNameBlacklist(this, profile)); - moduleManager.register(new ExpressionRule(this, profile)); - moduleManager.register(new ProgressCheatBlocker(this, profile)); - moduleManager.register(new MultiDialingBlocker(this, profile)); + log.info(tlUI(Lang.WAIT_FOR_MODULES_STARTUP)); + moduleManager.register(IPBlackList.class); + moduleManager.register(PeerIdBlacklist.class); + moduleManager.register(ClientNameBlacklist.class); + moduleManager.register(ExpressionRule.class); + moduleManager.register(ProgressCheatBlocker.class); + moduleManager.register(MultiDialingBlocker.class); //moduleManager.register(new ActiveProbing(this, profile)); - moduleManager.register(new AutoRangeBan(this, profile)); - moduleManager.register(new BtnNetworkOnline(this, profile)); - moduleManager.register(new DownloaderCIDRBlockList(this, profile)); - moduleManager.register(new IPBlackRuleList(this, profile, databaseHelper)); - moduleManager.register(new PBHMetricsController(this, profile)); - moduleManager.register(new PBHBanController(this, profile, databaseHelper)); - moduleManager.register(new PBHMetadataController(this, profile)); - moduleManager.register(new PBHDownloaderController(this, profile)); - moduleManager.register(new RuleSubController(this, profile)); - moduleManager.register(new PBHAuthenticateController(this, profile)); - moduleManager.register(new PBHLogsController(this, profile)); + moduleManager.register(AutoRangeBan.class); + moduleManager.register(BtnNetworkOnline.class); + moduleManager.register(DownloaderCIDRBlockList.class); + moduleManager.register(IPBlackRuleList.class); + moduleManager.register(PBHMetricsController.class); + moduleManager.register(PBHBanController.class); + moduleManager.register(PBHMetadataController.class); + moduleManager.register(PBHDownloaderController.class); + moduleManager.register(RuleSubController.class); + moduleManager.register(PBHAuthenticateController.class); + moduleManager.register(PBHLogsController.class); } public Map>> collectPeers() { @@ -580,7 +585,7 @@ public Map>> collectPeers() { Map> p = collectPeers(downloader); peers.put(downloader, p); } catch (Exception e) { - log.warn(Lang.DOWNLOADER_UNHANDLED_EXCEPTION, e); + log.error(tlUI(Lang.DOWNLOADER_UNHANDLED_EXCEPTION), e); } })); } @@ -589,15 +594,16 @@ public Map>> collectPeers() { public Map> collectPeers(Downloader downloader) { Map> peers = new ConcurrentHashMap<>(); - if (!downloader.login()) { - log.warn(Lang.ERR_CLIENT_LOGIN_FAILURE_SKIP, downloader.getName(), downloader.getEndpoint()); - downloader.setLastStatus(DownloaderLastStatus.ERROR, Lang.STATUS_TEXT_LOGIN_FAILED); + var loginResult = downloader.login(); + if (!loginResult.success()) { + log.error(tlUI(Lang.ERR_CLIENT_LOGIN_FAILURE_SKIP, downloader.getName(), downloader.getEndpoint(), tlUI(loginResult.getMessage()))); + downloader.setLastStatus(DownloaderLastStatus.ERROR, loginResult.getMessage()); return Collections.emptyMap(); } List torrents = downloader.getTorrents(); Semaphore parallelReqRestrict = new Semaphore(16); try (TimeoutProtect protect = new TimeoutProtect(ExceptedTime.COLLECT_PEERS.getTimeout(), (t) -> { - log.warn(Lang.TIMING_COLLECT_PEERS); + log.error(tlUI(Lang.TIMING_COLLECT_PEERS)); })) { torrents.forEach(torrent -> protect.getService().submit(() -> { try { @@ -609,7 +615,7 @@ public Map> collectPeers(Downloader downloader) { parallelReqRestrict.release(); } })); - downloader.setLastStatus(DownloaderLastStatus.HEALTHY, Lang.STATUS_TEXT_OK); + downloader.setLastStatus(DownloaderLastStatus.HEALTHY, new TranslationComponent(Lang.STATUS_TEXT_OK)); } return peers; @@ -619,43 +625,22 @@ public IPDBResponse queryIPDB(PeerAddress address) { try { return geoIpCache.get(address.getIp(), () -> { if (ipdb == null) { - return new IPDBResponse(new LazyLoad<>(() -> null), new LazyLoad<>(() -> null)); - } - return new IPDBResponse(new LazyLoad<>(() -> { - if (ipdb.getMmdbCity() != null) { - try { - return ipdb.getMmdbCity().city(address.getAddress().toInetAddress()); - } catch (IOException | GeoIp2Exception ignored) { - } - } - return null; - }), new LazyLoad<>(() -> { - if (ipdb.getMmdbCity() != null) { + return new IPDBResponse(new LazyLoad<>(() -> null)); + } else { + return new IPDBResponse(new LazyLoad<>(() -> { try { - return ipdb.getMmdbASN().asn(address.getAddress().toInetAddress()); - } catch (IOException | GeoIp2Exception ignored) { + return ipdb.query(address.getAddress().toInetAddress()); + } catch (Exception ignored) { + return null; } - } - return null; - })); + })); + } }); } catch (ExecutionException e) { - return new IPDBResponse(null, null); + return new IPDBResponse(null); } } - private boolean isHandshaking(Peer peer) { - if (peer.getPeerId() == null || peer.getPeerId().isEmpty()) { - // 跳过此 Peer,PeerId 不能为空,此时只建立了连接,但还没有完成交换 - return true; - } - //noinspection RedundantIfStatement - if (peer.getDownloadSpeed() <= 0 && peer.getUploadSpeed() <= 0) { - // 跳过此 Peer,速度都是0,可能是没有完成握手 - return true; - } - return false; - } /** * 检查一个在给定 Torrent 上的对等体是否需要被封禁 @@ -665,42 +650,56 @@ private boolean isHandshaking(Peer peer) { * @return 封禁规则检查结果 */ @NotNull - public BanResult checkBan(@NotNull Torrent torrent, @NotNull Peer peer) { - List results = new ArrayList<>(); + public CheckResult checkBan(@NotNull Torrent torrent, @NotNull Peer peer) { + List results = new ArrayList<>(); if (peer.getPeerAddress().getAddress().isAnyLocal()) { - return new BanResult(null, PeerAction.SKIP, "local access", "skip local network peers"); + return new CheckResult(getClass(), PeerAction.SKIP, 0, new TranslationComponent("general-rule-local-address"), new TranslationComponent("general-reason-skip-local-peers")); } for (IPAddress ignoreAddress : ignoreAddresses) { if (ignoreAddress.contains(peer.getPeerAddress().getAddress())) { - return new BanResult(null, PeerAction.SKIP, "ignored addresses", "skip peers from ignored addresses"); + return new CheckResult(getClass(), PeerAction.SKIP, 0, new TranslationComponent("general-rule-ignored-address"), new TranslationComponent("general-reason-skip-ignored-peers")); } } - for (FeatureModule registeredModule : moduleManager.getModules()) { - if (!(registeredModule instanceof RuleFeatureModule module)) { - continue; - } - if (module.needCheckHandshake() && isHandshaking(peer)) { - continue; // 如果模块需要握手检查且peer正在握手 则跳过检查 - } - if (module.isCheckCacheable()) { - if (moduleMatchCache.shouldSkipCheck(module, torrent, peer.getPeerAddress(), true)) { + try { + for (FeatureModule registeredModule : moduleManager.getModules()) { + if (!(registeredModule instanceof RuleFeatureModule module)) { continue; } + try { + CheckResult checkResult; + if (module.isThreadSafe()) { + checkResult = module.shouldBanPeer(torrent, peer, executor); + } else { + registeredModule.getThreadLock().lock(); + try { + checkResult = module.shouldBanPeer(torrent, peer, executor); + } finally { + registeredModule.getThreadLock().unlock(); + } + } + if (checkResult.action() == PeerAction.SKIP) { + results.add(checkResult); + } + results.add(checkResult); + } catch (Exception e) { + log.error("Unable to execute module {}, report to PeerBanHelper developer!", module.getName(), e); + } } - BanResult banResult = module.shouldBanPeer(torrent, peer, executor); - if (banResult.action() == PeerAction.SKIP) { - return banResult; - } - results.add(banResult); - } - BanResult result = new BanResult(null, PeerAction.NO_ACTION, "No matches", "No matches"); - for (BanResult r : results) { - if (r.action() == PeerAction.BAN) { - result = r; - break; + CheckResult result = NO_MATCHES_CHECK_RESULT; + for (CheckResult r : results) { + if (r.action() == PeerAction.SKIP) { + result = r; + break; // 立刻离开循环,处理跳过 + } + if (r.action() == PeerAction.BAN) { + result = r; + } } + return result; + } catch (Exception e) { + log.error("Failed to execute modules", e); + return new CheckResult(getClass(), PeerAction.NO_ACTION, 0, new TranslationComponent("ERROR"), new TranslationComponent("ERROR")); } - return result; } /** @@ -710,7 +709,17 @@ public BanResult checkBan(@NotNull Torrent torrent, @NotNull Peer peer) { */ @NotNull public Map getBannedPeers() { - return ImmutableMap.copyOf(BAN_LIST); + return Map.copyOf(BAN_LIST); + } + + /** + * 获取目前所有被封禁的对等体的集合的拷贝 + * + * @return 不可修改的集合拷贝 + */ + @NotNull + public Map getBannedPeersDirect() { + return BAN_LIST; } /** @@ -719,24 +728,49 @@ public Map getBannedPeers() { * @param peer 对等体 IP 地址 * @param banMetadata 封禁元数据 */ - public void banPeer(@NotNull BanMetadata banMetadata, @NotNull Torrent torrentObj, @NotNull Peer peer) { + private void banPeer(@NotNull BanMetadata banMetadata, @NotNull Torrent torrentObj, @NotNull Peer peer) { + if (BAN_LIST.containsKey(peer.getPeerAddress())) { + log.error(tlUI(Lang.DUPLICATE_BAN, banMetadata)); + } BAN_LIST.put(peer.getPeerAddress(), banMetadata); metrics.recordPeerBan(peer.getPeerAddress(), banMetadata); banListInvoker.forEach(i -> i.add(peer.getPeerAddress(), banMetadata)); + banMetadata.setReverseLookup("N/A"); if (mainConfig.getBoolean("lookup.dns-reverse-lookup")) { - executor.submit(() -> banMetadata.setReverseLookup(peer.getPeerAddress().getAddress().toReverseDNSLookupString())); - } else { - banMetadata.setReverseLookup("N/A"); + executor.submit(() -> { + String hostName = peer.getPeerAddress().getAddress().toInetAddress().getHostName(); + if (!peer.getPeerAddress().getIp().equals(hostName)) { + banMetadata.setReverseLookup(peer.getPeerAddress().getAddress().toInetAddress().getHostName()); + } + }); } Main.getEventBus().post(new PeerBanEvent(peer.getPeerAddress(), banMetadata, torrentObj, peer)); } + public void scheduleBanPeer(@NotNull BanMetadata banMetadata, @NotNull Torrent torrent, @NotNull Peer peer) { + Downloader downloader = getDownloaders().stream().filter(d -> d.getName().equals(banMetadata.getDownloader())) + .findFirst().orElseThrow(); + banPeer(banMetadata, torrent, peer); + scheduledBanListOperations.add(new ScheduledBanListOperation(true, new ScheduledPeerBanning( + downloader, + new BanDetail(torrent, + peer, + new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.USER_MANUALLY_BAN_RULE), new TranslationComponent(Lang.USER_MANUALLY_BAN_REASON)) + , banDuration) + ))); + } + + public void scheduleUnBanPeer(@NotNull PeerAddress peer) { + unbanPeer(peer); + scheduledBanListOperations.add(new ScheduledBanListOperation(false, peer)); + } + public String getWebUiUrl() { - return "http://localhost:" + Main.getServer().getHttpdPort() + "/?token=" + UrlEncoderDecoder.encodePath(getWebContainer().getToken()); + return "http://localhost:" + Main.getServer().getHttpdPort() + "/?token=" + UrlEncoderDecoder.encodePath(webContainer.getToken()); } public List getDownloaders() { - return ImmutableList.copyOf(downloaders); + return List.copyOf(downloaders); } /** @@ -746,7 +780,7 @@ public List getDownloaders() { * @return 此对等体的封禁元数据;返回 null 代表此对等体没有被封禁 */ @Nullable - public BanMetadata unbanPeer(@NotNull PeerAddress address) { + private BanMetadata unbanPeer(@NotNull PeerAddress address) { BanMetadata metadata = BAN_LIST.remove(address); if (metadata != null) { metrics.recordPeerUnban(address, metadata); @@ -756,20 +790,40 @@ public BanMetadata unbanPeer(@NotNull PeerAddress address) { return metadata; } - public ImmutableMap getLivePeersSnapshot() { + public Map getLivePeersSnapshot() { return LIVE_PEERS; } + /** + * Use @Autowired if available + * + * @return JavalinWebContainer + */ + @Nullable + @Deprecated + public JavalinWebContainer getWebContainer() { + return webContainer; + } + public record IPDBResponse( - LazyLoad cityResponse, - LazyLoad asnResponse + LazyLoad geoData ) { } public record BanDetail( Torrent torrent, Peer peer, - BanResult result + CheckResult result, + long banDuration + ) { + } + + public record ScheduledPeerBanning( + Downloader downloader, + BanDetail detail ) { } + + private record ScheduledBanListOperation(boolean ban, Object object) { + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/alert/AlertManager.java b/src/main/java/com/ghostchu/peerbanhelper/alert/AlertManager.java index 3b7c7ea997..b614ece825 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/alert/AlertManager.java +++ b/src/main/java/com/ghostchu/peerbanhelper/alert/AlertManager.java @@ -2,12 +2,13 @@ import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.event.NewAlertCreated; -import com.google.common.collect.ImmutableList; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +@Component public class AlertManager { private Map alerts = new ConcurrentHashMap<>(); @@ -29,6 +30,6 @@ public boolean removeAlert(Alert alert) { } public List getAlerts() { - return ImmutableList.copyOf(alerts.values()); + return List.copyOf(alerts.values()); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnConfig.java b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnConfig.java new file mode 100644 index 0000000000..be3f950bb8 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnConfig.java @@ -0,0 +1,40 @@ +package com.ghostchu.peerbanhelper.btn; + +import com.ghostchu.peerbanhelper.PeerBanHelperServer; +import com.ghostchu.peerbanhelper.text.Lang; +import lombok.extern.slf4j.Slf4j; +import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + +@Configuration +@Slf4j +public class BtnConfig { + @Autowired + private PeerBanHelperServer server; + @Autowired + @Qualifier("userAgent") + private String userAgent; + + @Bean + public BtnNetwork btnNetwork() { + ConfigurationSection section = server.getMainConfig().getConfigurationSection("btn"); + if (section.getBoolean("enabled")) { + log.info(tlUI(Lang.BTN_NETWORK_CONNECTING)); + var configUrl = server.getMainConfig().getString("btn.config-url"); + var submit = server.getMainConfig().getBoolean("btn.submit"); + var appId = server.getMainConfig().getString("btn.app-id"); + var appSecret = server.getMainConfig().getString("btn.app-secret"); + BtnNetwork btnNetwork = new BtnNetwork(server, userAgent, configUrl, submit, appId, appSecret); + log.info(tlUI(Lang.BTN_NETWORK_ENABLED)); + return btnNetwork; + } else { + log.info(tlUI(Lang.BTN_NETWORK_NOT_ENABLED)); + return null; + } + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnNetwork.java b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnNetwork.java index fa3f1f6d46..5bd7944bda 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnNetwork.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnNetwork.java @@ -1,6 +1,5 @@ package com.ghostchu.peerbanhelper.btn; -import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.btn.ability.*; import com.ghostchu.peerbanhelper.text.Lang; @@ -11,7 +10,8 @@ import com.google.gson.JsonParser; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import java.net.CookieManager; import java.net.CookiePolicy; @@ -22,44 +22,49 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; @Slf4j +@Getter public class BtnNetwork { private static final int BTN_PROTOCOL_VERSION = 5; @Getter - private final PeerBanHelperServer server; - @Getter - private final String configUrl; - private final boolean submit; - private final String appId; - private final String appSecret; - @Getter private final Map, BtnAbility> abilities = new HashMap<>(); @Getter private final ScheduledExecutorService executeService = Executors.newScheduledThreadPool(2); + private String configUrl; + private boolean submit; + private String appId; + private String appSecret; @Getter private HttpClient httpClient; + @Autowired + @Qualifier("userAgent") + private String userAgent; + private PeerBanHelperServer server; + private final AtomicBoolean configSuccess = new AtomicBoolean(false); - public BtnNetwork(PeerBanHelperServer server, ConfigurationSection section) { + public BtnNetwork(PeerBanHelperServer server, String userAgent, String configUrl, boolean submit, String appId, String appSecret) { this.server = server; - if (!section.getBoolean("enabled")) { - throw new IllegalStateException("BTN has been disabled"); - } - this.configUrl = section.getString("config-url"); - this.submit = section.getBoolean("submit"); - this.appId = section.getString("app-id"); - this.appSecret = section.getString("app-secret"); + this.userAgent = userAgent; + this.configUrl = configUrl; + this.submit = submit; + this.appId = appId; + this.appSecret = appSecret; setupHttpClient(); - configBtnNetwork(); + executeService.scheduleAtFixedRate(this::checkIfNeedRetryConfig, 600, 600, TimeUnit.SECONDS); } - private void configBtnNetwork() { + public void configBtnNetwork() { abilities.values().forEach(BtnAbility::unload); abilities.clear(); try { HttpResponse resp = HTTPUtil.retryableSend(httpClient, MutableRequest.GET(configUrl), HttpResponse.BodyHandlers.ofString()).join(); if (resp.statusCode() != 200) { - log.warn(Lang.BTN_CONFIG_FAILS, resp.statusCode() + " - " + resp.body()); + log.error(tlUI(Lang.BTN_CONFIG_FAILS, resp.statusCode() + " - " + resp.body(), 600)); return; } JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject(); @@ -68,11 +73,11 @@ private void configBtnNetwork() { } int min_protocol_version = json.get("min_protocol_version").getAsInt(); if (min_protocol_version > BTN_PROTOCOL_VERSION) { - throw new IllegalStateException(String.format(Lang.BTN_INCOMPATIBLE_SERVER)); + throw new IllegalStateException(tlUI(Lang.BTN_INCOMPATIBLE_SERVER)); } int max_protocol_version = json.get("max_protocol_version").getAsInt(); if (max_protocol_version > BTN_PROTOCOL_VERSION) { - throw new IllegalStateException(String.format(Lang.BTN_INCOMPATIBLE_SERVER)); + throw new IllegalStateException(tlUI(Lang.BTN_INCOMPATIBLE_SERVER)); } JsonObject ability = json.get("ability").getAsJsonObject(); if (ability.has("submit_peers") && submit) { @@ -93,12 +98,19 @@ private void configBtnNetwork() { abilities.values().forEach(a -> { try { a.load(); + configSuccess.set(true); } catch (Exception e) { log.error("Failed to load BTN ability", e); } }); } catch (Throwable e) { - log.warn(Lang.BTN_CONFIG_FAILS, e); + log.error(tlUI(Lang.BTN_CONFIG_FAILS, 600), e); + } + } + + private void checkIfNeedRetryConfig() { + if (!configSuccess.get()) { + configBtnNetwork(); } } @@ -108,8 +120,8 @@ private void setupHttpClient() { this.httpClient = Methanol .newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) - .userAgent(Main.getUserAgent()) - .defaultHeader("User-Agent", Main.getUserAgent()) + .userAgent(userAgent) + .defaultHeader("Accept-Encoding", "gzip,deflate") .defaultHeader("Content-Type", "application/json") .defaultHeader("BTN-AppID", appId) .defaultHeader("BTN-AppSecret", appSecret) @@ -122,7 +134,7 @@ private void setupHttpClient() { } public void close() { - log.info(Lang.BTN_SHUTTING_DOWN); + log.info(tlUI(Lang.BTN_SHUTTING_DOWN)); executeService.shutdown(); abilities.values().forEach(BtnAbility::unload); abilities.clear(); diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java index 7f746b1eb3..52eb88ec85 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/BtnRuleParsed.java @@ -1,6 +1,8 @@ package com.ghostchu.peerbanhelper.btn; import com.ghostchu.peerbanhelper.Main; +import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.IPAddressUtil; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.ghostchu.peerbanhelper.util.rule.Rule; @@ -53,8 +55,8 @@ public Map metadata() { } @Override - public String matcherName() { - return "BTN-Port"; + public TranslationComponent matcherName() { + return new TranslationComponent(Lang.BTN_PORT_RULE); } @Override @@ -87,8 +89,8 @@ public BtnRuleIpMatcher(String ruleId, String ruleName, List ruleData } @Override - public @NotNull String matcherName() { - return "BTN-IP"; + public @NotNull TranslationComponent matcherName() { + return new TranslationComponent(Lang.BTN_IP_RULE); } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityReconfigure.java b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityReconfigure.java index 9b273aacbd..f8f445a83c 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityReconfigure.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityReconfigure.java @@ -12,6 +12,8 @@ import java.util.Random; import java.util.concurrent.TimeUnit; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class BtnAbilityReconfigure implements BtnAbility { private final BtnNetwork btnNetwork; @@ -34,7 +36,7 @@ public void load() { private void checkIfReconfigure() { HttpResponse resp = HTTPUtil.retryableSend(btnNetwork.getHttpClient(), MutableRequest.GET(btnNetwork.getConfigUrl()), HttpResponse.BodyHandlers.ofString()).join(); if (resp.statusCode() != 200) { - log.warn(Lang.BTN_RECONFIGURE_CHECK_FAILED, resp.statusCode() + " - " + resp.body()); + log.error(tlUI(Lang.BTN_RECONFIGURE_CHECK_FAILED, resp.statusCode() + " - " + resp.body())); return; } JsonObject json = JsonParser.parseString(resp.body()).getAsJsonObject(); @@ -44,8 +46,8 @@ private void checkIfReconfigure() { } JsonObject reconfigure = ability.get("reconfigure").getAsJsonObject(); if (!reconfigure.get("version").getAsString().equals(this.version)) { - log.info(Lang.BTN_RECONFIGURING); - btnNetwork.getServer().setupBtn(); + log.info(tlUI(Lang.BTN_RECONFIGURING)); + btnNetwork.configBtnNetwork(); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityRules.java b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityRules.java index 60a52af7f2..4189a7f7d6 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityRules.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilityRules.java @@ -23,6 +23,8 @@ import java.util.Random; import java.util.concurrent.TimeUnit; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class BtnAbilityRules implements BtnAbility { private final BtnNetwork btnNetwork; @@ -81,7 +83,7 @@ private void updateRule() { return; } if (r.statusCode() != 200) { - log.warn(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body()); + log.error(tlUI(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body())); } else { BtnRule btr = JsonUtil.getGson().fromJson(r.body(), BtnRule.class); this.btnRule = new BtnRuleParsed(btr); @@ -90,11 +92,11 @@ private void updateRule() { Files.writeString(btnCacheFile.toPath(), r.body(), StandardCharsets.UTF_8); } catch (IOException ignored) { } - log.info(Lang.BTN_UPDATE_RULES_SUCCESSES, this.btnRule.getVersion()); + log.info(tlUI(Lang.BTN_UPDATE_RULES_SUCCESSES, this.btnRule.getVersion())); } }) .exceptionally((e) -> { - log.warn(Lang.BTN_REQUEST_FAILS, e); + log.error(tlUI(Lang.BTN_REQUEST_FAILS), e); return null; }); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitBans.java b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitBans.java index 3c5d5a2fc0..875205d7f1 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitBans.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitBans.java @@ -25,6 +25,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class BtnAbilitySubmitBans implements BtnAbility { private final BtnNetwork btnNetwork; @@ -58,7 +61,7 @@ public void onPeerBanEvent(PeerBanEvent event) { } private void submit() { - log.info(Lang.BTN_SUBMITTING_BANS); + log.info(tlUI(Lang.BTN_SUBMITTING_BANS)); List btnPeers = generateBans(); BtnBanPing ping = new BtnBanPing( System.currentTimeMillis(), @@ -70,13 +73,14 @@ private void submit() { HTTPUtil.nonRetryableSend(btnNetwork.getHttpClient(), request, HttpResponse.BodyHandlers.ofString()) .thenAccept(r -> { if (r.statusCode() != 200) { - log.warn(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body()); + log.error(tlUI(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body())); } else { - log.info(Lang.BTN_SUBMITTED_BANS, btnPeers.size()); + log.info(tlUI(Lang.BTN_SUBMITTED_BANS, btnPeers.size())); + lastReport = System.currentTimeMillis(); } }) .exceptionally(e -> { - log.warn(Lang.BTN_REQUEST_FAILS, e); + log.warn(tlUI(Lang.BTN_REQUEST_FAILS), e); return null; }); } @@ -91,11 +95,10 @@ private List generateBans() { btnBan.setBtnBan(e.getValue().getContext().equals(BtnNetworkOnline.class.getName())); btnBan.setPeer(e.getKey()); btnBan.setModule(e.getValue().getContext()); - btnBan.setRule(e.getValue().getDescription()); + btnBan.setRule(tl(Main.DEF_LOCALE, e.getValue().getDescription())); btnBan.setBanUniqueId(e.getValue().getRandomId().toString()); list.add(btnBan); } - lastReport = System.currentTimeMillis(); return list; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitPeers.java b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitPeers.java index dafe5f750a..a016fd0e0c 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitPeers.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitPeers.java @@ -17,6 +17,8 @@ import java.util.Random; import java.util.concurrent.TimeUnit; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class BtnAbilitySubmitPeers implements BtnAbility { private final BtnNetwork btnNetwork; @@ -38,7 +40,7 @@ public void load() { } private void submit() { - log.info(Lang.BTN_SUBMITTING_PEERS); + log.info(tlUI(Lang.BTN_SUBMITTING_PEERS)); List btnPeers = generatePing(); BtnPeerPing ping = new BtnPeerPing( System.currentTimeMillis(), @@ -50,13 +52,13 @@ private void submit() { HTTPUtil.nonRetryableSend(btnNetwork.getHttpClient(), request, HttpResponse.BodyHandlers.ofString()) .thenAccept(r -> { if (r.statusCode() != 200) { - log.warn(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body()); + log.error(tlUI(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body())); } else { - log.info(Lang.BTN_SUBMITTED_PEERS, btnPeers.size()); + log.info(tlUI(Lang.BTN_SUBMITTED_PEERS, btnPeers.size())); } }) .exceptionally(e -> { - log.warn(Lang.BTN_REQUEST_FAILS, e); + log.warn(tlUI(Lang.BTN_REQUEST_FAILS), e); return null; }); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitRulesHitRate.java b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitRulesHitRate.java index cbc37c7acd..200192455e 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitRulesHitRate.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/ability/BtnAbilitySubmitRulesHitRate.java @@ -21,6 +21,8 @@ import java.util.Random; import java.util.concurrent.TimeUnit; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class BtnAbilitySubmitRulesHitRate implements BtnAbility { private final BtnNetwork btnNetwork; @@ -47,7 +49,7 @@ public void unload() { private void submit() { - log.info(Lang.BTN_SUBMITTING_HITRATE); + log.info(tlUI(Lang.BTN_SUBMITTING_HITRATE)); Map metric = new HashMap<>(btnNetwork.getServer().getHitRateMetric().getHitRateMetric()); List dat = metric.entrySet().stream() .map(obj -> new RuleData(obj.getKey().getClass().getSimpleName(), obj.getValue().getHitCounter(), obj.getValue().getQueryCounter(), obj.getKey().metadata())) @@ -59,13 +61,13 @@ private void submit() { HTTPUtil.nonRetryableSend(btnNetwork.getHttpClient(), request, HttpResponse.BodyHandlers.ofString()) .thenAccept(r -> { if (r.statusCode() != 200) { - log.warn(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body()); + log.error(tlUI(Lang.BTN_REQUEST_FAILS, r.statusCode() + " - " + r.body())); } else { - log.info(Lang.BTN_SUBMITTED_HITRATE, dat.size()); + log.info(tlUI(Lang.BTN_SUBMITTED_HITRATE, dat.size())); } }) .exceptionally(e -> { - log.warn(Lang.BTN_REQUEST_FAILS, e); + log.error(tlUI(Lang.BTN_REQUEST_FAILS), e); return null; }); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/btn/ping/BtnPeer.java b/src/main/java/com/ghostchu/peerbanhelper/btn/ping/BtnPeer.java index 553c2693bb..765eccacb3 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/btn/ping/BtnPeer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/btn/ping/BtnPeer.java @@ -60,7 +60,7 @@ public static BtnPeer from(TorrentWrapper torrent, PeerWrapper peer) { btnPeer.setRtUploadSpeed(peer.getUploadSpeed()); btnPeer.setPeerProgress(peer.getProgress()); btnPeer.setDownloaderProgress(torrent.getProgress()); - btnPeer.setPeerFlag(peer.getFlags()); + btnPeer.setPeerFlag(peer.getFlags() == null ? null : peer.getFlags().toString()); return btnPeer; } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java b/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java index 584ddd1929..f2ea5172d2 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java +++ b/src/main/java/com/ghostchu/peerbanhelper/config/MainConfigUpdateScript.java @@ -9,6 +9,8 @@ import java.io.File; import java.util.UUID; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class MainConfigUpdateScript { private final YamlConfiguration conf; @@ -22,10 +24,32 @@ private void validate() { String token = conf.getString("server.token"); if (token == null || token.isBlank() || token.length() < 8) { conf.set("server.token", UUID.randomUUID().toString()); - log.info(Lang.TOO_WEAK_TOKEN); + log.info(tlUI(Lang.TOO_WEAK_TOKEN)); } } + @UpdateScript(version = 13) + public void proxyServerConfigSection() { + conf.set("proxy.setting", 0); + conf.set("proxy.host", "127.0.0.1"); + conf.set("proxy.port", 7890); + } + + @UpdateScript(version = 12) + public void externalWebUI() { + conf.set("server.external-webui", false); + } + + @UpdateScript(version = 11) + public void corsSetting() { + conf.set("server.allow-cors", false); + } + + @UpdateScript(version = 10) + public void languageSetting() { + conf.set("language", "default"); + } + @UpdateScript(version = 9) public void firewallIntegration() { conf.set("firewall-integration", null); diff --git a/src/main/java/com/ghostchu/peerbanhelper/config/PBHConfigUpdater.java b/src/main/java/com/ghostchu/peerbanhelper/config/PBHConfigUpdater.java index d70707be76..a6eaf3a642 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/config/PBHConfigUpdater.java +++ b/src/main/java/com/ghostchu/peerbanhelper/config/PBHConfigUpdater.java @@ -1,13 +1,17 @@ package com.ghostchu.peerbanhelper.config; -import com.ghostchu.peerbanhelper.text.Lang; +import com.google.common.io.CharStreams; import lombok.extern.slf4j.Slf4j; +import org.bspfsystems.yamlconfiguration.configuration.InvalidConfigurationException; import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -16,16 +20,24 @@ public class PBHConfigUpdater { private static final String CONFIG_VERSION_KEY = "config-version"; private final YamlConfiguration yaml; + private final YamlConfiguration bundle; private final File file; - public PBHConfigUpdater(File file, YamlConfiguration yaml) { + public PBHConfigUpdater(File file, YamlConfiguration yaml, InputStream resourceAsStream) { this.yaml = yaml; this.file = file; + this.bundle = new YamlConfiguration(); + try (resourceAsStream; InputStreamReader reader = new InputStreamReader(resourceAsStream, StandardCharsets.UTF_8)) { + this.bundle.loadFromString(CharStreams.toString(reader)); + } catch (IOException | InvalidConfigurationException e) { + log.error("Unable to load the bundled config from classloader resource", e); + } } public void update(@NotNull Object configUpdateScript) { - log.info(Lang.CONFIG_CHECKING); + log.info("Checking configuration..."); int selectedVersion = yaml.getInt(CONFIG_VERSION_KEY, -1); + String oldContent = yaml.saveToString(); for (Method method : getUpdateScripts(configUpdateScript)) { try { UpdateScript updateScript = method.getAnnotation(UpdateScript.class); @@ -33,12 +45,12 @@ public void update(@NotNull Object configUpdateScript) { if (current >= updateScript.version()) { continue; } - log.info(Lang.CONFIG_MIGRATING, current, updateScript.version()); + log.info("Upgrading configuration from {} to {}...", current, updateScript.version()); String scriptName = updateScript.description(); if (scriptName == null || scriptName.isEmpty()) { scriptName = method.getName(); } - log.info(Lang.CONFIG_EXECUTE_MIGRATE, scriptName); + log.info("Executing upgrade script: {}", scriptName); try { if (method.getParameterCount() == 0) { method.invoke(configUpdateScript); @@ -48,19 +60,38 @@ public void update(@NotNull Object configUpdateScript) { } } } catch (Exception e) { - log.info(Lang.CONFIG_MIGRATE_FAILED, method.getName(), updateScript.version(), e.getMessage(), e); + log.info("Error while executing upgrade script: method={}, target_ver={}", method.getName(), updateScript.version(), e); } yaml.set(CONFIG_VERSION_KEY, updateScript.version()); - log.info(Lang.CONFIG_UPGRADED, updateScript.version()); + log.info("Configuration successfully updated"); } catch (Throwable throwable) { - log.info(Lang.CONFIG_MIGRATE_FAILED, method.getName(), method.getAnnotation(UpdateScript.class).version(), throwable); + log.error("Error while updating configuration, method={}, target_ver={}", method.getName(), method.getAnnotation(UpdateScript.class).version(), throwable); } } - log.info(Lang.CONFIG_SAVE_CHANGES); + log.info("Saving configuration changes..."); try { - yaml.save(file); + migrateComments(yaml, bundle); + String newContent = yaml.saveToString(); + if (!newContent.equals(oldContent)) { + yaml.save(file); + } } catch (IOException e) { - log.warn(Lang.CONFIG_SAVE_ERROR, e); + log.error("Failed to save configuration!", e); + } + } + + private void migrateComments(YamlConfiguration yaml, YamlConfiguration bundle) { + for (String key : yaml.getKeys(true)) { + var inlineBundled = bundle.getInlineComments(key); + var inlineYaml = yaml.getInlineComments(key); + var stdBundled = bundle.getComments(key); + var stdYaml = yaml.getComments(key); + if (inlineYaml.isEmpty() && !inlineBundled.isEmpty()) { + yaml.setInlineComments(key, inlineBundled); + } + if (stdYaml.isEmpty() && !stdBundled.isEmpty()) { + yaml.setComments(key, stdBundled); + } } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java b/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java index fab461d15f..73cc5f569d 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java +++ b/src/main/java/com/ghostchu/peerbanhelper/config/ProfileUpdateScript.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Locale; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class ProfileUpdateScript { private final YamlConfiguration conf; @@ -22,6 +24,18 @@ public ProfileUpdateScript(YamlConfiguration conf) { this.conf = conf; } + @UpdateScript(version = 10) + public void addBanDuration() { + var module = conf.getConfigurationSection("module"); + if (module != null) { + for (String key : module.getKeys(false)) { + var mSec = module.getConfigurationSection(key); + if (mSec != null) { + mSec.set("ban-duration", "default"); + } + } + } + } @UpdateScript(version = 9) public void updateXmRules() { List bannedPeerIds = conf.getStringList("module.peer-id-blacklist.banned-peer-id"); @@ -116,7 +130,7 @@ private List convertRuleStringExclude(List oldRules) { oldRule = oldRule.toLowerCase(Locale.ROOT); String[] ruleExploded = oldRule.split("@", 2); if (ruleExploded.length != 2) { - log.warn(Lang.ERR_INVALID_RULE_SYNTAX, oldRule); + log.error(tlUI(Lang.ERR_INVALID_RULE_SYNTAX, oldRule)); continue; } String matchMethod = ruleExploded[0]; @@ -180,7 +194,7 @@ private List convertRuleString(List oldRules) { oldRule = oldRule.toLowerCase(Locale.ROOT); String[] ruleExploded = oldRule.split("@", 2); if (ruleExploded.length != 2) { - log.warn(Lang.ERR_INVALID_RULE_SYNTAX, oldRule); + log.error(tlUI(Lang.ERR_INVALID_RULE_SYNTAX, oldRule)); continue; } String matchMethod = ruleExploded[0]; diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/BanLog.java b/src/main/java/com/ghostchu/peerbanhelper/database/BanLog.java deleted file mode 100644 index 8d4a84e16e..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/database/BanLog.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ghostchu.peerbanhelper.database; - -import org.jetbrains.annotations.Nullable; - -public record BanLog( - long banAt, - long unbanAt, - String peerIp, - int peerPort, - String peerId, - String peerClientName, - long peerUploaded, - long peerDownloaded, - double peerProgress, - String torrentInfoHash, - String torrentName, - long torrentSize, - @Nullable - String module, - String description -) { -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/Database.java b/src/main/java/com/ghostchu/peerbanhelper/database/Database.java new file mode 100644 index 0000000000..ed76e96528 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/Database.java @@ -0,0 +1,52 @@ +package com.ghostchu.peerbanhelper.database; + +import com.ghostchu.peerbanhelper.Main; +import com.j256.ormlite.field.DataPersisterManager; +import com.j256.ormlite.jdbc.JdbcSingleConnectionSource; +import com.j256.ormlite.jdbc.db.SqliteDatabaseType; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import lombok.Getter; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.sql.SQLException; + +@Getter +@Component +public class Database { + private JdbcSingleConnectionSource dataSource; + private HikariDataSource hikari; + + public Database() throws SQLException { + File databaseDirectory = new File(Main.getDataDirectory(), "persist"); + if (!databaseDirectory.exists()) { + databaseDirectory.mkdirs(); + } + File sqliteDb = new File(databaseDirectory, "peerbanhelper.db"); + registerPersisters(); + setupDatabase(sqliteDb); + } + + private void registerPersisters() { + DataPersisterManager.registerDataPersisters(TranslationComponentPersistener.getSingleton()); + } + + public void setupDatabase(File file) throws SQLException { + HikariConfig config = new HikariConfig(); + config.setPoolName("PeerBanHelper SQLite Connection Pool"); + config.setDriverClassName("org.sqlite.JDBC"); + config.setJdbcUrl("jdbc:sqlite:" + file); + config.setConnectionTestQuery("SELECT 1"); + config.setMaxLifetime(60000); // 60 Sec + config.setMaximumPoolSize(1); // 50 Connections (including idle connections) + this.hikari = new HikariDataSource(config); + this.dataSource = new JdbcSingleConnectionSource("jdbc:sqlite:" + file, new SqliteDatabaseType(), this.hikari.getConnection()); + + // this.dataSource = new DataSourceConnectionSource( new HikariDataSource(config), new SqliteDatabaseType()); + } + + public void close() { + this.dataSource.closeQuietly(); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java b/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java index 321e4ef1f3..cf6c0f31bd 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java +++ b/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseHelper.java @@ -1,408 +1,60 @@ package com.ghostchu.peerbanhelper.database; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; -import com.ghostchu.peerbanhelper.module.IPBanRuleUpdateType; -import com.ghostchu.peerbanhelper.text.Lang; -import lombok.Cleanup; -import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - -import java.sql.Date; -import java.sql.*; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.*; - -@Slf4j +import com.ghostchu.peerbanhelper.database.table.*; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.logger.Level; +import com.j256.ormlite.logger.Logger; +import com.j256.ormlite.support.BaseConnectionSource; +import com.j256.ormlite.table.TableUtils; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; + +@Component +@Getter public class DatabaseHelper { - private final DatabaseManager manager; - private final PeerBanHelperServer server; + private final Database database; - public DatabaseHelper(PeerBanHelperServer server, DatabaseManager manager) throws SQLException { - this.server = server; - this.manager = manager; - try { - createTables(); - performUpgrade(); - int cleaned = cleanOutdatedBanLogs(); - if (cleaned > 0) { - log.info(Lang.DATABASE_OUTDATED_LOGS_CLEANED_UP, cleaned); - } - } catch (SQLException e) { - log.warn(Lang.DATABASE_SETUP_FAILED, e); - throw e; - } - } - - private void performUpgrade() throws SQLException { - try (Connection connection = manager.getConnection()) { - int v = 0; - String version = getMetadata("version"); - if (version != null) { - v = Integer.parseInt(version); - } - if (v == 0) { - try { - // 升级 peer_id / peer_clientname 段可空 - connection.prepareStatement("ALTER TABLE ban_logs RENAME TO ban_logs_v1_old_backup").execute(); - connection.prepareStatement("DROP INDEX ban_logs_idx").execute(); - createTables(); - } catch (Exception ignored) { - } - v++; - } - setMetadata("version", String.valueOf(v)); - } + public DatabaseHelper(@Autowired Database database) throws SQLException { + this.database = database; + Logger.setGlobalLogLevel(Level.WARNING); + createTables(); } - public int setMetadata(String key, String value) throws SQLException { - try (Connection connection = manager.getConnection()) { - @Cleanup - PreparedStatement ps = connection.prepareStatement("REPLACE INTO metadata (key, value) VALUES (?,?)"); - ps.setString(1, key); - ps.setString(2, value); - return ps.executeUpdate(); - } - } - public String getMetadata(String key) throws SQLException { - try (Connection connection = manager.getConnection()) { - @Cleanup - PreparedStatement ps = connection.prepareStatement("SELECT * FROM metadata WHERE `key` = ? LIMIT 1"); - @Cleanup - ResultSet set = ps.executeQuery(); - if (set.next()) { - return set.getString("key"); - } else { - return null; - } - } - } - - public long queryBanLogsCount() throws SQLException { - try (Connection connection = manager.getConnection()) { - @Cleanup - ResultSet set = connection.createStatement().executeQuery("SELECT COUNT(*) AS count FROM ban_logs"); - return set.getLong("count"); - } - } - - public List queryBanLogs(Date from, Date to, int pageIndex, int pageSize) throws SQLException { - try (Connection connection = manager.getConnection()) { - PreparedStatement ps; - if (from == null && to == null) { - ps = connection.prepareStatement("SELECT * FROM ban_logs ORDER BY id DESC LIMIT " + (pageIndex * pageSize) + ", " + pageSize); - } else { - if (from == null || to == null) { - throw new IllegalArgumentException("from or null cannot be null if any provided"); - } else { - ps = connection.prepareStatement("SELECT * FROM ban_logs WHERE ban_at >= ? AND ban_at <= ? ORDER BY id DESC LIMIT " + (pageIndex * pageSize) + ", " + pageSize); - } - } - try (ps) { - if (from != null) { - ps.setDate(1, from); - ps.setDate(2, to); - } - try (ResultSet set = ps.executeQuery()) { - List logs = new LinkedList<>(); // 尽可能节约内存,不使用 ArrayList - while (set.next()) { - BanLog banLog = new BanLog( - set.getLong("ban_at"), - set.getLong("unban_at"), - set.getString("peer_ip"), - set.getInt("peer_port"), - set.getString("peer_id"), - set.getString("peer_clientname"), - set.getLong("peer_uploaded"), - set.getLong("peer_downloaded"), - set.getDouble("peer_progress"), - set.getString("torrent_infohash"), - set.getString("torrent_name"), - set.getLong("torrent_size"), - set.getString("module"), - set.getString("description") - ); - logs.add(banLog); - } - return logs; - } - } - } - } - - public Map findMaxBans(int n) throws SQLException { - try (Connection connection = manager.getConnection()) { - @Cleanup - PreparedStatement ps = connection.prepareStatement("SELECT peer_ip, COUNT(*) AS count " + - "FROM ban_logs WHERE ban_at >= ?" + - "GROUP BY peer_ip " + - "ORDER BY count DESC LIMIT " + n); - ps.setTimestamp(1, new Timestamp(Instant.now().minus(14, ChronoUnit.DAYS).toEpochMilli())); - @Cleanup - ResultSet set = ps.executeQuery(); - Map map = new LinkedHashMap<>(); - while (set.next()) { - map.put(set.getString("peer_ip"), set.getLong("count")); - } - return map; - } - } - - public int cleanOutdatedBanLogs() { - int days = server.getMainConfig().getInt("persist.ban-logs-keep-days", 30); - try (Connection connection = manager.getConnection()) { - @Cleanup - PreparedStatement ps = connection.prepareStatement("DELETE FROM ban_logs where unban_at < ?"); - Instant instant = Instant.now().minus(days, ChronoUnit.DAYS); - ps.setTimestamp(1, new Timestamp(instant.toEpochMilli())); - return ps.executeUpdate(); - } catch (SQLException e) { - throw new RuntimeException(e); - } + private void createTables() throws SQLException { + TableUtils.createTableIfNotExists(database.getDataSource(), MetadataEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), TorrentEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), ModuleEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), RuleEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), HistoryEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), BanListEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), RuleSubInfoEntity.class); + TableUtils.createTableIfNotExists(database.getDataSource(), RuleSubLogEntity.class); } - public int insertBanLogs(BanLog banLog) throws SQLException { - try (Connection connection = manager.getConnection()) { - @Cleanup - PreparedStatement ps = connection.prepareStatement("INSERT INTO ban_logs (ban_at, unban_at, peer_ip, peer_port, peer_id, peer_clientname," + - " peer_downloaded, peer_uploaded, peer_progress, torrent_infohash, torrent_name, torrent_size, module, description)" + - "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"); - ps.setLong(1, banLog.banAt()); - ps.setLong(2, banLog.unbanAt()); - ps.setString(3, banLog.peerIp()); - ps.setInt(4, banLog.peerPort()); - String peerId = banLog.peerId(); - ps.setString(5, peerId); - String clientName = banLog.peerClientName(); - ps.setString(6, clientName); - ps.setLong(7, banLog.peerDownloaded()); - ps.setLong(8, banLog.peerUploaded()); - ps.setDouble(9, banLog.peerProgress()); - ps.setString(10, banLog.torrentInfoHash()); - ps.setString(11, banLog.torrentName()); - ps.setLong(12, banLog.torrentSize()); - ps.setString(13, banLog.module()); - ps.setString(14, banLog.description()); - return ps.executeUpdate(); - } - } - - public void createTables() throws SQLException { - try (Connection connection = manager.getConnection()) { - if (!hasTable("ban_logs")) { - @Cleanup - PreparedStatement ps = connection.prepareStatement(""" - CREATE TABLE ban_logs ( - "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - "ban_at" integer NOT NULL, - "unban_at" integer NOT NULL, - "peer_ip" TEXT NOT NULL, - "peer_port" integer NOT NULL, - "peer_id" TEXT NULL, - "peer_clientname" TEXT NULL, - "peer_downloaded" integer NOT NULL, - "peer_uploaded" integer NOT NULL, - "peer_progress" real NOT NULL, - "torrent_infohash" TEXT NOT NULL, - "torrent_name" TEXT NOT NULL, - "torrent_size" integer NOT NULL, - "module" TEXT, - "description" TEXT NOT NULL - ); - """); - ps.executeUpdate(); - ps = connection.prepareStatement(""" - CREATE INDEX ban_logs_idx - ON ban_logs ( - "id", - "ban_at", - "peer_ip", - "torrent_infohash", - "module" - ); - """); - ps.executeUpdate(); - } - if (!hasTable("metadata")) { - @Cleanup - PreparedStatement ps = connection.prepareStatement(""" - CREATE TABLE metadata ( - "key" TEXT NOT NULL PRIMARY KEY, - "value" TEXT - ); - """); - ps.executeUpdate(); - } - if (!hasTable("rule_sub_logs")) { - @Cleanup - PreparedStatement ps = connection.prepareStatement(""" - create table rule_sub_logs - ( - id integer not null - constraint rule_sub_logs_pk - primary key autoincrement, - rule_id integer not null, - update_time integer, - ent_count integer, - update_type TEXT - ); - """); - ps.executeUpdate(); - } - if (hasTable("rule_sub_logs")) { - @Cleanup - PreparedStatement ps1 = connection.prepareStatement(""" - update rule_sub_logs set update_type = 'AUTO' where update_type = '自动更新'; - """); - ps1.executeUpdate(); - @Cleanup - PreparedStatement ps2 = connection.prepareStatement(""" - update rule_sub_logs set update_type = 'MANUAL' where update_type = '手动更新'; - """); - ps2.executeUpdate(); - } - } - } - - - /** - * Returns true if the given table has the given column - * - * @param table The table - * @param column The column - * @return True if the given table has the given column - * @throws SQLException If the database isn't connected - */ - public boolean hasColumn(@NotNull String table, @NotNull String column) throws SQLException { - if (!hasTable(table)) { - return false; - } - String query = "SELECT * FROM " + table + " LIMIT 1"; - boolean match = false; - try (Connection connection = manager.getConnection(); PreparedStatement ps = connection.prepareStatement(query); ResultSet rs = ps.executeQuery()) { - ResultSetMetaData metaData = rs.getMetaData(); - for (int i = 1; i <= metaData.getColumnCount(); i++) { - if (metaData.getColumnLabel(i).equals(column)) { - match = true; - break; - } - } - } catch (SQLException e) { - return match; - } - return match; // Uh, wtf. - } - - - /** - * Returns true if the table exists - * - * @param table The table to check for - * @return True if the table is found - * @throws SQLException Throw exception when failed execute somethins on SQL - */ - public boolean hasTable(@NotNull String table) throws SQLException { - Connection connection = manager.getConnection(); - boolean match = false; - try (ResultSet rs = connection.getMetaData().getTables(null, null, "%", null)) { - while (rs.next()) { - if (table.equalsIgnoreCase(rs.getString("TABLE_NAME"))) { - match = true; - break; - } - } - } finally { - connection.close(); - } - return match; - } - - /** - * 查询订阅规则更新日志数量 - * - * @param ruleId 订阅规则ID - * @return 日志数量 - * @throws SQLException SQL异常 - */ - public int countRuleSubLogs(String ruleId) throws SQLException { - try (Connection connection = manager.getConnection()) { - PreparedStatement ps; - boolean idNotEmpty = null != ruleId && !ruleId.isEmpty(); - String sql = "SELECT count(1) as count FROM rule_sub_logs " + - (idNotEmpty ? "WHERE rule_id = ? " : ""); - ps = connection.prepareStatement(sql); - if (idNotEmpty) { - ps.setString(1, ruleId); - } - try (ps) { - try (ResultSet set = ps.executeQuery()) { - int count = 0; - while (set.next()) { - count = set.getInt("count"); - } - return count; - } + private void performUpgrade() throws SQLException { + Dao metadata = DaoManager.createDao(getDataSource(), MetadataEntity.class); + MetadataEntity version = metadata.createIfNotExists(new MetadataEntity("version", "1")); + int v = Integer.parseInt(version.getValue()); + if (v == 0) { + try { + // so something + } catch (Exception ignored) { } + v++; } + version.setValue(String.valueOf(v)); + metadata.update(version); } - /** - * 查询订阅规则更新日志 - * - * @param ruleId 订阅规则ID - * @param pageIndex 页码 - * @param pageSize 每页数量 - * @return 日志列表 - * @throws SQLException SQL异常 - */ - public List queryRuleSubLogs(String ruleId, int pageIndex, int pageSize) throws SQLException { - try (Connection connection = manager.getConnection()) { - PreparedStatement ps; - boolean idNotEmpty = null != ruleId && !ruleId.isEmpty(); - String sql = "SELECT * FROM rule_sub_logs " + - (idNotEmpty ? "WHERE rule_id = ? " : "") + - "ORDER BY id DESC LIMIT " + pageIndex * pageSize + ", " + pageSize; - ps = connection.prepareStatement(sql); - if (idNotEmpty) { - ps.setString(1, ruleId); - } - try (ps) { - try (ResultSet set = ps.executeQuery()) { - List infos = new ArrayList<>(); - while (set.next()) { - infos.add(new RuleSubLog( - set.getString("rule_id"), - set.getLong("update_time"), - set.getInt("ent_count"), - IPBanRuleUpdateType.valueOf(set.getString("update_type")) - )); - } - return infos; - } - } - } + public BaseConnectionSource getDataSource() { + return database.getDataSource(); } - /** - * 插入订阅规则更新日志 - * - * @param ruleId 订阅规则ID - * @param count 更新数量 - * @param updateType 更新类型 - * @throws SQLException SQL异常 - */ - public void insertRuleSubLog(String ruleId, int count, IPBanRuleUpdateType updateType) throws SQLException { - try (Connection connection = manager.getConnection()) { - PreparedStatement ps; - ps = connection.prepareStatement("INSERT INTO rule_sub_logs (rule_id, update_time, ent_count, update_type) VALUES (?,?,?,?)"); - ps.setString(1, ruleId); - ps.setLong(2, System.currentTimeMillis()); - ps.setInt(3, count); - ps.setString(4, updateType.toString()); - ps.executeUpdate(); - } - } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseManager.java b/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseManager.java deleted file mode 100644 index 47d984c54e..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/database/DatabaseManager.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.ghostchu.peerbanhelper.database; - -import com.ghostchu.peerbanhelper.Main; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; - -import java.io.File; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; - -public class DatabaseManager { - private HikariDataSource ds; - private final ScheduledExecutorService cleanupService = Executors.newScheduledThreadPool(1); - - public DatabaseManager() { - File databaseDirectory = new File(Main.getDataDirectory(), "persist"); - if (!databaseDirectory.exists()) { - databaseDirectory.mkdirs(); - } - File sqliteDb = new File(databaseDirectory, "persist-data.db"); - setupDatabase(sqliteDb); - } - - public void setupDatabase(File file) { - HikariConfig config = new HikariConfig(); - config.setPoolName("PeerBanHelper SQLite Connection Pool"); - config.setDriverClassName("org.sqlite.JDBC"); - config.setJdbcUrl("jdbc:sqlite:" + file); - config.setConnectionTestQuery("SELECT 1"); - config.setMaxLifetime(60000); // 60 Sec - config.setMaximumPoolSize(4); // 50 Connections (including idle connections) - this.ds = new HikariDataSource(config); - } - - public void close() { - this.ds.close(); - } - - public Connection getConnection() throws SQLException { - return ds.getConnection(); - } -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/RuleSubInfo.java b/src/main/java/com/ghostchu/peerbanhelper/database/RuleSubInfo.java deleted file mode 100644 index 15ecf1270b..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/database/RuleSubInfo.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ghostchu.peerbanhelper.database; - -public record RuleSubInfo( - String ruleId, - boolean enabled, - String ruleName, - String subUrl, - long lastUpdate, - int entCount -) { -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/RuleSubLog.java b/src/main/java/com/ghostchu/peerbanhelper/database/RuleSubLog.java deleted file mode 100644 index ab557f50cb..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/database/RuleSubLog.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.ghostchu.peerbanhelper.database; - -import com.ghostchu.peerbanhelper.module.IPBanRuleUpdateType; - -public record RuleSubLog( - String ruleId, - long updateTime, - int count, - IPBanRuleUpdateType updateType -) { -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/TranslationComponentPersistener.java b/src/main/java/com/ghostchu/peerbanhelper/database/TranslationComponentPersistener.java new file mode 100644 index 0000000000..324dfdcf15 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/TranslationComponentPersistener.java @@ -0,0 +1,39 @@ +package com.ghostchu.peerbanhelper.database; + +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import com.ghostchu.peerbanhelper.util.JsonUtil; +import com.j256.ormlite.field.FieldType; +import com.j256.ormlite.field.SqlType; +import com.j256.ormlite.field.types.StringType; + +public class TranslationComponentPersistener extends StringType { + + private static final TranslationComponentPersistener INSTANCE = new TranslationComponentPersistener(); + + private TranslationComponentPersistener() { + super(SqlType.STRING, new Class[]{TranslationComponent.class}); + } + + public static TranslationComponentPersistener getSingleton() { + return INSTANCE; + } + + @Override + public Object javaToSqlArg(FieldType fieldType, Object javaObject) { + TranslationComponent myFieldClass = (TranslationComponent) javaObject; + return myFieldClass != null ? getJsonFromMyFieldClass(myFieldClass) : null; + } + + @Override + public Object sqlArgToJava(FieldType fieldType, Object sqlArg, int columnPos) { + return sqlArg != null ? getMyFieldClassFromJson((String) sqlArg) : null; + } + + private String getJsonFromMyFieldClass(TranslationComponent myFieldClass) { + return JsonUtil.standard().toJson(myFieldClass); + } + + private TranslationComponent getMyFieldClassFromJson(String json) { + return JsonUtil.standard().fromJson(json, TranslationComponent.class); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/AbstractPBHDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/AbstractPBHDao.java new file mode 100644 index 0000000000..f22a778cd8 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/AbstractPBHDao.java @@ -0,0 +1,51 @@ +package com.ghostchu.peerbanhelper.database.dao; + +import com.j256.ormlite.dao.BaseDaoImpl; +import com.j256.ormlite.field.FieldType; +import com.j256.ormlite.stmt.QueryBuilder; +import com.j256.ormlite.stmt.SelectArg; +import com.j256.ormlite.stmt.Where; +import com.j256.ormlite.support.ConnectionSource; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.List; + +public class AbstractPBHDao extends BaseDaoImpl { + protected AbstractPBHDao(ConnectionSource connectionSource, Class dataClass) throws SQLException { + super(connectionSource, dataClass); + } + + public List queryByPaging(QueryBuilder qb, long pageIndex, long pageSize) throws SQLException { + return query(qb.offset(pageIndex * pageSize).limit(pageSize).prepare()); + } + + public List queryByPaging(ID id, long pageIndex, long pageSize) throws SQLException { + return query(queryBuilder().offset(pageIndex * pageSize).limit(pageSize).prepare()); + } + + public List queryByPaging(long pageIndex, long pageSize) throws SQLException { + return query(queryBuilder().offset(pageIndex * pageSize).limit(pageSize).prepare()); + } + + public List queryByPagingMatchArgs(T matchObject, long pageIndex, long pageSize) throws SQLException { + checkForInitialized(); + QueryBuilder qb = queryBuilder(); + Where where = qb.where(); + int fieldC = 0; + for (FieldType fieldType : tableInfo.getFieldTypes()) { + Object fieldValue = fieldType.getFieldValueIfNotDefault(matchObject); + if (fieldValue != null) { + fieldValue = new SelectArg(fieldValue); + where.eq(fieldType.getColumnName(), fieldValue); + fieldC++; + } + } + if (fieldC == 0) { + return Collections.emptyList(); + } else { + where.and(fieldC); + return qb.offset(pageIndex * pageSize).limit(pageSize).query(); + } + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/BanListDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/BanListDao.java new file mode 100644 index 0000000000..a5ccf211f4 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/BanListDao.java @@ -0,0 +1,49 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.BanListEntity; +import com.ghostchu.peerbanhelper.util.JsonUtil; +import com.ghostchu.peerbanhelper.wrapper.BanMetadata; +import com.ghostchu.peerbanhelper.wrapper.PeerAddress; +import com.j256.ormlite.table.TableUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +public class BanListDao extends AbstractPBHDao { + public BanListDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), BanListEntity.class); + } + + public Map readBanList() { + Map map = new HashMap<>(); + try { + queryForAll().forEach(e -> map.put( + JsonUtil.standard().fromJson(e.getAddress(), PeerAddress.class) + , JsonUtil.standard().fromJson(e.getMetadata(), BanMetadata.class))); + } catch (Exception e) { + log.error("Unable to read stored banlist, skipping...", e); + } + return map; + } + + public int saveBanList(Map banlist) throws SQLException { +// TableUtils.dropTable(this, true); +// TableUtils.createTableIfNotExists(getConnectionSource(), BanListEntity.class); + TableUtils.clearTable(getConnectionSource(), BanListEntity.class); + List entityList = new ArrayList<>(); + banlist.forEach((key, value) -> entityList.add(new BanListEntity( + JsonUtil.standard().toJson(key) + , JsonUtil.standard().toJson(value)))); + return create(entityList); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/HistoryDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/HistoryDao.java new file mode 100644 index 0000000000..136dd82475 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/HistoryDao.java @@ -0,0 +1,192 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.HistoryEntity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.regex.Pattern; + +@Component +public class HistoryDao extends AbstractPBHDao { + private final Pattern sqlSafePattern; + + public HistoryDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), HistoryEntity.class); + this.sqlSafePattern = Pattern.compile("^[A-Za-z0-9]+$"); + } + + @Override + public synchronized HistoryEntity createIfNotExists(HistoryEntity data) throws SQLException { + List list = queryForMatchingArgs(data); + if (list.isEmpty()) { + return super.createIfNotExists(data); + } + return list.getFirst(); + } + + public Map getBannedIps(int n) throws Exception { + Timestamp twoWeeksAgo = new Timestamp(Instant.now().minus(14, ChronoUnit.DAYS).toEpochMilli()); + + String sql = "SELECT ip, COUNT(*) AS count FROM " + getTableName() + " WHERE banAt >= ? " + + "GROUP BY ip ORDER BY count DESC LIMIT " + n; + + Map result = new HashMap<>(); + var banLogs = super.queryRaw(sql, twoWeeksAgo.toString()); + try (banLogs) { + var results = banLogs.getResults(); + results.forEach(arr -> result.put(arr[0], Long.parseLong(arr[1]))); + } + return result; + } + + public List sumField(String field, double percentFilter) throws Exception { + // SQL 无 PreparedStatement 防注入;这绝对不是最佳实践,但在这个场景下足够用了 + if (!sqlSafePattern.matcher(field).matches()) { + throw new IllegalArgumentException("Invalid field: " + field + ", only A-Z a-z 0-9 is allowed."); + } + List results = new ArrayList<>(); + var sql = """ + SELECT + %field%, + SUM( %field% ) AS ct, + SUM( %field% ) * 1.0 / ( SELECT SUM( %field% ) FROM history ) AS percent , + torrentName, + torrentInfoHash, + module + FROM + ( + SELECT + *, + torrents.infoHash AS torrentInfoHash, + torrents.name AS torrentName, + modules.name AS module\s + FROM + ( + ( ( history INNER JOIN torrents ON history.torrent_id = torrents.id ) INNER JOIN rules ON history.rule_id = rules.id )\s + ) + INNER JOIN modules ON modules.id = rules.module_id\s + )\s + GROUP BY + %field%\s + HAVING + percent > %percent%\s + ORDER BY + ct DESC; + """; + sql = sql.replace("%field%", field) + .replace("%percent%", String.valueOf(percentFilter)); + try (var resultSet = queryRaw(sql)) { + for (String[] result : resultSet.getResults()) { + results.add(new UniversalFieldNumResult(result[0], Long.parseLong(result[1]), Double.parseDouble(result[2]))); + } + } + return results; + } + + public List countField(String field, double percentFilter) throws Exception { + // SQL 无 PreparedStatement 防注入;这绝对不是最佳实践,但在这个场景下足够用了 + if (!sqlSafePattern.matcher(field).matches()) { + throw new IllegalArgumentException("Invalid field: " + field + ", only A-Z a-z 0-9 is allowed."); + } + List results = new ArrayList<>(); + var sql = """ + SELECT + %field%, + COUNT( %field% ) AS ct, + COUNT( %field% ) * 1.0 / ( SELECT COUNT( %field% ) FROM history ) AS percent , + torrentName, + torrentInfoHash, + module + FROM + ( + SELECT + *, + torrents.infoHash AS torrentInfoHash, + torrents.name AS torrentName, + modules.name AS module\s + FROM + ( + ( ( history INNER JOIN torrents ON history.torrent_id = torrents.id ) INNER JOIN rules ON history.rule_id = rules.id )\s + ) + INNER JOIN modules ON modules.id = rules.module_id\s + )\s + GROUP BY + %field%\s + HAVING + percent > %percent%\s + ORDER BY + ct DESC; + """; + + sql = sql.replace("%field%", field) + .replace("%percent%", String.valueOf(percentFilter)); + try (var resultSet = queryRaw(sql)) { + for (String[] result : resultSet.getResults()) { + results.add(new UniversalFieldNumResult(result[0], Long.parseLong(result[1]), Double.parseDouble(result[2]))); + } + } + return results; + } + + public List countDateField(long startAt, long endAt, Function timestampGetter, Function timestampTrimmer, double percentFilter) throws Exception { + Map counterMap = new HashMap<>(); + try (var it = iterator()) { + while (it.hasNext()) { + var row = it.next(); + Timestamp field = timestampGetter.apply(row); + long fieldT = field.getTime(); + if (!(fieldT >= startAt && fieldT <= endAt)) { + continue; + } + Calendar fuckCal = Calendar.getInstance(); + fuckCal.setTime(field); + Calendar trimmed = timestampTrimmer.apply(fuckCal); + long time = trimmed.getTime().getTime(); + AtomicLong atomicLong = counterMap.getOrDefault(time, new AtomicLong(0)); + atomicLong.incrementAndGet(); + counterMap.put(time, atomicLong); + } + } + // 计算总量 + long total = counterMap.values().stream().mapToLong(AtomicLong::get).sum(); + List results = new ArrayList<>(); + for (Map.Entry dateMappingAtomicLongEntry : counterMap.entrySet()) { + results.add(new UniversalFieldDateResult(dateMappingAtomicLongEntry.getKey(), + dateMappingAtomicLongEntry.getValue().get(), + (double) dateMappingAtomicLongEntry.getValue().get() / total + )); + } + results.removeIf(r -> r.percent() < percentFilter); + return results; + } + + + public record UniversalFieldNumResult(String data, long count, double percent) { + + } + + public record UniversalFieldDateResult(long timestamp, long count, + double percent) { + + } + + @NoArgsConstructor + @AllArgsConstructor + @Data + public static class PeerBanCount { + private String peerIp; + private long count; + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/MetadataDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/MetadataDao.java new file mode 100644 index 0000000000..f0240abb50 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/MetadataDao.java @@ -0,0 +1,26 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.MetadataEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; +import java.util.List; + +@Component +public class MetadataDao extends AbstractPBHDao { + public MetadataDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), MetadataEntity.class); + } + + @Override + public synchronized MetadataEntity createIfNotExists(MetadataEntity data) throws SQLException { + List list = queryForMatchingArgs(data); + if (list.isEmpty()) { + return super.createIfNotExists(data); + } + return list.getFirst(); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/ModuleDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/ModuleDao.java new file mode 100644 index 0000000000..5127cc5296 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/ModuleDao.java @@ -0,0 +1,26 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.ModuleEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; +import java.util.List; + +@Component +public class ModuleDao extends AbstractPBHDao { + public ModuleDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), ModuleEntity.class); + } + + @Override + public synchronized ModuleEntity createIfNotExists(ModuleEntity data) throws SQLException { + List list = queryForMatchingArgs(data); + if (list.isEmpty()) { + return super.createIfNotExists(data); + } + return list.getFirst(); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleDao.java new file mode 100644 index 0000000000..f1be02ef8b --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleDao.java @@ -0,0 +1,26 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.RuleEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; +import java.util.List; + +@Component +public class RuleDao extends AbstractPBHDao { + public RuleDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), RuleEntity.class); + } + + @Override + public synchronized RuleEntity createIfNotExists(RuleEntity data) throws SQLException { + List list = queryForMatchingArgs(data); + if (list.isEmpty()) { + return super.createIfNotExists(data); + } + return list.getFirst(); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleSubInfoDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleSubInfoDao.java new file mode 100644 index 0000000000..79fdac7375 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleSubInfoDao.java @@ -0,0 +1,16 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.RuleSubInfoEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; + +@Component +public class RuleSubInfoDao extends AbstractPBHDao { + public RuleSubInfoDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), RuleSubInfoEntity.class); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleSubLogsDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleSubLogsDao.java new file mode 100644 index 0000000000..adbce9be8b --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/RuleSubLogsDao.java @@ -0,0 +1,16 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.RuleSubLogEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; + +@Component +public class RuleSubLogsDao extends AbstractPBHDao { + public RuleSubLogsDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), RuleSubLogEntity.class); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/TorrentDao.java b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/TorrentDao.java new file mode 100644 index 0000000000..f8c95aa2e5 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/dao/impl/TorrentDao.java @@ -0,0 +1,26 @@ +package com.ghostchu.peerbanhelper.database.dao.impl; + +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.AbstractPBHDao; +import com.ghostchu.peerbanhelper.database.table.TorrentEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.sql.SQLException; +import java.util.List; + +@Component +public class TorrentDao extends AbstractPBHDao { + public TorrentDao(@Autowired Database database) throws SQLException { + super(database.getDataSource(), TorrentEntity.class); + } + + @Override + public synchronized TorrentEntity createIfNotExists(TorrentEntity data) throws SQLException { + List list = queryForMatchingArgs(data); + if (list.isEmpty()) { + return super.createIfNotExists(data); + } + return list.getFirst(); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/BanListEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/BanListEntity.java new file mode 100644 index 0000000000..5138991876 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/BanListEntity.java @@ -0,0 +1,18 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "banlist") +public final class BanListEntity { + @DatabaseField(id = true) + private String address; + @DatabaseField(canBeNull = false) + private String metadata; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/HistoryEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/HistoryEntity.java new file mode 100644 index 0000000000..5297cc95f6 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/HistoryEntity.java @@ -0,0 +1,46 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.ghostchu.peerbanhelper.database.TranslationComponentPersistener; +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "history") +public final class HistoryEntity { + @DatabaseField(generatedId = true) + private Long id; + @DatabaseField(canBeNull = false) + private Timestamp banAt; + @DatabaseField(canBeNull = false) + private Timestamp unbanAt; + @DatabaseField(canBeNull = false) + private String ip; + @DatabaseField(canBeNull = false) + private Integer port; + @DatabaseField + private String peerId; + @DatabaseField + private String peerClientName; + @DatabaseField + private Long peerUploaded; + @DatabaseField + private Long peerDownloaded; + @DatabaseField(canBeNull = false) + private Double peerProgress; + @DatabaseField(canBeNull = false, foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true) + private TorrentEntity torrent; + @DatabaseField(canBeNull = false, foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true) + private RuleEntity rule; + @DatabaseField(canBeNull = false, persisterClass = TranslationComponentPersistener.class) + private TranslationComponent description; + @DatabaseField + private String flags; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/MetadataEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/MetadataEntity.java new file mode 100644 index 0000000000..05b080fdec --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/MetadataEntity.java @@ -0,0 +1,18 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "metadata") +public final class MetadataEntity { + @DatabaseField(id = true) + private String key; + @DatabaseField + private String value; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/ModuleEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/ModuleEntity.java new file mode 100644 index 0000000000..59f36289dc --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/ModuleEntity.java @@ -0,0 +1,18 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "modules") +public final class ModuleEntity { + @DatabaseField(generatedId = true) + private Long id; + @DatabaseField(unique = true) + private String name; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleEntity.java new file mode 100644 index 0000000000..b5a713a15c --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleEntity.java @@ -0,0 +1,22 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.ghostchu.peerbanhelper.database.TranslationComponentPersistener; +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "rules") +public final class RuleEntity { + @DatabaseField(generatedId = true) + private Long id; + @DatabaseField(canBeNull = false, foreign = true, foreignAutoCreate = true, foreignAutoRefresh = true, uniqueCombo = true) + private ModuleEntity module; + @DatabaseField(canBeNull = false, uniqueCombo = true, persisterClass = TranslationComponentPersistener.class) + private TranslationComponent rule; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleSubInfoEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleSubInfoEntity.java new file mode 100644 index 0000000000..6fc45ee217 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleSubInfoEntity.java @@ -0,0 +1,26 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "rule_sub_info") +public class RuleSubInfoEntity { + @DatabaseField(id = true) + private String ruleId; + @DatabaseField + private boolean enabled; + @DatabaseField + private String ruleName; + @DatabaseField + private String subUrl; + @DatabaseField + private long lastUpdate; + @DatabaseField + private int entCount; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleSubLogEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleSubLogEntity.java new file mode 100644 index 0000000000..e47a0f5f75 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/RuleSubLogEntity.java @@ -0,0 +1,26 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.ghostchu.peerbanhelper.module.IPBanRuleUpdateType; +import com.j256.ormlite.field.DataType; +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "rule_sub_log") +public class RuleSubLogEntity { + @DatabaseField(generatedId = true) + private Long id; + @DatabaseField + private String ruleId; + @DatabaseField + private long updateTime; + @DatabaseField + private int count; + @DatabaseField(dataType = DataType.ENUM_NAME) + private IPBanRuleUpdateType updateType; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/database/table/TorrentEntity.java b/src/main/java/com/ghostchu/peerbanhelper/database/table/TorrentEntity.java new file mode 100644 index 0000000000..3616b06474 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/database/table/TorrentEntity.java @@ -0,0 +1,22 @@ +package com.ghostchu.peerbanhelper.database.table; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Data +@DatabaseTable(tableName = "torrents") +public final class TorrentEntity { + @DatabaseField(generatedId = true) + private Long id; + @DatabaseField(canBeNull = false, uniqueIndex = true, uniqueCombo = true) + private String infoHash; + @DatabaseField(canBeNull = false, uniqueCombo = true) + private String name; + @DatabaseField(canBeNull = false) + private Long size; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/Downloader.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/Downloader.java index 4e7db63d90..f867c9f403 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/Downloader.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/Downloader.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.downloader; import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; @@ -20,15 +21,15 @@ public interface Downloader extends AutoCloseable { String getEndpoint(); - String getWebUIEndpoint(); +// String getWebUIEndpoint(); - @Nullable - DownloaderBasicAuth getDownloaderBasicAuth(); - - @Nullable - WebViewScriptCallback getWebViewJavaScript(); - - boolean isSupportWebview(); +// @Nullable +// DownloaderBasicAuth getDownloaderBasicAuth(); +// +// @Nullable +// WebViewScriptCallback getWebViewJavaScript(); +// +// boolean isSupportWebview(); /** * 下载器用户定义名称 @@ -49,7 +50,7 @@ public interface Downloader extends AutoCloseable { * * @return 登陆是否成功 */ - boolean login(); + DownloaderLoginResult login(); /** * 获取此下载器的所有目前正在活动的 Torrent 列表 @@ -104,12 +105,12 @@ public interface Downloader extends AutoCloseable { * * @param lastStatus 最后请求状态 */ - void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage); + void setLastStatus(DownloaderLastStatus lastStatus, TranslationComponent statusMessage); /** * 获取客户端状态描述说明 * * @return 状态描述说明 */ - String getLastStatusMessage(); + TranslationComponent getLastStatusMessage(); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/DownloaderLoginResult.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/DownloaderLoginResult.java new file mode 100644 index 0000000000..8bd7e95b6b --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/DownloaderLoginResult.java @@ -0,0 +1,27 @@ +package com.ghostchu.peerbanhelper.downloader; + +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; + +@Data +@AllArgsConstructor +public class DownloaderLoginResult { + @Getter + private final Status status; + private final TranslationComponent message; + + + public boolean success() { + return status == Status.SUCCESS; + } + + public enum Status { + SUCCESS, + INCORRECT_CREDENTIAL, + MISSING_COMPONENTS, + NETWORK_ERROR, + EXCEPTION + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/BiglyBT.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/BiglyBT.java index 45be0211ce..776441c720 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/BiglyBT.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/BiglyBT.java @@ -1,10 +1,8 @@ package com.ghostchu.peerbanhelper.downloader.impl.biglybt; -import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.downloader.Downloader; -import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth; import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; -import com.ghostchu.peerbanhelper.downloader.WebViewScriptCallback; +import com.ghostchu.peerbanhelper.downloader.DownloaderLoginResult; import com.ghostchu.peerbanhelper.downloader.impl.biglybt.network.bean.clientbound.BanBean; import com.ghostchu.peerbanhelper.downloader.impl.biglybt.network.bean.clientbound.BanListReplacementBean; import com.ghostchu.peerbanhelper.downloader.impl.biglybt.network.wrapper.DownloadRecord; @@ -13,6 +11,7 @@ import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.peer.PeerImpl; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.torrent.TorrentImpl; import com.ghostchu.peerbanhelper.util.HTTPUtil; @@ -46,6 +45,8 @@ import java.util.Collection; import java.util.List; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + public class BiglyBT implements Downloader { private static final Logger log = org.slf4j.LoggerFactory.getLogger(BiglyBT.class); private final String apiEndpoint; @@ -53,7 +54,7 @@ public class BiglyBT implements Downloader { private final Config config; private DownloaderLastStatus lastStatus = DownloaderLastStatus.UNKNOWN; private String name; - private String statusMessage; + private TranslationComponent statusMessage; public BiglyBT(String name, Config config) { this.name = name; @@ -65,14 +66,13 @@ public BiglyBT(String name, Config config) { .newBuilder() .version(HttpClient.Version.valueOf(config.getHttpVersion())) .followRedirects(HttpClient.Redirect.ALWAYS) - .userAgent(Main.getUserAgent()) .defaultHeader("Authorization", "Bearer " + config.getToken()) .defaultHeader("Content-Type", "application/json") + .defaultHeader("Accept-Encoding", "gzip,deflate") .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) .headersTimeout(Duration.of(10, ChronoUnit.SECONDS)) .readTimeout(Duration.of(30, ChronoUnit.SECONDS)) .requestTimeout(Duration.of(30, ChronoUnit.SECONDS)) - .cookieHandler(cm); if (!config.isVerifySsl() && HTTPUtil.getIgnoreSslContext() != null) { builder.sslContext(HTTPUtil.getIgnoreSslContext()); @@ -100,8 +100,21 @@ public YamlConfiguration saveDownloader() { return config.saveToYaml(); } - public boolean login() { - return isLoggedIn(); + public DownloaderLoginResult login() { + HttpResponse resp; + try { + resp = httpClient.send(MutableRequest.GET(apiEndpoint + "/metadata"), HttpResponse.BodyHandlers.discarding()); + if (resp.statusCode() == 200) { + return new DownloaderLoginResult(DownloaderLoginResult.Status.SUCCESS, new TranslationComponent(Lang.STATUS_TEXT_OK)); + } + if (resp.statusCode() == 403) { + return new DownloaderLoginResult(DownloaderLoginResult.Status.INCORRECT_CREDENTIAL, new TranslationComponent(Lang.DOWNLOADER_LOGIN_INCORRECT_CRED)); + } + return new DownloaderLoginResult(DownloaderLoginResult.Status.EXCEPTION, new TranslationComponent(Lang.DOWNLOADER_LOGIN_EXCEPTION, "statusCode=" + resp.statusCode())); + } catch (Exception e) { + return new DownloaderLoginResult(DownloaderLoginResult.Status.NETWORK_ERROR, new TranslationComponent(Lang.DOWNLOADER_LOGIN_IO_EXCEPTION, e.getClass().getName() + ": " + e.getMessage())); + } + } @Override @@ -109,25 +122,25 @@ public String getEndpoint() { return apiEndpoint; } - @Override - public String getWebUIEndpoint() { - return config.getEndpoint(); - } - - @Override - public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { - return null; - } - - @Override - public @Nullable WebViewScriptCallback getWebViewJavaScript() { - return null; - } - - @Override - public boolean isSupportWebview() { - return false; - } +// @Override +// public String getWebUIEndpoint() { +// return config.getEndpoint(); +// } + +// @Override +// public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { +// return null; +// } +// +// @Override +// public @Nullable WebViewScriptCallback getWebViewJavaScript() { +// return null; +// } +// +// @Override +// public boolean isSupportWebview() { +// return false; +// } @Override public String getName() { @@ -139,16 +152,6 @@ public String getType() { return "BiglyBT"; } - public boolean isLoggedIn() { - HttpResponse resp; - try { - resp = httpClient.send(MutableRequest.GET(apiEndpoint + "/metadata"), HttpResponse.BodyHandlers.discarding()); - } catch (Exception e) { - return false; - } - return resp.statusCode() == 200; - } - @Override public void setBanList(@NotNull Collection fullList, @Nullable Collection added, @Nullable Collection removed) { if (removed != null && removed.isEmpty() && added != null && config.isIncrementBan()) { @@ -171,7 +174,7 @@ public List getTorrents() { throw new IllegalStateException(e); } if (request.statusCode() != 200) { - throw new IllegalStateException(String.format(Lang.DOWNLOADER_BIGLYBT_INCORRECT_RESPONSE, request.statusCode(), request.body())); + throw new IllegalStateException(tlUI(Lang.DOWNLOADER_BIGLYBT_INCORRECT_RESPONSE, request.statusCode(), request.body())); } List torrentDetail = JsonUtil.getGson().fromJson(request.body(), new TypeToken>() { }.getType()); @@ -209,7 +212,7 @@ public List getPeers(Torrent torrent) { throw new IllegalStateException(e); } if (resp.statusCode() != 200) { - throw new IllegalStateException(String.format(Lang.DOWNLOADER_BIGLYBT_FAILED_REQUEST_PEERS_LIST_IN_TORRENT, resp.statusCode(), resp.body())); + throw new IllegalStateException(tlUI(Lang.DOWNLOADER_BIGLYBT_FAILED_REQUEST_PEERS_LIST_IN_TORRENT, resp.statusCode(), resp.body())); } PeerManagerRecord peerManagerRecord = JsonUtil.getGson().fromJson(resp.body(), PeerManagerRecord.class); List peersList = new ArrayList<>(); @@ -223,7 +226,7 @@ public List getPeers(Torrent torrent) { peer.getStats().getRtUploadSpeed(), peer.getStats().getTotalSent(), peer.getPercentDoneInThousandNotation() / 1000d, - "unsupported" + null )); } return peersList; @@ -236,11 +239,11 @@ private void setBanListIncrement(Collection added) { .POST(apiEndpoint + "/bans", HttpRequest.BodyPublishers.ofString(JsonUtil.getGson().toJson(bean))) , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (request.statusCode() != 200) { - log.warn(Lang.DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body()); + log.error(tlUI(Lang.DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body())); throw new IllegalStateException("Save BiglyBT banlist error: statusCode=" + request.statusCode()); } } catch (Exception e) { - log.warn(Lang.DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage(), e); + log.error(tlUI(Lang.DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage()), e); throw new IllegalStateException(e); } } @@ -254,11 +257,11 @@ private void setBanListFull(Collection peerAddresses) { .build() , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (request.statusCode() != 200) { - log.warn(Lang.DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body()); + log.error(tlUI(Lang.DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body())); throw new IllegalStateException("Save BiglyBT banlist error: statusCode=" + request.statusCode()); } } catch (Exception e) { - log.warn(Lang.DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage(), e); + log.error(tlUI(Lang.DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage()), e); throw new IllegalStateException(e); } } @@ -269,13 +272,13 @@ public DownloaderLastStatus getLastStatus() { } @Override - public void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage) { + public void setLastStatus(DownloaderLastStatus lastStatus, TranslationComponent statusMessage) { this.lastStatus = lastStatus; this.statusMessage = statusMessage; } @Override - public String getLastStatusMessage() { + public TranslationComponent getLastStatusMessage() { return statusMessage; } @@ -302,8 +305,8 @@ public static Config readFromYaml(ConfigurationSection section) { if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况 config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1)); } - config.setToken(section.getString("token")); - config.setIncrementBan(section.getBoolean("increment-ban")); + config.setToken(section.getString("token", "")); + config.setIncrementBan(section.getBoolean("increment-ban", true)); config.setHttpVersion(section.getString("http-version", "HTTP_1_1")); config.setVerifySsl(section.getBoolean("verify-ssl", true)); return config; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/SimpleResponse.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/SimpleResponse.java index 5de3e39b5b..64cc1f4f2c 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/SimpleResponse.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/SimpleResponse.java @@ -7,6 +7,6 @@ @AllArgsConstructor @Data @NoArgsConstructor -public class SimpleResponse { +public final class SimpleResponse { private String msg; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanBean.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanBean.java index 65bff0f668..26e889b295 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanBean.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanBean.java @@ -9,6 +9,6 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class BanBean { +public final class BanBean { private List ips; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanListReplacementBean.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanListReplacementBean.java index ebe3e9e728..f1d79480b5 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanListReplacementBean.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/BanListReplacementBean.java @@ -9,7 +9,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class BanListReplacementBean { +public final class BanListReplacementBean { private List replaceWith; private boolean includeNonPBHEntries; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/NewBanListBean.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/NewBanListBean.java deleted file mode 100644 index e41d5300ee..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/NewBanListBean.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.ghostchu.peerbanhelper.downloader.impl.biglybt.network.bean.clientbound; - -public class NewBanListBean { -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/UnBanBean.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/UnBanBean.java index 9554258475..bdb0c32877 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/UnBanBean.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/clientbound/UnBanBean.java @@ -9,6 +9,6 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class UnBanBean { +public final class UnBanBean { private List ips; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/BatchOperationCallbackBean.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/BatchOperationCallbackBean.java index 632726920a..a3920620d3 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/BatchOperationCallbackBean.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/BatchOperationCallbackBean.java @@ -7,7 +7,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class BatchOperationCallbackBean { +public final class BatchOperationCallbackBean { private int success; private int failed; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/MetadataCallbackBean.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/MetadataCallbackBean.java index 6e8dba8fc7..03d61ca459 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/MetadataCallbackBean.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/bean/serverbound/MetadataCallbackBean.java @@ -7,7 +7,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class MetadataCallbackBean { +public final class MetadataCallbackBean { private String pluginVersion; private String applicationVersion; private String applicationName; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadRecord.java index 03403a9d7a..421b938d3f 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadRecord.java @@ -12,7 +12,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class DownloadRecord { +public final class DownloadRecord { private int state; private int subState; private long flags; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadStatsRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadStatsRecord.java index d67d5d6355..6fe34104af 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadStatsRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/DownloadStatsRecord.java @@ -7,7 +7,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class DownloadStatsRecord { +public final class DownloadStatsRecord { private String status; private int completedInThousandNotation; private int checkingDoneInThousandNotation; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerDescriptorRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerDescriptorRecord.java index 29b44433c5..f49713c8fa 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerDescriptorRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerDescriptorRecord.java @@ -7,7 +7,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class PeerDescriptorRecord { +public final class PeerDescriptorRecord { private String ip; private int tcpPort; private int udpPort; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerRecord.java index 0699946410..09b3615f3d 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerRecord.java @@ -9,7 +9,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class PeerManagerRecord { +public final class PeerManagerRecord { private List peers; private List pendingPeers; private PeerManagerStatsRecord peerStats; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerStatsRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerStatsRecord.java index eed40bb98e..36c1b788e6 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerStatsRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerManagerStatsRecord.java @@ -7,7 +7,7 @@ @AllArgsConstructor @Data @NoArgsConstructor -public class PeerManagerStatsRecord { +public final class PeerManagerStatsRecord { private int connectedSeeds; private int connectedLeechers; private long downloaded; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerRecord.java index 83970fb051..e3e47ec46f 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerRecord.java @@ -10,7 +10,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class PeerRecord { +public final class PeerRecord { private boolean myPeer; private int state; private String peerIdBase64; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerStatsRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerStatsRecord.java index fd2f569e27..bafd34d769 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerStatsRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/PeerStatsRecord.java @@ -10,7 +10,7 @@ @AllArgsConstructor @Data @NoArgsConstructor -public class PeerStatsRecord { +public final class PeerStatsRecord { private long rtDownloadSpeed; private long reception; private long rtUploadSpeed; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/SupportedMessageRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/SupportedMessageRecord.java index 7eded73675..eb37cd4690 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/SupportedMessageRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/SupportedMessageRecord.java @@ -7,7 +7,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class SupportedMessageRecord { +public final class SupportedMessageRecord { private String id; private int type; private String description; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/TorrentRecord.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/TorrentRecord.java index 9c1caf3731..23de64036d 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/TorrentRecord.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/biglybt/network/wrapper/TorrentRecord.java @@ -10,7 +10,7 @@ @Data @AllArgsConstructor @NoArgsConstructor -public class TorrentRecord { +public final class TorrentRecord { private String name; private String hashBase64; private long size; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java index 084acc3e47..8e83f87e46 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/Deluge.java @@ -1,13 +1,15 @@ package com.ghostchu.peerbanhelper.downloader.impl.deluge; import com.ghostchu.peerbanhelper.downloader.Downloader; -import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth; import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; -import com.ghostchu.peerbanhelper.downloader.WebViewScriptCallback; +import com.ghostchu.peerbanhelper.downloader.DownloaderLoginResult; import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.peer.PeerFlag; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.JsonUtil; +import com.ghostchu.peerbanhelper.util.StrUtil; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import com.ghostchu.peerbanhelper.wrapper.TorrentWrapper; @@ -26,8 +28,12 @@ import raccoonfink.deluge.responses.PBHActiveTorrentsResponse; import java.net.http.HttpClient; -import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; public class Deluge implements Downloader { private static final Logger log = org.slf4j.LoggerFactory.getLogger(Deluge.class); @@ -41,7 +47,7 @@ public class Deluge implements Downloader { private final DelugeServer client; private final Config config; private DownloaderLastStatus lastStatus = DownloaderLastStatus.UNKNOWN; - private String statusMessage; + private TranslationComponent statusMessage; public Deluge(String name, Config config) { this.name = name; @@ -59,13 +65,7 @@ public static Deluge loadFromConfig(String name, JsonObject section) { return new Deluge(name, config); } - private static String toStringHex(String s) { - byte[] baKeyword = new byte[s.length() / 2]; - for (int i = 0; i < baKeyword.length; i++) { - baKeyword[i] = (byte) (0xff & Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16)); - } - return new String(baKeyword, StandardCharsets.ISO_8859_1); - } + @Override public JsonObject saveDownloaderJson() { @@ -82,25 +82,25 @@ public String getEndpoint() { return config.getEndpoint(); } - @Override - public String getWebUIEndpoint() { - return config.getEndpoint(); - } - - @Override - public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { - return null; - } - - @Override - public @Nullable WebViewScriptCallback getWebViewJavaScript() { - return null; - } - - @Override - public boolean isSupportWebview() { - return true; - } +// @Override +// public String getWebUIEndpoint() { +// return config.getEndpoint(); +// } + +// @Override +// public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { +// return null; +// } +// +// @Override +// public @Nullable WebViewScriptCallback getWebViewJavaScript() { +// return null; +// } +// +// @Override +// public boolean isSupportWebview() { +// return true; +// } @Override public String getName() { @@ -113,20 +113,19 @@ public String getType() { } @Override - public boolean login() { + public DownloaderLoginResult login() { try { if (!this.client.login().isLoggedIn()) { - return false; + return new DownloaderLoginResult(DownloaderLoginResult.Status.INCORRECT_CREDENTIAL, new TranslationComponent(Lang.DOWNLOADER_LOGIN_INCORRECT_CRED)); } DelugeListMethodsResponse listMethodsResponse = this.client.listMethods(); if (!new HashSet<>(listMethodsResponse.getDelugeSupportedMethods()).containsAll(MUST_HAVE_METHODS)) { - log.warn(Lang.DOWNLOADER_DELUGE_PLUGIN_NOT_INSTALLED, getName()); - return false; + return new DownloaderLoginResult(DownloaderLoginResult.Status.MISSING_COMPONENTS, new TranslationComponent(Lang.DOWNLOADER_DELUGE_PLUGIN_NOT_INSTALLED)); } + return new DownloaderLoginResult(DownloaderLoginResult.Status.SUCCESS, new TranslationComponent(Lang.STATUS_TEXT_OK)); } catch (DelugeException e) { - throw new RuntimeException(e); + return new DownloaderLoginResult(DownloaderLoginResult.Status.EXCEPTION, new TranslationComponent(Lang.DOWNLOADER_LOGIN_IO_EXCEPTION, e.getClass().getName() + ": " + e.getMessage())); } - return true; } @Override @@ -138,14 +137,14 @@ public List getTorrents() { for (PBHActiveTorrentsResponse.ActiveTorrentsResponseDTO.PeersDTO peer : activeTorrent.getPeers()) { DelugePeer delugePeer = new DelugePeer( new PeerAddress(peer.getIp(), peer.getPort()), - toStringHex(peer.getPeerId()), + StrUtil.toStringHex(peer.getPeerId()), peer.getClientName(), peer.getTotalDownload(), peer.getPayloadDownSpeed(), peer.getTotalUpload(), peer.getPayloadUpSpeed(), peer.getProgress() / 100.0d, - parseFlag(peer.getFlags(), peer.getSource()) + parsePeerFlag(peer.getFlags(), peer.getSource()) ); peers.add(delugePeer); } @@ -162,7 +161,7 @@ public List getTorrents() { torrents.add(torrent); } } catch (DelugeException e) { - log.warn(Lang.DOWNLOADER_DELUGE_API_ERROR, e); + log.error(tlUI(Lang.DOWNLOADER_DELUGE_API_ERROR), e); } return torrents; } @@ -189,7 +188,7 @@ private void setBanListFull(Collection fullList) { try { this.client.replaceBannedPeers(fullList.stream().map(PeerAddress::getIp).toList()); } catch (DelugeException e) { - log.warn(Lang.DOWNLOADER_DELUGE_API_ERROR, e); + log.error(tlUI(Lang.DOWNLOADER_DELUGE_API_ERROR), e); } } @@ -197,7 +196,7 @@ private void setBanListIncrement(Collection added) { try { this.client.banPeers(added.stream().map(bm -> bm.getPeer().getAddress().getIp()).toList()); } catch (DelugeException e) { - log.warn(Lang.DOWNLOADER_DELUGE_API_ERROR, e); + log.error(tlUI(Lang.DOWNLOADER_DELUGE_API_ERROR), e); } } @@ -217,13 +216,13 @@ public DownloaderLastStatus getLastStatus() { } @Override - public void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage) { + public void setLastStatus(DownloaderLastStatus lastStatus, TranslationComponent statusMessage) { this.lastStatus = lastStatus; this.statusMessage = statusMessage; } @Override - public String getLastStatusMessage() { + public TranslationComponent getLastStatusMessage() { return statusMessage; } @@ -231,7 +230,8 @@ public String getLastStatusMessage() { public void close() { } - private String parseFlag(int peerFlag, int sourceFlag) { + + private PeerFlag parsePeerFlag(int peerFlag, int sourceFlag) { boolean interesting = (peerFlag & (1 << 0)) != 0; boolean choked = (peerFlag & (1 << 1)) != 0; boolean remoteInterested = (peerFlag & (1 << 2)) != 0; @@ -261,61 +261,96 @@ private String parseFlag(int peerFlag, int sourceFlag) { boolean resumeData = (sourceFlag & (1 << 4)) != 0; boolean incoming = (sourceFlag & (1 << 5)) != 0; - StringJoiner joiner = new StringJoiner(" "); - - if (interesting) { - if (remoteChoked) { - joiner.add("d"); - } else { - joiner.add("D"); - } - } - if (remoteInterested) { - if (choked) { - joiner.add("u"); - } else { - joiner.add("U"); - } - } - if (!remoteChoked && !interesting) - joiner.add("K"); - if (!choked && !remoteInterested) - joiner.add("?"); - if (optimisticUnchoke) - joiner.add("O"); - if (snubbed) - joiner.add("S"); - if (!localConnection) - joiner.add("I"); - if (dht) - joiner.add("H"); - if (pex) - joiner.add("X"); - if (lsd) - joiner.add("L"); - if (rc4Encrypted) - joiner.add("E"); - if (plainTextEncrypted) - joiner.add("e"); - if (utpSocket) - joiner.add("P"); - - return joiner.toString(); - } - - - private boolean c2b(char c) { - return c == '1'; + return new PeerFlag(interesting, choked, remoteInterested, remoteChoked, supportsExtensions, outgoingConnection, localConnection, handshake, + connecting, onParole, seed, optimisticUnchoke, snubbed, uploadOnly, endGameMode, holePunched, i2pSocket, utpSocket, sslSocket, + rc4Encrypted, plainTextEncrypted, tracker, dht, pex, lsd, resumeData, incoming); } - private String readBits(int i, int bitLength) { - StringBuilder builder = new StringBuilder(); - builder.append(Integer.toBinaryString(i)); - while (builder.length() < bitLength) { - builder.append("0"); - } - return builder.toString(); - } +// private String parseFlag(int peerFlag, int sourceFlag) { +// boolean interesting = (peerFlag & (1 << 0)) != 0; +// boolean choked = (peerFlag & (1 << 1)) != 0; +// boolean remoteInterested = (peerFlag & (1 << 2)) != 0; +// boolean remoteChoked = (peerFlag & (1 << 3)) != 0; +// boolean supportsExtensions = (peerFlag & (1 << 4)) != 0; +// boolean outgoingConnection = (peerFlag & (1 << 5)) != 0; +// boolean localConnection = (peerFlag & (1 << 6)) != 0; +// boolean handshake = (peerFlag & (1 << 7)) != 0; +// boolean connecting = (peerFlag & (1 << 8)) != 0; +// boolean onParole = (peerFlag & (1 << 9)) != 0; +// boolean seed = (peerFlag & (1 << 10)) != 0; +// boolean optimisticUnchoke = (peerFlag & (1 << 11)) != 0; +// boolean snubbed = (peerFlag & (1 << 12)) != 0; +// boolean uploadOnly = (peerFlag & (1 << 13)) != 0; +// boolean endGameMode = (peerFlag & (1 << 14)) != 0; +// boolean holePunched = (peerFlag & (1 << 15)) != 0; +// boolean i2pSocket = (peerFlag & (1 << 16)) != 0; +// boolean utpSocket = (peerFlag & (1 << 17)) != 0; +// boolean sslSocket = (peerFlag & (1 << 18)) != 0; +// boolean rc4Encrypted = (peerFlag & (1 << 19)) != 0; +// boolean plainTextEncrypted = (peerFlag & (1 << 20)) != 0; +// +// boolean tracker = (sourceFlag & (1 << 0)) != 0; +// boolean dht = (sourceFlag & (1 << 1)) != 0; +// boolean pex = (sourceFlag & (1 << 2)) != 0; +// boolean lsd = (sourceFlag & (1 << 3)) != 0; +// boolean resumeData = (sourceFlag & (1 << 4)) != 0; +// boolean incoming = (sourceFlag & (1 << 5)) != 0; +// +// StringJoiner joiner = new StringJoiner(" "); +// +// if (interesting) { +// if (remoteChoked) { +// joiner.add("d"); +// } else { +// joiner.add("D"); +// } +// } +// if (remoteInterested) { +// if (choked) { +// joiner.add("u"); +// } else { +// joiner.add("U"); +// } +// } +// if (!remoteChoked && !interesting) +// joiner.add("K"); +// if (!choked && !remoteInterested) +// joiner.add("?"); +// if (optimisticUnchoke) +// joiner.add("O"); +// if (snubbed) +// joiner.add("S"); +// if (!localConnection) +// joiner.add("I"); +// if (dht) +// joiner.add("H"); +// if (pex) +// joiner.add("X"); +// if (lsd) +// joiner.add("L"); +// if (rc4Encrypted) +// joiner.add("E"); +// if (plainTextEncrypted) +// joiner.add("e"); +// if (utpSocket) +// joiner.add("P"); +// +// return joiner.toString(); +// } +// +// +// private boolean c2b(char c) { +// return c == '1'; +// } +// +// private String readBits(int i, int bitLength) { +// StringBuilder builder = new StringBuilder(); +// builder.append(Integer.toBinaryString(i)); +// while (builder.length() < bitLength) { +// builder.append("0"); +// } +// return builder.toString(); +// } @NoArgsConstructor @Data @@ -332,15 +367,15 @@ public static class Config { public static Config readFromYaml(ConfigurationSection section) { Config config = new Config(); config.setType("deluge"); - config.setEndpoint(section.getString("endpoint")); + config.setEndpoint(section.getString("endpoint", "")); if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况 config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1)); } - config.setPassword(section.getString("password")); + config.setPassword(section.getString("password", "")); config.setRpcUrl(section.getString("rpc-url", "/json")); config.setHttpVersion(section.getString("http-version", "HTTP_1_1")); config.setVerifySsl(section.getBoolean("verify-ssl", true)); - config.setIncrementBan(section.getBoolean("increment-ban")); + config.setIncrementBan(section.getBoolean("increment-ban", true)); return config; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java index 40c9c3ab69..10499655b9 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugePeer.java @@ -1,13 +1,14 @@ package com.ghostchu.peerbanhelper.downloader.impl.deluge; import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.peer.PeerFlag; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor -public class DelugePeer implements Peer { +public final class DelugePeer implements Peer { private PeerAddress peerAddress; private String peerId; private String clientName; @@ -16,5 +17,5 @@ public class DelugePeer implements Peer { private long uploaded; private long uploadSpeed; private double progress; - private String flags; + private PeerFlag flags; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java index bc2a34729d..cbacc71d11 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/deluge/DelugeTorrent.java @@ -9,7 +9,7 @@ @Data @AllArgsConstructor -public class DelugeTorrent implements Torrent { +public final class DelugeTorrent implements Torrent { private String id; private String name; private String hash; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/Preferences.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/Preferences.java index 732f989229..fb51bd0236 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/Preferences.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/Preferences.java @@ -8,7 +8,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class Preferences { +public final class Preferences { @SerializedName("banned_IPs") private String bannedIps; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/SingleTorrentPeer.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBPeer.java similarity index 93% rename from src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/SingleTorrentPeer.java rename to src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBPeer.java index b212ff808a..7ca7b2b979 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/SingleTorrentPeer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBPeer.java @@ -2,14 +2,14 @@ import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.peer.PeerFlag; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import com.google.gson.annotations.SerializedName; import lombok.Setter; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @Setter -public class SingleTorrentPeer implements Peer { +public final class QBPeer implements Peer { @SerializedName("client") private String client; @SerializedName("connection") @@ -44,7 +44,7 @@ public class SingleTorrentPeer implements Peer { private Long uploaded; private transient PeerAddress peerAddress; - public SingleTorrentPeer() { + public QBPeer() { } @Override @@ -56,7 +56,7 @@ public PeerAddress getPeerAddress() { } @Override - @NotNull + @Nullable public String getPeerId() { return peerIdClient; } @@ -93,8 +93,8 @@ public double getProgress() { } @Override - public String getFlags() { - return flags; + public PeerFlag getFlags() { + return new PeerFlag(flags); } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/TorrentDetail.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBTorrent.java similarity index 99% rename from src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/TorrentDetail.java rename to src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBTorrent.java index 20afc2b5a2..ef492fded7 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/TorrentDetail.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBTorrent.java @@ -8,7 +8,7 @@ @AllArgsConstructor @NoArgsConstructor @Data -public class TorrentDetail { +public final class QBTorrent { @SerializedName("added_on") private Long addedOn; diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBittorrent.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBittorrent.java index 2d563527be..f334949a5d 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBittorrent.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/qbittorrent/QBittorrent.java @@ -1,18 +1,16 @@ package com.ghostchu.peerbanhelper.downloader.impl.qbittorrent; -import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.downloader.Downloader; -import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth; import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; -import com.ghostchu.peerbanhelper.downloader.WebViewScriptCallback; +import com.ghostchu.peerbanhelper.downloader.DownloaderLoginResult; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.torrent.TorrentImpl; import com.ghostchu.peerbanhelper.util.HTTPUtil; import com.ghostchu.peerbanhelper.util.IPAddressUtil; import com.ghostchu.peerbanhelper.util.JsonUtil; -import com.ghostchu.peerbanhelper.util.UrlEncoderDecoder; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import com.ghostchu.peerbanhelper.wrapper.TorrentWrapper; @@ -26,11 +24,11 @@ import inet.ipaddr.IPAddress; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; import java.net.*; import java.net.http.HttpClient; @@ -40,14 +38,16 @@ import java.time.temporal.ChronoUnit; import java.util.*; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + +@Slf4j public class QBittorrent implements Downloader { - private static final Logger log = org.slf4j.LoggerFactory.getLogger(QBittorrent.class); private final String apiEndpoint; private final HttpClient httpClient; private final Config config; private DownloaderLastStatus lastStatus = DownloaderLastStatus.UNKNOWN; private String name; - private String statusMessage; + private TranslationComponent statusMessage; public QBittorrent(String name, Config config) { this.name = name; @@ -58,8 +58,8 @@ public QBittorrent(String name, Config config) { Methanol.Builder builder = Methanol .newBuilder() .version(HttpClient.Version.valueOf(config.getHttpVersion())) + .defaultHeader("Accept-Encoding", "gzip,deflate") .followRedirects(HttpClient.Redirect.ALWAYS) - .userAgent(Main.getUserAgent()) .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) .headersTimeout(Duration.of(10, ChronoUnit.SECONDS)) .readTimeout(Duration.of(30, ChronoUnit.SECONDS)) @@ -97,8 +97,9 @@ public YamlConfiguration saveDownloader() { return config.saveToYaml(); } - public boolean login() { - if (isLoggedIn()) return true; // 重用 Session 会话 + public DownloaderLoginResult login() { + if (isLoggedIn()) + return new DownloaderLoginResult(DownloaderLoginResult.Status.SUCCESS, new TranslationComponent(Lang.STATUS_TEXT_OK)); // 重用 Session 会话 try { HttpResponse request = httpClient .send(MutableRequest.POST(apiEndpoint + "/auth/login", @@ -107,14 +108,16 @@ public boolean login() { .query("password", config.getPassword()).build()) .header("Content-Type", "application/x-www-form-urlencoded") , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); - - if (request.statusCode() != 200) { - log.warn(Lang.DOWNLOADER_QB_LOGIN_FAILED, name, request.statusCode(), "HTTP ERROR", request.body()); + if (request.statusCode() == 200) { + return new DownloaderLoginResult(DownloaderLoginResult.Status.SUCCESS, new TranslationComponent(Lang.STATUS_TEXT_OK)); } - return request.statusCode() == 200; + if (request.statusCode() == 403) { + return new DownloaderLoginResult(DownloaderLoginResult.Status.INCORRECT_CREDENTIAL, new TranslationComponent(Lang.DOWNLOADER_LOGIN_EXCEPTION, request.body())); + } + return new DownloaderLoginResult(DownloaderLoginResult.Status.EXCEPTION, new TranslationComponent(Lang.DOWNLOADER_LOGIN_INCORRECT_CRED)); + // return request.statusCode() == 200; } catch (Exception e) { - log.warn(Lang.DOWNLOADER_QB_LOGIN_FAILED, name, "N/A", e.getClass().getName(), e.getMessage()); - return false; + return new DownloaderLoginResult(DownloaderLoginResult.Status.EXCEPTION, new TranslationComponent(Lang.DOWNLOADER_LOGIN_IO_EXCEPTION, e.getClass().getName() + ": " + e.getMessage())); } } @@ -123,47 +126,47 @@ public String getEndpoint() { return apiEndpoint; } - @Override - public String getWebUIEndpoint() { - return config.getEndpoint(); - } - - @Override - public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { - if (config.getBasicAuth() != null) { - return new DownloaderBasicAuth(config.getEndpoint(), config.getBasicAuth().getUser(), config.getBasicAuth().getPass()); - } - return null; - } - - @Override - public @Nullable WebViewScriptCallback getWebViewJavaScript() { - return (url, content) -> { - if (content.contains("loginform")) { - return String.format(""" - const xhr = new XMLHttpRequest(); - xhr.open('POST', 'api/v2/auth/login', true); - xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); - xhr.addEventListener('readystatechange', function() { - if (xhr.readyState === 4) { // DONE state - if ((xhr.status === 200) && (xhr.responseText === "Ok.")) - location.reload(true); - } - } - ); - const queryString = "username=%s&password=%s"; - xhr.send(queryString); - """, UrlEncoderDecoder.encodePath(config.getUsername()), UrlEncoderDecoder.encodePath(config.getPassword())); - } else { - return null; - } - }; - } - - @Override - public boolean isSupportWebview() { - return true; - } +// @Override +// public String getWebUIEndpoint() { +// return config.getEndpoint(); +// } +// +// @Override +// public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { +// if (config.getBasicAuth() != null) { +// return new DownloaderBasicAuth(config.getEndpoint(), config.getBasicAuth().getUser(), config.getBasicAuth().getPass()); +// } +// return null; +// } +// +// @Override +// public @Nullable WebViewScriptCallback getWebViewJavaScript() { +// return (url, content) -> { +// if (content.contains("loginform")) { +// return String.format(""" +// const xhr = new XMLHttpRequest(); +// xhr.open('POST', 'api/v2/auth/login', true); +// xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); +// xhr.addEventListener('readystatechange', function() { +// if (xhr.readyState === 4) { // DONE state +// if ((xhr.status === 200) && (xhr.responseText === "Ok.")) +// location.reload(true); +// } +// } +// ); +// const queryString = "username=%s&password=%s"; +// xhr.send(queryString); +// """, UrlEncoderDecoder.encodePath(config.getUsername()), UrlEncoderDecoder.encodePath(config.getPassword())); +// } else { +// return null; +// } +// }; +// } +// +// @Override +// public boolean isSupportWebview() { +// return true; +// } @Override public String getName() { @@ -203,12 +206,12 @@ public List getTorrents() { throw new IllegalStateException(e); } if (request.statusCode() != 200) { - throw new IllegalStateException(String.format(Lang.DOWNLOADER_QB_FAILED_REQUEST_TORRENT_LIST, request.statusCode(), request.body())); + throw new IllegalStateException(tlUI(Lang.DOWNLOADER_QB_FAILED_REQUEST_TORRENT_LIST, request.statusCode(), request.body())); } - List torrentDetail = JsonUtil.getGson().fromJson(request.body(), new TypeToken>() { + List qbTorrent = JsonUtil.getGson().fromJson(request.body(), new TypeToken>() { }.getType()); List torrents = new ArrayList<>(); - for (TorrentDetail detail : torrentDetail) { + for (QBTorrent detail : qbTorrent) { torrents.add(new TorrentImpl(detail.getHash(), detail.getName(), detail.getHash(), detail.getTotalSize(), detail.getProgress(), detail.getUpspeed(), detail.getDlspeed())); } return torrents; @@ -234,7 +237,7 @@ public List getPeers(Torrent torrent) { throw new IllegalStateException(e); } if (resp.statusCode() != 200) { - throw new IllegalStateException(String.format(Lang.DOWNLOADER_QB_FAILED_REQUEST_PEERS_LIST_IN_TORRENT, resp.statusCode(), resp.body())); + throw new IllegalStateException(tlUI(Lang.DOWNLOADER_QB_FAILED_REQUEST_PEERS_LIST_IN_TORRENT, resp.statusCode(), resp.body())); } JsonObject object = JsonParser.parseString(resp.body()).getAsJsonObject(); @@ -242,97 +245,72 @@ public List getPeers(Torrent torrent) { List peersList = new ArrayList<>(); for (String s : peers.keySet()) { JsonObject singlePeerObject = peers.getAsJsonObject(s); - SingleTorrentPeer singleTorrentPeer = JsonUtil.getGson().fromJson(singlePeerObject.toString(), SingleTorrentPeer.class); - peersList.add(singleTorrentPeer); - } - return peersList; - } - - @NoArgsConstructor - @Data - public static class Config { - - private String type; - private String endpoint; - private String username; - private String password; - private BasicauthDTO basicAuth; - private String httpVersion; - private boolean incrementBan; - private boolean verifySsl; - - public static Config readFromYaml(ConfigurationSection section) { - Config config = new Config(); - config.setType("qbittorrent"); - config.setEndpoint(section.getString("endpoint")); - if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况 - config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1)); + QBPeer qbPeer = JsonUtil.getGson().fromJson(singlePeerObject.toString(), QBPeer.class); + // 一个 QB 本地化问题的 Workaround + if (qbPeer.getPeerId() == null || qbPeer.getPeerId().equals("Unknown") || qbPeer.getPeerId().equals("未知")) { + qbPeer.setPeerIdClient(""); } - config.setUsername(section.getString("username")); - config.setPassword(section.getString("password")); - Config.BasicauthDTO basicauthDTO = new BasicauthDTO(); - basicauthDTO.setUser(section.getString("basic-auth.user")); - basicauthDTO.setPass(section.getString("basic-auth.pass")); - config.setBasicAuth(basicauthDTO); - config.setHttpVersion(section.getString("http-version", "HTTP_1_1")); - config.setIncrementBan(section.getBoolean("increment-ban")); - config.setVerifySsl(section.getBoolean("verify-ssl", true)); - return config; - } - - public YamlConfiguration saveToYaml() { - YamlConfiguration section = new YamlConfiguration(); - section.set("type", "qbittorrent"); - section.set("endpoint", endpoint); - section.set("username", username); - section.set("password", password); - section.set("basic-auth.user", Objects.requireNonNullElse(basicAuth.user, "")); - section.set("basic-auth.pass", Objects.requireNonNullElse(basicAuth.pass, "")); - section.set("http-version", httpVersion); - section.set("increment-ban", incrementBan); - section.set("verify-ssl", verifySsl); - return section; - } - - @NoArgsConstructor - @Data - public static class BasicauthDTO { - @SerializedName("user") - private String user; - @SerializedName("pass") - private String pass; + if (qbPeer.getClientName() != null) { + if (qbPeer.getClientName().startsWith("Unknown [") && qbPeer.getClientName().endsWith("]")) { + String mid = qbPeer.getClientName().substring("Unknown [".length(), qbPeer.getClientName().length() - 1); + qbPeer.setClient(mid); + } + } + peersList.add(qbPeer); } + return peersList; } private void setBanListIncrement(Collection added) { - Map banTasks = new HashMap<>(); - added.forEach(p -> { - StringJoiner joiner = banTasks.getOrDefault(p.getTorrent().getHash(), new StringJoiner("|")); - IPAddress ipAddress = IPAddressUtil.getIPAddress(p.getPeer().getAddress().getIp()); - if (ipAddress.isIPv6()) { - joiner.add("[" + p.getPeer().getAddress().getIp() + "]" + ":" + p.getPeer().getAddress().getPort()); + added.forEach(meta -> { + IPAddress ipAddress = IPAddressUtil.getIPAddress(meta.getPeer().getAddress().getIp()); + String peers; + if (ipAddress.isIPv4()) { + peers = meta.getPeer().getAddress().getIp() + ":" + meta.getPeer().getAddress().getPort(); } else { - joiner.add(p.getPeer().getAddress().getIp() + ":" + p.getPeer().getAddress().getPort()); + peers = "[" + meta.getPeer().getAddress().getIp() + "]" + ":" + meta.getPeer().getAddress().getPort(); } - banTasks.put(p.getTorrent().getHash(), joiner); - }); - banTasks.forEach((hash, peers) -> { try { HttpResponse request = httpClient.send(MutableRequest .POST(apiEndpoint + "/transfer/banPeers", FormBodyPublisher.newBuilder() - .query("hash", hash) - .query("peers", peers.toString()).build()) + .query("hash", meta.getTorrent().getHash()) + .query("peers", peers).build()) .header("Content-Type", "application/x-www-form-urlencoded") , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (request.statusCode() != 200) { - log.warn(Lang.DOWNLOADER_QB_INCREAMENT_BAN_FAILED, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body()); + log.error(tlUI(Lang.DOWNLOADER_QB_INCREAMENT_BAN_FAILED, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body())); throw new IllegalStateException("Save qBittorrent banlist error: statusCode=" + request.statusCode()); } } catch (Exception e) { - log.warn(Lang.DOWNLOADER_QB_INCREAMENT_BAN_FAILED, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage(), e); + log.error(tlUI(Lang.DOWNLOADER_QB_INCREAMENT_BAN_FAILED, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage()), e); throw new IllegalStateException(e); } }); +// +// Map banTasks = new HashMap<>(); +// added.forEach(p -> { +// StringJoiner joiner = banTasks.getOrDefault(p.getTorrent().getHash(), new StringJoiner("|")); +// IPAddress ipAddress = IPAddressUtil.getIPAddress(p.getPeer().getAddress().getIp()); +// +// banTasks.put(p.getTorrent().getHash(), joiner); +// }); +// banTasks.forEach((hash, peers) -> { +// try { +// HttpResponse request = httpClient.send(MutableRequest +// .POST(apiEndpoint + "/transfer/banPeers", FormBodyPublisher.newBuilder() +// .query("hash", hash) +// .query("peers", peers.toString()).build()) +// .header("Content-Type", "application/x-www-form-urlencoded") +// , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); +// if (request.statusCode() != 200) { +// log.error(tlUI(Lang.DOWNLOADER_QB_INCREAMENT_BAN_FAILED, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body())); +// throw new IllegalStateException("Save qBittorrent banlist error: statusCode=" + request.statusCode()); +// } +// } catch (Exception e) { +// log.error(tlUI(Lang.DOWNLOADER_QB_INCREAMENT_BAN_FAILED, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage()), e); +// throw new IllegalStateException(e); +// } +// }); } private void setBanListFull(Collection peerAddresses) { @@ -345,11 +323,11 @@ private void setBanListFull(Collection peerAddresses) { .header("Content-Type", "application/x-www-form-urlencoded") , HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); if (request.statusCode() != 200) { - log.warn(Lang.DOWNLOADER_QB_FAILED_SAVE_BANLIST, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body()); + log.error(tlUI(Lang.DOWNLOADER_QB_FAILED_SAVE_BANLIST, name, apiEndpoint, request.statusCode(), "HTTP ERROR", request.body())); throw new IllegalStateException("Save qBittorrent banlist error: statusCode=" + request.statusCode()); } } catch (Exception e) { - log.warn(Lang.DOWNLOADER_QB_FAILED_SAVE_BANLIST, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage(), e); + log.error(tlUI(Lang.DOWNLOADER_QB_FAILED_SAVE_BANLIST, name, apiEndpoint, "N/A", e.getClass().getName(), e.getMessage()), e); throw new IllegalStateException(e); } } @@ -360,13 +338,13 @@ public DownloaderLastStatus getLastStatus() { } @Override - public void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage) { + public void setLastStatus(DownloaderLastStatus lastStatus, TranslationComponent statusMessage) { this.lastStatus = lastStatus; this.statusMessage = statusMessage; } @Override - public String getLastStatusMessage() { + public TranslationComponent getLastStatusMessage() { return statusMessage; } @@ -374,4 +352,60 @@ public String getLastStatusMessage() { public void close() { } + + @NoArgsConstructor + @Data + public static class Config { + + private String type; + private String endpoint; + private String username; + private String password; + private BasicauthDTO basicAuth; + private String httpVersion; + private boolean incrementBan; + private boolean verifySsl; + + public static Config readFromYaml(ConfigurationSection section) { + Config config = new Config(); + config.setType("qbittorrent"); + config.setEndpoint(section.getString("endpoint")); + if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况 + config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1)); + } + config.setUsername(section.getString("username", "")); + config.setPassword(section.getString("password", "")); + Config.BasicauthDTO basicauthDTO = new BasicauthDTO(); + basicauthDTO.setUser(section.getString("basic-auth.user")); + basicauthDTO.setPass(section.getString("basic-auth.pass")); + config.setBasicAuth(basicauthDTO); + config.setHttpVersion(section.getString("http-version", "HTTP_1_1")); + config.setIncrementBan(section.getBoolean("increment-ban", false)); + config.setVerifySsl(section.getBoolean("verify-ssl", true)); + return config; + } + + public YamlConfiguration saveToYaml() { + YamlConfiguration section = new YamlConfiguration(); + section.set("type", "qbittorrent"); + section.set("endpoint", endpoint); + section.set("username", username); + section.set("password", password); + section.set("basic-auth.user", Objects.requireNonNullElse(basicAuth.user, "")); + section.set("basic-auth.pass", Objects.requireNonNullElse(basicAuth.pass, "")); + section.set("http-version", httpVersion); + section.set("increment-ban", incrementBan); + section.set("verify-ssl", verifySsl); + return section; + } + + @NoArgsConstructor + @Data + public static class BasicauthDTO { + @SerializedName("user") + private String user; + @SerializedName("pass") + private String pass; + } + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/RTorrent.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/RTorrent.java new file mode 100644 index 0000000000..c213f8ab45 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/RTorrent.java @@ -0,0 +1,285 @@ +//package com.ghostchu.peerbanhelper.downloader.impl.rtorrent; +// +//import com.ghostchu.peerbanhelper.downloader.Downloader; +//import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; +//import com.ghostchu.peerbanhelper.peer.Peer; +//import com.ghostchu.peerbanhelper.torrent.Torrent; +//import com.ghostchu.peerbanhelper.util.HTTPUtil; +//import com.ghostchu.peerbanhelper.util.JsonUtil; +//import com.ghostchu.peerbanhelper.wrapper.BanMetadata; +//import com.ghostchu.peerbanhelper.wrapper.PeerAddress; +//import com.ghostchu.peerbanhelper.wrapper.TorrentWrapper; +//import com.github.mizosoft.methanol.Methanol; +//import com.github.mizosoft.methanol.MutableRequest; +//import com.google.gson.JsonObject; +//import com.google.gson.annotations.SerializedName; +//import de.timroes.axmlrpc.Call; +//import de.timroes.axmlrpc.ResponseParser; +//import de.timroes.axmlrpc.serializer.SerializerHandler; +//import lombok.Data; +//import lombok.NoArgsConstructor; +//import lombok.extern.slf4j.Slf4j; +//import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; +//import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +//import org.jetbrains.annotations.NotNull; +//import org.jetbrains.annotations.Nullable; +// +//import java.net.*; +//import java.net.http.HttpClient; +//import java.net.http.HttpRequest; +//import java.net.http.HttpResponse; +//import java.nio.charset.StandardCharsets; +//import java.time.Duration; +//import java.time.temporal.ChronoUnit; +//import java.util.Collection; +//import java.util.Collections; +//import java.util.List; +//import java.util.Objects; +// +//@Slf4j +//public class RTorrent implements Downloader { +// private final String apiEndpoint; +// +// private final Config config; +// private final Connector connector; +// private final SerializerHandler serializerHandler; +// private DownloaderLastStatus lastStatus = DownloaderLastStatus.UNKNOWN; +// private String name; +// private String statusMessage; +// +// public RTorrent(String name, Config config) { +// this.serializerHandler = new SerializerHandler(); +// this.name = name; +// this.config = config; +// if (config.getEndpoint().startsWith("http")) { +// this.apiEndpoint = config.getEndpoint(); +// this.connector = new XMLRPCOverHTTPConnector(serializerHandler, config.endpoint, HttpClient.Version.HTTP_1_1, "", "", false); +// //this.connector = new XMLRPCOverHTTPConnector(serializerHandler, this.apiEndpoint, HttpClient.Version.valueOf(config.getHttpVersion()), config.getBasicAuth().getUser(), config.getBasicAuth().getPass(), config.isVerifySsl()); +// } else { +// this.apiEndpoint = config.getEndpoint(); +// +// throw new RuntimeException("Not implemented yet"); +//// try { +//// this.connector = new XMLRPCUnixSocketSCGIConnector(serializerHandler, Path.of(this.apiEndpoint)); +//// } catch (IOException e) { +//// throw new RuntimeException(e); +//// } +// } +// } +// +// public static RTorrent loadFromConfig(String name, JsonObject section) { +// Config config = JsonUtil.getGson().fromJson(section.toString(), Config.class); +// return new RTorrent(name, config); +// } +// +// public static RTorrent loadFromConfig(String name, ConfigurationSection section) { +// Config config = Config.readFromYaml(section); +// return new RTorrent(name, config); +// } +// +// @Override +// public JsonObject saveDownloaderJson() { +// return JsonUtil.getGson().toJsonTree(config).getAsJsonObject(); +// } +// +// @Override +// public YamlConfiguration saveDownloader() { +// return config.saveToYaml(); +// } +// +// public boolean login() { +// if (isLoggedIn()) return true; // 重用 Session 会话 +// try { +// var obj = this.connector.sendPayload(new Call(serializerHandler, "system.listMethods"), true); +// return true; +// } catch (Exception e) { +// throw new RuntimeException(e); +// } +// } +// +// @Override +// public String getEndpoint() { +// return apiEndpoint; +// } +// +// +// @Override +// public String getName() { +// return name; +// } +// +// @Override +// public String getType() { +// return "rTorrent"; +// } +// +// public boolean isLoggedIn() { +// return false; +// } +// +// @Override +// public void setBanList(@NotNull Collection fullList, @Nullable Collection added, @Nullable Collection removed) { +// if (removed != null && removed.isEmpty() && added != null && config.isIncrementBan()) { +// setBanListIncrement(added); +// } else { +// setBanListFull(fullList); +// } +// } +// +// @Override +// public List getTorrents() { +// return Collections.emptyList(); +// } +// +// @Override +// public void relaunchTorrentIfNeeded(Collection torrents) { +// // QB 很棒,什么都不需要做 +// } +// +// @Override +// public void relaunchTorrentIfNeededByTorrentWrapper(Collection torrents) { +// // QB 很棒,什么都不需要做 +// } +// +// @Override +// public List getPeers(Torrent torrent) { +// return Collections.emptyList(); +// } +// +// private void setBanListIncrement(Collection added) { +// +// } +// +// private void setBanListFull(Collection peerAddresses) { +// +// } +// +// @Override +// public DownloaderLastStatus getLastStatus() { +// return lastStatus; +// } +// +// @Override +// public void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage) { +// this.lastStatus = lastStatus; +// this.statusMessage = statusMessage; +// } +// +// @Override +// public String getLastStatusMessage() { +// return statusMessage; +// } +// +// @Override +// public void close() { +// +// } +// +// public interface Connector { +// Object sendPayload(Call payload, boolean debug) throws Exception; +// } +// +// @NoArgsConstructor +// @Data +// public static class Config { +// +// private String type; +// private String endpoint; +// private String username; +// private String password; +// private BasicauthDTO basicAuth; +// private String httpVersion; +// private boolean incrementBan; +// private boolean verifySsl; +// +// public static Config readFromYaml(ConfigurationSection section) { +// Config config = new Config(); +// config.setType("rtorrent"); +// config.setEndpoint(section.getString("endpoint")); +// if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况 +// config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1)); +// } +// config.setUsername(section.getString("username")); +// config.setPassword(section.getString("password")); +// BasicauthDTO basicauthDTO = new BasicauthDTO(); +// basicauthDTO.setUser(section.getString("basic-auth.user")); +// basicauthDTO.setPass(section.getString("basic-auth.pass")); +// config.setBasicAuth(basicauthDTO); +// config.setHttpVersion(section.getString("http-version", "HTTP_1_1")); +// config.setIncrementBan(section.getBoolean("increment-ban")); +// config.setVerifySsl(section.getBoolean("verify-ssl", true)); +// return config; +// } +// +// public YamlConfiguration saveToYaml() { +// YamlConfiguration section = new YamlConfiguration(); +// section.set("type", "rtorrent"); +// section.set("endpoint", endpoint); +// section.set("username", username); +// section.set("password", password); +// section.set("basic-auth.user", Objects.requireNonNullElse(basicAuth.user, "")); +// section.set("basic-auth.pass", Objects.requireNonNullElse(basicAuth.pass, "")); +// section.set("http-version", httpVersion); +// section.set("increment-ban", incrementBan); +// section.set("verify-ssl", verifySsl); +// return section; +// } +// +// @NoArgsConstructor +// @Data +// public static class BasicauthDTO { +// @SerializedName("user") +// private String user; +// @SerializedName("pass") +// private String pass; +// } +// } +// +// public static class XMLRPCOverHTTPConnector implements Connector { +// private final HttpClient httpClient; +// private final SerializerHandler serializerHandler; +// private final String endpoint; +// +// public XMLRPCOverHTTPConnector(SerializerHandler serializerHandler, String endpoint, HttpClient.Version httpVersion, String baUser, String baPass, boolean verifySSL) { +// this.serializerHandler = serializerHandler; +// this.endpoint = endpoint; +// CookieManager cm = new CookieManager(); +// cm.setCookiePolicy(CookiePolicy.ACCEPT_ALL); +// Methanol.Builder builder = Methanol +// .newBuilder() +// .version(httpVersion) +// .defaultHeader("Accept-Encoding", "gzip,deflate") +// .defaultHeader("Content-Type", "text/xml") +// .followRedirects(HttpClient.Redirect.ALWAYS) +// .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) +// .headersTimeout(Duration.of(10, ChronoUnit.SECONDS)) +// .readTimeout(Duration.of(30, ChronoUnit.SECONDS)) +// .requestTimeout(Duration.of(30, ChronoUnit.SECONDS)) +// .authenticator(new Authenticator() { +// @Override +// public PasswordAuthentication requestPasswordAuthenticationInstance(String host, InetAddress addr, int port, String protocol, String prompt, String scheme, URL url, RequestorType reqType) { +// return new PasswordAuthentication(baUser, baPass.toCharArray()); +// } +// }) +// .cookieHandler(cm); +// if (!verifySSL && HTTPUtil.getIgnoreSslContext() != null) { +// builder.sslContext(HTTPUtil.getIgnoreSslContext()); +// } +// this.httpClient = builder.build(); +// +// } +// +// @Override +// public Object sendPayload(Call payload, boolean debug) throws Exception { +// String xml = payload.getXML(debug); +// var body = this.httpClient.send( +// MutableRequest.POST(endpoint, HttpRequest.BodyPublishers.ofString(xml, StandardCharsets.UTF_8)), +// HttpResponse.BodyHandlers.ofInputStream()); +// if (body.statusCode() != 200) { +// log.warn("XML-RPC over HTTP returns un-excepted statusCode: {}. The function may work properly", body.statusCode()); +// } +// System.out.println(body.body()); +// return new ResponseParser().parse(serializerHandler, body.body(), debug); +// } +// } +//} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/bean/RPeer.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/bean/RPeer.java new file mode 100644 index 0000000000..2d4b0f9b77 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/bean/RPeer.java @@ -0,0 +1,113 @@ +//package com.ghostchu.peerbanhelper.downloader.impl.rtorrent.bean; +// +//import com.ghostchu.peerbanhelper.peer.PeerFlag; +//import com.ghostchu.peerbanhelper.peer.Peer; +//import com.ghostchu.peerbanhelper.util.StrUtil; +//import com.ghostchu.peerbanhelper.wrapper.PeerAddress; +//import lombok.AllArgsConstructor; +//import lombok.Data; +//import org.jetbrains.annotations.Nullable; +// +//@AllArgsConstructor +//@Data +//public final class RPeer implements Peer { +// /* +// "2D7142343630302D217078332E4C5828364E454E", +// "46.185.189.249", +// "qBittorrent 4.6.0.0", +// "0", +// "1", +// "0", +// "100", +// "192079", +// "0", +// "6402", +// "0", +// "-qB4600-%21px3%2ELX%286NEN", +// "0", +// "0", +// "5694" +// */ +// private final String id; +// private final String address; +// private final String clientVersion; +// private final boolean incoming; +// private final boolean encrypted; +// private final boolean snubbed; +// private final long completedPercent; +// private final long downTotal; +// private final long upTotal; +// private final long downRate; +// private final long upRate; +// private final String idHtml; +// private final long peerRate; +// private final long peerTotal; +// private final int port; +// private transient PeerAddress peerAddress; +// +// public RPeer(String[] args) { +// this.id = args[0]; +// this.address = args[1]; +// this.clientVersion = args[2]; +// this.incoming = Boolean.parseBoolean(args[3]); +// this.encrypted = Boolean.parseBoolean(args[4]); +// this.snubbed = Boolean.parseBoolean(args[5]); +// this.completedPercent = Long.parseLong(args[6]); +// this.downTotal = Long.parseLong(args[7]); +// this.upTotal = Long.parseLong(args[8]); +// this.downRate = Long.parseLong(args[9]); +// this.upRate = Long.parseLong(args[10]); +// this.idHtml = args[11]; +// this.peerRate = Long.parseLong(args[12]); +// this.peerTotal = Long.parseLong(args[13]); +// this.port = Integer.parseInt(args[14]); +// } +// +// @Override +// public PeerAddress getPeerAddress() { +// if (this.peerAddress == null) { +// this.peerAddress = new PeerAddress(address, port); +// } +// return this.peerAddress; +// } +// +// @Override +// public String getPeerId() { +// return StrUtil.toStringHex(id); +// } +// +// @Override +// public String getClientName() { +// return clientVersion; +// } +// +// @Override +// public long getDownloadSpeed() { +// return downRate; +// } +// +// @Override +// public long getDownloaded() { +// return downTotal; +// } +// +// @Override +// public long getUploadSpeed() { +// return upRate; +// } +// +// @Override +// public long getUploaded() { +// return upTotal; +// } +// +// @Override +// public double getProgress() { +// return completedPercent / 100.0d; +// } +// +// @Override +// public @Nullable PeerFlag getFlags() { +// return null; +// } +//} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/bean/RTorrent.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/bean/RTorrent.java new file mode 100644 index 0000000000..890f4eed7b --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/bean/RTorrent.java @@ -0,0 +1,113 @@ +//package com.ghostchu.peerbanhelper.downloader.impl.rtorrent.bean; +// +//import com.ghostchu.peerbanhelper.torrent.Torrent; +//import lombok.AllArgsConstructor; +//import lombok.Data; +// +//@Data +//@AllArgsConstructor +//public final class RTorrent implements Torrent { +// private final String hash; +// private final boolean open; +// private final boolean hashChecking; +// private final boolean hashChecked; +// private final int state; +// private final String name; +// private final long sizeBytes; +// private final long completedChunks; +// private final long sizeChunks; +// private final long bytesDone; +// private final long upTotal; +// private final double ratio; +// private final long upRate; +// private final long downRate; +// private final long chunkSize; +// private final String custom1; +// private final int peerAccounted; +// private final int peersNotConnected; +// private final int peersConnected; +// private final int peersComplete; +// private final long leftBytes; +// private final int priority; +// private final long stateChanged; +// private final long skipTotal; +// private final boolean hashing; +// private final long chunkHashed; +// private final String basePath; +// private final long creationDate; +// private final int trackerSize; +// private final boolean active; +// private final String message; +// private final String custom2; +// private final long freeDiskSpace; +// private final boolean privateSeed; +// private final boolean multiFile; +// +// public RTorrent(String[] resp) { +// this.hash = resp[0]; +// this.open = Boolean.parseBoolean(resp[1]); +// this.hashChecking = Boolean.parseBoolean(resp[2]); +// this.hashChecked = Boolean.parseBoolean(resp[3]); +// this.state = Integer.parseInt(resp[4]); +// this.name = resp[5]; +// this.sizeBytes = Long.parseLong(resp[6]); +// this.completedChunks = Long.parseLong(resp[7]); +// this.sizeChunks = Long.parseLong(resp[8]); +// this.bytesDone = Long.parseLong(resp[9]); +// this.upTotal = Long.parseLong(resp[10]); +// this.ratio = Double.parseDouble(resp[11]); +// this.upRate = Long.parseLong(resp[12]); +// this.downRate = Long.parseLong(resp[13]); +// this.chunkSize = Long.parseLong(resp[14]); +// this.custom1 = resp[15]; +// this.peerAccounted = Integer.parseInt(resp[16]); +// this.peersNotConnected = Integer.parseInt(resp[17]); +// this.peersConnected = Integer.parseInt(resp[18]); +// this.peersComplete = Integer.parseInt(resp[19]); +// this.leftBytes = Long.parseLong(resp[20]); +// this.priority = Integer.parseInt(resp[21]); +// this.stateChanged = Long.parseLong(resp[22]); +// this.skipTotal = Long.parseLong(resp[23]); +// this.hashing = Boolean.parseBoolean(resp[24]); +// this.chunkHashed = Long.parseLong(resp[25]); +// this.basePath = resp[26]; +// this.creationDate = Long.parseLong(resp[27]); +// this.trackerSize = Integer.parseInt(resp[28]); +// this.active = Boolean.parseBoolean(resp[29]); +// this.message = resp[30]; +// this.custom2 = resp[31]; +// this.freeDiskSpace = Long.parseLong(resp[32]); +// this.privateSeed = Boolean.parseBoolean(resp[33]); +// this.multiFile = Boolean.parseBoolean(resp[34]); +// } +// +// @Override +// public String getId() { +// return hash; +// } +// +// @Override +// public double getProgress() { +// return (double) (sizeBytes - leftBytes) / sizeBytes; +// } +// +// @Override +// public long getSize() { +// return sizeBytes; +// } +// +// @Override +// public long getRtUploadSpeed() { +// return upRate; +// } +// +// @Override +// public long getRtDownloadSpeed() { +// return downRate; +// } +// +// @Override +// public String getHashedIdentifier() { +// return Torrent.super.getHashedIdentifier(); +// } +//} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/resp/ListResponse.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/resp/ListResponse.java new file mode 100644 index 0000000000..fa714d7a01 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/resp/ListResponse.java @@ -0,0 +1,25 @@ +//package com.ghostchu.peerbanhelper.downloader.impl.rtorrent.resp; +// +//import com.ghostchu.peerbanhelper.downloader.impl.rtorrent.bean.RTorrent; +//import com.google.gson.JsonElement; +//import com.google.gson.JsonObject; +//import lombok.Data; +// +//import java.util.ArrayList; +//import java.util.List; +// +//@Data +//public class ListResponse { +// private final List rpcTorrents = new ArrayList<>(); +// +// public ListResponse(JsonObject jsonObject) { +// JsonObject torrents = jsonObject.getAsJsonObject("t"); +// for (String hash : torrents.keySet()) { +// String[] args = torrents.getAsJsonArray(hash).asList().stream().map(JsonElement::getAsString).toArray(String[]::new); +// String[] mappedArgs = new String[args.length + 1]; +// mappedArgs[0] = hash; +// System.arraycopy(args, 0, mappedArgs, 1, args.length); +// rpcTorrents.add(new RTorrent(mappedArgs)); +// } +// } +//} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/resp/PrsResponse.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/resp/PrsResponse.java new file mode 100644 index 0000000000..71c3f25979 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/rtorrent/resp/PrsResponse.java @@ -0,0 +1,20 @@ +//package com.ghostchu.peerbanhelper.downloader.impl.rtorrent.resp; +// +//import com.ghostchu.peerbanhelper.downloader.impl.rtorrent.bean.RPeer; +//import com.google.gson.JsonArray; +//import com.google.gson.JsonElement; +//import lombok.Getter; +// +//import java.util.ArrayList; +//import java.util.List; +//@Getter +//public class PrsResponse { +// private final List rpcPeers = new ArrayList<>(); +// +// public PrsResponse(JsonArray arr) { +// for (JsonElement element : arr) { +// JsonArray peerArray = element.getAsJsonArray(); +// rpcPeers.add(new RPeer(peerArray.asList().stream().map(JsonElement::getAsString).toArray(String[]::new))); +// } +// } +//} diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRPeer.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRPeer.java index 313f5c8cec..be370583ee 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRPeer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRPeer.java @@ -1,10 +1,11 @@ package com.ghostchu.peerbanhelper.downloader.impl.transmission; import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.peer.PeerFlag; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import cordelia.rpc.types.Peers; -public class TRPeer implements Peer { +public final class TRPeer implements Peer { private final Peers backend; private transient PeerAddress peerAddress; @@ -57,8 +58,8 @@ public double getProgress() { } @Override - public String getFlags() { - return backend.getFlagStr(); + public PeerFlag getFlags() { + return new PeerFlag(backend.getFlagStr()); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRTorrent.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRTorrent.java index 6a00fbdf8b..8a8917c3bd 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRTorrent.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/TRTorrent.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.stream.Collectors; -public class TRTorrent implements Torrent { +public final class TRTorrent implements Torrent { private final Torrents backend; public TRTorrent(Torrents backend) { diff --git a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java index 26062c5325..8c2cc8f746 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java +++ b/src/main/java/com/ghostchu/peerbanhelper/downloader/impl/transmission/Transmission.java @@ -1,11 +1,11 @@ package com.ghostchu.peerbanhelper.downloader.impl.transmission; import com.ghostchu.peerbanhelper.downloader.Downloader; -import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth; import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; -import com.ghostchu.peerbanhelper.downloader.WebViewScriptCallback; +import com.ghostchu.peerbanhelper.downloader.DownloaderLoginResult; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.JsonUtil; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; @@ -32,6 +32,8 @@ import java.util.List; import java.util.stream.Collectors; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + public class Transmission implements Downloader { private static final Logger log = org.slf4j.LoggerFactory.getLogger(Transmission.class); private final String name; @@ -39,7 +41,7 @@ public class Transmission implements Downloader { private final String blocklistUrl; private final Config config; private DownloaderLastStatus lastStatus = DownloaderLastStatus.UNKNOWN; - private String statusMessage; + private TranslationComponent statusMessage; /* API 受限,实际实现起来意义不大 @@ -50,7 +52,7 @@ public Transmission(String name, String blocklistUrl, Config config) { this.config = config; this.client = new TrClient(config.getEndpoint() + config.getRpcUrl(), config.getUsername(), config.getPassword(), config.isVerifySsl(), HttpClient.Version.valueOf(config.getHttpVersion())); this.blocklistUrl = blocklistUrl; - log.warn(Lang.DOWNLOADER_TR_MOTD_WARNING); + log.warn(tlUI(Lang.DOWNLOADER_TR_MOTD_WARNING)); } private static String generateBlocklistUrl(String pbhServerAddress) { @@ -82,25 +84,25 @@ public String getEndpoint() { return config.getEndpoint(); } - @Override - public String getWebUIEndpoint() { - return config.getEndpoint(); - } - - @Override - public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { - return new DownloaderBasicAuth(config.getEndpoint(), config.getUsername(), config.getPassword()); - } - - @Override - public @Nullable WebViewScriptCallback getWebViewJavaScript() { - return null; - } - - @Override - public boolean isSupportWebview() { - return true; - } +// @Override +// public String getWebUIEndpoint() { +// return config.getEndpoint(); +// } + +// @Override +// public @Nullable DownloaderBasicAuth getDownloaderBasicAuth() { +// return new DownloaderBasicAuth(config.getEndpoint(), config.getUsername(), config.getPassword()); +// } +// +// @Override +// public @Nullable WebViewScriptCallback getWebViewJavaScript() { +// return null; +// } +// +// @Override +// public boolean isSupportWebview() { +// return true; +// } @Override @@ -114,14 +116,14 @@ public String getType() { } @Override - public boolean login() { + public DownloaderLoginResult login() { RqSessionGet get = new RqSessionGet(); TypedResponse resp = client.execute(get); // 执行任意 RPC 操作以刷新 session String version = resp.getArgs().getVersion(); if (version.startsWith("0.") || version.startsWith("1.") || version.startsWith("2.")) { - throw new IllegalStateException(String.format(Lang.DOWNLOADER_TR_KNOWN_INCOMPATIBILITY, Lang.DOWNLOADER_TR_INCOMPATIBILITY_BANAPI)); + return new DownloaderLoginResult(DownloaderLoginResult.Status.EXCEPTION, new TranslationComponent(Lang.DOWNLOADER_TR_KNOWN_INCOMPATIBILITY, "API Version")); } - return true; + return new DownloaderLoginResult(DownloaderLoginResult.Status.SUCCESS, new TranslationComponent(Lang.STATUS_TEXT_OK)); } @Override @@ -149,27 +151,34 @@ public void setBanList(Collection fullList, @Nullable Collection sessionSetResp = client.execute(set); if (!sessionSetResp.isSuccess()) { - log.warn(Lang.DOWNLOADER_TR_INCORRECT_BANLIST_API_RESP, sessionSetResp.getResult()); + log.error(tlUI(Lang.DOWNLOADER_TR_INCORRECT_BANLIST_API_RESP), sessionSetResp.getResult()); } Thread.sleep(3000); // Transmission 在这里疑似有崩溃问题? RqBlockList updateBlockList = new RqBlockList(); TypedResponse updateBlockListResp = client.execute(updateBlockList); if (!updateBlockListResp.isSuccess()) { - log.warn(Lang.DOWNLOADER_TR_INCORRECT_SET_BANLIST_API_RESP); + log.error(tlUI(Lang.DOWNLOADER_TR_INCORRECT_SET_BANLIST_API_RESP)); } else { - log.info(Lang.DOWNLOADER_TR_UPDATED_BLOCKLIST, updateBlockListResp.getArgs().getBlockListSize()); + log.info(tlUI(Lang.DOWNLOADER_TR_UPDATED_BLOCKLIST), updateBlockListResp.getArgs().getBlockListSize()); } } @Override public void relaunchTorrentIfNeeded(Collection torrents) { - relaunchTorrents(torrents.stream().map(t -> Long.parseLong(t.getId())).toList()); + relaunchTorrents(torrents.stream().filter(t -> { + try { + Long.parseLong(t.getId()); + return true; + } catch (Exception e) { + return false; + } + }).map(t -> Long.parseLong(t.getId())).toList()); } private void relaunchTorrents(Collection ids) { if (ids.isEmpty()) return; - log.info(Lang.DOWNLOADER_TR_DISCONNECT_PEERS, ids.size()); + log.info(tlUI(Lang.DOWNLOADER_TR_DISCONNECT_PEERS, ids.size())); RqTorrent stop = new RqTorrent(TorrentAction.STOP, new ArrayList<>()); for (long torrent : ids) { stop.add(torrent); @@ -189,7 +198,14 @@ private void relaunchTorrents(Collection ids) { @Override public void relaunchTorrentIfNeededByTorrentWrapper(Collection torrents) { - relaunchTorrents(torrents.stream().map(t -> Long.parseLong(t.getId())).toList()); + relaunchTorrents(torrents.stream().filter(t -> { + try { + Long.parseLong(t.getId()); + return true; + } catch (Exception e) { + return false; + } + }).map(t -> Long.parseLong(t.getId())).toList()); } @Override @@ -198,13 +214,13 @@ public DownloaderLastStatus getLastStatus() { } @Override - public void setLastStatus(DownloaderLastStatus lastStatus, String statusMessage) { + public void setLastStatus(DownloaderLastStatus lastStatus, TranslationComponent statusMessage) { this.lastStatus = lastStatus; this.statusMessage = statusMessage; } @Override - public String getLastStatusMessage() { + public TranslationComponent getLastStatusMessage() { return statusMessage; } @@ -233,8 +249,8 @@ public static Transmission.Config readFromYaml(ConfigurationSection section) { if (config.getEndpoint().endsWith("/")) { // 浏览器复制党 workaround 一下, 避免连不上的情况 config.setEndpoint(config.getEndpoint().substring(0, config.getEndpoint().length() - 1)); } - config.setUsername(section.getString("username")); - config.setPassword(section.getString("password")); + config.setUsername(section.getString("username", "")); + config.setPassword(section.getString("password", "")); config.setRpcUrl(section.getString("rpc-url", "/transmission/rpc")); config.setHttpVersion(section.getString("http-version", "HTTP_1_1")); config.setVerifySsl(section.getBoolean("verify-ssl", true)); diff --git a/src/main/java/com/ghostchu/peerbanhelper/event/LivePeersUpdatedEvent.java b/src/main/java/com/ghostchu/peerbanhelper/event/LivePeersUpdatedEvent.java index 24ae6b7684..33175a1df5 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/event/LivePeersUpdatedEvent.java +++ b/src/main/java/com/ghostchu/peerbanhelper/event/LivePeersUpdatedEvent.java @@ -2,14 +2,15 @@ import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import com.ghostchu.peerbanhelper.wrapper.PeerMetadata; -import com.google.common.collect.ImmutableMap; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.Map; + @AllArgsConstructor @NoArgsConstructor @Data public class LivePeersUpdatedEvent { - private ImmutableMap livePeers; + private Map livePeers; } diff --git a/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java b/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java index 28672864d3..ad3a1ad98c 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java +++ b/src/main/java/com/ghostchu/peerbanhelper/firewall/impl/CommandBasedImpl.java @@ -10,6 +10,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class CommandBasedImpl { @@ -27,7 +29,7 @@ public int invokeCommand(String command, Map env) throws IOExcep Process process = p.onExit().get(10, TimeUnit.SECONDS); if (process.isAlive()) { process.destroy(); - log.warn(Lang.COMMAND_EXECUTOR_FAILED_TIMEOUT, command); + log.error(tlUI(Lang.COMMAND_EXECUTOR_FAILED_TIMEOUT, command)); return -9999; } return process.exitValue(); diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/GuiManager.java b/src/main/java/com/ghostchu/peerbanhelper/gui/GuiManager.java index fa7b3ad033..189d285463 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/GuiManager.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/GuiManager.java @@ -9,8 +9,6 @@ public interface GuiManager { boolean isGuiAvailable(); - void showConfigurationSetupDialog(); - void createMainWindow(); void sync(); diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/PBHGuiManager.java b/src/main/java/com/ghostchu/peerbanhelper/gui/PBHGuiManager.java index 77ad13d055..ddbb7ff7b6 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/PBHGuiManager.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/PBHGuiManager.java @@ -24,11 +24,6 @@ public boolean isGuiAvailable() { return Desktop.isDesktopSupported(); } - @Override - public void showConfigurationSetupDialog() { - gui.showConfigurationSetupDialog(); - } - @Override public void createMainWindow() { gui.createMainWindow(); diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/GuiImpl.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/GuiImpl.java index e1f836f438..bbd3647f4b 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/GuiImpl.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/GuiImpl.java @@ -5,8 +5,6 @@ import java.util.logging.Level; public interface GuiImpl { - void showConfigurationSetupDialog(); - void setup(); void createMainWindow(); diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/console/ConsoleGuiImpl.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/console/ConsoleGuiImpl.java index ee59d525cb..cbdf98abcb 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/console/ConsoleGuiImpl.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/console/ConsoleGuiImpl.java @@ -1,11 +1,9 @@ package com.ghostchu.peerbanhelper.gui.impl.console; import com.ghostchu.peerbanhelper.gui.impl.GuiImpl; -import com.ghostchu.peerbanhelper.text.Lang; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import java.util.Scanner; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; @@ -16,13 +14,6 @@ public class ConsoleGuiImpl implements GuiImpl { public ConsoleGuiImpl(String[] args) { } - @Override - public void showConfigurationSetupDialog() { - log.info(Lang.CONFIG_PEERBANHELPER); - Scanner scanner = new Scanner(System.in); - scanner.nextLine(); - } - @Override public void setup() { // do nothing diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/Holder.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/Holder.java new file mode 100644 index 0000000000..82a51e9a2a --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/Holder.java @@ -0,0 +1,43 @@ +package com.ghostchu.peerbanhelper.gui.impl.javafx; + +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; + +import java.util.Objects; + +public final class Holder implements InvalidationListener { + public T value; + + public Holder() { + } + + public Holder(T value) { + this.value = value; + } + + @Override + public void invalidated(Observable observable) { + // no-op + } + + @Override + public int hashCode() { + return Objects.hashCode(value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (!(obj instanceof Holder)) + return false; + + return Objects.equals(this.value, ((Holder) obj).value); + } + + @Override + public String toString() { + return "Holder[" + value + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JFXUtil.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JFXUtil.java index 911dadb147..c742de67d2 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JFXUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JFXUtil.java @@ -1,8 +1,14 @@ package com.ghostchu.peerbanhelper.gui.impl.javafx; +import com.ghostchu.peerbanhelper.Main; import javafx.scene.Node; import javafx.scene.layout.AnchorPane; +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; + public class JFXUtil { public static void jfxNodeFitParent(Node _node) { AnchorPane.setTopAnchor(_node, 0.0); @@ -10,4 +16,12 @@ public static void jfxNodeFitParent(Node _node) { AnchorPane.setLeftAnchor(_node, 0.0); AnchorPane.setBottomAnchor(_node, 0.0); } + + public static void copyText(String content) { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + if (Main.getServer() != null && Main.getServer().getWebContainer() != null) { + Transferable ts = new StringSelection(content); + clipboard.setContents(ts, null); + } + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxImpl.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxImpl.java index 04557e54b4..23d78c6f0d 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxImpl.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxImpl.java @@ -1,55 +1,62 @@ package com.ghostchu.peerbanhelper.gui.impl.javafx; -import com.alessiodp.libby.Library; import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.MainJavaFx; -import com.ghostchu.peerbanhelper.downloader.Downloader; -import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth; import com.ghostchu.peerbanhelper.event.PBHServerStartedEvent; import com.ghostchu.peerbanhelper.gui.impl.GuiImpl; import com.ghostchu.peerbanhelper.gui.impl.console.ConsoleGuiImpl; import com.ghostchu.peerbanhelper.gui.impl.javafx.mainwindow.JFXWindowController; import com.ghostchu.peerbanhelper.log4j2.SwingLoggerAppender; import com.ghostchu.peerbanhelper.text.Lang; -import com.ghostchu.peerbanhelper.util.maven.GeoUtil; -import com.ghostchu.peerbanhelper.util.maven.MavenCentralMirror; +import com.ghostchu.peerbanhelper.util.collection.CircularArrayList; import com.google.common.eventbus.Subscribe; import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.css.PseudoClass; +import javafx.geometry.Insets; import javafx.scene.control.Alert; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; import javafx.scene.control.Tab; -import javafx.scene.control.TextArea; -import javafx.scene.control.TextFormatter; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.Region; import javafx.stage.Stage; import javafx.stage.WindowEvent; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; -import javax.swing.*; import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; @Getter @Slf4j public class JavaFxImpl extends ConsoleGuiImpl implements GuiImpl { + private static final int MAX_LINES = 300; + // private final AtomicBoolean needUpdate = new AtomicBoolean(false); +// private String logsBuffer; + private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty"); + private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal"); + private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error"); + private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn"); + private static final PseudoClass INFO = PseudoClass.getPseudoClass("info"); + private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug"); + private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace"); + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); @Getter private final boolean silentStart; private final String[] args; - private TrayIcon trayIcon; - private static final int MAX_LINES = 300; private final LinkedList lines = new LinkedList<>(); + private final Set> selected = new HashSet<>(); + private TrayIcon trayIcon; + private ListView logsView; + private boolean persistFlagTrayMessageSent = false; public JavaFxImpl(String[] args) { super(args); @@ -58,6 +65,7 @@ public JavaFxImpl(String[] args) { Main.getEventBus().register(this); } + private boolean isWebViewSupported() { try { Class.forName("javafx.scene.web.WebView"); @@ -67,11 +75,6 @@ private boolean isWebViewSupported() { } } - @Override - public void showConfigurationSetupDialog() { - log.info(Lang.CONFIG_PEERBANHELPER); - JOptionPane.showMessageDialog(null, Lang.CONFIG_PEERBANHELPER, "Dialog", JOptionPane.INFORMATION_MESSAGE); - } @Override public void setup() { @@ -81,86 +84,42 @@ public void setup() { @Subscribe public void onPBHServerStarted(PBHServerStartedEvent event) { + if (Arrays.stream(Main.getStartupArgs()).anyMatch(s -> s.equalsIgnoreCase("nowebuitab"))) { + return; + } CompletableFuture.runAsync(() -> { - if (!isWebViewSupported()) { - try { - tryLoadWebViewLibraries(); - } catch (Exception e) { - log.error("Unable to load webviews", e); - } - } if (isWebViewSupported()) { Platform.runLater(() -> { - MainJavaFx.getStage().setTitle(String.format(Lang.GUI_TITLE_LOADED, "JavaFx", Main.getMeta().getVersion(), Main.getMeta().getAbbrev())); + MainJavaFx.getStage().setTitle(tlUI(Lang.GUI_TITLE_LOADED, "JavaFx", Main.getMeta().getVersion(), Main.getMeta().getAbbrev())); if (isWebViewSupported()) { JFXWindowController controller = MainJavaFx.INSTANCE.getController(); - Tab webuiTab = JavaFxWebViewWrapper.installWebViewTab(controller.getTabPane(), Lang.GUI_MENU_WEBUI, Main.getServer().getWebUiUrl(), Collections.emptyMap(), null); + Tab webuiTab = JavaFxWebViewWrapper.installWebViewTab(controller.getTabPane(), tlUI(Lang.GUI_MENU_WEBUI), Main.getServer().getWebUiUrl(), Collections.emptyMap(), null); javafx.scene.control.SingleSelectionModel selectionModel = controller.getTabPane().getSelectionModel(); selectionModel.select(webuiTab); - log.info(Lang.WEBVIEW_ENABLED); - if (System.getProperty("pbh.enableDownloadWebView") != null) { - for (Downloader downloader : Main.getServer().getDownloaders()) { - if (!downloader.isSupportWebview()) { - continue; - } - DownloaderBasicAuth basicAuth = downloader.getDownloaderBasicAuth(); - Map headers = new HashMap<>(); - if (basicAuth != null) { - String cred = Base64.getEncoder().encodeToString((basicAuth.username() + ":" + basicAuth.password()).getBytes(StandardCharsets.UTF_8)); - headers.put("Authorization", "Basic " + cred); - } - JavaFxWebViewWrapper.installWebViewTab(controller.getTabPane(), - downloader.getName(), - downloader.getWebUIEndpoint(), - headers, downloader.getWebViewJavaScript()); - } - } + log.info(tlUI(Lang.WEBVIEW_ENABLED)); +// if (System.getProperty("pbh.enableDownloadWebView") != null) { +// for (Downloader downloader : Main.getServer().getDownloaders()) { +// if (!downloader.isSupportWebview()) { +// continue; +// } +// DownloaderBasicAuth basicAuth = downloader.getDownloaderBasicAuth(); +// Map headers = new HashMap<>(); +// if (basicAuth != null) { +// String cred = Base64.getEncoder().encodeToString((basicAuth.username() + ":" + basicAuth.password()).getBytes(StandardCharsets.UTF_8)); +// headers.put("Authorization", "Basic " + cred); +// } +// JavaFxWebViewWrapper.installWebViewTab(controller.getTabPane(), +// downloader.getName(), +// downloader.getWebUIEndpoint(), +// headers, downloader.getWebViewJavaScript()); +// } +// } } else { - log.info(Lang.WEBVIEW_DISABLED_WEBKIT_NOT_INCLUDED); + log.info(tlUI(Lang.WEBVIEW_DISABLED_WEBKIT_NOT_INCLUDED)); } }); } }); - - - } - - - private void tryLoadWebViewLibraries() { - String osName = System.getProperty("os.name").toLowerCase(); - String sysArch = "win"; - if (osName.contains("linux")) { - sysArch = "linux"; - } else if (osName.contains("mac")) { - sysArch = "mac"; - } - List server = GeoUtil.determineBestMirrorServer(log); - if (!server.isEmpty()) { - MavenCentralMirror mirror = server.getFirst(); - if (mirror != null) { - Main.getLibraryManager().addRepository(mirror.getRepoUrl()); - } - } - String javafx = Main.getMeta().getJavafx().replace("${javafx.version}", "22.0.1"); - Main.getLibraryManager().loadLibraries(Library.builder() - .groupId("org{}openjfx") // "{}" is replaced with ".", useful to avoid unwanted changes made by maven-shade-plugin - .artifactId("javafx-media") - .version(javafx) - .classifier(sysArch) - .resolveTransitiveDependencies(false) - .build(), Library.builder() - .groupId("org{}openjfx") // "{}" is replaced with ".", useful to avoid unwanted changes made by maven-shade-plugin - .artifactId("javafx-graphics") - .version(javafx) - .classifier(sysArch) - .resolveTransitiveDependencies(false) - .build(), Library.builder() - .groupId("org{}openjfx") // "{}" is replaced with ".", useful to avoid unwanted changes made by maven-shade-plugin - .artifactId("javafx-web") - .version(javafx) - .classifier(sysArch) - .resolveTransitiveDependencies(false) - .build()); } @SneakyThrows @@ -172,49 +131,112 @@ public void createMainWindow() { Thread.sleep(50); Thread.yield(); } - Platform.runLater(() -> setupJFXWindow()); + Platform.runLater(this::setupJFXWindow); } private void setupJFXWindow() { Stage st = MainJavaFx.getStage(); + st.getScene().getRoot().getStylesheets().add(Main.class.getResource("/javafx/css/root.css").toExternalForm()); st.getScene().getWindow().addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, this::closeWindowEvent); JFXWindowController controller = MainJavaFx.INSTANCE.getController(); - Pattern newline = Pattern.compile("\n"); - TextArea textArea = controller.getLogsTextArea(); - textArea.setTextFormatter(new TextFormatter(change -> { - String newText = change.getControlNewText(); - Matcher matcher = newline.matcher(newText); - int lines = 1; - while (matcher.find()) lines++; - if (lines <= SwingLoggerAppender.maxLinesSetting) return change; - int linesToDrop = lines - SwingLoggerAppender.maxLinesSetting; - int index = 0; - for (int i = 0; i < linesToDrop; i++) { - index = newText.indexOf('\n', index); + this.logsView = controller.getLogsListView(); + this.logsView.setStyle("-fx-font-family: Consolas, Monospace"); + this.logsView.setItems(FXCollections.observableList(new CircularArrayList<>(SwingLoggerAppender.maxLinesSetting + 1))); +// this.logsView.getItems().addListener((InvalidationListener) observable -> { +// if (!logsView.getItems().isEmpty()) { +// logsView.scrollTo(logsView.getItems().size() - 1); +// } +// }); + Holder lastCell = new Holder<>(); + this.logsView.setCellFactory(x -> new ListCell<>() { + { + getStyleClass().add("log-window-list-cell"); + Region clippedContainer = (Region) logsView.lookup(".clipped-container"); + if (clippedContainer != null) { + maxWidthProperty().bind(clippedContainer.widthProperty()); + prefWidthProperty().bind(clippedContainer.widthProperty()); + } + setPadding(new Insets(2)); + setWrapText(true); + setGraphic(null); + setOnMouseClicked(event -> { + if (!event.isControlDown()) { + for (ListCell logListCell : selected) { + if (logListCell != this) { + logListCell.pseudoClassStateChanged(SELECTED, false); + if (logListCell.getItem() != null) { + logListCell.getItem().setSelected(false); + } + } + } + selected.clear(); + } + + selected.add(this); + pseudoClassStateChanged(SELECTED, true); + if (getItem() != null) { + getItem().setSelected(true); + } + }); + } + + @Override + protected void updateItem(ListLogEntry item, boolean empty) { + super.updateItem(item, empty); + // https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html + if (this == lastCell.value && !isVisible()) + return; + lastCell.value = this; + pseudoClassStateChanged(EMPTY, empty); + pseudoClassStateChanged(ERROR, !empty && item.getLevel() == org.slf4j.event.Level.ERROR); + pseudoClassStateChanged(WARN, !empty && item.getLevel() == org.slf4j.event.Level.WARN); + pseudoClassStateChanged(INFO, !empty && item.getLevel() == org.slf4j.event.Level.INFO); + pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == org.slf4j.event.Level.DEBUG); + pseudoClassStateChanged(TRACE, !empty && item.getLevel() == org.slf4j.event.Level.TRACE); + pseudoClassStateChanged(SELECTED, !empty && item.isSelected()); + if (empty) { + setText(null); + } else { + setText(item.getLog()); + } + } + }); + logsView.setOnKeyPressed(event -> { + if (event.isControlDown() && event.getCode() == KeyCode.C) { + StringBuilder stringBuilder = new StringBuilder(); + + for (ListLogEntry item : logsView.getItems()) { + if (item != null && item.isSelected()) { + if (item.getLog() != null) { + stringBuilder.append(item.getLog()); + } + stringBuilder.append('\n'); + } + } + String cont = stringBuilder.toString(); + JFXUtil.copyText(cont); + createDialog(Level.INFO, tlUI(Lang.GUI_COPY_TO_CLIPBOARD_TITLE), tlUI(Lang.GUI_COPY_TO_CLIPBOARD_DESCRIPTION, cont)); } - change.setRange(0, change.getControlText().length()); - change.setText(newText.substring(index + 1)); - return change; - })); + }); initLoggerRedirection(); - controller.getMenuProgram().setText(Lang.GUI_MENU_PROGRAM); - controller.getMenuWebui().setText(Lang.GUI_MENU_WEBUI); - controller.getTabLogs().setText(Lang.GUI_TABBED_LOGS); - controller.getMenuProgramQuit().setText(Lang.GUI_MENU_QUIT); + controller.getMenuProgram().setText(tlUI(Lang.GUI_MENU_PROGRAM)); + controller.getMenuWebui().setText(tlUI(Lang.GUI_MENU_WEBUI)); + controller.getTabLogs().setText(tlUI(Lang.GUI_TABBED_LOGS)); + controller.getMenuProgramQuit().setText(tlUI(Lang.GUI_MENU_QUIT)); controller.getMenuProgramQuit().setOnAction(e -> System.exit(0)); - controller.getMenuProgramOpenInGithub().setText(Lang.ABOUT_VIEW_GITHUB); - controller.getMenuProgramOpenInGithub().setOnAction(e -> openWebpage(URI.create(Lang.GITHUB_PAGE))); - controller.getMenuProgramOpenInBrowser().setText(Lang.GUI_MENU_WEBUI_OPEN); + controller.getMenuProgramOpenInGithub().setText(tlUI(Lang.ABOUT_VIEW_GITHUB)); + controller.getMenuProgramOpenInGithub().setOnAction(e -> openWebpage(URI.create(tlUI(Lang.GITHUB_PAGE)))); + controller.getMenuProgramOpenInBrowser().setText(tlUI(Lang.GUI_MENU_WEBUI_OPEN)); controller.getMenuProgramOpenInBrowser().setOnAction(e -> openWebpage(URI.create(Main.getServer().getWebUiUrl()))); - controller.getMenuProgramCopyWebuiToken().setText(Lang.GUI_COPY_WEBUI_TOKEN); + controller.getMenuProgramCopyWebuiToken().setText(tlUI(Lang.GUI_COPY_WEBUI_TOKEN)); controller.getMenuProgramCopyWebuiToken().setOnAction(e -> { - Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - String content = Main.getServer().getWebContainer().getToken(); - Transferable ts = new StringSelection(content); - clipboard.setContents(ts, null); - createDialog(Level.INFO, Lang.GUI_COPY_TO_CLIPBOARD_TITLE, String.format(Lang.GUI_COPY_TO_CLIPBOARD_DESCRIPTION, content)); + if (Main.getServer() != null && Main.getServer().getWebContainer() != null) { + String content = Main.getServer().getWebContainer().getToken(); + JFXUtil.copyText(content); + createDialog(Level.INFO, tlUI(Lang.GUI_COPY_TO_CLIPBOARD_TITLE), String.format(tlUI(Lang.GUI_COPY_TO_CLIPBOARD_DESCRIPTION, content))); + } }); - controller.getMenuProgramOpenDataDirectory().setText(Lang.GUI_MENU_OPEN_DATA_DIRECTORY); + controller.getMenuProgramOpenDataDirectory().setText(tlUI(Lang.GUI_MENU_OPEN_DATA_DIRECTORY)); controller.getMenuProgramOpenDataDirectory().setOnAction(e -> { try { Desktop.getDesktop().open(Main.getDataDirectory()); @@ -234,31 +256,22 @@ private void closeWindowEvent(WindowEvent windowEvent) { private void minimizeToTray() { if (trayIcon != null) { setVisible(false); - trayIcon.displayMessage(Lang.GUI_TRAY_MESSAGE_CAPTION, Lang.GUI_TRAY_MESSAGE_DESCRIPTION, TrayIcon.MessageType.INFO); + if (!persistFlagTrayMessageSent) { + persistFlagTrayMessageSent = true; + trayIcon.displayMessage(tlUI(Lang.GUI_TRAY_MESSAGE_CAPTION), tlUI(Lang.GUI_TRAY_MESSAGE_DESCRIPTION), TrayIcon.MessageType.INFO); + } } } - private void insertLog(@NotNull String newText) { - String[] newLines = newText.split("\n"); - lines.addAll(Arrays.asList(newLines)); - while (lines.size() > MAX_LINES) { - lines.removeFirst(); - } - StringBuilder limitedText = new StringBuilder(); - for (String line : lines) { - limitedText.append(line).append("\n"); - } - JFXWindowController controller = MainJavaFx.INSTANCE.getController(); - javafx.scene.control.TextArea textArea = controller.getLogsTextArea(); - textArea.setText(String.join("\n", limitedText)); - textArea.positionCaret(limitedText.length()); - } private void initLoggerRedirection() { SwingLoggerAppender.registerListener(loggerEvent -> { try { Platform.runLater(() -> { - insertLog(loggerEvent.message()); + logsView.getItems().add(new ListLogEntry(loggerEvent.message(), loggerEvent.level())); + if (!logsView.getItems().isEmpty()) { + logsView.scrollTo(logsView.getItems().size() - 1); + } }); } catch (IllegalStateException exception) { exception.printStackTrace(); @@ -333,11 +346,14 @@ public void createDialog(Level level, String title, String description) { if (level.equals(Level.INFO)) { alertType = Alert.AlertType.INFORMATION; } - Alert alert = new Alert(alertType); - alert.setTitle(title); - alert.setHeaderText(title); - alert.setContentText(description); - alert.show(); + Alert.AlertType finalAlertType = alertType; + Platform.runLater(() -> { + Alert alert = new Alert(finalAlertType); + alert.setTitle(title); + alert.setHeaderText(title); + alert.setContentText(description); + alert.show(); + }); } @Override @@ -352,10 +368,13 @@ public void createNotification(Level level, String title, String description) { if (level.equals(Level.INFO)) { alertType = Alert.AlertType.INFORMATION; } - Alert alert = new Alert(alertType); - alert.setTitle(title); - alert.setHeaderText(title); - alert.setContentText(description); - alert.show(); + Alert.AlertType finalAlertType = alertType; + Platform.runLater(() -> { + Alert alert = new Alert(finalAlertType); + alert.setTitle(title); + alert.setHeaderText(title); + alert.setContentText(description); + alert.show(); + }); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxWebViewWrapper.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxWebViewWrapper.java index fb8f9ca688..87d1612646 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxWebViewWrapper.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/JavaFxWebViewWrapper.java @@ -1,17 +1,12 @@ package com.ghostchu.peerbanhelper.gui.impl.javafx; -import com.ghostchu.peerbanhelper.Main; -import com.ghostchu.peerbanhelper.downloader.Downloader; -import com.ghostchu.peerbanhelper.downloader.DownloaderBasicAuth; import com.ghostchu.peerbanhelper.downloader.WebViewScriptCallback; import com.ghostchu.peerbanhelper.text.Lang; import com.sun.javafx.scene.control.ContextMenuContent; -import javafx.concurrent.Worker; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.*; import javafx.scene.layout.AnchorPane; -import javafx.scene.web.WebEvent; import javafx.scene.web.WebView; import javafx.stage.PopupWindow; import javafx.stage.Window; @@ -25,11 +20,11 @@ import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.ByteArrayOutputStream; -import java.net.Authenticator; -import java.net.PasswordAuthentication; import java.util.Map; import java.util.StringJoiner; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class JavaFxWebViewWrapper { public static Tab installWebViewTab(TabPane tabPane, String tabName, String webuiPath, Map headers, @Nullable WebViewScriptCallback initScript) { @@ -40,7 +35,7 @@ public static Tab installWebViewTab(TabPane tabPane, String tabName, String webu } headers.forEach((key, value) -> joiner.add(key + ": " + value)); webView.getEngine().setUserAgent(joiner.toString()); - installWebViewEventListeners(webView, initScript); + //installWebViewEventListeners(webView, initScript); AnchorPane anchorPane = new AnchorPane(); anchorPane.setMinHeight(0); anchorPane.getChildren().add(webView); @@ -65,13 +60,13 @@ private static PopupWindow getPopupWindow(WebView webView, String webuiPath) { Node bridge = popup.lookup(".context-menu"); ContextMenuContent cmc = (ContextMenuContent) ((Parent) bridge).getChildrenUnmodifiable().get(0); ContextMenu contextMenu = new ContextMenu(); - MenuItem reload = new MenuItem(Lang.WEBVIEW_RELOAD_PAGE); + MenuItem reload = new MenuItem(tlUI(Lang.WEBVIEW_RELOAD_PAGE)); reload.setOnAction(e -> webView.getEngine().reload()); - MenuItem reset = new MenuItem(Lang.WEBVIEW_RESET_PAGE); + MenuItem reset = new MenuItem(tlUI(Lang.WEBVIEW_RESET_PAGE)); reset.setOnAction(e -> webView.getEngine().load(webuiPath)); - MenuItem back = new MenuItem(Lang.WEBVIEW_BACK); + MenuItem back = new MenuItem(tlUI(Lang.WEBVIEW_BACK)); back.setOnAction(e -> webView.getEngine().executeScript("history.back()")); - MenuItem forward = new MenuItem(Lang.WEBVIEW_FORWARD); + MenuItem forward = new MenuItem(tlUI(Lang.WEBVIEW_FORWARD)); forward.setOnAction(e -> webView.getEngine().executeScript("history.forward()")); contextMenu.getItems().addAll(back, forward, new SeparatorMenuItem(), reset); // add new item: @@ -111,44 +106,44 @@ public static String docToString(Document doc) { } return xmlStr; } - - - public static void installWebViewEventListeners(WebView webView, @Nullable WebViewScriptCallback callback) { - System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""); - System.setProperty("jdk.http.auth.proxying.disabledSchemes", ""); - if (callback != null) { - webView.getEngine().getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> { - if (newState == Worker.State.SUCCEEDED) { - String js = callback.pageLoaded(webView.getEngine().getLocation(), docToString(webView.getEngine().getDocument())); - if (js != null) { - webView.getEngine().executeScript(js); - } - } - }); - } - webView.getEngine().setOnError(event -> log.warn("[WebView] {}", event.getMessage(), event.getException())); - - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - String prefix = getRequestingScheme() + "://" + getRequestingHost() + ":" + getRequestingPort(); - for (Downloader downloader : Main.getServer().getDownloaders()) { - DownloaderBasicAuth dba = downloader.getDownloaderBasicAuth(); - if (dba != null) { - if (prefix.startsWith(dba.urlPrefix())) { - return new PasswordAuthentication(dba.username(), dba.password().toCharArray()); - } - } - } - return null; - } - }); - webView.getEngine().setOnAlert((WebEvent wEvent) -> { - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle(Lang.JFX_WEBVIEW_ALERT); - alert.setHeaderText(Lang.JFX_WEBVIEW_ALERT); - alert.setContentText(wEvent.getData()); - }); - } +// +// +// public static void installWebViewEventListeners(WebView webView, @Nullable WebViewScriptCallback callback) { +// System.setProperty("jdk.http.auth.tunneling.disabledSchemes", ""); +// System.setProperty("jdk.http.auth.proxying.disabledSchemes", ""); +// if (callback != null) { +// webView.getEngine().getLoadWorker().stateProperty().addListener((ov, oldState, newState) -> { +// if (newState == Worker.State.SUCCEEDED) { +// String js = callback.pageLoaded(webView.getEngine().getLocation(), docToString(webView.getEngine().getDocument())); +// if (js != null) { +// webView.getEngine().executeScript(js); +// } +// } +// }); +// } +// webView.getEngine().setOnError(event -> log.warn("[WebView] {}", event.getMessage(), event.getException())); +// +// Authenticator.setDefault(new Authenticator() { +// @Override +// protected PasswordAuthentication getPasswordAuthentication() { +// String prefix = getRequestingScheme() + "://" + getRequestingHost() + ":" + getRequestingPort(); +// for (Downloader downloader : Main.getServer().getDownloaders()) { +// DownloaderBasicAuth dba = downloader.getDownloaderBasicAuth(); +// if (dba != null) { +// if (prefix.startsWith(dba.urlPrefix())) { +// return new PasswordAuthentication(dba.username(), dba.password().toCharArray()); +// } +// } +// } +// return null; +// } +// }); +// webView.getEngine().setOnAlert((WebEvent wEvent) -> { +// Alert alert = new Alert(Alert.AlertType.INFORMATION); +// alert.setTitle(Lang.JFX_WEBVIEW_ALERT); +// alert.setHeaderText(Lang.JFX_WEBVIEW_ALERT); +// alert.setContentText(wEvent.getData()); +// }); +// } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/ListLogEntry.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/ListLogEntry.java new file mode 100644 index 0000000000..283c40eb43 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/ListLogEntry.java @@ -0,0 +1,17 @@ +package com.ghostchu.peerbanhelper.gui.impl.javafx; + +import lombok.Data; +import org.slf4j.event.Level; + +@Data +public class ListLogEntry { + private final String log; + private final Level level; + private boolean selected = false; + + public ListLogEntry(String log, Level level) { + this.log = log; + this.level = level; + } + +} \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/mainwindow/JFXWindowController.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/mainwindow/JFXWindowController.java index 3b3cef0714..2b74e70f7b 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/mainwindow/JFXWindowController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/javafx/mainwindow/JFXWindowController.java @@ -1,5 +1,6 @@ package com.ghostchu.peerbanhelper.gui.impl.javafx.mainwindow; +import com.ghostchu.peerbanhelper.gui.impl.javafx.ListLogEntry; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; @@ -17,7 +18,7 @@ public class JFXWindowController implements Initializable { private Tab tabLogs; @FXML @Getter - private TextArea logsTextArea; + private ListView logsListView; @FXML @Getter private Menu menuProgram; diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/MainWindow.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/MainWindow.java index 431050bbff..9312048bc1 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/MainWindow.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/MainWindow.java @@ -1,29 +1,21 @@ package com.ghostchu.peerbanhelper.gui.impl.swing; import com.ghostchu.peerbanhelper.Main; -import com.ghostchu.peerbanhelper.event.LivePeersUpdatedEvent; import com.ghostchu.peerbanhelper.text.Lang; -import com.ghostchu.peerbanhelper.util.MsgUtil; -import com.ghostchu.peerbanhelper.wrapper.BakedPeerMetadata; -import com.ghostchu.peerbanhelper.wrapper.PeerAddress; -import com.ghostchu.peerbanhelper.wrapper.PeerMetadata; -import com.google.common.eventbus.Subscribe; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.plaf.FontUIResource; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.JTableHeader; -import javax.swing.table.TableColumn; import javax.swing.text.StyleContext; import java.awt.*; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import java.net.URI; -import java.util.List; -import java.util.*; +import java.util.Locale; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; @Slf4j public class MainWindow extends JFrame { @@ -40,13 +32,12 @@ public class MainWindow extends JFrame { @Nullable @Getter private TrayIcon trayIcon; - private String[] peersTableColumn = new String[]{"Loading..."}; - private String[][] peersTableData = new String[0][0]; + private boolean persistFlagTrayMessageSent; public MainWindow(SwingGuiImpl swingGUI) { this.swingGUI = swingGUI; setJMenuBar(setupMenuBar()); - setTitle(String.format(Lang.GUI_TITLE_LOADED, "Swing UI", Main.getMeta().getVersion(), Main.getMeta().getAbbrev())); + setTitle(tlUI(Lang.GUI_TITLE_LOADED, "Swing UI", Main.getMeta().getVersion(), Main.getMeta().getAbbrev())); setSize(1000, 600); setContentPane(mainPanel); setupTabbedPane(); @@ -95,13 +86,6 @@ public void windowDeactivated(WindowEvent e) { setVisible(!swingGUI.isSilentStart()); } - private void minimizeToTray() { - if (trayIcon != null) { - setVisible(false); - trayIcon.displayMessage(Lang.GUI_TRAY_MESSAGE_CAPTION, Lang.GUI_TRAY_MESSAGE_DESCRIPTION, TrayIcon.MessageType.INFO); - } - } - public static void setTabTitle(JPanel tab, String title) { JTabbedPane tabbedPane = (JTabbedPane) SwingUtilities.getAncestorOfClass(JTabbedPane.class, tab); for (int tabIndex = 0; tabIndex < tabbedPane.getTabCount(); tabIndex++) { @@ -112,8 +96,18 @@ public static void setTabTitle(JPanel tab, String title) { } } + private void minimizeToTray() { + if (trayIcon != null) { + setVisible(false); + if (!persistFlagTrayMessageSent) { + persistFlagTrayMessageSent = true; + trayIcon.displayMessage(tlUI(Lang.GUI_TRAY_MESSAGE_CAPTION), tlUI(Lang.GUI_TRAY_MESSAGE_DESCRIPTION), TrayIcon.MessageType.INFO); + } + } + } + private void setComponents() { - setLivePeersTable(); + } private void setupSystemTray() { @@ -148,17 +142,22 @@ private JMenuBar setupMenuBar() { } private Component generateAboutMenu() { - JMenu aboutMenu = new JMenu(Lang.GUI_MENU_ABOUT); - JMenuItem viewOnGithub = new JMenuItem(Lang.ABOUT_VIEW_GITHUB); - viewOnGithub.addActionListener(e -> swingGUI.openWebpage(URI.create(Lang.GITHUB_PAGE))); + JMenu aboutMenu = new JMenu(tlUI(Lang.GUI_MENU_ABOUT)); + JMenuItem viewOnGithub = new JMenuItem(tlUI(Lang.ABOUT_VIEW_GITHUB)); + viewOnGithub.addActionListener(e -> swingGUI.openWebpage(URI.create(tlUI(Lang.GITHUB_PAGE)))); aboutMenu.add(viewOnGithub); return aboutMenu; } private JMenu generateWebUIMenu() { - JMenu webUIMenu = new JMenu(Lang.GUI_MENU_WEBUI); - JMenuItem openWebUIMenuItem = new JMenuItem(Lang.GUI_MENU_WEBUI_OPEN); - openWebUIMenuItem.addActionListener(e -> swingGUI.openWebpage(URI.create("http://localhost:" + Main.getServer().getWebContainer().javalin().port()))); + JMenu webUIMenu = new JMenu(tlUI(Lang.GUI_MENU_WEBUI)); + JMenuItem openWebUIMenuItem = new JMenuItem(tlUI(Lang.GUI_MENU_WEBUI_OPEN)); + + openWebUIMenuItem.addActionListener(e -> { + if (Main.getServer() != null && Main.getServer().getWebContainer() != null) { + swingGUI.openWebpage(URI.create("http://localhost:" + Main.getServer().getWebContainer().javalin().port())); + } + }); webUIMenu.add(openWebUIMenuItem); return webUIMenu; } @@ -168,119 +167,7 @@ public void sync() { } private void setupTabbedPane() { - setTabTitle(tabbedPaneLogs, Lang.GUI_TABBED_LOGS); - setTabTitle(tabbedPaneLivePeers, Lang.GUI_TABBED_PEERS); - } - - private void setLivePeersTable() { - livePeers.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); - resizeTable.setText(Lang.GUI_BUTTON_RESIZE_TABLE); - resizeTable.addActionListener(l -> fitTableColumns(livePeers)); - livePeers.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - peersTableColumn = Lang.GUI_LIVE_PEERS_COLUMN_NAMES; - livePeers.setModel(new AbstractTableModel() { - @Override - public int getRowCount() { - return peersTableData.length; - } - - @Override - public int getColumnCount() { - return peersTableColumn.length; - } - - @Override - public String getColumnName(int columnIndex) { - return peersTableColumn[columnIndex]; - } - - @Override - public boolean isCellEditable(int rowIndex, int columnIndex) { - return false; - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - return peersTableData[rowIndex][columnIndex]; - } - }); - } - - private void fitTableColumns(JTable myTable) { - JTableHeader header = myTable.getTableHeader(); - int rowCount = myTable.getRowCount(); - Enumeration columns = myTable.getColumnModel().getColumns(); - while (columns.hasMoreElements()) { - TableColumn column = columns.nextElement(); - int col = header.getColumnModel().getColumnIndex(column.getIdentifier()); - int width = (int) myTable.getTableHeader().getDefaultRenderer() - .getTableCellRendererComponent(myTable, column.getIdentifier() - , false, false, -1, col).getPreferredSize().getWidth(); - for (int row = 0; row < rowCount; row++) { - int preferedWidth = (int) myTable.getCellRenderer(row, col).getTableCellRendererComponent(myTable, - myTable.getValueAt(row, col), false, false, row, col).getPreferredSize().getWidth(); - width = Math.max(width, preferedWidth); - } - header.setResizingColumn(column); - column.setWidth(width + myTable.getIntercellSpacing().width); - } - } - - private void updateLivePeersTable(String[][] data) { - this.peersTableData = data; - if (livePeers.isShowing()) { // 只在显示时重绘以显示数据更新,节约非 peers 页面的资源消耗 - livePeers.repaint(); - } - } - - @Subscribe - public void onLivePeersUpdated(LivePeersUpdatedEvent event) { - String[][] data = new String[event.getLivePeers().size()][Lang.GUI_LIVE_PEERS_COLUMN_NAMES.length]; - List> entrySet = new ArrayList<>(event.getLivePeers().entrySet()); - for (int i = 0; i < entrySet.size(); i++) { - Map.Entry entry = entrySet.get(i); - String countryRegion = "N/A"; - String ip = entry.getKey().getIp(); - String peerId = entry.getValue().getPeer().getId(); - String clientName = entry.getValue().getPeer().getClientName(); - String progress = String.format("%.1f", entry.getValue().getPeer().getProgress() * 100) + "%"; - String uploadSpeed = MsgUtil.humanReadableByteCountBin(entry.getValue().getPeer().getUploadSpeed()) + "/s"; - String uploaded = MsgUtil.humanReadableByteCountBin(entry.getValue().getPeer().getUploaded()); - String downloadSpeed = MsgUtil.humanReadableByteCountBin(entry.getValue().getPeer().getDownloadSpeed()) + "/s"; - String downloaded = MsgUtil.humanReadableByteCountBin(entry.getValue().getPeer().getDownloaded()); - String torrent = entry.getValue().getTorrent().getName(); - String city = "N/A"; - String asn = "N/A"; - String asOrg = "N/A"; - String asNetwork = "N/A"; - BakedPeerMetadata bakedBanMetadata = new BakedPeerMetadata(entry.getValue()); - if (bakedBanMetadata.getGeo() != null) { - countryRegion = bakedBanMetadata.getGeo().getCountryRegion(); - city = bakedBanMetadata.getGeo().getCity(); - } - if (bakedBanMetadata.getAsn() != null) { - asn = "AS" + bakedBanMetadata.getAsn().getAsn(); - asOrg = bakedBanMetadata.getAsn().getAsOrganization(); - asNetwork = bakedBanMetadata.getAsn().getAsNetwork(); - } - List array = new ArrayList<>(); // 这里用 List,这样动态创建 array 就不用指定位置了 - array.add(countryRegion); - array.add(ip); - array.add(peerId); - array.add(clientName); - array.add(progress); - array.add(uploadSpeed); - array.add(uploaded); - array.add(downloadSpeed); - array.add(downloaded); - array.add(torrent); - array.add(city); - array.add(asn); - array.add(asOrg); - array.add(asNetwork); - System.arraycopy(array.toArray(new String[0]), 0, data[i], 0, array.size()); - } - updateLivePeersTable(data); + setTabTitle(tabbedPaneLogs, tlUI(Lang.GUI_TABBED_LOGS)); } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java index fb16370eab..24d4c1c86d 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java +++ b/src/main/java/com/ghostchu/peerbanhelper/gui/impl/swing/SwingGuiImpl.java @@ -4,7 +4,6 @@ import com.ghostchu.peerbanhelper.gui.impl.GuiImpl; import com.ghostchu.peerbanhelper.gui.impl.console.ConsoleGuiImpl; import com.ghostchu.peerbanhelper.log4j2.SwingLoggerAppender; -import com.ghostchu.peerbanhelper.text.Lang; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -28,11 +27,6 @@ public SwingGuiImpl(String[] args) { this.silentStart = Arrays.stream(args).anyMatch(s -> s.equalsIgnoreCase("silent")); } - @Override - public void showConfigurationSetupDialog() { - log.info(Lang.CONFIG_PEERBANHELPER); - JOptionPane.showMessageDialog(null, Lang.CONFIG_PEERBANHELPER, "Dialog", JOptionPane.INFORMATION_MESSAGE); - } @Override public void setup() { diff --git a/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java b/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java index 1df6cf01c1..ccbe395286 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java +++ b/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/CommandExec.java @@ -14,6 +14,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static com.ghostchu.peerbanhelper.Main.DEF_LOCALE; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class CommandExec implements BanListInvoker { private List resetCommands; @@ -29,7 +33,7 @@ public CommandExec(PeerBanHelperServer server) { this.resetCommands = server.getMainConfig().getStringList("banlist-invoker.command-exec.reset"); this.banCommands = server.getMainConfig().getStringList("banlist-invoker.command-exec.ban"); this.unbanCommands = server.getMainConfig().getStringList("banlist-invoker.command-exec.unban"); - log.info(Lang.BANLIST_INVOKER_REGISTERED, getClass().getName()); + log.info(tlUI(Lang.BANLIST_INVOKER_REGISTERED, getClass().getName())); } @@ -42,7 +46,7 @@ public void reset() { try { invokeCommand(c, Collections.emptyMap()); } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) { - log.warn(Lang.COMMAND_EXECUTOR_FAILED, e); + log.error(tlUI(Lang.COMMAND_EXECUTOR_FAILED), e); } } } @@ -60,7 +64,7 @@ public void add(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata) { try { invokeCommand(c, map); } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) { - log.warn(Lang.COMMAND_EXECUTOR_FAILED, e); + log.error(tlUI(Lang.COMMAND_EXECUTOR_FAILED), e); } } } @@ -78,7 +82,7 @@ public void remove(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata) try { invokeCommand(c, map); } catch (IOException | ExecutionException | InterruptedException | TimeoutException e) { - log.warn(Lang.COMMAND_EXECUTOR_FAILED, e); + log.error(tlUI(Lang.COMMAND_EXECUTOR_FAILED), e); } } } @@ -97,7 +101,7 @@ public int invokeCommand(String command, Map env) throws IOExcep Process process = p.onExit().get(10, TimeUnit.SECONDS); if (process.isAlive()) { process.destroy(); - log.warn(Lang.COMMAND_EXECUTOR_FAILED_TIMEOUT, command); + log.error(tlUI(Lang.COMMAND_EXECUTOR_FAILED_TIMEOUT), command); return -9999; } return process.exitValue(); @@ -108,7 +112,7 @@ private Map makeMap(@NotNull PeerAddress peer, @NotNull BanMetad argMap.put("peer.ip", peer.getIp()); argMap.put("peer.port", String.valueOf(peer.getPort())); argMap.put("meta.context", banMetadata.getContext()); - argMap.put("meta.description", banMetadata.getDescription()); + argMap.put("meta.description", tl(DEF_LOCALE, banMetadata.getDescription())); argMap.put("meta.banAt", String.valueOf(banMetadata.getBanAt())); argMap.put("meta.unbanAt", String.valueOf(banMetadata.getUnbanAt())); argMap.put("meta.peer.id", banMetadata.getPeer().getId()); diff --git a/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/IPFilterInvoker.java b/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/IPFilterInvoker.java index 0269537e8a..51193baef7 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/IPFilterInvoker.java +++ b/src/main/java/com/ghostchu/peerbanhelper/invoker/impl/IPFilterInvoker.java @@ -15,6 +15,8 @@ import java.io.IOException; import java.util.UUID; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class IPFilterInvoker implements BanListInvoker { private final PeerBanHelperServer server; @@ -35,7 +37,7 @@ public IPFilterInvoker(PeerBanHelperServer server) { } ipFilterDat.createNewFile(); } - log.info(Lang.BANLIST_INVOKER_REGISTERED, getClass().getName()); + log.info(tlUI(Lang.BANLIST_INVOKER_REGISTERED, getClass().getName())); } @Override @@ -47,7 +49,7 @@ public void reset() { fileWriter.write(""); fileWriter.flush(); } catch (IOException e) { - log.warn(Lang.BANLIST_INVOKER_IPFILTER_FAIL, e); + log.error(tlUI(Lang.BANLIST_INVOKER_IPFILTER_FAIL), e); } } @@ -60,7 +62,7 @@ public void add(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata) { fileWriter.write(generateIpFilterLine(peer) + "\n"); fileWriter.flush(); } catch (IOException e) { - log.warn(Lang.BANLIST_INVOKER_IPFILTER_FAIL, e); + log.error(tlUI(Lang.BANLIST_INVOKER_IPFILTER_FAIL), e); } } @@ -75,7 +77,7 @@ public void remove(@NotNull PeerAddress peer, @NotNull BanMetadata banMetadata) } fileWriter.flush(); } catch (IOException e) { - log.warn(Lang.BANLIST_INVOKER_IPFILTER_FAIL, e); + log.error(tlUI(Lang.BANLIST_INVOKER_IPFILTER_FAIL), e); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPDB.java b/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPDB.java index 62c49ff838..033af0b3ab 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPDB.java +++ b/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPDB.java @@ -2,14 +2,27 @@ import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.HTTPUtil; import com.github.mizosoft.methanol.Methanol; import com.github.mizosoft.methanol.MutableRequest; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.ice.tar.TarEntry; import com.ice.tar.TarInputStream; +import com.maxmind.db.MaxMindDbConstructor; +import com.maxmind.db.MaxMindDbParameter; +import com.maxmind.db.Reader; import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.model.AsnResponse; +import com.maxmind.geoip2.model.CityResponse; +import com.maxmind.geoip2.model.CountryResponse; +import com.maxmind.geoip2.record.City; +import com.maxmind.geoip2.record.Country; +import com.maxmind.geoip2.record.Location; import lombok.Cleanup; import lombok.Getter; +import lombok.ToString; import lombok.extern.slf4j.Slf4j; import java.io.*; @@ -25,27 +38,39 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.List; -import java.util.Locale; +import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import java.util.zip.GZIPInputStream; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class IPDB implements AutoCloseable { + private final Cache MINI_CACHE = CacheBuilder.newBuilder() + .maximumSize(1000) + .expireAfterAccess(5, TimeUnit.SECONDS) + .build(); private final File dataFolder; - private final long updateInterval = 86400000L; // 30天 + private final long updateInterval = 2592000000L; // 30天 private final String accountId; private final String licenseKey; private final File directory; private final File mmdbCityFile; private final File mmdbASNFile; private final boolean autoUpdate; + private final String userAgent; + private final File mmdbGeoCNFile; private Methanol httpClient; @Getter private DatabaseReader mmdbCity; @Getter private DatabaseReader mmdbASN; + private Reader geoCN; + private List languageTag; - public IPDB(File dataFolder, String accountId, String licenseKey, String databaseCity, String databaseASN, boolean autoUpdate) throws IllegalArgumentException, IOException { + public IPDB(File dataFolder, String accountId, String licenseKey, String databaseCity, String databaseASN, boolean autoUpdate, String userAgent) throws IllegalArgumentException, IOException { this.dataFolder = dataFolder; this.accountId = accountId; this.licenseKey = licenseKey; @@ -53,7 +78,9 @@ public IPDB(File dataFolder, String accountId, String licenseKey, String databas this.directory.mkdirs(); this.mmdbCityFile = new File(directory, "GeoIP-City.mmdb"); this.mmdbASNFile = new File(directory, "GeoIP-ASN.mmdb"); + this.mmdbGeoCNFile = new File(directory, "GeoCN.mmdb"); this.autoUpdate = autoUpdate; + this.userAgent = userAgent; setupHttpClient(); if (needUpdateMMDB(mmdbCityFile)) { updateMMDB(databaseCity, mmdbCityFile); @@ -61,18 +88,172 @@ public IPDB(File dataFolder, String accountId, String licenseKey, String databas if (needUpdateMMDB(mmdbASNFile)) { updateMMDB(databaseASN, mmdbASNFile); } + if (needUpdateMMDB(mmdbGeoCNFile)) { + updateGeoCN(mmdbGeoCNFile); + } loadMMDB(); } + public IPGeoData query(InetAddress address) { + try { + return MINI_CACHE.get(address, () -> { + IPGeoData geoData = new IPGeoData(); + geoData.setAs(queryAS(address)); + geoData.setCountry(queryCountry(address)); + geoData.setCity(queryCity(address)); + geoData.setNetwork(queryNetwork(address)); + if (geoData.getCountry() != null && geoData.getCountry().getIso() != null) { + String iso = geoData.getCountry().getIso(); + if (iso.equalsIgnoreCase("CN") || iso.equalsIgnoreCase("TW") + || iso.equalsIgnoreCase("HK") || iso.equalsIgnoreCase("MO")) { + queryGeoCN(address, geoData); + } + } + return geoData; + }); + } catch (ExecutionException e) { + return new IPGeoData(); + } + + } + + private void queryGeoCN(InetAddress address, IPGeoData geoData) { + try { + CNLookupResult cnLookupResult = geoCN.get(address, CNLookupResult.class); + if (cnLookupResult == null) { + return; + } + // City Data + IPGeoData.CityData cityResponse = Objects.requireNonNullElse(geoData.getCity(), new IPGeoData.CityData()); + String cityName = (cnLookupResult.getProvince() + " " + cnLookupResult.getCity() + " " + cnLookupResult.getDistricts()).trim(); + if (!cityName.isBlank()) { + cityResponse.setName(cityName); + } + Integer code = null; + if (cnLookupResult.getProvinceCode() != null) { + code = cnLookupResult.getProvinceCode().intValue(); + } + if (cnLookupResult.getCityCode() != null) { + code = cnLookupResult.getCityCode().intValue(); + } + if (cnLookupResult.getDistrictsCode() != null) { + code = cnLookupResult.getDistrictsCode().intValue(); + } + cityResponse.setIso(Long.parseLong("86" + code)); + geoData.setCity(cityResponse); + // Network Data + IPGeoData.NetworkData networkData = Objects.requireNonNullElse(geoData.getNetwork(), new IPGeoData.NetworkData()); + if (cnLookupResult.getIsp() != null && !cnLookupResult.getIsp().isBlank()) { + networkData.setIsp(cnLookupResult.getIsp()); + } + if (cnLookupResult.getNet() != null && !cnLookupResult.getNet().isBlank()) { + TranslationComponent component = new TranslationComponent(cnLookupResult.getNet()); + switch (cnLookupResult.getNet()) { + case "宽带" -> new TranslationComponent(Lang.NET_TYPE_WIDEBAND); + case "基站" -> new TranslationComponent(Lang.NET_TYPE_BASE_STATION); + case "政企专线" -> new TranslationComponent(Lang.NET_TYPE_GOVERNMENT_AND_ENTERPRISE_LINE); + case "业务平台" -> new TranslationComponent(Lang.NET_TYPE_BUSINESS_PLATFORM); + case "骨干网" -> new TranslationComponent(Lang.NET_TYPE_BACKBONE_NETWORK); + case "IP专网" -> new TranslationComponent(Lang.NET_TYPE_IP_PRIVATE_NETWORK); + case "网吧" -> new TranslationComponent(Lang.NET_TYPE_INTERNET_CAFE); + case "物联网" -> new TranslationComponent(Lang.NET_TYPE_IOT); + case "数据中心" -> new TranslationComponent(Lang.NET_TYPE_DATACENTER); + } + networkData.setNetType(tlUI(component)); + } + geoData.setNetwork(networkData); + } catch (Exception e) { + log.error("Unable to execute IPDB query", e); + } + } + + private IPGeoData.NetworkData queryNetwork(InetAddress address) { + IPGeoData.NetworkData networkData = new IPGeoData.NetworkData(); + try { + AsnResponse asnResponse = mmdbASN.asn(address); + networkData.setIsp(asnResponse.getAutonomousSystemOrganization()); + networkData.setNetType(null); + } catch (Exception ignored) { + } + return networkData; + } + + + private IPGeoData.CityData queryCity(InetAddress address) { + IPGeoData.CityData cityData = new IPGeoData.CityData(); + IPGeoData.CityData.LocationData locationData = new IPGeoData.CityData.LocationData(); + try { + CityResponse cityResponse = mmdbCity.city(address); + City city = cityResponse.getCity(); + Location location = cityResponse.getLocation(); + cityData.setName(city.getName()); + cityData.setIso(city.getGeoNameId()); + locationData.setTimeZone(location.getTimeZone()); + locationData.setLongitude(location.getLongitude()); + locationData.setLatitude(location.getLatitude()); + locationData.setAccuracyRadius(location.getAccuracyRadius()); + cityData.setLocation(locationData); + } catch (Exception e) { + } + return cityData; + } + + private IPGeoData.CountryData queryCountry(InetAddress address) { + IPGeoData.CountryData countryData = new IPGeoData.CountryData(); + try { + CountryResponse countryResponse = mmdbCity.country(address); + Country country = countryResponse.getCountry(); + countryData.setIso(country.getIsoCode()); + String countryRegionName = country.getName(); + // 对 TW,HK,MO 后处理,偷个懒 + if (languageTag.getFirst().equals("zh-CN") && (country.getIsoCode().equals("TW") || country.getIsoCode().equals("HK") || country.getIsoCode().equalsIgnoreCase("MO"))) { + countryRegionName = "中国" + countryRegionName; + } + countryData.setName(countryRegionName); + } catch (Exception ignored) { + } + return countryData; + } + + + private IPGeoData.ASData queryAS(InetAddress address) { + IPGeoData.ASData asData = new IPGeoData.ASData(); + try { + AsnResponse asnResponse = mmdbASN.asn(address); + IPGeoData.ASData.ASNetwork network = new IPGeoData.ASData.ASNetwork(); + network.setPrefixLength(asnResponse.getNetwork().getPrefixLength()); + network.setIpAddress(asnResponse.getNetwork().getNetworkAddress().getHostAddress()); + asData.setNumber(asnResponse.getAutonomousSystemNumber()); + asData.setOrganization(asnResponse.getAutonomousSystemOrganization()); + asData.setIpAddress(asnResponse.getIpAddress()); + asData.setNetwork(network); + } catch (Exception ignored) { + } + return asData; + } + + private void updateGeoCN(File mmdbGeoCNFile) throws IOException { + log.info(tlUI(Lang.IPDB_UPDATING, "GeoCN (github.com/ljxi/GeoCN)")); + MutableRequest request = MutableRequest.GET("https://github.com/ljxi/GeoCN/releases/download/Latest/GeoCN.mmdb"); + Path tmp = Files.createTempFile("GeoCN", ".mmdb"); + downloadFile(request, tmp, "GeoCN").join(); + if (!tmp.toFile().exists()) { + throw new IllegalStateException("Download mmdb database failed!"); + } + Files.move(tmp, mmdbGeoCNFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + private void loadMMDB() throws IOException { + this.languageTag = List.of(Main.DEF_LOCALE, "en"); this.mmdbCity = new DatabaseReader.Builder(mmdbCityFile) - .locales(List.of(Locale.getDefault().toLanguageTag(), "en")).build(); + .locales(List.of(Main.DEF_LOCALE, "en")).build(); this.mmdbASN = new DatabaseReader.Builder(mmdbASNFile) - .locales(List.of(Locale.getDefault().toLanguageTag(), "en")).build(); + .locales(List.of(Main.DEF_LOCALE, "en")).build(); + this.geoCN = new Reader(mmdbGeoCNFile); } private void updateMMDB(String databaseName, File target) throws IOException { - log.info(Lang.IPDB_UPDATING, databaseName); + log.info(tlUI(Lang.IPDB_UPDATING, databaseName)); MutableRequest request = MutableRequest.GET("https://download.maxmind.com/geoip/databases/" + databaseName + "/download?suffix=tar.gz"); Path tmp = Files.createTempFile("ipdb-mmdb-archive", ".tar.gz"); downloadFile(request, tmp, databaseName).join(); @@ -116,7 +297,8 @@ private void setupHttpClient() { .newBuilder() .version(HttpClient.Version.HTTP_1_1) .followRedirects(HttpClient.Redirect.ALWAYS) - .userAgent(Main.getUserAgent()) + .userAgent(userAgent) + .defaultHeader("Accept-Encoding", "gzip,deflate") .connectTimeout(Duration.of(15, ChronoUnit.SECONDS)) .headersTimeout(Duration.of(15, ChronoUnit.SECONDS)) .authenticator(new Authenticator() { @@ -139,18 +321,17 @@ private boolean needUpdateMMDB(File target) { return System.currentTimeMillis() - target.lastModified() > updateInterval; } - private CompletableFuture downloadFile(MutableRequest req, Path path, String databaseName) { return HTTPUtil.retryableSendProgressTracking(httpClient, req, HttpResponse.BodyHandlers.ofFile(path)) .thenAccept(r -> { if (r.statusCode() != 200) { - log.warn(Lang.IPDB_UPDATE_FAILED, databaseName, r.statusCode() + " - " + r.body()); + log.error(tlUI(Lang.IPDB_UPDATE_FAILED, databaseName, r.statusCode() + " - " + r.body())); } else { - log.info(Lang.IPDB_UPDATE_SUCCESS, databaseName); + log.info(tlUI(Lang.IPDB_UPDATE_SUCCESS, databaseName)); } }) .exceptionally(e -> { - log.warn(Lang.IPDB_UPDATE_FAILED, "Java Exception", e); + log.error(tlUI(Lang.IPDB_UPDATE_FAILED, "Java Exception"), e); File file = path.toFile(); if (file.exists()) { file.delete(); // 删除下载不完整的文件 @@ -159,7 +340,6 @@ private CompletableFuture downloadFile(MutableRequest req, Path path, Stri }); } - @Override public void close() { if (this.mmdbCity != null) { @@ -176,6 +356,47 @@ public void close() { } } + if (this.geoCN != null) { + try { + this.geoCN.close(); + } catch (IOException ignored) { + + } + } + } + + @Getter + @ToString + public static class CNLookupResult { + private final String isp; + private final String net; + private final String province; + private final Long provinceCode; + private final String city; + private final Long cityCode; + private final String districts; + private final Long districtsCode; + + @MaxMindDbConstructor + public CNLookupResult( + @MaxMindDbParameter(name = "isp") String isp, + @MaxMindDbParameter(name = "net") String net, + @MaxMindDbParameter(name = "province") String province, + @MaxMindDbParameter(name = "provinceCode") Object provinceCode, + @MaxMindDbParameter(name = "city") String city, + @MaxMindDbParameter(name = "cityCode") Object cityCode, + @MaxMindDbParameter(name = "districts") String districts, + @MaxMindDbParameter(name = "districtsCode") Object districtsCode + ) { + this.isp = isp; + this.net = net; + this.province = province; + this.provinceCode = Long.parseLong(provinceCode.toString()); + this.city = city; + this.cityCode = Long.parseLong(cityCode.toString()); + this.districts = districts; + this.districtsCode = Long.parseLong(districtsCode.toString()); + } } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPGeoData.java b/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPGeoData.java new file mode 100644 index 0000000000..05bfe5bb34 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/ipdb/IPGeoData.java @@ -0,0 +1,93 @@ +package com.ghostchu.peerbanhelper.ipdb; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.Nullable; + +@AllArgsConstructor +@NoArgsConstructor +@Data +public final class IPGeoData { + @Nullable + private CityData city; + @Nullable + private CountryData country; + @Nullable + private ASData as; + @Nullable + private NetworkData network; + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class CityData { + @Nullable + private String name; + + @Nullable + private Long iso; + + @Nullable + private LocationData location; + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class LocationData { + @Nullable + private Double latitude; + @Nullable + private Double longitude; + @Nullable + private String timeZone; + @Nullable + private Integer accuracyRadius; + } + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class CountryData { + @Nullable + private String name; + @Nullable + private String iso; + + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class ASData { + @Nullable + private Long number; + @Nullable + private String organization; + @Nullable + private String ipAddress; + @Nullable + private ASNetwork network; + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class ASNetwork { + @Nullable + private String ipAddress; + @Nullable + private Integer prefixLength; + } + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + public static class NetworkData { + @Nullable + private String isp; + @Nullable + private String netType; + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/log4j2/SwingLoggerAppender.java b/src/main/java/com/ghostchu/peerbanhelper/log4j2/SwingLoggerAppender.java index b65f0e87e0..ef09490cb3 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/log4j2/SwingLoggerAppender.java +++ b/src/main/java/com/ghostchu/peerbanhelper/log4j2/SwingLoggerAppender.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginElement; import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.slf4j.event.Level; import java.util.HashSet; import java.util.Set; @@ -53,9 +54,16 @@ public static void registerListener(Consumer listener) { @Override public void append(LogEvent event) { String message = new String(this.getLayout().toByteArray(event)); - listeners.forEach(c -> c.accept(new LoggerEvent(message, event, maxLinesSetting))); + Level lvl = switch (event.getLevel().getStandardLevel()) { + case FATAL, ERROR -> Level.ERROR; + case WARN -> Level.WARN; + case DEBUG -> Level.DEBUG; + case TRACE -> Level.TRACE; + default -> Level.INFO; + }; + listeners.forEach(c -> c.accept(new LoggerEvent(message.trim(), event, lvl, maxLinesSetting))); } - public record LoggerEvent(String message, LogEvent event, int maxLines) { + public record LoggerEvent(String message, LogEvent event, Level level, int maxLines) { } } \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/metric/impl/inmemory/InMemoryMetrics.java b/src/main/java/com/ghostchu/peerbanhelper/metric/impl/inmemory/InMemoryMetrics.java index 8a0913573c..671df38e0c 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/metric/impl/inmemory/InMemoryMetrics.java +++ b/src/main/java/com/ghostchu/peerbanhelper/metric/impl/inmemory/InMemoryMetrics.java @@ -3,8 +3,10 @@ import com.ghostchu.peerbanhelper.metric.BasicMetrics; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; +import org.springframework.stereotype.Component; // 简易记录,后续看情况添加 SQLite 数据库记录更详细的信息 +@Component("inMemoryMetrics") public class InMemoryMetrics implements BasicMetrics { private long checks = 0; private long bans = 0; diff --git a/src/main/java/com/ghostchu/peerbanhelper/metric/impl/persist/PersistMetrics.java b/src/main/java/com/ghostchu/peerbanhelper/metric/impl/persist/PersistMetrics.java index b659dd7d8c..70f2246957 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/metric/impl/persist/PersistMetrics.java +++ b/src/main/java/com/ghostchu/peerbanhelper/metric/impl/persist/PersistMetrics.java @@ -1,24 +1,40 @@ package com.ghostchu.peerbanhelper.metric.impl.persist; -import com.ghostchu.peerbanhelper.database.BanLog; -import com.ghostchu.peerbanhelper.database.DatabaseHelper; +import com.ghostchu.peerbanhelper.database.dao.impl.HistoryDao; +import com.ghostchu.peerbanhelper.database.dao.impl.ModuleDao; +import com.ghostchu.peerbanhelper.database.dao.impl.RuleDao; +import com.ghostchu.peerbanhelper.database.dao.impl.TorrentDao; +import com.ghostchu.peerbanhelper.database.table.HistoryEntity; +import com.ghostchu.peerbanhelper.database.table.ModuleEntity; +import com.ghostchu.peerbanhelper.database.table.RuleEntity; +import com.ghostchu.peerbanhelper.database.table.TorrentEntity; import com.ghostchu.peerbanhelper.metric.BasicMetrics; import com.ghostchu.peerbanhelper.metric.impl.inmemory.InMemoryMetrics; import com.ghostchu.peerbanhelper.text.Lang; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.sql.SQLException; +import java.sql.Timestamp; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; @Slf4j +@Component("persistMetrics") public class PersistMetrics implements BasicMetrics { - private final DatabaseHelper db; - private final BasicMetrics inMemory = new InMemoryMetrics(); - - public PersistMetrics(DatabaseHelper db) { - this.db = db; - } + @Autowired + private InMemoryMetrics inMemory; + @Autowired + private TorrentDao torrentDao; + @Autowired + private ModuleDao moduleDao; + @Autowired + private RuleDao ruleDao; + @Autowired + private HistoryDao historyDao; @Override public long getCheckCounter() { @@ -43,26 +59,44 @@ public void recordCheck() { @Override public void recordPeerBan(PeerAddress address, BanMetadata metadata) { inMemory.recordPeerBan(address, metadata); - try { - db.insertBanLogs(new BanLog( - metadata.getBanAt(), - metadata.getUnbanAt(), - address.getIp(), - address.getPort(), - metadata.getPeer().getId(), - metadata.getPeer().getClientName(), - metadata.getPeer().getUploaded(), - metadata.getPeer().getDownloaded(), - metadata.getPeer().getProgress(), - metadata.getTorrent().getHash(), - metadata.getTorrent().getName(), - metadata.getTorrent().getSize(), - metadata.getContext(), - metadata.getDescription() - )); - } catch (SQLException e) { - log.warn(Lang.DATABASE_SAVE_BUFFER_FAILED, e); - } + // 将数据库 IO 移动到虚拟线程上 + Thread.ofVirtual().start(() -> { + try { + TorrentEntity torrentEntity = torrentDao.createIfNotExists(new TorrentEntity( + null, + metadata.getTorrent().getHash(), + metadata.getTorrent().getName(), + metadata.getTorrent().getSize() + )); + ModuleEntity module = moduleDao.createIfNotExists(new ModuleEntity( + null, + metadata.getContext() + )); + RuleEntity rule = ruleDao.createIfNotExists(new RuleEntity( + null, + module, + metadata.getRule() + )); + historyDao.create(new HistoryEntity( + null, + new Timestamp(metadata.getBanAt()), + new Timestamp(metadata.getUnbanAt()), + address.getIp(), + address.getPort(), + metadata.getPeer().getId(), + metadata.getPeer().getClientName(), + metadata.getPeer().getUploaded(), + metadata.getPeer().getDownloaded(), + metadata.getPeer().getProgress(), + torrentEntity, + rule, + metadata.getDescription(), + metadata.getPeer().getFlags() == null ? null : metadata.getPeer().getFlags().toString() + )); + } catch (SQLException e) { + log.error(tlUI(Lang.DATABASE_SAVE_BUFFER_FAILED), e); + } + }); } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/AbstractFeatureModule.java b/src/main/java/com/ghostchu/peerbanhelper/module/AbstractFeatureModule.java index bb22f92e94..d15f879bcd 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/AbstractFeatureModule.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/AbstractFeatureModule.java @@ -3,38 +3,50 @@ import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; +import io.javalin.http.Context; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; @Slf4j +@Component public abstract class AbstractFeatureModule implements FeatureModule { - private final YamlConfiguration profile; @Getter - private final PeerBanHelperServer server; + @Autowired + private PeerBanHelperServer server; @Getter private boolean register; - - public AbstractFeatureModule(PeerBanHelperServer server, YamlConfiguration profile) { - this.server = server; - this.profile = profile; - } + @Getter + private final ReentrantLock lock = new ReentrantLock(); + @Autowired + private JavalinWebContainer javalinWebContainer; @Override public boolean isModuleEnabled() { try { return !isConfigurable() || getConfig().getBoolean("enabled"); } catch (Exception e) { - log.warn(Lang.CONFIGURATION_OUTDATED_MODULE_DISABLED, getName()); + log.warn(tlUI(Lang.CONFIGURATION_OUTDATED_MODULE_DISABLED, getName())); return false; } } + @Override + public ReentrantLock getThreadLock() { + return lock; + } + /** * 如果返回 false,则不初始化任何配置文件相关对象 * @@ -45,9 +57,9 @@ public boolean isModuleEnabled() { @Override public ConfigurationSection getConfig() { if (!isConfigurable()) return null; - ConfigurationSection section = Objects.requireNonNull(profile.getConfigurationSection("module")).getConfigurationSection(getConfigName()); + ConfigurationSection section = Objects.requireNonNull(server.getProfileConfig().getConfigurationSection("module")).getConfigurationSection(getConfigName()); if (section == null) { - log.warn(Lang.CONFIGURATION_OUTDATED_MODULE_DISABLED, getName()); + log.warn(tlUI(Lang.CONFIGURATION_OUTDATED_MODULE_DISABLED, getName())); YamlConfiguration configuration = new YamlConfiguration(); configuration.set("enabled", false); return configuration; @@ -60,7 +72,7 @@ public void disable() { if (register) { onDisable(); cleanupResources(); - log.info(Lang.MODULE_UNREGISTER, getName()); + log.info(tlUI(Lang.MODULE_UNREGISTER, getName())); } } @@ -72,13 +84,18 @@ private void cleanupResources() { public void enable() { register = true; onEnable(); - log.info(Lang.MODULE_REGISTER, getName()); + log.info(tlUI(Lang.MODULE_REGISTER, getName())); } @Override public void saveConfig() throws IOException { if (!isConfigurable()) return; - profile.set("module." + getConfigName(), getConfig()); - profile.save(new File(Main.getConfigDirectory(), "profile.yml")); + server.getProfileConfig().set("module." + getConfigName(), getConfig()); + server.getProfileConfig().save(new File(Main.getConfigDirectory(), "profile.yml")); } + + public String locale(Context ctx) { + return javalinWebContainer.reqLocale(ctx); + } + } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/AbstractRuleFeatureModule.java b/src/main/java/com/ghostchu/peerbanhelper/module/AbstractRuleFeatureModule.java index 960155a0b7..bfa1fcd619 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/AbstractRuleFeatureModule.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/AbstractRuleFeatureModule.java @@ -1,21 +1,29 @@ package com.ghostchu.peerbanhelper.module; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; +import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import com.ghostchu.peerbanhelper.util.rule.ModuleMatchCache; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; @Slf4j +@Component public abstract class AbstractRuleFeatureModule extends AbstractFeatureModule implements RuleFeatureModule { - @Getter - private final PeerBanHelperServer server; @Getter private boolean register; + public static final CheckResult TEAPOT_CHECK_RESULT = new CheckResult(AbstractRuleFeatureModule.class, PeerAction.NO_ACTION, 0, new TranslationComponent("N/A"), new TranslationComponent("I'm a teapot")); + public static final CheckResult OK_CHECK_RESULT = new CheckResult(AbstractRuleFeatureModule.class, PeerAction.NO_ACTION, 0, new TranslationComponent("N/A"), new TranslationComponent("Check passed")); + public static final CheckResult HANDSHAKING_CHECK_RESULT = new CheckResult(AbstractRuleFeatureModule.class, PeerAction.NO_ACTION, 0, new TranslationComponent("N/A"), new TranslationComponent("Peer handshaking")); + @Autowired + @Getter + private ModuleMatchCache cache; - public AbstractRuleFeatureModule(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - this.server = server; + public boolean isHandShaking(Peer peer) { + // 跳过此 Peer,速度都是0,可能是没有完成握手 + return peer.getDownloadSpeed() <= 0 && peer.getUploadSpeed() <= 0; } /** @@ -24,7 +32,18 @@ public AbstractRuleFeatureModule(PeerBanHelperServer server, YamlConfiguration p * @return 占位 BanResult */ @NotNull - protected BanResult teapot() { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "I'm a teapot"); + public CheckResult teapot() { + return TEAPOT_CHECK_RESULT; + } + + @NotNull + public CheckResult pass() { + return OK_CHECK_RESULT; } + + @NotNull + public CheckResult handshaking() { + return HANDSHAKING_CHECK_RESULT; + } + } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/BanResult.java b/src/main/java/com/ghostchu/peerbanhelper/module/BanResult.java deleted file mode 100644 index 9d4762877c..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/module/BanResult.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.ghostchu.peerbanhelper.module; - -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public record BanResult(@Nullable FeatureModule moduleContext, @NotNull PeerAction action, @NotNull String rule, - @NotNull String reason) { -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/CheckResult.java b/src/main/java/com/ghostchu/peerbanhelper/module/CheckResult.java new file mode 100644 index 0000000000..377c8f5852 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/module/CheckResult.java @@ -0,0 +1,13 @@ +package com.ghostchu.peerbanhelper.module; + +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import org.jetbrains.annotations.NotNull; + +public record CheckResult( + @NotNull Class moduleContext, + @NotNull PeerAction action, + long duration, + @NotNull TranslationComponent rule, + @NotNull TranslationComponent reason +) { +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/FeatureModule.java b/src/main/java/com/ghostchu/peerbanhelper/module/FeatureModule.java index 1d74682359..bb03162e22 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/FeatureModule.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/FeatureModule.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; +import java.util.concurrent.locks.ReentrantLock; public interface FeatureModule { /** @@ -26,6 +27,8 @@ public interface FeatureModule { ConfigurationSection getConfig(); + ReentrantLock getThreadLock(); + /** * 功能模块启用回调 */ diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/ModuleManager.java b/src/main/java/com/ghostchu/peerbanhelper/module/ModuleManager.java index 07630079ac..5655fff603 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/ModuleManager.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/ModuleManager.java @@ -1,22 +1,41 @@ package com.ghostchu.peerbanhelper.module; -import com.google.common.collect.ImmutableList; +import com.ghostchu.peerbanhelper.Main; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; @Slf4j +@Component public class ModuleManager { private final List modules = new ArrayList<>(); + /** + * 注册一个功能模块并启用它 + * + * @param moduleClass 功能模块 + */ + public void register(@NotNull Class moduleClass) { + Main.registerBean(moduleClass, null); + FeatureModule module = Main.getApplicationContext().getBean(moduleClass); + if (module.isModuleEnabled()) { + synchronized (modules) { + this.modules.add(module); + module.enable(); + } + } + } + /** * 注册一个功能模块并启用它 * * @param module 功能模块 */ - public void register(@NotNull FeatureModule module) { + public void register(@NotNull FeatureModule module, String beanName) { + Main.registerBean(module.getClass(), beanName); if (module.isModuleEnabled()) { synchronized (modules) { this.modules.add(module); @@ -59,7 +78,7 @@ public void unregister(@NotNull Class module) { * 解注册所有的功能模块 */ public void unregisterAll() { - ImmutableList.copyOf(this.modules).forEach(this::unregister); + List.copyOf(this.modules).forEach(this::unregister); } /** @@ -69,7 +88,7 @@ public void unregisterAll() { */ @NotNull public List getModules() { - return ImmutableList.copyOf(modules); + return List.copyOf(modules); } /** diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/RuleFeatureModule.java b/src/main/java/com/ghostchu/peerbanhelper/module/RuleFeatureModule.java index da5713b4bc..36ef6e0221 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/RuleFeatureModule.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/RuleFeatureModule.java @@ -7,20 +7,6 @@ import java.util.concurrent.ExecutorService; public interface RuleFeatureModule extends FeatureModule { - /** - * 模块检查结果是否可被缓存 - * - * @return 可否被缓存 - */ - boolean isCheckCacheable(); - - /** - * 模块是否需要首先进行握手检查 - * - * @return 是否先进行握手检查 - */ - boolean needCheckHandshake(); - /** * 检查一个特定的 Torrent 和 Peer 是否应该封禁 * @@ -30,5 +16,14 @@ public interface RuleFeatureModule extends FeatureModule { * @return 规则检查结果 */ @NotNull - BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor); + CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor); + + /** + * 指示模块的内部处理逻辑是否是线程安全的,如果线程不安全,PeerBanHelper 将在同步块中执行不安全的模块 + * 以避免出现线程安全错误 + * @return 是否是线程安全模块 + */ + default boolean isThreadSafe() { + return false; + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/AutoRangeBan.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/AutoRangeBan.java index b8e250a40b..4ec60519af 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/AutoRangeBan.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/AutoRangeBan.java @@ -2,35 +2,36 @@ import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; -import com.ghostchu.peerbanhelper.util.IPAddressUtil; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import inet.ipaddr.IPAddress; import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; -import java.util.Collections; import java.util.Map; -import java.util.WeakHashMap; import java.util.concurrent.ExecutorService; @Slf4j +@Component public class AutoRangeBan extends AbstractRuleFeatureModule { + @Autowired + private PeerBanHelperServer peerBanHelperServer; private int ipv4Prefix; private int ipv6Prefix; - private final Map banListMappingCache = Collections.synchronizedMap(new WeakHashMap<>()); - - public AutoRangeBan(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + private long banDuration; @Override public @NotNull String getName() { @@ -42,11 +43,6 @@ public AutoRangeBan(PeerBanHelperServer server, YamlConfiguration profile) { return "auto-range-ban"; } - @Override - public boolean needCheckHandshake() { - return true; - } - @Override public boolean isConfigurable() { return true; @@ -55,10 +51,15 @@ public boolean isConfigurable() { @Override public void onEnable() { reloadConfig(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/modules/" + getConfigName(), this::handleWebAPI, Role.USER_READ); } + @Override + public boolean isThreadSafe() { + return super.isThreadSafe(); + } + private void handleWebAPI(Context ctx) { ctx.status(HttpStatus.OK); ctx.json(Map.of("ipv4-prefix", ipv4Prefix, "ipv6-prefix", ipv6Prefix)); @@ -72,37 +73,40 @@ public void onDisable() { private void reloadConfig() { this.ipv4Prefix = getConfig().getInt("ipv4"); this.ipv6Prefix = getConfig().getInt("ipv6"); - banListMappingCache.clear(); - } - - @Override - public boolean isCheckCacheable() { - return false; + this.banDuration = getConfig().getLong("ban-duration", 0); } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { - if (peer.getPeerId() == null || peer.getPeerId().isEmpty()) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "Waiting for Bittorrent handshaking."); + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + if (isHandShaking(peer)) { + return pass(); } IPAddress peerAddress = peer.getPeerAddress().getAddress().withoutPrefixLength(); + if (peerAddress.isIPv4Convertible()) { + peerAddress = peerAddress.toIPv4(); + } for (PeerAddress bannedPeer : getServer().getBannedPeers().keySet()) { - IPAddress bannedPeerAddress = banListMappingCache.get(bannedPeer); - if (bannedPeerAddress == null) { - IPAddress bannedAddress = bannedPeer.getAddress(); - if (bannedPeer.getAddress().isIPv4()) { - bannedAddress = IPAddressUtil.toPrefixBlock(bannedAddress, ipv4Prefix); - } else { - bannedAddress = IPAddressUtil.toPrefixBlock(bannedAddress, ipv6Prefix); - } - bannedPeerAddress = bannedAddress; - banListMappingCache.put(bannedPeer, bannedPeerAddress); + IPAddress bannedAddress = bannedPeer.getAddress().withoutPrefixLength(); + if (bannedAddress.isIPv4Convertible()) { + bannedAddress = bannedAddress.toIPv4(); + } + if (peerAddress.isIPv4() != bannedAddress.isIPv4()) { + continue; + } + String addressType = "UNKNOWN"; + if (bannedAddress.isIPv4()) { + addressType = "IPv4/" + ipv4Prefix; + bannedAddress = bannedAddress.toPrefixBlock(ipv4Prefix); + } + if (bannedAddress.isIPv6()) { + addressType = "IPv6/" + ipv6Prefix; + bannedAddress = bannedAddress.toPrefixBlock(ipv6Prefix); } - if (bannedPeerAddress.contains(peerAddress)) { - return new BanResult(this, PeerAction.BAN, bannedPeerAddress.toString(), String.format(Lang.ARB_BANNED, peerAddress, bannedPeer.getAddress())); + if (bannedAddress.contains(peerAddress)) { + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(addressType), new TranslationComponent(Lang.ARB_BANNED, peerAddress.toString(), bannedPeer.getAddress().toString(), addressType)); } } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "All ok!"); + return pass(); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/BtnNetworkOnline.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/BtnNetworkOnline.java index 0e1f553a60..35451431c0 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/BtnNetworkOnline.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/BtnNetworkOnline.java @@ -1,32 +1,36 @@ package com.ghostchu.peerbanhelper.module.impl.rule; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.btn.BtnNetwork; import com.ghostchu.peerbanhelper.btn.BtnRuleParsed; import com.ghostchu.peerbanhelper.btn.ability.BtnAbilityRules; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.NullUtil; import com.ghostchu.peerbanhelper.util.rule.Rule; import com.ghostchu.peerbanhelper.util.rule.RuleMatchResult; import com.ghostchu.peerbanhelper.util.rule.RuleParser; import inet.ipaddr.IPAddress; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.List; import java.util.concurrent.ExecutorService; +@Slf4j +@Component public class BtnNetworkOnline extends AbstractRuleFeatureModule { - - public BtnNetworkOnline(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + private final CheckResult BTN_MANAGER_NOT_INITIALIZED = new CheckResult(getClass(), PeerAction.NO_ACTION, 0, new TranslationComponent(Lang.GENERAL_NA), new TranslationComponent("BtnManager not initialized")); + @Autowired(required = false) + private BtnNetwork manager; + private long banDuration; @Override public @NotNull String getName() { @@ -38,16 +42,6 @@ public BtnNetworkOnline(PeerBanHelperServer server, YamlConfiguration profile) { return "btn"; } - @Override - public boolean isCheckCacheable() { - return true; - } - - @Override - public boolean needCheckHandshake() { - return true; - } - @Override public boolean isConfigurable() { return true; @@ -64,78 +58,87 @@ public void onDisable() { } public void reloadConfig() { + this.banDuration = getConfig().getLong("ban-duration", 0); + } + @Override + public boolean isThreadSafe() { + return true; } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { - BtnNetwork manager = getServer().getBtnNetwork(); + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { if (manager == null) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "BtnManager not initialized"); + return BTN_MANAGER_NOT_INITIALIZED; } BtnAbilityRules ruleAbility = (BtnAbilityRules) manager.getAbilities().get(BtnAbilityRules.class); if (ruleAbility == null) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "BtnRulesAbility not get ready yet"); + return pass(); } BtnRuleParsed rule = ruleAbility.getBtnRule(); if (rule == null) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "BtnRules not get ready yet"); - } - BanResult result = null; - if (rule.getPeerIdRules() != null) { - result = NullUtil.anyNotNull(result, checkPeerIdRule(rule, torrent, peer, ruleExecuteExecutor)); - } - if (rule.getClientNameRules() != null) { - result = NullUtil.anyNotNull(result, checkClientNameRule(rule, torrent, peer, ruleExecuteExecutor)); - } - if (rule.getIpRules() != null) { - result = NullUtil.anyNotNull(result, checkIpRule(rule, torrent, peer, ruleExecuteExecutor)); - } - if (rule.getPortRules() != null) { - result = NullUtil.anyNotNull(result, checkPortRule(rule, torrent, peer, ruleExecuteExecutor)); + return pass(); } - if (result == null) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "OK!"); + if (isHandShaking(peer)) { + return handshaking(); } - return result; + return getCache().readCacheButWritePassOnly(this, "peer-" + peer.getCacheKey(), () -> { + CheckResult r = null; + if (rule.getPeerIdRules() != null) { + r = NullUtil.anyNotNull(r, checkPeerIdRule(rule, torrent, peer, ruleExecuteExecutor)); + } + if (rule.getClientNameRules() != null) { + r = NullUtil.anyNotNull(r, checkClientNameRule(rule, torrent, peer, ruleExecuteExecutor)); + } + if (rule.getIpRules() != null) { + r = NullUtil.anyNotNull(r, checkIpRule(rule, torrent, peer, ruleExecuteExecutor)); + } + if (rule.getPortRules() != null) { + r = NullUtil.anyNotNull(r, checkPortRule(rule, torrent, peer, ruleExecuteExecutor)); + } + if (r == null) { + return pass(); + } + return r; + }, true); } - private BanResult checkPortRule(BtnRuleParsed rule, Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { + private CheckResult checkPortRule(BtnRuleParsed rule, Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { for (String category : rule.getPortRules().keySet()) { RuleMatchResult matchResult = RuleParser.matchRule(rule.getPortRules().get(category), peer.getPeerId()); if (matchResult.hit()) { - return new BanResult(this, PeerAction.BAN, "BTN-" + category + "-" + matchResult.rule(), String.format(Lang.MODULE_BTN_BAN, "Port", category, matchResult.rule())); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.BTN_BTN_RULE, category, matchResult.rule().matcherIdentifier()), new TranslationComponent(Lang.MODULE_BTN_BAN, "Port", category, matchResult.rule().toString())); } } return null; } @Nullable - private BanResult checkClientNameRule(BtnRuleParsed rule, Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { + private CheckResult checkClientNameRule(BtnRuleParsed rule, Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { for (String category : rule.getClientNameRules().keySet()) { List rules = rule.getClientNameRules().get(category); RuleMatchResult matchResult = RuleParser.matchRule(rules, peer.getClientName()); if (matchResult.hit()) { - return new BanResult(this, PeerAction.BAN, "BTN-" + category + "-" + matchResult.rule(), String.format(Lang.MODULE_BTN_BAN, "ClientName", category, matchResult.rule())); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.BTN_BTN_RULE, category, matchResult.rule().matcherIdentifier()), new TranslationComponent(Lang.MODULE_BTN_BAN, "ClientName", category, matchResult.rule().toString())); } } return null; } @Nullable - private BanResult checkPeerIdRule(BtnRuleParsed rule, Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { + private CheckResult checkPeerIdRule(BtnRuleParsed rule, Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { for (String category : rule.getPeerIdRules().keySet()) { List rules = rule.getPeerIdRules().get(category); RuleMatchResult matchResult = RuleParser.matchRule(rules, peer.getClientName()); if (matchResult.hit()) { - return new BanResult(this, PeerAction.BAN, "BTN-" + category + "-" + matchResult.rule(), String.format(Lang.MODULE_BTN_BAN, "PeerId", category, matchResult.rule())); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.BTN_BTN_RULE, category, matchResult.rule().matcherIdentifier()), new TranslationComponent(Lang.MODULE_BTN_BAN, "PeerId", category, matchResult.rule().toString())); } } return null; } @Nullable - private BanResult checkIpRule(BtnRuleParsed rule, @NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + private CheckResult checkIpRule(BtnRuleParsed rule, @NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { IPAddress pa = peer.getPeerAddress().getAddress(); if (pa == null) return null; if (pa.isIPv4Convertible()) { @@ -144,7 +147,7 @@ private BanResult checkIpRule(BtnRuleParsed rule, @NotNull Torrent torrent, @Not for (String category : rule.getIpRules().keySet()) { RuleMatchResult matchResult = RuleParser.matchRule(rule.getIpRules().get(category), pa.toString()); if (matchResult.hit()) { - return new BanResult(this, PeerAction.BAN, "BTN-" + category + "-" + matchResult.rule(), String.format(Lang.MODULE_BTN_BAN, "IP", category, pa)); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.BTN_BTN_RULE, category, matchResult.rule().matcherIdentifier()), new TranslationComponent(Lang.MODULE_BTN_BAN, "IP", category, pa.toString())); } } return null; diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ClientNameBlacklist.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ClientNameBlacklist.java index d3635c71e5..e95cfb1594 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ClientNameBlacklist.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ClientNameBlacklist.java @@ -1,33 +1,35 @@ package com.ghostchu.peerbanhelper.module.impl.rule; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.rule.Rule; import com.ghostchu.peerbanhelper.util.rule.RuleMatchResult; import com.ghostchu.peerbanhelper.util.rule.RuleParser; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.Getter; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @Getter +@Component public class ClientNameBlacklist extends AbstractRuleFeatureModule { private List bannedPeers; - - public ClientNameBlacklist(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + private long banDuration; @Override public @NotNull String getName() { @@ -39,16 +41,6 @@ public ClientNameBlacklist(PeerBanHelperServer server, YamlConfiguration profile return "client-name-blacklist"; } - @Override - public boolean isCheckCacheable() { - return true; - } - - @Override - public boolean needCheckHandshake() { - return true; - } - @Override public boolean isConfigurable() { return true; @@ -57,13 +49,19 @@ public boolean isConfigurable() { @Override public void onEnable() { reloadConfig(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/modules/" + getConfigName(), this::handleWebAPI, Role.USER_READ); } + @Override + public boolean isThreadSafe() { + return true; + } + private void handleWebAPI(Context ctx) { ctx.status(HttpStatus.OK); - ctx.json(Map.of("clientName", bannedPeers.stream().map(Rule::toPrintableText).toList())); + String locale = locale(ctx); + ctx.json(Map.of("clientName", bannedPeers.stream().map(r -> r.toPrintableText(locale)).toList())); } @Override @@ -73,15 +71,21 @@ public void onDisable() { private void reloadConfig() { this.bannedPeers = RuleParser.parse(getConfig().getStringList("banned-client-name")); + this.banDuration = getConfig().getLong("ban-duration", 0); } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + if (isHandShaking(peer) && (peer.getClientName() == null || peer.getClientName().isBlank())) { + return handshaking(); + } + //return getCache().readCache(this, peer.getClientName(), () -> { RuleMatchResult matchResult = RuleParser.matchRule(bannedPeers, peer.getClientName()); if (matchResult.hit()) { - return new BanResult(this, PeerAction.BAN, matchResult.rule().toString(), String.format(Lang.MODULE_CNB_MATCH_CLIENT_NAME, matchResult.rule())); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(matchResult.rule().toString()), new TranslationComponent(Lang.MODULE_CNB_MATCH_CLIENT_NAME, String.valueOf(matchResult.rule()))); } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "No matches"); + return pass(); + //}, true); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ExpressionRule.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ExpressionRule.java index 9826d626b3..f492757f7b 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ExpressionRule.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ExpressionRule.java @@ -3,20 +3,17 @@ import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.HTTPUtil; import com.ghostchu.peerbanhelper.util.IPAddressUtil; import com.ghostchu.peerbanhelper.util.JsonUtil; import com.ghostchu.peerbanhelper.util.StrUtil; import com.ghostchu.peerbanhelper.util.time.InfoHashUtil; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableMap; -import com.google.common.io.ByteStreams; import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.EvalMode; import com.googlecode.aviator.Expression; @@ -26,64 +23,52 @@ import com.googlecode.aviator.runtime.JavaMethodReflectionFunctionMissing; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.math.MathContext; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; + +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; @Slf4j +@Component public class ExpressionRule extends AbstractRuleFeatureModule { private final long maxScriptExecuteTime = 1500; + private final Map threadLocks = new HashMap<>(); private Map expressions = new HashMap<>(); - private Cache>> cacheMap = CacheBuilder.newBuilder() - .expireAfterAccess(3, TimeUnit.MINUTES) - .maximumSize(2000) - .softValues() - .build(); - - public ExpressionRule(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + private long banDuration; @Override public boolean isConfigurable() { return true; } - @Override - public boolean isCheckCacheable() { - return false; - } - - @Override - public boolean needCheckHandshake() { - return true; - } - - @Nullable - public BanResult handleResult(Expression expression, Object returns) { + public CheckResult handleResult(Expression expression, Object returns) { + ExpressionMetadata meta = expressions.get(expression); if (returns instanceof Boolean status) { if (status) { - return new BanResult(this, PeerAction.BAN, "User Rule", expressions.get(expression).name()); + + return new CheckResult(getClass(), PeerAction.BAN, banDuration, + new TranslationComponent(Lang.USER_SCRIPT_RULE), + new TranslationComponent(Lang.USER_SCRIPT_RUN_RESULT, meta.name(), "true")); } return null; } @@ -92,24 +77,48 @@ public BanResult handleResult(Expression expression, Object returns) { if (i == 0) { return null; } else if (i == 1) { - return new BanResult(this, PeerAction.BAN, "User Rule", expressions.get(expression).name()); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, + new TranslationComponent(Lang.USER_SCRIPT_RULE), + new TranslationComponent(Lang.USER_SCRIPT_RUN_RESULT, meta.name(), String.valueOf(number))); } else if (i == 2) { - return new BanResult(this, PeerAction.SKIP, "User Rule", expressions.get(expression).name()); + return new CheckResult(getClass(), PeerAction.SKIP, banDuration, + new TranslationComponent(Lang.USER_SCRIPT_RULE), + new TranslationComponent(Lang.USER_SCRIPT_RUN_RESULT, meta.name(), String.valueOf(number))); } else { - log.warn(Lang.MODULE_EXPRESSION_RULE_INVALID_RETURNS, expressions.get(expression)); + log.error(tlUI(Lang.MODULE_EXPRESSION_RULE_INVALID_RETURNS, meta)); return null; } } if (returns instanceof PeerAction action) { - return new BanResult(this, action, "User Rule", expressions.get(expression).name()); + return new CheckResult(getClass(), action, banDuration, + new TranslationComponent(Lang.USER_SCRIPT_RULE), + new TranslationComponent(Lang.USER_SCRIPT_RUN_RESULT, meta.name(), action.name())); + } + if (returns instanceof String string) { + if (string.isBlank()) { + return pass(); + } else if (string.startsWith("@")) { + return new CheckResult(getClass(), PeerAction.SKIP, banDuration, + new TranslationComponent(Lang.USER_SCRIPT_RULE), + new TranslationComponent(string.substring(1))); + } else { + return new CheckResult(getClass(), PeerAction.BAN, banDuration, + new TranslationComponent(Lang.USER_SCRIPT_RULE), + new TranslationComponent(Lang.USER_SCRIPT_RUN_RESULT, meta.name(), string)); + } } - if (returns instanceof BanResult banResult) { - return banResult; + if (returns instanceof CheckResult checkResult) { + return checkResult; } - log.warn(Lang.MODULE_EXPRESSION_RULE_INVALID_RETURNS, expressions.get(expression)); + log.error(tlUI(Lang.MODULE_EXPRESSION_RULE_INVALID_RETURNS, meta)); return null; } + @Override + public boolean isThreadSafe() { + return true; + } + @Override public @NotNull String getName() { return "Expression Engine"; @@ -120,7 +129,6 @@ public BanResult handleResult(Expression expression, Object returns) { return "expression-engine"; } - private void registerFunctions(Class clazz) { try { AviatorEvaluator.addInstanceFunctions(clazz.getSimpleName(), clazz); @@ -131,42 +139,62 @@ private void registerFunctions(Class clazz) { @SneakyThrows @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { - String cacheKey = peer.getCacheKey(); + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + AtomicReference checkResult = new AtomicReference<>(pass()); + try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) { + for (Expression expression : expressions.keySet()) { + exec.submit(() -> { + CheckResult expressionRun = runExpression(expression, torrent, peer, ruleExecuteExecutor); + if (expressionRun.action() == PeerAction.SKIP) { + checkResult.set(expressionRun); // 提前退出 + return; + } + if (expressionRun.action() == PeerAction.BAN) { + if (checkResult.get().action() != PeerAction.SKIP) { + checkResult.set(expressionRun); + } + } + }); + } + } + return checkResult.get(); + } - for (Expression expression : expressions.keySet()) { - ExpressionMetadata expressionMetadata = expressions.get(expression); - Map> cached = cacheMap.get(cacheKey, HashMap::new); - BanResult result; - if (cached.containsKey(expressionMetadata.name())) { - result = cached.get(expressionMetadata.name()).orElse(null); - } else { - try { - Map env = new HashMap<>(); - env.put("torrent", torrent); - env.put("peer", peer); - env.put("cacheable", new AtomicBoolean(false)); - Object returns = expression.execute(env); - result = handleResult(expression, returns); - if (((AtomicBoolean) env.get("cacheable")).get()) { - cached.put(expressionMetadata.name(), Optional.ofNullable(result)); + public CheckResult runExpression(Expression expression, @NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + ExpressionMetadata expressionMetadata = expressions.get(expression); + return getCache().readCacheButWritePassOnly(this, expression.hashCode() + peer.getCacheKey(), () -> { + CheckResult result; + try { + Map env = expression.newEnv(); + env.put("torrent", torrent); + env.put("peer", peer); + env.put("cacheable", new AtomicBoolean(false)); + Object returns; + if (expressionMetadata.threadSafe()) { + returns = expression.execute(env); + } else { + ReentrantLock lock = threadLocks.get(expressionMetadata); + lock.lock(); + try { + returns = expression.execute(env); + } finally { + lock.unlock(); } - } catch (TimeoutException timeoutException) { - log.warn(Lang.MODULE_EXPRESSION_RULE_TIMEOUT, maxScriptExecuteTime, timeoutException); - continue; - } catch (Exception ex) { - log.warn(Lang.MODULE_EXPRESSION_RULE_ERROR, expressionMetadata.name(), ex); - continue; } - } - if (cached.isEmpty()) { - cacheMap.invalidate(cacheKey); + result = handleResult(expression, returns); + } catch (TimeoutException timeoutException) { + log.error(tlUI(Lang.MODULE_EXPRESSION_RULE_TIMEOUT, maxScriptExecuteTime), timeoutException); + return pass(); + } catch (Exception ex) { + log.error(tlUI(Lang.MODULE_EXPRESSION_RULE_ERROR, expressionMetadata.name()), ex); + return pass(); } if (result != null && result.action() != PeerAction.NO_ACTION) { return result; + } else { + return pass(); } - } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "All ok!"); + }, expressionMetadata.cacheable()); } @Override @@ -178,17 +206,16 @@ public void onEnable() { // EVAL 性能优先 AviatorEvaluator.getInstance().setOption(Options.OPTIMIZE_LEVEL, AviatorEvaluator.EVAL); // 降低浮点计算精度 - AviatorEvaluator.getInstance().setOption(Options.MATH_CONTEXT, MathContext.DECIMAL128); + AviatorEvaluator.getInstance().setOption(Options.MATH_CONTEXT, MathContext.DECIMAL32); // 启用变量语法糖 AviatorEvaluator.getInstance().setOption(Options.ENABLE_PROPERTY_SYNTAX_SUGAR, true); - // 表达式允许序列化和反序列化 - AviatorEvaluator.getInstance().setOption(Options.SERIALIZABLE, true); +// // 表达式允许序列化和反序列化 +// AviatorEvaluator.getInstance().setOption(Options.SERIALIZABLE, true); // 用户规则写糊保护 AviatorEvaluator.getInstance().setOption(Options.MAX_LOOP_COUNT, 5000); AviatorEvaluator.getInstance().setOption(Options.EVAL_TIMEOUT_MS, maxScriptExecuteTime); // 启用反射方法查找 AviatorEvaluator.getInstance().setFunctionMissing(JavaMethodReflectionFunctionMissing.getInstance()); - // 注册反射调用 registerFunctions(IPAddressUtil.class); registerFunctions(HTTPUtil.class); @@ -206,11 +233,12 @@ public void onEnable() { } } - private void reloadConfig() throws IOException, URISyntaxException { + private void reloadConfig() throws IOException { expressions.clear(); + threadLocks.clear(); + this.banDuration = getConfig().getLong("ban-duration", 0); initScripts(); - log.info(Lang.MODULE_EXPRESSION_RULE_COMPILING); - ConfigurationSection section = getConfig().getConfigurationSection("rules"); + log.info(tlUI(Lang.MODULE_EXPRESSION_RULE_COMPILING)); long start = System.currentTimeMillis(); Map userRules = new ConcurrentHashMap<>(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { @@ -226,27 +254,30 @@ private void reloadConfig() throws IOException, URISyntaxException { executor.submit(() -> { try { AviatorEvaluator.getInstance().validate(expressionMetadata.script()); - Expression expression = AviatorEvaluator.getInstance().compile(expressionMetadata.script()); + Expression expression = AviatorEvaluator.getInstance().compile(expressionMetadata.script(), false); expression.newEnv("peerbanhelper", getServer(), "moduleConfig", getConfig(), "ipdb", getServer().getIpdb()); userRules.put(expression, expressionMetadata); + if (!expressionMetadata.threadSafe()) { + threadLocks.put(expressionMetadata, new ReentrantLock()); + } } catch (ExpressionSyntaxErrorException err) { - log.warn(Lang.MODULE_EXPRESSION_RULE_BAD_EXPRESSION, err); + log.error(tlUI(Lang.MODULE_EXPRESSION_RULE_BAD_EXPRESSION), err); } }); } } - - } - expressions = ImmutableMap.copyOf(userRules); - log.info(Lang.MODULE_EXPRESSION_RULE_COMPILED, expressions.size(), System.currentTimeMillis() - start); + expressions = Map.copyOf(userRules); + log.info(tlUI(Lang.MODULE_EXPRESSION_RULE_COMPILED, expressions.size(), System.currentTimeMillis() - start)); } private ExpressionMetadata parseScriptMetadata(String fallbackName, String scriptContent) { try (BufferedReader reader = new BufferedReader(new StringReader(scriptContent))) { String name = fallbackName; String author = "Unknown"; + String version = "null"; boolean cacheable = true; + boolean threadSafe = true; while (true) { String line = reader.readLine(); if (line == null) { @@ -260,28 +291,32 @@ private ExpressionMetadata parseScriptMetadata(String fallbackName, String scrip author = line.substring(7).trim(); } else if (line.startsWith("@CACHEABLE")) { cacheable = Boolean.parseBoolean(line.substring(10).trim()); + } else if (line.startsWith("@VERSION")) { + version = line.substring(8).trim(); + } else if (line.startsWith("@THREADSAFE")) { + threadSafe = Boolean.parseBoolean(line.substring(11).trim()); } } } - return new ExpressionMetadata(name, author, cacheable, scriptContent); + return new ExpressionMetadata(name, author, cacheable, threadSafe, version, scriptContent); } catch (IOException e) { - return new ExpressionMetadata("Failed to parse name", "Unknown", true, scriptContent); + return new ExpressionMetadata("Failed to parse name", "Unknown", true, true, "null", scriptContent); } } - private void initScripts() throws IOException, URISyntaxException { + private void initScripts() throws IOException { File scriptDir = new File(Main.getDataDirectory(), "scripts"); if (scriptDir.exists()) { return; } scriptDir.mkdirs(); - for (String s : List.of("name-id-verify.av", "thunder-check.av", "peer-ids.av.example")) { - try (var is = Main.class.getResourceAsStream("/scripts/" + s)) { - String content = new String(ByteStreams.toByteArray(is), StandardCharsets.UTF_8); - File file = new File(scriptDir, s); - file.createNewFile(); - Files.writeString(file.toPath(), content); - } + PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(Main.class.getClassLoader()); + var res = resourcePatternResolver.getResources("classpath:scripts/**/*.*"); + for (Resource re : res) { + String content = new String(re.getContentAsByteArray(), StandardCharsets.UTF_8); + File file = new File(scriptDir, re.getFilename()); + file.createNewFile(); + Files.writeString(file.toPath(), content); } } @@ -290,6 +325,7 @@ public void onDisable() { } - record ExpressionMetadata(String name, String author, boolean cacheable, String script) { + record ExpressionMetadata(String name, String author, boolean cacheable, boolean threadSafe, String version, + String script) { } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackList.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackList.java index ea416faf2c..780480f834 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackList.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackList.java @@ -1,23 +1,24 @@ package com.ghostchu.peerbanhelper.module.impl.rule; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.IPAddressUtil; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import inet.ipaddr.Address; import inet.ipaddr.IPAddress; import io.javalin.http.Context; import io.javalin.http.HttpStatus; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -25,17 +26,18 @@ import java.util.Map; import java.util.concurrent.ExecutorService; -@Getter +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j +@Component public class IPBlackList extends AbstractRuleFeatureModule { private List ips; private List ports; private List asns; private List regions; - - public IPBlackList(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + private long banDuration; @Override public @NotNull String getName() { @@ -47,16 +49,6 @@ public IPBlackList(PeerBanHelperServer server, YamlConfiguration profile) { return "ip-address-blocker"; } - @Override - public boolean isCheckCacheable() { - return true; - } - - @Override - public boolean needCheckHandshake() { - return false; - } - @Override public boolean isConfigurable() { return true; @@ -65,10 +57,15 @@ public boolean isConfigurable() { @Override public void onEnable() { reloadConfig(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/modules/" + getConfigName(), this::handleWebAPI, Role.USER_READ); } + @Override + public boolean isThreadSafe() { + return true; + } + private void handleWebAPI(Context ctx) { Map map = new LinkedHashMap<>(); map.put("ip", ips.stream().map(Address::toString).toList()); @@ -85,6 +82,7 @@ public void onDisable() { } private void reloadConfig() { + this.banDuration = getConfig().getLong("ban-duration", 0); this.ips = new ArrayList<>(); for (String s : getConfig().getStringList("ips")) { IPAddress ipAddress = IPAddressUtil.getIPAddress(s); @@ -97,45 +95,51 @@ private void reloadConfig() { } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { - PeerAddress peerAddress = peer.getPeerAddress(); - if (ports.contains(peerAddress.getPort())) { - return new BanResult(this, PeerAction.BAN, String.valueOf(peerAddress.getPort()), String.format(Lang.MODULE_IBL_MATCH_PORT, peerAddress.getPort())); - } - IPAddress pa = IPAddressUtil.getIPAddress(peerAddress.getIp()); - for (IPAddress ra : ips) { - if (ra.equals(pa) || ra.contains(pa)) { - return new BanResult(this, PeerAction.BAN, ra.toString(), String.format(Lang.MODULE_IBL_MATCH_IP, ra)); + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + return getCache().readCacheButWritePassOnly(this, peer.getPeerAddress().getIp(), () -> { + PeerAddress peerAddress = peer.getPeerAddress(); + if (ports.contains(peerAddress.getPort())) { + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.IP_BLACKLIST_PORT_RULE), new TranslationComponent(Lang.MODULE_IBL_MATCH_PORT, String.valueOf(peerAddress.getPort()))); } - } - try { - BanResult ipdbResult = checkIPDB(torrent, peer, ruleExecuteExecutor); - if (ipdbResult.action() != PeerAction.NO_ACTION) { - return ipdbResult; + IPAddress pa = IPAddressUtil.getIPAddress(peerAddress.getIp()); + for (IPAddress ra : ips) { + if (ra.equals(pa) || ra.contains(pa)) { + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.IP_BLACKLIST_CIDR_RULE, ra.toString()), new TranslationComponent(Lang.MODULE_IBL_MATCH_IP, ra.toString())); + } } - } catch (Exception e) { - log.error(Lang.MODULE_IBL_EXCEPTION_GEOIP, e); - } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "No matches"); + try { + CheckResult ipdbResult = checkIPDB(torrent, peer, ruleExecuteExecutor); + if (ipdbResult.action() != PeerAction.NO_ACTION) { + return ipdbResult; + } + } catch (Exception e) { + log.error(tlUI(Lang.MODULE_IBL_EXCEPTION_GEOIP), e); + } + return pass(); + }, true); } - private BanResult checkIPDB(Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { + private CheckResult checkIPDB(Torrent torrent, Peer peer, ExecutorService ruleExecuteExecutor) { if (regions.isEmpty() && asns.isEmpty()) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "No feature enabled"); + return pass(); + } + var geoData = getServer().queryIPDB(peer.getPeerAddress()).geoData().get(); + if (geoData == null) { + return pass(); } - if (!asns.isEmpty()) { - long asn = getServer().queryIPDB(peer.getPeerAddress()).asnResponse().get().getAutonomousSystemNumber(); + if (!asns.isEmpty() && geoData.getAs() != null) { + Long asn = geoData.getAs().getNumber(); if (asns.contains(asn)) { - return new BanResult(this, PeerAction.BAN, String.valueOf(asn), String.format(Lang.MODULE_IBL_MATCH_ASN, asn)); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.IP_BLACKLIST_ASN_RULE, String.valueOf(asn)), new TranslationComponent(Lang.MODULE_IBL_MATCH_ASN, String.valueOf(asn))); } } - if (!regions.isEmpty()) { - String iso = getServer().queryIPDB(peer.getPeerAddress()).cityResponse().get().getCountry().getIsoCode(); + if (!regions.isEmpty() && geoData.getCountry() != null) { + String iso = geoData.getCountry().getIso(); if (regions.contains(iso)) { - return new BanResult(this, PeerAction.BAN, String.valueOf(iso), String.format(Lang.MODULE_IBL_MATCH_REGION, iso)); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.IP_BLACKLIST_REGION_RULE, iso), new TranslationComponent(Lang.MODULE_IBL_MATCH_REGION, iso)); } } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "N/A"); + return pass(); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackRuleList.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackRuleList.java index 29f02606ae..65e1993c04 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackRuleList.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/IPBlackRuleList.java @@ -1,17 +1,17 @@ package com.ghostchu.peerbanhelper.module.impl.rule; import com.ghostchu.peerbanhelper.Main; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; -import com.ghostchu.peerbanhelper.database.DatabaseHelper; -import com.ghostchu.peerbanhelper.database.RuleSubInfo; -import com.ghostchu.peerbanhelper.database.RuleSubLog; +import com.ghostchu.peerbanhelper.database.dao.impl.RuleSubLogsDao; +import com.ghostchu.peerbanhelper.database.table.RuleSubInfoEntity; +import com.ghostchu.peerbanhelper.database.table.RuleSubLogEntity; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.IPBanRuleUpdateType; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.module.impl.webapi.common.SlimMsg; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.HTTPUtil; import com.ghostchu.peerbanhelper.util.IPAddressUtil; @@ -25,8 +25,8 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; import java.io.File; import java.io.IOException; @@ -44,21 +44,26 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import static com.ghostchu.peerbanhelper.Main.DEF_LOCALE; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + /** * IP黑名单远程订阅模块 */ -@Getter @Slf4j +@Component +@Getter public class IPBlackRuleList extends AbstractRuleFeatureModule { - - final private DatabaseHelper db; + private final RuleSubLogsDao ruleSubLogsDao; private List ipBanMatchers; private long checkInterval = 86400000; // 默认24小时检查一次 private ScheduledExecutorService scheduledExecutorService; + private long banDuration; - public IPBlackRuleList(PeerBanHelperServer server, YamlConfiguration profile, DatabaseHelper db) { - super(server, profile); - this.db = db; + public IPBlackRuleList(RuleSubLogsDao ruleSubLogsDao) { + super(); + this.ruleSubLogsDao = ruleSubLogsDao; } @Override @@ -66,16 +71,6 @@ public boolean isConfigurable() { return true; } - @Override - public boolean isCheckCacheable() { - return true; - } - - @Override - public boolean needCheckHandshake() { - return false; - } - @Override public @NotNull String getName() { return "IP Blacklist Rule List"; @@ -91,7 +86,7 @@ public void onEnable() { ConfigurationSection config = getConfig(); // 读取检查间隔 checkInterval = config.getLong("check-interval", checkInterval); - scheduledExecutorService = Executors.newScheduledThreadPool(1); + scheduledExecutorService = Executors.newScheduledThreadPool(1, Thread.ofVirtual().factory()); scheduledExecutorService.scheduleAtFixedRate(this::reloadConfig, 0, checkInterval, TimeUnit.MILLISECONDS); } @@ -100,40 +95,44 @@ public void onDisable() { } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { - long t1 = System.currentTimeMillis(); - String ip = peer.getPeerAddress().getIp(); - List results = new ArrayList<>(); - try (var service = Executors.newVirtualThreadPerTaskExecutor()) { - ipBanMatchers.forEach(rule -> service.submit(() -> { - results.add(new IPBanResult(rule.getRuleName(), rule.match(ip))); - })); - } - AtomicReference matchRule = new AtomicReference<>(); - boolean mr = results.stream().anyMatch(ipBanResult -> { - try { - boolean match = ipBanResult.matchResult() == MatchResult.TRUE; - if (match) { - matchRule.set(ipBanResult); + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + return getCache().readCacheButWritePassOnly(this, peer.getPeerAddress().getIp(), () -> { + long t1 = System.currentTimeMillis(); + String ip = peer.getPeerAddress().getIp(); + List results = new ArrayList<>(); + try (var service = Executors.newVirtualThreadPerTaskExecutor()) { + ipBanMatchers.forEach(rule -> service.submit(() -> { + results.add(new IPBanResult(rule.getRuleName(), rule.match(ip))); + })); + } + AtomicReference matchRule = new AtomicReference<>(); + boolean mr = results.stream().anyMatch(ipBanResult -> { + try { + if (ipBanResult == null) return false; + boolean match = ipBanResult.matchResult() == MatchResult.TRUE; + if (match) { + matchRule.set(ipBanResult); + } + return match; + } catch (Exception e) { + log.error(tlUI(Lang.IP_BAN_RULE_MATCH_ERROR), e); + return false; } - return match; - } catch (Exception e) { - log.error(Lang.IP_BAN_RULE_MATCH_ERROR, e); - return false; + }); + long t2 = System.currentTimeMillis(); + log.debug(tlUI(Lang.IP_BAN_RULE_MATCH_TIME, t2 - t1)); + if (mr) { + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(matchRule.get().ruleName()), new TranslationComponent(Lang.MODULE_IBL_MATCH_IP_RULE, matchRule.get().ruleName(), ip)); } - }); - long t2 = System.currentTimeMillis(); - log.debug(Lang.IP_BAN_RULE_MATCH_TIME, t2 - t1); - if (mr) { - return new BanResult(this, PeerAction.BAN, ip, String.format(Lang.MODULE_IBL_MATCH_IP_RULE, matchRule.get().ruleName())); - } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "No matches"); + return pass(); + }, true); } /** * Reload the configuration for this module. */ private void reloadConfig() { + this.banDuration = getConfig().getLong("ban-duration", 0); if (null == ipBanMatchers) { ipBanMatchers = new ArrayList<>(); } @@ -146,9 +145,9 @@ private void reloadConfig() { for (String ruleId : rules.getKeys(false)) { ConfigurationSection rule = rules.getConfigurationSection(ruleId); assert rule != null; - updateRule(rule, IPBanRuleUpdateType.AUTO); + updateRule(DEF_LOCALE, rule, IPBanRuleUpdateType.AUTO); } - log.info(Lang.IP_BAN_RULE_UPDATE_FINISH); + log.info(tlUI(Lang.IP_BAN_RULE_UPDATE_FINISH)); } } @@ -157,14 +156,14 @@ private void reloadConfig() { * * @param rule 规则 */ - public SlimMsg updateRule(@NotNull ConfigurationSection rule, IPBanRuleUpdateType updateType) { + public SlimMsg updateRule(String locale, @NotNull ConfigurationSection rule, IPBanRuleUpdateType updateType) { AtomicReference result = new AtomicReference<>(); String ruleId = rule.getName(); if (!rule.getBoolean("enabled", false)) { // 检查ipBanMatchers是否有对应的规则,有则删除 ipBanMatchers.removeIf(ele -> ele.getRuleId().equals(ruleId)); // 未启用跳过更新逻辑 - return new SlimMsg(false, Lang.IP_BAN_RULE_DISABLED.replace("{}", ruleId), 400); + return new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_DISABLED, ruleId), 400); } String name = rule.getString("name", ruleId); String url = rule.getString("url"); @@ -186,18 +185,18 @@ public SlimMsg updateRule(@NotNull ConfigurationSection rule, IPBanRuleUpdateTyp try { fileToIPList(ruleFile, ipAddresses); ipBanMatchers.add(new IPMatcher(ruleId, name, ipAddresses)); - log.warn(Lang.IP_BAN_RULE_USE_CACHE, name); - result.set(new SlimMsg(false, Lang.IP_BAN_RULE_USE_CACHE.replace("{}", name), 500)); + log.warn(tlUI(Lang.IP_BAN_RULE_USE_CACHE, name)); + result.set(new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_USE_CACHE, name), 500)); } catch (IOException ex) { - log.error(Lang.IP_BAN_RULE_LOAD_FAILED, name, ex); - result.set(new SlimMsg(false, Lang.IP_BAN_RULE_LOAD_FAILED.replace("{}", name), 500)); + log.error(tlUI(Lang.IP_BAN_RULE_LOAD_FAILED, name), ex); + result.set(new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_LOAD_FAILED, name), 500)); } } else { - result.set(new SlimMsg(false, Lang.IP_BAN_RULE_UPDATE_FAILED.replace("{}", name), 500)); + result.set(new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_UPDATE_FAILED, name), 500)); } } else { // log.error(Lang.IP_BAN_RULE_LOAD_FAILED, name, throwable); - result.set(new SlimMsg(false, Lang.IP_BAN_RULE_LOAD_FAILED.replace("{}", name), 500)); + result.set(new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_LOAD_FAILED, name), 500)); } throw new RuntimeException(throwable); } @@ -219,8 +218,8 @@ public SlimMsg updateRule(@NotNull ConfigurationSection rule, IPBanRuleUpdateTyp if (ipBanMatchers.stream().noneMatch(ele -> ele.getRuleId().equals(ruleId))) { ent_count = fileToIPList(tempFile, ipAddresses); } else { - log.info(Lang.IP_BAN_RULE_NO_UPDATE, name); - result.set(new SlimMsg(true, Lang.IP_BAN_RULE_NO_UPDATE.replace("{}", name), 200)); + log.info(tlUI(Lang.IP_BAN_RULE_NO_UPDATE, name)); + result.set(new SlimMsg(true, tl(locale, Lang.IP_BAN_RULE_NO_UPDATE, name), 200)); } tempFile.delete(); } @@ -228,30 +227,30 @@ public SlimMsg updateRule(@NotNull ConfigurationSection rule, IPBanRuleUpdateTyp // 如果已经存在则更新,否则添加 ipBanMatchers.stream().filter(ele -> ele.getRuleId().equals(ruleId)).findFirst().ifPresentOrElse(ele -> { ele.setData(name, ipAddresses); - log.info(Lang.IP_BAN_RULE_UPDATE_SUCCESS, name); - result.set(new SlimMsg(true, Lang.IP_BAN_RULE_UPDATE_SUCCESS.replace("{}", name), 200)); + log.info(tlUI(Lang.IP_BAN_RULE_UPDATE_SUCCESS, name)); + result.set(new SlimMsg(true, tl(locale, Lang.IP_BAN_RULE_UPDATE_SUCCESS, name), 200)); }, () -> { ipBanMatchers.add(new IPMatcher(ruleId, name, ipAddresses)); - log.info(Lang.IP_BAN_RULE_LOAD_SUCCESS, name); - result.set(new SlimMsg(true, Lang.IP_BAN_RULE_LOAD_SUCCESS.replace("{}", name), 200)); + log.info(tlUI(Lang.IP_BAN_RULE_LOAD_SUCCESS, name)); + result.set(new SlimMsg(true, tl(locale, Lang.IP_BAN_RULE_LOAD_SUCCESS, name), 200)); }); // 更新日志 try { - db.insertRuleSubLog(ruleId, ent_count, updateType); - result.set(new SlimMsg(true, Lang.IP_BAN_RULE_UPDATED.replace("{}", name), 200)); + ruleSubLogsDao.create(new RuleSubLogEntity(null, ruleId, System.currentTimeMillis(), ent_count, updateType)); + result.set(new SlimMsg(true, tl(locale, Lang.IP_BAN_RULE_UPDATED, name), 200)); } catch (SQLException e) { - log.error(Lang.IP_BAN_RULE_UPDATE_LOG_ERROR, ruleId, e); - result.set(new SlimMsg(false, Lang.IP_BAN_RULE_UPDATE_LOG_ERROR.replace("{}", name), 500)); + log.error(tlUI(Lang.IP_BAN_RULE_UPDATE_LOG_ERROR, ruleId), e); + result.set(new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_UPDATE_LOG_ERROR, name), 500)); } } else { - result.set(new SlimMsg(true, Lang.IP_BAN_RULE_NO_UPDATE.replace("{}", name), 200)); + result.set(new SlimMsg(true, tl(locale, Lang.IP_BAN_RULE_NO_UPDATE, name), 200)); } } catch (IOException e) { throw new RuntimeException(e); } }).join(); } else { - result.set(new SlimMsg(false, Lang.IP_BAN_RULE_URL_WRONG.replace("{}", name), 400)); + result.set(new SlimMsg(false, tl(locale, Lang.IP_BAN_RULE_URL_WRONG, name), 400)); } return result.get(); } @@ -265,9 +264,15 @@ public SlimMsg updateRule(@NotNull ConfigurationSection rule, IPBanRuleUpdateTyp */ private int fileToIPList(File ruleFile, List ips) throws IOException { AtomicInteger count = new AtomicInteger(); - Files.readLines(ruleFile, StandardCharsets.UTF_8).forEach(ele -> { + Files.readLines(ruleFile, StandardCharsets.UTF_8).stream().filter(s -> !s.isBlank()).forEach(ele -> { + if (ele.startsWith("#")) { + return; // 注释 + } count.getAndIncrement(); - ips.add(IPAddressUtil.getIPAddress(ele)); + var ip = IPAddressUtil.getIPAddress(ele); + if (ip != null) { + ips.add(IPAddressUtil.getIPAddress(ele)); + } }); return count.get(); } @@ -287,7 +292,7 @@ public ConfigurationSection getRuleSubsConfig() { * @param ruleId 规则ID * @return 规则订阅信息 */ - public RuleSubInfo getRuleSubInfo(String ruleId) throws SQLException { + public RuleSubInfoEntity getRuleSubInfo(String ruleId) throws SQLException { ConfigurationSection rules = getRuleSubsConfig(); if (rules == null) { return null; @@ -296,10 +301,11 @@ public RuleSubInfo getRuleSubInfo(String ruleId) throws SQLException { if (rule == null) { return null; } - Optional first = db.queryRuleSubLogs(ruleId, 0, 1).stream().findFirst(); - long lastUpdate = first.map(RuleSubLog::updateTime).orElse(0L); - int count = first.map(RuleSubLog::count).orElse(0); - return new RuleSubInfo(ruleId, rule.getBoolean("enabled", false), rule.getString("name", ruleId), rule.getString("url"), lastUpdate, count); + + Optional first = ruleSubLogsDao.queryByPaging(ruleSubLogsDao.queryBuilder().orderBy("id", false).where().eq("ruleId", ruleId).queryBuilder(), 0, 1).stream().findFirst(); + long lastUpdate = first.map(RuleSubLogEntity::getUpdateTime).orElse(0L); + int count = first.map(RuleSubLogEntity::getCount).orElse(0); + return new RuleSubInfoEntity(ruleId, rule.getBoolean("enabled", false), rule.getString("name", ruleId), rule.getString("url"), lastUpdate, count); } /** @@ -309,12 +315,12 @@ public RuleSubInfo getRuleSubInfo(String ruleId) throws SQLException { * @return 保存后的规则订阅信息 * @throws IOException 保存异常 */ - public ConfigurationSection saveRuleSubInfo(@NotNull RuleSubInfo ruleSubInfo) throws IOException { + public ConfigurationSection saveRuleSubInfo(@NotNull RuleSubInfoEntity ruleSubInfo) throws IOException { ConfigurationSection rules = getRuleSubsConfig(); - String ruleId = ruleSubInfo.ruleId(); - rules.set(ruleId + ".enabled", ruleSubInfo.enabled()); - rules.set(ruleId + ".name", ruleSubInfo.ruleName()); - rules.set(ruleId + ".url", ruleSubInfo.subUrl()); + String ruleId = ruleSubInfo.getRuleId(); + rules.set(ruleId + ".enabled", ruleSubInfo.isEnabled()); + rules.set(ruleId + ".name", ruleSubInfo.getRuleName()); + rules.set(ruleId + ".url", ruleSubInfo.getSubUrl()); saveConfig(); return rules.getConfigurationSection(ruleId); } @@ -340,8 +346,12 @@ public void deleteRuleSubInfo(String ruleId) throws IOException { * @return 规则订阅日志 * @throws SQLException 查询异常 */ - public List queryRuleSubLogs(String ruleId, int pageIndex, int pageSize) throws SQLException { - return db.queryRuleSubLogs(ruleId, pageIndex, pageSize); + public List queryRuleSubLogs(String ruleId, int pageIndex, int pageSize) throws SQLException { + var builder = ruleSubLogsDao.queryBuilder(); + if (ruleId != null) { + builder = builder.where().eq("ruleId", ruleId).queryBuilder(); + } + return ruleSubLogsDao.queryByPaging(builder, pageIndex, pageSize); } /** @@ -351,8 +361,12 @@ public List queryRuleSubLogs(String ruleId, int pageIndex, int pageS * @return 规则订阅日志数量 * @throws SQLException 查询异常 */ - public int countRuleSubLogs(String ruleId) throws SQLException { - return db.countRuleSubLogs(ruleId); + public long countRuleSubLogs(String ruleId) throws SQLException { + var builder = ruleSubLogsDao.queryBuilder(); + if (ruleId != null) { + builder = builder.where().eq("ruleId", ruleId).queryBuilder(); + } + return ruleSubLogsDao.countOf(builder.setCountOf(true).prepare()); } /** diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java index 1e2f69a72b..1ea6cfa008 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/MultiDialingBlocker.java @@ -1,13 +1,14 @@ package com.ghostchu.peerbanhelper.module.impl.rule; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.IPAddressUtil; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -15,10 +16,11 @@ import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -28,6 +30,7 @@ * 同一网段集中下载同一个种子视为多拨,因为多拨和PCDN强相关所以可以直接封禁 */ @Slf4j +@Component public class MultiDialingBlocker extends AbstractRuleFeatureModule { // 计算缓存容量 private static final int TORRENT_PEER_MAX_NUM = 1024; @@ -39,24 +42,23 @@ public class MultiDialingBlocker extends AbstractRuleFeatureModule { private long cacheLifespan; private boolean keepHunting; private long keepHuntingTime; - - public MultiDialingBlocker(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + private long banDuration; @Override public void onEnable() { reloadConfig(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/modules/" + getConfigName(), this::handleConfig, Role.USER_READ) .get("/api/modules/" + getConfigName() + "/status", this::handleStatus, Role.USER_READ); } private void handleStatus(Context ctx) { - Map status = new LinkedHashMap<>(); + Map status = new HashMap<>(); status.put("huntingList", huntingList.asMap()); status.put("cache", cache.asMap()); - Map> mapSubnetCounter = new LinkedHashMap<>(); + Map> mapSubnetCounter = new HashMap<>(); subnetCounter.asMap().forEach((k, v) -> mapSubnetCounter.put(k, v.asMap())); status.put("subnetCounter", mapSubnetCounter); ctx.status(HttpStatus.OK); @@ -64,7 +66,7 @@ private void handleStatus(Context ctx) { } private void handleConfig(Context ctx) { - Map config = new LinkedHashMap<>(); + Map config = new HashMap<>(); config.put("subnetMaskLength", subnetMaskLength); config.put("subnetMaskV6Length", subnetMaskV6Length); config.put("tolerateNum", tolerateNum); @@ -85,16 +87,6 @@ private void handleConfig(Context ctx) { return "multi-dialing-blocker"; } - @Override - public boolean isCheckCacheable() { - return false; - } - - @Override - public boolean needCheckHandshake() { - return true; - } - @Override public boolean isConfigurable() { return true; @@ -106,6 +98,7 @@ public void onDisable() { } private void reloadConfig() { + this.banDuration = getConfig().getLong("ban-duration", 0); subnetMaskLength = getConfig().getInt("subnet-mask-length"); subnetMaskV6Length = getConfig().getInt("subnet-mask-v6-length"); tolerateNum = getConfig().getInt("tolerate-num"); @@ -133,8 +126,11 @@ private void reloadConfig() { } @Override - public @NotNull BanResult shouldBanPeer( + public @NotNull CheckResult shouldBanPeer( @NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + if (isHandShaking(peer)) { + return handshaking(); + } String torrentName = torrent.getName(); String torrentId = torrent.getId(); IPAddress peerAddress = peer.getPeerAddress().getAddress(); @@ -154,9 +150,8 @@ private void reloadConfig() { // 落库 huntingList.put(torrentSubnetStr, currentTimestamp); // 返回当前IP即可,其他IP会在下一周期被封禁 - return new BanResult(this, PeerAction.BAN, "Multi-dialing download detected", - String.format(Lang.MODULE_MDB_MULTI_DIALING_DETECTED, - peerSubnet, peerIpStr)); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.MDB_MULTI_DIALING_DETECTED), + new TranslationComponent(Lang.MODULE_MDB_MULTI_DIALING_DETECTED, peerSubnet.toString(), peerIpStr)); } if (keepHunting) { @@ -167,9 +162,8 @@ private void reloadConfig() { if (currentTimestamp - huntingTimestamp < keepHuntingTime) { // 落库 huntingList.put(torrentSubnetStr, currentTimestamp); - return new BanResult(this, PeerAction.BAN, "Multi-dialing hunting", - String.format(Lang.MODULE_MDB_MULTI_DIALING_HUNTING_TRIGGERED, - peerSubnet, peerIpStr)); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.MDB_MULTI_HUNTING), + new TranslationComponent(Lang.MODULE_MDB_MULTI_DIALING_HUNTING_TRIGGERED, peerSubnet.toString(), peerIpStr)); } else { huntingList.invalidate(torrentSubnetStr); } @@ -181,8 +175,7 @@ private void reloadConfig() { log.error("shouldBanPeer exception", e); } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", - String.format(Lang.MODULE_MDB_MULTI_DIALING_NOT_DETECTED, torrentName)); + return pass(); } // 是否已从数据库恢复追猎名单,持久化用的,目前没用 diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/PeerIdBlacklist.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/PeerIdBlacklist.java index 7dacea57f1..eac0358a64 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/PeerIdBlacklist.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/PeerIdBlacklist.java @@ -1,33 +1,33 @@ package com.ghostchu.peerbanhelper.module.impl.rule; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import com.ghostchu.peerbanhelper.util.rule.Rule; import com.ghostchu.peerbanhelper.util.rule.RuleMatchResult; import com.ghostchu.peerbanhelper.util.rule.RuleParser; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; import io.javalin.http.HttpStatus; -import lombok.Getter; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; -@Getter +@Component public class PeerIdBlacklist extends AbstractRuleFeatureModule { private List bannedPeers; - - public PeerIdBlacklist(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + private long banDuration; @Override public @NotNull String getName() { @@ -39,32 +39,29 @@ public PeerIdBlacklist(PeerBanHelperServer server, YamlConfiguration profile) { return "peer-id-blacklist"; } - @Override - public boolean needCheckHandshake() { - return true; - } @Override public boolean isConfigurable() { return true; } - @Override - public boolean isCheckCacheable() { - return true; - } - @Override public void onEnable() { reloadConfig(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/modules/" + getConfigName(), this::handleWebAPI, Role.USER_READ); } + @Override + public boolean isThreadSafe() { + return true; + } + private void handleWebAPI(Context ctx) { ctx.status(HttpStatus.OK); - ctx.json(Map.of("peerId", bannedPeers.stream().map(Rule::toPrintableText).toList())); + String locale = locale(ctx); + ctx.json(Map.of("peerId", bannedPeers.stream().map(r -> r.toPrintableText(locale)).toList())); } @Override @@ -73,17 +70,24 @@ public void onDisable() { } public void reloadConfig() { + this.banDuration = getConfig().getLong("ban-duration", 0); this.bannedPeers = RuleParser.parse(getConfig().getStringList("banned-peer-id")); } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + if (isHandShaking(peer) && (peer.getPeerId() == null || peer.getPeerId().isBlank())) { + return handshaking(); + } + //return getCache().readCache(this, peer.getPeerId(), () -> { RuleMatchResult matchResult = RuleParser.matchRule(bannedPeers, peer.getPeerId()); if (matchResult.hit()) { - return new BanResult(this, PeerAction.BAN, matchResult.rule().toString(), String.format(Lang.MODULE_PID_MATCH_PEER_ID, matchResult.rule())); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(matchResult.rule().toString()), new TranslationComponent(Lang.MODULE_PID_MATCH_PEER_ID, matchResult.rule().toString())); } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "No matches"); + return pass(); + //}, true); + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java index 3b4da19501..210ec82b78 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/rule/ProgressCheatBlocker.java @@ -1,12 +1,15 @@ package com.ghostchu.peerbanhelper.module.impl.rule; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; -import com.ghostchu.peerbanhelper.module.BanResult; +import com.ghostchu.peerbanhelper.module.CheckResult; import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; +import com.ghostchu.peerbanhelper.util.IPAddressUtil; +import com.ghostchu.peerbanhelper.util.MsgUtil; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -15,16 +18,18 @@ import io.javalin.http.HttpStatus; import lombok.AllArgsConstructor; import lombok.Data; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; -import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; +@Component public class ProgressCheatBlocker extends AbstractRuleFeatureModule { private Cache> progressRecorder; private long torrentMinimumSize; @@ -34,10 +39,9 @@ public class ProgressCheatBlocker extends AbstractRuleFeatureModule { private double rewindMaximumDifference; private int ipv4PrefixLength; private int ipv6PrefixLength; - - public ProgressCheatBlocker(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + private long banDuration; @Override public @NotNull String getName() { @@ -49,15 +53,10 @@ public ProgressCheatBlocker(PeerBanHelperServer server, YamlConfiguration profil return "progress-cheat-blocker"; } - @Override - public boolean isCheckCacheable() { - return false; - } - @Override public void onEnable() { reloadConfig(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/modules/" + getConfigName(), this::handleConfig, Role.USER_READ) .get("/api/modules/" + getConfigName() + "/status", this::handleStatus, Role.USER_READ); } @@ -83,7 +82,7 @@ public void handleConfig(Context ctx) { } @Override - public boolean needCheckHandshake() { + public boolean isThreadSafe() { return true; } @@ -99,8 +98,9 @@ public void onDisable() { } private void reloadConfig() { + this.banDuration = getConfig().getLong("ban-duration", 0); this.progressRecorder = CacheBuilder.newBuilder() - .maximumSize(512) + .maximumSize(2048) .expireAfterWrite(getServer().getBanDuration(), TimeUnit.MILLISECONDS) .softValues() .build(); @@ -114,18 +114,21 @@ private void reloadConfig() { } @Override - public @NotNull BanResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + public @NotNull CheckResult shouldBanPeer(@NotNull Torrent torrent, @NotNull Peer peer, @NotNull ExecutorService ruleExecuteExecutor) { + if (isHandShaking(peer)) { + return handshaking(); + } // 处理 IPV6 IPAddress peerIp; if (peer.getPeerAddress().getAddress().isIPv4()) { - peerIp = peer.getPeerAddress().getAddress().toPrefixBlock(ipv4PrefixLength); + peerIp = IPAddressUtil.toPrefixBlock(peer.getPeerAddress().getAddress(), ipv4PrefixLength); } else { - peerIp = peer.getPeerAddress().getAddress().toPrefixBlock(ipv6PrefixLength); + peerIp = IPAddressUtil.toPrefixBlock(peer.getPeerAddress().getAddress(), ipv6PrefixLength); } String peerIpString = peerIp.toString(); // 从缓存取数据 List lastRecordedProgress = progressRecorder.getIfPresent(peerIpString); - if (lastRecordedProgress == null) lastRecordedProgress = new ArrayList<>(); + if (lastRecordedProgress == null) lastRecordedProgress = new CopyOnWriteArrayList<>(); ClientTask clientTask = null; for (ClientTask recordedProgress : lastRecordedProgress) { if (recordedProgress.getTorrentId().equals(torrent.getId())) { @@ -134,13 +137,12 @@ private void reloadConfig() { } } if (clientTask == null) { - clientTask = new ClientTask(torrent.getId(), 0d, 0L, 0L); + clientTask = new ClientTask(torrent.getId(), 0d, 0L, 0L, 0, 0); lastRecordedProgress.add(clientTask); } long uploadedIncremental; // 上传增量 if (peer.getUploaded() < clientTask.getLastReportUploaded()) { uploadedIncremental = peer.getUploaded(); - ; } else { uploadedIncremental = peer.getUploaded() - clientTask.getLastReportUploaded(); } @@ -152,10 +154,10 @@ private void reloadConfig() { final long torrentSize = torrent.getSize(); // 过滤 if (torrentSize <= 0) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", Lang.MODULE_PCB_SKIP_UNKNOWN_SIZE_TORRENT); + return pass(); } if (torrentSize < torrentMinimumSize) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", "Skip due the torrent size"); + return pass(); } // 计算进度信息 final double actualProgress = (double) actualUploaded / torrentSize; // 实际进度 @@ -165,25 +167,32 @@ private void reloadConfig() { // 下载过量,检查 long maxAllowedExcessiveThreshold = (long) (torrentSize * excessiveThreshold); if (actualUploaded > maxAllowedExcessiveThreshold) { - return new BanResult(this, PeerAction.BAN, "Max allowed excessive threshold: " + maxAllowedExcessiveThreshold, String.format(Lang.MODULE_PCB_EXCESSIVE_DOWNLOAD, torrentSize, actualUploaded, maxAllowedExcessiveThreshold)); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.PCB_RULE_REACHED_MAX_ALLOWED_EXCESSIVE_THRESHOLD), new TranslationComponent(Lang.MODULE_PCB_EXCESSIVE_DOWNLOAD, String.valueOf(torrentSize), String.valueOf(actualUploaded), String.valueOf(maxAllowedExcessiveThreshold))); } } // 如果客户端报告自己进度更多,则跳过检查 if (actualProgress <= clientProgress) { - return new BanResult(this, PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_MORE_THAN_LOCAL_SKIP, percent(clientProgress), percent(actualProgress))); + return pass(); } // 计算进度差异 double difference = Math.abs(actualProgress - clientProgress); if (difference > maximumDifference) { - return new BanResult(this, PeerAction.BAN, "Over max Difference: " + difference + " Details: " + clientTask, String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference))); + clientTask.setProgressDifferenceCounter(clientTask.getProgressDifferenceCounter() + 1); + return new CheckResult(getClass(), PeerAction.BAN, banDuration, new TranslationComponent(Lang.PCB_RULE_REACHED_MAX_DIFFERENCE), new TranslationComponent(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference))); } if (rewindMaximumDifference > 0) { double lastRecord = clientTask.getLastReportProgress(); double rewind = lastRecord - peer.getProgress(); boolean ban = rewind > rewindMaximumDifference; - return new BanResult(this, ban ? PeerAction.BAN : PeerAction.NO_ACTION, "RewindAllow: " + rewindMaximumDifference + " Details: " + clientTask, String.format(Lang.MODULE_PCB_PEER_BAN_REWIND, percent(clientProgress), percent(actualProgress), percent(lastRecord), percent(rewind), percent(rewindMaximumDifference))); + if (ban) { + clientTask.setRewindCounter(clientTask.getRewindCounter() + 1); + progressRecorder.invalidate(peerIpString); // 封禁时,移除缓存 + } + + return new CheckResult(getClass(), ban ? PeerAction.BAN : PeerAction.NO_ACTION, 0, new TranslationComponent(Lang.PCB_RULE_PROGRESS_REWIND), new TranslationComponent(Lang.MODULE_PCB_PEER_BAN_REWIND, percent(clientProgress), percent(actualProgress), percent(lastRecord), percent(rewind), percent(rewindMaximumDifference))); } - return new BanResult(this, PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference))); + //return new CheckResult(getClass(), PeerAction.NO_ACTION, "N/A", String.format(Lang.MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, percent(clientProgress), percent(actualProgress), percent(difference))); + return pass(); } finally { // 无论如何都写入缓存,同步更改 clientTask.setLastReportUploaded(peer.getUploaded()); @@ -193,7 +202,7 @@ private void reloadConfig() { } private String percent(double d) { - return (d * 100) + "%"; + return MsgUtil.getPercentageFormatter().format(d); } record ClientTaskRecord(String address, List task) { @@ -206,6 +215,8 @@ static class ClientTask { private Double lastReportProgress; private long lastReportUploaded; private long trackingUploadedIncreaseTotal; + private int rewindCounter; + private int progressDifferenceCounter; } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/DownloaderCIDRBlockList.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/DownloaderCIDRBlockList.java index a6c8a88f78..6d7004c7a8 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/DownloaderCIDRBlockList.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/DownloaderCIDRBlockList.java @@ -1,7 +1,7 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; @@ -9,17 +9,17 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.Map; import java.util.UUID; +@Component public class DownloaderCIDRBlockList extends AbstractFeatureModule { - public DownloaderCIDRBlockList(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } - + @Autowired + private JavalinWebContainer webContainer; @Override public boolean isConfigurable() { return false; @@ -27,7 +27,7 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/blocklist/transmission", ctx -> { StringBuilder builder = new StringBuilder(); for (Map.Entry pair : getServer().getBannedPeers().entrySet()) { diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAlertController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAlertController.java index 7d17f2cf13..1417924a29 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAlertController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAlertController.java @@ -1,17 +1,20 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; +import com.ghostchu.peerbanhelper.alert.AlertManager; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +@Component public class PBHAlertController extends AbstractFeatureModule { - public PBHAlertController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } - + @Autowired + private AlertManager alertManager; + @Autowired + private JavalinWebContainer webContainer; @Override public boolean isConfigurable() { return false; @@ -29,17 +32,18 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin().get("/api/alerts", this::handleListing, Role.USER_READ); - getServer().getWebContainer().javalin().delete("/api/alert/{id}", this::handleDelete, Role.USER_WRITE); + webContainer.javalin() + .get("/api/alerts", this::handleListing, Role.USER_READ) + .delete("/api/alert/{id}", this::handleDelete, Role.USER_WRITE); } private void handleListing(Context ctx) { ctx.status(200); - ctx.json(getServer().getAlertManager().getAlerts()); + ctx.json(alertManager.getAlerts()); } private void handleDelete(Context ctx) { - if (getServer().getAlertManager().removeAlert(ctx.pathParam("id"))) { + if (alertManager.removeAlert(ctx.pathParam("id"))) { ctx.status(204); } else { ctx.status(404); diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAuthenticateController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAuthenticateController.java index cedc63ba2b..d3d365a476 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAuthenticateController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHAuthenticateController.java @@ -1,28 +1,28 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.Map; -public class PBHAuthenticateController extends AbstractFeatureModule { - private static final Logger log = LoggerFactory.getLogger(PBHAuthenticateController.class); - - public PBHAuthenticateController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +@Component +@Slf4j +public class PBHAuthenticateController extends AbstractFeatureModule { + @Autowired + private JavalinWebContainer webContainer; @Override public boolean isConfigurable() { return false; @@ -40,7 +40,7 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin() + webContainer.javalin() .post("/api/auth/login", this::handleLogin, Role.ANYONE) .post("/api/auth/logout", this::handleLogout, Role.ANYONE); } @@ -52,14 +52,14 @@ private void handleLogout(Context ctx) { private void handleLogin(Context ctx) { LoginRequest loginRequest = ctx.bodyAsClass(LoginRequest.class); - if (loginRequest == null || !getServer().getWebContainer().getToken().equals(loginRequest.getToken())) { + if (loginRequest == null || !webContainer.getToken().equals(loginRequest.getToken())) { ctx.status(HttpStatus.UNAUTHORIZED); - ctx.json(Map.of("message", Lang.WEBAPI_AUTH_INVALID_TOKEN)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.WEBAPI_AUTH_INVALID_TOKEN))); return; } - ctx.sessionAttribute("authenticated", getServer().getWebContainer().getToken()); + ctx.sessionAttribute("authenticated", webContainer.getToken()); ctx.status(HttpStatus.OK); - ctx.json(Map.of("message", Lang.WEBAPI_AUTH_OK)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.WEBAPI_AUTH_OK))); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanController.java index 8b5996be06..a65e5f749c 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHBanController.java @@ -1,34 +1,45 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; -import com.ghostchu.peerbanhelper.database.DatabaseHelper; -import com.ghostchu.peerbanhelper.metric.impl.persist.PersistMetrics; +import com.ghostchu.peerbanhelper.database.Database; +import com.ghostchu.peerbanhelper.database.dao.impl.HistoryDao; +import com.ghostchu.peerbanhelper.database.table.HistoryEntity; +import com.ghostchu.peerbanhelper.metric.BasicMetrics; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.ghostchu.peerbanhelper.wrapper.BakedBanMetadata; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; +import com.ghostchu.peerbanhelper.wrapper.PeerWrapper; import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; -import java.sql.SQLException; import java.util.*; import java.util.stream.Stream; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j +@Component public class PBHBanController extends AbstractFeatureModule { - private final DatabaseHelper db; - - public PBHBanController(PeerBanHelperServer server, YamlConfiguration profile, DatabaseHelper db) { - super(server, profile); - this.db = db; - } + @Autowired + private Database db; + @Autowired + @Qualifier("persistMetrics") + private BasicMetrics persistMetrics; + @Autowired + private JavalinWebContainer webContainer; + @Autowired + private HistoryDao historyDao; @Override public boolean isConfigurable() { @@ -47,7 +58,7 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/bans", this::handleBans, Role.USER_READ) .get("/api/bans/logs", this::handleLogs, Role.USER_READ) .get("/api/bans/ranks", this::handleRanks, Role.USER_READ) @@ -55,22 +66,23 @@ public void onEnable() { } private void handleBanDelete(Context context) { - UnbanRequest request = context.bodyAsClass(UnbanRequest.class); + List request = Arrays.asList(context.bodyAsClass(String[].class)); List pendingRemovals = new ArrayList<>(); for (PeerAddress address : getServer().getBannedPeers().keySet()) { - if (request.ips().contains(address.getIp())) { + if (request.contains(address.getIp())) { pendingRemovals.add(address); } } - pendingRemovals.forEach(pa -> getServer().unbanPeer(pa)); + pendingRemovals.forEach(pa -> getServer().scheduleUnBanPeer(pa)); context.status(HttpStatus.OK); context.json(Map.of("count", pendingRemovals.size())); } + private void handleRanks(Context ctx) { int number = Integer.parseInt(Objects.requireNonNullElse(ctx.queryParam("limit"), "50")); try { - Map countMap = db.findMaxBans(number); + Map countMap = historyDao.getBannedIps(number); List list = new ArrayList<>(countMap.size()); countMap.forEach((k, v) -> { if (v >= 2) { @@ -79,8 +91,8 @@ private void handleRanks(Context ctx) { }); ctx.status(HttpStatus.OK); ctx.json(list); - } catch (SQLException e) { - log.warn("Error on handling Web API request", e); + } catch (Exception e) { + log.error("Error on handling Web API request", e); ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); ctx.json(Map.of("message", "Internal server error")); } @@ -92,21 +104,20 @@ private void handleLogs(Context ctx) { ctx.json(Map.of("message", "Database not initialized on this PeerBanHelper server")); return; } - if (getServer().getMetrics() instanceof PersistMetrics persistMetrics) { - persistMetrics.flush(); - } + persistMetrics.flush(); int pageIndex = Integer.parseInt(Objects.requireNonNullElse(ctx.queryParam("pageIndex"), "0")); int pageSize = Integer.parseInt(Objects.requireNonNullElse(ctx.queryParam("pageSize"), "100")); try { + Map map = new HashMap<>(); map.put("pageIndex", pageIndex); map.put("pageSize", pageSize); - map.put("results", db.queryBanLogs(null, null, pageIndex, pageSize)); - map.put("total", db.queryBanLogsCount()); + map.put("results", historyDao.queryByPaging(historyDao.queryBuilder().orderBy("banAt", false), pageIndex, pageSize).stream().map(r -> new BanLogResponse(locale(ctx), r)).toList()); + map.put("total", historyDao.countOf()); ctx.status(HttpStatus.OK); ctx.json(map); - } catch (SQLException e) { - log.error(Lang.WEB_BANLOGS_INTERNAL_ERROR, e); + } catch (Exception e) { + log.error(tlUI(Lang.WEB_BANLOGS_INTERNAL_ERROR), e); ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); ctx.json(Map.of("message", "Internal server error")); } @@ -115,7 +126,7 @@ private void handleLogs(Context ctx) { private void handleBans(Context ctx) { long limit = Long.parseLong(Objects.requireNonNullElse(ctx.queryParam("limit"), "-1")); long lastBanTime = Long.parseLong(Objects.requireNonNullElse(ctx.queryParam("lastBanTime"), "-1")); - var banResponseList = getBanResponseStream(lastBanTime, limit); + var banResponseList = getBanResponseStream(locale(ctx), lastBanTime, limit); ctx.status(HttpStatus.OK); ctx.json(banResponseList.toList()); } @@ -126,11 +137,11 @@ public void onDisable() { } - private @NotNull Stream getBanResponseStream(long lastBanTime, long limit) { + private @NotNull Stream getBanResponseStream(String locale, long lastBanTime, long limit) { var banResponseList = getServer().getBannedPeers() .entrySet() .stream() - .map(entry -> new BanResponse(entry.getKey().getAddress().toString(), new BakedBanMetadata(entry.getValue()))) + .map(entry -> new BanResponse(entry.getKey().getAddress().toString(), new BakedBanMetadata(locale, entry.getValue()))) .sorted((o1, o2) -> Long.compare(o2.getBanMetadata().getBanAt(), o1.getBanMetadata().getBanAt())); if (lastBanTime > 0) { banResponseList = banResponseList.filter(b -> b.getBanMetadata().getBanAt() < lastBanTime); @@ -138,12 +149,58 @@ public void onDisable() { if (limit > 0) { banResponseList = banResponseList.limit(limit); } + banResponseList = banResponseList.peek(meta -> { + PeerWrapper peerWrapper = meta.getBanMetadata().getPeer(); + if (peerWrapper != null) { + var nullableGeoData = getServer().queryIPDB(peerWrapper.toPeerAddress()).geoData().get(); + meta.getBanMetadata().setGeo(nullableGeoData); + } + }); return banResponseList; } public record UnbanRequest(List ips) { } + @AllArgsConstructor + @NoArgsConstructor + @Data + static class BanLogResponse { + private long banAt; + private long unbanAt; + private String peerIp; + private int peerPort; + private String peerId; + private String peerClientName; + private long peerUploaded; + private long peerDownloaded; + private double peerProgress; + private String torrentInfoHash; + private String torrentName; + private long torrentSize; + private String module; + private String rule; + private String description; + + public BanLogResponse(String locale, HistoryEntity history) { + this.banAt = history.getBanAt().getTime(); + this.unbanAt = history.getUnbanAt().getTime(); + this.peerIp = history.getIp(); + this.peerPort = history.getPort(); + this.peerId = history.getPeerId(); + this.peerClientName = history.getPeerClientName(); + this.peerUploaded = history.getPeerUploaded(); + this.peerDownloaded = history.getPeerDownloaded(); + this.peerProgress = history.getPeerProgress(); + this.torrentInfoHash = history.getTorrent().getInfoHash(); + this.torrentName = history.getTorrent().getName(); + this.torrentSize = history.getTorrent().getSize(); + this.module = history.getRule().getModule().getName(); + this.rule = tl(locale, history.getRule().getRule()); + this.description = tl(locale, history.getDescription()); + } + } + @AllArgsConstructor @NoArgsConstructor @Data diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java index fd2ee6f710..6125a76e3b 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHDownloaderController.java @@ -3,8 +3,12 @@ import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.downloader.Downloader; import com.ghostchu.peerbanhelper.downloader.DownloaderLastStatus; +import com.ghostchu.peerbanhelper.ipdb.IPGeoData; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; +import com.ghostchu.peerbanhelper.module.impl.webapi.dto.PopulatedPeerDTO; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.ghostchu.peerbanhelper.wrapper.PeerMetadata; import com.ghostchu.peerbanhelper.wrapper.TorrentWrapper; @@ -13,19 +17,22 @@ import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.extern.slf4j.Slf4j; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; + @Slf4j +@Component public class PBHDownloaderController extends AbstractFeatureModule { - public PBHDownloaderController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; @Override public boolean isConfigurable() { @@ -44,7 +51,7 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/downloaders", this::handleDownloaderList, Role.USER_READ) .put("/api/downloaders", this::handleDownloaderPut, Role.USER_WRITE) .patch("/api/downloaders/{downloaderName}", ctx -> handleDownloaderPatch(ctx, ctx.pathParam("downloaderName")), Role.USER_WRITE) @@ -62,22 +69,22 @@ private void handleDownloaderPut(Context ctx) { Downloader downloader = getServer().createDownloader(name, config); if (downloader == null) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_ADD_FAILURE)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_ADD_FAILURE))); return; } if (getServer().registerDownloader(downloader)) { ctx.status(HttpStatus.CREATED); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_CREATED, "code", HttpStatus.CREATED.getCode())); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_CREATED), "code", HttpStatus.CREATED.getCode())); } else { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_CREATION_FAILED_ALREADY_EXISTS)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_CREATION_FAILED_ALREADY_EXISTS))); } try { getServer().saveDownloaders(); } catch (IOException e) { - log.warn("Internal server error, unable to create downloader due an I/O exception", e); + log.error("Internal server error, unable to create downloader due an I/O exception", e); ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_CREATION_FAILED_IO_EXCEPTION)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_CREATION_FAILED_IO_EXCEPTION))); } } @@ -88,7 +95,7 @@ private void handleDownloaderPatch(Context ctx, String downloaderName) { Downloader downloader = getServer().createDownloader(name, config); if (downloader == null) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_UPDATE_FAILURE)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_UPDATE_FAILURE))); return; } // 可能重命名了? @@ -97,17 +104,17 @@ private void handleDownloaderPatch(Context ctx, String downloaderName) { .forEach(d -> getServer().unregisterDownloader(d)); if (getServer().registerDownloader(downloader)) { ctx.status(HttpStatus.OK); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_UPDATED, "code", HttpStatus.OK.getCode())); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_UPDATED), "code", HttpStatus.OK.getCode())); } else { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_UPDATE_FAILURE_ALREADY_EXISTS)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_UPDATE_FAILURE_ALREADY_EXISTS))); } try { getServer().saveDownloaders(); } catch (IOException e) { - log.warn("Internal server error, unable to update downloader due an I/O exception", e); + log.error("Internal server error, unable to update downloader due an I/O exception", e); ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_CREATION_FAILED_IO_EXCEPTION)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_CREATION_FAILED_IO_EXCEPTION))); } } @@ -123,13 +130,18 @@ private void handleDownloaderTest(Context ctx) { Downloader downloader = getServer().createDownloader(name, config); if (downloader == null) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_ADD_FAILURE)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_ADD_FAILURE))); return; } try { - boolean testResult = downloader.login(); + var testResult = downloader.login(); ctx.status(HttpStatus.OK); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_TEST_OK, "valid", testResult)); + if (testResult.success()) { + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_TEST_OK), "valid", testResult.success())); + } else { + ctx.json(Map.of("message", tl(locale(ctx), testResult.getMessage()), "valid", testResult.success())); + } + downloader.close(); } catch (Exception e) { ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); ctx.json(Map.of("message", e.getMessage(), "valid", false)); @@ -140,7 +152,7 @@ private void handleDownloaderDelete(Context ctx, String downloaderName) { Optional selected = getServer().getDownloaders().stream().filter(d -> d.getName().equals(downloaderName)).findFirst(); if (selected.isEmpty()) { ctx.status(HttpStatus.NOT_FOUND); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_REMOVE_NOT_EXISTS)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_REMOVE_NOT_EXISTS))); return; } Downloader downloader = selected.get(); @@ -148,7 +160,7 @@ private void handleDownloaderDelete(Context ctx, String downloaderName) { try { getServer().saveDownloaders(); ctx.status(HttpStatus.OK); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_REMOVE_SAVED, "code", HttpStatus.OK.getCode())); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_REMOVE_SAVED), "code", HttpStatus.OK.getCode())); } catch (IOException e) { ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); ctx.json(Map.of("message", e.getClass().getName() + ": " + e.getMessage())); @@ -160,44 +172,58 @@ private void handlePeersInTorrentOnDownloader(Context ctx, String downloaderName Optional selected = getServer().getDownloaders().stream().filter(d -> d.getName().equals(downloaderName)).findFirst(); if (selected.isEmpty()) { ctx.status(HttpStatus.NOT_FOUND); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_DOWNLOADER_NOT_EXISTS)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_DOWNLOADER_NOT_EXISTS))); return; } Downloader downloader = selected.get(); - List peerWrappers = getServer().getLivePeersSnapshot().values() + List peerWrappers = getServer().getLivePeersSnapshot().values() .stream() .filter(p -> p.getDownloader().equals(downloader.getName())) .filter(p -> p.getTorrent().getHash().equals(torrentId)) + .sorted((o1, o2) -> Long.compare(o2.getPeer().getUploadSpeed(), o1.getPeer().getUploadSpeed())) + .map(this::populatePeerDTO) .toList(); ctx.status(HttpStatus.OK); ctx.json(peerWrappers); } + private PopulatedPeerDTO populatePeerDTO(PeerMetadata p) { + PopulatedPeerDTO dto = new PopulatedPeerDTO(p.getPeer(), null); + PeerBanHelperServer.IPDBResponse response = getServer().queryIPDB(p.getPeer().toPeerAddress()); + IPGeoData geoData = response.geoData().get(); + if (geoData != null) { + dto.setGeo(geoData); + } + return dto; + } + private void handleDownloaderTorrents(@NotNull Context ctx, String downloaderName) { Optional selected = getServer().getDownloaders().stream() .filter(d -> d.getName().equals(downloaderName)) .findFirst(); if (selected.isEmpty()) { ctx.status(HttpStatus.NOT_FOUND); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_DOWNLOADER_NOT_EXISTS)); + ctx.json(Map.of("message", tl(locale(ctx), Lang.DOWNLOADER_API_DOWNLOADER_NOT_EXISTS))); return; } Downloader downloader = selected.get(); - List torrentWrappers = getServer().getLivePeersSnapshot().values().stream().filter(p -> p.getDownloader().equals(downloader.getName())) + List torrentWrappers = getServer().getLivePeersSnapshot() + .values().stream().filter(p -> p.getDownloader().equals(downloader.getName())) .map(PeerMetadata::getTorrent) - .distinct() + .sorted((o1, o2) -> Long.compare(o2.getRtUploadSpeed(), o1.getRtUploadSpeed())) .toList(); ctx.status(HttpStatus.OK); ctx.json(torrentWrappers); } private void handleDownloaderStatus(@NotNull Context ctx, String downloaderName) { + String locale = locale(ctx); Optional selected = getServer().getDownloaders().stream() .filter(d -> d.getName().equals(downloaderName)) .findFirst(); if (selected.isEmpty()) { ctx.status(HttpStatus.NOT_FOUND); - ctx.json(Map.of("message", Lang.DOWNLOADER_API_DOWNLOADER_NOT_EXISTS)); + ctx.json(Map.of("message", tl(locale, Lang.DOWNLOADER_API_DOWNLOADER_NOT_EXISTS))); return; } Downloader downloader = selected.get(); @@ -214,7 +240,7 @@ private void handleDownloaderStatus(@NotNull Context ctx, String downloaderName) JsonObject config = downloader.saveDownloaderJson(); ctx.status(HttpStatus.OK); - ctx.json(new DownloaderStatus(lastStatus, downloader.getLastStatusMessage(), activeTorrents, activePeers, config)); + ctx.json(new DownloaderStatus(lastStatus, tl(locale, downloader.getLastStatusMessage() == null ? new TranslationComponent(Lang.STATUS_TEXT_UNKNOWN) : downloader.getLastStatusMessage()), activeTorrents, activePeers, config)); } private void handleDownloaderList(@NotNull Context ctx) { @@ -232,7 +258,8 @@ public void onDisable() { record DraftDownloader(String name, JsonObject config) { } - record DownloaderStatus(DownloaderLastStatus lastStatus, String lastStatusMessage, long activeTorrents, + record DownloaderStatus(DownloaderLastStatus lastStatus, String lastStatusMessage, + long activeTorrents, long activePeers, JsonObject config) { } diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHLogsController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHLogsController.java index a65d944e11..ec60828be1 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHLogsController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHLogsController.java @@ -1,21 +1,21 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; import com.ghostchu.peerbanhelper.log4j2.MemoryLoggerAppender; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.Map; import java.util.StringJoiner; +@Component public class PBHLogsController extends AbstractFeatureModule { - public PBHLogsController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } - + @Autowired + private JavalinWebContainer webContainer; @Override public boolean isConfigurable() { return false; @@ -33,7 +33,7 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin().get("/api/logs/main", this::handleLogs, Role.USER_READ); + webContainer.javalin().get("/api/logs/main", this::handleLogs, Role.USER_READ); } private void handleLogs(Context ctx) { diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetadataController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetadataController.java index 93e4009083..9f5fa01b96 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetadataController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetadataController.java @@ -1,22 +1,28 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.Main; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; +import com.ghostchu.peerbanhelper.BuildMeta; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; import com.ghostchu.peerbanhelper.module.FeatureModule; +import com.ghostchu.peerbanhelper.module.ModuleManager; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; import io.javalin.http.HttpStatus; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; +@Component public class PBHMetadataController extends AbstractFeatureModule { - public PBHMetadataController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private JavalinWebContainer webContainer; + @Autowired + private BuildMeta buildMeta; + @Autowired + private ModuleManager moduleManager; @Override public boolean isConfigurable() { @@ -35,14 +41,14 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getWebContainer().javalin().get("/api/metadata/manifest", this::handleManifest, Role.ANYONE); + webContainer.javalin().get("/api/metadata/manifest", this::handleManifest, Role.ANYONE); } private void handleManifest(Context ctx) { ctx.status(HttpStatus.OK); Map data = new HashMap<>(); - data.put("version", Main.getMeta()); - data.put("modules", getServer().getModuleManager().getModules().stream() + data.put("version", buildMeta); + data.put("modules", moduleManager.getModules().stream() .filter(FeatureModule::isModuleEnabled) .map(f -> new ModuleRecord(f.getClass().getName(), f.getConfigName())).toList()); ctx.json(data); diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetricsController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetricsController.java index a2e6f0079c..60fd97d4b9 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetricsController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/PBHMetricsController.java @@ -1,10 +1,14 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; +import com.ghostchu.peerbanhelper.database.dao.impl.HistoryDao; +import com.ghostchu.peerbanhelper.database.table.HistoryEntity; import com.ghostchu.peerbanhelper.metric.BasicMetrics; import com.ghostchu.peerbanhelper.metric.HitRateMetricRecorder; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; +import com.ghostchu.peerbanhelper.module.impl.webapi.common.StdMsg; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.Rule; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import com.ghostchu.peerbanhelper.wrapper.BanMetadata; import io.javalin.http.Context; @@ -12,20 +16,26 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.sql.Timestamp; +import java.util.*; +import java.util.function.Function; -public class PBHMetricsController extends AbstractFeatureModule { +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +@Component +public class PBHMetricsController extends AbstractFeatureModule { + @Autowired + @Qualifier("persistMetrics") private BasicMetrics metrics; - - public PBHMetricsController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } + @Autowired + private HistoryDao historyDao; + @Autowired + private JavalinWebContainer webContainer; @Override public boolean isConfigurable() { @@ -34,22 +44,129 @@ public boolean isConfigurable() { @Override public void onEnable() { - this.metrics = getServer().getMetrics(); - getServer().getWebContainer().javalin() + webContainer.javalin() .get("/api/statistic/counter", this::handleBasicCounter, Role.USER_READ) - .get("/api/statistic/rules", this::handleRules, Role.USER_READ); + .get("/api/statistic/rules", this::handleRules, Role.USER_READ) + .get("/api/statistic/analysis/field", this::handleHistoryNumberAccess, Role.USER_READ) + .get("/api/statistic/analysis/date", this::handleHistoryDateAccess, Role.USER_READ); } + private void handleHistoryDateAccess(Context ctx) throws Exception { + String startAtArg = ctx.queryParam("startAt"); + String endAtArg = ctx.queryParam("endAt"); + String filter = Objects.requireNonNullElse(ctx.queryParam("filter"), "0.0"); + String type = ctx.queryParam("type"); + String field = ctx.queryParam("field"); + if (startAtArg == null) { + throw new IllegalArgumentException("startAt cannot be null"); + } + if (endAtArg == null) { + throw new IllegalArgumentException("startAt cannot be null"); + } + if (field == null) { + throw new IllegalArgumentException("startAt cannot be null"); + } + Function trimmer = switch (type) { + case "year" -> (time) -> { + Calendar calendar = getZeroCalender(); + calendar.set(Calendar.YEAR, time.get(Calendar.YEAR)); + return calendar; + }; + case "month" -> (time) -> { + Calendar calendar = getZeroCalender(); + calendar.set(Calendar.YEAR, time.get(Calendar.YEAR)); + calendar.set(Calendar.MONTH, time.get(Calendar.MONTH)); + return calendar; + }; + case "day" -> (time) -> { + Calendar calendar = getZeroCalender(); + calendar.set(Calendar.YEAR, time.get(Calendar.YEAR)); + calendar.set(Calendar.MONTH, time.get(Calendar.MONTH)); + calendar.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH)); + return calendar; + }; + case "hour" -> (time) -> { + Calendar calendar = getZeroCalender(); + calendar.set(Calendar.YEAR, time.get(Calendar.YEAR)); + calendar.set(Calendar.MONTH, time.get(Calendar.MONTH)); + calendar.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH)); + calendar.set(Calendar.HOUR_OF_DAY, time.get(Calendar.HOUR_OF_DAY)); + return calendar; + }; + case "minute" -> (time) -> { + Calendar calendar = getZeroCalender(); + calendar.set(Calendar.YEAR, time.get(Calendar.YEAR)); + calendar.set(Calendar.MONTH, time.get(Calendar.MONTH)); + calendar.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH)); + calendar.set(Calendar.HOUR_OF_DAY, time.get(Calendar.HOUR_OF_DAY)); + calendar.set(Calendar.MINUTE, time.get(Calendar.MINUTE)); + return calendar; + }; + case "second" -> (time) -> { + Calendar calendar = getZeroCalender(); + calendar.set(Calendar.YEAR, time.get(Calendar.YEAR)); + calendar.set(Calendar.MONTH, time.get(Calendar.MONTH)); + calendar.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH)); + calendar.set(Calendar.HOUR_OF_DAY, time.get(Calendar.HOUR_OF_DAY)); + calendar.set(Calendar.MINUTE, time.get(Calendar.MINUTE)); + calendar.set(Calendar.SECOND, time.get(Calendar.SECOND)); + return calendar; + }; + case null, default -> throw new IllegalArgumentException("Unexpected value: " + type); + }; + + Function timestampGetter = switch (field) { + case "banAt" -> HistoryEntity::getBanAt; + case "unbanAt" -> HistoryEntity::getUnbanAt; + case null, default -> throw new IllegalArgumentException("Unexpected value: " + field); + }; + long startAt = Long.parseLong(startAtArg); + long endAt = Long.parseLong(endAtArg); + double pctFilter = Double.parseDouble(filter); + + var results = historyDao.countDateField(startAt, endAt, timestampGetter, trimmer, pctFilter); + ctx.status(HttpStatus.OK); + ctx.json(new StdMsg(true, "ok", results)); + } + + private Calendar getZeroCalender() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, 0); + calendar.set(Calendar.MONTH, 0); + calendar.set(Calendar.DAY_OF_MONTH, 0); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } + + private void handleHistoryNumberAccess(Context ctx) throws Exception { + // 过滤 X% 以下的数据 + String type = ctx.queryParam("type"); + String field = ctx.queryParam("field"); + double filter = Double.parseDouble(Objects.requireNonNullElse(ctx.queryParam("filter"), "0.0")); + List results = switch (type) { + case "count" -> historyDao.countField(field, filter); + case "sum" -> historyDao.sumField(field, filter); + case null, default -> throw new IllegalArgumentException("type invalid"); + }; + ctx.status(HttpStatus.OK); + ctx.json(new StdMsg(true, "ok", results)); + } + + private void handleRules(Context ctx) { + String locale = locale(ctx); Map metric = new HashMap<>(getServer().getHitRateMetric().getHitRateMetric()); Map dict = new HashMap<>(); List dat = metric.entrySet().stream() .map(obj -> { - String ruleType = obj.getKey().getClass().getName(); + TranslationComponent ruleType = new TranslationComponent(obj.getKey().getClass().getName()); if (obj.getKey().matcherName() != null) { ruleType = obj.getKey().matcherName(); } - dict.put(obj.getKey().matcherIdentifier(), ruleType); + dict.put(obj.getKey().matcherIdentifier(), tl(locale, ruleType)); return new RuleData(obj.getKey().matcherIdentifier(), obj.getValue().getHitCounter(), obj.getValue().getQueryCounter(), obj.getKey().metadata()); }) .sorted((o1, o2) -> Long.compare(o2.getHit(), o1.getHit())) diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/RuleSubController.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/RuleSubController.java index 1628da4fa3..c617a78314 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/RuleSubController.java +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/RuleSubController.java @@ -1,36 +1,41 @@ package com.ghostchu.peerbanhelper.module.impl.webapi; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; -import com.ghostchu.peerbanhelper.database.RuleSubInfo; +import com.ghostchu.peerbanhelper.database.table.RuleSubInfoEntity; import com.ghostchu.peerbanhelper.module.AbstractFeatureModule; import com.ghostchu.peerbanhelper.module.IPBanRuleUpdateType; +import com.ghostchu.peerbanhelper.module.ModuleManager; import com.ghostchu.peerbanhelper.module.impl.rule.IPBlackRuleList; import com.ghostchu.peerbanhelper.module.impl.webapi.common.SlimMsg; import com.ghostchu.peerbanhelper.module.impl.webapi.common.StdMsg; import com.ghostchu.peerbanhelper.text.Lang; import com.ghostchu.peerbanhelper.util.JsonUtil; +import com.ghostchu.peerbanhelper.web.JavalinWebContainer; import com.ghostchu.peerbanhelper.web.Role; import io.javalin.http.Context; import io.javalin.http.HttpStatus; import lombok.extern.slf4j.Slf4j; import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection; -import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.io.IOException; import java.sql.SQLException; import java.util.*; import java.util.concurrent.atomic.AtomicReference; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j +@Component public class RuleSubController extends AbstractFeatureModule { - + @Autowired + private JavalinWebContainer webContainer; IPBlackRuleList ipBlackRuleList; - public RuleSubController(PeerBanHelperServer server, YamlConfiguration profile) { - super(server, profile); - } - + @Autowired + private ModuleManager moduleManager; @Override public boolean isConfigurable() { return false; @@ -48,9 +53,9 @@ public boolean isConfigurable() { @Override public void onEnable() { - getServer().getModuleManager().getModules().stream().filter(ele -> ele.getConfigName().equals("ip-address-blocker-rules")).findFirst().ifPresent(ele -> { + moduleManager.getModules().stream().filter(ele -> ele.getConfigName().equals("ip-address-blocker-rules")).findFirst().ifPresent(ele -> { ipBlackRuleList = (IPBlackRuleList) ele; - getServer().getWebContainer().javalin() + webContainer.javalin() // 查询检查间隔 .get("/api/sub/interval", this::getCheckInterval, Role.USER_READ) // 修改检查间隔 @@ -58,9 +63,9 @@ public void onEnable() { // 新增订阅规则 .put("/api/sub/rule", ctx -> save(ctx, null, true), Role.USER_WRITE) // 更新订阅规则 - .post("/api/sub/rule/{ruleId}/update", ctx -> ctx.json(update(ctx.pathParam("ruleId"))), Role.USER_READ) + .post("/api/sub/rule/{ruleId}/update", ctx -> ctx.json(update(locale(ctx), ctx.pathParam("ruleId"))), Role.USER_READ) // 查询订阅规则 - .get("/api/sub/rule/{ruleId}", ctx -> ctx.json(get(ctx.pathParam("ruleId"))), Role.USER_READ) + .get("/api/sub/rule/{ruleId}", ctx -> ctx.json(get(locale(ctx), ctx.pathParam("ruleId"))), Role.USER_READ) // 修改订阅规则 .post("/api/sub/rule/{ruleId}", ctx -> save(ctx, ctx.pathParam("ruleId"), false), Role.USER_WRITE) // 删除订阅规则 @@ -68,9 +73,9 @@ public void onEnable() { // 启用/禁用订阅规则 .patch("/api/sub/rule/{ruleId}", this::switcher, Role.USER_WRITE) // 查询订阅规则列表 - .get("/api/sub/rules", ctx -> ctx.json(list()), Role.USER_READ) + .get("/api/sub/rules", ctx -> ctx.json(list(locale(ctx))), Role.USER_READ) // 手动更新全部订阅规则 - .post("/api/sub/rules/update", ctx -> ctx.json(updateAll()), Role.USER_WRITE) + .post("/api/sub/rules/update", ctx -> ctx.json(updateAll(locale(ctx))), Role.USER_WRITE) // 查询全部订阅规则更新日志 .get("/api/sub/logs", ctx -> logs(ctx, null), Role.USER_READ) // 查询订阅规则更新日志 @@ -88,7 +93,7 @@ public void onDisable() { * @param ctx 上下文 */ private void getCheckInterval(Context ctx) { - ctx.json(new StdMsg(true, Lang.IP_BAN_RULE_CHECK_INTERVAL_QUERY_SUCCESS, ipBlackRuleList.getCheckInterval())); + ctx.json(new StdMsg(true, tl(locale(ctx), Lang.IP_BAN_RULE_CHECK_INTERVAL_QUERY_SUCCESS), ipBlackRuleList.getCheckInterval())); } /** @@ -100,10 +105,10 @@ private void changeCheckInterval(Context ctx) { try { long interval = JsonUtil.readObject(ctx.body()).get("checkInterval").getAsLong(); ipBlackRuleList.changeCheckInterval(interval); - ctx.json(new SlimMsg(true, Lang.IP_BAN_RULE_CHECK_INTERVAL_UPDATED, HttpStatus.OK.getCode())); + ctx.json(new SlimMsg(true, tl(locale(ctx), Lang.IP_BAN_RULE_CHECK_INTERVAL_UPDATED), HttpStatus.OK.getCode())); } catch (Exception e) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_CHECK_INTERVAL_WRONG_PARAM, HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_CHECK_INTERVAL_WRONG_PARAM), HttpStatus.BAD_REQUEST.getCode())); } } @@ -124,11 +129,11 @@ private void logs(Context ctx, String ruleId) { map.put("pageSize", pageSize); map.put("results", ipBlackRuleList.queryRuleSubLogs(ruleId, pageIndex, pageSize)); map.put("total", ipBlackRuleList.countRuleSubLogs(ruleId)); - ctx.json(new StdMsg(true, Lang.IP_BAN_RULE_LOG_QUERY_SUCCESS, map)); + ctx.json(new StdMsg(true, tl(locale(ctx), Lang.IP_BAN_RULE_LOG_QUERY_SUCCESS), map)); } catch (Exception e) { - log.error(Lang.IP_BAN_RULE_LOG_QUERY_ERROR, e); + log.error(tlUI(Lang.IP_BAN_RULE_LOG_QUERY_ERROR), e); ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_LOG_QUERY_WRONG_PARAM, HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_LOG_QUERY_WRONG_PARAM), HttpStatus.BAD_REQUEST.getCode())); } } @@ -137,9 +142,12 @@ private void logs(Context ctx, String ruleId) { * * @return 响应 */ - private SlimMsg updateAll() { + private SlimMsg updateAll(String locale) { AtomicReference result = new AtomicReference<>(); - ipBlackRuleList.getRuleSubsConfig().getKeys(false).stream().map(this::update).filter(ele -> !ele.success()).findFirst().ifPresentOrElse(result::set, () -> result.set(new SlimMsg(true, Lang.IP_BAN_RULE_ALL_UPDATED, 200))); + ipBlackRuleList.getRuleSubsConfig().getKeys(false).stream().map(k -> update(locale, k)).filter(ele -> !ele.success()) + .findFirst() + .ifPresentOrElse(result::set, () -> + result.set(new SlimMsg(true, tl(locale, Lang.IP_BAN_RULE_ALL_UPDATED), 200))); return result.get(); } @@ -149,15 +157,15 @@ private SlimMsg updateAll() { * @param ruleId 规则ID * @return 响应 */ - private SlimMsg update(String ruleId) { + private SlimMsg update(String locale, String ruleId) { if (ruleId == null || ruleId.isEmpty()) { - return new SlimMsg(false, Lang.IP_BAN_RULE_NO_ID, HttpStatus.NOT_FOUND.getCode()); + return new SlimMsg(false, tlUI(Lang.IP_BAN_RULE_NO_ID), HttpStatus.NOT_FOUND.getCode()); } ConfigurationSection configurationSection = ipBlackRuleList.getRuleSubsConfig().getConfigurationSection(ruleId); if (null == configurationSection) { - return new SlimMsg(false, Lang.IP_BAN_RULE_CANT_FIND.replace("{}", ruleId), HttpStatus.NOT_FOUND.getCode()); + return new SlimMsg(false, tlUI(Lang.IP_BAN_RULE_CANT_FIND, ruleId), HttpStatus.NOT_FOUND.getCode()); } - return ipBlackRuleList.updateRule(configurationSection, IPBanRuleUpdateType.MANUAL); + return ipBlackRuleList.updateRule(locale, configurationSection, IPBanRuleUpdateType.MANUAL); } /** @@ -172,19 +180,19 @@ private void switcher(Context ctx) throws SQLException, IOException { enabled = JsonUtil.readObject(ctx.body()).get("enabled").getAsBoolean(); } catch (Exception e) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_ENABLED_WRONG_PARAM, HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_ENABLED_WRONG_PARAM), HttpStatus.BAD_REQUEST.getCode())); return; } - RuleSubInfo ruleSubInfo = ipBlackRuleList.getRuleSubInfo(ruleId); + RuleSubInfoEntity ruleSubInfo = ipBlackRuleList.getRuleSubInfo(ruleId); if (null == ruleSubInfo) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_CANT_FIND.replace("{}", ruleId), HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_CANT_FIND, ruleId), HttpStatus.BAD_REQUEST.getCode())); return; } - String msg = (enabled ? Lang.IP_BAN_RULE_ENABLED : Lang.IP_BAN_RULE_DISABLED).replace("{}", ruleSubInfo.ruleName()); - if (enabled != ruleSubInfo.enabled()) { - ConfigurationSection configurationSection = ipBlackRuleList.saveRuleSubInfo(new RuleSubInfo(ruleId, enabled, ruleSubInfo.ruleName(), ruleSubInfo.subUrl(), 0, 0)); - ipBlackRuleList.updateRule(configurationSection, IPBanRuleUpdateType.MANUAL); + String msg = tl(locale(ctx), (enabled ? Lang.IP_BAN_RULE_ENABLED : Lang.IP_BAN_RULE_DISABLED), ruleSubInfo.getRuleName()); + if (enabled != ruleSubInfo.isEnabled()) { + ConfigurationSection configurationSection = ipBlackRuleList.saveRuleSubInfo(new RuleSubInfoEntity(ruleId, enabled, ruleSubInfo.getRuleName(), ruleSubInfo.getSubUrl(), 0, 0)); + ipBlackRuleList.updateRule(locale(ctx), configurationSection, IPBanRuleUpdateType.MANUAL); log.info(msg); ctx.json(new SlimMsg(true, msg, 200)); } else { @@ -200,15 +208,15 @@ private void switcher(Context ctx) throws SQLException, IOException { */ private void delete(Context ctx) throws IOException, SQLException { String ruleId = ctx.pathParam("ruleId"); - RuleSubInfo ruleSubInfo = ipBlackRuleList.getRuleSubInfo(ruleId); + RuleSubInfoEntity ruleSubInfo = ipBlackRuleList.getRuleSubInfo(ruleId); if (null == ruleSubInfo) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_CANT_FIND.replace("{}", ruleId), HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_CANT_FIND, ruleId), HttpStatus.BAD_REQUEST.getCode())); return; } ipBlackRuleList.deleteRuleSubInfo(ruleId); ipBlackRuleList.getIpBanMatchers().removeIf(ele -> ele.getRuleId().equals(ruleId)); - String msg = Lang.IP_BAN_RULE_DELETED.replace("{}", ruleSubInfo.ruleName()); + String msg = tl(locale(ctx), Lang.IP_BAN_RULE_DELETED, ruleSubInfo.getRuleName()); log.info(msg); ctx.json(new SlimMsg(true, msg, 200)); } @@ -227,20 +235,20 @@ private void save(Context ctx, String ruleId, boolean isAdd) throws SQLException } if (ruleId == null || ruleId.isEmpty()) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_NO_ID, HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_NO_ID), HttpStatus.BAD_REQUEST.getCode())); return; } - RuleSubInfo ruleSubInfo = ipBlackRuleList.getRuleSubInfo(ruleId); + RuleSubInfoEntity ruleSubInfo = ipBlackRuleList.getRuleSubInfo(ruleId); if (isAdd && ruleSubInfo != null) { // 新增时检查规则是否存在 ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_ID_CONFLICT.replace("{}", ruleId), HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_ID_CONFLICT, ruleId), HttpStatus.BAD_REQUEST.getCode())); return; } if (!isAdd && ruleSubInfo == null) { // 更新时检查规则是否存在 ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_CANT_FIND.replace("{}", ruleId), HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_CANT_FIND, ruleId), HttpStatus.BAD_REQUEST.getCode())); return; } String ruleName = subInfo.ruleName(); @@ -248,27 +256,27 @@ private void save(Context ctx, String ruleId, boolean isAdd) throws SQLException if (isAdd) { if (ruleName == null || subUrl == null || ruleName.isEmpty() || subUrl.isEmpty()) { ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_PARAM_WRONG, HttpStatus.BAD_REQUEST.getCode())); + ctx.json(new SlimMsg(false, tlUI(Lang.IP_BAN_RULE_PARAM_WRONG), HttpStatus.BAD_REQUEST.getCode())); return; } } else { if (ruleName == null) { - ruleName = ruleSubInfo.ruleName(); + ruleName = ruleSubInfo.getRuleName(); } if (subUrl == null) { - subUrl = ruleSubInfo.subUrl(); + subUrl = ruleSubInfo.getSubUrl(); } } - ConfigurationSection configurationSection = ipBlackRuleList.saveRuleSubInfo(new RuleSubInfo(ruleId, isAdd || ruleSubInfo.enabled(), ruleName, subUrl, 0, 0)); + ConfigurationSection configurationSection = ipBlackRuleList.saveRuleSubInfo(new RuleSubInfoEntity(ruleId, isAdd || ruleSubInfo.isEnabled(), ruleName, subUrl, 0, 0)); assert configurationSection != null; try { - SlimMsg msg = ipBlackRuleList.updateRule(configurationSection, IPBanRuleUpdateType.MANUAL); + SlimMsg msg = ipBlackRuleList.updateRule(locale(ctx), configurationSection, IPBanRuleUpdateType.MANUAL); if (!msg.success()) { ctx.status(HttpStatus.BAD_REQUEST); ctx.json(msg); return; } - ctx.json(new SlimMsg(true, Lang.IP_BAN_RULE_SAVED, HttpStatus.CREATED.getCode())); + ctx.json(new SlimMsg(true, tlUI(Lang.IP_BAN_RULE_SAVED), HttpStatus.CREATED.getCode())); } catch (Exception e) { // 更新失败时回滚 if (isAdd) { @@ -277,8 +285,8 @@ private void save(Context ctx, String ruleId, boolean isAdd) throws SQLException ipBlackRuleList.saveRuleSubInfo(ruleSubInfo); } ctx.status(HttpStatus.BAD_REQUEST); - ctx.json(new SlimMsg(false, Lang.IP_BAN_RULE_URL_WRONG.replace("{}", ruleName), HttpStatus.BAD_REQUEST.getCode())); - log.warn("Unable to retrieve the sub from given URL", e); + ctx.json(new SlimMsg(false, tl(locale(ctx), Lang.IP_BAN_RULE_URL_WRONG, ruleName), HttpStatus.BAD_REQUEST.getCode())); + log.error("Unable to retrieve the sub from given URL", e); } } @@ -288,8 +296,8 @@ private void save(Context ctx, String ruleId, boolean isAdd) throws SQLException * @param ruleId 规则ID * @return 响应 */ - private StdMsg get(String ruleId) throws SQLException { - return new StdMsg(true, Lang.IP_BAN_RULE_INFO_QUERY_SUCCESS, ipBlackRuleList.getRuleSubInfo(ruleId)); + private StdMsg get(String locale, String ruleId) throws SQLException { + return new StdMsg(true, tl(locale, Lang.IP_BAN_RULE_INFO_QUERY_SUCCESS), ipBlackRuleList.getRuleSubInfo(ruleId)); } /** @@ -297,13 +305,13 @@ private StdMsg get(String ruleId) throws SQLException { * * @return 响应 */ - private StdMsg list() throws SQLException { + private StdMsg list(String locale) throws SQLException { List list = ipBlackRuleList.getRuleSubsConfig().getKeys(false).stream().toList(); - List data = new ArrayList<>(list.size()); + List data = new ArrayList<>(list.size()); for (String s : list) { data.add(ipBlackRuleList.getRuleSubInfo(s)); } - return new StdMsg(true, Lang.IP_BAN_RULE_INFO_QUERY_SUCCESS, data); + return new StdMsg(true, tl(locale, Lang.IP_BAN_RULE_INFO_QUERY_SUCCESS), data); } /** diff --git a/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/dto/PopulatedPeerDTO.java b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/dto/PopulatedPeerDTO.java new file mode 100644 index 0000000000..04dd8088d1 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/module/impl/webapi/dto/PopulatedPeerDTO.java @@ -0,0 +1,16 @@ +package com.ghostchu.peerbanhelper.module.impl.webapi.dto; + +import com.ghostchu.peerbanhelper.ipdb.IPGeoData; +import com.ghostchu.peerbanhelper.wrapper.PeerWrapper; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Peer DTO 对象,填充了额外的信息 + */ +@AllArgsConstructor +@Data +public final class PopulatedPeerDTO { + private PeerWrapper peer; + private IPGeoData geo; +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/peer/Peer.java b/src/main/java/com/ghostchu/peerbanhelper/peer/Peer.java index a582c297a2..9d8226d977 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/peer/Peer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/peer/Peer.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.peer; import com.ghostchu.peerbanhelper.wrapper.PeerAddress; +import org.jetbrains.annotations.Nullable; public interface Peer extends Comparable { /** @@ -67,7 +68,8 @@ public interface Peer extends Comparable { * * @return Flag */ - String getFlags(); + @Nullable + PeerFlag getFlags(); @Override default int compareTo(Peer o) { @@ -75,6 +77,7 @@ default int compareTo(Peer o) { } default String getCacheKey() { - return "pa=" + this.getPeerAddress().toString() + ",pid=" + this.getPeerId() + ",pname=" + this.getClientName(); + //return "pa=" + this.getPeerAddress().toString() + ",pid=" + this.getPeerId() + ",pname=" + this.getClientName(); + return getPeerAddress().getIp() + ':' + getPeerAddress().getPort(); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/peer/PeerFlag.java b/src/main/java/com/ghostchu/peerbanhelper/peer/PeerFlag.java new file mode 100644 index 0000000000..8fe177dab8 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/peer/PeerFlag.java @@ -0,0 +1,199 @@ +package com.ghostchu.peerbanhelper.peer; + +import lombok.Builder; +import lombok.Data; +import org.jetbrains.annotations.Nullable; + +import java.util.StringJoiner; + +@Data +public final class PeerFlag { + private final String ltStdString; + @Nullable + private Boolean interesting; + @Nullable + private Boolean choked; + @Nullable + private Boolean remoteInterested; + @Nullable + private Boolean remoteChoked; + @Nullable + private Boolean supportsExtensions; + @Nullable + private Boolean outgoingConnection; + @Nullable + private Boolean localConnection; + @Nullable + private Boolean handshake; + @Nullable + private Boolean connecting; + @Nullable + private Boolean onParole; + @Nullable + private Boolean seed; + @Nullable + private Boolean optimisticUnchoke; + @Nullable + private Boolean snubbed; + @Nullable + private Boolean uploadOnly; + @Nullable + private Boolean endGameMode; + @Nullable + private Boolean holePunched; + @Nullable + private Boolean i2pSocket; + @Nullable + private Boolean utpSocket; + @Nullable + private Boolean sslSocket; + @Nullable + private Boolean rc4Encrypted; + @Nullable + private Boolean plainTextEncrypted; + @Nullable + private Boolean fromTracker; + @Nullable + private Boolean fromDHT; + @Nullable + private Boolean fromPEX; + @Nullable + private Boolean fromLSD; + @Nullable + private Boolean fromResumeData; + @Nullable + private Boolean fromIncoming; + + public PeerFlag(String flags) { + parseLibTorrent(flags); + this.ltStdString = toString(); + } + + @Builder + public PeerFlag(boolean interesting, boolean choked, boolean remoteInterested, boolean remoteChoked, boolean supportsExtensions, boolean outgoingConnection, boolean localConnection, boolean handshake, boolean connecting, boolean onParole, boolean seed, boolean optimisticUnchoke, boolean snubbed, boolean uploadOnly, boolean endGameMode, boolean holePunched, boolean i2pSocket, boolean utpSocket, boolean sslSocket, boolean rc4Encrypted, boolean plainTextEncrypted, boolean fromTracker, boolean fromDHT, boolean fromPEX, boolean fromLSD, boolean fromResumeData, boolean fromIncoming) { + this.interesting = interesting; + this.choked = choked; + this.remoteInterested = remoteInterested; + this.remoteChoked = remoteChoked; + this.supportsExtensions = supportsExtensions; + this.outgoingConnection = outgoingConnection; + this.localConnection = localConnection; + this.handshake = handshake; + this.connecting = connecting; + this.onParole = onParole; + this.seed = seed; + this.optimisticUnchoke = optimisticUnchoke; + this.snubbed = snubbed; + this.uploadOnly = uploadOnly; + this.endGameMode = endGameMode; + this.holePunched = holePunched; + this.i2pSocket = i2pSocket; + this.utpSocket = utpSocket; + this.sslSocket = sslSocket; + this.rc4Encrypted = rc4Encrypted; + this.plainTextEncrypted = plainTextEncrypted; + this.fromTracker = fromTracker; + this.fromDHT = fromDHT; + this.fromPEX = fromPEX; + this.fromLSD = fromLSD; + this.fromResumeData = fromResumeData; + this.fromIncoming = fromIncoming; + this.ltStdString = toString(); + } + + @Override + public String toString() { + StringJoiner joiner = new StringJoiner(" "); + if (interesting != null && interesting) { + if (remoteChoked != null && remoteChoked) { + joiner.add("d"); + } else { + joiner.add("D"); + } + } + if (remoteInterested != null && remoteInterested) { + if (choked != null && choked) { + joiner.add("u"); + } else { + joiner.add("U"); + } + } + if (remoteChoked != null && interesting != null) { + if (!remoteChoked && !interesting) + joiner.add("K"); + } + if (choked != null && remoteInterested != null) { + if (!choked && !remoteInterested) + joiner.add("?"); + } + if (optimisticUnchoke != null && optimisticUnchoke) { + joiner.add("O"); + } + if (snubbed != null && snubbed) { + joiner.add("S"); + } + if (localConnection != null && !localConnection) { + joiner.add("I"); + } + if (fromDHT != null && fromDHT) { + joiner.add("H"); + } + if (fromPEX != null && fromPEX) { + joiner.add("X"); + } + if (fromLSD != null && fromLSD) { + joiner.add("L"); + } + if (rc4Encrypted != null && rc4Encrypted) { + joiner.add("E"); + } + if (plainTextEncrypted != null && plainTextEncrypted) { + joiner.add("e"); + } + if (utpSocket != null && utpSocket) { + joiner.add("P"); + } + return joiner.toString(); + } + + public void parseLibTorrent(String flags) { + for (char c : flags.toCharArray()) { + switch (c) { + case 'd' -> { + interesting = true; + remoteChoked = true; + } + case 'D' -> { + interesting = true; + remoteChoked = false; + } + case 'u' -> { + remoteInterested = true; + choked = true; + } + case 'U' -> { + remoteInterested = true; + choked = false; + } + case 'K' -> { + remoteChoked = false; + interesting = false; + } + case '?' -> { + choked = false; + remoteInterested = false; + } + case 'O' -> optimisticUnchoke = true; + case 'S' -> snubbed = false; + case 'I' -> localConnection = false; + case 'H' -> fromDHT = true; + case 'X' -> fromPEX = true; + case 'L' -> fromLSD = true; + case 'E' -> rc4Encrypted = true; + case 'e' -> plainTextEncrypted = true; + case 'P' -> utpSocket = true; + } + } + } + +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/peer/PeerImpl.java b/src/main/java/com/ghostchu/peerbanhelper/peer/PeerImpl.java index ff7b1791c1..1e58b591c1 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/peer/PeerImpl.java +++ b/src/main/java/com/ghostchu/peerbanhelper/peer/PeerImpl.java @@ -13,9 +13,9 @@ public class PeerImpl implements Peer { private long uploadSpeed; private long uploaded; private double progress; - private String flags; + private PeerFlag flags; - public PeerImpl(PeerAddress peerAddress, String peerId, String clientName, long downloadSpeed, long downloaded, long uploadSpeed, long uploaded, double progress, String flags) { + public PeerImpl(PeerAddress peerAddress, String peerId, String clientName, long downloadSpeed, long downloaded, long uploadSpeed, long uploaded, double progress, PeerFlag flags) { this.peerAddress = peerAddress; this.peerId = peerId; this.clientName = clientName; @@ -68,7 +68,7 @@ public double getProgress() { } @Override - public String getFlags() { + public PeerFlag getFlags() { return flags; } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java b/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java index 8fcc1cf0b9..3006c82fdd 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java +++ b/src/main/java/com/ghostchu/peerbanhelper/text/Lang.java @@ -1,278 +1,304 @@ package com.ghostchu.peerbanhelper.text; -import lombok.Getter; +public enum Lang { + ERR_BUILD_NO_INFO_FILE, + ERR_CANNOT_LOAD_BUILD_INFO, + MOTD, + LOADING_CONFIG, + CONFIG_PEERBANHELPER, + ERR_SETUP_CONFIGURATION, + DISCOVER_NEW_CLIENT, + ERR_INITIALIZE_BAN_PROVIDER_ENDPOINT_FAILURE, + WAIT_FOR_MODULES_STARTUP, + MODULE_REGISTER, + MODULE_UNREGISTER, + ERR_CLIENT_LOGIN_FAILURE_SKIP, + ERR_UNEXPECTED_API_ERROR, + PEER_UNBAN_WAVE, + ERR_UPDATE_BAN_LIST, + BAN_PEER, + CHECK_COMPLETED, + ERR_INVALID_RULE_SYNTAX, + MODULE_CNB_MATCH_CLIENT_NAME, + MODULE_IBL_MATCH_IP, + MODULE_IBL_MATCH_IP_RULE, + MODULE_IBL_MATCH_ASN, + MODULE_IBL_MATCH_REGION, + MODULE_IBL_EXCEPTION_GEOIP, + MODULE_IBL_MATCH_PORT, + MODULE_PID_MATCH_PEER_ID, + MODULE_PCB_EXCESSIVE_DOWNLOAD, + MODULE_PCB_PEER_MORE_THAN_LOCAL_SKIP, + MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS, + MODULE_PCB_PEER_BAN_REWIND, + MODULE_PCB_SKIP_UNKNOWN_SIZE_TORRENT, + GUI_BUTTON_RESIZE_TABLE, + MODULE_AP_SSL_CONTEXT_FAILURE, + MODULE_MDB_MULTI_DIALING_NOT_DETECTED, + MODULE_MDB_MULTI_DIALING_DETECTED, + MODULE_MDB_MULTI_DIALING_HUNTING_TRIGGERED, + DOWNLOADER_QB_LOGIN_FAILED, + DOWNLOADER_QB_FAILED_REQUEST_TORRENT_LIST, + DOWNLOADER_QB_FAILED_REQUEST_PEERS_LIST_IN_TORRENT, + DOWNLOADER_QB_API_PREFERENCES_ERR, + DOWNLOADER_QB_FAILED_SAVE_BANLIST, + DOWNLOADER_TR_MOTD_WARNING, + DOWNLOADER_TR_DISCONNECT_PEERS, + DOWNLOADER_TR_INCORRECT_BANLIST_API_RESP, + DOWNLOADER_TR_INCORRECT_SET_BANLIST_API_RESP, + DOWNLOADER_TR_INVALID_RESPONSE, + DOWNLOADER_TR_UPDATED_BLOCKLIST, + DOWNLOADER_TR_KNOWN_INCOMPATIBILITY, + DOWNLOADER_TR_INCOMPATIBILITY_BANAPI, + ERR_CONFIG_DIRECTORY_INCORRECT, + GUI_MENU_OPEN_DATA_DIRECTORY, + PBH_SHUTTING_DOWN, + ARB_BANNED, + DATABASE_SETUP_FAILED, + DATABASE_SAVE_BUFFER_FAILED, + WEB_BANLOGS_INTERNAL_ERROR, + BOOTSTRAP_FAILED, + DATABASE_FAILURE, + CONFIGURATION_OUTDATED_MODULE_DISABLED, + BTN_DOWNLOADER_GENERAL_FAILURE, + BTN_UPDATE_RULES_SUCCESSES, + BTN_REQUEST_FAILS, + BTN_CONFIG_FAILS, + MODULE_BTN_BAN, + BTN_NETWORK_CONNECTING, + BTN_NETWORK_NOT_ENABLED, + BTN_NETWORK_ENABLED, + BANLIST_INVOKER_REGISTERED, + BANLIST_INVOKER_IPFILTER_FAIL, + BANLIST_INVOKER_COMMAND_EXEC_TIMEOUT, + BANLIST_INVOKER_COMMAND_EXEC_FAILED, + BTN_INCOMPATIBLE_SERVER, + BTN_SUBMITTING_PEERS, + BTN_SUBMITTED_PEERS, + BTN_SUBMITTING_BANS, + BTN_SUBMITTED_BANS, + BTN_SUBMITTING_HITRATE, + BTN_SUBMITTED_HITRATE, + CONFIG_CHECKING, + CONFIG_MIGRATING, + CONFIG_EXECUTE_MIGRATE, + CONFIG_MIGRATE_FAILED, + CONFIG_UPGRADED, + CONFIG_SAVE_CHANGES, + CONFIG_SAVE_ERROR, + BTN_RECONFIGURE_CHECK_FAILED, + BTN_SHUTTING_DOWN, + BTN_RECONFIGURING, + RULE_MATCHER_STRING_CONTAINS, + RULE_MATCHER_STRING_ENDS_WITH, + RULE_MATCHER_STRING_STARTS_WITH, + RULE_MATCHER_STRING_LENGTH, + RULE_MATCHER_STRING_REGEX, + RULE_MATCHER_SUB_RULE, + RESET_DOWNLOADER_FAILED, + DOWNLOADER_QB_INCREAMENT_BAN_FAILED, + SHUTDOWN_CLOSE_METRICS, + SHUTDOWN_UNREGISTER_MODULES, + SHUTDOWN_CLOSE_DATABASE, + SHUTDOWN_CLEANUP_RESOURCES, + SHUTDOWN_DONE, + SAVED_BANLIST, + SAVE_BANLIST_FAILED, + LOAD_BANLIST_FROM_FILE, + LOAD_BANLIST_FAIL, + GUI_MENU_PROGRAM, + GUI_MENU_WEBUI, + GUI_MENU_WEBUI_OPEN, + GUI_MENU_ABOUT, + GUI_MENU_QUIT, + GUI_COPY_WEBUI_TOKEN, + GUI_TRAY_MESSAGE_CAPTION, + GUI_TRAY_MESSAGE_DESCRIPTION, + GUI_TABBED_LOGS, + GUI_TABBED_PEERS, + ABOUT_VIEW_GITHUB, + IPDB_UPDATING, + IPDB_UPDATE_FAILED, + IPDB_UPDATE_SUCCESS, + IPDB_INVALID, + IPDB_NEED_CONFIG, + DOWNLOAD_PROGRESS_DETERMINED, + DOWNLOAD_PROGRESS, + DOWNLOAD_COMPLETED, + BAN_WAVE_CHECK_COMPLETED, + WATCH_DOG_HUNGRY, + WATCH_DOG_CALLBACK_BLOCKED, + PBH_BAN_WAVE_STARTED, + BAN_WAVE_WATCH_DOG_TITLE, + BAN_WAVE_WATCH_DOG_DESCRIPTION, + INTERNAL_ERROR, + PART_TASKS_TIMED_OUT, + TOO_WEAK_TOKEN, + TIMING_RECOVER_PERSISTENT_BAN_LIST, + TIMING_CHECK_BANS, + TIMING_ADD_BANS, + TIMING_APPLY_BAN_LIST, + TIMING_COLLECT_PEERS, + TIMING_UNFINISHED_TASK, + CONFIGURATION_INVALID, + CONFIGURATION_INVALID_TITLE, + CONFIGURATION_INVALID_DESCRIPTION, + TRCLIENT_API_ERROR, + IP_BAN_RULE_MATCH_ERROR, + IP_BAN_RULE_MATCH_TIME, + IP_BAN_RULE_UPDATE_TYPE_AUTO, + IP_BAN_RULE_UPDATE_TYPE_MANUAL, + IP_BAN_RULE_UPDATE_FINISH, + IP_BAN_RULE_NO_UPDATE, + IP_BAN_RULE_UPDATE_SUCCESS, + IP_BAN_RULE_UPDATE_FAILED, + IP_BAN_RULE_LOAD_SUCCESS, + IP_BAN_RULE_UPDATE_LOG_ERROR, + IP_BAN_RULE_USE_CACHE, + IP_BAN_RULE_LOAD_FAILED, + IP_BAN_RULE_LOAD_CIDR, + IP_BAN_RULE_LOAD_IP, + RULE_SUB_API_INTERNAL_ERROR, + IP_BAN_RULE_NO_ID, + IP_BAN_RULE_ID_CONFLICT, + IP_BAN_RULE_CANT_FIND, + IP_BAN_RULE_PARAM_WRONG, + IP_BAN_RULE_URL_WRONG, + IP_BAN_RULE_ENABLED, + IP_BAN_RULE_DISABLED, + IP_BAN_RULE_UPDATED, + IP_BAN_RULE_ALL_UPDATED, + IP_BAN_RULE_SAVED, + IP_BAN_RULE_DELETED, + IP_BAN_RULE_INFO_QUERY_SUCCESS, + IP_BAN_RULE_LOG_QUERY_SUCCESS, + IP_BAN_RULE_LOG_QUERY_ERROR, + IP_BAN_RULE_LOG_QUERY_WRONG_PARAM, + IP_BAN_RULE_CHECK_INTERVAL_QUERY_SUCCESS, + IP_BAN_RULE_CHECK_INTERVAL_WRONG_PARAM, + IP_BAN_RULE_CHECK_INTERVAL_UPDATED, + IP_BAN_RULE_ENABLED_WRONG_PARAM, + WEBAPI_AUTH_INVALID_TOKEN, + WEBAPI_AUTH_OK, + WEBAPI_AUTH_BANNED_TOO_FREQ, + WEBAPI_NOT_LOGGED, + WEBAPI_INTERNAL_ERROR, + GITHUB_PAGE, + GUI_COPY_TO_CLIPBOARD_TITLE, + GUI_COPY_TO_CLIPBOARD_DESCRIPTION, + GUI_TITLE_LOADING, + GUI_TITLE_LOADED, + WEBVIEW_DISABLED_WEBKIT_NOT_INCLUDED, + WEBVIEW_ENABLED, + STATUS_TEXT_OK, + STATUS_TEXT_LOGIN_FAILED, + STATUS_TEXT_EXCEPTION, + STATUS_TEXT_NEED_PRIVILEGE, + SUGGEST_FIREWALL_IPTABELS, + SUGGEST_FIREWALL_FIREWALLD, + SUGGEST_FIREWALL_WINDOWS_FIREWALL_DISABLED, + MODULE_EXPRESSION_RULE_BAD_EXPRESSION, + MODULE_EXPRESSION_RULE_COMPILING, + MODULE_EXPRESSION_RULE_COMPILED, + MODULE_EXPRESSION_RULE_INVALID_RETURNS, + MODULE_EXPRESSION_RULE_TIMEOUT, + MODULE_EXPRESSION_RULE_ERROR, + MODULE_EXPRESSION_RULE_RELEASE_FILE_FAILED, + JFX_WEBVIEW_ALERT, + DATABASE_OUTDATED_LOGS_CLEANED_UP, + LIBRARIES_LOADER_DETERMINE_BEST_MIRROR, + LIBRARIES_LOADER_DETERMINE_TEST_RESULT, + LIBRARIES_DOWNLOAD_DIALOG_TITLE, + LIBRARIES_DOWNLOAD_DIALOG_DESCRIPTION, + LIBRARIES_DOWNLOAD_DIALOG_BAR_TEXT, + LIBRARIES_DOWNLOAD_DIALOG_TOOLTIP, + LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER, + LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_DESCRIPTION, + LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_TOOLTIP, + LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_BAR_TEXT, + WEBVIEW_RELOAD_PAGE, + WEBVIEW_RESET_PAGE, + WEBVIEW_BACK, + WEBVIEW_FORWARD, + DOWNLOADER_API_ADD_FAILURE, + DOWNLOADER_API_CREATED, + DOWNLOADER_API_UPDATED, + DOWNLOADER_API_CREATION_FAILED_ALREADY_EXISTS, + DOWNLOADER_API_CREATION_FAILED_IO_EXCEPTION, + DOWNLOADER_API_UPDATE_FAILURE, + DOWNLOADER_API_UPDATE_FAILURE_ALREADY_EXISTS, + DOWNLOADER_API_TEST_NAME_EXISTS, + DOWNLOADER_API_TEST_OK, + DOWNLOADER_API_REMOVE_NOT_EXISTS, + DOWNLOADER_API_REMOVE_SAVED, + DOWNLOADER_API_DOWNLOADER_NOT_EXISTS, + DOWNLOADER_BIGLYBT_INCORRECT_RESPONSE, + DOWNLOADER_BIGLYBT_FAILED_REQUEST_PEERS_LIST_IN_TORRENT, + DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED, + DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST, + ALERT_INCORRECT_PROXY_SETTING, + COMMAND_EXECUTOR, + COMMAND_EXECUTOR_FAILED, + COMMAND_EXECUTOR_FAILED_TIMEOUT, + DOWNLOADER_DELUGE_PLUGIN_NOT_INSTALLED, + DOWNLOADER_DELUGE_API_ERROR, + DOWNLOADER_UNHANDLED_EXCEPTION, + WEB_ENDPOINT_REGISTERED, + SKIP_LOAD_PLUGIN_FOR_NATIVE_IMAGE, + ERR_CANNOT_LOAD_PLUGIN, + ERR_CANNOT_UNLOAD_PLUGIN, + ARB_ERROR_TO_CONVERTING_IP, + DATABASE_BUFFER_SAVED, + PERSIST_DISABLED, + BTN_PREPARE_TO_SUBMIT, + BTN_UPDATE_RULES, + BTN_NETWORK_RECONFIGURED, + PERSIST_CLEAN_LOGS, + BAN_PEER_REVERSE_LOOKUP, + RULE_ENGINE_PARSE_FAILED, + RULE_ENGINE_INVALID_RULE, + RULE_ENGINE_NOT_A_RULE, + RULE_MATCHER_STRING_EQUALS, + NEW_SETUP_NO_DOWNLOADERS, + IP_BLACKLIST_PORT_RULE, + IP_BLACKLIST_CIDR_RULE, + IP_BLACKLIST_ASN_RULE, + IP_BLACKLIST_REGION_RULE, + AUTO_RANGE_BAN_IPV4_RULE, + AUTO_RANGE_BAN_IPV6_RULE, + GENERAL_NA, + PCB_RULE_REACHED_MAX_ALLOWED_EXCESSIVE_THRESHOLD, + PCB_RULE_REACHED_MAX_DIFFERENCE, + PCB_RULE_PROGRESS_REWIND, + MDB_MULTI_DIALING_DETECTED, + MDB_MULTI_HUNTING, + BTN_PORT_RULE, + BTN_IP_RULE, + BTN_BTN_RULE, + DUPLICATE_BAN, + NET_TYPE_WIDEBAND, + NET_TYPE_BASE_STATION, + NET_TYPE_GOVERNMENT_AND_ENTERPRISE_LINE, + NET_TYPE_BUSINESS_PLATFORM, + NET_TYPE_BACKBONE_NETWORK, + NET_TYPE_IP_PRIVATE_NETWORK, + NET_TYPE_INTERNET_CAFE, + NET_TYPE_IOT, + NET_TYPE_DATACENTER, + WEBUI_VALIDATION_DOWNLOAD_LOGIN_FAILED, + DOWNLOADER_LOGIN_EXCEPTION, + DOWNLOADER_LOGIN_INCORRECT_CRED, + STATUS_TEXT_UNKNOWN, + DOWNLOADER_LOGIN_IO_EXCEPTION, + USER_SCRIPT_RUN_RESULT, + USER_SCRIPT_RULE, + USER_MANUALLY_BAN_RULE, + USER_MANUALLY_BAN_REASON, + SCHEDULED_OPERATIONS -@Getter -public class Lang { - public static final String ERR_BUILD_NO_INFO_FILE = "错误:构建信息文件不存在"; - public static final String ERR_CANNOT_LOAD_BUILD_INFO = "错误:无法加载构建信息文件"; - public static final String MOTD = "PeerBanHelper v{} - by PBH-BTN Community, Made with ❤"; - public static final String LOADING_CONFIG = "正在加载配置文件……"; - public static final String CONFIG_PEERBANHELPER = "已初始化目录结构,相关文件已放置在运行目录的 data 文件夹下,请配置相关文件后,再重新启动 PeerBanHelper"; - public static final String ERR_SETUP_CONFIGURATION = "错误:无法初始化配置文件结构"; - public static final String DISCOVER_NEW_CLIENT = " + {} -> {} ({})"; - public static final String ERR_INITIALIZE_BAN_PROVIDER_ENDPOINT_FAILURE = "错误:无法初始化 API 提供端点,Transmission 模块的封禁功能将不起作用"; - public static final String WAIT_FOR_MODULES_STARTUP = "请等待功能模块初始化……"; - public static final String MODULE_REGISTER = "[注册] {}"; - public static final String MODULE_UNREGISTER = "[解注册] {}"; - public static final String ERR_CLIENT_LOGIN_FAILURE_SKIP = "登录到 {} ({}) 失败,跳过……"; - public static final String ERR_UNEXPECTED_API_ERROR = "在处理 {} ({}) 的 API 操作时出现了一个非预期的错误"; - public static final String PEER_UNBAN_WAVE = "[解封] 解除了 {} 个过期的对等体封禁"; - public static final String ERR_UPDATE_BAN_LIST = "在更新 {} ({}) 的封禁列表时出现了一个非预期的错误"; - public static final String BAN_PEER = "[封禁] {}, PeerId={}, ClientName={}, Progress={}, Uploaded={}, Downloaded={}, Torrent={}, Reason={}"; - public static final String CHECK_COMPLETED = "[完成] 已检查 {} 的 {} 个活跃 Torrent 和 {} 个对等体"; - public static final String ERR_INVALID_RULE_SYNTAX = "规则 {} 的表达式无效,请检查是否存在拼写错误"; - public static final String MODULE_CNB_MATCH_CLIENT_NAME = "匹配 ClientName (UserAgent): %s"; - public static final String MODULE_IBL_MATCH_IP = "匹配 IP 规则: %s"; - public static final String MODULE_IBL_MATCH_IP_RULE = "匹配 IP黑名单订阅 规则: %s"; - public static final String MODULE_IBL_MATCH_ASN = "匹配 ASN 规则: %s"; - public static final String MODULE_IBL_MATCH_REGION = "匹配国家或地区 ISO 代码规则: %s"; - public static final String MODULE_IBL_EXCEPTION_GEOIP = "匹配 GeoIP 信息时出现异常,请反馈错误给开发者"; - public static final String MODULE_IBL_MATCH_PORT = "匹配 Port 规则: %s"; - public static final String MODULE_PID_MATCH_PEER_ID = "匹配 PeerId 规则: %s"; - public static final String MODULE_PCB_EXCESSIVE_DOWNLOAD = "客户端下载过量:种子大小:%d,上传给此对等体的总量:%d,最大允许的过量下载总量:%d"; - public static final String MODULE_PCB_PEER_MORE_THAN_LOCAL_SKIP = "客户端进度:%s,实际进度:%s,客户端的进度多于本地进度,跳过检测"; - public static final String MODULE_PCB_PEER_BAN_INCORRECT_PROGRESS = "客户端进度:%s,实际进度:%s,差值:%s"; - public static final String MODULE_PCB_PEER_BAN_REWIND = "客户端进度:%s,实际进度:%s,上次记录进度:%s,本次进度:%s,差值:%s"; - public static final String MODULE_PCB_SKIP_UNKNOWN_SIZE_TORRENT = "种子大小未知"; - public static final String GUI_BUTTON_RESIZE_TABLE = "点击调整列宽"; - public static final String MODULE_AP_SSL_CONTEXT_FAILURE = "初始化 SSLContext 时出错"; - public static final String MODULE_MDB_MULTI_DIALING_NOT_DETECTED = "未发现多拨下载"; - public static final String MODULE_MDB_MULTI_DIALING_DETECTED = "发现多拨下载,请持续关注,子网:%s,触发IP:%s"; - public static final String MODULE_MDB_MULTI_DIALING_HUNTING_TRIGGERED = "触发多拨追猎名单,子网:%s,触发IP:%s"; - public static final String DOWNLOADER_QB_LOGIN_FAILED = "登录到 {} 失败:{} - {}: {}"; - public static final String DOWNLOADER_QB_FAILED_REQUEST_TORRENT_LIST = "请求 Torrents 列表失败 - %d - %s"; - public static final String DOWNLOADER_QB_FAILED_REQUEST_PEERS_LIST_IN_TORRENT = "请求 Torrent 的 Peers 列表失败 - %d - %s"; - public static final String DOWNLOADER_QB_API_PREFERENCES_ERR = "qBittorrent 的首选项 API 返回了非 200 预期响应 - %d - %s"; - public static final String DOWNLOADER_QB_FAILED_SAVE_BANLIST = "无法保存 {} ({}) 的 Banlist!{} - {}\n{}"; - public static final String DOWNLOADER_TR_MOTD_WARNING = "[受限] 由于 Transmission 的 RPC-API 限制,PeerId 黑名单功能和 ProgressCheatBlocker 功能的过量下载模块不可用"; - public static final String DOWNLOADER_TR_DISCONNECT_PEERS = "[重置] 正在断开 Transmission 上的 {} 个种子连接的对等体,以便应用 IP 屏蔽列表的更改"; - public static final String DOWNLOADER_TR_INCORRECT_BANLIST_API_RESP = "设置 Transmission 的 BanList 地址时,返回非成功响应:{}。"; - public static final String DOWNLOADER_TR_INCORRECT_SET_BANLIST_API_RESP = """ - 无法应用 IP 黑名单到 Transmission,PBH 没有生效! - 请求 Transmission 更新 BanList 时,返回非成功响应。 - 您是否正确映射了 PeerBanHelper 的外部交互端口,以便 Transmission 从 PBH 拉取 IP 黑名单? - 检查 Transmission 的 设置 -> 隐私 -> 屏蔽列表 中自动填写的 URL 是否正确,如果不正确,请在 PeerBanHelper 的 config.yml 中正确配置 server 部分的配置文件,确保 Transmission 能够正确连接到 IP 黑名单提供端点 - """; - public static final String DOWNLOADER_TR_INVALID_RESPONSE = "[错误] Transmission 返回无效 JSON 响应: {}"; - public static final String DOWNLOADER_TR_UPDATED_BLOCKLIST = "[响应] Transmission 屏蔽列表已更新成功,现在包含 {} 条规则"; - public static final String DOWNLOADER_TR_KNOWN_INCOMPATIBILITY = "[错误] 您正在使用的 Transmission 版本与 PeerBanHelper 不兼容: %s"; - public static final String DOWNLOADER_TR_INCOMPATIBILITY_BANAPI = "当前版本存在封禁 API 的已知问题,请升级至 3.0-20 或更高版本"; - public static final String ERR_CONFIG_DIRECTORY_INCORRECT = "初始化失败:config 不是一个目录。如果您正在使用 Docker,请确保其正确挂载。"; - public static final String GUI_MENU_OPEN_DATA_DIRECTORY = "打开数据文件存储位置..."; + ; - public static String WEB_ENDPOINT_REGISTERED = "[注册] WebAPI 端点已注册:{}"; - public static String SKIP_LOAD_PLUGIN_FOR_NATIVE_IMAGE = "检测到Native Images,跳过加载插件"; - public static final String PBH_SHUTTING_DOWN = "[退出] 正在退出,请等待我们完成剩余的工作……"; - public static String ERR_CANNOT_LOAD_PLUGIN = "[注册] 无法加载插件:{}"; - public static String ERR_CANNOT_UNLOAD_PLUGIN = "[退出] 无法卸载插件:{}"; - public static String ARB_ERROR_TO_CONVERTING_IP = "IP 地址 %s 既不是 IPV4 地址也不是 IPV6 地址。"; - public static final String ARB_BANNED = "IP 地址 %s 与另一个已封禁的 IP 地址 %s 处于同一封禁区间内,执行连锁封禁操作。"; - public static final String DATABASE_SETUP_FAILED = "[错误] 数据库初始化失败"; - public static String DATABASE_BUFFER_SAVED = "[保存] 已保存 {} 条内存缓存的封禁日志到数据库,用时 {}ms"; - public static final String DATABASE_SAVE_BUFFER_FAILED = "[错误] 刷写内存缓存的封禁日志时出现了 SQL 错误,未保存的数据已被丢弃"; - public static final String WEB_BANLOGS_INTERNAL_ERROR = "[错误] 读取封禁日志时遇到非预期错误"; - public static String PERSIST_DISABLED = "[禁用] Persist 持久化数据存储已在此服务器上被禁用"; - public static final String BOOTSTRAP_FAILED = "[错误] PeerBanHelper 启动失败,遇到致命错误,请检查控制台日志"; - public static final String DATABASE_FAILURE = "[错误] 无法连接到持久化数据存储数据库,请检查是否同时启动了多个 PBH 示例?(如果 SQLite 数据库损坏,请删除它,PBH 将会重新生成新的数据库文件)"; - public static final String CONFIGURATION_OUTDATED_MODULE_DISABLED = "[警告] 无法确认功能模块 {} 的配置状态。配置文件似乎已过期,因此无法读取此模块的模块配置文件"; - public static final String BTN_DOWNLOADER_GENERAL_FAILURE = "[BTN 网络] 从下载器 {} 获取当前 Torrent 任务信息失败,跳过……"; - public static String BTN_PREPARE_TO_SUBMIT = "[BTN 网络] 已收集了 {} 个 Peer 信息,将分为 {} 次提交到 BTN 网络,感谢您对 BTN 网络做出的贡献"; - public static String BTN_UPDATE_RULES = "[BTN 网络] 正在连接到 BTN 网络服务器并更新规则数据,本地数据版本:{}"; - public static final String BTN_UPDATE_RULES_SUCCESSES = "[BTN 网络] 规则数据更新成功,当前数据版本:{}"; - public static final String BTN_REQUEST_FAILS = "[BTN 网络] 请求时出现错误,操作已取消 {}"; - public static final String BTN_CONFIG_FAILS = "[BTN 网络] 所连接的 BTN 网络实例未返回有效配置响应,BTN 网络功能可能不会正常工作 {}"; - public static final String MODULE_BTN_BAN = "[BTN 封禁] 匹配 %s 规则集(%s):%s"; - public static final String BTN_NETWORK_CONNECTING = "[BTN 网络] 请等待我们连接到 BTN 网络……"; - public static final String BTN_NETWORK_NOT_ENABLED = "[BTN 网络] 未启用 BTN 功能:此 PeerBanHelper 客户端未加入 BTN 网络"; - public static final String BTN_NETWORK_ENABLED = "[BTN 网络] 功能已启用"; - public static String BTN_NETWORK_RECONFIGURED = "[BTN 网络] 服务器配置信息下发成功,已连接至 BTN 网络:{}"; - public static String PERSIST_CLEAN_LOGS = "[清理] 已成功清理 {} 条封禁日志"; - public static final String BANLIST_INVOKER_REGISTERED = "[BanListInvoker] 已注册:{}"; - public static final String BANLIST_INVOKER_IPFILTER_FAIL = "[BanListInvoker] 清空 ipfilter.dat 文件失败,出现 I/O 错误"; - public static final String BANLIST_INVOKER_COMMAND_EXEC_TIMEOUT = "[BanListInvoker] 执行命令 {} 时超时,PBH 不再继续等待进程"; - public static final String BANLIST_INVOKER_COMMAND_EXEC_FAILED = "[BanListInvoker] 执行命令 {} 时,进程返回非零状态码({}),这可能意味着命令未被成功执行,请查看"; - public static String BAN_PEER_REVERSE_LOOKUP = "[DNS反向查找] IP 地址 {} 反向 DNS 记录为:{}"; - public static final String BTN_INCOMPATIBLE_SERVER = "[BTN 网络] 您所连接的 BTN 实例与当前 BTN 客户端不兼容"; - public static final String BTN_SUBMITTING_PEERS = "[BTN 网络] 计划任务正在向 BTN 网络提交目前下载的 Peers 列表,请稍等……"; - public static final String BTN_SUBMITTED_PEERS = "[BTN 网络] 已向 BTN 网络提交 {} 个 Peers,感谢您对 BTN 网络的支持!"; - public static final String BTN_SUBMITTING_BANS = "[BTN 网络] 计划任务正在向 BTN 网络提交自上次汇报以来新增的封禁条目,请稍等……"; - public static final String BTN_SUBMITTED_BANS = "[BTN 网络] 已向 BTN 网络提交 {} 个封禁记录,感谢您对 BTN 网络的支持!"; - public static final String BTN_SUBMITTING_HITRATE = "[BTN 网络] 计划任务正在向 BTN 网络回报规则命中率数据,请稍等"; - public static final String BTN_SUBMITTED_HITRATE = "[BTN 网络] 已向 BTN 网络回报 {} 个规则的命中率数据,感谢您对 BTN 网络的支持!"; - public static final String CONFIG_CHECKING = "[配置升级实用工具] 请等待检查配置文件更新……"; - public static final String CONFIG_MIGRATING = "[配置升级实用工具] 迁移配置文件:从 {} 至 {} ……"; - public static final String CONFIG_EXECUTE_MIGRATE = "[配置升级实用工具] 执行配置文件升级脚本:{}"; - public static final String CONFIG_MIGRATE_FAILED = "[配置升级实用工具] 执行配置文件升级脚本 {}(升级到版本 {})时出现了错误,PeerBanHelper 可能无法正常运行:{}"; - public static final String CONFIG_UPGRADED = "[配置升级实用工具] 成功升级配置文件到版本 {}"; - public static final String CONFIG_SAVE_CHANGES = "[配置升级实用工具] 正在保存更改……"; - public static final String CONFIG_SAVE_ERROR = "[配置升级实用工具] 更改保存到磁盘失败"; - public static final String BTN_RECONFIGURE_CHECK_FAILED = "[BTN 网络] 检查重配置状态失败:{}"; - public static final String BTN_SHUTTING_DOWN = "[BTN 网络] 正在关闭 BTN 模块……"; - public static final String BTN_RECONFIGURING = "[BTN 网络] 发现服务器基本配置更新,正在重新配置 BTN 网络模块……"; - public static String RULE_ENGINE_PARSE_FAILED = "[规则引擎] 规则 {} 解析失败,解析过程中出现错误"; - public static String RULE_ENGINE_INVALID_RULE = "规则 {} 的参数 {} 无效,仅接受以下值:{}"; - public static String RULE_ENGINE_NOT_A_RULE = "[规则引擎] 表达式 {} 不是一个有效规则"; - public static final String RULE_MATCHER_STRING_CONTAINS = "子串匹配"; - public static final String RULE_MATCHER_STRING_ENDS_WITH = "匹配结尾"; - public static final String RULE_MATCHER_STRING_STARTS_WITH = "匹配开头"; - public static String RULE_MATCHER_STRING_EQUALS = "匹配相同"; - public static final String RULE_MATCHER_STRING_LENGTH = "匹配长度"; - public static final String RULE_MATCHER_STRING_REGEX = "匹配正则"; - public static final String RULE_MATCHER_SUB_RULE = "订阅规则"; - public static final String RESET_DOWNLOADER_FAILED = "[警告] 重置下载器封禁列表到初始状态时出现错误"; - public static final String DOWNLOADER_QB_INCREAMENT_BAN_FAILED = "[错误] 向下载器请求增量封禁对等体时出现错误,请在配置文件中关闭增量封禁(increment-ban)配置项"; - public static final String SHUTDOWN_CLOSE_METRICS = "[退出] 正在保存封禁日志和统计数据……"; - public static final String SHUTDOWN_UNREGISTER_MODULES = "[退出] 正在注销功能模块……"; - public static final String SHUTDOWN_CLOSE_DATABASE = "[退出] 正在安全关闭并保存持久化数据库……"; - public static final String SHUTDOWN_CLEANUP_RESOURCES = "[退出] 清理资源……"; - public static final String SHUTDOWN_DONE = "[退出] 全部完成!"; - public static final String SHUTDOWN_SAVE_BANLIST = "[退出] 正在保存封禁列表到本地缓存文件……"; - public static final String SHUTDOWN_SAVE_BANLIST_FAILED = "[退出] 保存封禁列表到文件失败"; - public static final String LOAD_BANLIST_FROM_FILE = "[封禁] 已从保存的封禁列表缓存文件中恢复了 {} 个封禁项"; - public static final String LOAD_BANLIST_FAIL = "[封禁] 加载封禁列表过程出现错误"; - public static final String GUI_MENU_PROGRAM = "程序"; - public static final String GUI_MENU_WEBUI = "WebUI"; - public static final String GUI_MENU_WEBUI_OPEN = "打开 WebUI..."; - public static final String GUI_MENU_ABOUT = "关于"; - public static final String GUI_MENU_QUIT = "退出"; - public static final String GUI_COPY_WEBUI_TOKEN = "复制 WebUI Token..."; - public static final String GUI_TRAY_MESSAGE_CAPTION = "PeerBanHelper 正在后台运行"; - public static final String GUI_TRAY_MESSAGE_DESCRIPTION = "点击托盘图标重新打开窗口;右键托盘图标可完全退出"; - public static final String GUI_TABBED_LOGS = "运行日志"; - public static final String GUI_TABBED_PEERS = "已连接的Peers"; - public static final String ABOUT_VIEW_GITHUB = "查看 Github 页面..."; - public static final String IPDB_UPDATING = "{} 数据库已过期且需要更新,请等待 PBH 连接到 Maxmind 服务器更新数据……"; - public static final String IPDB_UPDATE_FAILED = "从 Maxmind 下载数据库 {} 时出现错误:{}"; - public static final String IPDB_UPDATE_SUCCESS = "从 Maxmind 更新数据库 {} 成功!"; - public static final String IPDB_INVALID = "由于在初始化过程中出现错误,IPDB 功能已被自动禁用。请检查日志文件以修复问题"; - public static final String IPDB_NEED_CONFIG = "IPDB 功能需要配置才能使用,请在 config.yml 的 ip-database 中填写相关配置信息"; - public static final String DOWNLOAD_PROGRESS_DETERMINED = "下载进度:已下载 {}/{} 字节,进度:{}%"; - public static final String DOWNLOAD_PROGRESS = "下载进度:已下载 {} 字节"; - public static final String DOWNLOAD_COMPLETED = "下载进度:已完成!共传输 {} 字节的数据"; - public static final String[] GUI_LIVE_PEERS_COLUMN_NAMES = {"国家/地区", "IP地址", "PeerID", "客户端", "汇报进度", "上传速度", "上传量", "下载速度", "下载量", "Torrent", "城市", "ASN", "AS组织", "AS网络"}; - public static final String BAN_WAVE_CHECK_COMPLETED = "已检查 {} 个下载器的 {} 个活跃 Torrent 与 {} 个 Peers。共封禁 {} 个 Peers,并解除 {} 个过期的封禁 ({}ms)"; - public static final String WATCH_DOG_HUNGRY = "[警告] WatchDog Service {} 未在指定时间 {} 内得到重置,最后状态 {},正在转储进程线程信息,请发送给 PeerBanHelper 开发者以协助修复此问题"; - public static final String WATCH_DOG_CALLBACK_BLOCKED = "[错误] WatchDog Service 回调线程无响应,已强制离开回调"; - public static final String PBH_BAN_WAVE_STARTED = "PeerBanHelper BanWave Daemon 已启动"; - public static final String BAN_WAVE_WATCH_DOG_TITLE = "PeerBanHelper 正尝试从异常中恢复"; - public static final String BAN_WAVE_WATCH_DOG_DESCRIPTION = "我们检测到封禁线程因未知原因停止响应,因此 PeerBanHelper 已尝试重启问题线程。请查看程序日志并将有关信息发送给开发者以协助修复此错误。"; - public static final String INTERNAL_ERROR = "出现了一个内部错误,请检查控制台日志"; - public static final String PART_TASKS_TIMED_OUT = "[警告] 等待部分任务执行时超过最大时间限制,忽略未完成的任务…… 当前执行:{}"; - public static final String TOO_WEAK_TOKEN = "Web Auth Token 未初始化或不满足最低强度要求(长度 > 8),PeerBanHelper 已重新生成了一个满足复杂度的新 Token"; - public static final String TIMING_RECOVER_PERSISTENT_BAN_LIST = "[超时] 在恢复持久化封禁列表到下载器时出现操作超时,任务已被强制终止"; - public static final String TIMING_CHECK_BANS = "[超时] 在执行 Peers 检查时出现操作超时,任务已被强制终止"; - public static final String TIMING_ADD_BANS = "[超时] 在处理新增 Peers 封禁时出现操作超时,任务已被强制终止"; - public static final String TIMING_APPLY_BAN_LIST = "[超时] 在应用封禁列表到下载器时出现操作超时,任务已被强制终止"; - public static final String TIMING_COLLECT_PEERS = "[超时] 在请求下载器 WebAPI 以获取已连接的 Peers 时操作超时,任务已被强制终止,建议检查下载器状态和网络连接。"; - public static final String TIMING_UNFINISHED_TASK = "[超时] 未完成的任务已被强制终止 -> {}"; - public static final String CONFIGURATION_INVALID = "[错误] 配置文件加载失败,可能由于人为修改错误或设备异常断电导致损坏,请删除文件 {} 来重置配置文件"; - public static final String CONFIGURATION_INVALID_TITLE = "配置文件加载失败"; - public static final String CONFIGURATION_INVALID_DESCRIPTION = "PeerBanHelper 无法正确加载必要的配置文件,这可能由于人为修改错误或设备异常断电导致损坏,请删除文件 %s 来重置配置文件。\nPeerBanHelper 即将退出……"; - public static final String TRCLIENT_API_ERROR = "[错误] TrClient 请求下载器时出现错误 {} - {}"; - public static final String IP_BAN_RULE_MATCH_ERROR = "[错误] IP黑名单订阅规则匹配异常"; - public static final String IP_BAN_RULE_MATCH_TIME = "匹配IP黑名单订阅规则花费时间:{}"; - public static final String IP_BAN_RULE_UPDATE_TYPE_AUTO = "自动更新"; - public static final String IP_BAN_RULE_UPDATE_TYPE_MANUAL = "手动更新"; - public static final String IP_BAN_RULE_UPDATE_FINISH = "IP黑名单规则订阅完毕"; - public static final String IP_BAN_RULE_NO_UPDATE = "IP黑名单订阅规则 {} 未发生更新"; - public static final String IP_BAN_RULE_UPDATE_SUCCESS = "IP黑名单订阅规则 {} 更新成功"; - public static final String IP_BAN_RULE_UPDATE_FAILED = "IP黑名单订阅规则 {} 更新失败"; - public static final String IP_BAN_RULE_LOAD_SUCCESS = "IP黑名单订阅规则 {} 加载成功"; - public static final String IP_BAN_RULE_UPDATE_LOG_ERROR = "[错误] IP黑名单订阅规则 {} 更新日志失败"; - public static final String IP_BAN_RULE_USE_CACHE = "[警告] IP黑名单订阅规则 {} 订阅失败,使用本地缓存加载成功"; - public static final String IP_BAN_RULE_LOAD_FAILED = "[错误] IP黑名单订阅规则 {} 加载失败"; - public static final String IP_BAN_RULE_LOAD_CIDR = "IP黑名单订阅规则 {} 加载CIDR : {}"; - public static final String IP_BAN_RULE_LOAD_IP = "IP黑名单订阅规则 {} 加载精确IP : {}"; - public static final String RULE_SUB_API_INTERNAL_ERROR = "[错误] 订阅规则API遇到非预期错误"; - public static final String IP_BAN_RULE_NO_ID = "[错误] IP黑名单订阅规则ID为空"; - public static final String IP_BAN_RULE_ID_CONFLICT = "[错误] IP黑名单订阅规则ID冲突: {}"; - public static final String IP_BAN_RULE_CANT_FIND = "[错误] 未找到IP黑名单订阅规则: {}"; - public static final String IP_BAN_RULE_PARAM_WRONG = "[错误] IP黑名单订阅规则参数错误"; - public static final String IP_BAN_RULE_URL_WRONG = "[错误] IP黑名单订阅规则 {} URL错误"; - public static final String IP_BAN_RULE_ENABLED = "IP黑名单订阅规则 {} 已启用"; - public static final String IP_BAN_RULE_DISABLED = "IP黑名单订阅规则 {} 已禁用"; - public static final String IP_BAN_RULE_UPDATED = "IP黑名单订阅规则 {} 已更新"; - public static final String IP_BAN_RULE_ALL_UPDATED = "IP黑名单订阅规则已全部更新"; - public static final String IP_BAN_RULE_SAVED = "IP黑名单订阅规则已保存"; - public static final String IP_BAN_RULE_DELETED = "IP黑名单订阅规则 {} 已删除"; - public static final String IP_BAN_RULE_INFO_QUERY_SUCCESS = "IP黑名单订阅规则查询成功"; - public static final String IP_BAN_RULE_LOG_QUERY_SUCCESS = "IP黑名单订阅规则更新日志查询成功"; - public static final String IP_BAN_RULE_LOG_QUERY_ERROR = "IP黑名单订阅规则更新日志查询出错"; - public static final String IP_BAN_RULE_LOG_QUERY_WRONG_PARAM = "IP黑名单订阅规则更新日志查询参数错误"; - public static final String IP_BAN_RULE_CHECK_INTERVAL_QUERY_SUCCESS = "IP黑名单订阅规则更新间隔查询成功"; - public static final String IP_BAN_RULE_CHECK_INTERVAL_WRONG_PARAM = "IP黑名单订阅规则更新间隔参数错误"; - public static final String IP_BAN_RULE_CHECK_INTERVAL_UPDATED = "IP黑名单订阅规则更新间隔设置成功"; - public static final String IP_BAN_RULE_ENABLED_WRONG_PARAM = "IP黑名单订阅规则启用禁用参数错误"; - public static final String WEBAPI_AUTH_INVALID_TOKEN = "登录失败,Token 无效"; - public static final String WEBAPI_AUTH_OK = "登录成功"; - public static final String WEBAPI_AUTH_BANNED_TOO_FREQ = "登录错误次数过多,此 IP 地址已被暂时封禁"; - public static final String WEBAPI_NOT_LOGGED = "操作失败,您还未登录"; - public static final String WEBAPI_INTERNAL_ERROR = "处理 WebAPI 请求时出现了一个内部服务器错误,请查看控制台日志"; - public static final String GITHUB_PAGE = "https://github.com/PBH-BTN/PeerBanHelper"; - public static final String GUI_COPY_TO_CLIPBOARD_TITLE = "复制到剪贴板"; - public static final String GUI_COPY_TO_CLIPBOARD_DESCRIPTION = "已成功复制到系统剪贴板: \n%S"; - public static final String GUI_TITLE_LOADING = "PeerBanHelper (%s) - 正在加载,请稍候..."; - public static final String GUI_TITLE_LOADED = "PeerBanHelper (%s) - %s (%s)"; - public static final String WEBVIEW_DISABLED_WEBKIT_NOT_INCLUDED = "未找到 JavaFx Web 模块,您正在使用精简构建,WebUI 选项卡未启用"; - public static final String WEBVIEW_ENABLED = "已找到 JavaFx Web,WebUI 选项卡已启用"; - public static final String STATUS_TEXT_OK = "当前工作正常"; - public static final String STATUS_TEXT_LOGIN_FAILED = "尝试登陆到下载器失败"; - public static final String STATUS_TEXT_EXCEPTION = "出现异常,请检查 PeerBanHelper 控制台"; - public static final String STATUS_TEXT_NEED_PRIVILEGE = "权限不足,请求权限提升(以管理员/root身份运行)"; - public static final String SUGGEST_FIREWALL_IPTABELS = "不推荐使用原生 iptables,可能引起网络性能下降。请考虑安装 ipset 代替使用 iptabels"; - public static final String SUGGEST_FIREWALL_FIREWALLD = "不推荐使用原生 firewalld,可能引起网络性能下降。请考虑安装 ipset 代替使用 firewalld"; - public static final String SUGGEST_FIREWALL_WINDOWS_FIREWALL_DISABLED = "Windows 防火墙目前处于禁用状态,请为 “公用网络” 和 “专用网络” 打开 Windows 防火墙,否则系统防火墙集成将不起作用"; - public static final String MODULE_EXPRESSION_RULE_BAD_EXPRESSION = "解析表达式时出错,请检查是否有语法错误"; - public static final String MODULE_EXPRESSION_RULE_COMPILING = "请稍等,规则引擎正在编译用户脚本以提高执行性能,这可能需要一点时间……"; - public static final String MODULE_EXPRESSION_RULE_COMPILED = "已成功编译 {} 条用户脚本,耗时 {}ms"; - public static final String MODULE_EXPRESSION_RULE_INVALID_RETURNS = """ - 用户脚本 {} 返回了无效值,返回的值必须是以下类型中的其一: - Boolean: [false=不采取任何操作, true=封禁Peer] - Integer: [0=不采取任何操作,1=封禁Peer,2=跳过其它规则] - com.ghostchu.peerbanhelper.module.PeerAction: [NO_ACTION, BAN, SKIP] - com.ghostchu.peerbanhelper.module.BanResult - """; - public static final String MODULE_EXPRESSION_RULE_TIMEOUT = "用户脚本 {} 执行超时,最大允许时间是 {}ms"; - public static final String MODULE_EXPRESSION_RULE_ERROR = "执行用户脚本 {} 时出错"; - public static final String MODULE_EXPRESSION_RULE_RELEASE_FILE_FAILED = "[错误] 在释放预设脚本文件 {} 时遇到了系统错误"; - public static final String JFX_WEBVIEW_ALERT = "来自网页的消息"; - public static final String DATABASE_OUTDATED_LOGS_CLEANED_UP = "已清理数据库中 {} 条过期封禁日志数据"; - public static final String LIBRARIES_LOADER_DETERMINE_BEST_MIRROR = "请稍等,正在初始化并测试最佳下载源(最多 15 秒)……"; - public static final String LIBRARIES_LOADER_DETERMINE_TEST_RESULT = "下载测试结果:"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_TITLE = "下载依赖文件"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_DESCRIPTION = "正在下载:%s"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_BAR_TEXT = "下载进度:%s/%s"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_TOOLTIP = "正在下载所需的运行时依赖文件,请稍等……"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER = "测试最佳镜像源"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_DESCRIPTION = "正在测试共 %s 个 Maven 镜像仓库,最多 15 秒……"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_TOOLTIP = "使用最佳的镜像源可在下载依赖文件时显著提升下载速度。"; - public static final String LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_BAR_TEXT = "很快就好!"; - public static final String WEBVIEW_RELOAD_PAGE = "刷新网页"; - public static final String WEBVIEW_RESET_PAGE = "回到初始页"; - public static final String WEBVIEW_BACK = "后退"; - public static final String WEBVIEW_FORWARD = "前进"; - public static final String DOWNLOADER_API_ADD_FAILURE = "下载器创建失败,是否传递的是一个不受支持的下载器类型?"; - public static final String DOWNLOADER_API_CREATED = "下载器创建成功"; - public static final String DOWNLOADER_API_UPDATED = "下载器更新成功"; - public static final String DOWNLOADER_API_CREATION_FAILED_ALREADY_EXISTS = "下载器创建失败,已有相同的下载器配置存在"; - public static final String DOWNLOADER_API_CREATION_FAILED_IO_EXCEPTION = "下载器创建失败,出现 I/O 错误,请检查控制台日志"; - public static final String DOWNLOADER_API_UPDATE_FAILURE = "下载器更新失败,是否传递的是一个不受支持的下载器类型?"; - public static final String DOWNLOADER_API_UPDATE_FAILURE_ALREADY_EXISTS = "下载器更新失败,已有一个相同的下载器配置存在,且移除失败"; - public static final String DOWNLOADER_API_TEST_NAME_EXISTS = "下载器所使用的名称与另一个已存在的下载器冲突"; - public static final String DOWNLOADER_API_TEST_OK = "验证成功,配置有效"; - public static final String DOWNLOADER_API_REMOVE_NOT_EXISTS = "无法移除指定的下载器,指定的下载器并没有在 PeerBanHelper 中注册"; - public static final String DOWNLOADER_API_REMOVE_SAVED = "移除成功,配置已保存"; - public static final String DOWNLOADER_API_DOWNLOADER_NOT_EXISTS = "请求的下载器未在 PeerBanHelper 中注册"; - public static final String DOWNLOADER_BIGLYBT_INCORRECT_RESPONSE = "请求 Torrent 列表失败:%s - %s"; - public static final String DOWNLOADER_BIGLYBT_FAILED_REQUEST_PEERS_LIST_IN_TORRENT = "请求 Torrent 的 Peers 列表失败 - %d - %s"; - public static final String DOWNLOADER_BIGLYBT_INCREAMENT_BAN_FAILED = "[错误] 向下载器请求增量封禁对等体时出现错误,请尝试在配置文件中关闭增量封禁(increment-ban)配置项"; - public static final String DOWNLOADER_BIGLYBT_FAILED_SAVE_BANLIST = "无法保存 {} ({}) 的 Banlist!{} - {}\n{}"; - public static final String ALERT_INCORRECT_PROXY_SETTING = "警告!通过 HTTP_PROXY 环境变量无法为 Java 应用程序设置代理服务器!您的代理设置可能并不会生效。"; - public static final String COMMAND_EXECUTOR = "[CommandExecutor] 命令执行器正在执行系统终端命令:{}"; - public static final String COMMAND_EXECUTOR_FAILED = "[CommandExecutor] 系统终端命令执行失败:{}"; - public static final String COMMAND_EXECUTOR_FAILED_TIMEOUT = "[CommandExecutor] 系统终端命令执行超时:{}"; - public static final String DOWNLOADER_DELUGE_PLUGIN_NOT_INSTALLED = "无法登录到下载器 {},此 Deluge 下载器必须正确加载 PeerBanHelper Deluge Adapter 扩展插件:https://github.com/PBH-BTN/PBH-Adapter-Deluge"; - public static final String DOWNLOADER_DELUGE_API_ERROR = "执行 Deluge RPC 调用失败,操作被忽略"; - public static final String DOWNLOADER_UNHANDLED_EXCEPTION = "发生了一个未处理的异常,请反馈给 PeerBanHelper 开发者,此错误已被跳过……"; -} + public String getKey() { + return name(); + } +} \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/LanguageFilesManager.java b/src/main/java/com/ghostchu/peerbanhelper/text/LanguageFilesManager.java new file mode 100644 index 0000000000..aa9e3eeb5d --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/text/LanguageFilesManager.java @@ -0,0 +1,109 @@ +package com.ghostchu.peerbanhelper.text; + +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +// No to-do anymore! This used for not only messages_fallback.yml! Keep the extent ability! +public class LanguageFilesManager { + //distributionPath->[localeCode->OTA files] + private final Map locale2ContentMapping = new ConcurrentHashMap<>(); + + /** + * Deploy new locale to TextMapper with cloud values and bundle values + * + * @param locale The locale code + * @param newDistribution The values from Distribution platform + */ + public void deploy(@NotNull String locale, @NotNull YamlConfiguration newDistribution) { + if (!this.locale2ContentMapping.containsKey(locale)) { + this.locale2ContentMapping.put(locale, newDistribution); + } else { + YamlConfiguration exists = this.locale2ContentMapping.get(locale); + merge(exists, newDistribution); + } + } + + private void merge(@NotNull YamlConfiguration alreadyRegistered, @NotNull YamlConfiguration newConfiguration) { + for (String key : newConfiguration.getKeys(true)) { + if (newConfiguration.isConfigurationSection(key)) { + continue; + } + alreadyRegistered.set(key, newConfiguration.get(key)); + } + } + + public void fillMissing(@NotNull YamlConfiguration fallback) { + for (YamlConfiguration value : this.locale2ContentMapping.values()) { + mergeMissing(value, fallback); + } + } + + private void mergeMissing(@NotNull YamlConfiguration alreadyRegistered, @NotNull YamlConfiguration newConfiguration) { + for (String key : newConfiguration.getKeys(true)) { + if (newConfiguration.isConfigurationSection(key)) { + continue; + } + if (alreadyRegistered.isSet(key)) { + continue; + } + alreadyRegistered.set(key, newConfiguration.get(key)); + } + } + + public void destroy(@NotNull String locale) { + this.locale2ContentMapping.remove(locale); + } + + /** + * Getting specific locale data under specific distribution data + * + * @param locale The specific locale + * @return The locale data, null if never deployed + */ + public @Nullable YamlConfiguration getDistribution(@NotNull String locale) { + return this.locale2ContentMapping.get(locale); + } + + /** + * Getting specific locale data under specific distribution data + * + * @return The locale data, null if never deployed + */ + public @NotNull Map getDistributions() { + return locale2ContentMapping; + } + + /** + * Remove all locales data under specific distribution path + * + * @param distributionPath The distribution path + */ + public void remove(@NotNull String distributionPath) { + this.locale2ContentMapping.remove(distributionPath); + } + + /** + * Remove specific locales data under specific distribution path + * + * @param distributionPath The distribution path + * @param locale The locale + */ + public void remove(@NotNull String distributionPath, @NotNull String locale) { + if (this.locale2ContentMapping.containsKey(distributionPath)) { + this.locale2ContentMapping.remove(locale); + } + } + + /** + * Reset TextMapper + */ + public void reset() { + this.locale2ContentMapping.clear(); + } + + +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/ProxiedLocale.java b/src/main/java/com/ghostchu/peerbanhelper/text/ProxiedLocale.java new file mode 100644 index 0000000000..a59f5c6d55 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/text/ProxiedLocale.java @@ -0,0 +1,38 @@ +package com.ghostchu.peerbanhelper.text; + +import lombok.Data; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.NumberFormat; +import java.util.Locale; + +@Data +public class ProxiedLocale { + private Locale locale; + @Nullable + private String origin; + private String relative; + private NumberFormat nf; + + public ProxiedLocale(@Nullable String origin, String relative, @NotNull NumberFormat nf, @NotNull Locale locale) { + this.origin = origin; + this.relative = relative; + this.nf = nf; + this.locale = locale; + } + + public String getLocale() { + return relative; + } + + @NotNull + public NumberFormat getNumberFormat() { + return nf; + } + + @NotNull + public Locale getJavaLocale() { + return locale; + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/TextManager.java b/src/main/java/com/ghostchu/peerbanhelper/text/TextManager.java new file mode 100644 index 0000000000..ffdb2e451a --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/text/TextManager.java @@ -0,0 +1,333 @@ +package com.ghostchu.peerbanhelper.text; + +import com.ghostchu.peerbanhelper.Main; +import com.ghostchu.peerbanhelper.text.postprocessor.PostProcessor; +import com.ghostchu.peerbanhelper.text.postprocessor.impl.FillerProcessor; +import com.ghostchu.peerbanhelper.util.URLUtil; +import com.ghostchu.simplereloadlib.ReloadResult; +import com.ghostchu.simplereloadlib.ReloadStatus; +import com.ghostchu.simplereloadlib.Reloadable; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.bspfsystems.yamlconfiguration.configuration.InvalidConfigurationException; +import org.bspfsystems.yamlconfiguration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; + +import static com.ghostchu.peerbanhelper.Main.DEF_LOCALE; + +@Slf4j +public class TextManager implements Reloadable { + public static TextManager INSTANCE_HOLDER = new TextManager(); + public final Set postProcessors = new LinkedHashSet<>(); + // > + private final LanguageFilesManager languageFilesManager = new LanguageFilesManager(); + private final Set availableLanguages = new LinkedHashSet<>(); + private final File langDirectory; + private final File overrideDirectory; + + public TextManager() { + this.langDirectory = new File(Main.getDataDirectory(), "lang"); + this.overrideDirectory = new File(langDirectory, "overrides"); + if (!this.langDirectory.exists()) { + this.langDirectory.mkdirs(); + } + if (!this.overrideDirectory.exists()) { + this.overrideDirectory.mkdirs(); + } + load(); + } + + public static String tlUI(Lang key, Object... params) { + return tl(DEF_LOCALE, new TranslationComponent(key.getKey(), INSTANCE_HOLDER.convert(params))); + } + + public static String tlUI(TranslationComponent translationComponent) { + return tl(DEF_LOCALE, translationComponent); + } +// public static String tlUI(String key, Object... params) { +// return tl(DEF_LOCALE, new TranslationComponent(key, INSTANCE_HOLDER.convert(params))); +// } + + public static String tl(String locale, Lang key, Object... params) { + return tl(locale, new TranslationComponent(key.getKey(), INSTANCE_HOLDER.convert(params))); + } + + public static String tl(String locale, TranslationComponent translationComponent) { + locale = locale.toLowerCase(Locale.ROOT).replace("-", "_"); + YamlConfiguration yamlConfiguration = INSTANCE_HOLDER.languageFilesManager.getDistribution(locale); + if (yamlConfiguration == null) { + new Exception("Unsupported locale").printStackTrace(); + return "Unsupported locale: " + locale; + } + String str = yamlConfiguration.getString(translationComponent.getKey()); + if (str == null) { + return translationComponent.getKey(); + } + for (PostProcessor postProcessor : INSTANCE_HOLDER.postProcessors) { + try { + str = postProcessor.process(str, locale, translationComponent.getParams()); + } catch (Exception e) { + log.warn("Unable to process post processor: key={}, locale={}, params={}", translationComponent.getKey(), locale, translationComponent.getParams()); + } + } + return str; + } + + /** + * Loading Crowdin OTA module and i18n system + */ + public void load() { + log.info("Loading up translations, this may need a while..."); + //TODO: This will break the message processing system in-game until loading finished, need to fix it. + this.reset(); + // first, we need load built-in fallback translation. + languageFilesManager.deploy("en_us", loadBuiltInFallback()); + // second, load the bundled language files + loadBundled().forEach(languageFilesManager::deploy); + // then, load the translations from Crowdin + // and don't forget fix missing + languageFilesManager.fillMissing(loadBuiltInFallback()); + // finally, load override translations + Collection pending = getOverrideLocales(languageFilesManager.getDistributions().keySet()); + log.debug("Pending: {}", Arrays.toString(pending.toArray())); + pending.forEach(locale -> { + File file = getOverrideLocaleFile(locale); + if (file.exists()) { + YamlConfiguration configuration = new YamlConfiguration(); + try { + configuration.loadFromString(Files.readString(file.toPath(), StandardCharsets.UTF_8)); + languageFilesManager.deploy(locale, configuration); + } catch (InvalidConfigurationException | IOException e) { + log.warn("Failed to override translation for {}.", locale, e); + } + + } else { + log.debug("Override not applied: File {} not exists.", file.getAbsolutePath()); + } + }); + + // Remove disabled locales + //List enabledLanguagesRegex = .getStringList("enabled-languages"); + //enabledLanguagesRegex.replaceAll(s -> s.toLowerCase(Locale.ROOT).replace("-", "_")); +// Iterator it = pending.iterator(); +// while (it.hasNext()) { +// String locale = it.next(); +// if (!localeEnabled(locale, enabledLanguagesRegex)) { +// this.languageFilesManager.destroy(locale); +// it.remove(); +// } +// } + if (pending.isEmpty()) { + log.warn("Warning! You must enable at least one language! Forcing enable build-in en_us..."); + pending.add("en_us"); + this.languageFilesManager.deploy("en_us", loadBuiltInFallback()); + } + // Remember all available languages + availableLanguages.addAll(pending); + + // Register post processor + postProcessors.add(new FillerProcessor()); + } + + /** + * Reset everything + */ + private void reset() { + languageFilesManager.reset(); + postProcessors.clear(); + availableLanguages.clear(); + } + + @NotNull + private YamlConfiguration loadBuiltInFallback() { + YamlConfiguration configuration = new YamlConfiguration(); + try (InputStream inputStream = Main.class.getResourceAsStream("/lang/messages_fallback.yml")) { + if (inputStream == null) { + log.warn("Failed to load built-in fallback translation, fallback file not exists in jar."); + return configuration; + } + byte[] bytes = inputStream.readAllBytes(); + String content = new String(bytes, StandardCharsets.UTF_8); + configuration.loadFromString(content); + return configuration; + } catch (IOException | InvalidConfigurationException e) { + log.warn("Failed to load built-in fallback translation.", e); + return configuration; + } + } + + /** + * Loading translations from bundled resources + * + * @return The bundled translations, empty hash map if nothing can be load. + */ + @NotNull + @SneakyThrows + private Map loadBundled() { + Map availableLang = new HashMap<>(); + URL url = Main.class.getClassLoader().getResource(""); + if (url == null) { + return availableLang; + } + PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(Main.class.getClassLoader()); + var res = resourcePatternResolver.getResources("classpath:lang/**/*.yml"); + for (Resource re : res) { + String langName = URLUtil.getParentName(re.getURI()); + try { + YamlConfiguration configuration = new YamlConfiguration(); + configuration.loadFromString(new String(re.getContentAsByteArray(), StandardCharsets.UTF_8)); + availableLang.put(langName.toLowerCase(Locale.ROOT).replace("-", "_"), configuration); + } catch (IOException | InvalidConfigurationException e) { + log.warn("Failed to load bundled translation.", e); + } + } + return availableLang; + } + + /** + * Generate the override files storage path + * + * @param pool The language codes you already own. + * @return The pool copy with new added language codes. + */ + @SneakyThrows(IOException.class) + @NotNull + protected Collection getOverrideLocales(@NotNull Collection pool) { + // create the pool overrides placeholder directories + pool.forEach(single -> { + File f = new File(overrideDirectory, single); + if (!f.exists()) { + f.mkdirs(); + } + }); + // + File[] files = overrideDirectory.listFiles(); + if (files == null) { + return pool; + } + List newPool = new ArrayList<>(pool); + for (File file : files) { + if (file.isDirectory()) { + // custom language + newPool.add(file.getName()); + // create the paired file + File localeFile = new File(file, "messages_fallback.yml"); + if (!localeFile.exists()) { + localeFile.getParentFile().mkdirs(); + localeFile.createNewFile(); + } else { + if (localeFile.isDirectory()) { + localeFile.delete(); + } + } + } + } + return newPool; + } + + @NotNull + private File getOverrideLocaleFile(@NotNull String locale) { + File file; + // bug fixes workaround + file = new File(overrideDirectory, locale + ".yml"); + if (file.isDirectory()) { // Fix bad directory name. + file.delete(); + } + file = new File(overrideDirectory, locale); + file = new File(file, "messages_fallback.yml"); + return file; + } + + @Override + public ReloadResult reloadModule() { + load(); + return ReloadResult.builder().status(ReloadStatus.SUCCESS).build(); + } + + /** + * Gets specific locale status + * + * @param locale The locale + * @param regex The regexes + * @return The locale enabled status + */ + public boolean localeEnabled(@NotNull String locale, @NotNull List regex) { + return true; +// for (String languagesRegex : regex) { +// try { +// if (Pattern.matches(CommonUtil.createRegexFromGlob(languagesRegex), locale)) { +// return true; +// } +// } catch (PatternSyntaxException exception) { +// Log.debug("Pattern " + languagesRegex + " invalid, skipping..."); +// } +// } +// return false; + } + + /** + * Register the language phrase to QuickShop text manager in runtime. + * + * @param locale Target locale + * @param path The language key path + * @param text The language text + */ + @SneakyThrows(InvalidConfigurationException.class) + public void register(@NotNull String locale, @NotNull String path, @NotNull String text) { + YamlConfiguration configuration = languageFilesManager.getDistribution(locale); + if (configuration == null) { + configuration = new YamlConfiguration(); + configuration.loadFromString(languageFilesManager.getDistribution("en_us").saveToString()); + } + configuration.set(path, text); + languageFilesManager.deploy(locale, configuration); + } + + @NotNull + public String[] convert(@Nullable Object... args) { + if (args == null || args.length == 0) { + return new String[0]; + } + String[] components = new String[args.length]; + for (int i = 0; i < args.length; i++) { + Object obj = args[i]; + if (obj == null) { + components[i] = "null"; + continue; + } + // Class clazz = obj.getClass(); + // Check + try { + components[i] = obj.toString(); + } catch (Exception exception) { + log.debug("Failed to process the object: {}", obj); + components[i] = String.valueOf(obj); // null safe + } + // undefined + + } + return components; + } + + /** + * Return the set of available Languages + * + * @return the set of available Languages + */ + public List getAvailableLanguages() { + return new ArrayList<>(availableLanguages); + } + + +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/TranslationComponent.java b/src/main/java/com/ghostchu/peerbanhelper/text/TranslationComponent.java new file mode 100644 index 0000000000..3895e342d3 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/text/TranslationComponent.java @@ -0,0 +1,37 @@ +package com.ghostchu.peerbanhelper.text; + +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public class TranslationComponent { + private final String key; + private final String[] params; + + public TranslationComponent(String key) { + this.key = key; + this.params = new String[0]; + } + + public TranslationComponent(String key, String... params) { + this.key = key; + this.params = params; + } + + public TranslationComponent(Lang key) { + this(key.getKey()); + } + + public TranslationComponent(Lang key, String... params) { + this(key.getKey(), params); + } + + @Override + public String toString() { + return "TranslationComponent{" + + "key='" + key + '\'' + + ", params=" + Arrays.toString(params) + + '}'; + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/postprocessor/PostProcessor.java b/src/main/java/com/ghostchu/peerbanhelper/text/postprocessor/PostProcessor.java new file mode 100644 index 0000000000..5734821533 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/text/postprocessor/PostProcessor.java @@ -0,0 +1,12 @@ +package com.ghostchu.peerbanhelper.text.postprocessor; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Post-processing + */ +public interface PostProcessor { + @NotNull + String process(@NotNull String text, @Nullable String locale, @Nullable String... args); +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/text/postprocessor/impl/FillerProcessor.java b/src/main/java/com/ghostchu/peerbanhelper/text/postprocessor/impl/FillerProcessor.java new file mode 100644 index 0000000000..8fc8cba7ef --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/text/postprocessor/impl/FillerProcessor.java @@ -0,0 +1,15 @@ +package com.ghostchu.peerbanhelper.text.postprocessor.impl; + +import com.ghostchu.peerbanhelper.text.postprocessor.PostProcessor; +import com.ghostchu.peerbanhelper.util.MsgUtil; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@EqualsAndHashCode +public class FillerProcessor implements PostProcessor { + @Override + public @NotNull String process(@NotNull String text, @Nullable String locale, @Nullable String... args) { + return MsgUtil.fillArgs(text, args); + } +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/CommonUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/CommonUtil.java new file mode 100644 index 0000000000..296a4b0039 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/util/CommonUtil.java @@ -0,0 +1,38 @@ +package com.ghostchu.peerbanhelper.util; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; + +/** + * 特殊工具类,不得依赖任何外部类 + */ +public class CommonUtil { + + @NotNull + public static String getClassPath(@NotNull Class clazz) { + String jarPath = clazz.getProtectionDomain().getCodeSource().getLocation().getFile(); + jarPath = URLDecoder.decode(jarPath, StandardCharsets.UTF_8); + return jarPath; + } + + @NotNull + public static File getAppJarFile(@NotNull Class clazz) throws FileNotFoundException { + String path = getClassJarPath(clazz); + File file = new File(path); + if (!file.exists()) { + throw new FileNotFoundException("File not found: " + path); + } + return file; + } + + + @NotNull + public static String getClassJarPath(@NotNull Class clazz) { + return CommonUtil.getClassPath(clazz); + } + +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/HTTPUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/HTTPUtil.java index dc4ca518bf..008abad691 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/HTTPUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/HTTPUtil.java @@ -31,6 +31,8 @@ import java.util.function.Function; import java.util.zip.GZIPOutputStream; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class HTTPUtil { private static final int MAX_RESEND = 5; @@ -79,7 +81,7 @@ public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngi sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom()); ignoreSslContext = sslContext; } catch (Exception e) { - log.warn(Lang.MODULE_AP_SSL_CONTEXT_FAILURE, e); + log.error(tlUI(Lang.MODULE_AP_SSL_CONTEXT_FAILURE), e); } } @@ -87,6 +89,7 @@ public static HttpClient getHttpClient(boolean ignoreSSL, ProxySelector proxySel Methanol.Builder builder = Methanol .newBuilder() .followRedirects(HttpClient.Redirect.ALWAYS) + .defaultHeader("Accept-Encoding", "gzip,deflate") .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) .headersTimeout(Duration.of(15, ChronoUnit.SECONDS)) .readTimeout(Duration.of(15, ChronoUnit.SECONDS)) @@ -126,12 +129,12 @@ public static WritableBodyPublisher gzipBody(InputStream is) { public static void onProgress(ProgressTracker.Progress progress) { if (progress.determinate()) { // Overall progress can be measured var percent = 100 * progress.value(); - log.info(Lang.DOWNLOAD_PROGRESS_DETERMINED, progress.totalBytesTransferred(), progress.contentLength(), String.format("%.2f", percent)); + log.info(tlUI(Lang.DOWNLOAD_PROGRESS_DETERMINED, progress.totalBytesTransferred(), progress.contentLength(), String.format("%.2f", percent))); } else { - log.info(Lang.DOWNLOAD_PROGRESS, progress.totalBytesTransferred()); + log.info(tlUI(Lang.DOWNLOAD_PROGRESS, progress.totalBytesTransferred())); } if (progress.done()) { - log.info(Lang.DOWNLOAD_COMPLETED, progress.totalBytesTransferred()); + log.info(tlUI(Lang.DOWNLOAD_COMPLETED, progress.totalBytesTransferred())); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/IPAddressUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/IPAddressUtil.java index f3a9224df5..fd4f63d94b 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/IPAddressUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/IPAddressUtil.java @@ -5,7 +5,6 @@ import inet.ipaddr.IPAddress; import inet.ipaddr.IPAddressString; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import java.util.concurrent.ExecutionException; @@ -33,7 +32,6 @@ public class IPAddressUtil { * @param ip * @return */ - @Contract("_ -> !null") public static IPAddress getIPAddress(String ip) { try { return IP_ADDRESS_CACHE.get(ip, () -> { diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/LazyLoad.java b/src/main/java/com/ghostchu/peerbanhelper/util/LazyLoad.java index bd66818628..a38e2a357f 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/LazyLoad.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/LazyLoad.java @@ -1,7 +1,8 @@ package com.ghostchu.peerbanhelper.util; import java.util.function.Supplier; -public class LazyLoad { + +public final class LazyLoad { private Supplier loader; private T content; diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/MsgUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/MsgUtil.java index f1365b91b3..d7b2dc0b97 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/MsgUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/MsgUtil.java @@ -1,12 +1,17 @@ package com.ghostchu.peerbanhelper.util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.lang.management.LockInfo; import java.lang.management.MonitorInfo; import java.lang.management.ThreadInfo; import java.text.CharacterIterator; +import java.text.DecimalFormat; import java.text.StringCharacterIterator; public class MsgUtil { + private static final DecimalFormat df = new DecimalFormat("0.00%"); public static String humanReadableByteCountBin(long bytes) { long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes); if (absB < 1024) { @@ -97,4 +102,43 @@ public static String threadInfoToString(ThreadInfo info) { sb.append('\n'); return sb.toString(); } + + public static DecimalFormat getPercentageFormatter() { + return df; + } + + + /** + * Replace args in raw to args + * + * @param raw text + * @param args args + * @return filled text + */ + @NotNull + public static String fillArgs(@Nullable String raw, @Nullable String... args) { + if (raw == null || raw.isEmpty()) { + return ""; + } + StringBuilder result = new StringBuilder(); + int start = 0; + int argIndex = 0; + + while (start < raw.length()) { + int placeholderIndex = raw.indexOf("{}", start); + if (placeholderIndex == -1) { + result.append(raw.substring(start)); + break; + } + result.append(raw, start, placeholderIndex); + if (args != null && argIndex < args.length) { + result.append(args[argIndex] != null ? args[argIndex] : ""); + argIndex++; + } else { + result.append("{}"); + } + start = placeholderIndex + 2; + } + return result.toString(); + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/PBHLibrariesLoader.java b/src/main/java/com/ghostchu/peerbanhelper/util/PBHLibrariesLoader.java index 37eb1d65c4..eed48eefc7 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/PBHLibrariesLoader.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/PBHLibrariesLoader.java @@ -4,7 +4,6 @@ import com.alessiodp.libby.LibraryManager; import com.formdev.flatlaf.FlatIntelliJLaf; import com.ghostchu.peerbanhelper.gui.crossimpl.CrossDownloaderDialog; -import com.ghostchu.peerbanhelper.text.Lang; import com.ghostchu.peerbanhelper.util.maven.GeoUtil; import com.ghostchu.peerbanhelper.util.maven.MavenCentralMirror; import lombok.extern.slf4j.Slf4j; @@ -32,12 +31,12 @@ private void addRepositories() { if (repoAdded) return; FlatIntelliJLaf.setup(); CrossDownloaderDialog downloaderDialog = new CrossDownloaderDialog(); - downloaderDialog.setTitle(Lang.LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER); - downloaderDialog.getTaskTitle().setText(Lang.LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER); - downloaderDialog.getTooltip().setText(Lang.LIBRARIES_DOWNLOAD_DIALOG_TOOLTIP); - downloaderDialog.getDescription().setText(String.format(Lang.LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_DESCRIPTION, MavenCentralMirror.values().length)); + downloaderDialog.setTitle("Testing the best mirror server..."); + downloaderDialog.getTaskTitle().setText("Testing the best mirror server..."); + downloaderDialog.getTooltip().setText("We're testing fastest mirror for downloading libraries...."); + downloaderDialog.getDescription().setText(String.format("PBH libraries testing %s mirrors, soon™!", MavenCentralMirror.values().length)); downloaderDialog.getProgressBar().setIndeterminate(true); - downloaderDialog.getProgressBar().setString(Lang.LIBRARIES_DOWNLOAD_DIALOG_TEST_SERVER_BAR_TEXT); + downloaderDialog.getProgressBar().setString("Please wait up to 15 seconds..."); downloaderDialog.getProgressBar().setStringPainted(true); downloaderDialog.setVisible(true); try { @@ -59,22 +58,20 @@ private void addRepositories() { public void loadLibraries(List libraries, Map env) throws RuntimeException { FlatIntelliJLaf.setup(); CrossDownloaderDialog downloaderDialog = new CrossDownloaderDialog(); - downloaderDialog.setTitle(Lang.LIBRARIES_DOWNLOAD_DIALOG_TITLE); - downloaderDialog.getTaskTitle().setText(Lang.LIBRARIES_DOWNLOAD_DIALOG_TITLE); - downloaderDialog.getTooltip().setText(Lang.LIBRARIES_DOWNLOAD_DIALOG_TOOLTIP); + downloaderDialog.setTitle("Downloading libraries..."); + downloaderDialog.getTaskTitle().setText("Downloading libraries..."); + downloaderDialog.getTooltip().setText("PeerBanHelper download necessary libraries..."); downloaderDialog.getProgressBar().setValue(0); try { - loadLibraries0(libraries, env, (dependency, pos, total) -> { - SwingUtilities.invokeLater(() -> { - if (!downloaderDialog.isVisible()) { - downloaderDialog.setVisible(true); - } - downloaderDialog.getDescription().setText(String.format(Lang.LIBRARIES_DOWNLOAD_DIALOG_DESCRIPTION, dependency)); - downloaderDialog.getProgressBar().setString(String.format(Lang.LIBRARIES_DOWNLOAD_DIALOG_BAR_TEXT, pos, total)); - downloaderDialog.getProgressBar().setMaximum(total); - downloaderDialog.getProgressBar().setValue(pos); - }); - }); + loadLibraries0(libraries, env, (dependency, pos, total) -> SwingUtilities.invokeLater(() -> { + if (!downloaderDialog.isVisible()) { + downloaderDialog.setVisible(true); + } + downloaderDialog.getDescription().setText(String.format("Downloading: %s", dependency)); + downloaderDialog.getProgressBar().setString(String.format("Progress: %s/%s", pos, total)); + downloaderDialog.getProgressBar().setMaximum(total); + downloaderDialog.getProgressBar().setValue(pos); + })); } finally { downloaderDialog.dispose(); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/StrUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/StrUtil.java index 9eb07b6921..a6f7c946e7 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/StrUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/StrUtil.java @@ -1,5 +1,6 @@ package com.ghostchu.peerbanhelper.util; +import java.nio.charset.StandardCharsets; import java.util.Locale; public class StrUtil { @@ -18,4 +19,12 @@ public static boolean isEmpty(String str) { public static boolean isBlank(String str) { return str == null || str.trim().isEmpty(); } + + public static String toStringHex(String s) { + byte[] baKeyword = new byte[s.length() / 2]; + for (int i = 0; i < baKeyword.length; i++) { + baKeyword[i] = (byte) (0xff & Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16)); + } + return new String(baKeyword, StandardCharsets.ISO_8859_1); + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/URLUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/URLUtil.java index 648ac79fb5..37e25c86d4 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/URLUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/URLUtil.java @@ -1,5 +1,6 @@ package com.ghostchu.peerbanhelper.util; +import java.net.URI; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -46,4 +47,18 @@ public static String getParamByUrl(String url, String name) { return null; } } + + public static String getParentName(URI uri) { + try { + // 查找文件路径中的上一级目录 + String[] pathSegments = uri.toString().split("/"); + if (pathSegments.length > 1) { + return pathSegments[pathSegments.length - 2]; + } else { + return ""; + } + } catch (Exception e) { + return ""; + } + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/WatchDog.java b/src/main/java/com/ghostchu/peerbanhelper/util/WatchDog.java index 5e1a35fe35..fa492d7663 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/WatchDog.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/WatchDog.java @@ -10,6 +10,8 @@ import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class WatchDog { private final String name; @@ -54,7 +56,7 @@ private void watchDogCheck() { } }, executor).get(3, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { - log.error(Lang.WATCH_DOG_CALLBACK_BLOCKED, e); + log.error(tlUI(Lang.WATCH_DOG_CALLBACK_BLOCKED), e); } } @@ -66,7 +68,7 @@ private void good() { } private void hungry() { - log.info(Lang.WATCH_DOG_HUNGRY, name, timeout + "ms", lastOperation); + log.info(tlUI(Lang.WATCH_DOG_HUNGRY, name, timeout + "ms", lastOperation)); if (hungry != null) { hungry.run(); } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/collection/CircularArrayList.java b/src/main/java/com/ghostchu/peerbanhelper/util/collection/CircularArrayList.java new file mode 100644 index 0000000000..866face139 --- /dev/null +++ b/src/main/java/com/ghostchu/peerbanhelper/util/collection/CircularArrayList.java @@ -0,0 +1,351 @@ +package com.ghostchu.peerbanhelper.util.collection; + +import org.jetbrains.annotations.Contract; + +import java.util.AbstractList; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.RandomAccess; + +/** + * @author Glavo + */ +@SuppressWarnings("unchecked") +public final class CircularArrayList extends AbstractList implements RandomAccess { + + private static final int DEFAULT_CAPACITY = 10; + private static final Object[] EMPTY_ARRAY = new Object[0]; + + private Object[] elements; + private int begin = -1; + private int end = 0; + + public CircularArrayList() { + this.elements = EMPTY_ARRAY; + } + + public CircularArrayList(int initialCapacity) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("illegal initialCapacity: " + initialCapacity); + } + + this.elements = initialCapacity == 0 ? EMPTY_ARRAY : new Object[initialCapacity]; + } + + private static int inc(int i, int capacity) { + return i + 1 >= capacity ? 0 : i + 1; + } + + private static int inc(int i, int distance, int capacity) { + if ((i += distance) - capacity >= 0) { + i -= capacity; + } + + return i; + } + + private static int dec(int i, int capacity) { + return i - 1 < 0 ? capacity - 1 : i - 1; + } + + private static int sub(int i, int distance, int capacity) { + if ((i -= distance) < 0) { + i += capacity; + } + return i; + } + + private static int newCapacity(int oldCapacity, int minCapacity) { + return oldCapacity == 0 + ? Math.max(DEFAULT_CAPACITY, minCapacity) + : Math.max(Math.max(oldCapacity, minCapacity), oldCapacity + (oldCapacity >> 1)); + } + + private static void checkElementIndex(int index, int size) throws IndexOutOfBoundsException { + if (index < 0 || index >= size) { + // Optimized for execution by hotspot + checkElementIndexFailed(index, size); + } + } + + @Contract("_, _ -> fail") + private static void checkElementIndexFailed(int index, int size) { + if (size < 0) { + throw new IllegalArgumentException("size(" + size + ") < 0"); + } + if (index < 0) { + throw new IndexOutOfBoundsException("index(" + index + ") < 0"); + } + if (index >= size) { + throw new IndexOutOfBoundsException("index(" + index + ") >= size(" + size + ")"); + } + throw new AssertionError(); + } + + private static void checkPositionIndex(int index, int size) throws IndexOutOfBoundsException { + if (index < 0 || index > size) { + // Optimized for execution by hotspot + checkPositionIndexFailed(index, size); + } + } + + @Contract("_, _ -> fail") + private static void checkPositionIndexFailed(int index, int size) { + if (size < 0) { + throw new IllegalArgumentException("size(" + size + ") < 0"); + } + if (index < 0) { + throw new IndexOutOfBoundsException("index(" + index + ") < 0"); + } + if (index > size) { + throw new IndexOutOfBoundsException("index(" + index + ") > size(" + size + ")"); + } + throw new AssertionError(); + } + + private void grow() { + grow(elements.length + 1); + } + + private void grow(int minCapacity) { + final int oldCapacity = elements.length; + final int size = size(); + final int newCapacity = newCapacity(oldCapacity, minCapacity); + + final Object[] newElements; + if (size == 0) { + newElements = new Object[newCapacity]; + } else if (begin < end) { + newElements = Arrays.copyOf(elements, newCapacity, Object[].class); + } else { + newElements = new Object[newCapacity]; + System.arraycopy(elements, begin, newElements, 0, elements.length - begin); + System.arraycopy(elements, 0, newElements, elements.length - begin, end); + begin = 0; + end = size; + } + this.elements = newElements; + } + + @Override + public boolean isEmpty() { + return begin == -1; + } + + @Override + public int size() { + if (isEmpty()) { + return 0; + } else if (begin < end) { + return end - begin; + } else { + return elements.length - begin + end; + } + } + + @Override + public E get(int index) { + if (isEmpty()) { + throw new IndexOutOfBoundsException("Index out of range: " + index); + } else if (begin < end) { + checkElementIndex(index, end - begin); + return (E) elements[begin + index]; + } else { + checkElementIndex(index, elements.length - begin + end); + return (E) elements[inc(begin, index, elements.length)]; + } + } + + @Override + public E set(int index, E element) { + int arrayIndex; + if (isEmpty()) { + throw new IndexOutOfBoundsException(); + } else if (begin < end) { + checkElementIndex(index, end - begin); + arrayIndex = begin + index; + } else { + final int size = elements.length - begin + end; + checkElementIndex(index, size); + arrayIndex = inc(begin, index, elements.length); + } + + E oldValue = (E) elements[arrayIndex]; + elements[arrayIndex] = element; + return oldValue; + } + + @Override + public void add(int index, E element) { + if (index == 0) { + addFirst(element); + return; + } + + final int oldSize = size(); + if (index == oldSize) { + addLast(element); + return; + } + + checkPositionIndex(index, oldSize); + + if (oldSize == elements.length) { + grow(); + } + + if (begin < end) { + final int targetIndex = begin + index; + if (end < elements.length) { + System.arraycopy(elements, targetIndex, elements, targetIndex + 1, end - targetIndex); + end++; + } else { + System.arraycopy(elements, begin, elements, begin - 1, targetIndex - begin + 1); + begin--; + } + elements[targetIndex] = element; + } else { + int targetIndex = inc(begin, index, elements.length); + if (targetIndex <= end) { + System.arraycopy(elements, targetIndex, elements, targetIndex + 1, end - targetIndex); + elements[targetIndex] = element; + end++; + } else { + System.arraycopy(elements, begin, elements, begin - 1, targetIndex - begin); + elements[targetIndex - 1] = element; + begin--; + } + } + } + + @Override + public E remove(int index) { + final int oldSize = size(); + checkElementIndex(index, oldSize); + + if (index == 0) { + return removeFirst(); + } + + if (index == oldSize - 1) { + return removeLast(); + } + + final Object res; + + if (begin < end) { + final int targetIndex = begin + index; + res = elements[targetIndex]; + System.arraycopy(elements, targetIndex + 1, elements, targetIndex, end - targetIndex - 1); + end--; + } else { + final int targetIndex = inc(begin, index, elements.length); + res = elements[targetIndex]; + if (targetIndex < end) { + System.arraycopy(elements, targetIndex + 1, elements, targetIndex, end - targetIndex - 1); + end--; + } else { + System.arraycopy(elements, begin, elements, begin + 1, targetIndex - begin); + begin = inc(begin, elements.length); + } + } + + return (E) res; + } + + @Override + public void clear() { + if (isEmpty()) { + return; + } + + if (begin < end) { + Arrays.fill(elements, begin, end, null); + } else { + Arrays.fill(elements, 0, end, null); + Arrays.fill(elements, begin, elements.length, null); + } + + begin = -1; + end = 0; + } + + // Deque + + public void addFirst(E e) { + final int oldSize = size(); + if (oldSize == elements.length) { + grow(); + } + + if (oldSize == 0) { + begin = elements.length - 1; + } else { + begin = dec(begin, elements.length); + } + elements[begin] = e; + } + + public void addLast(E e) { + final int oldSize = size(); + if (oldSize == elements.length) { + grow(); + } + elements[end] = e; + end = inc(end, elements.length); + + if (oldSize == 0) { + begin = 0; + } + } + + public E removeFirst() { + final int oldSize = size(); + if (oldSize == 0) { + throw new NoSuchElementException(); + } + + Object res = elements[begin]; + elements[begin] = null; + + if (oldSize == 1) { + begin = -1; + end = 0; + } else { + begin = inc(begin, elements.length); + } + return (E) res; + } + + public E removeLast() { + final int oldSize = size(); + if (oldSize == 0) { + throw new NoSuchElementException(); + } + final int lastIdx = dec(end, elements.length); + E res = (E) elements[lastIdx]; + elements[lastIdx] = null; + + if (oldSize == 1) { + begin = -1; + end = 0; + } else { + end = lastIdx; + } + return res; + } + + public E getFirst() { + if (isEmpty()) + throw new NoSuchElementException(); + + return get(0); + } + + public E getLast() { + if (isEmpty()) + throw new NoSuchElementException(); + + return get(size() - 1); + } +} \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/maven/GeoUtil.java b/src/main/java/com/ghostchu/peerbanhelper/util/maven/GeoUtil.java index acdfa5d27e..6261d60e93 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/maven/GeoUtil.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/maven/GeoUtil.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.maven; import com.ghostchu.peerbanhelper.text.Lang; +import com.github.mizosoft.methanol.Methanol; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -21,6 +22,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentSkipListMap; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + public class GeoUtil { private static volatile Boolean inChinaRegion = null; @@ -37,7 +40,8 @@ public static CompletableFuture connectTest(String ipAddress, int port, } private static long sendGetTest(String urlStr) { - try (HttpClient client = HttpClient.newBuilder() + try (HttpClient client = Methanol.newBuilder() + .defaultHeader("Accept-Encoding", "gzip,deflate") .connectTimeout(Duration.of(5, ChronoUnit.SECONDS)) .followRedirects(HttpClient.Redirect.ALWAYS) .build()) { @@ -59,7 +63,7 @@ private static long sendGetTest(String urlStr) { @NotNull public static List determineBestMirrorServer(Logger logger) { - logger.info(Lang.LIBRARIES_LOADER_DETERMINE_BEST_MIRROR); + logger.info(tlUI(Lang.LIBRARIES_LOADER_DETERMINE_BEST_MIRROR)); List> testEntry = new ArrayList<>(); Map mirrorPingMap = new ConcurrentSkipListMap<>(); for (MavenCentralMirror value : MavenCentralMirror.values()) { @@ -71,7 +75,7 @@ public static List determineBestMirrorServer(Logger logger) testEntry.forEach(CompletableFuture::join); List> list = new ArrayList<>(mirrorPingMap.entrySet()); list.sort(Map.Entry.comparingByValue()); - logger.info(Lang.LIBRARIES_LOADER_DETERMINE_TEST_RESULT); + logger.info(tlUI(Lang.LIBRARIES_LOADER_DETERMINE_TEST_RESULT)); list.forEach(e -> { String cost = "DNF"; if (e.getValue() != Long.MAX_VALUE) { diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/ModuleMatchCache.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/ModuleMatchCache.java index 5c35bb0805..e4c601cba7 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/ModuleMatchCache.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/ModuleMatchCache.java @@ -2,60 +2,85 @@ import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.event.BtnRuleUpdateEvent; -import com.ghostchu.peerbanhelper.module.FeatureModule; +import com.ghostchu.peerbanhelper.module.AbstractRuleFeatureModule; +import com.ghostchu.peerbanhelper.module.CheckResult; +import com.ghostchu.peerbanhelper.module.PeerAction; import com.ghostchu.peerbanhelper.module.RuleFeatureModule; -import com.ghostchu.peerbanhelper.module.impl.rule.BtnNetworkOnline; -import com.ghostchu.peerbanhelper.torrent.Torrent; -import com.ghostchu.peerbanhelper.wrapper.PeerAddress; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.eventbus.Subscribe; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +@Component +@Slf4j public class ModuleMatchCache { - public final Map> CACHE_POOL = new ConcurrentHashMap<>(); + public final Cache CACHE_POOL = CacheBuilder + .newBuilder() + .maximumWeight(50000L) + .weigher((key, value) -> { + if (value == AbstractRuleFeatureModule.HANDSHAKING_CHECK_RESULT + || value == AbstractRuleFeatureModule.TEAPOT_CHECK_RESULT + || value == AbstractRuleFeatureModule.OK_CHECK_RESULT) { + return 1; + } + return 5; + }) + .softValues() + .expireAfterAccess(10, TimeUnit.MINUTES) + .build(); public ModuleMatchCache() { Main.getEventBus().register(this); } - public boolean shouldSkipCheck(RuleFeatureModule module, Torrent torrent, PeerAddress peerAddress, boolean writeCache) { - Wrapper wrapper = new Wrapper(torrent.getId(), peerAddress); - Cache ruleCacheZone = CACHE_POOL.get(module); - if (ruleCacheZone == null) { - ruleCacheZone = CacheBuilder.newBuilder() - .expireAfterAccess(30, TimeUnit.MINUTES) - .maximumSize(3000) - .softValues() - .build(); - CACHE_POOL.put(module, ruleCacheZone); - } - Boolean cached = ruleCacheZone.getIfPresent(wrapper); - boolean result = cached != null; + public CheckResult readCache(RuleFeatureModule module, String cacheKey, Callable resultSupplier, boolean writeCache) { + String _cacheKey = module.getConfigName() + '@' + cacheKey; if (writeCache) { - ruleCacheZone.put(wrapper, result); + try { + return CACHE_POOL.get(_cacheKey, resultSupplier); + } catch (ExecutionException e) { + log.error("Unable to get cache value from cache, the resultSupplier throws unexpected exception", e); + return null; + } + } else { + return CACHE_POOL.getIfPresent(_cacheKey); } - return result; } - @Subscribe - public void onBtnRuleUpdated(BtnRuleUpdateEvent event) { - for (FeatureModule featureModule : CACHE_POOL.keySet()) { - if (featureModule.getClass().equals(BtnNetworkOnline.class)) { - CACHE_POOL.remove(featureModule); - return; + // 只缓存 PeerAction 为 BAN 以外的结果 + public CheckResult readCacheButWritePassOnly(RuleFeatureModule module, String cacheKey, Callable resultSupplier, boolean writeCache) { + String _cacheKey = module.getConfigName() + '@' + cacheKey; + var cached = CACHE_POOL.getIfPresent(_cacheKey); + if (cached == null) { + try { + cached = resultSupplier.call(); + } catch (Exception e) { + log.warn("Unable to compute result", e); } } + if (writeCache && cached != null && cached.action() != PeerAction.BAN) { + CACHE_POOL.put(_cacheKey, cached); + } + return cached; + } - public void close() { - Main.getEventBus().unregister(this); + @Subscribe + public void onBtnRuleUpdated(BtnRuleUpdateEvent event) { + CACHE_POOL.invalidateAll(); } - public record Wrapper(String torrentId, PeerAddress address) { + public void invalidateAll() { + CACHE_POOL.invalidateAll(); + } + public void close() { + Main.getEventBus().unregister(this); } + } diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/Rule.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/Rule.java index 94607acff2..19fe6b3abc 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/Rule.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/Rule.java @@ -1,26 +1,29 @@ package com.ghostchu.peerbanhelper.util.rule; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.JsonUtil; import org.jetbrains.annotations.NotNull; import java.util.LinkedHashMap; import java.util.Map; +import static com.ghostchu.peerbanhelper.text.TextManager.tl; + public interface Rule { @NotNull MatchResult match(@NotNull String content); Map metadata(); - default String matcherName() { + default TranslationComponent matcherName() { return null; } String matcherIdentifier(); - default String toPrintableText() { + default String toPrintableText(String locale) { Map info = new LinkedHashMap<>(); - info.put("matcherName", matcherName()); + info.put("matcherName", tl(locale, matcherName())); info.put("matcherIdentifier", matcherIdentifier()); info.put("metadata", metadata()); return JsonUtil.standard().toJson(info); diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/IPMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/IPMatcher.java index e2b2428381..f7336d3577 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/IPMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/IPMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.IPAddressUtil; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.ghostchu.peerbanhelper.util.rule.RuleMatcher; @@ -16,6 +17,8 @@ import java.util.ArrayList; import java.util.List; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @@ -50,7 +53,7 @@ public void setData(String ruleName, List ruleData) { ipAddress.nonZeroHostIterator().forEachRemaining(ipsList::add); } else { this.subnets.add(ipAddress); - log.debug(Lang.IP_BAN_RULE_LOAD_CIDR, ruleName, ipAddress); + log.debug(tlUI(Lang.IP_BAN_RULE_LOAD_CIDR, ruleName, ipAddress)); } } else { ipsList.add(ipAddress); @@ -58,7 +61,7 @@ public void setData(String ruleName, List ruleData) { ipsList.forEach(ip -> { ip = ip.withoutPrefixLength(); this.ips.add(ip); - log.debug(Lang.IP_BAN_RULE_LOAD_IP, ruleName, ip); + log.debug(tlUI(Lang.IP_BAN_RULE_LOAD_IP, ruleName, ip)); }); }); bloomFilter = BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), this.ips.size(), 0.01); @@ -83,8 +86,8 @@ public void setData(String ruleName, List ruleData) { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_SUB_RULE; + public TranslationComponent matcherName() { + return new TranslationComponent(Lang.RULE_MATCHER_SUB_RULE, getRuleName()); } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringContainsMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringContainsMatcher.java index a48a82d4e0..6174362f68 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringContainsMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringContainsMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.AbstractJsonMatcher; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.google.gson.JsonObject; @@ -14,6 +15,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StringContainsMatcher extends AbstractJsonMatcher { + private static final TranslationComponent nameComponent = new TranslationComponent(Lang.RULE_MATCHER_STRING_CONTAINS); private final String rule; private MatchResult hit = MatchResult.TRUE; private MatchResult miss = MatchResult.DEFAULT; @@ -40,8 +42,8 @@ public StringContainsMatcher(JsonObject syntax) { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_STRING_CONTAINS; + public TranslationComponent matcherName() { + return nameComponent; } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEndsWithMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEndsWithMatcher.java index eef29be36c..2c284f293f 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEndsWithMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEndsWithMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.AbstractJsonMatcher; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.google.gson.JsonObject; @@ -14,6 +15,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StringEndsWithMatcher extends AbstractJsonMatcher { + private static final TranslationComponent nameComponent = new TranslationComponent(Lang.RULE_MATCHER_STRING_ENDS_WITH); private final String rule; private MatchResult hit = MatchResult.TRUE; private MatchResult miss = MatchResult.DEFAULT; @@ -40,8 +42,8 @@ public StringEndsWithMatcher(JsonObject syntax) { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_STRING_ENDS_WITH; + public TranslationComponent matcherName() { + return nameComponent; } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEqualsMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEqualsMatcher.java index 0e172547fd..8fb754b79f 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEqualsMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringEqualsMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.AbstractJsonMatcher; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.google.gson.JsonObject; @@ -13,6 +14,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StringEqualsMatcher extends AbstractJsonMatcher { + private static final TranslationComponent nameComponent = new TranslationComponent(Lang.RULE_MATCHER_STRING_EQUALS); private final String rule; private MatchResult hit = MatchResult.TRUE; private MatchResult miss = MatchResult.DEFAULT; @@ -38,8 +40,8 @@ public StringEqualsMatcher(JsonObject syntax) { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_STRING_LENGTH; + public TranslationComponent matcherName() { + return nameComponent; } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringLengthMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringLengthMatcher.java index 2515adff14..3476ae1b5f 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringLengthMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringLengthMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.AbstractJsonMatcher; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.google.gson.JsonObject; @@ -13,6 +14,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StringLengthMatcher extends AbstractJsonMatcher { + private static final TranslationComponent nameComponent = new TranslationComponent(Lang.RULE_MATCHER_STRING_LENGTH); private final int min; private final int max; private MatchResult hit = MatchResult.TRUE; @@ -37,8 +39,8 @@ public StringLengthMatcher(JsonObject syntax) { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_STRING_LENGTH; + public TranslationComponent matcherName() { + return nameComponent; } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringRegexMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringRegexMatcher.java index 08eeb466b8..99d7451838 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringRegexMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringRegexMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.AbstractJsonMatcher; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.google.gson.JsonObject; @@ -14,6 +15,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StringRegexMatcher extends AbstractJsonMatcher { + private static final TranslationComponent nameComponent = new TranslationComponent(Lang.RULE_MATCHER_STRING_REGEX); private final Pattern rule; private MatchResult hit = MatchResult.TRUE; private MatchResult miss = MatchResult.DEFAULT; @@ -44,8 +46,8 @@ public String matcherIdentifier() { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_STRING_REGEX; + public TranslationComponent matcherName() { + return nameComponent; } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringStartsWithMatcher.java b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringStartsWithMatcher.java index b196187cb2..6b9b2c7a6e 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringStartsWithMatcher.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/rule/matcher/StringStartsWithMatcher.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.util.rule.matcher; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.util.rule.AbstractJsonMatcher; import com.ghostchu.peerbanhelper.util.rule.MatchResult; import com.google.gson.JsonObject; @@ -14,6 +15,7 @@ @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class StringStartsWithMatcher extends AbstractJsonMatcher { + private static final TranslationComponent nameComponent = new TranslationComponent(Lang.RULE_MATCHER_STRING_STARTS_WITH); private final String rule; private MatchResult hit = MatchResult.TRUE; private MatchResult miss = MatchResult.DEFAULT; @@ -46,8 +48,8 @@ public String matcherIdentifier() { } @Override - public @NotNull String matcherName() { - return Lang.RULE_MATCHER_STRING_STARTS_WITH; + public TranslationComponent matcherName() { + return nameComponent; } @Override diff --git a/src/main/java/com/ghostchu/peerbanhelper/util/time/TimeoutProtect.java b/src/main/java/com/ghostchu/peerbanhelper/util/time/TimeoutProtect.java index 40b0c868c8..dc0e111af9 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/util/time/TimeoutProtect.java +++ b/src/main/java/com/ghostchu/peerbanhelper/util/time/TimeoutProtect.java @@ -9,6 +9,8 @@ import java.util.concurrent.Executors; import java.util.function.Consumer; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public class TimeoutProtect implements AutoCloseable { @Getter @@ -53,7 +55,7 @@ public void runIfTimeout(Consumer timeout) { public void printUnfinishedTasks() { if (this.unfinishedTasks != null) { this.unfinishedTasks.forEach(r -> { - log.warn(Lang.TIMING_UNFINISHED_TASK, r); + log.warn(tlUI(Lang.TIMING_UNFINISHED_TASK, r)); }); } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/web/JavalinWebContainer.java b/src/main/java/com/ghostchu/peerbanhelper/web/JavalinWebContainer.java index aa7d2fef9a..28b8585638 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/web/JavalinWebContainer.java +++ b/src/main/java/com/ghostchu/peerbanhelper/web/JavalinWebContainer.java @@ -1,10 +1,15 @@ package com.ghostchu.peerbanhelper.web; +import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.text.Lang; +import com.ghostchu.peerbanhelper.text.TextManager; import com.ghostchu.peerbanhelper.util.JsonUtil; import com.ghostchu.peerbanhelper.web.exception.IPAddressBannedException; import com.ghostchu.peerbanhelper.web.exception.NotLoggedInException; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import io.javalin.Javalin; +import io.javalin.http.Context; import io.javalin.http.HttpStatus; import io.javalin.http.staticfiles.Location; import io.javalin.json.JsonMapper; @@ -12,18 +17,27 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Component; +import java.io.File; import java.lang.reflect.Type; -import java.util.Map; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static com.ghostchu.peerbanhelper.text.TextManager.tl; @Slf4j +@Component public class JavalinWebContainer { private final Javalin javalin; @Getter - private final String token; + private String token; + private Cache FAIL2BAN = CacheBuilder.newBuilder() + .expireAfterWrite(15, TimeUnit.MINUTES) + .build(); - public JavalinWebContainer(String host, int port, String token) { - this.token = token; + public JavalinWebContainer() { JsonMapper gsonMapper = new JsonMapper() { @Override public @NotNull String toJsonString(@NotNull Object obj, @NotNull Type type) { @@ -41,29 +55,50 @@ public JavalinWebContainer(String host, int port, String token) { c.showJavalinBanner = false; c.jsonMapper(gsonMapper); c.useVirtualThreads = true; - c.bundledPlugins.enableCors(cors -> cors.addRule(CorsPluginConfig.CorsRule::anyHost)); - c.staticFiles.add(staticFiles -> { - staticFiles.hostedPath = "/"; - staticFiles.directory = "/static"; - staticFiles.location = Location.CLASSPATH; - staticFiles.precompress = false; - staticFiles.aliasCheck = null; - staticFiles.skipFileFunction = req -> false; - }); - c.spaRoot.addFile("/", "/static/index.html", Location.CLASSPATH); + if (Main.getMainConfig().getBoolean("server.allow-cors")) { + c.bundledPlugins.enableCors(cors -> cors.addRule(CorsPluginConfig.CorsRule::anyHost)); + } + if (Main.getMainConfig().getBoolean("server.external-webui", false)) { + c.staticFiles.add(staticFiles -> { + staticFiles.hostedPath = "/"; + staticFiles.directory = new File(Main.getDataDirectory(), "static").getPath(); + staticFiles.location = Location.EXTERNAL; + staticFiles.precompress = false; + staticFiles.aliasCheck = null; + staticFiles.skipFileFunction = req -> false; + staticFiles.headers.put("Cache-Control", "no-cache"); + }); + c.spaRoot.addFile("/", new File(new File(Main.getDataDirectory(), "static"), "index.html").getPath(), Location.EXTERNAL); + } else { + c.staticFiles.add(staticFiles -> { + staticFiles.hostedPath = "/"; + staticFiles.directory = "/static"; + staticFiles.location = Location.CLASSPATH; + staticFiles.precompress = false; + staticFiles.aliasCheck = null; + staticFiles.skipFileFunction = req -> false; + staticFiles.headers.put("Cache-Control", "no-cache"); + }); + c.spaRoot.addFile("/", "/static/index.html", Location.CLASSPATH); + } + }) .exception(IPAddressBannedException.class, (e, ctx) -> { ctx.status(HttpStatus.TOO_MANY_REQUESTS); - ctx.json(Map.of("message", Lang.WEBAPI_AUTH_BANNED_TOO_FREQ)); + ctx.json(Map.of("message", tl(reqLocale(ctx), Lang.WEBAPI_AUTH_BANNED_TOO_FREQ))); }) .exception(NotLoggedInException.class, (e, ctx) -> { ctx.status(HttpStatus.FORBIDDEN); - ctx.json(Map.of("message", Lang.WEBAPI_NOT_LOGGED)); + ctx.json(Map.of("message", tl(reqLocale(ctx), Lang.WEBAPI_NOT_LOGGED))); + }) + .exception(IllegalArgumentException.class, (e, ctx) -> { + ctx.status(HttpStatus.BAD_REQUEST); + ctx.json(Map.of("message", e.getMessage())); }) .exception(Exception.class, (e, ctx) -> { ctx.status(HttpStatus.INTERNAL_SERVER_ERROR); - ctx.json(Map.of("message", Lang.WEBAPI_INTERNAL_ERROR)); - log.warn("500 Internal Server Error", e); + ctx.json(Map.of("message", tl(reqLocale(ctx), Lang.WEBAPI_INTERNAL_ERROR))); + log.error("500 Internal Server Error", e); }) .beforeMatched(ctx -> { if (ctx.routeRoles().isEmpty()) { @@ -76,22 +111,77 @@ public JavalinWebContainer(String host, int port, String token) { if (authenticated != null && authenticated.equals(token)) { return; } + // 开始登陆验证 + var counter = FAIL2BAN.get(ctx.ip(), () -> new AtomicInteger(0)); + if (counter.get() > 10) { + throw new IPAddressBannedException(); + } String authToken = ctx.header("Authorization"); if (authToken != null) { if (authToken.startsWith("Bearer ")) { String tk = authToken.substring(7); if (tk.equals(token)) { + counter.set(0); return; } } } + counter.incrementAndGet(); throw new NotLoggedInException(); }) - .options("/*", ctx -> ctx.status(200)) - .start(host, port); + .options("/*", ctx -> ctx.status(200)); + + } + + public void start(String host, int port, String token) { + this.token = token; + javalin.start(host, port); } public Javalin javalin() { return javalin; } + + public String reqLocale(Context context) { + for (AcceptLanguages requestLocale : requestLocales(context)) { + String pbhCode = requestLocale.code.toLowerCase(Locale.ROOT).replace("-", "_"); + if (TextManager.INSTANCE_HOLDER.getAvailableLanguages().contains(pbhCode)) { + return pbhCode; + } + } + return Main.DEF_LOCALE; + } + + private List requestLocales(Context context) { + String headerLocale = context.header("Accept-Language"); + if (headerLocale == null) { + return List.of(new AcceptLanguages(Main.DEF_LOCALE, 1.0f)); + } + // zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6 + // zh + List preferLocales = new ArrayList<>(); + String[] browserRequested = headerLocale.split(","); + for (String s : browserRequested) { + String[] localeSettings = s.split(";"); + String localeCode = localeSettings[0]; + float prefer = 1.0f; + try { + if (localeSettings.length > 1) { + prefer = Float.parseFloat(localeSettings[1].substring(2)); + } + } catch (Exception ignored) { + } + preferLocales.add(new AcceptLanguages(localeCode, prefer)); + } + preferLocales.sort(Comparator.reverseOrder()); + return preferLocales; + } + + + public record AcceptLanguages(String code, float prefer) implements Comparable { + @Override + public int compareTo(@NotNull JavalinWebContainer.AcceptLanguages o) { + return Float.compare(prefer, o.prefer); + } + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/ASNWrapper.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/ASNWrapper.java deleted file mode 100644 index c883bd3959..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/ASNWrapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ghostchu.peerbanhelper.wrapper; - -import com.maxmind.geoip2.model.AsnResponse; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; - -@Data -@NoArgsConstructor -@AllArgsConstructor -public class ASNWrapper { - private long asn; - private String asOrganization; - private String asNetwork; - - public ASNWrapper(@NotNull AsnResponse asnResponse) { - this.asn = asnResponse.getAutonomousSystemNumber(); - this.asOrganization = asnResponse.getAutonomousSystemOrganization(); - this.asNetwork = asnResponse.getNetwork().toString(); - } -} diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedBanMetadata.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedBanMetadata.java index 764088dfd6..315e1b4bd9 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedBanMetadata.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedBanMetadata.java @@ -1,24 +1,46 @@ package com.ghostchu.peerbanhelper.wrapper; -import com.ghostchu.peerbanhelper.Main; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; +import com.ghostchu.peerbanhelper.ipdb.IPGeoData; import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.jetbrains.annotations.NotNull; + +import java.io.Serializable; +import java.util.UUID; + +import static com.ghostchu.peerbanhelper.text.TextManager.tl; -@EqualsAndHashCode(callSuper = true) @Data -public class BakedBanMetadata extends BanMetadata { - private GeoWrapper geo; - private ASNWrapper asn; +@NoArgsConstructor +public class BakedBanMetadata implements Comparable, Serializable { + private String downloader; + private UUID randomId; + private TorrentWrapper torrent; + private PeerWrapper peer; + private IPGeoData geo; + private String reverseLookup = "N/A"; + private String context; + private long banAt; + private long unbanAt; + private String rule; + private String description; + + public BakedBanMetadata(String locale, BanMetadata banMetadata) { + this.randomId = banMetadata.getRandomId(); + this.downloader = banMetadata.getDownloader(); + this.torrent = banMetadata.getTorrent(); + this.peer = banMetadata.getPeer(); + this.geo = banMetadata.getGeo(); + this.reverseLookup = banMetadata.getReverseLookup(); + this.context = banMetadata.getContext(); + this.banAt = banMetadata.getBanAt(); + this.unbanAt = banMetadata.getUnbanAt(); + this.rule = tl(locale, banMetadata.getRule()); + this.description = tl(locale, banMetadata.getDescription()); + } - public BakedBanMetadata(BanMetadata banMetadata) { - super(banMetadata.getContext(), banMetadata.getDownloader(), banMetadata.getBanAt(), - banMetadata.getUnbanAt(), banMetadata.getTorrent(), banMetadata.getPeer(), banMetadata.getRule(), - banMetadata.getDescription()); - PeerBanHelperServer.IPDBResponse resp = Main.getServer().queryIPDB(new PeerAddress(banMetadata.getPeer().getAddress().getIp(), banMetadata.getPeer().getAddress().getPort())); - if (resp.cityResponse().get() != null) { - this.geo = new GeoWrapper(resp.cityResponse().get()); - this.asn = new ASNWrapper(resp.asnResponse().get()); - } + @Override + public int compareTo(@NotNull BakedBanMetadata o) { + return this.randomId.compareTo(o.randomId); } -} \ No newline at end of file +} diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedPeerMetadata.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedPeerMetadata.java deleted file mode 100644 index a632ffadcc..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/BakedPeerMetadata.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ghostchu.peerbanhelper.wrapper; - -import com.ghostchu.peerbanhelper.Main; -import com.ghostchu.peerbanhelper.PeerBanHelperServer; -import lombok.Data; -import lombok.EqualsAndHashCode; - -@EqualsAndHashCode(callSuper = true) -@Data -public class BakedPeerMetadata extends PeerMetadata { - private GeoWrapper geo; - private ASNWrapper asn; - - public BakedPeerMetadata(PeerMetadata banMetadata) { - super(banMetadata.getDownloader(), banMetadata.getTorrent(), banMetadata.getPeer()); - PeerBanHelperServer.IPDBResponse resp = Main.getServer().queryIPDB(new PeerAddress(banMetadata.getPeer().getAddress().getIp(), banMetadata.getPeer().getAddress().getPort())); - if (resp.cityResponse().get() != null) { - this.geo = new GeoWrapper(resp.cityResponse().get()); - } - if (resp.asnResponse().get() != null) { - this.asn = new ASNWrapper(resp.asnResponse().get()); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/BanMetadata.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/BanMetadata.java index 99c67c7fcb..f665544163 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/BanMetadata.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/BanMetadata.java @@ -1,23 +1,26 @@ package com.ghostchu.peerbanhelper.wrapper; import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.text.TranslationComponent; import com.ghostchu.peerbanhelper.torrent.Torrent; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import java.io.Serializable; + @EqualsAndHashCode(callSuper = true) -@Data @NoArgsConstructor -public class BanMetadata extends PeerMetadata implements Comparable { +@Data +public class BanMetadata extends PeerMetadata implements Comparable, Serializable { private String context; private long banAt; private long unbanAt; - private String rule; - private String description; + private TranslationComponent rule; + private TranslationComponent description; - public BanMetadata(String context, String downloader, long banAt, long unbanAt, Torrent torrent, Peer peer, String rule, - String description) { + public BanMetadata(String context, String downloader, long banAt, long unbanAt, Torrent torrent, Peer peer, TranslationComponent rule, + TranslationComponent description) { super(downloader, torrent, peer); this.context = context; this.banAt = banAt; @@ -26,8 +29,8 @@ public BanMetadata(String context, String downloader, long banAt, long unbanAt, this.description = description; } - public BanMetadata(String context, String downloader, long banAt, long unbanAt, TorrentWrapper torrent, PeerWrapper peer, String rule, - String description) { + public BanMetadata(String context, String downloader, long banAt, long unbanAt, TorrentWrapper torrent, PeerWrapper peer, TranslationComponent rule, + TranslationComponent description) { super(downloader, torrent, peer); this.context = context; this.banAt = banAt; @@ -35,4 +38,6 @@ public BanMetadata(String context, String downloader, long banAt, long unbanAt, this.rule = rule; this.description = description; } + + } diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/GeoWrapper.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/GeoWrapper.java deleted file mode 100644 index a37bd014c0..0000000000 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/GeoWrapper.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.ghostchu.peerbanhelper.wrapper; - -import com.maxmind.geoip2.model.CityResponse; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.jetbrains.annotations.NotNull; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class GeoWrapper { - private String iso; - private String countryRegion; - private String city; - private Double latitude; - private Double longitude; - private Integer accuracyRadius; - - public GeoWrapper(@NotNull CityResponse cityResponse) { - if (cityResponse.getCountry() != null) { - this.iso = cityResponse.getCountry().getIsoCode(); - this.countryRegion = cityResponse.getCountry().getName(); - } - if (cityResponse.getCity() != null) { - this.city = cityResponse.getCity().getName(); - } - if (cityResponse.getLocation() != null) { - this.latitude = cityResponse.getLocation().getLatitude(); - this.longitude = cityResponse.getLocation().getLongitude(); - this.accuracyRadius = cityResponse.getLocation().getAccuracyRadius(); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddress.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddress.java index acbfa4d684..5025231507 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddress.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddress.java @@ -7,11 +7,13 @@ import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import java.io.Serializable; + @Data @AllArgsConstructor @NoArgsConstructor @Slf4j -public class PeerAddress implements Comparable { +public final class PeerAddress implements Comparable, Serializable { private String ip; private transient IPAddress address; @@ -23,7 +25,6 @@ public class PeerAddress implements Comparable { public PeerAddress(String ip, int port) { this.ip = ip; this.port = port; - this.address = IPAddressUtil.getIPAddress(ip); } public IPAddress getAddress() { @@ -33,6 +34,13 @@ public IPAddress getAddress() { return address; } + @Override + public String toString() { + return "PeerAddress{" + + "ip='" + ip + '\'' + + ", port=" + port + + '}'; + } @Override public int compareTo(PeerAddress o) { diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddressWrapper.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddressWrapper.java index 985954cf80..6203f3775b 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddressWrapper.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerAddressWrapper.java @@ -7,7 +7,7 @@ @Data @NoArgsConstructor @AllArgsConstructor -public class PeerAddressWrapper { +public final class PeerAddressWrapper { private int port; private String ip; diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerMetadata.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerMetadata.java index 648437b3f1..45acdcd7ae 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerMetadata.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerMetadata.java @@ -1,5 +1,6 @@ package com.ghostchu.peerbanhelper.wrapper; +import com.ghostchu.peerbanhelper.ipdb.IPGeoData; import com.ghostchu.peerbanhelper.peer.Peer; import com.ghostchu.peerbanhelper.torrent.Torrent; import lombok.AllArgsConstructor; @@ -16,9 +17,9 @@ public class PeerMetadata implements Comparable { private UUID randomId; private TorrentWrapper torrent; private PeerWrapper peer; + private IPGeoData geo; private String reverseLookup = "N/A"; - public PeerMetadata(String downloader, Torrent torrent, Peer peer) { this.randomId = UUID.randomUUID(); this.downloader = downloader; diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerWrapper.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerWrapper.java index 199611a4a5..292a92017a 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerWrapper.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/PeerWrapper.java @@ -1,6 +1,7 @@ package com.ghostchu.peerbanhelper.wrapper; import com.ghostchu.peerbanhelper.peer.Peer; +import com.ghostchu.peerbanhelper.peer.PeerFlag; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -8,7 +9,7 @@ @Data @NoArgsConstructor @AllArgsConstructor -public class PeerWrapper { +public final class PeerWrapper { private PeerAddressWrapper address; private String id; private String clientName; @@ -17,7 +18,7 @@ public class PeerWrapper { private long uploaded; private long uploadSpeed; private double progress; - private String flags; + private PeerFlag flags; public PeerWrapper(Peer peer) { this.id = peer.getPeerId(); @@ -30,4 +31,8 @@ public PeerWrapper(Peer peer) { this.progress = peer.getProgress(); this.flags = peer.getFlags(); } + + public PeerAddress toPeerAddress() { + return new PeerAddress(address.getIp(), address.getPort()); + } } diff --git a/src/main/java/com/ghostchu/peerbanhelper/wrapper/TorrentWrapper.java b/src/main/java/com/ghostchu/peerbanhelper/wrapper/TorrentWrapper.java index 504187a202..58b4f0cf28 100644 --- a/src/main/java/com/ghostchu/peerbanhelper/wrapper/TorrentWrapper.java +++ b/src/main/java/com/ghostchu/peerbanhelper/wrapper/TorrentWrapper.java @@ -8,7 +8,7 @@ @Data @NoArgsConstructor @AllArgsConstructor -public class TorrentWrapper { +public final class TorrentWrapper { private String id; private long size; private String name; diff --git a/src/main/java/cordelia/client/TrClient.java b/src/main/java/cordelia/client/TrClient.java index e0af93dfd2..be154784f2 100644 --- a/src/main/java/cordelia/client/TrClient.java +++ b/src/main/java/cordelia/client/TrClient.java @@ -1,6 +1,5 @@ package cordelia.client; -import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.text.Lang; import com.ghostchu.peerbanhelper.util.HTTPUtil; import com.ghostchu.peerbanhelper.util.JsonUtil; @@ -28,6 +27,8 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; +import static com.ghostchu.peerbanhelper.text.TextManager.tlUI; + @Slf4j public final class TrClient { @@ -64,7 +65,7 @@ public TrClient(String url, String user, String password, boolean verifySSL, Htt .newBuilder() .version(httpVersion) .followRedirects(HttpClient.Redirect.ALWAYS) - .userAgent(Main.getUserAgent()) + .defaultHeader("Accept-Encoding", "gzip,deflate") .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) .headersTimeout(Duration.of(10, ChronoUnit.SECONDS)) .readTimeout(Duration.of(15, ChronoUnit.SECONDS)) @@ -104,10 +105,10 @@ public TypedResponse execute(E String json = om.toJson(raw.getArguments()); return new TypedResponse<>(raw.getTag(), raw.getResult(), om.fromJson(json, req.answerClass())); } catch (JsonSyntaxException jsonSyntaxException) { - log.warn(Lang.DOWNLOADER_TR_INVALID_RESPONSE, jsonBuffer, jsonSyntaxException); + log.error(tlUI(Lang.DOWNLOADER_TR_INVALID_RESPONSE, jsonBuffer, jsonSyntaxException)); throw new IllegalStateException(jsonSyntaxException); } catch (IOException | InterruptedException e) { - log.warn("Request Transmission JsonRPC failure", e); + log.error("Request Transmission JsonRPC failure", e); throw new IllegalStateException(e); } } @@ -124,7 +125,7 @@ private Session session(boolean forceUpdate) { String sessionId = resp.headers().firstValue(Session.SESSION_ID).orElseThrow(); sessionStore.set(new Session(sessionId)); } catch (IOException | InterruptedException e) { - log.warn(Lang.TRCLIENT_API_ERROR, e.getClass().getName(), e.getMessage()); + log.error(tlUI(Lang.TRCLIENT_API_ERROR, e.getClass().getName()), e.getMessage()); throw new IllegalStateException(e); } } diff --git a/src/main/java/io/github/szabogabriel/jscgi/Mode.java b/src/main/java/io/github/szabogabriel/jscgi/Mode.java new file mode 100644 index 0000000000..7aec74ff3f --- /dev/null +++ b/src/main/java/io/github/szabogabriel/jscgi/Mode.java @@ -0,0 +1,22 @@ +package io.github.szabogabriel.jscgi; + +/** + * Operation modes of the underlying application. + *

+ * The STANDARD mode is compatible with most standard implementation of SCGI + * servers and clients. + *

+ * The SCGI_MESSAGE_BASED mode can be used to work entirely with + * {@link SCGIMessage} classes on both the request + * and the response levels. This method won't close the sockets opened + * beforehand, thus providing a better performance with the tradeoff of not + * being able to stream the data sent. + * + * @author gszabo + */ +public enum Mode { + + STANDARD, SCGI_MESSAGE_BASED, + ; + +} diff --git a/src/main/java/io/github/szabogabriel/jscgi/SCGIMessage.java b/src/main/java/io/github/szabogabriel/jscgi/SCGIMessage.java new file mode 100644 index 0000000000..c648f1b637 --- /dev/null +++ b/src/main/java/io/github/szabogabriel/jscgi/SCGIMessage.java @@ -0,0 +1,216 @@ +package io.github.szabogabriel.jscgi; + +import io.github.szabogabriel.jscgi.util.SCGIUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * The SCGIMessage class is the foundation for this SCGI implementation. It + * holds, serializes and deserializes SCGI messages. + * + * @author gszabo + */ +public class SCGIMessage { + + private static final String CONTENT_LENGTH = "CONTENT_LENGTH"; + + private Map headers; + private int bodyLengthInt = 0; + private byte[] body; + private InputStream socketIn; + + /** + * Create an empty SCGI message without any headers or body. + */ + public SCGIMessage() { + this(new HashMap<>(), new byte[]{}); + } + + /** + * Create an SCGI message with default values. + * + * @param headers a {@link Map} holding the HTTP headers. + * @param body a simple byte array as body. + */ + public SCGIMessage(Map headers, byte[] body) { + if (headers == null || body == null) + throw new NullPointerException(); + + setHeaders(headers); + setBody(body); + } + + /** + * An SCGI message can be created from an InputStream. This constructor is + * useful used mainly by the underlying implementation of the client and the + * server. + * + * @param in the {@link InputStream} to be used as source. + * @throws IOException + */ + public SCGIMessage(InputStream in) throws IOException { + this(); + socketIn = in; + + int contentLength = contentLength(); + byte[] headersStream = read(contentLength); + int comma = socketIn.read(); + + if (comma == ',') { + headers = SCGIUtil.parseHeaders(headersStream); + + String bodyLength = headers.get(CONTENT_LENGTH); + if (bodyLength != null && bodyLength.length() > 0) { + bodyLengthInt = Integer.parseInt(bodyLength); + } + } + } + + /** + * Adds a specific header to the SCGIMessage. It overrides the previous value if + * present. + * + * @param key of the header attribute to be added. + * @param value of the header attribute to be added. + */ + public void addHeader(String key, String value) { + headers.put(key, value); + } + + /** + * Remove the header identified by the key. If not present, nothing will happen. + * + * @param key key of the header attribute to be removed. + */ + public void removeHeader(String key) { + headers.remove(key); + } + + /** + * Fetch the header attribute identified by the key. + * + * @param key of the header attribute to be returned. + * @return the header attribute or {@code null} if not present. + */ + public String getHeader(String key) { + return headers.get(key); + } + + /** + * Returns all the headers present in the given SCGI message. + * + * @return {@link Map} holding the header attributes. + */ + public Map getHeaders() { + return headers; + } + + /** + * Convenience method setting the {@link Map} holding the header + * attributes. The previous header values (if any) will be overwritten. + * + * @param headers new header values. + */ + public void setHeaders(Map headers) { + this.headers = headers; + } + + /** + * Fetch the curent SCGI message body. + * + * @return a byte array representing the body. + */ + public byte[] getBody() { + byte[] ret = {}; + + try { + ret = read(bodyLengthInt); + } catch (IOException e) { + e.printStackTrace(); + } + + return ret; + } + + /** + * Set the new body value, if not null. + * + * @param body new body of the SCGI message. + */ + public void setBody(byte[] body) { + if (body != null) { + this.body = body; + } + } + + /** + * Fetch the stream from which the SCGI message was created. If the stream was + * not used, it will return a null value. + * + * @return + */ + public InputStream getBodyStream() { + return socketIn; + } + + /** + * Checks whether a body is present in the SCGI message. + * + * @return + */ + public boolean isBodyAvailable() { + return bodyLengthInt > 0; + } + + /** + * Returns an integer representing the size of the body held by this SCGI + * message. + * + * @return + */ + public int getBodySize() { + return bodyLengthInt; + } + + /** + * Write this SCGI message to the {@link OutputStream} provided as a + * attribute. + * + * @param out + * @throws IOException + */ + public void serialize(OutputStream out) throws IOException { + headers.put(CONTENT_LENGTH, "" + body.length); + byte[] headerData = SCGIUtil.createHeaders(headers); + byte[] length = Integer.toString(headerData.length).getBytes(); + + out.write(length); + out.write(58); // the character ':' + out.write(headerData); + out.write(44); // the character ',' + out.write(body); + + } + + private byte[] read(int length) throws IOException { + byte[] buffer = new byte[length]; + socketIn.read(buffer, 0, length); + return buffer; + } + + private int contentLength() throws IOException { + int ret = 0; + int buf; + + while ((buf = socketIn.read()) != ':') { + ret = (ret * 10) + (buf - '0'); + } + + return ret; + } + +} diff --git a/src/main/java/io/github/szabogabriel/jscgi/client/SCGIClient.java b/src/main/java/io/github/szabogabriel/jscgi/client/SCGIClient.java new file mode 100644 index 0000000000..dec25c862c --- /dev/null +++ b/src/main/java/io/github/szabogabriel/jscgi/client/SCGIClient.java @@ -0,0 +1,136 @@ +package io.github.szabogabriel.jscgi.client; + +import io.github.szabogabriel.jscgi.Mode; +import io.github.szabogabriel.jscgi.SCGIMessage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + * A simple client implementation for the SCGI protocol. + * + * @author gszabo + */ +public class SCGIClient { + + private String host; + private int port; + + private Socket socket; + + private InputStream socketIn; + private OutputStream socketOut; + private byte[] buffer = new byte[2048]; + + private Mode mode; + + /** + * Create an SCGI client in the + * {@link Mode.STANDARD} mode. + * + * @param host host of the SCGI server to connect to. + * @param port port of the SCGI server to connect to. + */ + public SCGIClient(String host, int port) { + this(host, port, Mode.STANDARD); + } + + /** + * Create an SCGI lient. + * + * @param host host of the SCGI server to connect to. + * @param port port of the SCGI server to connect to. + * @param mode mode of the SCGI communication. + */ + public SCGIClient(String host, int port, Mode mode) { + this.host = host; + this.port = port; + this.mode = mode; + } + + private void setup() throws IOException { + if (socket == null || socket.isClosed()) { + socket = new Socket(host, port); + + socketIn = socket.getInputStream(); + socketOut = socket.getOutputStream(); + } + } + + /** + * This method is only available for the + * {@link Mode} SCGI_MESSAGE_BASED. It will fetch + * the data into memory and return a new SCGI message instance. + * + * @param request + * @return + * @throws IOException + */ + public SCGIMessage sendAndReceiveAsScgiMessage(SCGIMessage request) throws IOException { + if (mode == Mode.SCGI_MESSAGE_BASED) { + setup(); + request.serialize(socketOut); + return new SCGIMessage(socketIn); + } else { + throw new IllegalStateException(); + } + } + + /** + * This method sends an SCGI request and returns the SCGI message as a byte + * array. This method is only available for the + * {@link Mode} STANDARD mode. + * + * @param request + * @return + * @throws UnknownHostException + * @throws IOException + */ + public byte[] sendAndReceiveAsByteArray(SCGIMessage request) throws UnknownHostException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + sendRequest(request, out); + return out.toByteArray(); + } + + /** + * This method sends an SCGI request and writes the answer into the + * {@link OutputStream} provided as a parameter. This method is only + * available for the {@link Mode} STANDARD mode. + * + * @param request + * @return + * @throws UnknownHostException + * @throws IOException + */ + public void sendRequest(SCGIMessage request, OutputStream response) throws UnknownHostException, IOException { + if (mode == Mode.STANDARD) { + setup(); + + try { + request.serialize(socketOut); + + int read; + while ((read = socketIn.read(buffer)) > 0) { + response.write(buffer, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + socketIn.close(); + socketOut.close(); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + throw new IllegalStateException(); + } + } + +} diff --git a/src/main/java/io/github/szabogabriel/jscgi/client/SCGIUnixSocketClient.java b/src/main/java/io/github/szabogabriel/jscgi/client/SCGIUnixSocketClient.java new file mode 100644 index 0000000000..a3c7b65ff2 --- /dev/null +++ b/src/main/java/io/github/szabogabriel/jscgi/client/SCGIUnixSocketClient.java @@ -0,0 +1,119 @@ +package io.github.szabogabriel.jscgi.client; + +import io.github.szabogabriel.jscgi.Mode; +import io.github.szabogabriel.jscgi.SCGIMessage; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.UnixDomainSocketAddress; +import java.net.UnknownHostException; +import java.nio.channels.Channels; +import java.nio.channels.SocketChannel; + +/** + * A simple client implementation for the SCGI protocol. + * + * @author gszabo + */ +public class SCGIUnixSocketClient { + + private final SocketChannel socketChannel; + private String host; + private int port; + + private InputStream socketIn; + private OutputStream socketOut; + private byte[] buffer = new byte[2048]; + + private Mode mode; + + /** + * Create an SCGI lient. + * + * @param unixDomainSocketAddress The Unix Domain Socket Address + * @param mode mode of the SCGI communication. + */ + public SCGIUnixSocketClient(UnixDomainSocketAddress unixDomainSocketAddress, Mode mode) throws IOException { + this.socketChannel = SocketChannel.open(unixDomainSocketAddress); + this.mode = mode; + } + + private void setup() throws IOException { + this.socketIn = Channels.newInputStream(socketChannel); + this.socketOut = Channels.newOutputStream(socketChannel); + } + + /** + * This method is only available for the + * {@link Mode} SCGI_MESSAGE_BASED. It will fetch + * the data into memory and return a new SCGI message instance. + * + * @param request + * @return + * @throws IOException + */ + public SCGIMessage sendAndReceiveAsScgiMessage(SCGIMessage request) throws IOException { + if (mode == Mode.SCGI_MESSAGE_BASED) { + setup(); + request.serialize(socketOut); + return new SCGIMessage(socketIn); + } else { + throw new IllegalStateException(); + } + } + + /** + * This method sends an SCGI request and returns the SCGI message as a byte + * array. This method is only available for the + * {@link Mode} STANDARD mode. + * + * @param request + * @return + * @throws UnknownHostException + * @throws IOException + */ + public byte[] sendAndReceiveAsByteArray(SCGIMessage request) throws UnknownHostException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + sendRequest(request, out); + return out.toByteArray(); + } + + /** + * This method sends an SCGI request and writes the answer into the + * {@link OutputStream} provided as a parameter. This method is only + * available for the {@link Mode} STANDARD mode. + * + * @param request + * @return + * @throws UnknownHostException + * @throws IOException + */ + public void sendRequest(SCGIMessage request, OutputStream response) throws UnknownHostException, IOException { + if (mode == Mode.STANDARD) { + setup(); + + try { + request.serialize(socketOut); + + int read; + while ((read = socketIn.read(buffer)) > 0) { + response.write(buffer, 0, read); + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + socketIn.close(); + socketOut.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } else { + throw new IllegalStateException(); + } + } + +} diff --git a/src/main/java/io/github/szabogabriel/jscgi/util/SCGIUtil.java b/src/main/java/io/github/szabogabriel/jscgi/util/SCGIUtil.java new file mode 100644 index 0000000000..08c8e20593 --- /dev/null +++ b/src/main/java/io/github/szabogabriel/jscgi/util/SCGIUtil.java @@ -0,0 +1,99 @@ +package io.github.szabogabriel.jscgi.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class SCGIUtil { + + public static Map parseHeaders(byte[] headers) { + Map ret = new HashMap<>(); + + boolean parsingKey = true; + String key = ""; + StringBuilder value = new StringBuilder(); + for (byte header : headers) { + if (parsingKey) { + if (header != 0) { + key += (char) header; + } else { + parsingKey = false; + } + } else { + if (header != 0) { + value.append((char) header); + } else { + parsingKey = true; + if (ret.containsKey(key)) { + value.insert(0, ret.get(key) + ", "); + } + ret.put(key, value.toString()); + key = ""; + value = new StringBuilder(); + } + } + } + + return ret; + } + + public static Map parseHeadersOld(byte[] headers) { + Map ret = new HashMap<>(); + + boolean parsingKey = true; + int from = 0, keyDivider = 0, valDivider = 0, keyLength = 0, valLength = 0; + byte[] key = {}, value = {}; + for (int i = 0; i < headers.length; i++) { + if (parsingKey) { + if (headers[i] != 0) { + keyDivider = i; + } else { + parsingKey = false; + keyLength = keyDivider - from + 1; + key = new byte[keyLength]; + System.arraycopy(headers, from, key, 0, keyLength); + from = i + 1; + } + } else { + if (headers[i] != 0) { + valDivider = i; + } else { + parsingKey = true; + if (valDivider >= from) { + valLength = valDivider - from + 1; + value = new byte[valLength]; + System.arraycopy(headers, from, value, 0, valLength); + ret.put(new String(key), new String(value)); + } else { + ret.put(new String(key), ""); + } + from = i + 1; + } + } + } + + return ret; + } + + public static byte[] createHeaders(Map headers) { + ByteArrayOutputStream ret = new ByteArrayOutputStream(); + + for (String key : headers.keySet()) { + try { + ret.write(key.getBytes()); + ret.write(0); + String value = headers.get(key); + if (value != null) { + ret.write(value.getBytes()); + } + ret.write(0); + } catch (IOException e) { + e.printStackTrace(); + } + } + + return ret.toByteArray(); + } + +} diff --git a/src/main/java/raccoonfink/deluge/DelugeServer.java b/src/main/java/raccoonfink/deluge/DelugeServer.java index 7b9563c270..f97497a658 100644 --- a/src/main/java/raccoonfink/deluge/DelugeServer.java +++ b/src/main/java/raccoonfink/deluge/DelugeServer.java @@ -1,6 +1,5 @@ package raccoonfink.deluge; -import com.ghostchu.peerbanhelper.Main; import com.ghostchu.peerbanhelper.util.HTTPUtil; import com.github.mizosoft.methanol.Methanol; import com.github.mizosoft.methanol.MutableRequest; @@ -42,13 +41,12 @@ public DelugeServer(final String url, final String password, boolean verifySSL, .newBuilder() .version(httpVersion) .followRedirects(HttpClient.Redirect.ALWAYS) - .userAgent(Main.getUserAgent()) .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) .headersTimeout(Duration.of(10, ChronoUnit.SECONDS)) .readTimeout(Duration.of(15, ChronoUnit.SECONDS)) .requestTimeout(Duration.of(15, ChronoUnit.SECONDS)) .defaultHeader("Accept", "application/json") - .defaultHeader("Accept-Encoding", "compress;q=0.5, gzip;q=1.0") + .defaultHeader("Accept-Encoding", "gzip,deflate") .defaultHeader("Content-Type", "application/json") .authenticator(new Authenticator() { @Override diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index e972deee6f..1a5d5c07e4 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,48 +1,9 @@ -config-version: 9 -# 客户端设置 -client: - # 名字,可以自己起,会在日志中显示,只能由字母数字横线组成,数字不能打头 - qbittorrent-001: - # 客户端类型 - # 支持的客户端列表: - # qBittorrent - # Transmission - # BiglyBT - # 其它也许以后会加 - type: qBittorrent - # 客户端地址 - endpoint: "http://ip:8085" - # 登录信息(暂不支持 Basic Auth) - # 用户名 - username: "username" - # 密码 - password: "password" - # Basic Auth - 不知道这是什么的话,请保持默认 - basic-auth: - user: "" - pass: "" - # 验证 SSL 证书有效性 - verify-ssl: true - # Http 协议版本 - http-version: "HTTP_1_1" - # 增量封禁(有助于缓解保存封禁列表时的下载器压力,但可能不稳定,可能在部分下载器上会导致无法封禁Peers) - increment-ban: false - transmission-002: - type: Transmission - endpoint: "http://127.0.0.1:9091" - username: "admin" - password: "admin" - verify-ssl: true - http-version: "HTTP_1_1" - rpc-url: "/transmission/rpc" - biglybt-003: - type: BiglyBT - # 填写 PeerBanHelper 提供的 BiglyBT 适配插件的 IP+端口 - endpoint: "http://127.0.0.1:55667" - # 填写 PeerBanHelper 提供的 BiglyBT 适配插件的 Token - token: "" - # 是否使用增量封禁,在 BiglyBT 上这是推荐的选项 - increment-ban: true +config-version: 12 +# 设置程序语言 +# default 跟随操作系统 (Follow the operating system) +# en_us English (US) +# zh_cn Chinese Simplified (简体中文) +language: default # Http 服务器设置 server: # WebUI 监听端口 @@ -53,6 +14,8 @@ server: prefix: "http://127.0.0.1:9898" # 要访问 WebUI 端点,则必须输入此 Token 以进行身份验证 token: "" + # 允许 CORS 跨站,仅在使用外部 PBH WebUI 时才应该启用 + allow-cors: false # 日志记录器配置 logger: # 是否隐藏 [完成] 已检查 XX 的 X 个活跃 Torrent 和 X 个对等体 的日志消息? @@ -71,6 +34,7 @@ persist: btn: # 启用 BTN 模块 # 启用后,才可以使用由 BTN 提供的云规则功能 + # 为了隐私起见,此功能需要您手动启用 enabled: false # 启用数据提交(匿名) # BTN 网络基于所有启用此功能的用户提交的数据,对 Peers 进行可信度验证 @@ -84,7 +48,8 @@ btn: app-id: "example-app-id" app-secret: "example-app-secret" # 填写实例 URL,您需要自行寻找一个 BTN 实例服务器 - config-url: "http://127.0.0.1:9988/ping/config" + # 默认使用 PBH-BTN 社区 BTN 服务器,请前往 https://btn-prod.ghostchu-services.top 注册并获取一个账号 + config-url: "https://btn-prod.ghostchu-services.top/ping/config" # 封禁列表处理 # PBH 能够除了调用 BT 客户端的封禁 API 外,还能够进行如下操作,以便适配更多其它客户端 banlist-invoker: @@ -140,3 +105,16 @@ ip-database: firewall-integration: # 高级 Windows 防火墙(基于动态关键字),需要 Windows 10 或更高版本 windows-adv-firewall: true +# 代理服务器设定 +proxy: + # 代理服务器设置 + # 注意:不支持需要密码验证的代理服务器 + # 0 = 不使用代理 + # 1 = 使用系统代理 + # 2 = 使用 HTTP(s) 代理 + # 3 = 使用 socks5 代理 + setting: 0 + # 代理服务器地址 + host: "127.0.0.1" + # 代理服务器端口号 + port: 7890 diff --git a/src/main/resources/javafx/css/root.css b/src/main/resources/javafx/css/root.css new file mode 100644 index 0000000000..be2558dd55 --- /dev/null +++ b/src/main/resources/javafx/css/root.css @@ -0,0 +1,40 @@ +.root { +} + +.log-window-list-cell { + -fx-text-fill: black; + -fx-border-width: 0 0 1 0; + -fx-border-color: #dddddd; +} + +.log-window-list-cell:empty { + -fx-border-width: 0; +} + +.log-window-list-cell:fatal { + -fx-background-color: #F7A699; +} + +.log-window-list-cell:error { + -fx-background-color: #FFCCBB; +} + +.log-window-list-cell:warn { + -fx-background-color: #FFEECC; +} + +.log-window-list-cell:info { + -fx-background-color: #FFFFFF; +} + +.log-window-list-cell:debug { + -fx-background-color: #EEE9E0; +} + +.log-window-list-cell:trace { + -fx-background-color: #EEE9E0; +} + +.log-window-list-cell:selected { + -fx-background-color: #C4C4C4; +} \ No newline at end of file diff --git a/src/main/resources/javafx/main_window.fxml b/src/main/resources/javafx/main_window.fxml index 4b29e9d9e5..e87b775f68 100644 --- a/src/main/resources/javafx/main_window.fxml +++ b/src/main/resources/javafx/main_window.fxml @@ -35,7 +35,8 @@ + fx:controller="com.ghostchu.peerbanhelper.gui.impl.javafx.mainwindow.JFXWindowController" + style="-fx-font-family: 'Henti SC',STHenti,'Microsoft YaHei UI','Microsoft JhengHei UI','Helvetica Neue',Helvetica,Arial,SimSun-ExtB, SimSun"> @@ -66,13 +67,12 @@ -