Repository: afwfv/DD-AD Branch: main Commit: 8564f80644f0 Files: 34 Total size: 103.5 KB Directory structure: gitextract_rl4mzeuj/ ├── .devcontainer/ │ ├── Dockerfile │ └── devcontainer.json ├── .github/ │ └── workflows/ │ └── auto-update.yml ├── .gitignore ├── LICENSE ├── README.md ├── config/ │ ├── application-example.yaml │ └── application.yaml ├── pom.xml ├── rule/ │ └── DD-AD.txt └── src/ └── main/ ├── java/ │ └── org/ │ └── fordes/ │ └── adfs/ │ ├── AdFSApplication.java │ ├── config/ │ │ └── AdFSProperties.java │ ├── constant/ │ │ ├── Constants.java │ │ └── RegConstants.java │ ├── enums/ │ │ ├── HandleType.java │ │ └── RuleSet.java │ ├── handler/ │ │ ├── Parser.java │ │ ├── Tracker.java │ │ ├── dns/ │ │ │ └── DnsDetector.java │ │ ├── fetch/ │ │ │ ├── Fetcher.java │ │ │ ├── HttpFetcher.java │ │ │ └── LocalFetcher.java │ │ └── rule/ │ │ ├── ClashHandler.java │ │ ├── DnsmasqHandler.java │ │ ├── EasylistHandler.java │ │ ├── Handler.java │ │ ├── HostsHandler.java │ │ └── SmartdnsHandler.java │ ├── model/ │ │ └── Rule.java │ └── util/ │ └── Util.java └── resources/ ├── application-dev.yml ├── application.yml ├── banner.txt └── logback-spring.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ # [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 11, 17, 11-bullseye, 17-bullseye, 11-buster, 17-buster ARG VARIANT=21-bullseye FROM mcr.microsoft.com/vscode/devcontainers/java:${VARIANT} # [Option] Install Maven ARG INSTALL_MAVEN="true" ARG MAVEN_VERSION="3.9.0" # [Option] Install Gradle ARG INSTALL_GRADLE="false" ARG GRADLE_VERSION="" RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 ARG NODE_VERSION="none" RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends # [Optional] Uncomment this line to install global node packages. # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "name": "CodeSpace", "dockerFile": "Dockerfile", "extensions": [ "vscjava.vscode-java-pack", "vscjava.vscode-java-debug", "vscjava.vscode-java-test", "vscjava.vscode-spring-boot-dashboard", "vmware.vscode-spring-boot", "vscjava.vscode-maven" ] } ================================================ FILE: .github/workflows/auto-update.yml ================================================ name: Update Filters on: push: paths-ignore: - 'README.md' - 'README_en.md' - '.github/**' - '.devcontainer/**' - 'LICENSE' schedule: - cron: 0 */8 * * * workflow_dispatch: inputs: profile: required: false default: 'prod' type: string description: 'application running profile' should-commit: required: false default: true type: boolean description: 'Whether to commit output files' release-branch: required: false default: 'release' type: string description: 'Release branch name, if should-commit is false, this parameter will be ignored' env: TZ: Asia/Shanghai jobs: Update_Filters: runs-on: ubuntu-latest permissions: contents: write actions: write steps: - name: Checkout uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} - name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' cache: maven - name: Set default values id: defaults run: | echo "profile=${{ github.event.inputs.profile || 'prod' }}" >> $GITHUB_OUTPUT echo "should-commit=${{ github.event.inputs.should-commit || 'true' }}" >> $GITHUB_OUTPUT echo "release-branch=${{ github.event.inputs.release-branch || 'release' }}" >> $GITHUB_OUTPUT - name: Run Jar run: mvn spring-boot:run -Dspring-boot.run.profiles=${{ steps.defaults.outputs.profile }} - name: Archive tracking log if: hashFiles('logs/tracking.list') != '' uses: actions/upload-artifact@v4 with: name: tracking-log path: logs/tracking.list - name: Clean up if: steps.defaults.outputs.should-commit == 'true' run: | find . -maxdepth 1 ! -name 'rule' ! -name '.' ! -name '.git' -exec rm -rf {} + mv rule/* . rm -rf rule - name: Set current date id: date if: steps.defaults.outputs.should-commit == 'true' run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT - name: Commit Changes id: commit if: steps.defaults.outputs.should-commit == 'true' uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: 'chore: ✅ auto release at ${{ steps.date.outputs.date }}' branch: ${{ steps.defaults.outputs.release-branch }} skip_dirty_check: true push_options: '--force' create_branch: true skip_fetch: true skip_checkout: true commit_options: '--allow-empty' - name: Delete Workflow Runs uses: Mattraks/delete-workflow-runs@v2 with: token: ${{secrets.GITHUB_TOKEN}} repository: ${{ github.repository }} retain_days: 1 keep_minimum_runs: 1 ================================================ FILE: .gitignore ================================================ # Compiled class file *.class # Log file *.log # BlueJ files *.ctxt # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* .idea target src/test ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 以谶 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # DD-AD ## [***✈️ tg 频道***](https://t.me/DDadsss)
[***🐧 QQ群***](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=z4tq1QhIHdGOX6PslCFBqDRBqH6WGfXb&authKey=Inrcu9LZL6G6%2F26qpdxo9WEAw0nQuJ%2FpIqGuKsrX1kOgSVSZRQkyLxqfvKoJDlEB&noverify=0&group_code=666178576) ## 🎯 ***规则订阅*** > AdGuard客户端(软件、扩展)、AdBlock、AdBlockPlus、uBlock Origin 推荐使用:`easylist`
> AdGuardHome 推荐使用:`dns.txt`
> AdAway 等其他仅支持 hosts 的工具,推荐使用:`hosts.txt` | 文件 | 说明 | github | ghproxy | jsdelivr | |-----------------|:----------------------------|:--------------------:|:------------------------:|:-------------------------:| | `easylist.txt` | 完整主规则 | [订阅][easylist-raw] | [订阅][easylist-ghproxy] | [订阅][easylist-jsdelivr] | | `modify.txt` | 不含 DNS 过滤规则的 `easylist.txt` | [订阅][modify-raw] | [订阅][modify-ghproxy] | [订阅][modify-jsdelivr] | | `dns.txt` | 仅含 DNS 过滤规则的 `easylist.txt` | [订阅][dns-raw] | [订阅][dns-ghproxy] | [订阅][dns-jsdelivr] | | `dnsmasq.conf` | dnsmasq 及其衍生版本 | [订阅][dnsmasq-raw] | [订阅][dnsmasq-ghproxy] | [订阅][dnsmasq-jsdelivr] | | `clash.yaml` | clash 及其衍生版本 | [订阅][clash-raw] | [订阅][clash-ghproxy] | [订阅][clash-jsdelivr] | | `smartdns.conf` | smartdns | [订阅][smartdns-raw] | [订阅][smartdns-ghproxy] | [订阅][smartdns-jsdelivr] | | `hosts` | 几乎所有操作系统原生支持 | [订阅][hosts-raw] | [订阅][hosts-ghproxy] | [订阅][hosts-jsdelivr] | | `DD-AD.txt` | 本仓库维护的私有规则,以 easylist 形式提供 | [订阅][DD-AD-raw] | [订阅][DD-AD-ghproxy] | [订阅][DD-AD-jsdelivr] | [easylist-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/easylist.txt [easylist-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/easylist.txt [easylist-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/easylist.txt [modify-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/modify.txt [modify-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/modify.txt [modify-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/modify.txt [dns-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dns.txt [dns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dns.txt [dns-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/dns.txt [dnsmasq-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dnsmasq.conf [dnsmasq-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/dnsmasq.conf [dnsmasq-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/dnsmasq.conf [clash-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/clash.yaml [clash-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/clash.yaml [clash-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/clash.yaml [smartdns-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/smartdns.conf [smartdns-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/smartdns.conf [smartdns-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/smartdns.conf [hosts-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/hosts [hosts-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/hosts [hosts-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/hosts [DD-AD-raw]: https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/DD-AD.txt [DD-AD-ghproxy]: https://ghproxy.net/https://raw.githubusercontent.com/afwfv/DD-AD/refs/heads/release/DD-AD.txt [DD-AD-jsdelivr]: https://gcore.jsdelivr.net/gh/afwfv/DD-AD@refs/heads/release/DD-AD.txt ### 说明 广告过滤规则整合,使用 *[ad-filters-subscriber](https://github.com/fordes123/ad-filters-subscriber)* 1.**针对番茄小说广告添加了规则** 2.**针对七猫小说广告添加了规则** 3.**私人dns:dd.afwfv.cn** ================================================ FILE: config/application-example.yaml ================================================ ########################################## ## !!! 示例配置,修改无效 !!! ## !!! example config, modify invalid !!! ########################################## application: # 规则源配置 # !使用前请删除下方示例配置, 注意缩进 input: - name: 'Subscription 1' #必要参数: 规则名称,请勿重复 path: 'https://example.org/rule.txt' #必要参数: 规则 url (http/https) 或 本地文件位置 (绝对/相对路径) type: easylist #可选参数: 规则类型:easylist (默认)、dnsmasq、clash、smartdns、hosts - name: 'Subscription 2' path: 'rule/local.txt' type: hosts #输出配置 output: #文件头配置,将自动作为注释添加至每个规则文件开始 #可使用占位符 ${name}、${type}、${desc} 以及 ${date} (当前日期)、${total} (规则总数) file_header: | ADFS AdBlock ${type} Last Modified: ${date} Total Size: ${total} Homepage: https://github.com/fordes123/ad-filters-subscriber/ #输出规则文件列表 (!注意缩进,且每个类型只能输出一个文件) files: - name: easylist.txt #必要参数: 文件名 type: easylist #必要参数: 文件类型: easylist、dnsmasq、clash、smartdns、hosts file_header: #可选参数: 文件头配置,将自动作为注释添加至每个规则文件开始 (此处优先于 output.file_header) desc: 'ADFS EasyList' #可选参数: 文件描述,可在file_header中通过 ${} 中使用 filter: - basic #基本规则,不包含任何控制、匹配符号, 可以转换为 hosts - wildcard #通配规则,仅使用通配符 - unknown #其他规则,如使用了正则、高级修饰符号的规则,这些规则目前无法转换为其他格式 rule: #可选参数: 限定此文件使用的规则源,如果不指定则使用 input 中的所有规则源 - Subscription 1 - Subscription 2 - name: dns.txt type: easylist filter: - basic - wildcard - name: dnsmasq.txt type: dnsmasq - name: clash.yaml type: clash - name: smartdns.txt type: smartdns - name: hosts.txt type: hosts # 程序运行配置(可选) # !删除即使用默认配置 config: expected_quantity: 2000000 #预期规则数量 fault_tolerance: 0.001 #容错率 warn_limit: 6 #警告阈值, 原始规则长度小于该值时会输出警告日志 # 域名检测,启用时将进行解析以验证域名有效性 # !开启此功能可能导致处理时间大幅延长 # !仅支持特定规则,具体原因请查看项目主页说明 domain-detect: enable: false # 是否启用 timeout: 2000 # 查询超时(毫秒) concurrency: 10 # 并发查询数 cache-ttl-min: 600 # 缓存最小TTL(秒) cache-ttl-max: 86400 # 缓存最大TTL(秒) cache-negative-ttl: 300 # 失败结果缓存TTL(秒) provider: # 使用的 DNS 服务器,仅支持 IP,多个之间具备优先级; 为空则使用默认DNS - 223.5.5.5 # 排除指定内容 # 在解析结束后,规则内容命中此列表将直接认定为无效 exclude: - "www.example.org" # 通过文件收集程序认定无效的规则,用于排查问题 tracking: enable: true path: logs/tracking.list ================================================ FILE: config/application.yaml ================================================ # 参考 `application-example.yaml` 并在处添加订阅规则,请注意缩进 # Refer to `application-example.yaml` and add subscription rules there,please note the indentation application: config: domain-detect: enable: true timeout: 3000 concurrency: 128 provider: - 1.1.1.1 - 8.8.8.8 exclude: # 屏蔽此域会导致Edge浏览器翻译不可用 - "edge.microsoft.com" # 屏蔽此域会导致小米天气网络异常 - "metok.sys.miui.com" # 屏蔽此域会导致10086 App登陆异常 - "client.app.coc.10086.cn" # 屏蔽此域会导致MSN页面无法正常加载 - "assets.msn.com" rule: remote: - name: AdGuard 基础过滤器 path: https://filters.adtidy.org/extension/ublock/filters/2_optimized.txt type: easylist - name: AdGuard 移动广告过滤器 path: https://filters.adtidy.org/extension/ublock/filters/11_optimized.txt type: easylist - name: AdGuard 中文过滤器 path: https://filters.adtidy.org/extension/ublock/filters/224_optimized.txt type: easylist - name: NoAppDownload path: https://raw.githubusercontent.com/Noyllopa/NoAppDownload/master/NoAppDownload.txt type: easylist - name: xndeye web-ad-rule path: https://raw.githubusercontent.com/xndeye/web-ad-rule/master/easylist.txt type: easylist - name: xinggsf movie path: https://raw.githubusercontent.com/xinggsf/Adblock-Plus-Rule/master/mv.txt - name: cjxlist annoyance path: https://raw.githubusercontent.com/cjx82630/cjxlist/master/cjx-annoyance.txt - name: ABP filters path: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt - name: ABP 反广告过滤 path: https://easylist-downloads.adblockplus.org/abp-filters-anti-cv.txt - name: uBlockOrigin privacy path: https://raw.githubusercontent.com/uBlockOrigin/uAssets/refs/heads/master/filters/privacy.txt local: - name: 'DD-AD' path: 'rule/DD-AD.txt' type: easylist output: file_header: | Title: DD-AD of ${type} Last Modified: ${date} Homepage: https://github.com/afwfv/DD-AD files: - name: easylist.txt type: easylist filter: - basic - wildcard - unknown - name: dns.txt type: easylist file_header: | Title: DD-AD Last Modified: ${date} Homepage: https://github.com/afwfv/DD-AD filter: - basic - wildcard - name: modify.txt type: easylist filter: - unknown - name: dnsmasq.conf type: dnsmasq - name: clash.yaml type: clash - name: smartdns.conf type: smartdns - name: hosts type: hosts ================================================ FILE: pom.xml ================================================ ad-filters-subscriber org.fordes AD Filters Subscriber 4.0.0 ad-filters-subscriber 1.4.0 21 UTF-8 UTF-8 3.5.7 3.12.1 org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-validation com.github.alexandrnikitin bloom-filter_2.13 0.13.1 org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} ${java.version} ${java.version} UTF-8 -parameters org.springframework.boot spring-boot-maven-plugin ${spring-boot.version} repackage repackage src/main/resources/application.yml src/main/resources true ================================================ FILE: rule/DD-AD.txt ================================================ ! Title: AFWFV DD-AD list of DD-AD ! Last Modified: 2025-11-05 00:00:00 ! Homepage: https://github.com/afwfv/DD-AD/ ! Description: 对本仓库上游规则的补充与修正, 不定期更新. !-------------白名单--------------- #图床 @@||pic.imgdb.cn #微信支付 @@||payapp.weixin.qq.com^ #夸克网盘 @@||pan.quark.cn^ @@||drive.quark.cn^ #夸克首页 @@||myquark.cn^ #小米骚扰拦截 数据库更新 @@||api.sec.miui.com^ #小米推送(mipush) @@||cn.app.chat.xiaomi.net^ #个推 @@||sdk.open.talk.gepush.com^ #公共CDN @@||bootcdn.net^ #斗鱼 @@||msg.douyu.com^ @@||bjfesdk.douyucdn.cn^ @@||gift.douyucdn.cn^ #360 @@||360.cn^ @@down.360safe.com^ #酷安 @@||316.coolapk1s.com^ @@||api.coolapk.com^ @@||avatar.coolapk.com^ @@||umengjmacs.m.taobao.com^ #抖音网页 @@||lf3-cdn-tos.bytegoofy.com^ @@||lf6-cdn-tos.bytegoofy.com^ @@||lf1-cdn-tos.bytegoofy.com^ #京东 @@||m.jingxi.com^ @@||gia.jd.com^ #笔趣阁 @@||www.bqgyy.com^ #皮皮虾pro @@||is.snssdk.com^ @@||ib.snssdk.com^ #qq @@||c.pc.qq.com^ #163 ||dl.reg.163.com^ #放行log文件,减少重复请求造成的发热 @@||log-api.pangolin-sdk-toutiao-b.com^ @@||log-api.pangolin-sdk-toutiao.com^ @@||dig.lianjia.com^ ##豆包 @@||lf-c-flwb.bytetos.com^ @@||p3-search.byteimg.com^ @@||p3-flow-imagex-sign.byteimg.com^ @@||p6-flow-imagex-sign.byteimg.com^ @@||p9-flow-imagex-sign.byteimg.com^ ##今日头条 @@||mssdk3-normal-lf.zijieapi.com^ @@||www.toutiao.com^ @@||m.toutiao.com^ @@||app.toutiao.com^ @@||lf3-short.ibytedapm.com^ @@||ttwid.bytedance.com^ @@||lf3-cdn2-tos.bytescm.com^ @@||lf*-cdn-tos.bytegoofy.com^ @@||*ad*.ksyungslb.com^ @@||p*-img.searchpstatp.com^ @@||lf*material.searchpstatp.com^ @@||p*.toutiaoimg.com^ ##百度云盘 @@||staticsns.cdn.bcebos.com^ @@||tcbox.baidu.com^ @@||nsclick.baidu.com^ ##谷歌广告 ||tpc.googlesyndication.com^ ##虎牙直播 @@||ylog.huya.com^ ##懂车帝 @@||p1-dcd.byteimg.com^ @@||p3-dcd.byteimg.com^ @@||p9-dcd.byteimg.com^ @@||p3-dcd-sign.byteimg.com^ @@||p6-dcd-sign.byteimg.com^ @@||p9-dcd-sign.byteimg.com^ @@||open.bytedanceapi.com^ ##百度搜索 @@||ms.bdimg.com^ @@||feed-image.baidu.com^ ##咪咕视频 @@||v.miguvideo.com^ ##Appshare @@||edu-image.nosdn.127.net^ ##快看视频 @@||yanxuan.nosdn.127.net^ ##中国移动 @@||qwhdcdn.cmcc-cs.cn^ ##Omofun @@||pic2.zhimg.com^ @@||lf6-imcloud-file-sign.bytetos.com^ @@||lf3-imcloud-file-sign.bytetos.com^ ##巴哈姆特動畫瘋 @@||data.flurry.com^ ##飞猪 @@||cdn.ynuf.aliapp.org^ ##酷安 @@||necaptcha.nosdn.127.net^ ##微信 @@||cm-10-*.getui.com^ ##网易企业邮箱 @@||mimg.127.net^ ##QQ @@||qzonestyle.gtimg.cn^ @@||tianshu.gtimg.cn^ ##QQ音乐 @@||i2.y.qq.com^ ##百度输入法 @@||res.mi.baidu.com^ @@||www.paypalobjects.com^ ##抖音 @@||auth.zijieapi.com^ @@||aweme.snssdk.com^ @@||v*.douyinvod.com^ @@||lf3-cdn-tos.bytegoofy.com^ @@||saveu*-normal-lf.zijieapi.com^ @@||lf-webcast-gr-sourcecdn.bytegecko.com^ @@||lf*-webcast-cdn-tos-ncdn.bytegecko.com^ @@||n*-lf-webcast-cdn-tos-ncdn.bytegecko.com^ @@||n*-lf*-dy-ncdn-tos-sub.bytegecko.com^ @@||lf*-dy-cdn-tos.bytegecko.com^ @@||lf*-static.bytednsdoc.com^ @@||lf-webcast-sourcecdn-tos.bytegecko.com^ @@||lf*-webcast-cdn-tos.bytegecko.com^ @@||lf*-webcastcdn-tos.douyinstatic.com^ @@||ma*-normal-lf.zijieapi.com^ @@||vcs.zijieapi.com^ @@||pitaya.bytedance.com^ @@||abtest3-misc-lf.zijieapi.com^ @@||p*-piu.byteimg.com^ @@||p*-ecom-fe.byteimg.com^ @@||p*-ecop-imagex.byteimg.com^ @@||p*-im.byteimg.com^ @@||p9-dy.byteimg.com @@||p6-dy-ipv6.byteimg.com^ @@||p3-notice.byteimg.com^ @@||f-cpay.snssdk.com^ @@||f-cashloan.snssdk.com^ @@||p3-cpay.byteimg.com^ @@||p3-insurance.byteimg.com^ @@||lf*-infras.bytetos.com^ @@||lf1-cdn-tos.bytescm.com^ @@||survey.feelgood.cn^ @@||p*-sdbk2-media.byteimg.com^ @@||vcs-quality.zijieapi.com^ @@||lf*-baike.searchpstatp.com^ ##乐播投屏 @@||tvapp.hpplay.cn^ @@||h5.hpplay.com.cn^ @@||sl.hpplay.cn^ @@||conf.hpplay.cn^ ##菜鸟裹裹 @@||wgo.mmstat.com^ @@||gm.mmstat.com^ @@||cainiao-app-logs.cn-zhangjiakou.log.aliyuncs.com^ @@||na61-na62.wagbridge.alibaba.tanx.com^ ##快手 @@||d*.static.yximgs.com^ @@||d*-magic.static.yximgs.com^ @@||bs-origin.pull.yximgs.com^ @@||live*.static.yximgs.com^ @@||*m.chenzhongtech.com^ @@||js-gamezone.static.yximgs.com^ @@||ali-gamezone.static.yximgs.com^ ##番茄小说 @@||p*-novel.byteimg.com^ @@||p*-novel-sign.byteimg.com^ ##皮皮虾 @@||p*-ppx-sign.byteimg.com^ @@||p*-ppx-report.byteimg.com^ @@||saveu.zijieapi.com^ @@||vas.snssdk.com^ @@||bgp.video-upload-v*.lf.bytelb.net^ @@||lf-cdn-tos.bytescm.com^ ##阿里云 @@||click.aliyun.com^ ##闲鱼 @@||dinamicx.alibabausercontent.com^ ##今日头条极速版 @@||lf3-search.searchpstatp.com^ @@||monsetting.toutiao.com^ @@||gecko.zijieapi.com^ @@||polaris.zijieapi.com^ @@||tnc3-bjlgy.zijieapi.com^ ##囧次元 @@||wxsnsdy.wxs.qq.com^ ##BT1207 @@||lf3-cdn-tos.bytecdntp.com^ ##原神 @@||ad-log-upload.mihoyo.com^ ##菠菜 @@||meiqia.com^ ##当鸟动漫 @@||mooc-image.nosdn.127.net^ ##头条PRO @@||ib.snssdk.com^ @@||ic.snssdk.com^ @@||is.snssdk.com^ ##360防骚扰大师 @@||s.mvconf.f.360.cn^ @@||scan.call.f.360.cn^ @@||shouji.360.cn^ ##淘宝 ||s.click.taobao.com^ ##樱花动漫 @@||v16m-default.akamaized.net^ ##快手极速版 @@||v*.kwaicdn.com^ @@||ali.static.yximgs.com^ ##360摄像机 @@||passport.360.cn^ @@||report.passport.360.cn^ @@||app.home.360.cn^ @@||jia.360.cn^ @@||fastconn-api.iot.360.cn^ @@||speed.live.360.cn^ @@||cloudcontrol.live.360.cn^ @@||sdk.live.360.cn^ @@||mdm.openapi.360.cn^ @@||dp.push.dc.360.cn^ @@||mall.360.cn^ ##夸克 @@||download.uc.cn^ @@||soutiapi.quark.cn^ @@||souti-ims.sm.cn^ ##蓝奏 @@||develope-oss.lanzoug.com^ ##网易有道词典 @@||ydlunacommon-cdn.nosdn.127.net^ @@||luna-dict-community.nosdn.127.net^ @@||foundation.youdao.com^ ##火山引擎 @@||portal.volccdn.com^ ##360扫地机 @@||smart.360.cn^ ##有柿 @@||p3-iz-e-sign.byteimg.com^ ##淘小说 @@||tybook.taoyuewenhua.net^ ##火山翻译 @@||p3-scmimg.bytescm.com^ @@||ipolyfill-origin.bytedance.com^ @@||lf3-cdn-tos.bytescm.com^ ##快影 @@||ali-ky.static.yximgs.com^ ##剪映 @@||p*-faceu-img-sign.byteimg.com^ @@||p*-passport.byteacctimg.com^ ##网易严选 @@||yanxuan-dl.nosdn.127.net^ @@||yanxuan-item.nosdn.127.net^ ##网易邮箱大师 @@||onegoods.nosdn.127.net^ ##Faceu激萌 @@||effect.snssdk.com^ ##风行视频 @@||cp2-cdn-play.funshion.com^ @@||img.funshion.com^ @@||img*.funshion.com^ @@||puser.funshion.com^ @@||pvip.funshion.com^ @@||po.funshion.com^ @@||pm.funshion.com^ ##酷我音乐 @@||search.kuwo.cn^ @@||mobilist.kuwo.cn^ @@||ncomment.kuwo.cn^ @@||vip1.kuwo.cn^ ##钉钉 @@||adash-emas.cn-hangzhou.aliyuncs.com^ !-------------黑名单-------------------- #信息流及评论区广告 ||s2s.adjust.cn^ ||ctobsnssdk.com^ ||pangolin.snssdk.com^ ||ulogs.umeng.com^ ||aaid.umeng.com^ ||mssdk-bu.bytedance.com^ #帖子详情好物推荐 ||api2.coolapk.com/v6/feed/detail$replace=/"include_goods_ids":\[.*?]\,"include_goods":\[.*?]\,/ / #帖子详情赞助内容 ||api2.coolapk.com/v6/feed/detail$replace=/\,"detailSponsorCard":{.*}/}}/ #发现页去除酷品 ||api2.coolapk.com/v6/main/init$replace=/{"id":1170.*?}\,/ / #应用游戏页去除推广视频 ||api2.coolapk.com/v6/page/dataList$replace=/{"entityType":"card"\,"entityTemplate":"apkImageCard".*?\\u0022}"}\,/ / #去除首页还有什么值得买推广 ||api2.coolapk.com/v6/main/indexV8$replace=/{"entityType":"card"\,"entityTemplate":"listCard".*?}"}\,/ / #夏普电视开机广告 ||flnet.com^ ||miaozhen.com^ #QQ浏览器小说广告 ||kde.qq.com^ #字节系 AD API SDK 屏蔽 /pangolin-sdk.*/ /pangolin-sdk-toutiao.*/ /pangolin-sdk-toutiao1.*/ /pglstatp-toutiao.*/ ||bsccdn.net^ ||dig.zjurl.cn^ ||dig.bdurl.net^ ||ad.zijieapi.com^ ||apps.bytesfield.com^ ||pangolin-sdk-toutiao.com^ ||pangolin-sdk-toutiao1.com^ ||pangolin-sdk-toutiao-b.com^ ||pglstatp-toutiao.com^ ||vcs.zijieapi.com^ ||ads*-normal.amemv.com^ ||ads*-normal-lq.amemv.com^ ||ads*-normal-lf.amemv.com^ ||ads*-normal-hl.amemv.com^ ||ads*-normal.zijieapi.com^ ||ads*-normal-lq.zijieapi.com^ ||ads*-normal-lf.zijieapi.com^ ||ads*-normal-hl.zijieapi.com^ ||ads*-normal-lq.zijieapi.com.w.cdngslb.com^ ||ads*-normal-lq.zijieapi.com.queniuiq.com^ ||p*-ad-sign.byteimg.com^ ||p*-ad.byteimg.com^ ||lf*-orange.byteorge.com^ ||v*-novelapp.fqnovelvod.com^ ||sf*-ttcdn-tos.pstatp.com^ ||p*-local-ads.byteimg.com.queniuso.com^ ||p*-local-ads.byteimg.com^ ||localads.chengzijianzhan.cn^ ||localads.chengzijianzhan.cn.queniurc.com^ ||ads*-normal-lf.zijieapi.com.dhcp^ #七猫免费小说 ||cdn-new-ad.wtzw.com^ ||a-remad.qm989.com^ ||open.kuaishouzt.com^ #得间 ||*-ad.wtzw.com^ ||aiclk.com^ ||smad.ms.zhangyue.net^ ||adx.ads.heytapmobi.com^ ||uapi.ads.heytapmobi.com^ ||saad.ms.zhangyue.net^ ||sentry-app.ireader.com^ #熊猫小说 ||cdn-ad.wtzw.com^ ||a-qmad.qimao.com^ #笔趣阁网页广告 ||hlryzsqo.icu^ ||scenery.hk^ ||xiedao.hk^ ||bcnsport.us^ ||ncbtsdea.xyz^ ||vjyaghdw.icu^ #书旗 ||dsp-x.jd.com^ ||xlog.jd.com^ ||api.th.wanzjhb.com^ ||janapi.jd.com^ ||partner.uc.cn^ ||mos.m.taobao.com^ ||sdk-log.partner.sm.cn^ ||opehs.tanx.com^ #OPPO ||sms.ads.heytapmobi.com^ ||cl-data.ads.heytapmobi.com^ !-----------------待定----------------- #小米浏览器热点资讯 ||hot.browser.miui.com^ #未知来源 广告/日志 ||toblog.ctobsnssdk.com^ #gamepass日志 ||catalog.gamepass.com^ #杂七杂八 ||googleads.g.doubleclick.nes^ ||ads.lingyuexiu.cn^ ||adscfg.togothermany.cn^ ||ads-marketing-vivofs.vivo.com.cn ||adsfs.heytapimage.com^ ||sdklog.adintl.cn^ ||sdk-infra.inmobi.cn^ ||sv.adintl.cn^ ||trc-sdk.inmobi.cn^ ||sdk.beizi.biz^ ||sdk.alibaba.com.ailbaba.me^ ||telemetry-sdk-metrics.inmobi.cn^ ||api-htp.beizi.biz^ ||sdkcfg.adintl.cn^ ||p1-lm.adukwai.com^ ||union.w.inmobi.cn^ ||adskp.org^ ||api.open.adsaide.cn^ ||ads-video-al.xhscdn.com^ ||ads-img-al.xhscdn.com^ ||ads.cup.com.cn^ ||bdapi.ads.heytapmobi.com^ ||adx-cn.ads.vungle.com^ ||config.ads.vungle.com^ ||ads.themoneytizer.com^ ||ads.sunmaker.com^ ||ad.letmeads.com^ ||mads.meituan.com^ ||st-adsgame.vivo.com.cn^ ||bjm.1vkx.cn^ ||img.juhagngj.xyz^ ||img.ayghbngnazh.xyz^ ||img.leiahngiuheong.xyz^ ||img.soweonglsh.xyz^ ||img.yuwhehan.xyz^ ||img.syebgng.xyz^ ||img.mjhsghnwg.xyz^ ||img.wqpamgngnb.xyz^ ||img.huanagyehgn.xyz^ ||img.shznegh.xyz^ ||img.zhangilihew.xyz^ ||yh.yanghetp.vip^ ||tu.mt20230625tu.com^ ||www.9129666tp.com^ ||line.txwlwwvvimg.com^ ||uu5512uu.com^ ||xoxo.xoxoimg.com^ ||ad-log-upload.mihoyo.com^ ||ad-trace.lenovomm.com^ ||ad-sdk.lenovomm.com^ ||p*-ad-aweme.byteimg.com^ ||ad-r.soulapp.cn^ ||v*-ad.video.yximgs.com^ ||soul-ad.soulapp.cn^ ||mkt-advertisement.tuhu.cn^ ||advert.swiftpass.cn^ ||mktadsodr.ppdai.com^ ||iadsdk.qtlcdn.com^ ||ck.ads.oppomobile.com^ ||mobads-pre-config.cdn.bcebos.com^ ||api.adserver.jvmai.com^ ||mbads.paas.cmbchina.com^ ||skadsdk.appsflyer.com^ ||cm.ads.oppomobile.com^ ||ads.chuanmeixing.com^ ||ads.bjlxin.cn^ ||sis.jpush.io^ ||ad.e.kuaishou.com^ ||ali2.a.yximgs.com^ ||open-set-api.shenshiads.com^ ||adsdkapi.dxys.pro^ ||adsdk.9imobi.com^ ||uapi-ads.vivo.com.cn^ ||adsfilebssdlbig.ali.kugou.com^ ||ad.cyapi.cn^ ||ad.tianmu.mobi^ ||multi-az-ad.kuaishou.cn^ ||ad.qingcigame.com^ ||ads.tiktok.com^ ||ads.linkedin.com^ ||advice-ads.s3.amazonaws.com^ ||ads30.adcolony.com^ ||adsfile20.bssdlbig.kugou.com^ ||adsfileretry.service.kugou.com^ ||adserver.unityads.unity3d.com^ ||adserviceretry.kglink.cn^ ||ck.ads.heytapmobi.com^ ||ads.games.laohu.com^ ||ads-drcn.platform.hihonorcloud.com^ ||ads-lb.serving-sys.cn^ ||ads.serving-sys.cn^ ||ads.uutest.cn^ ||adserver.html.it^ ||adsmind.ugdtimg.com^ ||mobads.baidu.com^ ||ads.api.fakeloc.cc^ ||api.installer.xiaomi.com^ ||applog.snssdk.com^ ||adservice.aiyouaiyou.cn^ ||ads-shopping.shouqianba.com^ ||ads.superawesome.tv^ ||ads.zhinengxiyifang.cn^ ||ads.huawei.com^ ||cross-ad-api.afunapp.com^ ||t.jmads.net^ ||adserv.ontek.com.tr^ ||optimus-ads.amap.com^ ||adashx.ut.cainiao.com^ ||image-ad.sm.cn^ ||h-adashx.ut.dingtalk.com^ ||adashx.ut.dingtalk.com^ ||v6-adashx.ut.cainiao.com^ ||api-access.pangolin-sdk-toutiao.com.queniuyk.com^ ||opencdnpddpic2.cdn.vsjwtcdn.com^ ||opencdnpddvod2.jomodns.com^ ||b1-data.ads.heytapmobi.com^ ||googleads.g.doubleclick.net^ ||stg-data.ads.heytapmobi.com^ ||is.snssdk.com.queniukw.com^ ||i.snssdk.com^ ||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com^、 ||4pn.cn^ ||4a9.cn^ ||3p4.cn^ ||y4n.cn^ ||u7x.cn^ ||aqdlt.com^ ||aqdz91.com^ ||aqdz149.com^ ||share.jileme.net^ ||settings.ttwebview.com^ ||st-ads-jssdk.vivo.com.cn^ ||xmadsdk.cos.tx.xmcdn.com^ ||ads-video-1251524319.cos.ap-shanghai.myqcloud.com^ ||line3-adscore-api.biligame.net^ ||adsmind.gdtimg.com.svideolego-dk.sched.dcloudstc.com^ ||configv2.unityads.unitychina.cn^ ||abtest3-misc-hl.zijieapi.com^ ||abtest3-misc-lq.zijieapi.com^ ||abtest3-misc-lf.zijieapi.com^ ||monitor.ads.8le8le.com^ ||leads.baidu.com^ ||x61.inyvf.top^ ||b2.ghj7l.com^ ||nosup-jd1.127.net^ ||p*-webcast-sign.douyinpic.com^ ||v6-adashx.ut.ele.me^ ||h-adashx.ut.cainiao.com^ ||h-adashx4bc.ut.taobao.com^ ||v6-adashx.ut.amap.com^ ||ad-audit-provider.soulapp.cn^ ||o2o-ad-log-gateway.alibaba.com^ ||ad-sdk-config.youdao.com^ ||m1.ad.10010.com.w.cdngslb.com^ ||business-ad.cdn-go.cn^ ||market-ad.oss-cn-shenzhen.aliyuncs.com^ ||advertising.yandex.ru^ ||advertising.apple.com^ ||advertising.yahoo.com^ ||na61-na62.wagbridge.advertisement.tanx.com^ ||ads.pinterest.com^ ||ads-api.tiktok.com^ ||partnerads-test.ysm.yahoo.net^ ||partnerads.ysm.yahoo.com^ ||adsbdfs.heytapimage.com^ ||s364-ad-canl5.nbjz.db9w.com^ ||ads3.fingersoft.net^ ||ads-sg.tiktok.com^ ||dm.kantsuu.com^ ||dm.video.weibocdn.com^ ||dm.wo.com.cn^ ||ad.mcloud.139.com^ ||ad.zhuanzhuan.com^ ||adserving-oss.bangdao-tech.com^ ||v6-novelapp-nxyd.fqnovelvod.com^ ||wcp.taobao.com^ ||ad.bczcdn.com^ ||miav-cse.avlyun.com^ ||a0.app.xiaomi.com^ ||miui-fxcse.avlyun.com^ ||ads5-normal-lq.zijieapi.com.c.dsa.cdnbuild.net^ ||adsdk.state.duoku.com^ ||ads5-normal-lf.zijieapi.com.skyworth^ ||ads3-normal-lf.zijieapi.com.skyworth^ ||ad.oceanengine.com.w.cdngslb.com^ ||ad.oceanengine.com.queniukw.com^ ||ad.oceanengine.com^ ||ad.k98y.cn^ ||ads.api.lerist.cc^ ||ads.chinaums.com^ ||ad-monitor.sh9130.com^ ||ads.ysepay.com^ ||imp.ads.rgyun.com^ ||c.ads.rgyun.com^ ||dsp.fcbox.com^ ||hugelog.fcbox.com^ ||ads.adfox.ru^ ||ad.xmmnsl.com^ ||ad-*file.myqcloud.com^ ||ads.iosappsworld.com^ ||ads.adfox.ru^ ||data-ads.oneplusmobile.com^ ||dgstatic.jd.com^ ||wdr160gz.dayugslb.com^ ||tnc3-alisc1.zijieapi.com.queniuyk.com^ ||ad.partner.gifshow.com^ ||ads.lejuliang.com^ ||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com^ ||ad-display.diwodiwo.xyz^ ||metrics-ad-drcn.dt.hihonorcloud.com^ ||ads.95516.com^ ||p10528-ad-server.ejoy.com^ ||zlsdk.1rtb.net^ ||ads.aralego.com^ ||hw-ot-ad.a.yximgs.com^ ||pmp-ad.cdn.bcebos.com^ ||market-ad.oss-accelerate.aliyuncs.com^ ||api-ad-adapter-sg.wps.com^ ||p10528-ad-server.ejoy.com^ ||o2o-ad-dev.oss-cn-shanghai.aliyuncs.com.gds.alibabadns.com^ ||metrics-ad-drcn.dt.hihonorcloud.com^ ||ad-ga.nocode.com^ ||ai.advertisement.alibabacorp.sm.cn^ ||cdn-cbd-advert-prod.shuxinyc.com^ ||websgsadvert.sanguosha.com^ ||ms-ads-monitoring-events.presage.io^ ||ssp-adx.ads.heytapmobi.com^ ||ir-sdk.dun.163.com^ ||adx.halomobi.com^ ||api.jietuhb.com^ ||adsmartgetqtt.guangtuikeji.com^ ||tracker.medproad.com^ ||adtracker.medproad.com^ ||creative.medproad.com^ ||content-understand-strategy.sm.cn^ ||lbs.netease.im^ ||ac.dun.163yun.com^ ||advert-prod.hyrz.qq.com^ ||ads.ishansong.com^ ||ads-api.twitter.com^ ||multi-az-ad.kuaishou.com^ ||sdkconfig.ad.xiaomi.com.mshome.net^ ||hw-ot-ad.a.yximgs.com^ ||ad.99jiasu.com^ ||ad.tk.cn^ ||*-ad.adbkwai.com^ ||*-ad.gskwai.com^ ||cdn.wwads.cn^ ||ae.iads.unity3d.com^ ||o.iads.unity3d.com^ ||ads.ubestkid.com^ ||ad.xunkids.com^ ||*-ad.adukwai.com^ ||ads.lejuliang.cn^ ||ads.inmobi.com^ ||sdk.1rtb.net^ ||volcsirius.com^ ||vegslb.com^ ||weilayun.com^ ||adx-bj.anythinktech.com^ ================================================ FILE: src/main/java/org/fordes/adfs/AdFSApplication.java ================================================ package org.fordes.adfs; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.config.AdFSProperties; import org.fordes.adfs.constant.Constants; import org.fordes.adfs.handler.Parser; import org.fordes.adfs.handler.rule.Handler; import org.fordes.adfs.model.Rule; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.atomic.AtomicLong; @Slf4j @Component @SpringBootApplication public class AdFSApplication { private final ApplicationContext context; private final Set inputs; private final AdFSProperties.OutputProperties output; private final Parser parser; private final Map outputMap; public record Output(AdFSProperties.OutputItem item, Path tempFile, AtomicLong count) { } public static void main(String[] args) { SpringApplication.run(AdFSApplication.class, args); } public AdFSApplication(ApplicationContext context, AdFSProperties properties, Parser parser) { this.context = context; this.parser = parser; this.inputs = properties.getInput(); this.output = properties.getOutput(); this.outputMap = new HashMap<>(properties.getOutput().files().size(), 1); properties.getOutput().files().forEach(file -> { try { Path tempFile = Files.createTempFile(file.name(), ".tmp"); this.outputMap.put(file.name(), new Output(file, tempFile, new AtomicLong(0L))); } catch (IOException e) { log.error("create temp file failed", e); this.exit(); } }); } @Bean public ApplicationRunner start() { return args -> { long start = System.currentTimeMillis(); Flux.fromIterable(inputs) .flatMap(parser::handle, 1) .flatMap(rule -> this.output(output.files(), rule)) .groupBy(Tuple2::getT1, Tuple2::getT2) .flatMap(group -> group .bufferTimeout(5000, Duration.ofSeconds(1)) .flatMap(batch -> asyncBatchWrite(group.key().name(), batch)) .subscribeOn(Schedulers.boundedElastic()) ) .then(Mono.defer(this::createOutputDirectory)) .then(Mono.defer(this::processOutputFiles)) .doOnError(ex -> { log.error("processing failed", ex); this.exit(); }) .doFinally(signal -> { log.info("all done, cost: {} ms", System.currentTimeMillis() - start); this.exit(); }) .subscribe(); }; } public Flux> output(Set outputs, Rule rule) { return Flux.fromIterable(outputs) .filter(file -> file.rule().isEmpty() || file.rule().contains(rule.getSourceName())) .filter(file -> file.filter().isEmpty() || file.filter().contains(rule.getType())) .flatMap(file -> { Handler handler = Handler.getHandler(file.type()); String content = handler.format(rule); return content != null ? Mono.just(Tuples.of(file, content)) : Mono.empty(); }); } private Mono createOutputDirectory() { return Mono.fromCallable(() -> Files.createDirectories(Path.of(output.path()))) .subscribeOn(Schedulers.boundedElastic()) .onErrorResume(ex -> { log.error("create output dir failed", ex); return Mono.error(ex); }); } private Mono processOutputFiles() { Path dir = Path.of(output.path()); return Flux.fromIterable(output.files()) .flatMap(file -> { Output opt = outputMap.get(file.name()); Path tempFile = opt.tempFile; Path targetFile = dir.resolve(file.name()); String header = buildHeader(file, output.fileHeader(), Long.toString(opt.count.get())); log.info("[{}] written completed, total size => {}", file.name(), opt.count.get()); return prependAndMove(targetFile, tempFile, header).subscribeOn(Schedulers.boundedElastic()); }) .then(); } private Mono asyncBatchWrite(String fileName, List batch) { Output opt = outputMap.get(fileName); opt.count().addAndGet(batch.size()); return asyncBatchWrite(opt.tempFile, batch); } private Mono asyncBatchWrite(Path path, List batch) { return Mono.fromCallable(() -> { Files.write(path, batch, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND); return null; }) .onErrorResume(e -> { log.error("Write failed", e); return Mono.empty(); }) .then(); } private Mono prependAndMove(Path targetFile, Path tempFile, String header) { return Mono.fromCallable(() -> { if (Files.exists(targetFile)) { Path intermediateFile = Files.createTempFile(targetFile.getFileName().toString(), ".intermediate"); try (BufferedWriter writer = Files.newBufferedWriter(intermediateFile, StandardCharsets.UTF_8); BufferedReader tempReader = Files.newBufferedReader(tempFile, StandardCharsets.UTF_8)) { if (!header.isBlank()) { writer.write(header); } tempReader.transferTo(writer); } Files.move(intermediateFile, targetFile, StandardCopyOption.REPLACE_EXISTING); Files.deleteIfExists(tempFile); } else { try (BufferedWriter writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { if (!header.isBlank()) { List lines = Files.readAllLines(tempFile, StandardCharsets.UTF_8); Files.writeString(tempFile, header, StandardCharsets.UTF_8); Files.write(tempFile, lines, StandardCharsets.UTF_8, StandardOpenOption.APPEND); } } Files.move(tempFile, targetFile, StandardCopyOption.REPLACE_EXISTING); } return targetFile; }); } private void exit() { int exit = SpringApplication.exit(this.context, () -> 0); System.exit(exit); } private String buildHeader(AdFSProperties.OutputItem config, String parentHeader, String total) { Handler handler = Handler.getHandler(config.type()); StringBuilder builder = new StringBuilder(); String template = config.fileHeader().isBlank() ? parentHeader : config.fileHeader(); if (!template.isBlank()) { String header = handler.commented(template .replace(Constants.Placeholder.HEADER_DATE, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))) .replace(Constants.Placeholder.HEADER_NAME, config.name()) .replace(Constants.Placeholder.HEADER_DESC, config.desc()) .replace(Constants.Placeholder.HEADER_TYPE, config.type().name().toLowerCase())) .replace(Constants.Placeholder.HEADER_TOTAL, total); builder.append(header).append(System.lineSeparator()); } Optional.ofNullable(handler.headFormat()).filter(StringUtils::hasText) .ifPresent(e -> builder.append(e).append(System.lineSeparator())); return builder.toString(); } } ================================================ FILE: src/main/java/org/fordes/adfs/config/AdFSProperties.java ================================================ package org.fordes.adfs.config; import jakarta.validation.Valid; import jakarta.validation.constraints.*; import lombok.Data; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.bind.DefaultValue; import org.springframework.context.annotation.Configuration; import java.util.*; /** * * @author fordes on 2025/10/29 */ @Data @Configuration @ConfigurationProperties(prefix = "application") public class AdFSProperties implements InitializingBean { @NotNull private Config config; @NotNull @NotEmpty private Set input; @NotNull private OutputProperties output; /** * @see #input */ @Deprecated private Map> rule; @Override public void afterPropertiesSet() { if (rule != null && !rule.isEmpty()) { rule.forEach((k, v) -> this.input.addAll(v)); } } public record OutputProperties( @DefaultValue("") String fileHeader, @DefaultValue("rule") String path, @NotEmpty Set<@Valid @NotNull OutputItem> files ) { } public record InputProperties( @NotBlank String name, @DefaultValue("EASYLIST") RuleSet type, @NotBlank String path ) { @Override public boolean equals(Object obj) { if (obj instanceof InputProperties prop) { return prop.path.equals(this.path) || prop.name.equals(this.name); } return false; } @Override public int hashCode() { return path.hashCode(); } } public record OutputItem( @NotBlank String name, @NotNull RuleSet type, @DefaultValue("") String desc, @DefaultValue("") String fileHeader, @NotEmpty @DefaultValue({}) Set filter, @NotEmpty @DefaultValue({}) Set<@NotBlank String> rule ) { } public record Config( @Positive @DefaultValue("0.0001") Double faultTolerance, @Positive @DefaultValue("2000000") Long expectedQuantity, @Min(1) @DefaultValue("6") Integer warnLimit, @DefaultValue() Set exclude, @DefaultValue DomainDetect domainDetect, @DefaultValue Tracking tracking ) { } public record DomainDetect( @DefaultValue("false") Boolean enable, @Positive @DefaultValue("1000") Integer timeout, @Positive @DefaultValue("600") Integer cacheTtlMin, @Positive @DefaultValue("86400") Integer cacheTtlMax, @Positive @DefaultValue("300") Integer cacheNegativeTtl, @Positive @DefaultValue("128") Integer concurrency, @DefaultValue List provider) { } public record Tracking(@DefaultValue("false") Boolean enable, @DefaultValue("logs/tracking.list") String path) { } } ================================================ FILE: src/main/java/org/fordes/adfs/constant/Constants.java ================================================ package org.fordes.adfs.constant; import java.io.File; import java.util.Set; public class Constants { public static final String ROOT_PATH = System.getProperty("user.dir"); public static final String FILE_SEPARATOR = File.separator; public static final Set LOCAL_IPS = Set.of("0.0.0.0", "127.0.0.1", "::1"); public static final Set LOCAL_DOMAINS = Set.of("localhost", "localhost.localdomain", "local", "ip6-localhost", "ip6-loopback"); public static final String LOCAL_V4 = "127.0.0.1"; public static final String UNKNOWN_IP = "0.0.0.0"; public static final String LOCAL_V6 = "::1"; public static final String LOCALHOST = "localhost"; public static final String DOUBLE_AT = "@@"; public static final String IMPORTANT = "important"; public static final String DOMAIN = "domain"; public static final String TAB = "\t"; public static final String PAYLOAD = "payload"; public static final String DNSMASQ_HEADER = "address=/"; public static final String SMARTDNS_HEADER = "address /"; public record Collector() { public static final String PARSER = "parser"; public static final String DNS_CHECK = "dns-check"; } public record Placeholder() { public static final String HEADER_DATE = "${date}"; public static final String HEADER_NAME = "${name}"; public static final String HEADER_TOTAL = "${total}"; public static final String HEADER_TYPE = "${type}"; public static final String HEADER_DESC = "${desc}"; } public record Symbol() { public static final String EMPTY = ""; public static final String DOT = "."; public static final String EXCLAMATION = "!"; public static final String HASH = "#"; public static final String AT = "@"; public static final String PERCENT = "%"; public static final String DOLLAR = "$"; public static final String UNDERLINE = "_"; public static final String DASH = "-"; public static final String TILDE = "~"; public static final String COMMA = ","; public static final String SLASH = "/"; public static final String LEFT_BRACKETS = "["; public static final String RIGHT_BRACKETS = "]"; public static final String OR = "||"; public static final String ASTERISK = "*"; public static final String QUESTION_MARK = "?"; public static final String A = "a"; public static final String CARET = "^"; public static final String WHITESPACE = " "; public static final String CR = "\r"; public static final String LF = "\n"; public static final String TAB = "\t"; public static final String CRLF = CR + LF; public static final String QUOTE = "\""; public static final String SINGLE_QUOTE = "'"; public static final String ADD = "+"; public static final String COLON = ":"; public static final String EQUAL = "="; } } ================================================ FILE: src/main/java/org/fordes/adfs/constant/RegConstants.java ================================================ package org.fordes.adfs.constant; import java.util.regex.Pattern; /** * @author fordes on 2024/4/9 */ public class RegConstants { public static final Pattern PATTERN_PATH_ABSOLUTE = Pattern.compile("^[a-zA-Z]:([/\\\\].*)?"); 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))"); 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})+$"); public static Pattern DOMAIN_PART = Pattern.compile("^([-a-zA-Z0-9]{0,62})+$"); } ================================================ FILE: src/main/java/org/fordes/adfs/enums/HandleType.java ================================================ package org.fordes.adfs.enums; import lombok.AllArgsConstructor; @AllArgsConstructor public enum HandleType { LOCAL, REMOTE, ; } ================================================ FILE: src/main/java/org/fordes/adfs/enums/RuleSet.java ================================================ package org.fordes.adfs.enums; import lombok.AllArgsConstructor; import lombok.Getter; import java.util.stream.Stream; @Getter @AllArgsConstructor public enum RuleSet { EASYLIST, DNSMASQ, CLASH, SMARTDNS, HOSTS, ; public static RuleSet of(String name) { return Stream.of(values()) .filter(v -> v.name().equalsIgnoreCase(name)) .findFirst() .orElseThrow(() -> new IllegalArgumentException("unknown format: " + name)); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/Parser.java ================================================ package org.fordes.adfs.handler; import bloomfilter.mutable.BloomFilter; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.config.AdFSProperties; import org.fordes.adfs.constant.Constants; import org.fordes.adfs.enums.HandleType; import org.fordes.adfs.handler.dns.DnsDetector; import org.fordes.adfs.handler.fetch.Fetcher; import org.fordes.adfs.handler.rule.Handler; import org.fordes.adfs.model.Rule; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import static org.fordes.adfs.config.AdFSProperties.Config; import static org.fordes.adfs.config.AdFSProperties.InputProperties; @Slf4j @Component public class Parser { protected final BloomFilter filter; protected final Config config; protected final DnsDetector detector; protected final Tracker tracker; public Parser(AdFSProperties properties, Optional detector, Optional tracker) { this.config = properties.getConfig(); this.detector = detector.orElse(null); this.tracker = tracker.orElse(null); this.filter = BloomFilter.apply(config.expectedQuantity(), config.faultTolerance(), rule -> rule.hashCode()); } public Flux handle(InputProperties prop) { if (prop.path().startsWith("http")) { return this.handle(prop, HandleType.REMOTE); } return this.handle(prop, HandleType.LOCAL); } public Flux handle(InputProperties prop, HandleType type) { AtomicLong total = new AtomicLong(0L); AtomicLong invalid = new AtomicLong(0L); AtomicLong repeat = new AtomicLong(0L); AtomicLong effective = new AtomicLong(0L); Set exclude = Optional.ofNullable(config.exclude()).orElseGet(Set::of); long start = System.currentTimeMillis(); return Flux.just(prop) .flatMap(p -> { Fetcher fetcher = Fetcher.getFetcher(type); return fetcher.fetch(p.path()); }) .filter(StringUtils::hasText) .flatMap(line -> { Handler handler = Handler.getHandler(prop.type()); if (handler.isComment(line)) { return Mono.empty(); } Rule rule = handler.parse(line); total.incrementAndGet(); if (Rule.EMPTY.equals(rule)) { invalid.incrementAndGet(); log.debug("[{}] parse fail: {}", prop.name(), line); if (tracker != null) { tracker.writeSync(Constants.Collector.PARSER, prop.name(), line); } return Mono.empty(); } rule.setSourceName(prop.name()); return Mono.just(rule); }) .flatMap(e -> { if (e.getTarget() != null && exclude.contains(e.getTarget())) { log.info("exclude rule: {}", e.getOrigin()); return Mono.empty(); } if (filter.mightContain(e)) { log.debug("already exists rule: {}", e.getOrigin()); repeat.incrementAndGet(); return Mono.empty(); } if (e.getOrigin().length() <= config.warnLimit()) { log.warn("[{}] suspicious rule => {}", prop.name(), e.getOrigin()); } return Mono.just(e); }) .onErrorResume(ex -> { log.error("Parse {} failed", prop.name(), ex); return Mono.empty(); }) .flatMap(rule -> { /** * 假设有规则 ||example.org^ * 通过DNS查询 example.org 是否存在 A/AAAA/CNAME 记录作为判断依据 * 不可避免的误判是,example.org 没有有效记录,而其存在有效子域如 test.example.org */ if (detector != null && Rule.Type.BASIC.equals(rule.getType()) && Rule.Scope.DOMAIN.equals(rule.getScope())) { return Flux.just(rule.getTarget()) .flatMap(e -> detector.lookup(e), 1) .flatMap(e -> { if (!e) { invalid.incrementAndGet(); log.debug("[{}] dns check invalid rule => {}", prop.name(), rule.getOrigin()); if (tracker != null) { tracker.writeSync(Constants.Collector.DNS_CHECK,prop.name(), rule.getOrigin()); } return Mono.empty(); } return Mono.just(rule); }); } return Mono.just(rule); }, config.domainDetect().concurrency()) .flatMap(e -> { filter.add(e); effective.incrementAndGet(); return Mono.just(e); }) .doFinally(signal -> { log.info("[{}] parsing cost {} ms, total: {}, effective: {}, repeat: {}, invalid: {}", prop.name(), System.currentTimeMillis() - start, total.get(), effective.get(), repeat.get(), invalid.get()); }); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/Tracker.java ================================================ package org.fordes.adfs.handler; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.config.AdFSProperties; import org.fordes.adfs.constant.Constants; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.time.Duration; import java.util.List; /** * @author Chengfs on 2025/10/31 */ @Slf4j @Component @ConditionalOnProperty(prefix = "application.config.tracking", name = "enable", havingValue = "true") public class Tracker implements DisposableBean { private final Path file; private final Sinks.Many sink; private final Disposable subscription; public Tracker(AdFSProperties properties) throws IOException { var config = properties.getConfig().tracking(); this.file = Path.of(config.path()); if (file.getParent() != null) { Files.createDirectories(file.getParent()); } Files.writeString(file, Constants.Symbol.EMPTY, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); this.sink = Sinks.many().unicast().onBackpressureBuffer(); this.subscription = sink.asFlux() .bufferTimeout(5000, Duration.ofSeconds(30)) .filter(batch -> !batch.isEmpty()) .concatMap(this::writeBatch) .retry() .subscribe(); log.info("lost collector is enabled, path: {}", file); } public Mono write(String source, String ruleName, String rule) { String line = String.format("[%s] [%s] %s\n", source, ruleName, rule); return Mono.fromRunnable(() -> sink.tryEmitNext(line)); } public void writeSync(String source, String ruleName, String rule) { write(source,ruleName, rule).subscribe(); } private Mono writeBatch(List lines) { return Mono.fromCallable(() -> { StringBuilder sb = new StringBuilder(lines.size() * 100); lines.forEach(sb::append); Files.writeString(file, sb.toString(), StandardCharsets.UTF_8, StandardOpenOption.APPEND, StandardOpenOption.CREATE); return null; }) .subscribeOn(Schedulers.boundedElastic()) .then(); } @Override public void destroy() throws Exception { sink.tryEmitComplete(); if (subscription != null) { Thread.sleep(2000); subscription.dispose(); } } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/dns/DnsDetector.java ================================================ package org.fordes.adfs.handler.dns; import io.netty.channel.EventLoop; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.handler.codec.dns.DnsRecord; import io.netty.resolver.ResolvedAddressTypes; import io.netty.resolver.dns.*; import io.netty.util.concurrent.Future; import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.config.AdFSProperties; import org.fordes.adfs.constant.Constants; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.stream.IntStream; @Data @Slf4j @Component @ConditionalOnProperty(prefix = "application.config.domain-detect", name = "enable", havingValue = "true") public class DnsDetector { private final NioEventLoopGroup group; private final List resolvers; private final AtomicLong nextIndex; private final LongAdder cacheHits; public DnsDetector(AdFSProperties properties) { var config = properties.getConfig().domainDetect(); int concurrency = config.concurrency(); this.group = new NioEventLoopGroup(concurrency); this.resolvers = new ArrayList<>(concurrency); this.nextIndex = new AtomicLong(0); this.cacheHits = new LongAdder(); //初始化 DnsServerAddressStreamProvider provider = buildProvider(config.provider()); DnsCache sharedCache = this.buildDnsCache(config.cacheTtlMin(), config.cacheTtlMax(), config.cacheNegativeTtl()); DnsCnameCache sharedCnameCache = new DefaultDnsCnameCache(config.cacheTtlMin(), config.cacheTtlMax()); IntStream.range(0, concurrency).forEach(i -> { EventLoop loop = group.next(); DnsNameResolver resolver = new DnsNameResolverBuilder(loop) .datagramChannelFactory(NioDatagramChannel::new) .nameServerProvider(provider) .queryTimeoutMillis(config.timeout()) .maxQueriesPerResolve(2) .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) .resolveCache(sharedCache) .cnameCache(sharedCnameCache) .optResourceEnabled(false) .build(); resolvers.add(resolver); }); log.info("dns detector init success, concurrency:{}", concurrency); } private DnsServerAddressStreamProvider buildProvider(List provider) { if (provider.isEmpty()) { return DnsServerAddressStreamProviders.platformDefault(); } InetSocketAddress[] addresses = provider.stream() .map(e -> { String[] parts = e.split(Constants.Symbol.COLON); return new InetSocketAddress(parts[0], parts.length > 1 ? Integer.parseInt(parts[1]) : 53); }) .toArray(InetSocketAddress[]::new); return new SequentialDnsServerAddressStreamProvider(addresses); } private DnsCache buildDnsCache(int minTtl, int maxTtl, int negativeTtl) { return new DefaultDnsCache(minTtl, maxTtl, negativeTtl) { @Override public List get(String hostname, DnsRecord[] additionals) { List result = super.get(hostname, additionals); if (result != null && !result.isEmpty()) { cacheHits.increment(); } return result; } }; } public Mono lookup(String domain) { if (domain == null || domain.isEmpty()) { return Mono.just(true); } String normalizedDomain = domain.toLowerCase().trim(); DnsNameResolver resolver = resolvers.get((int) (nextIndex.getAndIncrement() % resolvers.size())); return lookup(resolver, normalizedDomain); } private Mono lookup(DnsNameResolver resolver, String domain) { return Mono.create(sink -> { Future> future = resolver.resolveAll(domain); future.addListener(result -> { boolean res = true; if (!result.isSuccess()) { Throwable cause = result.cause(); if (cause instanceof UnknownHostException) { res = false; } else { log.warn("dns check failed: {} => {}", domain, cause.getMessage()); } } sink.success(res); log.debug("dns check done, available: {}", resolvers.size()); }); }); } @PreDestroy public void destroy() { try { log.info("dns detector shutdown, total queries:{}, cache hits:{}", nextIndex.get(), cacheHits.sum()); resolvers.forEach(DnsNameResolver::close); group.shutdownGracefully().await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error("Error during shutdown", e); } } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/fetch/Fetcher.java ================================================ package org.fordes.adfs.handler.fetch; import jakarta.annotation.Nonnull; import org.fordes.adfs.enums.HandleType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import reactor.core.publisher.Flux; import java.nio.charset.Charset; public abstract class Fetcher { protected @Nonnull Charset charset() { return Charset.defaultCharset(); } public abstract Flux fetch(@Nonnull String path); protected Flux fetch(Flux buffers) { final StringBuilder lineBuffer = new StringBuilder(); return buffers.flatMap(data -> { String chunk = data.toString(this.charset()); DataBufferUtils.release(data); lineBuffer.append(chunk); String full = lineBuffer.toString(); String[] lines = full.split("\\r?\\n", -1); int len = lines.length; lineBuffer.setLength(0); if (!full.endsWith("\n") && !full.endsWith("\r")) { lineBuffer.append(lines[len - 1]); len--; } return Flux.fromArray(lines).take(len); }) .concatWith(Flux.defer(() -> { if (lineBuffer.length() > 0) { return Flux.just(lineBuffer.toString()); } else { return Flux.empty(); } })); } public final static Fetcher getFetcher(HandleType type) { switch (type) { case LOCAL: return new LocalFetcher(); case REMOTE: return new HttpFetcher(); } throw new IllegalArgumentException("unsupported handle type: " + type); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/fetch/HttpFetcher.java ================================================ package org.fordes.adfs.handler.fetch; import io.netty.channel.ChannelOption; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.handler.timeout.WriteTimeoutHandler; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; import reactor.netty.http.client.HttpClient; import java.net.URI; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.concurrent.TimeUnit; @Slf4j public class HttpFetcher extends Fetcher { private final WebClient webClient; private Integer connectTimeout = 10_000; private Integer readTimeout = 30_000; private Integer writeTimeout = 30_000; private Integer bufferSize = 4096; public HttpFetcher() { HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, this.connectTimeout) // 连接超时 10 秒 .responseTimeout(Duration.ofSeconds(30)) // 响应超时 30 秒 .doOnConnected(conn -> conn .addHandlerLast(new ReadTimeoutHandler(this.readTimeout, TimeUnit.MILLISECONDS)) // 读超时 .addHandlerLast(new WriteTimeoutHandler(this.writeTimeout, TimeUnit.MILLISECONDS)) // 写超时 ); ExchangeStrategies strategies = ExchangeStrategies.builder() .codecs(configurer -> configurer .defaultCodecs() .maxInMemorySize(this.bufferSize) ) .build(); this.webClient = WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .exchangeStrategies(strategies) .defaultHeader(HttpHeaders.CONNECTION, "keep-alive") // .defaultHeader(HttpHeaders.ACCEPT_CHARSET, this.charset().displayName()) // .defaultHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate, br") .build(); } public HttpFetcher(Integer connectTimeout, Integer readTimeout, Integer writeTimeout, Integer bufferSize) { this(); this.connectTimeout = connectTimeout; this.readTimeout = readTimeout; this.writeTimeout = writeTimeout; this.bufferSize = bufferSize; } @Override public Flux fetch(String path) { Flux data = webClient.get() .uri(URI.create(path)) .retrieve() .bodyToFlux(DataBuffer.class) .onErrorResume(e -> { log.error("http rule => {}, fetch failed => {}", path, e.getMessage(), e); return Flux.empty(); }); return this.fetch(data); } @Override protected Charset charset() { return StandardCharsets.UTF_8; } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/fetch/LocalFetcher.java ================================================ package org.fordes.adfs.handler.fetch; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.util.Util; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import reactor.core.publisher.Flux; import java.nio.charset.Charset; import java.nio.file.Path; @Slf4j public class LocalFetcher extends Fetcher { private int bufferSize = 4096; public LocalFetcher() { super(); } public LocalFetcher(int bufferSize) { super(); this.bufferSize = bufferSize; } @Override public Flux fetch(String path) { Flux data = Flux.just(path) .map(Util::normalizePath) .map(Path::of) .flatMap(p -> DataBufferUtils.read(p, new DefaultDataBufferFactory(), this.bufferSize)) .onErrorResume(e -> { log.error("local rule => {}, read failed => {}", path, e.getMessage(), e); return Flux.empty(); }); return this.fetch(data); } @Override protected Charset charset() { return super.charset(); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/rule/ClashHandler.java ================================================ package org.fordes.adfs.handler.rule; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import org.fordes.adfs.util.Util; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import static org.fordes.adfs.constant.Constants.*; import static org.fordes.adfs.constant.Constants.Symbol.CRLF; import static org.fordes.adfs.constant.RegConstants.PATTERN_DOMAIN; /** * @author fordes123 on 2024/5/27 */ @Slf4j @Component public final class ClashHandler extends Handler implements InitializingBean { @Override public Rule parse(String line) { //跳过文件头 if (line.startsWith(PAYLOAD)) { return Rule.EMPTY; } Rule rule = new Rule(); rule.setOrigin(line); rule.setSourceType(RuleSet.EASYLIST); //只匹配 domain 规则,ipcidr、classical 规则暂不支持 String content = (line.startsWith(Symbol.DASH) ? line.substring(Symbol.DASH.length()) : line).trim(); if (content.startsWith(Symbol.SINGLE_QUOTE)) { content = Util.subBetween(line, Symbol.SINGLE_QUOTE, Symbol.SINGLE_QUOTE).trim(); } else if (content.startsWith(Symbol.QUOTE)) { content = Util.subBetween(line, Symbol.QUOTE, Symbol.QUOTE).trim(); } //通配符 * 一次只能匹配一级域名,无法转换为easylist if (content.startsWith(Symbol.ASTERISK)) { rule.setType(Rule.Type.UNKNOWN); return rule; } //通配符 + if (content.startsWith(Symbol.ADD)) { content = content.substring(content.startsWith("+.") ? 2 : 1); rule.setControls(Set.of(Rule.Control.OVERLAY)); } //判断是否是domain boolean haveAsterisk = content.contains(Symbol.ASTERISK); String temp = haveAsterisk ? content.replace(Symbol.ASTERISK, Symbol.A) : content; if (PATTERN_DOMAIN.matcher(temp).matches()) { rule.setType(haveAsterisk ? Rule.Type.WILDCARD : Rule.Type.BASIC); } rule.setTarget(content); rule.setDest(UNKNOWN_IP); rule.setMode(Rule.Mode.DENY); rule.setScope(Rule.Scope.DOMAIN); if (rule.getType() == null) { rule.setType(Rule.Type.UNKNOWN); } return rule; } @Override public String format(Rule rule) { if (Rule.Type.UNKNOWN == rule.getType()) { if (RuleSet.CLASH == rule.getSourceType()) { return rule.getOrigin(); } return null; } else if (rule.getMode() == Rule.Mode.DENY && rule.getScope() == Rule.Scope.DOMAIN) { StringBuilder builder = new StringBuilder(); builder.append(Symbol.WHITESPACE).append(Symbol.WHITESPACE).append(Symbol.DASH).append(Symbol.WHITESPACE).append(Symbol.QUOTE); Set controls = Optional.ofNullable(rule.getControls()).orElse(Set.of()); if (controls.contains(Rule.Control.OVERLAY)) { builder.append(Symbol.ADD).append(Symbol.DOT); } builder.append(rule.getTarget()); builder.append(Symbol.QUOTE); return builder.toString(); } return null; } @Override public String headFormat() { return PAYLOAD + Symbol.COLON; } @Override public boolean isComment(String line) { return line.startsWith(Symbol.HASH); } @Override public String commented(String value) { return Util.splitIgnoreBlank(value, Symbol.LF).stream() .map(e -> Symbol.HASH + e.trim()) .collect(Collectors.joining(CRLF)); } @Override public void afterPropertiesSet() { this.register(RuleSet.CLASH, this); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/rule/DnsmasqHandler.java ================================================ package org.fordes.adfs.handler.rule; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import org.fordes.adfs.util.Util; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import java.util.List; import java.util.stream.Collectors; import static org.fordes.adfs.constant.Constants.*; /** * @author fordes123 on 2024/5/27 */ @Component public final class DnsmasqHandler extends Handler implements InitializingBean { @Override public Rule parse(String line) { String content = Util.subAfter(line, DNSMASQ_HEADER, true); List data = Util.splitIgnoreBlank(content, Symbol.SLASH); if (data.size() == 1 || data.size() == 2) { String domain = data.getFirst(); String ip = data.size() > 1 ? data.get(1) : null; Rule rule = new Rule(); rule.setSourceType(RuleSet.DNSMASQ); rule.setOrigin(line); rule.setTarget(domain); rule.setDest(ip); rule.setScope(Rule.Scope.DOMAIN); rule.setType(Rule.Type.BASIC); rule.setMode((ip == null || LOCAL_IPS.contains(ip)) ? Rule.Mode.DENY : Rule.Mode.REWRITE); return rule; } return Rule.EMPTY; } @Override public String format(Rule rule) { if (Rule.Scope.DOMAIN == rule.getScope() && Rule.Type.BASIC == rule.getType() && Rule.Mode.ALLOW != rule.getMode()) { StringBuilder builder = new StringBuilder(); builder.append(DNSMASQ_HEADER) .append(rule.getTarget()); if (Rule.Mode.REWRITE.equals(rule.getMode())) { builder.append(Symbol.SLASH) .append(rule.getDest()); } builder.append(Symbol.SLASH); return builder.toString(); } return null; } @Override public String commented(String value) { return Util.splitIgnoreBlank(value, Symbol.LF).stream() .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim()) .collect(Collectors.joining(Symbol.CRLF)); } @Override public boolean isComment(String line) { return line.startsWith(Symbol.HASH); } @Override public void afterPropertiesSet() { this.register(RuleSet.DNSMASQ, this); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/rule/EasylistHandler.java ================================================ package org.fordes.adfs.handler.rule; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import org.fordes.adfs.util.Util; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import static org.fordes.adfs.constant.Constants.*; /** * @author fordes123 on 2024/5/27 */ @Slf4j @Component public final class EasylistHandler extends Handler implements InitializingBean { @Override public Rule parse(String line) { Rule rule = new Rule(); rule.setOrigin(line); rule.setSourceType(RuleSet.EASYLIST); rule.setMode(Rule.Mode.DENY); if (line.startsWith(DOUBLE_AT)) { rule.setMode(Rule.Mode.ALLOW); line = line.substring(2); } int _head = 0; if (line.startsWith(Symbol.OR)) { _head = Symbol.OR.length(); rule.getControls().add(Rule.Control.OVERLAY); } //修饰部分 int _tail = line.indexOf(Symbol.CARET); if (_tail > 0) { rule.getControls().add(Rule.Control.QUALIFIER); String modify = line.substring(_tail + 1); if (!modify.isEmpty()) { modify = modify.startsWith(Symbol.DOLLAR) ? modify.substring(1) : modify; String[] array = modify.split(Symbol.COMMA); if (Arrays.stream(array).allMatch(IMPORTANT::equals)) { rule.getControls().add(Rule.Control.IMPORTANT); } else { rule.setType(Rule.Type.UNKNOWN); return rule; } } } else if (line.endsWith(Symbol.DOLLAR + IMPORTANT)) { rule.getControls().add(Rule.Control.IMPORTANT); _tail = line.length() - (Symbol.DOLLAR.length() + IMPORTANT.length()); } //内容部分 String content = line.substring(_head, _tail > 0 ? _tail : line.length()); if (content.startsWith(Symbol.SLASH) && content.endsWith(Symbol.SLASH)) { content = content.substring(1, content.length() - 1); rule.setType(Rule.Type.UNKNOWN); } //判断是否为基本或通配规则 Util.isBaseRule(content, (origin, e) -> { if (rule.getType() == null) { rule.setType(e); } rule.setScope(Rule.Scope.DOMAIN); rule.setTarget(origin); if (Rule.Mode.DENY.equals(rule.getMode())) { rule.setDest(UNKNOWN_IP); } }, e -> { Map.Entry entry = Util.parseHosts(e); if (entry != null) { rule.setSourceType(RuleSet.HOSTS); rule.setTarget(entry.getValue()); rule.setMode(LOCAL_IPS.contains(entry.getKey()) && !LOCAL_DOMAINS.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE); rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey()); rule.setScope(Rule.Scope.DOMAIN); rule.setType(Rule.Type.BASIC); } else { rule.setType(Rule.Type.UNKNOWN); } }); return rule; } @Override public String format(Rule rule) { if (Rule.Type.UNKNOWN != rule.getType() && Rule.Mode.REWRITE != rule.getMode()) { StringBuilder builder = new StringBuilder(); Optional.of(rule.getMode()) .filter(Rule.Mode.ALLOW::equals) .ifPresent(m -> builder.append(DOUBLE_AT)); Optional.of(rule.getControls()) .filter(e -> e.contains(Rule.Control.OVERLAY)) .ifPresent(c -> builder.append(Symbol.OR)); builder.append(rule.getTarget()); Optional.of(rule.getControls()) .filter(e -> e.contains(Rule.Control.QUALIFIER)) .ifPresent(c -> builder.append(Symbol.CARET)); Optional.of(rule.getControls()) .filter(e -> e.contains(Rule.Control.IMPORTANT)) .ifPresent(c -> builder.append(Symbol.DOLLAR).append(IMPORTANT)); return builder.toString(); } //同源未知规则可直接写出 if (Rule.Type.UNKNOWN == rule.getType() && RuleSet.EASYLIST == rule.getSourceType()) { return rule.getOrigin(); } return null; } @Override public String commented(String value) { return Util.splitIgnoreBlank(value, Symbol.LF).stream() .map(e -> Symbol.EXCLAMATION + Symbol.WHITESPACE + e.trim()) .collect(Collectors.joining(Symbol.CRLF)); } @Override public void afterPropertiesSet() { this.register(RuleSet.EASYLIST, this); } @Override public boolean isComment(String line) { return Util.startWithAny(line, Symbol.HASH, Symbol.EXCLAMATION) || Util.between(line, Symbol.LEFT_BRACKETS, Symbol.RIGHT_BRACKETS); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/rule/Handler.java ================================================ package org.fordes.adfs.handler.rule; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.fordes.adfs.constant.Constants; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import java.util.HashMap; import java.util.Map; public abstract sealed class Handler permits EasylistHandler, DnsmasqHandler, ClashHandler, SmartdnsHandler, HostsHandler { private static final Map handlerMap = new HashMap<>(RuleSet.values().length, 1); /** * 解析规则
* 返回 {@link Rule#EMPTY} 即表示解析失败 * * @param line 规则文本 * @return {@link Rule} */ public abstract @Nonnull Rule parse(String line); /** * 转换规则
* * @param rule {@link Rule} null 表示无法转换或失败 * @return 规则文本 */ public abstract @Nullable String format(Rule rule); /** * 生成注释 * @param value 目标内容 * @return 注释 */ public abstract String commented(String value); /** * 某些规则格式拥有固定的头部内容,可实现此方法以返回 */ public String headFormat() { return Constants.Symbol.EMPTY; } /** * 某些规则格式拥有固定的尾部内容,可实现此方法以返回 */ public String tailFormat() { return Constants.Symbol.EMPTY; } /** * 验证规则文本是否为注释
* 并不强制子类实现此方法,且不是注释不表示此规则有效 * * @param line 规则文本 * @return 默认 false */ public boolean isComment(String line) { return false; } /** * 根据 RuleSet 获取 Handler * * @param type {@link RuleSet} * @return {@link Handler} */ public static Handler getHandler(RuleSet type) { return handlerMap.get(type); } protected void register(RuleSet type, Handler handler) { handlerMap.put(type, handler); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/rule/HostsHandler.java ================================================ package org.fordes.adfs.handler.rule; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import org.fordes.adfs.util.Util; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import static org.fordes.adfs.constant.Constants.*; /** * @author fordes123 on 2024/5/27 */ @Slf4j @Component public final class HostsHandler extends Handler implements InitializingBean { @Override public Rule parse(String line) { Map.Entry entry = Util.parseHosts(line); if (entry == null || Objects.equals(entry.getKey(), entry.getValue())) { return Rule.EMPTY; } Rule rule = new Rule(); rule.setSourceType(RuleSet.HOSTS); rule.setOrigin(line); rule.setTarget(entry.getValue()); rule.setMode(LOCAL_IPS.contains(entry.getKey()) && !LOCAL_DOMAINS.contains(entry.getValue()) ? Rule.Mode.DENY : Rule.Mode.REWRITE); rule.setDest(Rule.Mode.DENY == rule.getMode() ? UNKNOWN_IP : entry.getKey()); rule.setScope(Rule.Scope.DOMAIN); rule.setType(Rule.Type.BASIC); return rule; } @Override public String format(Rule rule) { if (Rule.Scope.DOMAIN == rule.getScope() && Rule.Type.BASIC == rule.getType() && Rule.Mode.ALLOW != rule.getMode()) { return Optional.ofNullable(rule.getDest()).orElse(UNKNOWN_IP) + Symbol.TAB + rule.getTarget(); } return null; } @Override public String commented(String value) { return Util.splitIgnoreBlank(value, Symbol.LF).stream() .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim()) .collect(Collectors.joining(Symbol.CRLF)); } @Override public boolean isComment(String line) { return line.startsWith(Symbol.HASH); } @Override public void afterPropertiesSet() { this.register(RuleSet.HOSTS, this); } } ================================================ FILE: src/main/java/org/fordes/adfs/handler/rule/SmartdnsHandler.java ================================================ package org.fordes.adfs.handler.rule; import org.fordes.adfs.enums.RuleSet; import org.fordes.adfs.model.Rule; import org.fordes.adfs.util.Util; import org.springframework.beans.factory.InitializingBean; import org.springframework.stereotype.Component; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import static org.fordes.adfs.constant.Constants.*; import static org.fordes.adfs.model.Rule.Mode.ALLOW; /** * @author fordes123 on 2024/5/27 */ @Component public final class SmartdnsHandler extends Handler implements InitializingBean { @Override public Rule parse(String line) { String content = Util.subAfter(line, SMARTDNS_HEADER, true); List data = Util.splitIgnoreBlank(content, Symbol.SLASH); if (data.size() == 2) { String domain = data.getFirst(); String control = data.get(1); Rule rule = new Rule(); rule.setOrigin(line); rule.setSourceType(RuleSet.EASYLIST); switch (control) { case Symbol.HASH -> rule.setMode(Rule.Mode.DENY); case Symbol.DASH -> rule.setMode(ALLOW); default -> { //未知或不支持的控制符 如 #6 #4 rule.setType(Rule.Type.UNKNOWN); return rule; } } //仅匹配主域名 if (domain.startsWith(Symbol.DASH)) { domain = domain.substring(0, line.length() - Symbol.DASH.length()); } else { rule.setControls(Set.of(Rule.Control.OVERLAY)); } if (domain.startsWith(Symbol.DOT)) { domain = domain.substring(0, line.length() - Symbol.DOT.length()); } rule.setType(domain.startsWith(Symbol.ASTERISK) ? Rule.Type.WILDCARD : Rule.Type.BASIC); rule.setTarget(domain); rule.setDest(UNKNOWN_IP); rule.setScope(Rule.Scope.DOMAIN); return rule; } return Rule.EMPTY; } @Override public String format(Rule rule) { if (Rule.Type.UNKNOWN == rule.getType()) { if (RuleSet.SMARTDNS == rule.getSourceType()) { return rule.getOrigin(); } return null; } else if (rule.getMode() != Rule.Mode.REWRITE && rule.getScope() == Rule.Scope.DOMAIN) { switch (rule.getType()) { case BASIC -> { return SMARTDNS_HEADER + (!rule.getControls().contains(Rule.Control.OVERLAY) ? (Symbol.ASTERISK + Symbol.DOT) : Symbol.EMPTY) + rule.getTarget() + Symbol.SLASH + (Rule.Mode.DENY.equals(rule.getMode()) ? Symbol.HASH : Symbol.DASH); } case WILDCARD -> { String domain = rule.getTarget(); if (domain.lastIndexOf(Symbol.ASTERISK) == 0) { return SMARTDNS_HEADER + domain + Symbol.SLASH + Symbol.DASH; } } } } return null; } @Override public String commented(String value) { return Util.splitIgnoreBlank(value, Symbol.LF).stream() .map(e -> Symbol.HASH + Symbol.WHITESPACE + e.trim()) .collect(Collectors.joining(Symbol.CRLF)); } @Override public boolean isComment(String line) { return line.startsWith(Symbol.HASH); } @Override public void afterPropertiesSet() { this.register(RuleSet.SMARTDNS, this); } } ================================================ FILE: src/main/java/org/fordes/adfs/model/Rule.java ================================================ package org.fordes.adfs.model; import lombok.Data; import org.fordes.adfs.enums.RuleSet; import java.util.HashSet; import java.util.Objects; import java.util.Set; /** * @author fordes123 on 2024/5/27 */ @Data public class Rule { /** * 规则来源名称,及 {@link org.fordes.adfs.config.AdFSProperties.InputProperties#name} */ private String sourceName; /** * 规则来源类型 {@link RuleSet} */ private RuleSet sourceType; /** * 原始规则 */ private String origin; /** * 作用目标 */ private String target; /** * 重定向/重写目标 */ private String dest; /** * 模式 {@link Mode} */ private Mode mode; /** * 作用范围 {@link Scope} */ private Scope scope; /** * 规则类型 {@link Type} */ private Type type; /** * 控制符集 {@link Control} */ private Set controls = new HashSet<>(Control.values().length, 1.0f); public static final Rule EMPTY = new Rule(); /** * 规则控制参数 */ public enum Control { /** * 最高优先级 */ IMPORTANT, /** * 覆盖子域名 */ OVERLAY, /** * 限定符,通常是 ^ */ QUALIFIER, ; } /** * 规则模式 */ public enum Mode { /** * 阻止 */ DENY, /** * 解除阻止 */ ALLOW, /** * 重写
* 通常 hosts规则指向特定ip(非localhost)时即为重写 */ REWRITE, ; } /** * 规则类型 */ public enum Type { /** * 基本规则,不包含任何控制、匹配符号, 可以转换为 hosts */ BASIC, /** * 通配规则,仅使用通配符 */ WILDCARD, /** * 其他规则,如使用了正则、高级修饰符号等,这表示目前无法支持 */ UNKNOWN, ; } /** * 作用域 */ public enum Scope { /** * ipv4或ipv6地址 */ HOST, /** * 域名 */ DOMAIN, /** * 路径、文件等 */ PATH, ; } @Override public boolean equals(Object o) { if (this == o) return true; if (o instanceof Rule rule) { if (Type.UNKNOWN == this.type || Type.UNKNOWN == rule.getType()) { return Objects.equals(this.origin, rule.origin); } return Objects.equals(this.target, rule.target) && this.mode == rule.mode && this.scope == rule.scope && this.type == rule.type; } return false; } @Override public int hashCode() { if (Type.UNKNOWN == this.type) { return Objects.hash(this.origin); } return Objects.hash(getTarget(), getMode(), getScope(), getType()); } @Override public String toString() { if (Type.UNKNOWN == this.type) { return "Rule{" + "origin='" + origin + '\'' + '}'; } return "Rule{" + "target='" + target + '\'' + ", mode=" + mode + ", scope=" + scope + ", type=" + type + '}'; } } ================================================ FILE: src/main/java/org/fordes/adfs/util/Util.java ================================================ package org.fordes.adfs.util; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.fordes.adfs.model.Rule; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Consumer; import static org.fordes.adfs.constant.Constants.*; import static org.fordes.adfs.constant.RegConstants.*; /** * @author fordes123 on 2022/9/19 */ @Slf4j public class Util { /** * 给定字符串是否以特定前缀开始 * * @param str 给定字符串 * @param prefixes 前缀 * @return 给定字符串是否以特定前缀开始 */ public static boolean startWithAny(String str, String... prefixes) { if (!StringUtils.hasText(str) || ObjectUtils.isEmpty(prefixes)) { return false; } return Arrays.stream(prefixes).anyMatch(str::startsWith); } /** * 给定字符串是否以特定字符串开始和结束 * * @param str 给定字符串 * @param start 开始字符串 * @param end 结束字符串 * @return 给定字符串是否以特定字符串开始和结束 */ public static boolean between(String str, String start, String end) { if (StringUtils.hasLength(str) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) { return str.startsWith(start) && str.endsWith(end); } return false; } /** * 截取分隔字符串之前的字符串,不包括分隔字符串
* 截取不到时返回空串 * * @param str 被截取的字符串 * @param flag 分隔字符串 * @param isLast 是否是最后一个 * @return 分隔字符串之前的字符串 */ public static String subBefore(String str, String flag, boolean isLast) { if (StringUtils.hasLength(str) && StringUtils.hasLength(flag)) { int index = isLast ? str.lastIndexOf(flag) : str.indexOf(flag); if (index >= 0) { return str.substring(0, index); } } return Symbol.EMPTY; } /** * 截取分隔字符串之后的字符串,不包括分隔字符串
* 截取不到时返回空串 * * @param content 被截取的字符串 * @param flag 分隔字符串 * @param isLast 是否是最后一个 * @return 分隔字符串之后的字符串 */ public static String subAfter(String content, String flag, boolean isLast) { if (StringUtils.hasLength(content) && StringUtils.hasLength(flag)) { int index = isLast ? content.lastIndexOf(flag) : content.indexOf(flag); if (index >= 0) { return content.substring(index + flag.length()); } } return Symbol.EMPTY; } /** * 截取分隔字符串之间的字符串,不包括分隔字符串
* 截取不到时返回空串 * * @param content 被截取的字符串 * @param start 开始分隔字符串 * @param end 结束分隔字符串 * @return 分隔字符串之间的字符串 */ public static String subBetween(String content, String start, String end) { if (StringUtils.hasLength(content) && StringUtils.hasLength(start) && StringUtils.hasLength(end)) { int startIndex = content.indexOf(start); int endIndex = content.lastIndexOf(end); if (startIndex >= 0 && endIndex > 0 && startIndex < endIndex) { return content.substring(startIndex + start.length(), endIndex); } } return Symbol.EMPTY; } /** * 切分字符串并移除空项 * * @param str 待切分字符串 * @param flag 分隔符 * @return 切分后的字符串 */ public static List splitIgnoreBlank(String str, String flag) { if (!StringUtils.hasLength(str) || !StringUtils.hasLength(flag)) { return List.of(); } return Arrays.stream(str.split(flag)) .filter(e -> !e.isBlank()) .toList(); } /** * 给定字符串是等于任一字符串 * * @param str 给定字符串 * @param values 任意字符串 * @return 给定字符串是等于任一字符串 */ public static boolean equalsAny(String str, String... values) { if (!StringUtils.hasLength(str) || ObjectUtils.isEmpty(values)) { return false; } return Arrays.asList(values).contains(str); } /** * 解析hosts规则,如不是则返回null * * @param content 待解析字符串 * @return {@link Map.Entry} key:ip, value:域名 */ public static @Nullable Map.Entry parseHosts(String content) { if (content.contains(Symbol.TAB)) { content = content.replace(TAB, Symbol.WHITESPACE); } List list = splitIgnoreBlank(content, Symbol.WHITESPACE); if (list.size() == 2) { String ip = list.get(0).trim(); String domain = list.get(1).trim(); if (PATTERN_IP.matcher(ip).matches() && PATTERN_DOMAIN.matcher(domain).matches()) { return Map.entry(ip, domain); } } return null; } /** * 休眠线程,忽略中断异常 * * @param millis 休眠时间,毫秒 */ public static void sleep(long millis) { if (millis > 0L) { try { Thread.sleep(millis); } catch (InterruptedException ignored) { } } } /** * 转换相对路径为绝对路径 * * @param path 路径 * @return 规范化后的路径 */ public static String normalizePath(@Nonnull String path) { boolean isAbsPath = '/' == path.charAt(0) || PATTERN_PATH_ABSOLUTE.matcher(path).matches(); if (!isAbsPath) { if (path.startsWith(Symbol.DOT)) { path = path.substring(1); } if (path.startsWith(FILE_SEPARATOR)) { path = path.substring(FILE_SEPARATOR.length()); } path = ROOT_PATH + FILE_SEPARATOR + path; } return path; } public static void isBaseRule(String content, BiConsumer ifPresent, Consumer orElse) { String temp = content; if (temp.contains(Symbol.ASTERISK)) { temp = content.replace(Symbol.ASTERISK, Symbol.A); } if (temp.startsWith(Symbol.DOT)) { temp = temp.substring(1); } if (temp.endsWith(Symbol.DOT)) { temp = temp.substring(0, temp.length() - 1); } if (PATTERN_DOMAIN.matcher(temp).matches()) { ifPresent.accept(content, content.equals(temp) ? Rule.Type.BASIC : Rule.Type.WILDCARD); } else if (DOMAIN_PART.matcher(temp).matches()) { ifPresent.accept(content, Rule.Type.WILDCARD); } else { orElse.accept(content); } } } ================================================ FILE: src/main/resources/application-dev.yml ================================================ logging: level: org.fordes.adfs: info application: input: - name: anti-ad smartdns path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/anti-ad-smartdns.conf type: smartdns - name: anti-ad dnsmasq path: https://raw.githubusercontent.com/privacy-protection-tools/anti-AD/master/adblock-for-dnsmasq.conf type: dnsmasq - name: AdGuard 基础过滤器 path: https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_Base/filter.txt type: easylist - name: Loyalsoldier/clash-rules 广告域名列表 path: https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt type: clash - name: 1Hosts lite path: https://o0.pages.dev/Lite/hosts.txt type: hosts output: files: - name: easylist.txt type: easylist filter: - basic - wildcard - unknown - name: dns.txt type: easylist file-header: | Test ${total} filter: - basic - wildcard - name: modify.txt type: easylist filter: - unknown - name: dnsmasq.conf type: dnsmasq - name: clash.yaml type: clash - name: smartdns.conf type: smartdns - name: hosts type: hosts config: domain-detect: enable: true concurrency: 256 provider: - 1.1.1.1 ================================================ FILE: src/main/resources/application.yml ================================================ spring: application: name: "@project.name@" version: "@project.version@" profiles: active: prod logging: file: path: ./logs application: input: config: tracking: enable: true path: logs/tracking.list ================================================ FILE: src/main/resources/banner.txt ================================================ ${AnsiColor.BRIGHT_GREEN}██████╗ ██████╗ █████╗ ██████╗ ${AnsiColor.BRIGHT_GREEN}██╔══██╗██╔══██╗ ██╔══██╗██╔══██╗ ${AnsiColor.BRIGHT_GREEN}██║ ██║██║ ██║█████╗███████║██║ ██║ ${AnsiColor.BRIGHT_GREEN}██║ ██║██║ ██║╚════╝██╔══██║██║ ██║ ${AnsiColor.BRIGHT_GREEN}██████╔╝██████╔╝ ██║ ██║██████╔╝ ${AnsiColor.BRIGHT_GREEN}╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ${AnsiColor.BRIGHT_BLACK} ───────────────────────────────────────────────────────── ${AnsiColor.BRIGHT_WHITE} ➤ Runtime ${AnsiColor.BRIGHT_BLACK}${os.name} • Java ${java.version} • Spring Boot v${spring-boot.version} ${AnsiColor.BRIGHT_WHITE} ➤ Profile ${AnsiColor.BRIGHT_YELLOW}${spring.profiles.active:default} ${AnsiColor.BRIGHT_BLACK}• ${AnsiColor.BRIGHT_WHITE}${server.port:N/A} ${AnsiColor.BRIGHT_BLACK}• ${AnsiColor.BRIGHT_BLACK}PID ${AnsiColor.BRIGHT_WHITE}${PID} ${AnsiColor.BRIGHT_WHITE} ➤ Issue tracker: ${AnsiColor.BRIGHT_BLACK}https://github.com/fordes123/ad-filters-subscriber/issues ${AnsiColor.BRIGHT_WHITE} ================================================ FILE: src/main/resources/logback-spring.xml ================================================ %clr(%d{${LOG_DATEFORMAT_PATTERN:-HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(---){faint} %clr([%10.10t]){faint} %clr(%-40.40logger{39}){cyan} %clr(%-6L){yellow} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx} UTF-8 ${LOG_HOME}/${APP_NAME}.log ${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.log 30 3GB true %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} UTF-8 true 4096 true