diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..fdecc96d78 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: vernesong +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..1015061429 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,153 @@ +name: Bug报告 +description: Create a Bug report to help us improve +title: "[Bug] " +labels: ["bug"] +body: + + - type: checkboxes + id: verify_step + attributes: + label: Verify Steps + description: | + 在提交之前,请确认 / Please verify that you've followed these steps. + options: + - label: Tracker 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 + required: true + - label: Branch 我知道 OpenClash 的 Dev 分支切换开关位于插件设置-版本更新中,或者我会手动下载并安装 Dev 分支的 OpenClash + required: true + - label: Latest 我已经**使用最新 Dev 版本**测试过,问题依旧存在 + required: true + - label: Relevant 我知道 OpenClash 与 内核(Core)、控制面板(Dashboard)、在线订阅转换(Subconverter)等项目之间**无直接关系**,仅相互调用 + required: true + - label: Definite 这确实是 OpenClash 出现的问题 + required: true + - label: Contributors 我有能力协助 OpenClash 开发并解决此问题 + required: false + - label: Meaningless 我提交的**是无意义的**催促更新或修复请求 + required: false + + - type: input + id: openclash_version + attributes: + label: OpenClash Version + description: | + OpenClash 版本号 + placeholder: "v0.0.0-beta" + validations: + required: true + + - type: dropdown + id: bug_os + attributes: + label: Bug on Environment + description: | + 发现问题所在的系统环境 / System Environment + multiple: true + options: + - Official OpenWrt + - Lean + - Immortalwrt + - Istoreos + - Docker + - Other + validations: + required: true + + - type: input + id: openwrt_version + attributes: + label: OpenWrt Version + description: | + Openwrt 固件版本 + placeholder: "OpenWrt 0.0.0 r0-0" + validations: + required: true + + - type: dropdown + id: bug_platform + attributes: + label: Bug on Platform + description: | + 发现问题所在的架构平台 / Platform + multiple: true + options: + - Linux-386 + - Linux-amd64(x86-64) + - Linux-amd64-v3(x86-64) + - Linux-armv5 + - Linux-armv6 + - Linux-armv7 + - Linux-arm64 + - Linux-loong64 + - Linux-mips-hardfloat + - Linux-mips-softfloat + - Linux-mips64 + - Linux-mips64le + - Linux-mipsle-hardfloat + - Linux-mipsle-softfloat + - Linux-riscv64 + - Other + - All + validations: + required: true + + - type: textarea + id: describe_bug + attributes: + label: Describe the Bug + description: | + 对 Bug 本身清晰而简洁的描述 / Describe the Bug + validations: + required: true + + - type: textarea + id: reproduce_bug + attributes: + label: To Reproduce + description: | + 复现此 Bug 的步骤 / How to reproduce? + validations: + required: true + + - type: textarea + id: openclash_log + attributes: + label: OpenClash Log + description: | + 在下方附上 OpenClash 调试日志 / OpenClash Debug Log + 调试日志在插件设置-调试日志中生成,**并非只有运行日志**,如调试日志过长,可作为附件在最下方上传 + **隐私提示: 上传此日志前请注意检查、屏蔽公网IP、节点、密码等相关敏感信息** + placeholder: "我已知晓缺失日志可能会导致开发者无法了解我的情况并降低本issue的处理优先级" + render: log + validations: + required: true + + - type: textarea + id: openclash_config + attributes: + label: OpenClash Config + description: | + 在下方附上与 Bug 相关的系统配置、防火墙规则或环境变量 / System config + 非 Clash Yaml 文件 + **非必填项** + render: shell + validations: + required: false + + - type: textarea + id: excepted_behavior + attributes: + label: Expected Behavior + description: | + 对预期修复后情况的清晰明了的描述 / Expected behavior + validations: + required: true + + - type: textarea + id: additional_context + attributes: + label: Additional Context + description: | + 在此处添加其它信息或屏幕截图 / Additional Context + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..d9c8c78ca6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,47 @@ +blank_issues_enabled: false + +contact_links: + + - name: 发表Issue前请先在Issues与Discussions中搜索相关内容 + url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md#在提问之前 + about: 请有意义且描述明确的描述问题症状而非你的猜测 问题解决后加个简短的补充说明 + + - name: 启动成功后无法访问个别网站 | 启动成功后个别网站不想走代理流量 + url: https://www.bing.com/search?q=clash+rule+写+教&ensearch=1 + about: 想要了解clash规则编写相关的问题 + + - name: CLASH CORE | PERIUM CORE & 内核问题 | 节点协议加密方式 | 规则 | 内核功能建议 + url: https://github.com/Dreamacro/clash/issues/new/choose + about: 原版内核 | P核 + + - name: META CORE | MIHOMO CORE & 内核问题 | 节点协议加密方式 | 规则 | 内核功能建议 + url: https://github.com/MetaCubeX/mihomo/issues/new/choose + about: M核 + + - name: 在线订阅转换问题 | 在线订阅转换增加节点协议加密方式 | 在线订阅转换功能建议 + url: https://github.com/tindy2013/subconverter/issues/new/choose + about: 提供并增加预设订阅转换服务地址请通过上方 功能请求 发表 + + - name: DASHBOARD & 控制面板问题 | 面板功能建议 + url: https://github.com/Dreamacro/clash-dashboard/issues/new/choose + about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问 + + - name: YACD & 控制面板问题 | 面板功能建议 + url: https://github.com/haishanh/yacd/issues/new/choose + about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问 + + - name: METACUBEXD & 控制面板问题 | 面板功能建议 + url: https://github.com/MetaCubeX/metacubexd/issues/new/choose + about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问 + + - name: YACD-META & 控制面板问题 | 面板功能建议 + url: https://github.com/MetaCubeX/Yacd-meta/issues/new/choose + about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问 + + - name: DASHBOARD-META & 控制面板问题 | 面板功能建议 + url: https://github.com/MetaCubeX/Razord-meta/issues/new/choose + about: 面板问题先请尝试 更换近期未使用过面板设备 或 使用浏览器隐私匿名无痕...模式 访问 + + - name: 我想讨论OpenClash的某个功能 | 我不确定是什么问题 | 我的问题不属于上述选项 | 我的问题无法得到原作者原仓库的帮助 + url: https://github.com/vernesong/OpenClash/discussions/new/choose + about: 礼多人不怪 而且有时还很有帮助 diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..7ffa993529 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,40 @@ +name: 功能请求 +description: Suggest an idea for this project +title: "[Feature] " +labels: [enhancement] +body: + + - type: checkboxes + id: verify_step + attributes: + label: Verify Steps + description: "在提交之前,请确认 / Please verify that you've followed these steps" + options: + - label: Tracker 我已经在 [Issue Tracker](……/) 中找过我要提出的问题 + required: true + - label: Latest 我已经**使用最新 Dev 版本**查看过,并不包含该功能特性或者还不完善 + required: true + - label: Relevant 我知道 OpenClash 与 内核(Core)、控制面板(Dashboard)、在线订阅转换(Subconverter)等项目之间**无直接关系**,仅相互调用 + required: true + - label: Definite 这确实是 OpenClash 应包含的特性 + required: true + - label: Contributors 我有能力协助 OpenClash 开发或完善此功能特性 + required: false + - label: Meaningless 我提交的**是无意义的**催促更新或修复请求 + required: false + + - type: textarea + attributes: + label: Describe the Feature + description: | + 清晰明了地描述您的想法. 例如你想实现什么 Feature 特性或功能? 如何实现该功能? 目前 OpenClash 的行为是什么? + validations: + required: true + + - type: textarea + attributes: + label: Describe Alternatives + description: | + 对您考虑过的任何替代解决方案或备选功能进行清晰、简洁的描述. + validations: + required: false diff --git a/.github/workflows/clear_cache.yml b/.github/workflows/clear_cache.yml new file mode 100644 index 0000000000..6a5e596623 --- /dev/null +++ b/.github/workflows/clear_cache.yml @@ -0,0 +1,41 @@ +name: Clean jsDelivr Cache + +on: + push: + branches: + - master + - dev + - core + - package + paths-ignore: + - 'README.md' + - 'LICENSE' + +jobs: + jsDelivr: + runs-on: ubuntu-latest + + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install jq + + - uses: actions/checkout@v4 + + # https://github.com/marketplace/actions/git-changesets + - id: changed_files + name: git-changesets + uses: collin-miller/git-changesets@v1 + with: + # Default format is 'csv'. Other valid options are 'space-delimited' and 'json'. + format: json + + - name: Clean jsDelivr Cache + run: | + echo '${{ steps.changed_files.outputs.added_modified }}' | jq -r '.[]' | while read file; do + # echo ${{ steps.changed_files.outputs.all }} + curl -sL --retry 3 "https://purge.jsdelivr.net/gh/vernesong/OpenClash@${{ github.ref_name }}/${file}" + done diff --git a/.github/workflows/close_issues.yml b/.github/workflows/close_issues.yml new file mode 100644 index 0000000000..1a8866a528 --- /dev/null +++ b/.github/workflows/close_issues.yml @@ -0,0 +1,19 @@ + +name: Mark and close stale issues + +on: + schedule: + - cron: "30 8 * * *" + +jobs: + stale: + + runs-on: ubuntu-latest + + steps: + - uses: actions/stale@v5 + with: + stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 5 days' + days-before-stale: 60 + days-before-close: 5 + operations-per-run: 200 diff --git a/.github/workflows/compile_dev_core.yml b/.github/workflows/compile_dev_core.yml new file mode 100644 index 0000000000..6779cb116b --- /dev/null +++ b/.github/workflows/compile_dev_core.yml @@ -0,0 +1,118 @@ + +name: Compile The New Clash Dev Core + +on: + #schedule: + # - cron: "0 19 * * 1,3,5,6" + workflow_dispatch: + +jobs: + Get-Commit-id: + runs-on: ubuntu-latest + outputs: + current_id: ${{ steps.current_id.outputs.current_id }} + upstream_id: ${{ steps.upstream_id.outputs.upstream_id }} + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install git + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Get Current Version + id: current_id + run: | + echo "current_id=$(sed -n 1p ./dev/core_version)" >> $GITHUB_OUTPUT + echo "current_id: $(sed -n 1p ./dev/core_version)" + + - name: Clone Clash Repository + uses: actions/checkout@v4 + with: + repository: Dreamacro/clash + ref: dev + fetch-depth: '0' + + - name: Get Upstream Version + id: upstream_id + run: | + echo "upstream_id=$(git describe --tags)" >> $GITHUB_OUTPUT + echo "upstream_id: $(git describe --tags)" + + Compile: + runs-on: ubuntu-latest + needs: Get-Commit-id + if: ${{ needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id }} + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install wget git tar + + - name: Setup UPX + run: | + cd .. + wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz + tar xf upx-3.95-amd64_linux.tar.xz + echo "upx=../upx-3.95-amd64_linux/upx" >> $GITHUB_ENV + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Copy Makefile + run: | + cd .. + mkdir tmp + mkdir tmp/bin + cp ./OpenClash/.github/makefile/dev ./tmp/Makefile + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: "1.21" + check-latest: true + + - name: Clone Clash Repository + uses: actions/checkout@v4 + with: + repository: Dreamacro/clash + ref: dev + fetch-depth: '0' + + - name: Compile Dev Clash + run: | + cp ../tmp/Makefile ./Makefile + make releases + + - name: Copy Clash Bin + run: | + cp -rf "./bin/." "../tmp/bin/" + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Change Version + run: | + sed -i '1d' ./dev/core_version + sed -i "1i ${{ needs.Get-Commit-id.outputs.upstream_id }}" ./dev/core_version + + - name: Commit and push + run: | + rm -rf ./dev/dev/* + cp -rf "../tmp/bin/." "./dev/dev/" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "Auto update Dev core: ${{ needs.Get-Commit-id.outputs.upstream_id }}" + git push diff --git a/.github/workflows/compile_meta_core.yml b/.github/workflows/compile_meta_core.yml new file mode 100644 index 0000000000..a3d3ac6a0b --- /dev/null +++ b/.github/workflows/compile_meta_core.yml @@ -0,0 +1,393 @@ +name: Compile The New Clash Core + +on: + schedule: + - cron: "0 20 * * 1,3,5,6" + workflow_dispatch: + +jobs: + Get-Commit-id: + runs-on: ubuntu-latest + outputs: + current_id: ${{ steps.current_id.outputs.current_id }} + current_smart_id: ${{ steps.current_id.outputs.current_smart_id }} + upstream_id: ${{ steps.upstream_id.outputs.upstream_id }} + upstream_smart_id: ${{ steps.upstream_id.outputs.upstream_smart_id }} + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install git + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Get Current Version + id: current_id + run: | + current_meta_ver=$(sed -n '1p' ./dev/core_version 2>/dev/null || echo "null") + echo "current_id=$current_meta_ver" >> $GITHUB_OUTPUT + echo "current_id: $current_meta_ver" + + current_smart_ver=$(sed -n '2p' ./dev/core_version 2>/dev/null || echo "null") + echo "current_smart_id=$current_smart_ver" >> $GITHUB_OUTPUT + echo "current_smart_id: $current_smart_ver" + + - name: Get Upstream Version + id: upstream_id + run: | + # MetaCubeX/mihomo + git clone --depth=1 --branch=Alpha https://github.com/MetaCubeX/mihomo.git metacubex_mihomo + cd metacubex_mihomo + echo "upstream_id=alpha-g$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "upstream_id: alpha-g$(git rev-parse --short HEAD)" + cd .. + # vernesong/mihomo + git clone --depth=1 --branch=Alpha https://github.com/vernesong/mihomo.git vernesong_mihomo + cd vernesong_mihomo + echo "upstream_smart_id=alpha-smart-g$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "upstream_smart_id: alpha-smart-g$(git rev-parse --short HEAD)" + + Compile_Meta_Core: + runs-on: ubuntu-latest + needs: Get-Commit-id + if: ${{ needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id }} + strategy: + matrix: + arch: [normal, loongarch_abi1] + steps: + - name: Checkout OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install wget git tar + + - name: Setup UPX + run: | + wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz + tar xf upx-3.95-amd64_linux.tar.xz + echo "upx=${{ github.workspace }}/upx-3.95-amd64_linux/upx" >> $GITHUB_ENV + + - name: Prepare Makefiles and Temp Dirs for Meta + run: | + mkdir -p ${{ github.workspace }}/build_temp/meta_makefiles/normal + cp ${{ github.workspace }}/.github/makefile/meta ${{ github.workspace }}/build_temp/meta_makefiles/normal/Makefile + + mkdir -p ${{ github.workspace }}/build_temp/meta_makefiles/loong64_abi1 + cp ${{ github.workspace }}/.github/makefile/meta_loongarch_abi1 ${{ github.workspace }}/build_temp/meta_makefiles/loong64_abi1/Makefile + + mkdir -p ${{ github.workspace }}/build_temp/meta_output/${{ matrix.arch }}/bin + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: "1.25" + check-latest: true + cache: false + + - name: Define Go Archive Directory + id: go_paths + run: | + echo "archive_dir=${{ runner.temp }}/go-archives" >> $GITHUB_OUTPUT + mkdir -p ${{ runner.temp }}/go-archives + + - name: Cache Go loongarch abi1 tarball + id: cache_go_abi1 + uses: actions/cache@v4 + with: + path: ${{ steps.go_paths.outputs.archive_dir }}/go1.24.0.linux-amd64-abi1.tar.gz + key: go-loongarch-abi1-1.24.0-${{ matrix.arch }} + restore-keys: | + go-loongarch-abi1-1.24.0- + + - name: Clone MetaCubeX/mihomo source + uses: actions/checkout@v4 + with: + repository: MetaCubeX/mihomo + ref: Alpha + path: ./metacubex_src + + - name: Compile Meta Core for ${{ matrix.arch }} + run: | + set -e + cd ./metacubex_src + GO_ARCHIVE_DIR="${{ steps.go_paths.outputs.archive_dir }}" + + case "${{ matrix.arch }}" in + normal) + echo "Using default Go and Makefile" + cp ../build_temp/meta_makefiles/normal/Makefile ./Makefile + ;; + loongarch_abi1) + echo "Setup Go for loongarch abi1" + GO_TAR_FILE="go1.24.0.linux-amd64-abi1.tar.gz" + if [[ "${{ steps.cache_go_abi1.outputs.cache-hit }}" != 'true' ]]; then + echo "Cache not found for $GO_TAR_FILE, downloading..." + wget -q "https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/$GO_TAR_FILE" -O "$GO_ARCHIVE_DIR/$GO_TAR_FILE" + else + echo "Restored $GO_TAR_FILE from cache." + fi + sudo rm -rf /usr/local/go || true + sudo tar zxf "$GO_ARCHIVE_DIR/$GO_TAR_FILE" -C /usr/local + echo "/usr/local/go/bin" >> $GITHUB_PATH + cp ../build_temp/meta_makefiles/loong64_abi1/Makefile ./Makefile + ;; + esac + go version + make releases + cp -rf ./bin/. ../build_temp/meta_output/${{ matrix.arch }}/bin/ + rm -rf ./bin/* + + - name: Upload Meta Core Artifact + uses: actions/upload-artifact@v4 + with: + name: meta-core-${{ matrix.arch }} + path: build_temp/meta_output/${{ matrix.arch }}/bin + + Compile_Smart_Core: + runs-on: ubuntu-latest + needs: Get-Commit-id + if: ${{ needs.Get-Commit-id.outputs.current_smart_id != needs.Get-Commit-id.outputs.upstream_smart_id }} + strategy: + matrix: + arch: [normal, loongarch_abi1] + steps: + - name: Checkout OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install wget git tar + + - name: Setup UPX + run: | + wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz + tar xf upx-3.95-amd64_linux.tar.xz + echo "upx=${{ github.workspace }}/upx-3.95-amd64_linux/upx" >> $GITHUB_ENV + + - name: Prepare Makefiles and Temp Dirs for Smart + run: | + mkdir -p ${{ github.workspace }}/build_temp/smart_makefiles/normal + cp ${{ github.workspace }}/.github/makefile/meta ${{ github.workspace }}/build_temp/smart_makefiles/normal/Makefile + sed -i 's/VERSION=alpha-g/VERSION=alpha-smart-g/' ${{ github.workspace }}/build_temp/smart_makefiles/normal/Makefile + + mkdir -p ${{ github.workspace }}/build_temp/smart_makefiles/loong64_abi1 + cp ${{ github.workspace }}/.github/makefile/meta_loongarch_abi1 ${{ github.workspace }}/build_temp/smart_makefiles/loong64_abi1/Makefile + sed -i 's/VERSION=alpha-g/VERSION=alpha-smart-g/' ${{ github.workspace }}/build_temp/smart_makefiles/loong64_abi1/Makefile + + mkdir -p ${{ github.workspace }}/build_temp/smart_output/${{ matrix.arch }}/bin + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + check-latest: true + cache: false + + - name: Define Go Archive Directory + id: go_paths + run: | + echo "archive_dir=${{ runner.temp }}/go-archives" >> $GITHUB_OUTPUT + mkdir -p ${{ runner.temp }}/go-archives + + - name: Cache Go loongarch abi1 tarball + id: cache_go_abi1 + uses: actions/cache@v4 + with: + path: ${{ steps.go_paths.outputs.archive_dir }}/go1.24.0.linux-amd64-abi1.tar.gz + key: go-loongarch-abi1-1.24.0-${{ matrix.arch }} + restore-keys: | + go-loongarch-abi1-1.24.0- + + - name: Clone vernesong/mihomo source + uses: actions/checkout@v4 + with: + repository: vernesong/mihomo + ref: Alpha + path: ./vernesong_src + + - name: Compile Smart Core for ${{ matrix.arch }} + run: | + set -e + cd ./vernesong_src + GO_ARCHIVE_DIR="${{ steps.go_paths.outputs.archive_dir }}" + + case "${{ matrix.arch }}" in + normal) + echo "Using default Go and Makefile" + cp ../build_temp/smart_makefiles/normal/Makefile ./Makefile + ;; + loongarch_abi1) + echo "Setup Go for loongarch abi1" + GO_TAR_FILE="go1.24.0.linux-amd64-abi1.tar.gz" + if [ ! -f "$GO_ARCHIVE_DIR/$GO_TAR_FILE" ]; then + echo "CRITICAL: $GO_TAR_FILE not found in $GO_ARCHIVE_DIR. Attempting fallback download." + wget -q "https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/$GO_TAR_FILE" -O "$GO_ARCHIVE_DIR/$GO_TAR_FILE" + if [ ! -f "$GO_ARCHIVE_DIR/$GO_TAR_FILE" ]; then + echo "ERROR: Fallback download for $GO_TAR_FILE failed. Exiting." + exit 1 + fi + fi + sudo rm -rf /usr/local/go || true + sudo tar zxf "$GO_ARCHIVE_DIR/$GO_TAR_FILE" -C /usr/local + echo "/usr/local/go/bin" >> $GITHUB_PATH + cp ../build_temp/smart_makefiles/loong64_abi1/Makefile ./Makefile + ;; + esac + go version + make releases + cp -rf ./bin/. ../build_temp/smart_output/${{ matrix.arch }}/bin/ + rm -rf ./bin/* + + - name: Upload Smart Core Artifact + uses: actions/upload-artifact@v4 + with: + name: smart-core-${{ matrix.arch }} + path: build_temp/smart_output/${{ matrix.arch }}/bin + + Publish: + runs-on: ubuntu-latest + needs: [Get-Commit-id, Compile_Meta_Core, Compile_Smart_Core] + if: always() && (needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id || needs.Get-Commit-id.outputs.current_smart_id != needs.Get-Commit-id.outputs.upstream_smart_id) + steps: + - name: Checkout OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Download All Artifacts + uses: actions/download-artifact@v4 + with: + path: ./downloaded-artifacts + + - name: Organize Artifacts, Update Version, and Prepare Commit Data + id: organize_commit_data + run: | + commit_description="" + changes_made_to_dev_meta=false + changes_made_to_dev_smart=false + core_version_updated=false + + mkdir -p ./dev/meta + mkdir -p ./dev/smart + + if [ "${{ needs.Get-Commit-id.outputs.current_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_id }}" ]; then + echo "Organizing MetaCubeX compiled files..." + if [ "${{ needs.Compile_Meta_Core.result }}" == "success" ] || [ "${{ needs.Compile_Meta_Core.result }}" == "skipped" ]; then + rm -rf ./dev/meta/* + for arch_type in normal loongarch_abi1; do + if [ -d ./downloaded-artifacts/meta-core-$arch_type ] && [ -n "$(ls -A ./downloaded-artifacts/meta-core-$arch_type)" ]; then + echo "Copying meta-core-$arch_type files..." + cp -rf ./downloaded-artifacts/meta-core-$arch_type/. ./dev/meta/ + changes_made_to_dev_meta=true + fi + done + if [ "$changes_made_to_dev_meta" = true ]; then + git add ./dev/meta + commit_description="Meta to ${{ needs.Get-Commit-id.outputs.upstream_id }}" + fi + else + echo "Compile_Meta_Core job failed, skipping..." + fi + fi + + if [ "${{ needs.Get-Commit-id.outputs.current_smart_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" ]; then + echo "Organizing vernesong compiled files..." + if [ "${{ needs.Compile_Smart_Core.result }}" == "success" ] || [ "${{ needs.Compile_Smart_Core.result }}" == "skipped" ]; then + rm -rf ./dev/smart/* + for arch_type in normal loongarch_abi1; do + if [ -d ./downloaded-artifacts/smart-core-$arch_type ] && [ -n "$(ls -A ./downloaded-artifacts/smart-core-$arch_type)" ]; then + echo "Copying smart-core-$arch_type files..." + cp -rf ./downloaded-artifacts/smart-core-$arch_type/. ./dev/smart/ + changes_made_to_dev_smart=true + fi + done + if [ "$changes_made_to_dev_smart" = true ]; then + git add ./dev/smart + if [ -n "$commit_description" ]; then + commit_description="$commit_description, " + fi + commit_description="${commit_description}Smart to ${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" + fi + else + echo "Compile_Smart_Core job failed, skipping..." + fi + fi + + final_meta_id="${{ needs.Get-Commit-id.outputs.current_id }}" + if [ "${{ needs.Get-Commit-id.outputs.current_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_id }}" ] && [ "$changes_made_to_dev_meta" = true ]; then + final_meta_id="${{ needs.Get-Commit-id.outputs.upstream_id }}" + fi + + final_smart_id="${{ needs.Get-Commit-id.outputs.current_smart_id }}" + if [ "${{ needs.Get-Commit-id.outputs.current_smart_id }}" != "${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" ] && [ "$changes_made_to_dev_smart" = true ]; then + final_smart_id="${{ needs.Get-Commit-id.outputs.upstream_smart_id }}" + fi + + echo "Updating ./dev/core_version with:" + echo "Meta: $final_meta_id" + echo "Smart: $final_smart_id" + + current_core_version_content=$(cat ./dev/core_version 2>/dev/null || printf "null\nnull") + new_core_version_content=$(printf "%s\n%s" "$final_meta_id" "$final_smart_id") + + if [ "$current_core_version_content" != "$new_core_version_content" ]; then + echo "$final_meta_id" > ./dev/core_version + echo "$final_smart_id" >> ./dev/core_version + git add ./dev/core_version + core_version_updated=true + echo "./dev/core_version updated." + else + echo "./dev/core_version is already up to date." + fi + echo "Content of ./dev/core_version after potential update:" + cat ./dev/core_version + + overall_changes_exist=false + if [ "$changes_made_to_dev_meta" = true ] || [ "$changes_made_to_dev_smart" = true ] || [ "$core_version_updated" = true ]; then + overall_changes_exist=true + fi + + echo "commit_description_output=$commit_description" >> $GITHUB_OUTPUT + echo "overall_changes_exist_output=$overall_changes_exist" >> $GITHUB_OUTPUT + + - name: Commit and push + env: + COMMIT_DESCRIPTION: ${{ steps.organize_commit_data.outputs.commit_description_output }} + OVERALL_CHANGES_EXIST: ${{ steps.organize_commit_data.outputs.overall_changes_exist_output }} + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + + echo "Commit description from output: $COMMIT_DESCRIPTION" + echo "Overall changes exist from output: $OVERALL_CHANGES_EXIST" + + if ! git diff --staged --quiet || [ "$OVERALL_CHANGES_EXIST" = "true" ]; then + final_commit_message="Auto update" + if [ -n "$COMMIT_DESCRIPTION" ]; then + final_commit_message="$final_commit_message: $COMMIT_DESCRIPTION" + elif [ "$OVERALL_CHANGES_EXIST" = "true" ]; then + final_commit_message="$final_commit_message: Update core versions" + fi + + echo "Committing with message: $final_commit_message" + git commit -m "$final_commit_message" + git push + else + echo "No changes to commit." + fi \ No newline at end of file diff --git a/.github/workflows/compile_new_ipk.yml b/.github/workflows/compile_new_ipk.yml new file mode 100644 index 0000000000..de31f5b1e3 --- /dev/null +++ b/.github/workflows/compile_new_ipk.yml @@ -0,0 +1,202 @@ +name: Compile The New Version OpenClash + +on: + push: + branches: + - dev + paths: + - 'luci-app-openclash/Makefile' + workflow_dispatch: + +jobs: + Get-Version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + current_version: ${{ steps.current_version.outputs.version }} + steps: + - name: Clone Repository + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: New Version + id: version + run: | + echo "version=$(grep 'PKG_VERSION:=' ./luci-app-openclash/Makefile |awk -F '=' '{print $2}')" >> $GITHUB_OUTPUT + + - name: Clone Repository + uses: actions/checkout@v4 + with: + ref: package + + - name: Current Version + id: current_version + run: | + echo "version=$(sed -n 1p ./${{ github.ref_name }}/version |awk -F 'v' '{print $2}')" >> $GITHUB_OUTPUT + + Compile: + runs-on: ubuntu-latest + needs: Get-Version + if: ${{ needs.Get-Version.outputs.version != needs.Get-Version.outputs.current_version }} + strategy: + matrix: + target: + - { name: ipk, sdk_url: "https://mirrors.pku.edu.cn/openwrt/releases/22.03.0/targets/x86/64/openwrt-sdk-22.03.0-x86-64_gcc-11.2.0_musl.Linux-x86_64.tar.xz", sdk_tar: "SDK.tar.xz", sdk_backup_tar: "SDK.tar.bz2", sdk_dir: "SDK", artifact_pattern: "luci-app-openclash_*.ipk" } + - { name: apk, sdk_url: "snapshot", sdk_tar: "SNAPSDK.tar.zst", sdk_backup_tar: "SNAPSDK.tar.zst", sdk_dir: "SNAPSDK", artifact_pattern: "luci-app-openclash-*.apk" } + steps: + - name: Checkout OpenClash Source + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: Install Dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install curl git tar zstd python3 python3-pyelftools python3-setuptools + + - name: Download SDK + run: | + mkdir -p tmp + if [ "${{ matrix.target.name }}" = "ipk" ]; then + if ! curl -SLk --connect-timeout 30 --retry 2 "${{ matrix.target.sdk_url }}" -o "./tmp/${{ matrix.target.sdk_tar }}"; then + curl -SLk --connect-timeout 30 --retry 2 "https://archive.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2" -o "./tmp/SDK.tar.xz" + cd tmp + tar xjf ${{ matrix.target.sdk_backup_tar }} + mv OpenWrt-SDK-* ${{ matrix.target.sdk_dir }} + else + cd tmp + tar xf ${{ matrix.target.sdk_tar }} + mv openwrt-sdk-* ${{ matrix.target.sdk_dir }} + fi + else + BASE_URL="https://downloads.openwrt.org/snapshots/targets/x86/64/" + SDK_NAME=$(curl -s $BASE_URL | grep -oE 'openwrt-sdk-x86-64[^"]+\.tar\.zst' | head -n 1) + if ! curl -SLk --connect-timeout 30 --retry 2 "$BASE_URL/$SDK_NAME" -o "./tmp/${{ matrix.target.sdk_tar }}"; then + BASE_URL2="https://mirrors.pku.edu.cn/files/openwrt/snapshots/targets/x86/64/" + SDK_NAME=$(curl -s $BASE_URL2 | grep -oE 'openwrt-sdk-x86-64[^"]+\.tar\.zst' | head -n 1) + curl -SLk --connect-timeout 30 --retry 2 "$BASE_URL2/$SDK_NAME" -o "./tmp/${{ matrix.target.sdk_tar }}" + fi + cd tmp + zstd -d ${{ matrix.target.sdk_tar }} + tar xf SNAPSDK.tar + SDK_DIR=$(tar tf SNAPSDK.tar | head -n 1 | cut -d/ -f1) + mv "$SDK_DIR" "${{ matrix.target.sdk_dir }}" + fi + + - name: Copy OpenClash Source Codes + run: | + mkdir -p tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash + cp -rf ./luci-app-openclash/. tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/ + + - name: Copy OpenClash Source Codes + run: | + mkdir -p tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash + cp -rf ./luci-app-openclash/. tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/ + + - name: Update Third-Party Resources + run: | + CHNR_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/china_ip_route.ipset + CHNR6_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/china_ip6_route.ipset + ZASHBOARD_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/usr/share/openclash/ui/zashboard + METACUBEXD_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/usr/share/openclash/ui/metacubexd + GEOSITE_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/GeoSite.dat + COUNTRY_MMDB_PATH=tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/root/etc/openclash/Country.mmdb + + # 更新 China IP Route + mkdir -p tmp + curl -sSL https://ispip.clang.cn/all_cn.txt -o tmp/china_ip_route.ipset + mkdir -p $(dirname $CHNR_PATH) + cp tmp/china_ip_route.ipset $CHNR_PATH + curl -sSL https://ispip.clang.cn/all_cn_ipv6.txt -o tmp/china_ip6_route.ipset + mkdir -p $(dirname $CHNR6_PATH) + cp tmp/china_ip6_route.ipset $CHNR6_PATH + + # 更新 MetaCubeXD UI + mkdir -p tmp/metacubexd_zip + curl -sSL https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip -o tmp/metacubexd.zip + unzip -q -o tmp/metacubexd.zip -d tmp/metacubexd_zip + METACUBEXD_UNZIP_PATH="tmp/metacubexd_zip/metacubexd-gh-pages" + mkdir -p "$METACUBEXD_PATH" + rm -rf "$METACUBEXD_PATH"/* + cp -rf "$METACUBEXD_UNZIP_PATH"/* "$METACUBEXD_PATH" + + # 更新 ZashBoard UI + mkdir -p tmp/zashboard_zip + curl -sSL https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip -o tmp/zashboard.zip + unzip -q -o tmp/zashboard.zip -d tmp/zashboard_zip + mkdir -p "$ZASHBOARD_PATH" + rm -rf "$ZASHBOARD_PATH"/* + cp -rf tmp/zashboard_zip/dist/* "$ZASHBOARD_PATH" + + # 更新 GeoSite.dat + RULES_RELEASE=$(curl -s https://api.github.com/repos/Loyalsoldier/v2ray-rules-dat/releases/latest | jq -r '.tag_name') + curl -sSL "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/download/${RULES_RELEASE}/geosite.dat" -o tmp/GeoSite.dat + mkdir -p $(dirname "$GEOSITE_PATH") + cp tmp/GeoSite.dat "$GEOSITE_PATH" + + # 更新 Country.mmdb + curl -sSL "https://github.com/alecthw/mmdb_china_ip_list/releases/latest/download/Country-lite.mmdb" -o tmp/Country.mmdb + mkdir -p $(dirname "$COUNTRY_MMDB_PATH") + cp tmp/Country.mmdb "$COUNTRY_MMDB_PATH" + + - name: Compile po2lmo + run: | + cd tmp/${{ matrix.target.sdk_dir }}/package/luci-app-openclash/tools/po2lmo + make && sudo make install + + - name: Compile OpenClash + run: | + cd tmp/${{ matrix.target.sdk_dir }} + if [ -f ../${{ matrix.target.sdk_tar }} ]; then + make defconfig + fi + make package/luci-app-openclash/compile V=99 + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: openclash-${{ matrix.target.name }} + path: tmp/${{ matrix.target.sdk_dir }}/bin/packages/x86_64/base/${{ matrix.target.artifact_pattern }} + + Post-Process: + runs-on: ubuntu-latest + needs: [Compile, Get-Version] + steps: + - name: Checkout package branch + uses: actions/checkout@v4 + with: + ref: package + + - name: Download IPK Artifact + uses: actions/download-artifact@v4 + with: + name: openclash-ipk + path: ./ + + - name: Download APK Artifact + uses: actions/download-artifact@v4 + with: + name: openclash-apk + path: ./ + + - name: Commit and Push New Version + run: | + rm -rf ./${{ github.ref_name }}/luci-app-openclash* + echo "v${{ needs.Get-Version.outputs.version }}" > ./${{ github.ref_name }}/version + echo "https://img.shields.io/badge/New Release-v${{ needs.Get-Version.outputs.version }}-orange.svg" >> ./${{ github.ref_name }}/version + cp ./luci-app-openclash_*.ipk "./${{ github.ref_name }}/" + cp ./luci-app-openclash-*.apk "./${{ github.ref_name }}/" + rm -f ./luci-app-openclash_*.ipk + rm -f ./luci-app-openclash-*.apk + sed -i -E "s/OpenClash\/tree\/v(.*)/OpenClash\/tree\/v${{ needs.Get-Version.outputs.version }}/g" ./${{ github.ref_name }}/README.md + sed -i -E "s/OpenClash\/releases\/tag\/v(.*)/OpenClash\/releases\/tag\/v${{ needs.Get-Version.outputs.version }}/g" ./${{ github.ref_name }}/README.md + sed -i -E "s/source code-v(.*)-green/source code-v${{ needs.Get-Version.outputs.version }}-green/g" ./${{ github.ref_name }}/README.md + sed -i -E "s/New Release-v(.*)-orange/New Release-v${{ needs.Get-Version.outputs.version }}-orange/g" ./${{ github.ref_name }}/README.md + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "Auto Release: v${{ needs.Get-Version.outputs.version }}" + git push \ No newline at end of file diff --git a/.github/workflows/compile_premium_core.yml b/.github/workflows/compile_premium_core.yml new file mode 100644 index 0000000000..9e9271c35a --- /dev/null +++ b/.github/workflows/compile_premium_core.yml @@ -0,0 +1,95 @@ + +name: Compile The New Clash Premium Core + +on: + #schedule: + # - cron: "0 21 * * 1,3,5" + workflow_dispatch: + +jobs: + Get-Commit-id: + runs-on: ubuntu-latest + outputs: + current_id: ${{ steps.current_id.outputs.current_id }} + upstream_id: ${{ steps.upstream_id.outputs.upstream_id }} + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install curl git gzip + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Get Current Version + id: current_id + run: | + echo "current_id=$(sed -n 2p ./dev/core_version)" >> $GITHUB_OUTPUT + echo "current_id: $(sed -n 2p ./dev/core_version)" + + - name: Get Upstream Version + id: upstream_id + run: | + cd .. + mkdir tmp + GZNAME="clash-latest.gz" + curl -kSL --retry 2 https://release.dreamacro.workers.dev/latest//clash-linux-amd64-latest.gz -o ./tmp/$GZNAME + gzip -d ./tmp/$GZNAME + chmod +x ./tmp/clash-latest + echo "upstream_id=$(./tmp/clash-latest -v |awk -F ' ' '{print $2}')" >> $GITHUB_OUTPUT + echo "upstream_id: $(./tmp/clash-latest -v |awk -F ' ' '{print $2}')" + + Compile: + runs-on: ubuntu-latest + needs: Get-Commit-id + if: ${{ needs.Get-Commit-id.outputs.current_id != needs.Get-Commit-id.outputs.upstream_id }} + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install curl git tar gzip + + - name: Setup UPX + run: | + cd .. + wget https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz + tar xf upx-3.95-amd64_linux.tar.xz + echo "upx=../upx-3.95-amd64_linux/upx" >> $GITHUB_ENV + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Copy Scripts + run: | + cd .. + mkdir tmp + mkdir tmp/bin + cp ./OpenClash/.github/makefile/tun.sh ./tmp/tun.sh + + - name: Compile Premium Clash + run: | + bash ../tmp/tun.sh + rm -rf ../tmp/tun.sh + + - name: Change Version + run: | + sed -i '2d' ./dev/core_version + sed -i "2i ${{ needs.Get-Commit-id.outputs.upstream_id }}" ./dev/core_version + + - name: Commit and push + run: | + rm -rf ./dev/premium/* + cp -rf "../tmp/." "./dev/premium/" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "Auto update Premium core: ${{ needs.Get-Commit-id.outputs.upstream_id }}" + git push diff --git a/.github/workflows/delete_old_workflow.yml b/.github/workflows/delete_old_workflow.yml new file mode 100644 index 0000000000..42892408d5 --- /dev/null +++ b/.github/workflows/delete_old_workflow.yml @@ -0,0 +1,17 @@ +name: Delete old workflow runs +on: + schedule: + - cron: '0 0 1 * *' +# Run monthly, at 00:00 on the 1st day of month. + +jobs: + del_runs: + runs-on: ubuntu-latest + steps: + - name: Delete workflow runs + uses: Mattraks/delete-workflow-runs@v2 + with: + token: ${{ github.token }} + repository: ${{ github.repository }} + retain_days: 30 + keep_minimum_runs: 6 \ No newline at end of file diff --git a/.github/workflows/master_release_sync.yml b/.github/workflows/master_release_sync.yml new file mode 100644 index 0000000000..887732b819 --- /dev/null +++ b/.github/workflows/master_release_sync.yml @@ -0,0 +1,62 @@ +name: Sync Core and Package to Master When release +on: + push: + tags: + - "v*" + workflow_dispatch: + +jobs: + Sync_core_to_master: + runs-on: ubuntu-latest + steps: + - name: Apt Update + env: + DEBIAN_FRONTEND: noninteractive + run: | + sudo apt-get update + sudo apt-get -y install git + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: core + + - name: Sync dev core to master + run: | + if [ -n "$(diff -r ./master/ ./dev/)" ]; then + rm -rf ./master/premium + rm -rf ./master/dev + rm -rf ./master/meta/* + rm -rf ./master/smart/* + rm -rf ./master/core_version + cp -rf "./dev/." "./master/" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "Release: Auto sync dev core" + git push + else + echo "master core already sync to dev, exit" + fi + + - name: Clone OpenClash Repository + uses: actions/checkout@v4 + with: + ref: package + + - name: Sync dev package to master + run: | + if [ -n "$(diff -r ./master/ ./dev/)" ]; then + rm -rf ./master/README.md + rm -rf ./master/luci-app-openclash* + rm -rf ./master/version + cp -rf "./dev/." "./master/" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add . + git commit -m "Release: Auto sync dev package" + git push + else + echo "master package already sync to dev, exit" + fi + \ No newline at end of file diff --git a/LICENSE b/LICENSE index b130363c31..bbf66334d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 vernesong +Copyright (c) 2019-2024 vernesong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5c86731381..8ba9626cca 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,191 @@ -
- - A Clash Client For OpenWrt Based on Clash、 Clash For Openwrt. -
- - -# Preview - -* State -
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+本插件是一个可运行在 OpenWrt 上的 Clash 客户端 +
++兼容 Shadowsocks、ShadowsocksR、Vmess、Trojan、Snell 等协议,根据灵活的规则配置实现策略代理 +
++- 感谢 frainzy1477 ,本插件基于 Luci For Clash 进行二次开发 - +
+ +使用手册 +--- + + +* [Wiki](https://github.com/vernesong/OpenClash/wiki) + + +下载地址 +--- + + +* IPK [前往下载](https://github.com/vernesong/OpenClash/releases) + + +依赖 +--- + +* luci +* luci-base +* dnsmasq-full +* bash +* curl +* ca-bundle +* ipset +* ip-full +* ruby +* ruby-yaml +* unzip +* iptables(iptables) +* kmod-ipt-nat(iptables) +* iptables-mod-tproxy(iptables) +* iptables-mod-extra(iptables) +* kmod-tun(TUN模式) +* luci-compat(Luci >= 19.07) +* ip6tables-mod-nat(iptables-ipv6) +* kmod-inet-diag(PROCESS-NAME) +* kmod-nft-tproxy(Firewall4) + + +编译 +--- + + +从 OpenWrt 的 [SDK](https://archive.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2) 编译 +```bash +# 解压下载好的 SDK +curl -SLk --connect-timeout 30 --retry 2 "https://archive.openwrt.org/chaos_calmer/15.05.1/ar71xx/generic/OpenWrt-SDK-15.05.1-ar71xx-generic_gcc-4.8-linaro_uClibc-0.9.33.2.Linux-x86_64.tar.bz2" -o "/tmp/SDK.tar.bz2" +cd \tmp +tar xjf SDK.tar.bz2 +cd OpenWrt-SDK-15.05.1-* + +# Clone 项目 +mkdir package/luci-app-openclash +cd package/luci-app-openclash +git init +git remote add -f origin https://github.com/vernesong/OpenClash.git +git config core.sparsecheckout true +echo "luci-app-openclash" >> .git/info/sparse-checkout +git pull --depth 1 origin master +git branch --set-upstream-to=origin/master master + +# 编译 po2lmo (如果有po2lmo可跳过) +pushd luci-app-openclash/tools/po2lmo +make && sudo make install +popd + +# 开始编译 + +# 先回退到SDK主目录 +cd ../.. +make package/luci-app-openclash/luci-app-openclash/compile V=99 + +# IPK文件位置 +./bin/ar71xx/packages/base/luci-app-openclash_*-beta_all.ipk +``` + +```bash +# 同步源码 +cd package/luci-app-openclash/luci-app-openclash +git pull + +# 您也可以直接拷贝 `luci-app-openclash` 文件夹至其他 `OpenWrt` 项目的 `Package` 目录下随固件编译 + +make menuconfig +# 选择要编译的包 LuCI -> Applications -> luci-app-openclash + +``` + + +许可 +--- + + +* [MIT License](https://github.com/vernesong/OpenClash/blob/master/LICENSE) +* 内核 [clash](https://github.com/Dreamacro/clash) by [Dreamacro](https://github.com/Dreamacro) +* 本项目代码基于 [Luci For Clash](https://github.com/frainzy1477/luci-app-clash) by [frainzy1477](https://github.com/frainzy1477) +* GEOIP数据库 [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) by [MaxMind](https://www.maxmind.com) +* IP检查 [MyIP](https://github.com/SukkaW/MyIP) by [SukkaW](https://github.com/SukkaW) +* 控制面板 [clash-dashboard](https://github.com/Dreamacro/clash-dashboard) by [Dreamacro](https://github.com/Dreamacro) +* 控制面板 [yacd](https://github.com/haishanh/yacd) by [haishanh](https://github.com/haishanh) +* lhie1规则 [lhie1-Rules](https://github.com/lhie1/Rules) by [lhie1](https://github.com/lhie1) +* ConnersHua规则 [ConnersHua-Rules](https://github.com/ConnersHua/Profiles/tree/master) by [ConnersHua](https://github.com/ConnersHua) +* 游戏规则 [SSTap-Rule](https://github.com/FQrabbit/SSTap-Rule) by [FQrabbit](https://github.com/FQrabbit) +* 流媒体解锁检测 [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) by [lmc999](https://github.com/lmc999) + +请作者喝杯咖啡 +--- + +* PayPal + + +* USDT-TRC20 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
]] +align_mid_off = [[
]] + +function IsYamlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-5,-1)) + return e == ".yaml" +end +function IsYmlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-4,-1)) + return e == ".yml" +end + +function default_config_set(f) + local cf = fs.uci_get_config("config", "config_path") + if cf == "/etc/openclash/config/"..f or not cf or cf == "" or not fs.isfile(cf) then + if CHIF == "1" and cf == "/etc/openclash/config/"..f then + return + end + local fis = fs.glob("/etc/openclash/config/*")[1] + if fis ~= nil then + fcf = fs.basename(fis) + if fcf then + uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..fcf) + uci:commit("openclash") + end + else + uci:set("openclash", "config", "config_path", "/etc/openclash/config/config.yaml") + uci:commit("openclash") + end + end +end + +ful = SimpleForm("upload", translate("Config Manage"), nil) +ful.reset = false +ful.submit = false + +sul =ful:section(SimpleSection, "") +o = sul:option(FileUpload, "") +o.template = "openclash/upload" +um = sul:option(DummyValue, "", nil) +um.template = "openclash/dvalue" + +local dir, fd, clash +clash = "/etc/openclash/clash" +dir = "/etc/openclash/config/" +bakck_dir="/etc/openclash/backup" +proxy_pro_dir="/etc/openclash/proxy_provider/" +rule_pro_dir="/etc/openclash/rule_provider/" +core_dir="/etc/openclash/core/core/" +backup_dir="/tmp/" +create_bakck_dir=fs.mkdir(bakck_dir) +create_proxy_pro_dir=fs.mkdir(proxy_pro_dir) +create_rule_pro_dir=fs.mkdir(rule_pro_dir) + +HTTP.setfilehandler( + function(meta, chunk, eof) + local fp = HTTP.formvalue("file_type") + if not fd then + if not meta then return end + + if fp == "config" then + if meta and chunk then fd = nixio.open(dir .. meta.file, "w") end + elseif fp == "proxy-provider" then + if meta and chunk then fd = nixio.open(proxy_pro_dir .. meta.file, "w") end + elseif fp == "rule-provider" then + if meta and chunk then fd = nixio.open(rule_pro_dir .. meta.file, "w") end + elseif fp == "clash_meta" then + create_core_dir=fs.mkdir(core_dir) + if meta and chunk then fd = nixio.open(core_dir .. meta.file, "w") end + elseif fp == "backup-file" then + if meta and chunk then fd = nixio.open(backup_dir .. meta.file, "w") end + end + + if not fd then + um.value = translate("upload file error.") + return + end + end + if chunk and fd then + fd:write(chunk) + end + if eof and fd then + fd:close() + fd = nil + if fp == "config" then + CHIF = "1" + if IsYamlFile(meta.file) then + local yamlbackup="/etc/openclash/backup/" .. meta.file + local c=fs.copy(dir .. meta.file,yamlbackup) + default_config_set(meta.file) + end + if IsYmlFile(meta.file) then + local ymlname=string.lower(string.sub(meta.file,0,-5)) + local ymlbackup="/etc/openclash/backup/".. ymlname .. ".yaml" + local c=fs.rename(dir .. meta.file,"/etc/openclash/config/".. ymlname .. ".yaml") + local c=fs.copy("/etc/openclash/config/".. ymlname .. ".yaml",ymlbackup) + local yamlname=ymlname .. ".yaml" + default_config_set(yamlname) + end + um.value = translate("File saved to") .. ' "/etc/openclash/config/"' + elseif fp == "proxy-provider" then + um.value = translate("File saved to") .. ' "/etc/openclash/proxy_provider/"' + elseif fp == "rule-provider" then + um.value = translate("File saved to") .. ' "/etc/openclash/rule_provider/"' + elseif fp == "clash_meta" then + local archive_path = core_dir .. meta.file + if string.lower(string.sub(meta.file, -7, -1)) == ".tar.gz" then + -- tar.gz + os.execute(string.format("tar -C '/etc/openclash/core/core' -xzf '%s' >/dev/null 2>&1", archive_path)) + os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path)) + local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.tar.gz' ! -name '*.tar' ! -name '*.gz' 2>/dev/null | head -1" + local first_file = io.popen(first_file_cmd):read("*line") + if first_file and first_file ~= "" then + os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp)) + end + elseif string.lower(string.sub(meta.file, -4, -1)) == ".tar" then + -- tar + os.execute(string.format("tar -C '/etc/openclash/core/core' -xf '%s' >/dev/null 2>&1", archive_path)) + os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path)) + local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.tar' ! -name '*.gz' 2>/dev/null | head -1" + local first_file = io.popen(first_file_cmd):read("*line") + if first_file and first_file ~= "" then + os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp)) + end + elseif string.lower(string.sub(meta.file, -3, -1)) == ".gz" then + -- gz + os.execute(string.format("gzip -fd '%s' >/dev/null 2>&1", archive_path)) + os.execute(string.format("rm -f '%s' >/dev/null 2>&1", archive_path)) + local first_file_cmd = "find /etc/openclash/core/core -type f ! -name '*.gz' 2>/dev/null | head -1" + local first_file = io.popen(first_file_cmd):read("*line") + if first_file and first_file ~= "" then + os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", first_file, fp)) + end + else + os.execute(string.format("mv '%s' '/etc/openclash/core/%s' >/dev/null 2>&1", (core_dir .. meta.file), fp)) + end + + os.execute(string.format("chmod 4755 '/etc/openclash/core/%s' >/dev/null 2>&1", fp)) + os.execute(string.format("rm -rf %s >/dev/null 2>&1", core_dir)) + um.value = translate("File saved to") .. ' "/etc/openclash/core/"' + elseif fp == "backup-file" then + os.execute("tar -C '/etc/openclash/' -xzf %s >/dev/null 2>&1" % (backup_dir .. meta.file)) + os.execute("mv /etc/openclash/openclash /etc/config/openclash >/dev/null 2>&1") + fs.unlink(backup_dir .. meta.file) + um.value = translate("Backup File Restore Successful!") + end + fs.unlink("/tmp/Proxy_Group") + end + end +) + +if HTTP.formvalue("upload") then + if not um.value then + um.value = translate("No Specify Upload File") + end +end + +local e,a={} +for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do +a=fs.stat(o) +if a then +e[t]={} +e[t].name=fs.basename(o) +BACKUP_FILE="/etc/openclash/backup/".. e[t].name +if fs.mtime(BACKUP_FILE) then + e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(BACKUP_FILE)) +else + e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",a.mtime) +end +if fs.uci_get_config("config", "config_path") and string.sub(fs.uci_get_config("config", "config_path"), 23, -1) == e[t].name then + e[t].state=translate("Enabled") +else + e[t].state=translate("Disabled") +end +e[t].size=fs.filesize(a.size) +e[t].remove=0 +end +end + +form=SimpleForm("config_file_list",translate("Config File List")) +form.reset=false +form.submit=false +tb=form:section(Table,e) +st=tb:option(DummyValue,"state",translate("State")) +nm=tb:option(DummyValue,"name",translate("Config Alias")) +sb=tb:option(DummyValue,"name",translate("Subscription Info")) +mt=tb:option(DummyValue,"mtime",translate("Update Time")) +sz=tb:option(DummyValue,"size",translate("Size")) +st.template="openclash/cfg_check" +sb.template="openclash/sub_info_show" + +btnis=tb:option(Button,"switch",translate("SwiTch")) +btnis.template="openclash/other_button" +btnis.render=function(o,t,a) +if not e[t] then return false end +if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then +a.display="" +else +a.display="none" +end +o.inputstyle="apply" +Button.render(o,t,a) +end +btnis.write=function(a,t) +fs.unlink("/tmp/Proxy_Group") +uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name) +uci:commit("openclash") +HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config")) +end + +btnrn=tb:option(DummyValue,"/etc/openclash/config/",translate("Rename")) +btnrn.template="openclash/input_rename" +btnrn.rawhtml = true +btnrn.render=function(c,t,a) + if not e[t] then return end + c.value = e[t].name + DummyValue.render(c,t,a) +end + +local actions = tb:option(ListValue, "actions", translate("Other")) +actions.render = function(self, t, a) + if not e[t] then return end + self.keylist = {} + self.vallist = {} + -- Edit + table.insert(self.keylist, "edit") + table.insert(self.vallist, translate("Edit")) + + -- Copy + if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then + table.insert(self.keylist, "copy") + table.insert(self.vallist, translate("Copy Config")) + end + + -- Download + table.insert(self.keylist, "download") + table.insert(self.vallist, translate("Download Config")) + + -- Download Running + if NXFS.access("/etc/openclash/"..e[t].name) then + table.insert(self.keylist, "download_run") + table.insert(self.vallist, translate("Download Running Config")) + end + + -- Remove + table.insert(self.keylist, "remove") + table.insert(self.vallist, translate("Remove")) + + ListValue.render(self, t, a) +end + +local btnapply = tb:option(Button, "apply", translate("Apply")) +btnapply.inputstyle = "apply" +btnapply.write = function(self, t) + if not e[t] then return end + local action = self.map:formvalue("cbid." .. self.map.config .. "." .. t .. ".actions") + + if action == "edit" then + local file_path = "etc/openclash/config/" .. fs.basename(e[t].name) + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config", "%s") % file_path) + elseif action == "copy" then + local num = 1 + while true do + num = num + 1 + if not fs.isfile("/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml") then + fs.copy("/etc/openclash/config/"..e[t].name, "/etc/openclash/config/"..fs.filename(e[t].name).."("..num..")"..".yaml") + break + end + end + HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config")) + elseif action == "download" then + local sPath, sFile, fd, block + sPath = "/etc/openclash/config/"..e[t].name + sFile = NXFS.basename(sPath) + if fs.isdirectory(sPath) then + fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r") + sFile = sFile .. ".tar.gz" + else + fd = nixio.open(sPath, "r") + end + if not fd then return end + HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile}) + HTTP.prepare_content("application/octet-stream") + while true do + block = fd:read(nixio.const.buffersize) + if (not block) or (#block == 0) then break end + HTTP.write(block) + end + fd:close() + HTTP.close() + elseif action == "download_run" then + local sPath, sFile, fd, block + sPath = "/etc/openclash/"..e[t].name + sFile = NXFS.basename(sPath) + if fs.isdirectory(sPath) then + fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r") + sFile = sFile .. ".tar.gz" + else + fd = nixio.open(sPath, "r") + end + if not fd then return end + HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile}) + HTTP.prepare_content("application/octet-stream") + while true do + block = fd:read(nixio.const.buffersize) + if (not block) or (#block == 0) then break end + HTTP.write(block) + end + fd:close() + HTTP.close() + elseif action == "remove" then + fs.unlink("/tmp/Proxy_Group") + fs.unlink("/etc/openclash/backup/"..fs.basename(e[t].name)) + fs.unlink("/etc/openclash/history/"..fs.filename(e[t].name)..".db") + fs.unlink("/etc/openclash/"..fs.basename(e[t].name)) + local a=fs.unlink("/etc/openclash/config/"..fs.basename(e[t].name)) + default_config_set(fs.basename(e[t].name)) + if a then table.remove(e,t) end + HTTP.redirect(DISP.build_url("admin", "services", "openclash","config")) + end +end + +p = SimpleForm("provider_file_manage",translate("Provider File Manage")) +p.reset = false +p.submit = false + +local provider_manage = { + {proxy_mg, rule_mg, game_mg} +} + +promg = p:section(Table, provider_manage) + +o = promg:option(Button, "proxy_mg", " ") +o.inputtitle = translate("Proxy Provider File List") +o.inputstyle = "reload" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage")) +end + +o = promg:option(Button, "rule_mg", " ") +o.inputtitle = translate("Rule Providers File List") +o.inputstyle = "reload" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage")) +end + +o = promg:option(Button, "game_mg", " ") +o.inputtitle = translate("Game Rules File List") +o.inputstyle = "reload" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage")) +end + +m = SimpleForm("openclash",translate("Config File Edit")) +m.reset = false +m.submit = false + +local tab = { + {user, default} +} + +s = m:section(Table, tab) +s.description = align_mid..translate("Support syntax check, press").." "..font_green..bold_on.."F10"..bold_off..font_off.." "..translate("to control diff option, press").." "..font_green..bold_on.."F11"..bold_off..font_off.." "..translate("to enter full screen editing mode")..align_mid_off +s.anonymous = true +s.addremove = false + +local conf = fs.uci_get_config("config", "config_path") +local dconf = "/usr/share/openclash/res/default.yaml" +if not conf then conf = "/etc/openclash/config/config.yaml" end +local conf_name = fs.basename(conf) +if not conf_name then conf_name = "config.yaml" end +local sconf = "/etc/openclash/"..conf_name + +sev = s:option(TextValue, "user") +---sev.description = align_mid..translate("Modify Your Config file:").." "..font_green..bold_on..conf_name..bold_off..font_off.." "..translate("Here, Except The Settings That Were Taken Over")..align_mid_off +sev.rows = 40 +sev.wrap = "off" +sev.cfgvalue = function(self, section) + return NXFS.readfile(conf) or NXFS.readfile(dconf) or "" +end +sev.write = function(self, section, value) +if (CHIF == "0") then + value = value:gsub("\r\n?", "\n") + local old_value = NXFS.readfile(conf) + if value ~= old_value then + NXFS.writefile(conf, value) + end +end +end + +def = s:option(TextValue, "default") +if fs.isfile(sconf) then + ---def.description = align_mid..translate("Config File Edited By OpenClash For Running")..align_mid_off +else + ---def.description = align_mid..translate("Default Config File With Correct Template")..align_mid_off +end +def.rows = 40 +def.wrap = "off" +def.readonly = true +def.cfgvalue = function(self, section) + return NXFS.readfile(sconf) or NXFS.readfile(dconf) or "" +end +def.write = function(self, section, value) +end + +local t = { + {Commit, Create, Apply} +} + +a = m:section(Table, t) + +o = a:option(Button, "Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + fs.unlink("/tmp/Proxy_Group") + uci:commit("openclash") +end + +o = a:option(DummyValue, "Create", " ") +o.rawhtml = true +o.template = "openclash/input_file_name" +o.value = "/etc/openclash/config/" + +o = a:option(Button, "Apply", " ") +o.inputtitle = translate("Apply Settings") +o.inputstyle = "apply" +o.write = function() + fs.unlink("/tmp/Proxy_Group") + uci:set("openclash", "config", "enable", 1) + uci:commit("openclash") + SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &") + HTTP.redirect(DISP.build_url("admin", "services", "openclash")) +end + +m:append(Template("openclash/config_editor")) + +return ful , form , p , m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/custom-dns-edit.lua b/luci-app-openclash/luasrc/model/cbi/openclash/custom-dns-edit.lua new file mode 100644 index 0000000000..3a16ade337 --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/custom-dns-edit.lua @@ -0,0 +1,176 @@ +local m, s, o +local openclash = "openclash" +local uci = luci.model.uci.cursor() +local fs = require "luci.openclash" +local SYS = require "luci.sys" +local sid = arg[1] + +font_red = [[]] +font_green = [[]] +font_off = [[]] +bold_on = [[]] +bold_off = [[]] + +m = Map(openclash, translate("Add Custom DNS Servers")) +m.pageaction = false +m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-overwrite") +if m.uci:get(openclash, sid) ~= "dns_servers" then + luci.http.redirect(m.redirect) + return +end + +-- [[ Edit Custom DNS ]] -- +s = m:section(NamedSection, sid, "dns_servers") +s.anonymous = true +s.addremove = false + +---- group +o = s:option(ListValue, "group", translate("DNS Server Group")) +o.description = font_red..bold_on..translate("NameServer Group Must Be Set")..bold_off..font_off +o:value("nameserver", translate("NameServer ")) +o:value("fallback", translate("FallBack ")) +o:value("default", translate("Default-NameServer")) +o.default = "nameserver" +o.rempty = false + +---- IP address +o = s:option(Value, "ip", translate("DNS Server Address")) +o.description = translate("Do Not Add Type Ahead") +o.placeholder = translate("Not Null") +o.datatype = "or(host, string)" +o.rmempty = true + +---- port +o = s:option(Value, "port", translate("DNS Server Port")) +o.description = translate("Require When Use Non-Standard Port") +o.datatype = "port" +o.rempty = true + +---- type +o = s:option(ListValue, "type", translate("DNS Server Type")) +o.description = translate("Communication protocol") +o:value("udp", translate("UDP")) +o:value("tcp", translate("TCP")) +o:value("tls", translate("TLS")) +o:value("https", translate("HTTPS")) +o:value("quic", translate("QUIC")) +o.default = "udp" +o.rempty = false + +---- interface +o = s:option(Value, "interface", translate("Specific Interface")) +o.description = translate("DNS Lookup Only Through The Specific Interface") +local interfaces = SYS.exec("ls -l /sys/class/net/ 2>/dev/null |awk '{print $9}' 2>/dev/null") +for interface in string.gmatch(interfaces, "%S+") do + o:value(interface) +end +o:value("Disable", translate("Disable")) +o.default = "Disable" +o.rempty = false + +---- direct-nameserver +o = s:option(Flag, "direct_nameserver", translate("Direct Nameserver"), translate("Use For Domain Need Direct")) +o.rmempty = false +o.default = o.disbled + +---- Node Domain Resolve +o = s:option(Flag, "node_resolve", translate("Node Domain Resolve"), translate("Use For Node Domain Resolve")) +o.rmempty = false +o.default = o.disbled + +---- Force HTTP/3 +o = s:option(Flag, "http3", translate("Force HTTP/3"), translate("Force HTTP/3 to connect")) +o:depends("type", "https") +o.rmempty = false +o.default = o.disbled + +---- Skip-cert-verify +o = s:option(Flag, "skip_cert_verify", translate("skip-cert-verify"), translate("skip-cert-verify")) +o:depends("type", "https") +o:depends("type", "quic") +o.rmempty = false +o.default = o.disbled + +---- ECS Subnet +o = s:option(Value, "ecs_subnet", translate("ECS Subnet"),translate("Specify the ECS Subnet Address")) +o:depends("type", "https") +o.rmempty = true +o.datatype = "ipaddr" +o:value("1.1.1.1/24") + +---- ECS Override +o = s:option(Flag, "ecs_override", translate("ECS Override"),translate("Override the ECS Subnet Address")) +o:depends("type", "https") +o.rmempty = false +o.default = o.disbled + +---- disable-ipv4 +o = s:option(Flag, "disable_ipv4", translate("Disable-IPv4"),translate("Drop The Type of DNS Responsed")) +o.rmempty = false +o.default = o.disbled + +---- disable-ipv6 +o = s:option(Flag, "disable_ipv6", translate("Disable-IPv6"),translate("Drop The Type of DNS Responsed")) +o.rmempty = false +o.default = o.disbled + +---- disable-qtype +o = s:option(DynamicList, "disable_qtype", translate("Disable-Qtype"),translate("Drop The Type of DNS Responsed")) +o.rmempty = true +o.datatype = "uinteger" +o.default = o.disbled + +---- Proxy group +o = s:option(Value, "specific_group", translate("Specific Group (Support Regex)")) +o.description = translate("Group Use For Proxy The DNS") +o:depends("group", "nameserver") +o:depends("group", "fallback") +local groupnames,filename +filename = m.uci:get(openclash, "config", "config_path") +if filename then + groupnames = SYS.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',filename)) + if groupnames then + for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do + if groupname ~= nil and groupname ~= "" then + o:value(groupname) + end + end + end +end + +m.uci:foreach("openclash", "groups", +function(s) + if s.name ~= "" and s.name ~= nil then + o:value(s.name) + end +end) + +o:value("DIRECT") +o:value("GLOBAL") +o:value("Disable", translate("Disable")) +o.default = "Disable" +o.rempty = false + +local t = { + {Commit, Back} +} +a = m:section(Table, t) + +o = a:option(Button,"Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + m.uci:commit(openclash) + luci.http.redirect(m.redirect) +end + +o = a:option(Button,"Back", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + m.uci:revert(openclash, sid) + luci.http.redirect(m.redirect) +end + +m:append(Template("openclash/toolbar_show")) +return m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/game-rules-file-manage.lua b/luci-app-openclash/luasrc/model/cbi/openclash/game-rules-file-manage.lua new file mode 100644 index 0000000000..26be4bc7f2 --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/game-rules-file-manage.lua @@ -0,0 +1,123 @@ + +local rule_form +local openclash = "openclash" +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local DISP = require "luci.dispatcher" +local UTIL = require "luci.util" +local fs = require "luci.openclash" +local uci = require "luci.model.uci".cursor() + +local g,h={} +for n,m in ipairs(fs.glob("/etc/openclash/game_rules/*"))do +h=fs.stat(m) +if h then +g[n]={} +g[n].num=string.format(n) +g[n].name=fs.basename(m) +g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime) +g[n].size=fs.filesize(h.size) +g[n].remove=0 +g[n].enable=false +end +end + +rule_form=SimpleForm("game_rules_file_list",translate("Game Rules File List")) +rule_form.reset=false +rule_form.submit=false +tb2=rule_form:section(Table,g) +nu2=tb2:option(DummyValue,"num",translate("Serial Number")) +nm2=tb2:option(DummyValue,"name",translate("File Name")) +mt2=tb2:option(DummyValue,"mtime",translate("Update Time")) +sz2=tb2:option(DummyValue,"size",translate("Size")) + +btned1=tb2:option(Button,"edit",translate("Edit")) +btned1.render=function(g,n,h) +g.inputstyle="apply" +Button.render(g,n,h) +end +btned1.write=function(h,n) + local file_path = "etc/openclash/game_rules/" .. fs.basename(g[n].name) + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "game-rules-file-manage", "%s") %file_path) +end + +btndl2 = tb2:option(Button,"download2",translate("Download Config")) +btndl2.template="openclash/other_button" +btndl2.render=function(m,n,h) +m.inputstyle="remove" +Button.render(m,n,h) +end +btndl2.write = function (h,n) + local sPath, sFile, fd, block + sPath = "/etc/openclash/game_rules/"..g[n].name + sFile = NXFS.basename(sPath) + if fs.isdirectory(sPath) then + fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r") + sFile = sFile .. ".tar.gz" + else + fd = nixio.open(sPath, "r") + end + if not fd then + return + end + HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile}) + HTTP.prepare_content("application/octet-stream") + while true do + block = fd:read(nixio.const.buffersize) + if (not block) or (#block ==0) then + break + else + HTTP.write(block) + end + end + fd:close() + HTTP.close() +end + +btnrm2=tb2:option(Button,"remove2",translate("Remove")) +btnrm2.render=function(g,n,h) +g.inputstyle="reset" +Button.render(g,n,h) +end +btnrm2.write=function(h,n) +local h=fs.unlink("/etc/openclash/game_rules/"..luci.openclash.basename(g[n].name)) +if h then table.remove(g,n)end +return h +end + +local t = { + {Refresh, Create, Delete_all, Apply} +} + +a = rule_form:section(Table, t) + +o = a:option(Button, "Refresh", " ") +o.inputtitle = translate("Refresh Page") +o.inputstyle = "apply" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage")) +end + +o = a:option(DummyValue, "Create", " ") +o.rawhtml = true +o.template = "openclash/input_file_name" +o.value = "/etc/openclash/game_rules/" + +o = a:option(Button, "Delete_all", " ") +o.inputtitle = translate("Delete All File") +o.inputstyle = "remove" +o.write = function() + luci.sys.call("rm -rf /etc/openclash/game_rules/* >/dev/null 2>&1") + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-file-manage")) +end + +o = a:option(Button, "Apply", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config")) +end + +rule_form:append(Template("openclash/toolbar_show")) +return rule_form diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/game-rules-manage.lua b/luci-app-openclash/luasrc/model/cbi/openclash/game-rules-manage.lua new file mode 100644 index 0000000000..6b49941586 --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/game-rules-manage.lua @@ -0,0 +1,100 @@ + +local form, m +local openclash = "openclash" +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local DISP = require "luci.dispatcher" +local UTIL = require "luci.util" +local fs = require "luci.openclash" +local uci = require "luci.model.uci".cursor() + +m = SimpleForm("openclash", translate("Game Rules List")) +m.description=translate("Rule Project:").." SSTap-Rule ( https://github.com/FQrabbit/SSTap-Rule )" +m.reset = false +m.submit = false + +local t = { + {Refresh, Apply} +} + +a = m:section(Table, t) + +o = a:option(Button, "Refresh", " ") +o.inputtitle = translate("Refresh Page") +o.inputstyle = "apply" +o.write = function() + SYS.call("rm -rf /tmp/rules_name 2>/dev/null") + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage")) +end + +o = a:option(Button, "Apply", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-settings")) +end + +if not NXFS.access("/tmp/rules_name") then + SYS.call("awk -F ',' '{print $1}' /usr/share/openclash/res/game_rules.list > /tmp/rules_name 2>/dev/null") +end +file = io.open("/tmp/rules_name", "r"); + +---- Rules List +local e={},o,t +if NXFS.access("/tmp/rules_name") then +for o in file:lines() do +table.insert(e,o) +end +for t,o in ipairs(e) do +e[t]={} +e[t].num=string.format(t) +e[t].name=o +e[t].filename=string.sub(luci.sys.exec(string.format("grep -F '%s,' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $3}' 2>/dev/null",e[t].name)),1,-2) +if e[t].filename == "" then +e[t].filename=string.sub(luci.sys.exec(string.format("grep -F '%s,' /usr/share/openclash/res/game_rules.list |awk -F ',' '{print $2}' 2>/dev/null",e[t].name)),1,-2) +end +RULE_FILE="/etc/openclash/game_rules/".. e[t].filename +if fs.mtime(RULE_FILE) then +e[t].size=fs.filesize(fs.stat(RULE_FILE).size) +e[t].mtime=os.date("%Y-%m-%d %H:%M:%S",fs.mtime(RULE_FILE)) +else +e[t].size="/" +e[t].mtime="/" +end +if fs.isfile(RULE_FILE) then + e[t].exist=translate("Exist") +else + e[t].exist=translate("Not Exist") +end +e[t].remove=0 +end +end +file:close() + +form=SimpleForm("filelist") +form.reset=false +form.submit=false +tb=form:section(Table,e) +nu=tb:option(DummyValue,"num",translate("Serial Number")) +st=tb:option(DummyValue,"exist",translate("State")) +st.template="openclash/cfg_check" +nm=tb:option(DummyValue,"name",translate("Rule Name")) +fm=tb:option(DummyValue,"filename",translate("File Name")) +sz=tb:option(DummyValue,"size",translate("Size")) +mt=tb:option(DummyValue,"mtime",translate("Update Time")) + +btnis=tb:option(DummyValue,"filename",translate("Download Rule")) +btnis.template="openclash/download_rule" + +btnrm=tb:option(Button,"remove",translate("Remove")) +btnrm.render=function(e,t,a) +e.inputstyle="reset" +Button.render(e,t,a) +end +btnrm.write=function(a,t) +fs.unlink("/etc/openclash/game_rules/"..e[t].filename) +HTTP.redirect(DISP.build_url("admin", "services", "openclash", "game-rules-manage")) +end + +return m, form diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua b/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua new file mode 100644 index 0000000000..5c7f5121fb --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/groups-config.lua @@ -0,0 +1,221 @@ + +local m, s, o +local openclash = "openclash" +local uci = luci.model.uci.cursor() +local fs = require "luci.openclash" +local sys = require "luci.sys" +local sid = arg[1] + +font_red = [[]] +font_off = [[]] +bold_on = [[]] +bold_off = [[]] + +function IsYamlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-5,-1)) + return e == ".yaml" +end +function IsYmlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-4,-1)) + return e == ".yml" +end + +m = Map(openclash, translate("Edit Group")) +m.pageaction = false +m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers") +if m.uci:get(openclash, sid) ~= "groups" then + luci.http.redirect(m.redirect) + return +end + +-- [[ Groups Setting ]]-- +s = m:section(NamedSection, sid, "groups") +s.anonymous = true +s.addremove = false + +o = s:option(ListValue, "config", translate("Config File")) +o:value("all", translate("Use For All Config File")) +local e,a={} +for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do + a=fs.stat(f) + if a then + e[t]={} + e[t].name=fs.basename(f) + if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then + o:value(e[t].name) + end + end +end + +o = s:option(ListValue, "type", translate("Group Type")) +o.rmempty = false +o.description = translate("Choose The Operation Mode") +o:value("select", translate("Manual-Select")) +o:value("smart", translate("Smart-Select")) +o:value("url-test", translate("URL-Test")) +o:value("fallback", translate("Fallback")) +o:value("load-balance", translate("Load-Balance")) + +o = s:option(Value, "name", translate("Group Name")) +o.rmempty = false +o.default = "Group - "..sid + +o = s:option(ListValue, "disable_udp", translate("Disable UDP")) +o:value("false", translate("Disable")) +o:value("true", translate("Enable")) +o.default = "false" +o.rmempty = true + +o = s:option(Value, "test_url", translate("Test URL")) +o:value("http://cp.cloudflare.com/generate_204") +o:value("http://www.gstatic.com/generate_204") +o:value("https://cp.cloudflare.com/generate_204") +o.rmempty = true +o:depends("type", "url-test") +o:depends("type", "fallback") +o:depends("type", "load-balance") +o:depends("type", "smart") + +o = s:option(Value, "test_interval", translate("Test Interval(s)")) +o.default = "300" +o.rmempty = true +o:depends("type", "url-test") +o:depends("type", "fallback") +o:depends("type", "load-balance") +o:depends("type", "smart") + +o = s:option(ListValue, "strategy", translate("Strategy Type")) +o.rmempty = true +o.description = translate("Choose The Load-Balance's Strategy Type") +o:value("round-robin", translate("Round-robin")) +o:value("consistent-hashing", translate("Consistent-hashing")) +o:value("sticky-sessions", translate("Sticky-sessions")) +o:depends("type", "load-balance") + +o = s:option(ListValue, "uselightgbm", translate("Uselightgbm")) +o.description = translate("Use LightGBM Model For Smart Group Weight Prediction") +o:value("false", translate("Disable")) +o:value("true", translate("Enable")) +o.default = "false" +o.rmempty = true +o:depends("type", "smart") + +o = s:option(ListValue, "collectdata", translate("Collectdata")) +o.description = translate("Collect Datas For Smart Group Model Training") +o:value("false", translate("Disable")) +o:value("true", translate("Enable")) +o.default = "false" +o.rmempty = true +o:depends("type", "smart") + +o = s:option(Value, "policy_priority", translate("Policy Priority")) +o.description = translate("The Priority Of The Nodes, The Higher Than 1, The More Likely It Is To Be Selected, The Default Is 1, Support Regex") +o.rmempty = true +o.placeholder = "Premium:0.9;SG:1.3" +o.rmempty = true +o:depends("type", "smart") + +o = s:option(Value, "tolerance", translate("Tolerance(ms)")) +o.default = "150" +o.rmempty = true +o:depends("type", "url-test") + +o = s:option(Value, "policy_filter", translate("Provider Filter")) +o.rmempty = true +o.placeholder = "bgp|sg" + +o = s:option(Value, "icon", translate("Icon")) +o.rmempty = true + +-- [[ other-setting ]]-- +o = s:option(Value, "other_parameters", translate("Other Parameters")) +o.template = "cbi/tvalue" +o.rows = 20 +o.wrap = "off" +o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off +o.rmempty = true +function o.cfgvalue(self, section) + if self.map:get(section, "other_parameters") == nil then + return "# Example:\n".. + "# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n".. + "# 示例:\n".. + "# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n".. + "# type: select\n".. + "# proxies:\n".. + "# - DIRECT\n".. + "# - ss\n".. + "# use:\n".. + "# - provider1\n".. + "# - provider1\n".. + "# url: 'https://www.gstatic.com/generate_204'\n".. + "# interval: 300\n".. + "# lazy: true\n".. + "# timeout: 5000\n".. + "# max-failed-times: 5\n".. + "# disable-udp: true\n".. + "# interface-name: en0\n".. + "# routing-mark: 11451\n".. + "# include-all: false\n".. + "# include-all-proxies: false\n".. + "# include-all-providers: false\n".. + "# filter: \"(?i)港|hk|hongkong|hong kong\"\n".. + "# exclude-filter: \"美|日\"\n".. + "# exclude-type: \"Shadowsocks|Http\"\n".. + "# expected-status: 204\n".. + "# hidden: true\n".. + "# icon: xxx" + else + return Value.cfgvalue(self, section) + end +end +function o.validate(self, value) + if value then + value = value:gsub("\r\n?", "\n") + value = value:gsub("%c*$", "") + end + return value +end + +o = s:option(DynamicList, "other_group", translate("Other Group (Support Regex)")) +o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT' & 'REJECT-DROP' & 'PASS' & 'GLOBAL'")..bold_off..font_off +o:value("all", translate("All Groups")) +uci:foreach("openclash", "groups", + function(s) + if s.name ~= "" and s.name ~= nil and s.name ~= m.uci:get(openclash, sid, "name") then + o:value(s.name) + end + end) +o:value("DIRECT") +o:value("REJECT") +o:value("REJECT-DROP") +o:value("PASS") +o:value("GLOBAL") +o.rmempty = true + +local t = { + {Commit, Back} +} +a = m:section(Table, t) + +o = a:option(Button,"Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + m.uci:commit(openclash) + sys.call("/usr/share/openclash/yml_groups_name_ch.sh") + luci.http.redirect(m.redirect) +end + +o = a:option(Button,"Back", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + m.uci:revert(openclash, sid) + luci.http.redirect(m.redirect) +end + +m:append(Template("openclash/toolbar_show")) +m:append(Template("openclash/config_editor")) +return m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/log.lua b/luci-app-openclash/luasrc/model/cbi/openclash/log.lua new file mode 100644 index 0000000000..57bff6935d --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/log.lua @@ -0,0 +1,22 @@ +-- +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" + +m = Map("openclash", translate("Server Logs")) +s = m:section(TypedSection, "openclash") +m.pageaction = false +s.anonymous = true +s.addremove=false + +log = s:option(TextValue, "clog") +log.readonly=true +log.pollcheck=true +log.template="openclash/log" +log.description = translate("") +log.rows = 29 + +m:append(Template("openclash/toolbar_show")) +m:append(Template("openclash/config_editor")) + +return m \ No newline at end of file diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua b/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua new file mode 100644 index 0000000000..4b60c68115 --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua @@ -0,0 +1,73 @@ +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local fs = require "luci.openclash" +local file_path = "" +local edit_file_name = "/tmp/openclash_edit_file_name" + +for i = 2, #(arg) do + file_path = file_path .. "/" .. luci.http.urlencode(arg[i]) +end + +if not fs.isfile(file_path) and file_path ~= "" then + file_path = luci.http.urldecode(file_path) +end + +--re-get file path to save +if NXFS.readfile(edit_file_name) ~= file_path and fs.isfile(file_path) then + NXFS.writefile(edit_file_name, file_path) +else + if not fs.isfile(file_path) and fs.isfile(edit_file_name) then + file_path = NXFS.readfile(edit_file_name) + fs.unlink(edit_file_name) + end +end + +m = Map("openclash", translate("File Edit")) +m.pageaction = false +m.redirect = luci.dispatcher.build_url("admin/services/openclash/"..arg[1]) +s = m:section(TypedSection, "openclash") +s.anonymous = true +s.addremove=false + +o = s:option(TextValue, "edit_file") +o.rows = 50 +o.wrap = "off" + +function o.write(self, section, value) + if value then + value = value:gsub("\r\n?", "\n") + local old_value = NXFS.readfile(file_path) + if value ~= old_value then + NXFS.writefile(file_path, value) + end + end +end + +function o.cfgvalue(self, section) + return NXFS.readfile(file_path) or "" +end + +local t = { + {Commit, Back} +} + +a = m:section(Table, t) + +o = a:option(Button, "Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + luci.http.redirect(m.redirect) +end + +o = a:option(Button,"Back", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + luci.http.redirect(m.redirect) +end + +m:append(Template("openclash/config_editor")) +m:append(Template("openclash/toolbar_show")) +return m \ No newline at end of file diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/other-rules-edit.lua b/luci-app-openclash/luasrc/model/cbi/openclash/other-rules-edit.lua new file mode 100644 index 0000000000..adc46290ba --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/other-rules-edit.lua @@ -0,0 +1,347 @@ + +local m, s, o +local openclash = "openclash" +local uci = luci.model.uci.cursor() +local fs = require "luci.openclash" +local sys = require "luci.sys" +local sid = arg[1] + +font_red = [[]] +font_green = [[]] +font_off = [[]] +bold_on = [[]] +bold_off = [[]] + +function IsYamlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-5,-1)) + return e == ".yaml" +end +function IsYmlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-4,-1)) + return e == ".yml" +end + +m = Map(openclash, translate("Other Rules Edit")) +m.pageaction = false +m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-overwrite") +if m.uci:get(openclash, sid) ~= "other_rules" then + luci.http.redirect(m.redirect) + return +end + +-- [[ Other Rules Setting ]]-- +s = m:section(NamedSection, sid, "other_rules") +s.anonymous = true +s.addremove = false + +o = s:option(Value, "Note", translate("Note")) +o.default = "default" +o.rmempty = false + +o = s:option(ListValue, "config", translate("Config File")) +local e,a={} +local groupnames,filename +local group_list = {} +for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do + a=fs.stat(f) + if a then + e[t]={} + e[t].name=fs.basename(f) + if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then + o:value(e[t].name) + end + if e[t].name == m.uci:get(openclash, sid, "config") then + filename = e[t].name + groupnames = sys.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',f)) + if groupnames then + for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do + if groupname ~= nil and groupname ~= "" then + table.insert(group_list, groupname) + end + end + end + end + end +end + +m.uci:foreach("openclash", "groups", +function(s) + if s.name ~= "" and s.name ~= nil then + table.insert(group_list, s.name) + end +end) + +table.sort(group_list) +table.insert(group_list, "DIRECT") +table.insert(group_list, "REJECT") +table.insert(group_list, "REJECT-DROP") +table.insert(group_list, "PASS") +table.insert(group_list, "GLOBAL") + +o = s:option(Button, translate("Get Group Names")) +o.title = translate("Get Group Names") +o.inputtitle = translate("Get Group Names") +o.description = translate("Get Group Names After Select Config File") +o.inputstyle = "reload" +o.write = function() + m.uci:commit("openclash") + luci.http.redirect(luci.dispatcher.build_url("admin/services/openclash/other-rules-edit/%s") % sid) +end + +if group_list ~= nil and filename ~= nil then +o = s:option(ListValue, "rule_name", translate("Other Rules Name")) +o.rmempty = true +o:value("lhie1", translate("lhie1 Rules")) + +o = s:option(ListValue, "GlobalTV", translate("GlobalTV")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "AsianTV", translate("AsianTV")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "MainlandTV", translate("CN Mainland TV")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Proxy", translate("Proxy")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Youtube", translate("Youtube")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Bilibili", translate("Bilibili")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Bahamut", translate("Bahamut")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "HBOMax", translate("HBO Max")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Pornhub", translate("Pornhub")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Apple", translate("Apple")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "AppleTV", translate("Apple TV")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "GoogleFCM", translate("Google FCM")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Scholar", translate("Scholar")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Microsoft", translate("Microsoft")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "AI_Suite", translate("AI Suite")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Netflix", translate("Netflix")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Disney", translate("Disney Plus")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Discovery", translate("Discovery Plus")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "DAZN", translate("DAZN")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Spotify", translate("Spotify")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Steam", translate("Steam")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "TikTok", translate("TikTok")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "miHoYo", translate("miHoYo")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Speedtest", translate("Speedtest")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Telegram", translate("Telegram")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Crypto", translate("Crypto")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Discord", translate("Discord")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "PayPal", translate("PayPal")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "AdBlock", translate("AdBlock")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "HTTPDNS", translate("HTTPDNS")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Domestic", translate("Domestic")) +o:depends("rule_name", "lhie1") +o.rmempty = true +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o = s:option(ListValue, "Others", translate("Others")) +o:depends("rule_name", "lhie1") +o.rmempty = true +o.description = translate("Choose Proxy Groups, Base On Your Config File").." ( "..font_green..bold_on..filename..bold_off..font_off.." )" +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +end + +local t = { + {Commit, Back} +} +a = m:section(Table, t) + +o = a:option(Button,"Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + m.uci:commit(openclash) + --luci.http.redirect(m.redirect) +end + +o = a:option(Button,"Back", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + m.uci:revert(openclash, sid) + luci.http.redirect(m.redirect) +end + +m:append(Template("openclash/toolbar_show")) +return m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/proxy-provider-config.lua b/luci-app-openclash/luasrc/model/cbi/openclash/proxy-provider-config.lua new file mode 100644 index 0000000000..242bed72ab --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/proxy-provider-config.lua @@ -0,0 +1,194 @@ + +local m, s, o +local openclash = "openclash" +local uci = luci.model.uci.cursor() +local sys = require "luci.sys" +local sid = arg[1] +local fs = require "luci.openclash" + +font_red = [[]] +font_off = [[]] +bold_on = [[]] +bold_off = [[]] + +function IsYamlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-5,-1)) + return e == ".yaml" +end +function IsYmlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-4,-1)) + return e == ".yml" +end + +m = Map(openclash, translate("Edit Proxy-Provider")) +m.pageaction = false +m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers") +if m.uci:get(openclash, sid) ~= "proxy-provider" then + luci.http.redirect(m.redirect) + return +end + +-- [[ Provider Setting ]]-- +s = m:section(NamedSection, sid, "proxy-provider") +s.anonymous = true +s.addremove = false + +o = s:option(ListValue, "config", translate("Config File")) +o:value("all", translate("Use For All Config File")) +local e,a={} +for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do + a=fs.stat(f) + if a then + e[t]={} + e[t].name=fs.basename(f) + if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then + o:value(e[t].name) + end + end +end + +o = s:option(Flag, "manual", translate("Custom Tag")) +o.rmempty = false +o.default = "0" +o.description = translate("Mark as Custom Node to Prevent Retention config from being Deleted When Enabled") + +o = s:option(ListValue, "type", translate("Provider Type")) +o.rmempty = true +o.description = translate("Choose The Provider Type") +o:value("http") +o:value("file") +o:value("inline") + +o = s:option(Value, "name", translate("Provider Name")) +o.rmempty = false +o.default = "Proxy-provider - "..sid + +o = s:option(ListValue, "path", translate("Provider Path")) +o.description = translate("Update Your Proxy Provider File From Config Luci Page") +local p,h={} +for t,f in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do + h=fs.stat(f) + if h then + p[t]={} + p[t].name=fs.basename(f) + if IsYamlFile(p[t].name) or IsYmlFile(p[t].name) then + o:value("./proxy_provider/"..p[t].name) + end + end +end +o.rmempty = false +o:depends("type", "file") + +o = s:option(Value, "provider_url", translate("Provider URL")) +o.rmempty = false +o:depends("type", "http") + +o = s:option(Value, "provider_filter", translate("Provider Filter")) +o.rmempty = true +o.placeholder = "bgp|sg" + +o = s:option(Value, "provider_interval", translate("Provider Interval(s)")) +o.default = "3600" +o.rmempty = false +o:depends("type", "http") + +o = s:option(ListValue, "health_check", translate("Provider Health Check")) +o:value("false", translate("Disable")) +o:value("true", translate("Enable")) +o.default = true + +o = s:option(Value, "health_check_url", translate("Health Check URL")) +o:value("http://cp.cloudflare.com/generate_204") +o:value("http://www.gstatic.com/generate_204") +o:value("https://cp.cloudflare.com/generate_204") +o.rmempty = false + +o = s:option(Value, "health_check_interval", translate("Health Check Interval(s)")) +o.default = "300" +o.rmempty = false + +-- [[ other-setting ]]-- +o = s:option(Value, "other_parameters", translate("Other Parameters")) +o.template = "cbi/tvalue" +o.rows = 20 +o.wrap = "off" +o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off +o.rmempty = true +function o.cfgvalue(self, section) + if self.map:get(section, "other_parameters") == nil then + return "# Example:\n".. + "# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n".. + "# 示例:\n".. + "# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n".. + "# header:\n".. + "# User-Agent:\n".. + "# - \"Clash/v1.18.0\"\n".. + "# - \"mihomo/1.18.3\"\n".. + "# Authorization:\n".. + "# - \"token 1231231\"\n".. + "# override:\n".. + "# skip-cert-verify: true\n".. + "# additional-prefix: \"provider1 prefix |\"\n".. + "# additional-suffix: \"| provider1 suffix\"\n".. + "# proxy-name:\n".. + "# - pattern: \"IPLC-(.*?)倍\"\n".. + "# target: \"iplc x $1\"\n".. + "# exclude-type: \"ss|http\"\n".. + "\n".. + "# inline Example:\n".. + "# payload:\n".. + "# - name: \"ss1\"\n".. + "# type: ss\n".. + "# server: server\n".. + "# port: 443\n".. + "# cipher: chacha20-ietf-poly1305\n".. + "# password: \"password\"" + else + return Value.cfgvalue(self, section) + end +end +function o.validate(self, value) + if value then + value = value:gsub("\r\n?", "\n") + value = value:gsub("%c*$", "") + end + return value +end + +o = s:option(DynamicList, "groups", translate("Proxy Group (Support Regex)")) +o.description = font_red..bold_on..translate("No Need Set when Config Create, The added Proxy Groups Must Exist")..bold_off..font_off +o.rmempty = true +o:value("all", translate("All Groups")) +m.uci:foreach("openclash", "groups", + function(s) + if s.name ~= "" and s.name ~= nil then + o:value(s.name) + end + end) + +local t = { + {Commit, Back} +} +a = m:section(Table, t) + +o = a:option(Button,"Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + m.uci:commit(openclash) + luci.http.redirect(m.redirect) +end + +o = a:option(Button,"Back", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + m.uci:revert(openclash, sid) + luci.http.redirect(m.redirect) +end + +m:append(Template("openclash/toolbar_show")) +m:append(Template("openclash/config_editor")) +return m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/proxy-provider-file-manage.lua b/luci-app-openclash/luasrc/model/cbi/openclash/proxy-provider-file-manage.lua new file mode 100644 index 0000000000..4549b19fb8 --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/proxy-provider-file-manage.lua @@ -0,0 +1,123 @@ + +local proxy_form +local openclash = "openclash" +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local DISP = require "luci.dispatcher" +local UTIL = require "luci.util" +local fs = require "luci.openclash" +local uci = require "luci.model.uci".cursor() + +local p,r={} +for x,y in ipairs(fs.glob("/etc/openclash/proxy_provider/*"))do +r=fs.stat(y) +if r then +p[x]={} +p[x].num=string.format(x) +p[x].name=fs.basename(y) +p[x].mtime=os.date("%Y-%m-%d %H:%M:%S",r.mtime) +p[x].size=fs.filesize(r.size) +p[x].remove=0 +p[x].enable=false +end +end + +proxy_form=SimpleForm("proxy_provider_file_list",translate("Proxy Provider File List")) +proxy_form.reset=false +proxy_form.submit=false +tb1=proxy_form:section(Table,p) +nu1=tb1:option(DummyValue,"num",translate("Serial Number")) +nm1=tb1:option(DummyValue,"name",translate("File Name")) +mt1=tb1:option(DummyValue,"mtime",translate("Update Time")) +sz1=tb1:option(DummyValue,"size",translate("Size")) + +btned1=tb1:option(Button,"edit",translate("Edit")) +btned1.render=function(p,x,r) +p.inputstyle="apply" +Button.render(p,x,r) +end +btned1.write=function(r,x) + local file_path = "etc/openclash/proxy_provider/" .. fs.basename(p[x].name) + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage", "%s") %file_path) +end + +btndl1 = tb1:option(Button,"download1",translate("Download Config")) +btndl1.template="openclash/other_button" +btndl1.render=function(y,x,r) +y.inputstyle="remove" +Button.render(y,x,r) +end +btndl1.write = function (r,x) + local sPath, sFile, fd, block + sPath = "/etc/openclash/proxy_provider/"..p[x].name + sFile = NXFS.basename(sPath) + if fs.isdirectory(sPath) then + fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r") + sFile = sFile .. ".tar.gz" + else + fd = nixio.open(sPath, "r") + end + if not fd then + return + end + HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile}) + HTTP.prepare_content("application/octet-stream") + while true do + block = fd:read(nixio.const.buffersize) + if (not block) or (#block ==0) then + break + else + HTTP.write(block) + end + end + fd:close() + HTTP.close() +end + +btnrm1=tb1:option(Button,"remove1",translate("Remove")) +btnrm1.render=function(p,x,r) +p.inputstyle="reset" +Button.render(p,x,r) +end +btnrm1.write=function(r,x) +local r=fs.unlink("/etc/openclash/proxy_provider/"..luci.openclash.basename(p[x].name)) +if r then table.remove(p,x)end +return r +end + +local t = { + {Refresh, Create, Delete_all, Apply} +} + +a = proxy_form:section(Table, t) + +o = a:option(Button, "Refresh", " ") +o.inputtitle = translate("Refresh Page") +o.inputstyle = "apply" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage")) +end + +o = a:option(DummyValue, "Create", " ") +o.rawhtml = true +o.template = "openclash/input_file_name" +o.value = "/etc/openclash/proxy_provider/" + +o = a:option(Button, "Delete_all", " ") +o.inputtitle = translate("Delete All File") +o.inputstyle = "remove" +o.write = function() + luci.sys.call("rm -rf /etc/openclash/proxy_provider/* >/dev/null 2>&1") + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "proxy-provider-file-manage")) +end + +o = a:option(Button, "Apply", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config")) +end + +proxy_form:append(Template("openclash/toolbar_show")) +return proxy_form diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-config.lua b/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-config.lua new file mode 100644 index 0000000000..1d47c96798 --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-config.lua @@ -0,0 +1,208 @@ + +local m, s, o +local openclash = "openclash" +local uci = luci.model.uci.cursor() +local fs = require "luci.openclash" +local sys = require "luci.sys" +local sid = arg[1] + +font_red = [[]] +font_off = [[]] +bold_on = [[]] +bold_off = [[]] + +function IsYamlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-5,-1)) + return e == ".yaml" +end +function IsYmlFile(e) + e=e or"" + local e=string.lower(string.sub(e,-4,-1)) + return e == ".yml" +end + +m = Map(openclash, translate("Edit Rule Providers")) +m.pageaction = false +m.description=translate("规则集使用介绍:https://wiki.metacubex.one/config/rule-providers/content/") +m.redirect = luci.dispatcher.build_url("admin/services/openclash/rule-providers-settings") +if m.uci:get(openclash, sid) ~= "rule_providers" then + luci.http.redirect(m.redirect) + return +end + +-- [[ Rule Providers Setting ]]-- +s = m:section(NamedSection, sid, "rule_providers") +s.anonymous = true +s.addremove = false + +o = s:option(ListValue, "config", translate("Config File")) +o:value("all", translate("Use For All Config File")) +local e,a={} +for t,f in ipairs(fs.glob("/etc/openclash/config/*"))do + a=fs.stat(f) + if a then + e[t]={} + e[t].name=fs.basename(f) + if IsYamlFile(e[t].name) or IsYmlFile(e[t].name) then + o:value(e[t].name) + end + end +end + +o = s:option(Value, "name", translate("Rule Providers Name")) +o.rmempty = false +o.default = "Rule-provider - "..sid + +o = s:option(ListValue, "type", translate("Rule Providers Type")) +o.rmempty = true +o.description = translate("Choose The Rule Providers Type") +o:value("http") +o:value("file") +o:value("inline") + +o = s:option(ListValue, "format", translate("Rule Format")) +o.rmempty = true +o.description = translate("Choose The Rule File Format, For More Info:").." ".."https://wiki.metacubex.one/config/rule-providers/content/" +o:value("yaml") +o:value("text") +o:value("mrs") +o:depends("type", "file") +o:depends("type", "http") + +o = s:option(ListValue, "behavior", translate("Rule Behavior")) +o.rmempty = true +o.description = translate("Choose The Rule Behavior") +o:value("domain") +o:value("ipcidr") +o:value("classical", translate("classical").." "..translate("(Not Support mrs Format)")) + +o = s:option(ListValue, "path", translate("Rule Providers Path")) +o.description = translate("Update Your Rule Providers File From Config Luci Page") +local p,h={} +for t,f in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do + h=fs.stat(f) + if h then + p[t]={} + p[t].name=fs.basename(f) + o:value("./rule_provider/"..p[t].name) + end +end +for t,f in ipairs(fs.glob("/etc/openclash/game_rules/*"))do + h=fs.stat(f) + if h then + p[t]={} + p[t].name=fs.basename(f) + o:value("./game_rules/"..p[t].name) + end +end +o.rmempty = false +o:depends("type", "file") + +o = s:option(Value, "url", translate("Rule Providers URL")) +o.rmempty = false +o:depends("type", "http") + +o = s:option(Value, "interval", translate("Rule Providers Interval(s)")) +o.default = "86400" +o.rmempty = false +o:depends("type", "http") + +o = s:option(ListValue, "position", translate("Append Position")) +o.rmempty = false +o:value("0", translate("Priority Match")) +o:value("1", translate("Extended Match")) + +o = s:option(ListValue, "group", translate("Set Proxy Group")) +o.description = font_red..bold_on..translate("The Added Proxy Groups Must Exist Except 'DIRECT' & 'REJECT' & 'REJECT-DROP' & 'PASS' & 'GLOBAL'")..bold_off..font_off +o.rmempty = true + +local groupnames, filename +local group_list = {} + +filename = m.uci:get(openclash, "config", "config_path") +if filename then + groupnames = sys.exec(string.format('ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "YAML.load_file(\'%s\')[\'proxy-groups\'].each do |i| puts i[\'name\']+\'##\' end" 2>/dev/null',filename)) + if groupnames then + for groupname in string.gmatch(groupnames, "([^'##\n']+)##") do + if groupname ~= nil and groupname ~= "" then + table.insert(group_list, groupname) + end + end + end +end + +m.uci:foreach("openclash", "groups", +function(s) + if s.name ~= "" and s.name ~= nil then + table.insert(group_list, s.name) + end +end) + +table.sort(group_list) + +for _, groupname in ipairs(group_list) do + o:value(groupname) +end + +o:value("DIRECT") +o:value("REJECT") +o:value("REJECT-DROP") +o:value("PASS") +o:value("GLOBAL") + +-- [[ other-setting ]]-- +o = s:option(Value, "other_parameters", translate("Other Parameters")) +o.template = "cbi/tvalue" +o.rows = 20 +o.wrap = "off" +o.description = font_red..bold_on..translate("Edit Your Other Parameters Here")..bold_off..font_off +o.rmempty = true +function o.cfgvalue(self, section) + if self.map:get(section, "other_parameters") == nil then + return "# Example:\n".. + "# Only support YAML, four spaces need to be reserved at the beginning of each line to maintain formatting alignment\n".. + "# 示例:\n".. + "# 仅支持 YAML, 每行行首需要多保留四个空格以使脚本处理后能够与上方配置保持格式对齐\n".. + "# inline Example:\n".. + "# payload:\n".. + "# - '.blogger.com'\n".. + "# - '*.*.microsoft.com'\n".. + "# - 'books.itunes.apple.com'\n" + else + return Value.cfgvalue(self, section) + end +end +function o.validate(self, value) + if value then + value = value:gsub("\r\n?", "\n") + value = value:gsub("%c*$", "") + end + return value +end + +local t = { + {Commit, Back} +} +a = m:section(Table, t) + +o = a:option(Button,"Commit", " ") +o.inputtitle = translate("Commit Settings") +o.inputstyle = "apply" +o.write = function() + m.uci:commit(openclash) + sys.call("/usr/share/openclash/yml_groups_name_ch.sh") + luci.http.redirect(m.redirect) +end + +o = a:option(Button,"Back", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + m.uci:revert(openclash, sid) + luci.http.redirect(m.redirect) +end + +m:append(Template("openclash/toolbar_show")) +m:append(Template("openclash/config_editor")) +return m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-file-manage.lua b/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-file-manage.lua new file mode 100644 index 0000000000..4b23f123ff --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-file-manage.lua @@ -0,0 +1,123 @@ + +local rule_form +local openclash = "openclash" +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local DISP = require "luci.dispatcher" +local UTIL = require "luci.util" +local fs = require "luci.openclash" +local uci = require "luci.model.uci".cursor() + +local g,h={} +for n,m in ipairs(fs.glob("/etc/openclash/rule_provider/*"))do +h=fs.stat(m) +if h then +g[n]={} +g[n].num=string.format(n) +g[n].name=fs.basename(m) +g[n].mtime=os.date("%Y-%m-%d %H:%M:%S",h.mtime) +g[n].size=fs.filesize(h.size) +g[n].remove=0 +g[n].enable=false +end +end + +rule_form=SimpleForm("rule_provider_file_list",translate("Rule Providers File List")) +rule_form.reset=false +rule_form.submit=false +tb2=rule_form:section(Table,g) +nu2=tb2:option(DummyValue,"num",translate("Serial Number")) +nm2=tb2:option(DummyValue,"name",translate("File Name")) +mt2=tb2:option(DummyValue,"mtime",translate("Update Time")) +sz2=tb2:option(DummyValue,"size",translate("Size")) + +btned1=tb2:option(Button,"edit",translate("Edit")) +btned1.render=function(g,n,h) +g.inputstyle="apply" +Button.render(g,n,h) +end +btned1.write=function(h,n) + local file_path = "etc/openclash/rule_provider/" .. fs.basename(g[n].name) + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage", "%s") %file_path) +end + +btndl2 = tb2:option(Button,"download2",translate("Download Config")) +btndl2.template="openclash/other_button" +btndl2.render=function(m,n,h) +m.inputstyle="remove" +Button.render(m,n,h) +end +btndl2.write = function (h,n) + local sPath, sFile, fd, block + sPath = "/etc/openclash/rule_provider/"..g[n].name + sFile = NXFS.basename(sPath) + if fs.isdirectory(sPath) then + fd = io.popen('tar -C "%s" -cz .' % {sPath}, "r") + sFile = sFile .. ".tar.gz" + else + fd = nixio.open(sPath, "r") + end + if not fd then + return + end + HTTP.header('Content-Disposition', 'attachment; filename="%s"' % {sFile}) + HTTP.prepare_content("application/octet-stream") + while true do + block = fd:read(nixio.const.buffersize) + if (not block) or (#block ==0) then + break + else + HTTP.write(block) + end + end + fd:close() + HTTP.close() +end + +btnrm2=tb2:option(Button,"remove2",translate("Remove")) +btnrm2.render=function(g,n,h) +g.inputstyle="reset" +Button.render(g,n,h) +end +btnrm2.write=function(h,n) +local h=fs.unlink("/etc/openclash/rule_provider/"..luci.openclash.basename(g[n].name)) +if h then table.remove(g,n)end +return h +end + +local t = { + {Refresh, Create, Delete_all, Apply} +} + +a = rule_form:section(Table, t) + +o = a:option(Button, "Refresh", " ") +o.inputtitle = translate("Refresh Page") +o.inputstyle = "apply" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage")) +end + +o = a:option(DummyValue, "Create", " ") +o.rawhtml = true +o.template = "openclash/input_file_name" +o.value = "/etc/openclash/rule_provider/" + +o = a:option(Button, "Delete_all", " ") +o.inputtitle = translate("Delete All File") +o.inputstyle = "remove" +o.write = function() + luci.sys.call("rm -rf /etc/openclash/rule_provider/* >/dev/null 2>&1") + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "rule-providers-file-manage")) +end + +o = a:option(Button, "Apply", " ") +o.inputtitle = translate("Back Settings") +o.inputstyle = "reset" +o.write = function() + HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config")) +end + +rule_form:append(Template("openclash/toolbar_show")) +return rule_form diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-manage.lua b/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-manage.lua new file mode 100644 index 0000000000..d37959b7fc --- /dev/null +++ b/luci-app-openclash/luasrc/model/cbi/openclash/rule-providers-manage.lua @@ -0,0 +1,106 @@ + +local form, m +local openclash = "openclash" +local NXFS = require "nixio.fs" +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local DISP = require "luci.dispatcher" +local UTIL = require "luci.util" +local fs = require "luci.openclash" +local uci = require "luci.model.uci".cursor() + +m = SimpleForm("openclash", translate("Other Rule Providers List")) +m.description=translate("Rule Project:").." lhie1 ( https://github.com/dler-io/Rules )