├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github └── workflows │ └── auto-update.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_en.md ├── config ├── application-example.yaml └── application.yaml ├── pom.xml ├── rule └── private.txt └── src └── main ├── java └── org │ └── fordes │ └── adfs │ ├── Application.java │ ├── config │ ├── Config.java │ ├── InputProperties.java │ └── OutputProperties.java │ ├── constant │ ├── Constants.java │ └── RegConstants.java │ ├── enums │ ├── HandleType.java │ └── RuleSet.java │ ├── handler │ ├── Parser.java │ ├── dns │ │ └── DnsChecker.java │ ├── fetch │ │ ├── Fetcher.java │ │ ├── HttpFetcher.java │ │ └── LocalFetcher.java │ └── rule │ │ ├── ClashHandler.java │ │ ├── DnsmasqHandler.java │ │ ├── EasylistHandler.java │ │ ├── Handler.java │ │ ├── HostsHandler.java │ │ └── SmartdnsHandler.java │ ├── model │ └── Rule.java │ ├── task │ └── Endpoint.java │ └── util │ ├── BloomFilter.java │ └── Util.java └── resources ├── application-dev.yml ├── application.yml ├── banner.txt └── logback-spring.xml /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster 2 | ARG VARIANT=21-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/java:${VARIANT} 4 | 5 | # [Option] Install Maven 6 | ARG INSTALL_MAVEN="true" 7 | ARG MAVEN_VERSION="3.9.0" 8 | # [Option] Install Gradle 9 | ARG INSTALL_GRADLE="false" 10 | ARG GRADLE_VERSION="" 11 | RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ 12 | && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi 13 | 14 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 15 | ARG NODE_VERSION="none" 16 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 17 | 18 | # [Optional] Uncomment this section to install additional OS packages. 19 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 20 | # && apt-get -y install --no-install-recommends 21 | 22 | # [Optional] Uncomment this line to install global node packages. 23 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 24 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodeSpace", 3 | "dockerFile": "Dockerfile", 4 | "extensions": [ 5 | "vscjava.vscode-java-pack", 6 | "vscjava.vscode-java-debug", 7 | "vscjava.vscode-java-test", 8 | "vscjava.vscode-spring-boot-dashboard", 9 | "vmware.vscode-spring-boot", 10 | "vscjava.vscode-maven" 11 | ] 12 | } -------------------------------------------------------------------------------- /.github/workflows/auto-update.yml: -------------------------------------------------------------------------------- 1 | name: Update Filters 2 | on: 3 | push: 4 | paths-ignore: 5 | - 'README.md' 6 | - 'README_en.md' 7 | - '.github/**' 8 | - '.devcontainer/**' 9 | - 'LICENSE' 10 | schedule: 11 | - cron: 0 */8 * * * 12 | workflow_dispatch: 13 | 14 | env: 15 | TZ: Asia/Shanghai 16 | 17 | jobs: 18 | Update_Filters: 19 | runs-on: ubuntu-latest 20 | permissions: 21 | contents: write 22 | actions: write 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | ref: ${{ github.head_ref }} 29 | 30 | - name: Set up JDK 21 31 | uses: actions/setup-java@v4 32 | with: 33 | java-version: '21' 34 | distribution: 'temurin' 35 | cache: maven 36 | 37 | - name: Run Jar 38 | run: mvn spring-boot:run 39 | 40 | - name: Clean up 41 | run: | 42 | find . -maxdepth 1 ! -name 'rule' ! -name '.' ! -name '.git' -exec rm -rf {} + 43 | mv rule/* . 44 | rm -rf rule 45 | 46 | - name: Commit Changes 47 | id: commit 48 | uses: stefanzweifel/git-auto-commit-action@v5 49 | with: 50 | commit_message: 🚀 CI Updated 51 | branch: release 52 | skip_dirty_check: true 53 | push_options: '--force' 54 | create_branch: true 55 | skip_fetch: true 56 | skip_checkout: true 57 | commit_options: '--allow-empty' 58 | 59 | - name: Delete Workflow Runs 60 | uses: Mattraks/delete-workflow-runs@v2 61 | with: 62 | token: ${{secrets.GITHUB_TOKEN}} 63 | repository: ${{ github.repository }} 64 | retain_days: 1 65 | keep_minimum_runs: 1 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | .idea 26 | target 27 | src/test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 以谶 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AbBlock List 2 | 3 | ![Last Update](https://img.shields.io/github/last-commit/xndeye/adblock_list?style=flat-square&branch=release) 4 | ![Build Status](https://img.shields.io/github/actions/workflow/status/xndeye/adblock_list/auto-update.yml?branch=main&style=flat-square) 5 | ![Stars](https://img.shields.io/github/stars/xndeye/adblock_list?style=flat-square) 6 | ![Forks](https://img.shields.io/github/forks/xndeye/adblock_list?style=flat-square) 7 | 8 | 9 | 💪 强大而克制的广告过滤规则,可拦截 99%[^1] 的 Web 广告! 10 | 11 | > [!TIP] 12 | > 本仓库通过 [ad-filters-subscriber](https://github.com/fordes123/ad-filters-subscriber/) 构建,定时合并多个优质上游规则,并去除重复和失效项。 13 | > 构建和转换错误请反馈至 [此处](https://github.com/fordes123/ad-filters-subscriber/issues),误杀和规则推荐请提交至本仓库 [issues](https://github.com/xndeye/adblock_list/issues) 14 | 15 | | 文件 | 说明 | github | ghproxy | jsdelivr | 16 | |-----------------|:----------------------------|:--------------------:|:------------------------:|:-------------------------:| 17 | | `easylist.txt` | 完整主规则 | [link][easylist-raw] | [link][easylist-ghproxy] | [link][easylist-jsdelivr] | 18 | | `modify.txt` | 不含 DNS 过滤规则的 `easylist.txt` | [link][modify-raw] | [link][modify-ghproxy] | [link][modify-jsdelivr] | 19 | | `dns.txt` | 仅含 DNS 过滤规则的 `easylist.txt` | [link][dns-raw] | [link][dns-ghproxy] | [link][dns-jsdelivr] | 20 | | `dnsmasq.conf` | dnsmasq 及其衍生版本 | [link][dnsmasq-raw] | [link][dnsmasq-ghproxy] | [link][dnsmasq-jsdelivr] | 21 | | `clash.yaml` | clash 及其衍生版本 | [link][clash-raw] | [link][clash-ghproxy] | [link][clash-jsdelivr] | 22 | | `smartdns.conf` | smartdns | [link][smartdns-raw] | [link][smartdns-ghproxy] | [link][smartdns-jsdelivr] | 23 | | `hosts` | 几乎所有操作系统原生支持 | [link][hosts-raw] | [link][hosts-ghproxy] | [link][hosts-jsdelivr] | 24 | | `private.txt` | 本仓库维护的私有规则,以 easylist 形式提供 | [link][private-raw] | [link][private-ghproxy] | [link][private-jsdelivr] | 25 | 26 | [easylist-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/easylist.txt 27 | 28 | [easylist-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/easylist.txt 29 | 30 | [easylist-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/easylist.txt 31 | 32 | [modify-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/modify.txt 33 | 34 | [modify-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/modify.txt 35 | 36 | [modify-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/modify.txt 37 | 38 | [dns-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dns.txt 39 | 40 | [dns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dns.txt 41 | 42 | [dns-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/dns.txt 43 | 44 | [dnsmasq-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dnsmasq.conf 45 | 46 | [dnsmasq-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dnsmasq.conf 47 | 48 | [dnsmasq-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/dnsmasq.conf 49 | 50 | [clash-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/clash.yaml 51 | 52 | [clash-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/clash.yaml 53 | 54 | [clash-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/clash.yaml 55 | 56 | [smartdns-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/smartdns.conf 57 | 58 | [smartdns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/smartdns.conf 59 | 60 | [smartdns-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/smartdns.conf 61 | 62 | [hosts-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/hosts 63 | 64 | [hosts-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/hosts 65 | 66 | [hosts-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/hosts 67 | 68 | [private-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/private.txt 69 | 70 | [private-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/private.txt 71 | 72 | [private-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/private.txt 73 | 74 |
75 | 点击查看上游规则 76 | 95 |
96 | 97 | [^1]: 数据在 `Chrome 130.0` 上使用 `AdGuard 浏览器扩展` 订阅本仓库 `easylist.txt` 规则, 98 | 通过 [d3ward/toolz](https://d3ward.github.io/toolz/adblock.html) 测得,[结果](https://github.com/user-attachments/assets/76ccfcac-9ffd-4bed-89d7-08cdfe6cc33d)仅供参考 99 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # AdBlock List 2 | 3 | ![Last Update](https://img.shields.io/github/last-commit/xndeye/adblock_list?style=flat-square&branch=release) 4 | ![Build Status](https://img.shields.io/github/actions/workflow/status/xndeye/adblock_list/auto-update.yml?branch=main&style=flat-square) 5 | ![Stars](https://img.shields.io/github/stars/xndeye/adblock_list?style=flat-square) 6 | ![Forks](https://img.shields.io/github/forks/xndeye/adblock_list?style=flat-square) 7 | 8 | 💪 Powerful yet restrained ad filtering rules that can block 99%[^1] of Web ads! 9 | 10 | > [!TIP] 11 | > This repository is built using [ad-filters-subscriber](https://github.com/fordes123/ad-filters-subscriber/), which regularly merges multiple high-quality upstream rules and removes duplicates and invalid items. 12 | > Please report build and conversion errors [here](https://github.com/fordes123/ad-filters-subscriber/issues), and submit false positives and rule recommendations to this repository's [issues](https://github.com/xndeye/adblock_list/issues) 13 | 14 | | File | Description | github | ghproxy | jsdelivr | 15 | |------|:------------|:------:|:-------:|:--------:| 16 | | `easylist.txt` | Complete main rules | [link][easylist-raw] | [link][easylist-ghproxy] | [link][easylist-jsdelivr] | 17 | | `modify.txt` | `easylist.txt` without DNS filtering rules | [link][modify-raw] | [link][modify-ghproxy] | [link][modify-jsdelivr] | 18 | | `dns.txt` | DNS filtering rules only from `easylist.txt` | [link][dns-raw] | [link][dns-ghproxy] | [link][dns-jsdelivr] | 19 | | `dnsmasq.conf` | For dnsmasq and its derivatives | [link][dnsmasq-raw] | [link][dnsmasq-ghproxy] | [link][dnsmasq-jsdelivr] | 20 | | `clash.yaml` | For clash and its derivatives | [link][clash-raw] | [link][clash-ghproxy] | [link][clash-jsdelivr] | 21 | | `smartdns.conf` | For smartdns | [link][smartdns-raw] | [link][smartdns-ghproxy] | [link][smartdns-jsdelivr] | 22 | | `hosts` | Natively supported by almost all operating systems | [link][hosts-raw] | [link][hosts-ghproxy] | [link][hosts-jsdelivr] | 23 | | `private.txt` | Private rules maintained by this repository, provided in easylist format | [link][private-raw] | [link][private-ghproxy] | [link][private-jsdelivr] | 24 | 25 | [easylist-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/easylist.txt 26 | 27 | [easylist-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/easylist.txt 28 | 29 | [easylist-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/easylist.txt 30 | 31 | [modify-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/modify.txt 32 | 33 | [modify-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/modify.txt 34 | 35 | [modify-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/modify.txt 36 | 37 | [dns-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dns.txt 38 | 39 | [dns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dns.txt 40 | 41 | [dns-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/dns.txt 42 | 43 | [dnsmasq-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dnsmasq.conf 44 | 45 | [dnsmasq-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/dnsmasq.conf 46 | 47 | [dnsmasq-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/dnsmasq.conf 48 | 49 | [clash-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/clash.yaml 50 | 51 | [clash-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/clash.yaml 52 | 53 | [clash-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/clash.yaml 54 | 55 | [smartdns-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/smartdns.conf 56 | 57 | [smartdns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/smartdns.conf 58 | 59 | [smartdns-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/smartdns.conf 60 | 61 | [hosts-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/hosts 62 | 63 | [hosts-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/hosts 64 | 65 | [hosts-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/hosts 66 | 67 | [private-raw]: https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/private.txt 68 | 69 | [private-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/xndeye/adblock_list/refs/heads/release/private.txt 70 | 71 | [private-jsdelivr]: https://gcore.jsdelivr.net/gh/xndeye/adblock_list@refs/heads/release/private.txt 72 | 73 |
74 | Click to view upstream rules 75 | 94 |
95 | 96 | [^1]: Data measured using [d3ward/toolz](https://d3ward.github.io/toolz/adblock.html) with `AdGuard Browser Extension` subscribing to this repository's `easylist.txt` rules on `Chrome 130.0`. [Results](https://github.com/user-attachments/assets/76ccfcac-9ffd-4bed-89d7-08cdfe6cc33d) are for reference only. 97 | -------------------------------------------------------------------------------- /config/application-example.yaml: -------------------------------------------------------------------------------- 1 | ########################################## 2 | ## !!! 示例配置,修改无效 !!! 3 | ## !!! example config, modify invalid !!! 4 | ########################################## 5 | application: 6 | 7 | # 程序配置(可选) 8 | # !以下为默认值,如非了解不建议修改 9 | config: 10 | expected_quantity: 2000000 #预期规则数量 11 | fault_tolerance: 0.001 #容错率 12 | warn_limit: 6 #警告阈值, 原始规则长度小于该值时会输出警告日志 13 | 14 | # 域名检测,启用时将进行解析以验证域名有效性 15 | # !开启此功能可能导致处理时间大幅延长 16 | # !仅支持特定规则,具体原因请查看项目主页说明 17 | domain-detect: 18 | enable: false # 是否启用 19 | timeout: 2000 # 查询超时(毫秒) 20 | concurrency: 10 # 并发查询数 21 | provider: # 使用的 DNS 服务器,仅支持 IP,多个之间具备优先级; 为空则使用默认DNS 22 | - 223.5.5.5 23 | 24 | # 排除指定内容 25 | # 在解析结束后,规则内容命中此列表将直接认定为无效 26 | exclude: 27 | - "www.example.org" 28 | 29 | 30 | # 规则源配置 31 | # remote为远程规则,local为本地规则,支持多个规则源 32 | rule: 33 | #远程订阅规则 (!使用前请删除下方示例配置, 注意缩进) 34 | remote: 35 | - name: 'Subscription 1' #可选参数: 规则名称,如无将使用 path 作为名称 36 | path: 'https://example.org/rule.txt' #必要参数: 规则url,仅支持 http/https,不限定响应内容格式 37 | type: easylist #可选参数: 规则类型:easylist (默认)、dnsmasq、clash、smartdns、hosts 38 | 39 | - name: 'Subscription 2' 40 | path: 'http://example.org/rule.txt' 41 | 42 | #本地规则文件 43 | local: 44 | - name: 'local rule' 45 | path: 'local-rule.txt' #支持绝对/相对路径 46 | 47 | 48 | #输出配置 49 | output: 50 | #文件头配置,将自动作为注释添加至每个规则文件开始 51 | #可使用占位符 ${name}、${type}、${desc} 以及 ${date} (当前日期) 52 | file_header: | 53 | ADFS AdBlock ${type} 54 | Last Modified: ${date} 55 | Homepage: https://github.com/fordes123/ad-filters-subscriber/ 56 | 57 | #输出规则文件列表 (!注意缩进,且每个类型只能输出一个文件) 58 | files: 59 | - name: easylist.txt #必要参数: 文件名 60 | type: easylist #必要参数: 文件类型: easylist、dnsmasq、clash、smartdns、hosts 61 | file_header: #可选参数: 文件头配置,将自动作为注释添加至每个规则文件开始 (此处优先于 output.file_header) 62 | desc: 'ADFS EasyList' #可选参数: 文件描述,可在file_header中通过 ${} 中使用 63 | filter: 64 | - basic #基本规则,不包含任何控制、匹配符号, 可以转换为 hosts 65 | - wildcard #通配规则,仅使用通配符 66 | - unknown #其他规则,如使用了正则、高级修饰符号等,这表示目前无法支持 67 | 68 | - name: dns.txt 69 | type: easylist 70 | filter: 71 | - basic 72 | - wildcard 73 | 74 | - name: dnsmasq.txt 75 | type: dnsmasq 76 | 77 | - name: clash.yaml 78 | type: clash 79 | 80 | - name: smartdns.txt 81 | type: smartdns 82 | 83 | - name: hosts.txt 84 | type: hosts 85 | -------------------------------------------------------------------------------- /config/application.yaml: -------------------------------------------------------------------------------- 1 | # 参考 `application-example.yaml` 并在处添加订阅规则,请注意缩进 2 | # Refer to `application-example.yaml` and add subscription rules there,please note the indentation 3 | 4 | application: 5 | config: 6 | domain-detect: 7 | enable: true 8 | timeout: 3000 9 | concurrency: 128 10 | provider: 11 | - 1.1.1.1 12 | - 8.8.8.8 13 | 14 | exclude: 15 | # 屏蔽此域会导致Edge浏览器翻译不可用 16 | - "edge.microsoft.com" 17 | # 屏蔽此域会导致小米天气网络异常 18 | - "metok.sys.miui.com" 19 | # 屏蔽此域会导致10086 App登陆异常 20 | - "client.app.coc.10086.cn" 21 | # 屏蔽此域会导致MSN页面无法正常加载 22 | - "assets.msn.com" 23 | 24 | rule: 25 | remote: 26 | - name: AdGuard 基础过滤器 27 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_Base/filter.txt 28 | type: easylist 29 | 30 | - name: AdGuard 移动广告过滤器 31 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_11_Mobile/filter.txt 32 | type: easylist 33 | 34 | - name: AdGuard 防跟踪保护过滤器 35 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_3_Spyware/filter.txt 36 | type: easylist 37 | 38 | - name: AdGuard URL跟踪过滤器 39 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_17_TrackParam/filter.txt 40 | type: easylist 41 | 42 | - name: AdGuard 恼人广告过滤器 43 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_14_Annoyances/filter.txt 44 | type: easylist 45 | 46 | - name: AdGuard 解除搜索广告和自我推销过滤器 47 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_10_Useful/filter.txt 48 | type: easylist 49 | 50 | - name: AdGuard 中文过滤器 51 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_224_Chinese/filter.txt 52 | type: easylist 53 | 54 | - name: AWAvenue-Adblock-Rule 55 | path: https://raw.githubusercontent.com/TG-Twilight/AWAvenue-Ads-Rule/main/AWAvenue-Ads-Rule.txt 56 | type: easylist 57 | 58 | - name: NoAppDownload 59 | path: https://raw.githubusercontent.com/Noyllopa/NoAppDownload/master/NoAppDownload.txt 60 | type: easylist 61 | 62 | - name: xndeye web-ad-rule 63 | path: https://raw.githubusercontent.com/xndeye/web-ad-rule/master/easylist.txt 64 | type: easylist 65 | 66 | - name: xinggsf movie 67 | path: https://raw.githubusercontent.com/xinggsf/Adblock-Plus-Rule/master/mv.txt 68 | 69 | - name: damengzhu jiekouAD 70 | path: https://raw.githubusercontent.com/damengzhu/banad/main/jiekouAD.txt 71 | 72 | - name: cjxlist annoyance 73 | path: https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjx-annoyance.txt 74 | 75 | - name: ABP filters 76 | path: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt 77 | 78 | - name: ABP 反广告过滤 79 | path: https://easylist-downloads.adblockplus.org/abp-filters-anti-cv.txt 80 | 81 | - name: uBlockOrigin privacy 82 | path: https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/privacy.txt 83 | 84 | - name: Telemetry Lists - Aggregated 85 | path: https://raw.githubusercontent.com/SystemJargon/filters/refs/heads/main/telemetry.txt 86 | 87 | 88 | local: 89 | - name: 'xndeye private adblock list' 90 | path: 'rule/private.txt' 91 | type: easylist 92 | 93 | output: 94 | file_header: | 95 | Title: xndeye adblock list of ${type} 96 | Last Modified: ${date} 97 | Homepage: https://github.com/xndeye/adblock_list 98 | 99 | files: 100 | - name: easylist.txt 101 | type: easylist 102 | filter: 103 | - basic 104 | - wildcard 105 | - unknown 106 | 107 | - name: dns.txt 108 | type: easylist 109 | file_header: | 110 | Title: xndeye adblock list of dns 111 | Last Modified: ${date} 112 | Homepage: https://github.com/xndeye/adblock_list 113 | filter: 114 | - basic 115 | - wildcard 116 | 117 | - name: modify.txt 118 | type: easylist 119 | filter: 120 | - unknown 121 | 122 | - name: dnsmasq.conf 123 | type: dnsmasq 124 | 125 | - name: clash.yaml 126 | type: clash 127 | 128 | - name: smartdns.conf 129 | type: smartdns 130 | 131 | - name: hosts 132 | type: hosts 133 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | ad-filters-subscriber 5 | org.fordes 6 | AD Filters Subscriber 7 | 8 | 4.0.0 9 | ad-filters-subscriber 10 | 1.0.0 11 | 12 | 13 | 21 14 | UTF-8 15 | UTF-8 16 | 3.4.0 17 | 6.2.0 18 | 3.1.1 19 | 3.12.1 20 | 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter 27 | ${spring-boot.version} 28 | 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-reactor-netty 33 | ${spring-boot.version} 34 | 35 | 36 | 37 | org.springframework 38 | spring-webflux 39 | ${webflux.version} 40 | 41 | 42 | 43 | org.projectlombok 44 | lombok 45 | true 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | test 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-dependencies 61 | ${spring-boot.version} 62 | pom 63 | import 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | ${maven-compiler-plugin.version} 74 | 75 | ${java.version} 76 | ${java.version} 77 | UTF-8 78 | 79 | -parameters 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | ${spring-boot.version} 87 | 88 | 89 | repackage 90 | 91 | repackage 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /rule/private.txt: -------------------------------------------------------------------------------- 1 | ! Title: xndeye adblock list of private 2 | ! Last Modified: 2025-02-18 00:00:00 3 | ! Homepage: https://github.com/xndeye/adblock_list/ 4 | ! Description: 对本仓库上游规则的补充与修正, 不定期更新. 5 | 6 | #----------------黑名单----------------- 7 | ||log.uu.163.com^ 8 | ||ksmobile.com^ 9 | #斗鱼cdn日志 10 | ||p2plog.douyucdn.cn^ 11 | ||p2perrorlog.douyucdn.cn^ 12 | #微软遥测/广告 13 | ||events.data.microsoft.com^$important 14 | ||solitaireevents.microsoftcasualgames.com^$important 15 | ||adsdkprod.azureedge.net^$important 16 | #小米跟踪器/广告 17 | ||tracker.xiaomixiaoai.com^$important 18 | ||exp.sug.browser.miui.com^ 19 | ||sentry.d.xiaomi.net^ 20 | ||httpdns.browser.miui.com^ 21 | ||track.ad.xiaomi.com^$important 22 | #萤石日志 23 | ||log.ys7.com^ 24 | #-----------------待定------------------ 25 | #未知来源 广告/日志 26 | ||toblog.ctobsnssdk.com^ 27 | ||xc.maoxiongtv.com^$important 28 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/Application.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs; 2 | 3 | import lombok.Getter; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.ConfigurableApplicationContext; 7 | 8 | @SpringBootApplication 9 | public class Application { 10 | 11 | @Getter 12 | private static ConfigurableApplicationContext context; 13 | 14 | public static void main(String[] args) { 15 | context = SpringApplication.run(Application.class, args); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/config/Config.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.config; 2 | 3 | import lombok.Data; 4 | import org.fordes.adfs.model.Rule; 5 | import org.fordes.adfs.util.BloomFilter; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import java.util.Optional; 11 | import java.util.Set; 12 | 13 | /** 14 | * @author fordes on 2024/4/9 15 | */ 16 | @Data 17 | @Configuration 18 | @ConfigurationProperties(prefix = "application.config") 19 | public class Config { 20 | 21 | private Double faultTolerance = 0.0001; 22 | private Integer expectedQuantity = 2000000; 23 | private Integer warnLimit = 6; 24 | private Set exclude; 25 | private DomainDetect domainDetect; 26 | 27 | public record DomainDetect(Boolean enable, Integer timeout) { 28 | 29 | } 30 | 31 | @Bean 32 | public BloomFilter bloomFilter() { 33 | double falsePositiveProbability = Optional.ofNullable(faultTolerance).orElse(0.0001); 34 | int expectedNumberOfElements = Optional.ofNullable(expectedQuantity).orElse(2000000); 35 | return new BloomFilter<>(falsePositiveProbability, expectedNumberOfElements); 36 | } 37 | 38 | // @Bean("producer") 39 | // public ExecutorService producerExecutor() { 40 | // return Executors.newVirtualThreadPerTaskExecutor(); 41 | // } 42 | // 43 | // @Bean("consumer") 44 | // public ExecutorService consumerExecutor() { 45 | // return Executors.newVirtualThreadPerTaskExecutor(); 46 | // } 47 | // 48 | // @Bean("singleExecutor") 49 | // public ThreadPoolTaskExecutor taskExecutor() { 50 | // ThreadPoolTaskExecutor sentinel = new ThreadPoolTaskExecutorBuilder() 51 | // .awaitTermination(false) 52 | // .corePoolSize(1) 53 | // .queueCapacity(1) 54 | // .maxPoolSize(1) 55 | // .threadNamePrefix("sentinel-") 56 | // .build(); 57 | // sentinel.initialize(); 58 | // return sentinel; 59 | // } 60 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/config/InputProperties.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.config; 2 | 3 | import lombok.Data; 4 | import org.fordes.adfs.enums.HandleType; 5 | import org.fordes.adfs.enums.RuleSet; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import java.util.Set; 13 | import java.util.stream.Collectors; 14 | import java.util.stream.Stream; 15 | 16 | @Data 17 | @Component 18 | @ConfigurationProperties(prefix = "application.rule") 19 | public class InputProperties implements InitializingBean { 20 | 21 | private Set remote = Set.of(); 22 | private Set local = Set.of(); 23 | 24 | public Stream> stream() { 25 | return Stream.concat(local.stream().map(e -> Map.entry(HandleType.LOCAL, e)), 26 | remote.stream().map(e -> Map.entry(HandleType.REMOTE, e))); 27 | } 28 | 29 | public boolean isEmpty() { 30 | return (remote == null || remote.isEmpty()) && (local == null || local.isEmpty()); 31 | } 32 | 33 | public void setRemote(Set remote) { 34 | this.remote = Optional.ofNullable(remote) 35 | .map(e -> e.stream().filter(p -> !p.path.isEmpty()).collect(Collectors.toSet())).orElse(Set.of()); 36 | } 37 | 38 | public void setLocal(Set local) { 39 | this.local = Optional.ofNullable(local) 40 | .map(e -> e.stream().filter(p -> !p.path.isEmpty()).collect(Collectors.toSet())).orElse(Set.of()); 41 | } 42 | 43 | @Override 44 | public void afterPropertiesSet() { 45 | if ((remote == null || remote.isEmpty()) && (local == null || local.isEmpty())) { 46 | throw new IllegalArgumentException("application.rule is required"); 47 | } 48 | } 49 | 50 | public record Prop(String name, RuleSet type, String path) { 51 | 52 | public Prop(String name, RuleSet type, String path) { 53 | this.path = Optional.ofNullable(path).filter(e -> !e.isBlank()).orElseThrow(() -> new IllegalArgumentException("application.rule.path is required")).trim(); 54 | this.type = Optional.ofNullable(type).orElse(RuleSet.EASYLIST); 55 | this.name = Optional.ofNullable(name).filter(e -> !e.isBlank()).orElse(path).trim(); 56 | } 57 | 58 | @Override 59 | public boolean equals(Object obj) { 60 | if (obj instanceof Prop prop) { 61 | return prop.path.equals(this.path); 62 | } 63 | return false; 64 | } 65 | 66 | public int hashCode() { 67 | return path.hashCode(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/config/OutputProperties.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.config; 2 | 3 | import lombok.Data; 4 | import org.fordes.adfs.constant.Constants; 5 | import org.fordes.adfs.enums.RuleSet; 6 | import org.fordes.adfs.handler.rule.Handler; 7 | import org.fordes.adfs.model.Rule; 8 | import org.fordes.adfs.util.Util; 9 | import org.springframework.beans.factory.InitializingBean; 10 | import org.springframework.boot.context.properties.ConfigurationProperties; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.util.StringUtils; 13 | 14 | import java.time.LocalDateTime; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.Optional; 17 | import java.util.Set; 18 | 19 | import static org.fordes.adfs.constant.Constants.*; 20 | 21 | /** 22 | * 输出配置 23 | * 24 | * @author fordes123 on 2022/9/19 25 | */ 26 | @Data 27 | @Component 28 | @ConfigurationProperties(prefix = "application.output") 29 | public class OutputProperties implements InitializingBean { 30 | 31 | private String fileHeader; 32 | private String path = "rule"; 33 | private Set files; 34 | 35 | public record OutputFile(String name, RuleSet type, Set filter, String desc, String fileHeader) { 36 | 37 | public OutputFile(String name, RuleSet type, Set filter, String desc, String fileHeader) { 38 | this.name = Optional.ofNullable(name).filter(StringUtils::hasText).orElseThrow(() -> new IllegalArgumentException("application.output.files.name is required")); 39 | this.type = Optional.ofNullable(type).orElseThrow(() -> new IllegalArgumentException("application.output.files.type is required")); 40 | this.desc = Optional.ofNullable(desc).filter(StringUtils::hasText).orElse(Constants.EMPTY); 41 | this.filter = Optional.ofNullable(filter).orElse(Set.of(Rule.Type.values())); 42 | this.fileHeader = Optional.ofNullable(fileHeader).filter(StringUtils::hasText).orElse(null); 43 | } 44 | 45 | public String displayHeader(Handler handler, String parentHeader) { 46 | StringBuilder builder = new StringBuilder(); 47 | //文件头 48 | Optional.ofNullable(Optional.ofNullable(this.fileHeader()).orElse(parentHeader)) 49 | .filter(StringUtils::hasText) 50 | .ifPresent(e -> { 51 | String header = handler.commented(e 52 | .replace(HEADER_DATE, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) 53 | .replace(HEADER_NAME, this.name()) 54 | .replace(HEADER_DESC, this.desc()) 55 | .replace(HEADER_TYPE, this.type().name().toLowerCase())); 56 | builder.append(header).append(System.lineSeparator()); 57 | }); 58 | 59 | //格式头 60 | Optional.ofNullable(handler.headFormat()).filter(StringUtils::hasText) 61 | .ifPresent(e -> builder.append(e).append(System.lineSeparator())); 62 | 63 | return builder.toString(); 64 | } 65 | } 66 | 67 | @Override 68 | public void afterPropertiesSet() { 69 | this.files = Optional.ofNullable(files) 70 | .filter(e -> !e.isEmpty()) 71 | .orElseThrow(() -> new IllegalArgumentException("application.output.files is required")); 72 | 73 | this.path = Optional.ofNullable(path).filter(StringUtils::hasText) 74 | .map(Util::normalizePath) 75 | .orElseThrow(() -> new IllegalArgumentException("application.output.path is required")); 76 | } 77 | 78 | public boolean isEmpty() { 79 | return files == null || files.isEmpty(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/constant/Constants.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.constant; 2 | 3 | import java.io.File; 4 | import java.util.Set; 5 | 6 | public class Constants { 7 | 8 | public static final String ROOT_PATH = System.getProperty("user.dir"); 9 | public static final String FILE_SEPARATOR = File.separator; 10 | 11 | public static final String HEADER_DATE = "${date}"; 12 | public static final String HEADER_NAME = "${name}"; 13 | public static final String HEADER_TOTAL = "${total}"; 14 | public static final String HEADER_TYPE = "${type}"; 15 | public static final String HEADER_DESC = "${desc}"; 16 | 17 | public static final String EMPTY = ""; 18 | public static final String DOT = "."; 19 | public static final String EXCLAMATION = "!"; 20 | public static final String HASH = "#"; 21 | public static final String AT = "@"; 22 | public static final String PERCENT = "%"; 23 | public static final String DOLLAR = "$"; 24 | public static final String UNDERLINE = "_"; 25 | public static final String DASH = "-"; 26 | public static final String TILDE = "~"; 27 | public static final String COMMA = ","; 28 | public static final String SLASH = "/"; 29 | public static final String LEFT_BRACKETS = "["; 30 | public static final String RIGHT_BRACKETS = "]"; 31 | public static final String OR = "||"; 32 | public static final String ASTERISK = "*"; 33 | public static final String QUESTION_MARK = "?"; 34 | public static final String A = "a"; 35 | public static final String CARET = "^"; 36 | public static final String WHITESPACE = " "; 37 | public static final String CR = "\r"; 38 | public static final String LF = "\n"; 39 | public static final String CRLF = CR + LF; 40 | public static final String QUOTE = "\""; 41 | public static final String SINGLE_QUOTE = "'"; 42 | public static final String ADD = "+"; 43 | public static final String COLON = ":"; 44 | public static final String EQUAL = "="; 45 | 46 | 47 | public static final Set LOCAL_IP = Set.of("0.0.0.0", "127.0.0.1", "::1"); 48 | public static final Set LOCAL_DOMAIN = Set.of("localhost", "localhost.localdomain", "local", "ip6-localhost", "ip6-loopback"); 49 | public static final String LOCAL_V4 = "127.0.0.1"; 50 | public static final String UNKNOWN_IP = "0.0.0.0"; 51 | public static final String LOCAL_V6 = "::1"; 52 | public static final String LOCALHOST = "localhost"; 53 | public static final String DOUBLE_AT = "@@"; 54 | public static final String IMPORTANT = "important"; 55 | public static final String DOMAIN = "domain"; 56 | public static final String TAB = "\t"; 57 | public static final String PAYLOAD = "payload"; 58 | 59 | public static final String DNSMASQ_HEADER = "address=/"; 60 | public static final String SMARTDNS_HEADER = "address /"; 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/constant/RegConstants.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.constant; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * @author fordes on 2024/4/9 7 | */ 8 | public class RegConstants { 9 | 10 | public static final Pattern PATTERN_PATH_ABSOLUTE = Pattern.compile("^[a-zA-Z]:([/\\\\].*)?"); 11 | public static Pattern PATTERN_IP = Pattern.compile("((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))"); 12 | public static Pattern PATTERN_DOMAIN = Pattern.compile("(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$"); 13 | public static Pattern DOMAIN_PART = Pattern.compile("^([-a-zA-Z0-9]{0,62})+$"); 14 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/enums/HandleType.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | 5 | @AllArgsConstructor 6 | public enum HandleType { 7 | 8 | LOCAL, 9 | 10 | REMOTE, 11 | 12 | ; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/enums/RuleSet.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | import java.util.stream.Stream; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | public enum RuleSet { 11 | 12 | EASYLIST, 13 | DNSMASQ, 14 | CLASH, 15 | SMARTDNS, 16 | HOSTS, 17 | ; 18 | 19 | public static RuleSet of(String name) { 20 | return Stream.of(values()) 21 | .filter(v -> v.name().equalsIgnoreCase(name)) 22 | .findFirst() 23 | .orElseThrow(() -> new IllegalArgumentException("unknown format: " + name)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/Parser.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.fordes.adfs.config.Config; 6 | import org.fordes.adfs.config.InputProperties; 7 | import org.fordes.adfs.enums.HandleType; 8 | import org.fordes.adfs.handler.dns.DnsChecker; 9 | import org.fordes.adfs.handler.fetch.Fetcher; 10 | import org.fordes.adfs.handler.rule.Handler; 11 | import org.fordes.adfs.model.Rule; 12 | import org.fordes.adfs.util.BloomFilter; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.StringUtils; 15 | import reactor.core.publisher.Flux; 16 | import reactor.core.publisher.Mono; 17 | 18 | import java.util.Optional; 19 | import java.util.Set; 20 | import java.util.concurrent.atomic.AtomicLong; 21 | 22 | @Slf4j 23 | @Component 24 | @AllArgsConstructor 25 | public class Parser { 26 | 27 | protected final BloomFilter filter; 28 | protected Config config; 29 | protected DnsChecker dnsChecker; 30 | 31 | public Flux handle(InputProperties.Prop prop, HandleType type) { 32 | 33 | AtomicLong invalid = new AtomicLong(0L); 34 | AtomicLong repeat = new AtomicLong(0L); 35 | AtomicLong effective = new AtomicLong(0L); 36 | Set exclude = Optional.ofNullable(config.getExclude()).orElseGet(Set::of); 37 | 38 | return Flux.just(prop) 39 | .flatMap(p -> { 40 | Fetcher fetcher = Fetcher.getFetcher(type); 41 | return fetcher.fetch(p.path()); 42 | }) 43 | .filter(StringUtils::hasText) 44 | .flatMap(line -> { 45 | Handler handler = Handler.getHandler(prop.type()); 46 | 47 | if (handler.isComment(line)) { 48 | return Mono.empty(); 49 | } 50 | 51 | Rule rule = handler.parse(line); 52 | if (Rule.EMPTY.equals(rule)) { 53 | invalid.incrementAndGet(); 54 | log.debug("parse fail: {}", line); 55 | return Mono.empty(); 56 | } 57 | 58 | return Mono.just(rule); 59 | }) 60 | .flatMap(e -> { 61 | 62 | if (e.getTarget() != null && exclude.contains(e.getTarget())) { 63 | log.info("exclude rule: {}", e.getOrigin()); 64 | return Mono.empty(); 65 | } 66 | 67 | if (filter.contains(e)) { 68 | log.debug("already exists rule: {}", e.getOrigin()); 69 | repeat.incrementAndGet(); 70 | return Mono.empty(); 71 | } 72 | 73 | if (e.getOrigin().length() <= config.getWarnLimit()) { 74 | log.warn("[{}] Suspicious rule => {}", prop.name(), e.getOrigin()); 75 | } 76 | 77 | return Mono.just(e); 78 | 79 | }) 80 | .onErrorResume(ex -> { 81 | log.error(ex.getMessage(), ex); 82 | return Mono.empty(); 83 | }) 84 | .flatMap(rule -> { 85 | 86 | /** 87 | * 假设有规则 ||example.org^ 88 | * 通过DNS查询 example.org 是否存在 A/AAAA/CNAME 记录作为判断依据 89 | * 不可避免的误判是,example.org 没有有效记录,而其存在有效子域如 test.example.org 90 | */ 91 | if (dnsChecker.getConfig().enable() && Rule.Type.BASIC.equals(rule.getType()) 92 | && Rule.Scope.DOMAIN.equals(rule.getScope())) { 93 | 94 | return Flux.just(rule.getTarget()) 95 | .flatMap(e -> dnsChecker.lookup(e), 1) 96 | .flatMap(e -> { 97 | if (!e) { 98 | log.debug("[{}] dns check invalid rule => {}", prop.name(), rule.getOrigin()); 99 | invalid.incrementAndGet(); 100 | return Mono.empty(); 101 | } 102 | return Mono.just(rule); 103 | }); 104 | } 105 | return Mono.just(rule); 106 | }, dnsChecker.getConfig().concurrency()) 107 | .flatMap(e -> { 108 | filter.add(e); 109 | effective.incrementAndGet(); 110 | return Mono.just(e); 111 | 112 | }) 113 | .doFinally(signal -> { 114 | log.info("[{}] parser done => invalid: {}, repeat: {}, effective: {}", prop.name(), 115 | invalid.get(), repeat.get(), effective.get()); 116 | }); 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/dns/DnsChecker.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.dns; 2 | 3 | import io.netty.channel.EventLoop; 4 | import io.netty.channel.nio.NioEventLoopGroup; 5 | import io.netty.channel.socket.nio.NioDatagramChannel; 6 | import io.netty.channel.socket.nio.NioSocketChannel; 7 | import io.netty.resolver.ResolvedAddressTypes; 8 | import io.netty.resolver.dns.*; 9 | import io.netty.util.concurrent.Future; 10 | import lombok.Data; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.fordes.adfs.constant.Constants; 13 | import org.springframework.boot.context.properties.ConfigurationProperties; 14 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 15 | import org.springframework.stereotype.Component; 16 | import reactor.core.publisher.Mono; 17 | 18 | import java.net.InetAddress; 19 | import java.net.InetSocketAddress; 20 | import java.net.UnknownHostException; 21 | import java.util.List; 22 | import java.util.Optional; 23 | import java.util.concurrent.ArrayBlockingQueue; 24 | import java.util.stream.IntStream; 25 | 26 | @Data 27 | @Slf4j 28 | @Component 29 | @EnableConfigurationProperties(DnsChecker.Config.class) 30 | public class DnsChecker { 31 | 32 | private final Config config; 33 | private final NioEventLoopGroup eventLoopGroup; 34 | private final DnsServerAddressStreamProvider provider; 35 | private final ArrayBlockingQueue resolvers; 36 | 37 | public DnsChecker(Config config) { 38 | this.config = config; 39 | if (config.enable) { 40 | this.eventLoopGroup = new NioEventLoopGroup(config.concurrency); 41 | this.resolvers = new ArrayBlockingQueue<>(config.concurrency); 42 | if (config.provider.isEmpty()) { 43 | this.provider = DnsServerAddressStreamProviders.platformDefault(); 44 | } else { 45 | InetSocketAddress[] array = config.provider.stream().map(e -> { 46 | String[] split = e.split(Constants.COLON); 47 | return new InetSocketAddress(split[0], split.length > 1 ? Integer.parseInt(split[1]) : 53); 48 | }).toArray(InetSocketAddress[]::new); 49 | 50 | this.provider = new SequentialDnsServerAddressStreamProvider(array); 51 | } 52 | 53 | final DnsCache cache = new DefaultDnsCache(5, 60, 60); 54 | final DnsCnameCache cnameCache = new DefaultDnsCnameCache(5, 60); 55 | IntStream.range(0, config.concurrency) 56 | .forEach(i -> { 57 | EventLoop eventLoop = this.eventLoopGroup.next(); 58 | DnsNameResolver resolver = new DnsNameResolverBuilder(eventLoop) 59 | .datagramChannelFactory(NioDatagramChannel::new) 60 | .socketChannelFactory(NioSocketChannel::new) 61 | .nameServerProvider(this.provider) 62 | .queryTimeoutMillis(config.timeout) 63 | .maxQueriesPerResolve(1) 64 | .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) 65 | .resolveCache(cache) 66 | .cnameCache(cnameCache) 67 | .build(); 68 | resolvers.add(resolver); 69 | }); 70 | 71 | } else { 72 | log.warn("dns check is disabled"); 73 | this.resolvers = null; 74 | this.eventLoopGroup = null; 75 | this.provider = null; 76 | } 77 | } 78 | 79 | @ConfigurationProperties(prefix = "application.config.domain-detect") 80 | public record Config(Boolean enable, Integer timeout, Integer concurrency, List provider) { 81 | 82 | public Config(Boolean enable, Integer timeout, Integer concurrency, List provider) { 83 | this.enable = Optional.ofNullable(enable).orElse(Boolean.TRUE); 84 | this.timeout = Optional.ofNullable(timeout).orElse(1000); 85 | this.concurrency = Optional.ofNullable(concurrency).orElse(4); 86 | this.provider = Optional.ofNullable(provider).filter(e -> !e.isEmpty()).orElse(List.of()); 87 | } 88 | } 89 | 90 | public Mono lookup(String domain) { 91 | return Mono.create(sink -> { 92 | if (resolvers == null) { 93 | sink.success(true); 94 | return; 95 | } 96 | 97 | DnsNameResolver resolver; 98 | try { 99 | resolver = resolvers.take(); 100 | } catch (InterruptedException e) { 101 | sink.success(true); 102 | log.error("dns resolve interrupted", e); 103 | return; 104 | } 105 | 106 | Future> future = resolver.resolveAll(domain); 107 | future.addListener(result -> { 108 | 109 | boolean res = true; 110 | if (!result.isSuccess()) { 111 | Throwable cause = result.cause(); 112 | 113 | if (cause instanceof UnknownHostException) { 114 | res = false; 115 | } else { 116 | log.warn("dns check failed: {} => {}", domain, cause.getMessage()); 117 | } 118 | } 119 | sink.success(res); 120 | resolvers.offer(resolver); 121 | log.debug("dns check done, available: {}", resolvers.size()); 122 | }); 123 | }); 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/fetch/Fetcher.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.fetch; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import org.fordes.adfs.enums.HandleType; 5 | import org.springframework.core.io.buffer.DataBuffer; 6 | import org.springframework.core.io.buffer.DataBufferUtils; 7 | import reactor.core.publisher.Flux; 8 | 9 | import java.nio.charset.Charset; 10 | 11 | public abstract class Fetcher { 12 | 13 | protected @Nonnull Charset charset() { 14 | return Charset.defaultCharset(); 15 | } 16 | 17 | public abstract Flux fetch(@Nonnull String path); 18 | 19 | protected Flux fetch(Flux buffers) { 20 | final StringBuilder lineBuffer = new StringBuilder(); 21 | return buffers.flatMap(data -> { 22 | String chunk = data.toString(this.charset()); 23 | DataBufferUtils.release(data); 24 | 25 | lineBuffer.append(chunk); 26 | String full = lineBuffer.toString(); 27 | 28 | String[] lines = full.split("\\r?\\n", -1); 29 | int len = lines.length; 30 | 31 | lineBuffer.setLength(0); 32 | 33 | if (!full.endsWith("\n") && !full.endsWith("\r")) { 34 | lineBuffer.append(lines[len - 1]); 35 | len--; 36 | } 37 | 38 | return Flux.fromArray(lines).take(len); 39 | }) 40 | .concatWith(Flux.defer(() -> { 41 | if (lineBuffer.length() > 0) { 42 | return Flux.just(lineBuffer.toString()); 43 | } else { 44 | return Flux.empty(); 45 | } 46 | })); 47 | } 48 | 49 | public final static Fetcher getFetcher(HandleType type) { 50 | switch (type) { 51 | case LOCAL: 52 | return new LocalFetcher(); 53 | case REMOTE: 54 | return new HttpFetcher(); 55 | } 56 | throw new IllegalArgumentException("unsupported handle type: " + type); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/fetch/HttpFetcher.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.fetch; 2 | 3 | import io.netty.channel.ChannelOption; 4 | import io.netty.handler.timeout.ReadTimeoutHandler; 5 | import io.netty.handler.timeout.WriteTimeoutHandler; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.core.io.buffer.DataBuffer; 8 | import org.springframework.http.HttpHeaders; 9 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; 10 | import org.springframework.web.reactive.function.client.ExchangeStrategies; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | import reactor.core.publisher.Flux; 13 | import reactor.netty.http.client.HttpClient; 14 | 15 | import java.net.URI; 16 | import java.nio.charset.Charset; 17 | import java.nio.charset.StandardCharsets; 18 | import java.time.Duration; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | @Slf4j 22 | public class HttpFetcher extends Fetcher { 23 | 24 | private final WebClient webClient; 25 | private Integer connectTimeout = 10_000; 26 | private Integer readTimeout = 30_000; 27 | private Integer writeTimeout = 30_000; 28 | private Integer bufferSize = 4096; 29 | 30 | public HttpFetcher() { 31 | 32 | HttpClient httpClient = HttpClient.create() 33 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout) // 连接超时 10 秒 34 | .responseTimeout(Duration.ofSeconds(30)) // 响应超时 30 秒 35 | .doOnConnected(conn -> conn 36 | .addHandlerLast(new ReadTimeoutHandler(this.readTimeout, TimeUnit.MILLISECONDS)) // 读超时 37 | .addHandlerLast(new WriteTimeoutHandler(this.writeTimeout, TimeUnit.MILLISECONDS)) // 写超时 38 | ); 39 | 40 | ExchangeStrategies strategies = ExchangeStrategies.builder() 41 | .codecs(configurer -> configurer 42 | .defaultCodecs() 43 | .maxInMemorySize(this.bufferSize) 44 | ) 45 | .build(); 46 | 47 | this.webClient = WebClient.builder() 48 | .clientConnector(new ReactorClientHttpConnector(httpClient)) 49 | .exchangeStrategies(strategies) 50 | .defaultHeader(HttpHeaders.CONNECTION, "keep-alive") 51 | // .defaultHeader(HttpHeaders.ACCEPT_CHARSET, this.charset().displayName()) 52 | // .defaultHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate, br") 53 | .build(); 54 | } 55 | 56 | public HttpFetcher(Integer connectTimeout, Integer readTimeout, Integer writeTimeout, Integer bufferSize) { 57 | this(); 58 | this.connectTimeout = connectTimeout; 59 | this.readTimeout = readTimeout; 60 | this.writeTimeout = writeTimeout; 61 | this.bufferSize = bufferSize; 62 | } 63 | 64 | @Override 65 | public Flux fetch(String path) { 66 | Flux data = webClient.get() 67 | .uri(URI.create(path)) 68 | .retrieve() 69 | .bodyToFlux(DataBuffer.class) 70 | .onErrorResume(e -> { 71 | log.error("http rule => {}, fetch failed => {}", path, e.getMessage(), e); 72 | return Flux.empty(); 73 | }); 74 | 75 | return this.fetch(data); 76 | } 77 | 78 | @Override 79 | protected Charset charset() { 80 | return StandardCharsets.UTF_8; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/fetch/LocalFetcher.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.fetch; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.fordes.adfs.util.Util; 5 | import org.springframework.core.io.buffer.DataBuffer; 6 | import org.springframework.core.io.buffer.DataBufferUtils; 7 | import org.springframework.core.io.buffer.DefaultDataBufferFactory; 8 | import reactor.core.publisher.Flux; 9 | 10 | import java.nio.charset.Charset; 11 | import java.nio.file.Path; 12 | 13 | @Slf4j 14 | public class LocalFetcher extends Fetcher { 15 | 16 | private int bufferSize = 4096; 17 | 18 | public LocalFetcher() { 19 | super(); 20 | } 21 | 22 | public LocalFetcher(int bufferSize) { 23 | super(); 24 | this.bufferSize = bufferSize; 25 | } 26 | 27 | @Override 28 | public Flux fetch(String path) { 29 | 30 | Flux data = Flux.just(path) 31 | .map(Util::normalizePath) 32 | .map(Path::of) 33 | .flatMap(p -> DataBufferUtils.read(p, new DefaultDataBufferFactory(), this.bufferSize)) 34 | .onErrorResume(e -> { 35 | log.error("local rule => {}, read failed => {}", path, e.getMessage(), e); 36 | return Flux.empty(); 37 | }); 38 | 39 | return this.fetch(data); 40 | } 41 | 42 | @Override 43 | protected Charset charset() { 44 | return super.charset(); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/rule/ClashHandler.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.rule; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.fordes.adfs.enums.RuleSet; 5 | import org.fordes.adfs.model.Rule; 6 | import org.fordes.adfs.util.Util; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Optional; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | 14 | import static org.fordes.adfs.constant.Constants.*; 15 | import static org.fordes.adfs.constant.RegConstants.PATTERN_DOMAIN; 16 | 17 | /** 18 | * @author fordes123 on 2024/5/27 19 | */ 20 | @Slf4j 21 | @Component 22 | public final class ClashHandler extends Handler implements InitializingBean { 23 | 24 | @Override 25 | public Rule parse(String line) { 26 | //跳过文件头 27 | if (line.startsWith(PAYLOAD)) { 28 | return Rule.EMPTY; 29 | } 30 | 31 | Rule rule = new Rule(); 32 | rule.setOrigin(line); 33 | rule.setSource(RuleSet.EASYLIST); 34 | 35 | //只匹配 domain 规则,ipcidr、classical 规则暂不支持 36 | String content = (line.startsWith(DASH) ? line.substring(DASH.length()) : line).trim(); 37 | if (content.startsWith(SINGLE_QUOTE)) { 38 | content = Util.subBetween(line, SINGLE_QUOTE, SINGLE_QUOTE).trim(); 39 | } else if (content.startsWith(QUOTE)) { 40 | content = Util.subBetween(line, QUOTE, QUOTE).trim(); 41 | } 42 | 43 | //通配符 * 一次只能匹配一级域名,无法转换为easylist 44 | if (content.startsWith(ASTERISK)) { 45 | rule.setType(Rule.Type.UNKNOWN); 46 | return rule; 47 | } 48 | 49 | //通配符 + 50 | if (content.startsWith(ADD)) { 51 | content = content.substring(content.startsWith("+.") ? 2 : 1); 52 | rule.setControls(Set.of(Rule.Control.OVERLAY)); 53 | } 54 | 55 | //判断是否是domain 56 | boolean haveAsterisk = content.contains(ASTERISK); 57 | String temp = haveAsterisk ? content.replace(ASTERISK, A) : content; 58 | if (PATTERN_DOMAIN.matcher(temp).matches()) { 59 | rule.setType(haveAsterisk ? Rule.Type.WILDCARD : Rule.Type.BASIC); 60 | } 61 | 62 | rule.setTarget(content); 63 | rule.setDest(UNKNOWN_IP); 64 | rule.setMode(Rule.Mode.DENY); 65 | rule.setScope(Rule.Scope.DOMAIN); 66 | if (rule.getType() == null) { 67 | rule.setType(Rule.Type.UNKNOWN); 68 | } 69 | return rule; 70 | } 71 | 72 | @Override 73 | public String format(Rule rule) { 74 | if (Rule.Type.UNKNOWN == rule.getType()) { 75 | if (RuleSet.CLASH == rule.getSource()) { 76 | return rule.getOrigin(); 77 | } 78 | return null; 79 | } else if (rule.getMode() == Rule.Mode.DENY && rule.getScope() == Rule.Scope.DOMAIN) { 80 | StringBuilder builder = new StringBuilder(); 81 | builder.append(WHITESPACE).append(WHITESPACE).append(DASH).append(WHITESPACE).append(QUOTE); 82 | 83 | Set controls = Optional.ofNullable(rule.getControls()).orElse(Set.of()); 84 | if (controls.contains(Rule.Control.OVERLAY)) { 85 | builder.append(ADD).append(DOT); 86 | } 87 | builder.append(rule.getTarget()); 88 | builder.append(QUOTE); 89 | return builder.toString(); 90 | } 91 | return null; 92 | } 93 | 94 | @Override 95 | public String headFormat() { 96 | return PAYLOAD + COLON; 97 | } 98 | 99 | @Override 100 | public boolean isComment(String line) { 101 | return line.startsWith(HASH); 102 | } 103 | 104 | @Override 105 | public String commented(String value) { 106 | return Util.splitIgnoreBlank(value, LF).stream() 107 | .map(e -> HASH + e.trim()) 108 | .collect(Collectors.joining(CRLF)); 109 | } 110 | 111 | @Override 112 | public void afterPropertiesSet() { 113 | this.register(RuleSet.CLASH, this); 114 | } 115 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/rule/DnsmasqHandler.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.rule; 2 | 3 | import org.fordes.adfs.enums.RuleSet; 4 | import org.fordes.adfs.model.Rule; 5 | import org.fordes.adfs.util.Util; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | import static org.fordes.adfs.constant.Constants.*; 13 | 14 | /** 15 | * @author fordes123 on 2024/5/27 16 | */ 17 | @Component 18 | public final class DnsmasqHandler extends Handler implements InitializingBean { 19 | 20 | @Override 21 | public Rule parse(String line) { 22 | 23 | String content = Util.subAfter(line, DNSMASQ_HEADER, true); 24 | List data = Util.splitIgnoreBlank(content, SLASH); 25 | if (data.size() == 1 || data.size() == 2) { 26 | String domain = data.getFirst(); 27 | String ip = data.size() > 1 ? data.get(1) : null; 28 | 29 | Rule rule = new Rule(); 30 | rule.setSource(RuleSet.DNSMASQ); 31 | rule.setOrigin(line); 32 | rule.setTarget(domain); 33 | rule.setDest(ip); 34 | rule.setScope(Rule.Scope.DOMAIN); 35 | rule.setType(Rule.Type.BASIC); 36 | rule.setMode((ip == null || LOCAL_IP.contains(ip)) ? Rule.Mode.DENY : Rule.Mode.REWRITE); 37 | return rule; 38 | } 39 | return Rule.EMPTY; 40 | } 41 | 42 | @Override 43 | public String format(Rule rule) { 44 | if (Rule.Scope.DOMAIN == rule.getScope() && 45 | Rule.Type.BASIC == rule.getType() && 46 | Rule.Mode.ALLOW != rule.getMode()) { 47 | 48 | StringBuilder builder = new StringBuilder(); 49 | builder.append(DNSMASQ_HEADER) 50 | .append(rule.getTarget()); 51 | if (Rule.Mode.REWRITE.equals(rule.getMode())) { 52 | builder.append(SLASH) 53 | .append(rule.getDest()); 54 | } 55 | builder.append(SLASH); 56 | return builder.toString(); 57 | } 58 | return null; 59 | } 60 | 61 | @Override 62 | public String commented(String value) { 63 | return Util.splitIgnoreBlank(value, LF).stream() 64 | .map(e -> HASH + WHITESPACE + e.trim()) 65 | .collect(Collectors.joining(CRLF)); 66 | } 67 | 68 | @Override 69 | public boolean isComment(String line) { 70 | return line.startsWith(HASH); 71 | } 72 | 73 | @Override 74 | public void afterPropertiesSet() { 75 | this.register(RuleSet.DNSMASQ, this); 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/rule/EasylistHandler.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.rule; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.fordes.adfs.enums.RuleSet; 5 | import org.fordes.adfs.model.Rule; 6 | import org.fordes.adfs.util.Util; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Arrays; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.stream.Collectors; 14 | 15 | import static org.fordes.adfs.constant.Constants.*; 16 | 17 | /** 18 | * @author fordes123 on 2024/5/27 19 | */ 20 | @Slf4j 21 | @Component 22 | public final class EasylistHandler extends Handler implements InitializingBean { 23 | 24 | @Override 25 | public Rule parse(String line) { 26 | Rule rule = new Rule(); 27 | rule.setOrigin(line); 28 | rule.setSource(RuleSet.EASYLIST); 29 | rule.setMode(Rule.Mode.DENY); 30 | 31 | if (line.startsWith(DOUBLE_AT)) { 32 | rule.setMode(Rule.Mode.ALLOW); 33 | line = line.substring(2); 34 | } 35 | 36 | int _head = 0; 37 | if (line.startsWith(OR)) { 38 | _head = OR.length(); 39 | rule.getControls().add(Rule.Control.OVERLAY); 40 | } 41 | 42 | 43 | //修饰部分 44 | int _tail = line.indexOf(CARET); 45 | if (_tail > 0) { 46 | rule.getControls().add(Rule.Control.QUALIFIER); 47 | 48 | String modify = line.substring(_tail + 1); 49 | if (!modify.isEmpty()) { 50 | modify = modify.startsWith(DOLLAR) ? modify.substring(1) : modify; 51 | String[] array = modify.split(COMMA); 52 | if (Arrays.stream(array).allMatch(IMPORTANT::equals)) { 53 | rule.getControls().add(Rule.Control.IMPORTANT); 54 | } else { 55 | rule.setType(Rule.Type.UNKNOWN); 56 | return rule; 57 | } 58 | } 59 | } else if (line.endsWith(DOLLAR + IMPORTANT)) { 60 | rule.getControls().add(Rule.Control.IMPORTANT); 61 | _tail = line.length() - (DOLLAR.length() + IMPORTANT.length()); 62 | } 63 | 64 | 65 | //内容部分 66 | String content = line.substring(_head, _tail > 0 ? _tail : line.length()); 67 | 68 | if (content.startsWith(SLASH) && content.endsWith(SLASH)) { 69 | content = content.substring(1, content.length() - 1); 70 | rule.setType(Rule.Type.UNKNOWN); 71 | } 72 | 73 | //判断是否为基本或通配规则 74 | Util.isBaseRule(content, (origin, e) -> { 75 | if (rule.getType() == null) { 76 | rule.setType(e); 77 | } 78 | rule.setScope(Rule.Scope.DOMAIN); 79 | rule.setTarget(origin); 80 | if (Rule.Mode.DENY.equals(rule.getMode())) { 81 | rule.setDest(UNKNOWN_IP); 82 | } 83 | }, e -> { 84 | Map.Entry entry = Util.parseHosts(e); 85 | if (entry != null) { 86 | rule.setSource(RuleSet.HOSTS); 87 | rule.setTarget(entry.getValue()); 88 | rule.setMode(LOCAL_IP.contains(entry.getKey()) && !LOCAL_DOMAIN.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE); 89 | rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey()); 90 | rule.setScope(Rule.Scope.DOMAIN); 91 | rule.setType(Rule.Type.BASIC); 92 | } else { 93 | rule.setType(Rule.Type.UNKNOWN); 94 | } 95 | }); 96 | return rule; 97 | } 98 | 99 | @Override 100 | public String format(Rule rule) { 101 | if (Rule.Type.UNKNOWN != rule.getType() && Rule.Mode.REWRITE != rule.getMode()) { 102 | 103 | StringBuilder builder = new StringBuilder(); 104 | Optional.of(rule.getMode()) 105 | .filter(Rule.Mode.ALLOW::equals) 106 | .ifPresent(m -> builder.append(DOUBLE_AT)); 107 | 108 | Optional.of(rule.getControls()) 109 | .filter(e -> e.contains(Rule.Control.OVERLAY)) 110 | .ifPresent(c -> builder.append(OR)); 111 | 112 | builder.append(rule.getTarget()); 113 | 114 | Optional.of(rule.getControls()) 115 | .filter(e -> e.contains(Rule.Control.QUALIFIER)) 116 | .ifPresent(c -> builder.append(CARET)); 117 | 118 | Optional.of(rule.getControls()) 119 | .filter(e -> e.contains(Rule.Control.IMPORTANT)) 120 | .ifPresent(c -> builder.append(DOLLAR).append(IMPORTANT)); 121 | return builder.toString(); 122 | } 123 | 124 | //同源未知规则可直接写出 125 | if (Rule.Type.UNKNOWN == rule.getType() && RuleSet.EASYLIST == rule.getSource()) { 126 | return rule.getOrigin(); 127 | } 128 | return null; 129 | } 130 | 131 | @Override 132 | public String commented(String value) { 133 | return Util.splitIgnoreBlank(value, LF).stream() 134 | .map(e -> EXCLAMATION + WHITESPACE + e.trim()) 135 | .collect(Collectors.joining(CRLF)); 136 | } 137 | 138 | @Override 139 | public void afterPropertiesSet() { 140 | this.register(RuleSet.EASYLIST, this); 141 | } 142 | 143 | @Override 144 | public boolean isComment(String line) { 145 | return Util.startWithAny(line, HASH, EXCLAMATION) || Util.between(line, LEFT_BRACKETS, RIGHT_BRACKETS); 146 | } 147 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/rule/Handler.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.rule; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import org.fordes.adfs.constant.Constants; 6 | import org.fordes.adfs.enums.RuleSet; 7 | import org.fordes.adfs.model.Rule; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public abstract sealed class Handler permits EasylistHandler, DnsmasqHandler, ClashHandler, 13 | SmartdnsHandler, HostsHandler { 14 | 15 | private static final Map handlerMap = new HashMap<>(RuleSet.values().length, 1); 16 | 17 | /** 18 | * 解析规则
19 | * 返回 null 即表示解析失败 20 | * 21 | * @param line 规则文本 22 | * @return {@link Rule} 23 | */ 24 | public abstract @Nonnull Rule parse(String line); 25 | 26 | /** 27 | * 转换规则
28 | * 29 | * @param rule {@link Rule} null 表示无法转换或失败 30 | * @return 规则文本 31 | */ 32 | public abstract @Nullable String format(Rule rule); 33 | 34 | /** 35 | * 生成注释 36 | * @param value 目标内容 37 | * @return 注释 38 | */ 39 | public abstract String commented(String value); 40 | 41 | /** 42 | * 某些规则格式拥有固定的头部内容,可实现此方法以返回 43 | */ 44 | public String headFormat() { 45 | return Constants.EMPTY; 46 | } 47 | 48 | /** 49 | * 某些规则格式拥有固定的尾部内容,可实现此方法以返回 50 | */ 51 | public String tailFormat() { 52 | return Constants.EMPTY; 53 | } 54 | 55 | /** 56 | * 验证规则文本是否为注释
57 | * 并不强制子类实现此方法,且不是注释不表示此规则有效 58 | * 59 | * @param line 规则文本 60 | * @return 默认 false 61 | */ 62 | public boolean isComment(String line) { 63 | return false; 64 | } 65 | 66 | /** 67 | * 根据 RuleSet 获取 Handler 68 | * 69 | * @param type {@link RuleSet} 70 | * @return {@link Handler} 71 | */ 72 | public static Handler getHandler(RuleSet type) { 73 | return handlerMap.get(type); 74 | } 75 | 76 | protected void register(RuleSet type, Handler handler) { 77 | handlerMap.put(type, handler); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/rule/HostsHandler.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.rule; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.fordes.adfs.enums.RuleSet; 5 | import org.fordes.adfs.model.Rule; 6 | import org.fordes.adfs.util.Util; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Map; 11 | import java.util.Objects; 12 | import java.util.Optional; 13 | import java.util.stream.Collectors; 14 | 15 | import static org.fordes.adfs.constant.Constants.*; 16 | 17 | /** 18 | * @author fordes123 on 2024/5/27 19 | */ 20 | @Slf4j 21 | @Component 22 | public final class HostsHandler extends Handler implements InitializingBean { 23 | 24 | @Override 25 | public Rule parse(String line) { 26 | Map.Entry entry = Util.parseHosts(line); 27 | if (entry == null || Objects.equals(entry.getKey(), entry.getValue())) { 28 | return Rule.EMPTY; 29 | } 30 | 31 | Rule rule = new Rule(); 32 | rule.setSource(RuleSet.HOSTS); 33 | rule.setOrigin(line); 34 | rule.setTarget(entry.getValue()); 35 | rule.setMode(LOCAL_IP.contains(entry.getKey()) && !LOCAL_DOMAIN.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE); 36 | rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey()); 37 | rule.setScope(Rule.Scope.DOMAIN); 38 | rule.setType(Rule.Type.BASIC); 39 | return rule; 40 | } 41 | 42 | @Override 43 | public String format(Rule rule) { 44 | if (Rule.Scope.DOMAIN == rule.getScope() && 45 | Rule.Type.BASIC == rule.getType() && 46 | Rule.Mode.ALLOW != rule.getMode()) { 47 | return Optional.ofNullable(rule.getDest()).orElse(UNKNOWN_IP) + TAB + rule.getTarget(); 48 | } 49 | return null; 50 | } 51 | 52 | @Override 53 | public String commented(String value) { 54 | return Util.splitIgnoreBlank(value, LF).stream() 55 | .map(e -> HASH + WHITESPACE + e.trim()) 56 | .collect(Collectors.joining(CRLF)); 57 | } 58 | 59 | @Override 60 | public boolean isComment(String line) { 61 | return line.startsWith(HASH); 62 | } 63 | 64 | @Override 65 | public void afterPropertiesSet() { 66 | this.register(RuleSet.HOSTS, this); 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/handler/rule/SmartdnsHandler.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.handler.rule; 2 | 3 | import org.fordes.adfs.enums.RuleSet; 4 | import org.fordes.adfs.model.Rule; 5 | import org.fordes.adfs.util.Util; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.List; 10 | import java.util.Set; 11 | import java.util.stream.Collectors; 12 | 13 | import static org.fordes.adfs.constant.Constants.*; 14 | import static org.fordes.adfs.model.Rule.Mode.ALLOW; 15 | 16 | /** 17 | * @author fordes123 on 2024/5/27 18 | */ 19 | @Component 20 | public final class SmartdnsHandler extends Handler implements InitializingBean { 21 | 22 | @Override 23 | public Rule parse(String line) { 24 | 25 | String content = Util.subAfter(line, SMARTDNS_HEADER, true); 26 | List data = Util.splitIgnoreBlank(content, SLASH); 27 | if (data.size() == 2) { 28 | String domain = data.getFirst(); 29 | String control = data.get(1); 30 | Rule rule = new Rule(); 31 | rule.setOrigin(line); 32 | rule.setSource(RuleSet.EASYLIST); 33 | 34 | switch (control) { 35 | case HASH -> rule.setMode(Rule.Mode.DENY); 36 | case DASH -> rule.setMode(ALLOW); 37 | default -> { 38 | //未知或不支持的控制符 如 #6 #4 39 | rule.setType(Rule.Type.UNKNOWN); 40 | return rule; 41 | } 42 | } 43 | 44 | //仅匹配主域名 45 | if (domain.startsWith(DASH)) { 46 | domain = domain.substring(0, line.length() - DASH.length()); 47 | } else { 48 | rule.setControls(Set.of(Rule.Control.OVERLAY)); 49 | } 50 | 51 | if (domain.startsWith(DOT)) { 52 | domain = domain.substring(0, line.length() - DOT.length()); 53 | } 54 | 55 | rule.setType(domain.startsWith(ASTERISK) ? Rule.Type.WILDCARD : Rule.Type.BASIC); 56 | rule.setTarget(domain); 57 | rule.setDest(UNKNOWN_IP); 58 | rule.setScope(Rule.Scope.DOMAIN); 59 | return rule; 60 | } 61 | return Rule.EMPTY; 62 | } 63 | 64 | @Override 65 | public String format(Rule rule) { 66 | if (Rule.Type.UNKNOWN == rule.getType()) { 67 | if (RuleSet.SMARTDNS == rule.getSource()) { 68 | return rule.getOrigin(); 69 | } 70 | return null; 71 | } else if (rule.getMode() != Rule.Mode.REWRITE && rule.getScope() == Rule.Scope.DOMAIN) { 72 | 73 | switch (rule.getType()) { 74 | case BASIC -> { 75 | return SMARTDNS_HEADER + 76 | (!rule.getControls().contains(Rule.Control.OVERLAY) ? 77 | (ASTERISK + DOT) : EMPTY) + 78 | rule.getTarget() + 79 | SLASH + 80 | (Rule.Mode.DENY.equals(rule.getMode()) ? HASH : DASH); 81 | } 82 | case WILDCARD -> { 83 | String domain = rule.getTarget(); 84 | if (domain.lastIndexOf(ASTERISK) == 0) { 85 | return SMARTDNS_HEADER + domain + SLASH + DASH; 86 | } 87 | } 88 | } 89 | } 90 | return null; 91 | } 92 | 93 | @Override 94 | public String commented(String value) { 95 | return Util.splitIgnoreBlank(value, LF).stream() 96 | .map(e -> HASH + WHITESPACE + e.trim()) 97 | .collect(Collectors.joining(CRLF)); 98 | } 99 | 100 | @Override 101 | public boolean isComment(String line) { 102 | return line.startsWith(HASH); 103 | } 104 | 105 | @Override 106 | public void afterPropertiesSet() { 107 | this.register(RuleSet.SMARTDNS, this); 108 | } 109 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/model/Rule.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.model; 2 | 3 | import lombok.Data; 4 | import org.fordes.adfs.enums.RuleSet; 5 | 6 | import java.util.HashSet; 7 | import java.util.Objects; 8 | import java.util.Set; 9 | 10 | /** 11 | * @author fordes123 on 2024/5/27 12 | */ 13 | @Data 14 | public class Rule { 15 | private RuleSet source; 16 | private String origin; 17 | 18 | private String target; 19 | private String dest; 20 | 21 | private Mode mode; 22 | private Scope scope; 23 | private Type type; 24 | private Set controls = new HashSet<>(Control.values().length, 1.0f); 25 | 26 | public static final Rule EMPTY = new Rule(); 27 | 28 | /** 29 | * 规则控制参数 30 | */ 31 | public enum Control { 32 | /** 33 | * 最高优先级 34 | */ 35 | IMPORTANT, 36 | 37 | /** 38 | * 覆盖子域名 39 | */ 40 | 41 | OVERLAY, 42 | 43 | /** 44 | * 限定符,通常是 ^ 45 | */ 46 | QUALIFIER, 47 | 48 | ; 49 | } 50 | 51 | /** 52 | * 规则模式 53 | */ 54 | public enum Mode { 55 | 56 | /** 57 | * 阻止 58 | */ 59 | DENY, 60 | 61 | /** 62 | * 解除阻止 63 | */ 64 | ALLOW, 65 | 66 | /** 67 | * 重写
68 | * 通常 hosts规则指向特定ip(非localhost)时即为重写 69 | */ 70 | REWRITE, 71 | 72 | ; 73 | } 74 | 75 | /** 76 | * 规则类型 77 | */ 78 | public enum Type { 79 | /** 80 | * 基本规则,不包含任何控制、匹配符号, 可以转换为 hosts 81 | */ 82 | BASIC, 83 | 84 | /** 85 | * 通配规则,仅使用通配符 86 | */ 87 | WILDCARD, 88 | 89 | /** 90 | * 其他规则,如使用了正则、高级修饰符号等,这表示目前无法支持 91 | */ 92 | UNKNOWN, 93 | 94 | ; 95 | } 96 | 97 | /** 98 | * 作用域 99 | */ 100 | public enum Scope { 101 | /** 102 | * ipv4或ipv6地址 103 | */ 104 | HOST, 105 | 106 | /** 107 | * 域名 108 | */ 109 | DOMAIN, 110 | 111 | /** 112 | * 路径、文件等 113 | */ 114 | PATH, 115 | 116 | ; 117 | } 118 | 119 | @Override 120 | public boolean equals(Object o) { 121 | if (this == o) return true; 122 | if (o instanceof Rule rule) { 123 | if (Type.UNKNOWN == this.type || Type.UNKNOWN == rule.getType()) { 124 | return Objects.equals(this.origin, rule.origin); 125 | } 126 | return Objects.equals(this.target, rule.target) && 127 | this.mode == rule.mode && 128 | this.scope == rule.scope && 129 | this.type == rule.type; 130 | } 131 | return false; 132 | } 133 | 134 | @Override 135 | public int hashCode() { 136 | if (Type.UNKNOWN == this.type) { 137 | return Objects.hash(this.origin); 138 | } 139 | return Objects.hash(getTarget(), getMode(), getScope(), getType()); 140 | } 141 | 142 | @Override 143 | public String toString() { 144 | if (Type.UNKNOWN == this.type) { 145 | return "Rule{" + 146 | "origin='" + origin + '\'' + 147 | '}'; 148 | } 149 | return "Rule{" + 150 | "target='" + target + '\'' + 151 | ", mode=" + mode + 152 | ", scope=" + scope + 153 | ", type=" + type + 154 | '}'; 155 | } 156 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/task/Endpoint.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.task; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.fordes.adfs.config.InputProperties; 6 | import org.fordes.adfs.config.OutputProperties; 7 | import org.fordes.adfs.handler.Parser; 8 | import org.fordes.adfs.handler.rule.Handler; 9 | import org.springframework.boot.ApplicationRunner; 10 | import org.springframework.boot.SpringApplication; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.stereotype.Component; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | import reactor.core.scheduler.Schedulers; 17 | import reactor.util.function.Tuple2; 18 | import reactor.util.function.Tuples; 19 | 20 | import java.nio.charset.StandardCharsets; 21 | import java.nio.file.Files; 22 | import java.nio.file.Path; 23 | import java.nio.file.StandardOpenOption; 24 | import java.time.Duration; 25 | import java.util.List; 26 | 27 | /** 28 | * @author fordes on 2024/4/10 29 | */ 30 | @Slf4j 31 | @Component 32 | @RequiredArgsConstructor 33 | public class Endpoint { 34 | 35 | private final ApplicationContext context; 36 | private final InputProperties input; 37 | private final OutputProperties output; 38 | private final Parser parser; 39 | 40 | @Bean 41 | ApplicationRunner start() { 42 | return args -> { 43 | long start = System.currentTimeMillis(); 44 | this.initialize() 45 | .thenMany(Flux.fromStream(input.stream())) 46 | .flatMap(e -> parser.handle(e.getValue(), e.getKey()), 1) 47 | .flatMap(rule -> Flux.fromIterable(output.getFiles()) 48 | .filter(file -> file.filter().contains(rule.getType())) 49 | .flatMap(config -> { 50 | Handler handler = Handler.getHandler(config.type()); 51 | String content = handler.format(rule); 52 | 53 | return content == null 54 | ? Mono.empty() 55 | : Mono.just(Tuples.of(config, content)); 56 | })) 57 | .groupBy(Tuple2::getT1, Tuple2::getT2) 58 | .flatMap(group -> { 59 | Path path = Path.of(output.getPath(), group.key().name()); 60 | return group 61 | .bufferTimeout(5000, Duration.ofSeconds(1)) 62 | .concatMap(batch -> asyncBatchWrite(path, batch)) 63 | .subscribeOn(Schedulers.single()); 64 | }) 65 | .then() 66 | .doFinally(signal -> { 67 | log.info("all done, cost {}ms", System.currentTimeMillis() - start); 68 | this.exit(); 69 | }) 70 | .block(); 71 | }; 72 | } 73 | 74 | private Mono initialize() { 75 | long start = System.currentTimeMillis(); 76 | return Mono.just(output.getPath()) 77 | .flatMapMany(p -> { 78 | Path path = Path.of(p); 79 | return Mono.fromCallable(() -> { 80 | Files.createDirectories(path); 81 | return p; 82 | }).subscribeOn(Schedulers.boundedElastic()).flux(); 83 | }) 84 | .onErrorResume(ex -> { 85 | log.error("create output dir failed", ex); 86 | return Mono.empty(); 87 | }) 88 | .flatMap(dir -> 89 | Flux.fromIterable(output.getFiles()).map(config -> { 90 | String header = config.displayHeader(Handler.getHandler(config.type()), output.getFileHeader()); 91 | Path path = Path.of(dir, config.name()); 92 | return Tuples.of(header, path); 93 | }) 94 | ) 95 | .flatMap(t -> { 96 | String fileHeader = t.getT1(); 97 | Path path = t.getT2(); 98 | return Mono.fromCallable(() -> { 99 | 100 | Files.writeString( 101 | path, 102 | fileHeader, 103 | StandardOpenOption.CREATE, 104 | StandardOpenOption.TRUNCATE_EXISTING, 105 | StandardOpenOption.WRITE 106 | ); 107 | 108 | return true; 109 | }).subscribeOn(Schedulers.boundedElastic()).then(); 110 | }).doOnError(ex -> { 111 | log.error("initialize failed", ex); 112 | this.exit(); 113 | }) 114 | .doFinally(signal -> { 115 | log.info("initialize done in {}ms", System.currentTimeMillis() - start); 116 | }) 117 | .then(); 118 | } 119 | 120 | private Mono asyncBatchWrite(Path path, List batch) { 121 | return Mono.fromCallable(() -> { 122 | Files.write(path, batch, StandardCharsets.UTF_8, 123 | StandardOpenOption.CREATE, StandardOpenOption.APPEND); 124 | return (Void) null; 125 | }) 126 | .onErrorResume(e -> Mono.fromRunnable(() -> log.error("Write failed", e))) 127 | .subscribeOn(Schedulers.boundedElastic()); 128 | } 129 | 130 | private void exit() { 131 | int exit = SpringApplication.exit(this.context, () -> 0); 132 | System.exit(exit); 133 | } 134 | 135 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/util/BloomFilter.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This program is free software: you can redistribute it and/or modify 3 | * it under the terms of the GNU Lesser General Public License as published by 4 | * the Free Software Foundation, either version 3 of the License, or 5 | * (at your option) any later version. 6 | * 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU Lesser General Public License for more details. 11 | * 12 | * You should have received a copy of the GNU Lesser General Public License 13 | * along with this program. If not, see . 14 | */ 15 | 16 | package org.fordes.adfs.util; 17 | 18 | import java.io.Serializable; 19 | import java.nio.charset.Charset; 20 | import java.security.MessageDigest; 21 | import java.security.NoSuchAlgorithmException; 22 | import java.util.BitSet; 23 | import java.util.Collection; 24 | 25 | /** 26 | * Implementation of a Bloom-filter, as described here: 27 | * http://en.wikipedia.org/wiki/Bloom_filter 28 | * 29 | * For updates and bugfixes, see http://github.com/magnuss/java-bloomfilter 30 | * 31 | * Inspired by the SimpleBloomFilter-class written by Ian Clarke. This 32 | * implementation provides a more evenly distributed Hash-function by 33 | * using a proper digest instead of the Java RNG. Many of the changes 34 | * were proposed in comments in his blog: 35 | * http://blog.locut.us/2008/01/12/a-decent-stand-alone-java-bloom-filter-implementation/ 36 | * 37 | * @param Object type that is to be inserted into the Bloom filter, e.g. String or Integer. 38 | * @author Magnus Skjegstad 39 | */ 40 | @SuppressWarnings("all") 41 | public class BloomFilter implements Serializable { 42 | private BitSet bitset; 43 | private int bitSetSize; 44 | private double bitsPerElement; 45 | private int expectedNumberOfFilterElements; // expected (maximum) number of elements to be added 46 | private int numberOfAddedElements; // number of elements actually added to the Bloom filter 47 | private int k; // number of hash functions 48 | 49 | static final Charset charset = Charset.forName("UTF-8"); // encoding used for storing hash values as strings 50 | 51 | static final String hashName = "MD5"; // MD5 gives good enough accuracy in most circumstances. Change to SHA1 if it's needed 52 | static final MessageDigest digestFunction; 53 | static { // The digest method is reused between instances 54 | MessageDigest tmp; 55 | try { 56 | tmp = java.security.MessageDigest.getInstance(hashName); 57 | } catch (NoSuchAlgorithmException e) { 58 | tmp = null; 59 | } 60 | digestFunction = tmp; 61 | } 62 | 63 | /** 64 | * Constructs an empty Bloom filter. The total length of the Bloom filter will be 65 | * c*n. 66 | * 67 | * @param c is the number of bits used per element. 68 | * @param n is the expected number of elements the filter will contain. 69 | * @param k is the number of hash functions used. 70 | */ 71 | public BloomFilter(double c, int n, int k) { 72 | this.expectedNumberOfFilterElements = n; 73 | this.k = k; 74 | this.bitsPerElement = c; 75 | this.bitSetSize = (int)Math.ceil(c * n); 76 | numberOfAddedElements = 0; 77 | this.bitset = new BitSet(bitSetSize); 78 | } 79 | 80 | /** 81 | * Constructs an empty Bloom filter. The optimal number of hash functions (k) is estimated from the total size of the Bloom 82 | * and the number of expected elements. 83 | * 84 | * @param bitSetSize defines how many bits should be used in total for the filter. 85 | * @param expectedNumberOElements defines the maximum number of elements the filter is expected to contain. 86 | */ 87 | public BloomFilter(int bitSetSize, int expectedNumberOElements) { 88 | this(bitSetSize / (double)expectedNumberOElements, 89 | expectedNumberOElements, 90 | (int) Math.round((bitSetSize / (double)expectedNumberOElements) * Math.log(2.0))); 91 | } 92 | 93 | /** 94 | * Constructs an empty Bloom filter with a given false positive probability. The number of bits per 95 | * element and the number of hash functions is estimated 96 | * to match the false positive probability. 97 | * 98 | * @param falsePositiveProbability is the desired false positive probability. 99 | * @param expectedNumberOfElements is the expected number of elements in the Bloom filter. 100 | */ 101 | public BloomFilter(double falsePositiveProbability, int expectedNumberOfElements) { 102 | this(Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2))) / Math.log(2), // c = k / ln(2) 103 | expectedNumberOfElements, 104 | (int)Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2)))); // k = ceil(-log_2(false prob.)) 105 | } 106 | 107 | /** 108 | * Construct a new Bloom filter based on existing Bloom filter data. 109 | * 110 | * @param bitSetSize defines how many bits should be used for the filter. 111 | * @param expectedNumberOfFilterElements defines the maximum number of elements the filter is expected to contain. 112 | * @param actualNumberOfFilterElements specifies how many elements have been inserted into the filterData BitSet. 113 | * @param filterData a BitSet representing an existing Bloom filter. 114 | */ 115 | public BloomFilter(int bitSetSize, int expectedNumberOfFilterElements, int actualNumberOfFilterElements, BitSet filterData) { 116 | this(bitSetSize, expectedNumberOfFilterElements); 117 | this.bitset = filterData; 118 | this.numberOfAddedElements = actualNumberOfFilterElements; 119 | } 120 | 121 | /** 122 | * Generates a digest based on the contents of a String. 123 | * 124 | * @param val specifies the input data. 125 | * @param charset specifies the encoding of the input data. 126 | * @return digest as long. 127 | */ 128 | public static int createHash(String val, Charset charset) { 129 | return createHash(val.getBytes(charset)); 130 | } 131 | 132 | /** 133 | * Generates a digest based on the contents of a String. 134 | * 135 | * @param val specifies the input data. The encoding is expected to be UTF-8. 136 | * @return digest as long. 137 | */ 138 | public static int createHash(String val) { 139 | return createHash(val, charset); 140 | } 141 | 142 | /** 143 | * Generates a digest based on the contents of an array of bytes. 144 | * 145 | * @param data specifies input data. 146 | * @return digest as long. 147 | */ 148 | public static int createHash(byte[] data) { 149 | return createHashes(data, 1)[0]; 150 | } 151 | 152 | /** 153 | * Generates digests based on the contents of an array of bytes and splits the result into 4-byte int's and store them in an array. The 154 | * digest function is called until the required number of int's are produced. For each call to digest a salt 155 | * is prepended to the data. The salt is increased by 1 for each call. 156 | * 157 | * @param data specifies input data. 158 | * @param hashes number of hashes/int's to produce. 159 | * @return array of int-sized hashes 160 | */ 161 | public static int[] createHashes(byte[] data, int hashes) { 162 | int[] result = new int[hashes]; 163 | 164 | int k = 0; 165 | byte salt = 0; 166 | while (k < hashes) { 167 | byte[] digest; 168 | synchronized (digestFunction) { 169 | digestFunction.update(salt); 170 | salt++; 171 | digest = digestFunction.digest(data); 172 | } 173 | 174 | for (int i = 0; i < digest.length/4 && k < hashes; i++) { 175 | int h = 0; 176 | for (int j = (i*4); j < (i*4)+4; j++) { 177 | h <<= 8; 178 | h |= ((int) digest[j]) & 0xFF; 179 | } 180 | result[k] = h; 181 | k++; 182 | } 183 | } 184 | return result; 185 | } 186 | 187 | /** 188 | * Compares the contents of two instances to see if they are equal. 189 | * 190 | * @param obj is the object to compare to. 191 | * @return True if the contents of the objects are equal. 192 | */ 193 | @Override 194 | public boolean equals(Object obj) { 195 | if (obj == null) { 196 | return false; 197 | } 198 | if (getClass() != obj.getClass()) { 199 | return false; 200 | } 201 | final BloomFilter other = (BloomFilter) obj; 202 | if (this.expectedNumberOfFilterElements != other.expectedNumberOfFilterElements) { 203 | return false; 204 | } 205 | if (this.k != other.k) { 206 | return false; 207 | } 208 | if (this.bitSetSize != other.bitSetSize) { 209 | return false; 210 | } 211 | if (this.bitset != other.bitset && (this.bitset == null || !this.bitset.equals(other.bitset))) { 212 | return false; 213 | } 214 | return true; 215 | } 216 | 217 | /** 218 | * Calculates a hash code for this class. 219 | * @return hash code representing the contents of an instance of this class. 220 | */ 221 | @Override 222 | public int hashCode() { 223 | int hash = 7; 224 | hash = 61 * hash + (this.bitset != null ? this.bitset.hashCode() : 0); 225 | hash = 61 * hash + this.expectedNumberOfFilterElements; 226 | hash = 61 * hash + this.bitSetSize; 227 | hash = 61 * hash + this.k; 228 | return hash; 229 | } 230 | 231 | 232 | /** 233 | * Calculates the expected probability of false positives based on 234 | * the number of expected filter elements and the size of the Bloom filter. 235 | *

236 | * The value returned by this method is the expected rate of false 237 | * positives, assuming the number of inserted elements equals the number of 238 | * expected elements. If the number of elements in the Bloom filter is less 239 | * than the expected value, the true probability of false positives will be lower. 240 | * 241 | * @return expected probability of false positives. 242 | */ 243 | public double expectedFalsePositiveProbability() { 244 | return getFalsePositiveProbability(expectedNumberOfFilterElements); 245 | } 246 | 247 | /** 248 | * Calculate the probability of a false positive given the specified 249 | * number of inserted elements. 250 | * 251 | * @param numberOfElements number of inserted elements. 252 | * @return probability of a false positive. 253 | */ 254 | public double getFalsePositiveProbability(double numberOfElements) { 255 | // (1 - e^(-k * n / m)) ^ k 256 | return Math.pow((1 - Math.exp(-k * (double) numberOfElements 257 | / (double) bitSetSize)), k); 258 | 259 | } 260 | 261 | /** 262 | * Get the current probability of a false positive. The probability is calculated from 263 | * the size of the Bloom filter and the current number of elements added to it. 264 | * 265 | * @return probability of false positives. 266 | */ 267 | public double getFalsePositiveProbability() { 268 | return getFalsePositiveProbability(numberOfAddedElements); 269 | } 270 | 271 | 272 | /** 273 | * Returns the value chosen for K.
274 | *
275 | * K is the optimal number of hash functions based on the size 276 | * of the Bloom filter and the expected number of inserted elements. 277 | * 278 | * @return optimal k. 279 | */ 280 | public int getK() { 281 | return k; 282 | } 283 | 284 | /** 285 | * Sets all bits to false in the Bloom filter. 286 | */ 287 | public void clear() { 288 | bitset.clear(); 289 | numberOfAddedElements = 0; 290 | } 291 | 292 | /** 293 | * Adds an object to the Bloom filter. The output from the object's 294 | * toString() method is used as input to the hash functions. 295 | * 296 | * @param element is an element to register in the Bloom filter. 297 | */ 298 | public void add(E element) { 299 | add(element.toString().getBytes(charset)); 300 | } 301 | 302 | /** 303 | * Adds an array of bytes to the Bloom filter. 304 | * 305 | * @param bytes array of bytes to add to the Bloom filter. 306 | */ 307 | public void add(byte[] bytes) { 308 | int[] hashes = createHashes(bytes, k); 309 | for (int hash : hashes) 310 | bitset.set(Math.abs(hash % bitSetSize), true); 311 | numberOfAddedElements ++; 312 | } 313 | 314 | /** 315 | * Adds all elements from a Collection to the Bloom filter. 316 | * @param c Collection of elements. 317 | */ 318 | public void addAll(Collection c) { 319 | for (E element : c) 320 | add(element); 321 | } 322 | 323 | /** 324 | * Returns true if the element could have been inserted into the Bloom filter. 325 | * Use getFalsePositiveProbability() to calculate the probability of this 326 | * being correct. 327 | * 328 | * @param element element to check. 329 | * @return true if the element could have been inserted into the Bloom filter. 330 | */ 331 | public boolean contains(E element) { 332 | return contains(element.toString().getBytes(charset)); 333 | } 334 | 335 | /** 336 | * Returns true if the array of bytes could have been inserted into the Bloom filter. 337 | * Use getFalsePositiveProbability() to calculate the probability of this 338 | * being correct. 339 | * 340 | * @param bytes array of bytes to check. 341 | * @return true if the array could have been inserted into the Bloom filter. 342 | */ 343 | public boolean contains(byte[] bytes) { 344 | int[] hashes = createHashes(bytes, k); 345 | for (int hash : hashes) { 346 | if (!bitset.get(Math.abs(hash % bitSetSize))) { 347 | return false; 348 | } 349 | } 350 | return true; 351 | } 352 | 353 | /** 354 | * Returns true if all the elements of a Collection could have been inserted 355 | * into the Bloom filter. Use getFalsePositiveProbability() to calculate the 356 | * probability of this being correct. 357 | * @param c elements to check. 358 | * @return true if all the elements in c could have been inserted into the Bloom filter. 359 | */ 360 | public boolean containsAll(Collection c) { 361 | for (E element : c) 362 | if (!contains(element)) 363 | return false; 364 | return true; 365 | } 366 | 367 | /** 368 | * Read a single bit from the Bloom filter. 369 | * @param bit the bit to read. 370 | * @return true if the bit is set, false if it is not. 371 | */ 372 | public boolean getBit(int bit) { 373 | return bitset.get(bit); 374 | } 375 | 376 | /** 377 | * Set a single bit in the Bloom filter. 378 | * @param bit is the bit to set. 379 | * @param value If true, the bit is set. If false, the bit is cleared. 380 | */ 381 | public void setBit(int bit, boolean value) { 382 | bitset.set(bit, value); 383 | } 384 | 385 | /** 386 | * Return the bit set used to store the Bloom filter. 387 | * @return bit set representing the Bloom filter. 388 | */ 389 | public BitSet getBitSet() { 390 | return bitset; 391 | } 392 | 393 | /** 394 | * Returns the number of bits in the Bloom filter. Use count() to retrieve 395 | * the number of inserted elements. 396 | * 397 | * @return the size of the bitset used by the Bloom filter. 398 | */ 399 | public int size() { 400 | return this.bitSetSize; 401 | } 402 | 403 | /** 404 | * Returns the number of elements added to the Bloom filter after it 405 | * was constructed or after clear() was called. 406 | * 407 | * @return number of elements added to the Bloom filter. 408 | */ 409 | public int count() { 410 | return this.numberOfAddedElements; 411 | } 412 | 413 | /** 414 | * Returns the expected number of elements to be inserted into the filter. 415 | * This value is the same value as the one passed to the constructor. 416 | * 417 | * @return expected number of elements. 418 | */ 419 | public int getExpectedNumberOfElements() { 420 | return expectedNumberOfFilterElements; 421 | } 422 | 423 | /** 424 | * Get expected number of bits per element when the Bloom filter is full. This value is set by the constructor 425 | * when the Bloom filter is created. See also getBitsPerElement(). 426 | * 427 | * @return expected number of bits per element. 428 | */ 429 | public double getExpectedBitsPerElement() { 430 | return this.bitsPerElement; 431 | } 432 | 433 | /** 434 | * Get actual number of bits per element based on the number of elements that have currently been inserted and the length 435 | * of the Bloom filter. See also getExpectedBitsPerElement(). 436 | * 437 | * @return number of bits per element. 438 | */ 439 | public double getBitsPerElement() { 440 | return this.bitSetSize / (double)numberOfAddedElements; 441 | } 442 | } -------------------------------------------------------------------------------- /src/main/java/org/fordes/adfs/util/Util.java: -------------------------------------------------------------------------------- 1 | package org.fordes.adfs.util; 2 | 3 | import jakarta.annotation.Nonnull; 4 | import jakarta.annotation.Nullable; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.fordes.adfs.model.Rule; 7 | import org.springframework.util.ObjectUtils; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.function.BiConsumer; 14 | import java.util.function.Consumer; 15 | 16 | import static org.fordes.adfs.constant.Constants.*; 17 | import static org.fordes.adfs.constant.RegConstants.*; 18 | 19 | /** 20 | * @author fordes123 on 2022/9/19 21 | */ 22 | @Slf4j 23 | public class Util { 24 | 25 | /** 26 | * 给定字符串是否以特定前缀开始 27 | * 28 | * @param str 给定字符串 29 | * @param prefixes 前缀 30 | * @return 给定字符串是否以特定前缀开始 31 | */ 32 | public static boolean startWithAny(String str, String... prefixes) { 33 | if (!StringUtils.hasText(str) || ObjectUtils.isEmpty(prefixes)) { 34 | return false; 35 | } 36 | return Arrays.stream(prefixes).anyMatch(str::startsWith); 37 | } 38 | 39 | /** 40 | * 给定字符串是否以特定字符串开始和结束 41 | * 42 | * @param str 给定字符串 43 | * @param start 开始字符串 44 | * @param end 结束字符串 45 | * @return 给定字符串是否以特定字符串开始和结束 46 | */ 47 | public static boolean between(String str, String start, String end) { 48 | if (StringUtils.hasLength(str) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) { 49 | return str.startsWith(start) && str.endsWith(end); 50 | } 51 | return false; 52 | } 53 | 54 | /** 55 | * 截取分隔字符串之前的字符串,不包括分隔字符串
56 | * 截取不到时返回空串 57 | * 58 | * @param str 被截取的字符串 59 | * @param flag 分隔字符串 60 | * @param isLast 是否是最后一个 61 | * @return 分隔字符串之前的字符串 62 | */ 63 | public static String subBefore(String str, String flag, boolean isLast) { 64 | if (StringUtils.hasLength(str) && StringUtils.hasLength(flag)) { 65 | int index = isLast ? str.lastIndexOf(flag) : str.indexOf(flag); 66 | if (index >= 0) { 67 | return str.substring(0, index); 68 | } 69 | } 70 | return EMPTY; 71 | } 72 | 73 | /** 74 | * 截取分隔字符串之后的字符串,不包括分隔字符串
75 | * 截取不到时返回空串 76 | * 77 | * @param content 被截取的字符串 78 | * @param flag 分隔字符串 79 | * @param isLast 是否是最后一个 80 | * @return 分隔字符串之后的字符串 81 | */ 82 | public static String subAfter(String content, String flag, boolean isLast) { 83 | if (StringUtils.hasLength(content) && StringUtils.hasLength(flag)) { 84 | int index = isLast ? content.lastIndexOf(flag) : content.indexOf(flag); 85 | if (index >= 0) { 86 | return content.substring(index + flag.length()); 87 | } 88 | } 89 | return EMPTY; 90 | } 91 | 92 | /** 93 | * 截取分隔字符串之间的字符串,不包括分隔字符串
94 | * 截取不到时返回空串 95 | * 96 | * @param content 被截取的字符串 97 | * @param start 开始分隔字符串 98 | * @param end 结束分隔字符串 99 | * @return 分隔字符串之间的字符串 100 | */ 101 | public static String subBetween(String content, String start, String end) { 102 | if (StringUtils.hasLength(content) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) { 103 | int startIndex = content.indexOf(start); 104 | int endIndex = content.lastIndexOf(end); 105 | if (startIndex >= 0 && endIndex > 0 && startIndex < endIndex) { 106 | return content.substring(startIndex + start.length(), endIndex); 107 | } 108 | } 109 | return EMPTY; 110 | } 111 | 112 | /** 113 | * 切分字符串并移除空项 114 | * 115 | * @param str 待切分字符串 116 | * @param flag 分隔符 117 | * @return 切分后的字符串 118 | */ 119 | public static List splitIgnoreBlank(String str, String flag) { 120 | if (!StringUtils.hasLength(str) || !StringUtils.hasLength(flag)) { 121 | return List.of(); 122 | } 123 | return Arrays.stream(str.split(flag)) 124 | .filter(e -> !e.isBlank()) 125 | .toList(); 126 | } 127 | 128 | /** 129 | * 给定字符串是等于任一字符串 130 | * 131 | * @param str 给定字符串 132 | * @param values 任意字符串 133 | * @return 给定字符串是等于任一字符串 134 | */ 135 | public static boolean equalsAny(String str, String... values) { 136 | if (!StringUtils.hasLength(str) || ObjectUtils.isEmpty(values)) { 137 | return false; 138 | } 139 | return Arrays.asList(values).contains(str); 140 | } 141 | 142 | /** 143 | * 解析hosts规则,如不是则返回null 144 | * 145 | * @param content 待解析字符串 146 | * @return {@link Map.Entry} key:ip, value:域名 147 | */ 148 | public static @Nullable Map.Entry parseHosts(String content) { 149 | if (content.contains(TAB)) { 150 | content = content.replace(TAB, WHITESPACE); 151 | } 152 | List list = splitIgnoreBlank(content, WHITESPACE); 153 | if (list.size() == 2) { 154 | String ip = list.get(0).trim(); 155 | String domain = list.get(1).trim(); 156 | 157 | if (PATTERN_IP.matcher(ip).matches() && PATTERN_DOMAIN.matcher(domain).matches()) { 158 | return Map.entry(ip, domain); 159 | } 160 | } 161 | return null; 162 | } 163 | 164 | /** 165 | * 休眠线程,忽略中断异常 166 | * 167 | * @param millis 休眠时间,毫秒 168 | */ 169 | public static void sleep(long millis) { 170 | if (millis > 0L) { 171 | try { 172 | Thread.sleep(millis); 173 | } catch (InterruptedException ignored) { 174 | } 175 | } 176 | } 177 | 178 | /** 179 | * 转换相对路径为绝对路径 180 | * 181 | * @param path 路径 182 | * @return 规范化后的路径 183 | */ 184 | public static String normalizePath(@Nonnull String path) { 185 | 186 | boolean isAbsPath = '/' == path.charAt(0) || PATTERN_PATH_ABSOLUTE.matcher(path).matches(); 187 | 188 | if (!isAbsPath) { 189 | if (path.startsWith(DOT)) { 190 | path = path.substring(1); 191 | } 192 | if (path.startsWith(FILE_SEPARATOR)) { 193 | path = path.substring(FILE_SEPARATOR.length()); 194 | } 195 | path = ROOT_PATH + FILE_SEPARATOR + path; 196 | } 197 | return path; 198 | } 199 | 200 | 201 | public static void isBaseRule(String content, BiConsumer ifPresent, Consumer orElse) { 202 | String temp = content; 203 | if (temp.contains(ASTERISK)) { 204 | temp = content.replace(ASTERISK, A); 205 | } 206 | 207 | if (temp.startsWith(DOT)) { 208 | temp = temp.substring(1); 209 | } 210 | 211 | if (temp.endsWith(DOT)) { 212 | temp = temp.substring(0, temp.length() - 1); 213 | } 214 | 215 | if (PATTERN_DOMAIN.matcher(temp).matches()) { 216 | ifPresent.accept(content, content.equals(temp) ? Rule.Type.BASIC : Rule.Type.WILDCARD); 217 | } else if (DOMAIN_PART.matcher(temp).matches()) { 218 | ifPresent.accept(content, Rule.Type.WILDCARD); 219 | } else { 220 | orElse.accept(content); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | org.fordes.adfs: info 4 | 5 | application: 6 | config: 7 | domain-detect: 8 | enable: true 9 | concurrency: 64 10 | timeout: 2000 11 | provider: 12 | - 223.5.5.5 13 | rule: 14 | remote: 15 | - name: AdGuard 基础过滤器 16 | path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_Base/filter.txt 17 | type: easylist 18 | 19 | - name: Loyalsoldier/clash-rules 广告域名列表 20 | path: https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt 21 | type: clash 22 | 23 | - name: 1Hosts lite 24 | path: https://o0.pages.dev/Lite/hosts.txt 25 | type: hosts 26 | 27 | - name: anti-ad smartdns 28 | path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-smartdns.conf 29 | type: smartdns 30 | 31 | - name: 32 | path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/adblock-for-dnsmasq.conf 33 | type: dnsmasq 34 | 35 | output: 36 | files: 37 | - name: easylist.txt 38 | type: easylist 39 | filter: 40 | - basic 41 | - wildcard 42 | - unknown 43 | 44 | - name: dns.txt 45 | type: easylist 46 | file-header: | 47 | Test 48 | filter: 49 | - basic 50 | - wildcard 51 | 52 | - name: modify.txt 53 | type: easylist 54 | filter: 55 | - unknown 56 | 57 | - name: dnsmasq.conf 58 | type: dnsmasq 59 | 60 | - name: clash.yaml 61 | type: clash 62 | 63 | - name: smartdns.conf 64 | type: smartdns 65 | 66 | - name: hosts 67 | type: hosts 68 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: ad-filters-subscriber 4 | profiles: 5 | active: dev #切换至 dev 即可输出调试信息 6 | 7 | logging: 8 | file: 9 | path: ./logs 10 | 11 | application: 12 | config: 13 | expected_quantity: 2000000 #预期规则数量 14 | fault_tolerance: 0.001 #容错率 15 | warn_limit: 6 #警告阈值, 原始规则长度小于该值时会输出警告日志 16 | 17 | # 域名检测,启用时将进行解析以验证域名有效性 18 | # 注意: 开启此功能可能导致处理时间大幅延长 19 | domain-detect: 20 | enable: false 21 | timeout: 10 22 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | ${AnsiColor.BRIGHT_GREEN} █████╗ ██████╗ ███████╗ ███████╗ 3 | ${AnsiColor.BRIGHT_GREEN} ██╔══██╗ ██╔══██╗ ██╔════╝ ██╔════╝ 4 | ${AnsiColor.BRIGHT_GREEN} ███████║ ██║ ██║ █████╗ ███████╗ 5 | ${AnsiColor.BRIGHT_GREEN} ██╔══██║ ██║ ██║ ██╔══╝ ╚════██║ 6 | ${AnsiColor.BRIGHT_GREEN} ██║ ██║ ██████╔╝ ██║ ███████║ 7 | ${AnsiColor.BRIGHT_GREEN} ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚══════╝ 8 | ${AnsiColor.BRIGHT_CYAN} :: Application :: ${AnsiColor.BRIGHT_RED}${spring.application.name} 9 | ${AnsiColor.BRIGHT_CYAN} :: Developers :: ${AnsiColor.BRIGHT_RED}fordes123 10 | ${AnsiColor.BRIGHT_CYAN} :: Github :: ${AnsiColor.BRIGHT_RED}https://github.com/fordes123/ad-filters-subscriber${AnsiColor.BRIGHT_WHITE} 11 | -------------------------------------------------------------------------------- /src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%10.10t]){faint} %clr(%-40.40logger{39}){cyan} %clr(%-6L){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx} 19 | UTF-8 20 | 21 | 22 | 23 | 24 | 25 | ${LOG_HOME}/${APP_NAME}.log 26 | 27 | 28 | ${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log 29 | 30 | 30 31 | 3GB 32 | 33 | true 34 | 35 | %d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx} 36 | UTF-8 37 | 38 | 39 | 40 | 41 | true 42 | 4096 43 | true 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | --------------------------------------------------------------------------------