mirror of
https://github.com/bestnite/sub2clash.git
synced 2025-11-03 12:20:36 +00:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
800c5ff7f1
|
|||
|
892fa7ce41
|
|||
|
007093ac48
|
|||
|
ed479b7efa
|
|||
|
dac4760289
|
|||
|
86b74f30e7
|
|||
|
1e8a79c2d2
|
|||
|
be656cca57
|
|||
|
23a85f573b
|
|||
|
fce75baed4
|
|||
| f5686561f9 | |||
| 83c24170a0 | |||
| 80d91efca4 | |||
| 83a728a415 | |||
| a178d06248 | |||
|
|
99d36d93d8 | ||
| 0a9892503d | |||
| ff81d03492 | |||
| 0fa95888cb | |||
| b44703fa0f | |||
| b256c5e809 | |||
| 307c36aa8d | |||
| 06da2e91c2 | |||
| 33d37e631b | |||
| c1012750ff | |||
| 69deed91df | |||
| 6a780a5e27 | |||
| f88ae52a29 | |||
| 0c8bbac2e6 | |||
| c4554d9030 | |||
| 8db2c46bf0 | |||
| 8b3ae45623 | |||
| cdc8ef7e32 | |||
|
|
62229fba97 | ||
| b80afd97f1 | |||
| 2a042968d0 | |||
| cdf69ce65f | |||
| 2d8508f390 | |||
| 73616c98a3 | |||
| 44163d30e1 | |||
| a2e97aaa01 | |||
| 4f3c2bb280 | |||
| 1b662de245 | |||
| 4a9297f4a3 | |||
| fcb1358846 | |||
| da9a17201b | |||
| 5b7a94e65c | |||
| b5fcbab1a5 | |||
|
|
0b8299f432 | ||
|
|
84c5a6e5f4 | ||
|
|
06887d91ac | ||
| db00433931 | |||
|
|
5b3a12f00d | ||
| d4d7010d8f | |||
|
|
2331cd4d18 | ||
|
|
88d8653ab5 | ||
|
|
cc0b73d7a4 | ||
| b57e4cf49f | |||
|
|
cc80e237d6 | ||
|
|
fefb4b895a | ||
| 66f214ae10 | |||
| f7dc78aabc | |||
| 6bb2d16e4b | |||
| 98ef93c7bb | |||
| 6e09c44d17 | |||
| 42fd251eb5 | |||
| e504a6cca4 | |||
| 3cfa4bdf24 | |||
| dedbf2bc03 | |||
|
|
7158ad467a | ||
|
|
fd22cd1499 | ||
| 0946412ea7 | |||
| 98bca0a7ac | |||
| 036907fba4 | |||
| 3e720ec14d | |||
| b73a02bdbf | |||
| 25e47453cb | |||
| 3330412243 | |||
| 35deaa015f | |||
| ddd297492c | |||
| effd22c750 | |||
| 566965bb6a | |||
| 3d3b4e0bea | |||
| b86aa2559a | |||
| 4a2fa21a0a | |||
| 3b8352a34f | |||
| ac4ad3c8aa | |||
| ebc91d8aad | |||
| 48dece2a51 | |||
| aa9e102a81 | |||
| faaf5c366a | |||
| 4384e56cc6 | |||
| abbd7b8b19 | |||
| b687acb94c | |||
| 7748407583 | |||
|
|
822793b085 | ||
| 3d058066b9 | |||
| 1c5e17a4ab | |||
| 867da56f45 | |||
| 14c3b97ed2 | |||
| 1d9de31f47 | |||
|
|
0681f599f1 | ||
|
|
68c64ad867 | ||
|
|
18b6a81896 | ||
|
|
e5c3852f06 | ||
|
|
ff73da3a4c | ||
| 916670cf68 | |||
| 94a320a682 | |||
| 65dccc3b51 |
@@ -1,8 +0,0 @@
|
||||
PORT=8011
|
||||
META_TEMPLATE=meta_template.json
|
||||
CLASH_TEMPLATE=clash_template.json
|
||||
REQUEST_RETRY_TIMES=3
|
||||
REQUEST_MAX_FILE_SIZE=1048576
|
||||
CACHE_EXPIRE=300
|
||||
LOG_LEVEL=info
|
||||
BASE_PATH=/
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
92
.github/workflows/docker.yml
vendored
92
.github/workflows/docker.yml
vendored
@@ -1,69 +1,57 @@
|
||||
name: Build and Push to GHCR
|
||||
name: docker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare-and-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
nite07/sub2clash
|
||||
ghcr.io/bestnite/sub2clash
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=match,pattern=(alpha|beta|rc),group=1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set tag name
|
||||
id: set_tag
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/heads/* ]]; then
|
||||
echo "::set-output name=tag::$(echo $GITHUB_REF | cut -d'/' -f3)"
|
||||
else
|
||||
echo "::set-output name=tag::${{ github.ref_name }}"
|
||||
fi
|
||||
|
||||
- name: Check if triggered by tag
|
||||
id: check_tag
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
echo "::set-output name=triggered_by_tag::true"
|
||||
else
|
||||
echo "::set-output name=triggered_by_tag::false"
|
||||
fi
|
||||
|
||||
- name: Build and push Docker image for dev branch
|
||||
if: steps.check_tag.outputs.triggered_by_tag == 'false'
|
||||
uses: docker/build-push-action@v2
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
build-args: |
|
||||
dev=true
|
||||
version=${{ github.sha }}
|
||||
"version=${{ github.ref_name }}"
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
|
||||
|
||||
- name: Build and push Docker image for tags
|
||||
if: steps.check_tag.outputs.triggered_by_tag == 'true'
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
build-args: |
|
||||
dev=false
|
||||
version=${{ steps.set_tag.outputs.tag }}
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
|
||||
ghcr.io/${{ github.repository }}:latest
|
||||
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: Containerfile
|
||||
|
||||
|
||||
110
.github/workflows/go.yml
vendored
110
.github/workflows/go.yml
vendored
@@ -1,110 +0,0 @@
|
||||
# This workflow will build a golang project
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
|
||||
|
||||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
LDFLAGS="-s -w -X config.Version=${{ github.ref_name }}"
|
||||
|
||||
# Linux
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-amd64 main.go
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm64 main.go
|
||||
|
||||
# Darwin
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-amd64 main.go
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-arm64 main.go
|
||||
|
||||
# Windows
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-amd64.exe main.go
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm64.exe main.go
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref_name }}
|
||||
release_name: Release ${{ github.ref_name }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
|
||||
- name: Upload Release Asset (Linux amd64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./output/sub2clash-linux-amd64
|
||||
asset_name: sub2clash-linux-amd64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (Linux arm64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./output/sub2clash-linux-arm64
|
||||
asset_name: sub2clash-linux-arm64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (Darwin amd64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./output/sub2clash-darwin-amd64
|
||||
asset_name: sub2clash-darwin-amd64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (Darwin arm64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./output/sub2clash-darwin-arm64
|
||||
asset_name: sub2clash-darwin-arm64
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (Windows amd64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./output/sub2clash-windows-amd64.exe
|
||||
asset_name: sub2clash-windows-amd64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
- name: Upload Release Asset (Windows arm64)
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./output/sub2clash-windows-arm64.exe
|
||||
asset_name: sub2clash-windows-arm64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
38
.github/workflows/release.yml
vendored
Normal file
38
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
|
||||
- name: Install UPX
|
||||
uses: crazy-max/ghaction-upx@v3
|
||||
with:
|
||||
install-only: true
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "latest"
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,7 +1,9 @@
|
||||
.idea
|
||||
dist
|
||||
subs
|
||||
test
|
||||
logs
|
||||
sub2clash.db
|
||||
data
|
||||
.env
|
||||
config.yaml
|
||||
config.yml
|
||||
config.json
|
||||
@@ -1,17 +1,47 @@
|
||||
#before:
|
||||
# hooks:
|
||||
# - go mod tidy
|
||||
version: 2
|
||||
project_name: sub2clash
|
||||
before:
|
||||
hooks:
|
||||
- bash ./build-frontend.sh {{ .Version }}
|
||||
builds:
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X sub2clash/config.Version={{ .Version }}
|
||||
no_unique_dist_dir: true
|
||||
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
||||
- env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w -X github.com/bestnite/sub2clash/constant.Version={{ .Version }}
|
||||
flags:
|
||||
- -trimpath
|
||||
archives:
|
||||
- formats: ["tar.gz"]
|
||||
format_overrides:
|
||||
- formats: ["zip"]
|
||||
goos: windows
|
||||
wrap_in_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
- README.md
|
||||
- templates
|
||||
release:
|
||||
draft: true
|
||||
# upx:
|
||||
# - enabled: true
|
||||
# compress: best
|
||||
nfpms:
|
||||
- id: sub2clash
|
||||
homepage: https://github.com/bestnite/sub2clash
|
||||
maintainer: Nite <admin@nite07.com>
|
||||
license: "MIT"
|
||||
formats:
|
||||
- apk
|
||||
- deb
|
||||
- rpm
|
||||
- termux.deb
|
||||
- archlinux
|
||||
provides:
|
||||
- sub2clash
|
||||
|
||||
25
.vscode/launch.json
vendored
25
.vscode/launch.json
vendored
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "debug",
|
||||
"program": "${workspaceFolder}/main.go",
|
||||
"output": "${workspaceFolder}/dist/main.exe",
|
||||
"buildFlags": "-ldflags '-X sub2clash/config.Dev=true -X sub2clash/config.Version=dev'"
|
||||
}
|
||||
]
|
||||
}
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "debug",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"args": [],
|
||||
"preLaunchTask": "build frontend"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
.vscode/tasks.json
vendored
Normal file
21
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build frontend",
|
||||
"type": "shell",
|
||||
"command": "npm",
|
||||
"args": [
|
||||
"run",
|
||||
"build"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/server/frontend"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
# `/clash`, `/meta`
|
||||
|
||||
获取 Clash/Clash.Meta 配置链接
|
||||
|
||||
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|--------------|--------|-------------------|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| sub | string | sub/proxy 至少有一项存在 | - | 订阅链接,可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个,用 `,` 分隔) |
|
||||
| proxy | string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个,用 `,` 分隔) |
|
||||
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
|
||||
| template | string | 否 | - | 外部模板链接或内部模板名称 |
|
||||
| ruleProvider | string | 否 | - | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集使用的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到 MATCH 规则之前) |
|
||||
| rule | string | 否 | - | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到 MATCH 规则之前) |
|
||||
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
|
||||
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
|
||||
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
|
||||
| replace | string | 否 | - | 通过正则表达式重命名节点,格式 `[<ReplaceKey>,<ReplaceTo>],[<ReplaceKey>,<ReplaceTo>]...` |
|
||||
| remove | string | 否 | - | 通过正则表达式删除节点 |
|
||||
| nodeList | bool | 否 | `false` | 只输出节点 |
|
||||
|
||||
# `/short`
|
||||
|
||||
获取短链,Content-Type 为 `application/json`
|
||||
具体参考使用可以参考 [api\templates\index.html](api/static/index.html)
|
||||
|
||||
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|----------|--------|------|-----|------------------|
|
||||
| url | string | 是 | - | 需要转换的 Query 参数部分 |
|
||||
| password | string | 否 | - | 短链密码 |
|
||||
|
||||
# `/s/:hash`
|
||||
|
||||
短链跳转
|
||||
`hash` 为动态路由参数,可以通过 `/short` 接口获取
|
||||
|
||||
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
|----------|--------|------|-----|------|
|
||||
| password | string | 否 | - | 短链密码 |
|
||||
21
Containerfile
Normal file
21
Containerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM node:latest AS frontend_builder
|
||||
WORKDIR /app/server/frontend
|
||||
COPY server/frontend/package*.json ./
|
||||
RUN npm install
|
||||
COPY server/frontend .
|
||||
ARG version
|
||||
ENV VITE_APP_VERSION=${version}
|
||||
RUN npm run build
|
||||
|
||||
FROM golang:1.25 AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
COPY --from=frontend_builder /app/server/frontend/dist /app/server/frontend/dist
|
||||
RUN go mod download
|
||||
ARG version
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X github.com/bestnite/sub2clash/constant.Version=${version}" -o sub2clash .
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/sub2clash /app/sub2clash
|
||||
ENTRYPOINT ["/app/sub2clash"]
|
||||
28
Dockerfile
28
Dockerfile
@@ -1,28 +0,0 @@
|
||||
# 使用官方 Golang 镜像作为构建环境
|
||||
FROM golang:1.21-alpine as builder
|
||||
LABEL authors="nite07"
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制源代码到工作目录
|
||||
COPY . .
|
||||
RUN go mod download
|
||||
|
||||
# 获取参数
|
||||
ARG version
|
||||
ARG dev
|
||||
|
||||
# 使用 -ldflags 参数进行编译
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X sub2clash/config.Version=${version} -X sub2clash/config.Dev=${dev}" -o sub2clash main.go
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从 builder 镜像中复制出编译好的二进制文件
|
||||
COPY --from=builder /app/sub2clash /app/sub2clash
|
||||
|
||||
# 设置容器的默认启动命令
|
||||
ENTRYPOINT ["/app/sub2clash"]
|
||||
126
README.md
126
README.md
@@ -1,53 +1,114 @@
|
||||
# sub2clash
|
||||
|
||||
将订阅链接转换为 Clash、Clash.Meta 配置
|
||||
[预览](https://www.nite07.com/sub)
|
||||
[预览](https://clash.nite07.com/)
|
||||
|
||||
## 特性
|
||||
|
||||
- 开箱即用的规则、策略组配置
|
||||
- 自动根据节点名称按国家划分策略组
|
||||
- 支持多订阅合并
|
||||
- 支持添加自定义 Rule Provider、Rule
|
||||
- 多订阅合并
|
||||
- 自定义 Rule Provider、Rule
|
||||
- 支持多种协议
|
||||
- Shadowsocks
|
||||
- ShadowsocksR
|
||||
- Vmess
|
||||
- Vless (Clash.Meta)
|
||||
- Trojan
|
||||
- Hysteria (Clash.Meta)
|
||||
- Hysteria2 (Clash.Meta)
|
||||
- Shadowsocks
|
||||
- ShadowsocksR
|
||||
- Vmess
|
||||
- Vless (Clash.Meta)
|
||||
- Trojan
|
||||
- Hysteria (Clash.Meta)
|
||||
- Hysteria2 (Clash.Meta)
|
||||
- Socks5
|
||||
- Anytls (Clash.Meta)
|
||||
|
||||
## 使用
|
||||
|
||||
### 运行
|
||||
### 部署
|
||||
|
||||
- [docker compose](./docker-compose.yml)
|
||||
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
|
||||
- [docker compose](./compose.yml)
|
||||
- 运行[二进制文件](https://github.com/bestnite/sub2clash/releases/latest)
|
||||
|
||||
### 配置
|
||||
|
||||
可以通过编辑 .env 文件来修改默认配置,docker 直接添加环境变量
|
||||
支持多种配置方式,按优先级排序:
|
||||
|
||||
| 变量名 | 说明 | 默认值 |
|
||||
|-----------------------|----------------------------------------|-----------------------|
|
||||
| PORT | 端口 | `8011` |
|
||||
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
|
||||
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
|
||||
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
|
||||
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` |
|
||||
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
|
||||
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
|
||||
| SHORT_LINK_LENGTH | 短链长度 | `6` |
|
||||
1. **配置文件**:支持多种格式(YAML、JSON),按以下优先级搜索:
|
||||
- `config.yaml` / `config.yml`
|
||||
- `config.json`
|
||||
- `sub2clash.yaml` / `sub2clash.yml`
|
||||
- `sub2clash.json`
|
||||
2. **环境变量**:使用 `SUB2CLASH_` 前缀,例如 `SUB2CLASH_ADDRESS=0.0.0.0:8011`
|
||||
3. **默认值**:内置默认配置
|
||||
|
||||
| 配置项 | 环境变量 | 说明 | 默认值 |
|
||||
| --------------------- | ------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| address | SUB2CLASH_ADDRESS | 服务监听地址 | `0.0.0.0:8011` |
|
||||
| meta_template | SUB2CLASH_META_TEMPLATE | 默认 meta 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml` |
|
||||
| clash_template | SUB2CLASH_CLASH_TEMPLATE | 默认 clash 模板 URL | `https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml` |
|
||||
| request_retry_times | SUB2CLASH_REQUEST_RETRY_TIMES | 请求重试次数 | `3` |
|
||||
| request_max_file_size | SUB2CLASH_REQUEST_MAX_FILE_SIZE | 请求文件最大大小(byte) | `1048576` |
|
||||
| cache_expire | SUB2CLASH_CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
|
||||
| log_level | SUB2CLASH_LOG_LEVEL | 日志等级:`debug`,`info`,`warn`,`error` | `info` |
|
||||
| short_link_length | SUB2CLASH_SHORT_LINK_LENGTH | 短链长度 | `6` |
|
||||
|
||||
#### 配置文件示例
|
||||
|
||||
参考示例文件:
|
||||
|
||||
- [config.example.yaml](./config.example.yaml) - YAML 格式
|
||||
- [config.example.json](./config.example.json) - JSON 格式
|
||||
|
||||
### API
|
||||
|
||||
[API 文档](./API_README.md)
|
||||
#### `GET /convert/:config`
|
||||
|
||||
获取 Clash/Clash.Meta 配置链接
|
||||
|
||||
| Path 参数 | 类型 | 说明 |
|
||||
| --------- | ------ | ---------------------------------------------- |
|
||||
| config | string | Base64 URL Safe 编码后的 JSON 字符串,格式如下 |
|
||||
|
||||
##### `config` JSON 结构
|
||||
|
||||
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||
| ------------------ | ----------------- | ------------------------ | --------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| clashType | int | 是 | 1 | 配置文件类型 (1: Clash, 2: Clash.Meta) |
|
||||
| subscriptions | []string | sub/proxy 至少有一项存在 | - | 订阅链接(v2ray 或 clash 格式),可以在链接结尾加上`#名称`,来给订阅中的节点加上统一前缀(可以输入多个) |
|
||||
| proxies | []string | sub/proxy 至少有一项存在 | - | 节点分享链接(可以输入多个) |
|
||||
| refresh | bool | 否 | `false` | 强制刷新配置(默认缓存 5 分钟) |
|
||||
| template | string | 否 | - | 外部模板链接或内部模板名称 |
|
||||
| ruleProviders | []RuleProvider | 否 | - | 规则 |
|
||||
| rules | []Rule | 否 | - | 规则 |
|
||||
| autoTest | bool | 否 | `false` | 国家策略组是否自动测速 |
|
||||
| lazy | bool | 否 | `false` | 自动测速是否启用 lazy |
|
||||
| sort | string | 否 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc` |
|
||||
| replace | map[string]string | 否 | - | 通过正则表达式重命名节点 |
|
||||
| remove | string | 否 | - | 通过正则表达式删除节点 |
|
||||
| nodeList | bool | 否 | `false` | 只输出节点 |
|
||||
| ignoreCountryGroup | bool | 否 | `false` | 是否忽略国家分组 |
|
||||
| userAgent | string | 否 | - | 订阅 user-agent |
|
||||
| useUDP | bool | 否 | `false` | 是否使用 UDP |
|
||||
|
||||
###### `RuleProvider` 结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| -------- | ------ | ---------------------------------------------------------------- |
|
||||
| behavior | string | rule-set 的 behavior |
|
||||
| url | string | rule-set 的 url |
|
||||
| group | string | 该规则集使用的策略组名 |
|
||||
| prepend | bool | 如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部 |
|
||||
| name | string | 该 rule-provider 的名称,不能重复 |
|
||||
|
||||
###### `Rule` 结构
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| ------- | ------ | ---------------------------------------------------------------- |
|
||||
| rule | string | 规则 |
|
||||
| prepend | bool | 如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部 |
|
||||
|
||||
### 模板
|
||||
|
||||
可以通过变量自定义模板中的策略组代理节点
|
||||
解释的不太清楚,可以参考下方默认模板
|
||||
具体参考下方默认模板
|
||||
|
||||
- `<all>` 为添加所有节点
|
||||
- `<countries>` 为添加所有国家策略组
|
||||
@@ -58,6 +119,15 @@
|
||||
- [Clash](./templates/template_clash.yaml)
|
||||
- [Clash.Meta](./templates/template_meta.yaml)
|
||||
|
||||
## 已知问题
|
||||
## 开发
|
||||
|
||||
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
|
||||
### 添加新协议支持
|
||||
|
||||
添加新协议支持需要实现以下组件:
|
||||
|
||||
1. 在 `parser` 目录下实现协议解析器,用于解析节点链接
|
||||
2. 在 `model/proxy` 目录下定义协议结构体
|
||||
|
||||
## 贡献者
|
||||
|
||||
[](https://github.com/bestnite/sub2clash/graphs/contributors)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sub2clash/config"
|
||||
"sub2clash/model"
|
||||
"sub2clash/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func SubmodHandler(c *gin.Context) {
|
||||
// 从请求中获取参数
|
||||
query, err := validator.ParseQuery(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
sub, err := BuildSub(model.Clash, query, config.Default.ClashTemplate)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
// 输出
|
||||
if query.NodeListMode {
|
||||
nodelist := model.NodeList{}
|
||||
nodelist.Proxies = sub.Proxies
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/logger"
|
||||
"sub2clash/model"
|
||||
"sub2clash/parser"
|
||||
"sub2clash/utils"
|
||||
"sub2clash/validator"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) (
|
||||
*model.Subscription, error,
|
||||
) {
|
||||
// 定义变量
|
||||
var temp = &model.Subscription{}
|
||||
var sub = &model.Subscription{}
|
||||
var err error
|
||||
var templateBytes []byte
|
||||
// 加载模板
|
||||
if query.Template != "" {
|
||||
template = query.Template
|
||||
}
|
||||
if strings.HasPrefix(template, "http") {
|
||||
templateBytes, err = utils.LoadSubscription(template, query.Refresh)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
unescape, err := url.QueryUnescape(template)
|
||||
if err != nil {
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
templateBytes, err = utils.LoadTemplate(unescape)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载模板失败: " + err.Error())
|
||||
}
|
||||
}
|
||||
// 解析模板
|
||||
err = yaml.Unmarshal(templateBytes, &temp)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("parse template failed", zap.Error(err))
|
||||
return nil, errors.New("解析模板失败: " + err.Error())
|
||||
}
|
||||
var proxyList []model.Proxy
|
||||
// 加载订阅
|
||||
for i := range query.Subs {
|
||||
data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
|
||||
subName := ""
|
||||
if strings.Contains(query.Subs[i], "#") {
|
||||
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
|
||||
}
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||
}
|
||||
// 解析订阅
|
||||
err = yaml.Unmarshal(data, &sub)
|
||||
newProxies := make([]model.Proxy, 0)
|
||||
if err != nil {
|
||||
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria)://")
|
||||
if reg.Match(data) {
|
||||
p := utils.ParseProxy(strings.Split(string(data), "\n")...)
|
||||
newProxies = p
|
||||
} else {
|
||||
// 如果无法直接解析,尝试Base64解码
|
||||
base64, err := parser.DecodeBase64(string(data))
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"parse subscription failed", zap.String("url", query.Subs[i]),
|
||||
zap.String("data", string(data)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||
}
|
||||
p := utils.ParseProxy(strings.Split(base64, "\n")...)
|
||||
newProxies = p
|
||||
}
|
||||
} else {
|
||||
newProxies = sub.Proxies
|
||||
}
|
||||
if subName != "" {
|
||||
for i := range newProxies {
|
||||
newProxies[i].SubName = subName
|
||||
}
|
||||
}
|
||||
proxyList = append(proxyList, newProxies...)
|
||||
}
|
||||
// 添加自定义节点
|
||||
if len(query.Proxies) != 0 {
|
||||
proxyList = append(proxyList, utils.ParseProxy(query.Proxies...)...)
|
||||
}
|
||||
// 给节点添加订阅名称
|
||||
for i := range proxyList {
|
||||
if proxyList[i].SubName != "" {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
}
|
||||
// 去掉配置相同的节点
|
||||
proxies := make(map[string]*model.Proxy)
|
||||
newProxies := make([]model.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
key := proxyList[i].Server + ":" + strconv.Itoa(proxyList[i].Port) + ":" + proxyList[i].Type
|
||||
if _, exist := proxies[key]; !exist {
|
||||
proxies[key] = &proxyList[i]
|
||||
newProxies = append(newProxies, proxyList[i])
|
||||
}
|
||||
}
|
||||
proxyList = newProxies
|
||||
// 删除节点
|
||||
if strings.TrimSpace(query.Remove) != "" {
|
||||
newProxyList := make([]model.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
removeReg, err := regexp.Compile(query.Remove)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("remove 参数非法: " + err.Error())
|
||||
}
|
||||
// 删除匹配到的节点
|
||||
if removeReg.MatchString(proxyList[i].Name) {
|
||||
continue // 如果匹配到要删除的元素,跳过该元素,不添加到新切片中
|
||||
}
|
||||
newProxyList = append(newProxyList, proxyList[i]) // 将要保留的元素添加到新切片中
|
||||
}
|
||||
proxyList = newProxyList
|
||||
}
|
||||
// 重命名
|
||||
if len(query.ReplaceKeys) != 0 {
|
||||
// 创建重命名正则表达式
|
||||
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
|
||||
for _, v := range query.ReplaceKeys {
|
||||
replaceReg, err := regexp.Compile(v)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("replace 参数非法: " + err.Error())
|
||||
}
|
||||
replaceRegs = append(replaceRegs, replaceReg)
|
||||
}
|
||||
for i := range proxyList {
|
||||
// 重命名匹配到的节点
|
||||
for j, v := range replaceRegs {
|
||||
if err != nil {
|
||||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||||
return nil, errors.New("replaceName 参数非法: " + err.Error())
|
||||
}
|
||||
if v.MatchString(proxyList[i].Name) {
|
||||
proxyList[i].Name = v.ReplaceAllString(
|
||||
proxyList[i].Name, query.ReplaceTo[j],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 重名检测
|
||||
names := make(map[string]int)
|
||||
for i := range proxyList {
|
||||
if _, exist := names[proxyList[i].Name]; exist {
|
||||
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
|
||||
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
|
||||
} else {
|
||||
names[proxyList[i].Name] = 0
|
||||
}
|
||||
}
|
||||
// trim
|
||||
for i := range proxyList {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
|
||||
var t = &model.Subscription{}
|
||||
utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||||
// 排序策略组
|
||||
switch query.Sort {
|
||||
case "sizeasc":
|
||||
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
|
||||
case "sizedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups)))
|
||||
case "nameasc":
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||
case "namedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups)))
|
||||
default:
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||
}
|
||||
// 合并新节点和模板
|
||||
MergeSubAndTemplate(temp, t)
|
||||
// 处理自定义规则
|
||||
for _, v := range query.Rules {
|
||||
if v.Prepend {
|
||||
utils.PrependRules(temp, v.Rule)
|
||||
} else {
|
||||
utils.AppendRules(temp, v.Rule)
|
||||
}
|
||||
}
|
||||
// 处理自定义 ruleProvider
|
||||
for _, v := range query.RuleProviders {
|
||||
hash := sha256.Sum224([]byte(v.Url))
|
||||
name := hex.EncodeToString(hash[:])
|
||||
provider := model.RuleProvider{
|
||||
Type: "http",
|
||||
Behavior: v.Behavior,
|
||||
Url: v.Url,
|
||||
Path: "./" + name + ".yaml",
|
||||
Interval: 3600,
|
||||
}
|
||||
if v.Prepend {
|
||||
utils.PrependRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
} else {
|
||||
utils.AppenddRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
|
||||
// 只合并节点、策略组
|
||||
// 统计所有国家策略组名称
|
||||
var countryGroupNames []string
|
||||
for _, proxyGroup := range sub.ProxyGroups {
|
||||
if proxyGroup.IsCountryGrop {
|
||||
countryGroupNames = append(
|
||||
countryGroupNames, proxyGroup.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
var proxyNames []string
|
||||
for _, proxy := range sub.Proxies {
|
||||
proxyNames = append(proxyNames, proxy.Name)
|
||||
}
|
||||
// 将订阅中的节点添加到模板中
|
||||
temp.Proxies = append(temp.Proxies, sub.Proxies...)
|
||||
// 将订阅中的策略组添加到模板中
|
||||
for i := range temp.ProxyGroups {
|
||||
if temp.ProxyGroups[i].IsCountryGrop {
|
||||
continue
|
||||
}
|
||||
newProxies := make([]string, 0)
|
||||
countryGroupMap := make(map[string]model.ProxyGroup)
|
||||
for _, v := range sub.ProxyGroups {
|
||||
if v.IsCountryGrop {
|
||||
countryGroupMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
for j := range temp.ProxyGroups[i].Proxies {
|
||||
reg := regexp.MustCompile("<(.*?)>")
|
||||
if reg.Match([]byte(temp.ProxyGroups[i].Proxies[j])) {
|
||||
key := reg.FindStringSubmatch(temp.ProxyGroups[i].Proxies[j])[1]
|
||||
switch key {
|
||||
case "all":
|
||||
newProxies = append(newProxies, proxyNames...)
|
||||
case "countries":
|
||||
newProxies = append(newProxies, countryGroupNames...)
|
||||
default:
|
||||
if len(key) == 2 {
|
||||
newProxies = append(
|
||||
newProxies, countryGroupMap[utils.GetContryName(key)].Proxies...,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
|
||||
}
|
||||
}
|
||||
temp.ProxyGroups[i].Proxies = newProxies
|
||||
}
|
||||
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
"sub2clash/config"
|
||||
"sub2clash/model"
|
||||
"sub2clash/validator"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func SubHandler(c *gin.Context) {
|
||||
// 从请求中获取参数
|
||||
query, err := validator.ParseQuery(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
sub, err := BuildSub(model.ClashMeta, query, config.Default.MetaTemplate)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
// 输出
|
||||
if query.NodeListMode {
|
||||
nodelist := model.NodeList{}
|
||||
nodelist.Proxies = sub.Proxies
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/config"
|
||||
"sub2clash/logger"
|
||||
"sub2clash/model"
|
||||
"sub2clash/utils"
|
||||
"sub2clash/utils/database"
|
||||
"sub2clash/validator"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ShortLinkGenHandler(c *gin.Context) {
|
||||
// 从请求中获取参数
|
||||
var params validator.ShortLinkGenValidator
|
||||
if err := c.ShouldBind(¶ms); err != nil {
|
||||
c.String(400, "参数错误: "+err.Error())
|
||||
}
|
||||
if strings.TrimSpace(params.Url) == "" {
|
||||
c.String(400, "参数错误")
|
||||
return
|
||||
}
|
||||
// 生成hash
|
||||
hash := utils.RandomString(config.Default.ShortLinkLength)
|
||||
var item model.ShortLink
|
||||
result := database.FindShortLinkByUrl(params.Url, &item)
|
||||
if result.Error == nil {
|
||||
if item.Password != params.Password {
|
||||
item.Password = params.Password
|
||||
database.SaveShortLink(&item)
|
||||
c.String(200, item.Hash+"?password="+params.Password)
|
||||
} else {
|
||||
c.String(200, item.Hash)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
c.String(500, "数据库错误: "+result.Error.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
// 如果记录存在则重新生成hash,直到记录不存在
|
||||
result = database.FindShortLinkByHash(hash, &item)
|
||||
for result.Error == nil {
|
||||
hash = utils.RandomString(config.Default.ShortLinkLength)
|
||||
result = database.FindShortLinkByHash(hash, &item)
|
||||
}
|
||||
// 创建记录
|
||||
database.FirstOrCreateShortLink(
|
||||
&model.ShortLink{
|
||||
Hash: hash,
|
||||
Url: params.Url,
|
||||
LastRequestTime: -1,
|
||||
Password: params.Password,
|
||||
},
|
||||
)
|
||||
// 返回短链接
|
||||
if params.Password != "" {
|
||||
hash += "?password=" + params.Password
|
||||
}
|
||||
c.String(200, hash)
|
||||
}
|
||||
|
||||
func ShortLinkGetHandler(c *gin.Context) {
|
||||
// 获取动态路由
|
||||
hash := c.Param("hash")
|
||||
password := c.Query("password")
|
||||
if strings.TrimSpace(hash) == "" {
|
||||
c.String(400, "参数错误")
|
||||
return
|
||||
}
|
||||
// 查询数据库
|
||||
var shortLink model.ShortLink
|
||||
result := database.FindShortLinkByHash(hash, &shortLink)
|
||||
// 重定向
|
||||
if result.Error != nil {
|
||||
c.String(404, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(404, "未找到短链接或密码错误")
|
||||
return
|
||||
}
|
||||
// 更新最后访问时间
|
||||
shortLink.LastRequestTime = time.Now().Unix()
|
||||
database.SaveShortLink(&shortLink)
|
||||
get, err := utils.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("get short link data failed", zap.Error(err))
|
||||
c.String(500, "请求错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
all, err := io.ReadAll(get.Body)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("read short link data failed", zap.Error(err))
|
||||
c.String(500, "读取错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(all))
|
||||
}
|
||||
66
api/route.go
66
api/route.go
@@ -1,66 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"sub2clash/api/controller"
|
||||
"sub2clash/config"
|
||||
"sub2clash/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
var staticFiles embed.FS
|
||||
|
||||
func SetRoute(r *gin.Engine) {
|
||||
r.Use(middleware.ZapLogger())
|
||||
|
||||
// 使用内嵌的模板文件
|
||||
tpl, err := template.ParseFS(staticFiles, "static/*")
|
||||
if err != nil {
|
||||
log.Fatalf("Error parsing templates: %v", err)
|
||||
}
|
||||
r.SetHTMLTemplate(tpl)
|
||||
|
||||
r.GET(
|
||||
"/static/*filepath", func(c *gin.Context) {
|
||||
c.FileFromFS("static/"+c.Param("filepath"), http.FS(staticFiles))
|
||||
},
|
||||
)
|
||||
r.GET(
|
||||
"/", func(c *gin.Context) {
|
||||
version := config.Version
|
||||
if len(config.Version) > 7 {
|
||||
version = config.Version[:7]
|
||||
}
|
||||
c.HTML(
|
||||
200, "index.html", gin.H{
|
||||
"Version": version,
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
r.GET(
|
||||
"/clash", func(c *gin.Context) {
|
||||
controller.SubmodHandler(c)
|
||||
},
|
||||
)
|
||||
r.GET(
|
||||
"/meta", func(c *gin.Context) {
|
||||
controller.SubHandler(c)
|
||||
},
|
||||
)
|
||||
r.POST(
|
||||
"/short", func(c *gin.Context) {
|
||||
controller.ShortLinkGenHandler(c)
|
||||
},
|
||||
)
|
||||
r.GET(
|
||||
"/s/:hash", func(c *gin.Context) {
|
||||
controller.ShortLinkGetHandler(c)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>sub2clash</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link
|
||||
crossorigin="anonymous"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- Bootstrap JS -->
|
||||
<script
|
||||
crossorigin="anonymous"
|
||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
||||
></script>
|
||||
<!-- Axios -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>
|
||||
<script src="./static/index.js"></script>
|
||||
<style>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.btn-xs {
|
||||
padding: 2px 2px; /* 调整内边距以减小按钮大小 */
|
||||
font-size: 10px; /* 设置字体大小 */
|
||||
line-height: 1.2; /* 调整行高 */
|
||||
border-radius: 3px; /* 可选的边框半径调整 */
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container mt-5">
|
||||
<div class="mb-4">
|
||||
<h2>sub2clash</h2>
|
||||
<span class="text-muted fst-italic"
|
||||
>通用订阅链接转 Clash(Meta) 配置工具
|
||||
<a
|
||||
href="https://github.com/nitezs/sub2clash#clash-meta"
|
||||
target="_blank"
|
||||
>使用文档</a
|
||||
></span
|
||||
><br><span class="text-muted fst-italic"
|
||||
>注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Input URL -->
|
||||
<div class="form-group mb-5">
|
||||
<label for="apiLink">解析链接:</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
id="urlInput"
|
||||
type="text"
|
||||
placeholder="通过生成的链接重新填写下方设置"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="parseInputURL()"
|
||||
type="button"
|
||||
>
|
||||
解析
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- API Endpoint -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="endpoint">客户端类型:</label>
|
||||
<select class="form-control" id="endpoint" name="endpoint">
|
||||
<option value="clash">Clash</option>
|
||||
<option value="meta">Clash.Meta</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Template -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="template">模板链接或名称:</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="template"
|
||||
name="template"
|
||||
placeholder="输入外部模板链接或内部模板名称(可选)"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<!-- Subscription Link -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="sub">订阅链接:</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="sub"
|
||||
name="sub"
|
||||
placeholder="每行输入一个订阅链接"
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
<!-- Proxy Link -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="proxy">节点分享链接:</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="proxy"
|
||||
name="proxy"
|
||||
placeholder="每行输入一个节点分享链接"
|
||||
rows="5"
|
||||
></textarea>
|
||||
</div>
|
||||
<!-- Refresh -->
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="refresh"
|
||||
name="refresh"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="refresh"
|
||||
>强制重新获取订阅</label
|
||||
>
|
||||
</div>
|
||||
<!-- Node List -->
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="nodeList"
|
||||
name="nodeList"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="nodeList"
|
||||
>输出为 Node List</label
|
||||
>
|
||||
</div>
|
||||
<!-- Auto Test -->
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="autoTest"
|
||||
name="autoTest"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="autoTest"
|
||||
>国家策略组自动测速</label
|
||||
>
|
||||
</div>
|
||||
<!-- Lazy -->
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="lazy"
|
||||
name="lazy"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label class="form-check-label" for="lazy"
|
||||
>自动测速启用 lazy 模式</label
|
||||
>
|
||||
</div>
|
||||
<!-- Rule Provider -->
|
||||
<div class="form-group mb-3" id="ruleProviderGroup">
|
||||
<label>Rule Provider:</label>
|
||||
<button
|
||||
class="btn btn-primary mb-1 btn-xs"
|
||||
onclick="addRuleProvider()"
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<!-- Rule -->
|
||||
<div class="form-group mb-3" id="ruleGroup">
|
||||
<label>规则:</label>
|
||||
<button
|
||||
class="btn btn-primary mb-1 btn-xs"
|
||||
onclick="addRule()"
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<!-- Sort -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="sort">国家策略组排序规则:</label>
|
||||
<select class="form-control" id="sort" name="sort">
|
||||
<option value="nameasc">名称(升序)</option>
|
||||
<option value="namedesc">名称(降序)</option>
|
||||
<option value="sizeasc">节点数量(升序)</option>
|
||||
<option value="sizedesc">节点数量(降序)</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Remove -->
|
||||
<div class="form-group mb-3">
|
||||
<label for="remove">排除节点:</label>
|
||||
<input
|
||||
class="form-control"
|
||||
type="text"
|
||||
name="remove"
|
||||
id="remove"
|
||||
placeholder="正则表达式"
|
||||
/>
|
||||
</div>
|
||||
<!-- Rename -->
|
||||
<div class="form-group mb-3" id="replaceGroup">
|
||||
<label>节点名称替换:</label>
|
||||
<button
|
||||
class="btn btn-primary mb-1 btn-xs"
|
||||
onclick="addReplace()"
|
||||
type="button"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Display the API Link -->
|
||||
<div class="form-group mb-5">
|
||||
<label for="apiLink">配置链接:</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
class="form-control"
|
||||
id="apiLink"
|
||||
readonly
|
||||
type="text"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="generateURL()"
|
||||
type="button"
|
||||
>
|
||||
生成链接
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="copyToClipboard('apiLink',this)"
|
||||
type="button"
|
||||
>
|
||||
复制链接
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input
|
||||
class="form-control"
|
||||
id="apiShortLink"
|
||||
readonly
|
||||
type="text"
|
||||
/>
|
||||
<input
|
||||
class="form-control"
|
||||
id="password"
|
||||
type="text"
|
||||
placeholder="密码"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="generateShortLink()"
|
||||
type="button"
|
||||
>
|
||||
生成短链
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
onclick="copyToClipboard('apiShortLink',this)"
|
||||
type="button"
|
||||
>
|
||||
复制短链
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- footer-->
|
||||
<footer>
|
||||
<p class="text-center">
|
||||
Powered by
|
||||
<a
|
||||
class="link-primary"
|
||||
href="https://github.com/nitezs/sub2clash"
|
||||
>sub2clash</a
|
||||
>
|
||||
</p>
|
||||
<p class="text-center">Version {{.Version}}</p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,421 +0,0 @@
|
||||
function clearExistingValues() {
|
||||
// 清除简单输入框和复选框的值
|
||||
document.getElementById("endpoint").value = "clash";
|
||||
document.getElementById("sub").value = "";
|
||||
document.getElementById("proxy").value = "";
|
||||
document.getElementById("refresh").checked = false;
|
||||
document.getElementById("autoTest").checked = false;
|
||||
document.getElementById("lazy").checked = false;
|
||||
document.getElementById("template").value = "";
|
||||
document.getElementById("sort").value = "nameasc";
|
||||
document.getElementById("remove").value = "";
|
||||
document.getElementById("apiLink").value = "";
|
||||
document.getElementById("apiShortLink").value = "";
|
||||
document.getElementById("password").value = "";
|
||||
document.getElementById("nodeList").checked = false;
|
||||
|
||||
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
|
||||
clearInputGroup("ruleProviderGroup");
|
||||
clearInputGroup("replaceGroup");
|
||||
clearInputGroup("ruleGroup");
|
||||
}
|
||||
|
||||
function generateURI() {
|
||||
const queryParams = [];
|
||||
|
||||
// 获取 API Endpoint
|
||||
const endpoint = document.getElementById("endpoint").value;
|
||||
|
||||
// 获取并组合订阅链接
|
||||
let subLines = document
|
||||
.getElementById("sub")
|
||||
.value.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
let noSub = false;
|
||||
// 去除 subLines 中空元素
|
||||
subLines = subLines.map((item) => {
|
||||
if (item !== "") {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
if (subLines.length > 0) {
|
||||
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
|
||||
} else {
|
||||
noSub = true;
|
||||
}
|
||||
|
||||
// 获取并组合节点分享链接
|
||||
let proxyLines = document
|
||||
.getElementById("proxy")
|
||||
.value.split("\n")
|
||||
.filter((line) => line.trim() !== "");
|
||||
let noProxy = false;
|
||||
// 去除 proxyLines 中空元素
|
||||
proxyLines = proxyLines.map((item) => {
|
||||
if (item !== "") {
|
||||
return item;
|
||||
}
|
||||
});
|
||||
if (proxyLines.length > 0) {
|
||||
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
|
||||
} else {
|
||||
noProxy = true;
|
||||
}
|
||||
if (noSub && noProxy) {
|
||||
alert("订阅链接和节点分享链接不能同时为空!");
|
||||
return "";
|
||||
}
|
||||
// 获取复选框的值
|
||||
const refresh = document.getElementById("refresh").checked;
|
||||
queryParams.push(`refresh=${refresh ? "true" : "false"}`);
|
||||
const autoTest = document.getElementById("autoTest").checked;
|
||||
queryParams.push(`autoTest=${autoTest ? "true" : "false"}`);
|
||||
const lazy = document.getElementById("lazy").checked;
|
||||
queryParams.push(`lazy=${lazy ? "true" : "false"}`);
|
||||
const nodeList = document.getElementById("nodeList").checked;
|
||||
queryParams.push(`nodeList=${nodeList ? "true" : "false"}`);
|
||||
|
||||
// 获取模板链接或名称(如果存在)
|
||||
const template = document.getElementById("template").value;
|
||||
if (template.trim() !== "") {
|
||||
queryParams.push(`template=${encodeURIComponent(template)}`);
|
||||
}
|
||||
|
||||
// 获取Rule Provider和规则
|
||||
const ruleProviders = document.getElementsByName("ruleProvider");
|
||||
const rules = document.getElementsByName("rule");
|
||||
let providers = [];
|
||||
for (let i = 0; i < ruleProviders.length / 5; i++) {
|
||||
let baseIndex = i * 5;
|
||||
let behavior = ruleProviders[baseIndex].value;
|
||||
let url = ruleProviders[baseIndex + 1].value;
|
||||
let group = ruleProviders[baseIndex + 2].value;
|
||||
let prepend = ruleProviders[baseIndex + 3].value;
|
||||
let name = ruleProviders[baseIndex + 4].value;
|
||||
// 是否存在空值
|
||||
if (
|
||||
behavior.trim() === "" ||
|
||||
url.trim() === "" ||
|
||||
group.trim() === "" ||
|
||||
prepend.trim() === "" ||
|
||||
name.trim() === ""
|
||||
) {
|
||||
alert("Rule Provider 中存在空值,请检查后重试!");
|
||||
return "";
|
||||
}
|
||||
providers.push(`[${behavior},${url},${group},${prepend},${name}]`);
|
||||
}
|
||||
queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`);
|
||||
|
||||
let ruleList = [];
|
||||
for (let i = 0; i < rules.length / 3; i++) {
|
||||
if (rules[i * 3].value.trim() !== "") {
|
||||
let rule = rules[i * 3].value;
|
||||
let prepend = rules[i * 3 + 1].value;
|
||||
let group = rules[i * 3 + 2].value;
|
||||
// 是否存在空值
|
||||
if (
|
||||
rule.trim() === "" ||
|
||||
prepend.trim() === "" ||
|
||||
group.trim() === ""
|
||||
) {
|
||||
alert("Rule 中存在空值,请检查后重试!");
|
||||
return "";
|
||||
}
|
||||
ruleList.push(`[${rule},${prepend},${group}]`);
|
||||
}
|
||||
}
|
||||
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
|
||||
|
||||
// 获取排序策略
|
||||
const sort = document.getElementById("sort").value;
|
||||
queryParams.push(`sort=${sort}`);
|
||||
|
||||
// 获取删除节点的正则表达式
|
||||
const remove = document.getElementById("remove").value;
|
||||
if (remove.trim() !== "") {
|
||||
queryParams.push(`remove=${encodeURIComponent(remove)}`);
|
||||
}
|
||||
|
||||
// 获取替换节点名称的正则表达式
|
||||
let replaceList = [];
|
||||
const replaces = document.getElementsByName("replace");
|
||||
for (let i = 0; i < replaces.length / 2; i++) {
|
||||
let replaceStr = `<${replaces[i * 2].value}>`;
|
||||
let replaceTo = `<${replaces[i * 2 + 1].value}>`;
|
||||
if (replaceStr.trim() === "") {
|
||||
alert("重命名设置中存在空值,请检查后重试!");
|
||||
return "";
|
||||
}
|
||||
replaceList.push(`[${replaceStr},${replaceTo}]`);
|
||||
}
|
||||
queryParams.push(`replace=${encodeURIComponent(replaceList.join(","))}`);
|
||||
|
||||
return `${endpoint}?${queryParams.join("&")}`;
|
||||
}
|
||||
|
||||
// 将输入框中的 URL 解析为参数
|
||||
function parseInputURL() {
|
||||
// 获取输入框中的 URL
|
||||
const inputURL = document.getElementById("urlInput").value;
|
||||
|
||||
if (!inputURL) {
|
||||
alert("请输入有效的链接!");
|
||||
return;
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
url = new URL(inputURL);
|
||||
} catch (_) {
|
||||
alert("无效的链接!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除现有的输入框值
|
||||
clearExistingValues();
|
||||
|
||||
// 获取查询参数
|
||||
const params = new URLSearchParams(url.search);
|
||||
|
||||
// 分配值到对应的输入框
|
||||
const pathSections = url.pathname.split("/");
|
||||
const lastSection = pathSections[pathSections.length - 1];
|
||||
const clientTypeSelect = document.getElementById("endpoint");
|
||||
switch (lastSection.toLowerCase()) {
|
||||
case "meta":
|
||||
clientTypeSelect.value = "meta";
|
||||
break;
|
||||
case "clash":
|
||||
default:
|
||||
clientTypeSelect.value = "clash";
|
||||
break;
|
||||
}
|
||||
|
||||
if (params.has("sub")) {
|
||||
document.getElementById("sub").value = decodeURIComponent(
|
||||
params.get("sub")
|
||||
)
|
||||
.split(",")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
if (params.has("proxy")) {
|
||||
document.getElementById("proxy").value = decodeURIComponent(
|
||||
params.get("proxy")
|
||||
)
|
||||
.split(",")
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
if (params.has("refresh")) {
|
||||
document.getElementById("refresh").checked =
|
||||
params.get("refresh") === "true";
|
||||
}
|
||||
|
||||
if (params.has("autoTest")) {
|
||||
document.getElementById("autoTest").checked =
|
||||
params.get("autoTest") === "true";
|
||||
}
|
||||
|
||||
if (params.has("lazy")) {
|
||||
document.getElementById("lazy").checked = params.get("lazy") === "true";
|
||||
}
|
||||
|
||||
if (params.has("template")) {
|
||||
document.getElementById("template").value = decodeURIComponent(
|
||||
params.get("template")
|
||||
);
|
||||
}
|
||||
|
||||
if (params.has("sort")) {
|
||||
document.getElementById("sort").value = params.get("sort");
|
||||
}
|
||||
|
||||
if (params.has("remove")) {
|
||||
document.getElementById("remove").value = decodeURIComponent(
|
||||
params.get("remove")
|
||||
);
|
||||
}
|
||||
|
||||
if (params.has("replace")) {
|
||||
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
|
||||
}
|
||||
|
||||
if (params.has("ruleProvider")) {
|
||||
parseAndFillRuleProviderParams(
|
||||
decodeURIComponent(params.get("ruleProvider"))
|
||||
);
|
||||
}
|
||||
|
||||
if (params.has("rule")) {
|
||||
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
|
||||
}
|
||||
|
||||
if (params.has("nodeList")) {
|
||||
document.getElementById("nodeList").checked =
|
||||
params.get("nodeList") === "true";
|
||||
}
|
||||
}
|
||||
|
||||
function clearInputGroup(groupId) {
|
||||
// 清空第二个之后的child
|
||||
const group = document.getElementById(groupId);
|
||||
while (group.children.length > 2) {
|
||||
group.removeChild(group.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndFillReplaceParams(replaceParams) {
|
||||
const replaceGroup = document.getElementById("replaceGroup");
|
||||
let matches;
|
||||
const regex = /\[(<.*?>),(<.*?>)\]/g;
|
||||
const str = decodeURIComponent(replaceParams);
|
||||
while ((matches = regex.exec(str)) !== null) {
|
||||
const div = createReplace();
|
||||
const original = matches[1].slice(1, -1); // Remove < and >
|
||||
const replacement = matches[2].slice(1, -1); // Remove < and >
|
||||
|
||||
div.children[0].value = original;
|
||||
div.children[1].value = replacement;
|
||||
replaceGroup.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndFillRuleProviderParams(ruleProviderParams) {
|
||||
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
|
||||
let matches;
|
||||
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
|
||||
const str = decodeURIComponent(ruleProviderParams);
|
||||
while ((matches = regex.exec(str)) !== null) {
|
||||
const div = createRuleProvider();
|
||||
div.children[0].value = matches[1];
|
||||
div.children[1].value = matches[2];
|
||||
div.children[2].value = matches[3];
|
||||
div.children[3].value = matches[4];
|
||||
div.children[4].value = matches[5];
|
||||
ruleProviderGroup.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
function parseAndFillRuleParams(ruleParams) {
|
||||
const ruleGroup = document.getElementById("ruleGroup");
|
||||
let matches;
|
||||
const regex = /\[(.*?),(.*?),(.*?)\]/g;
|
||||
const str = decodeURIComponent(ruleParams);
|
||||
while ((matches = regex.exec(str)) !== null) {
|
||||
const div = createRule();
|
||||
div.children[0].value = matches[1];
|
||||
div.children[1].value = matches[2];
|
||||
div.children[2].value = matches[3];
|
||||
ruleGroup.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToClipboard(elem, e) {
|
||||
const apiLinkInput = document.querySelector(`#${elem}`).value;
|
||||
try {
|
||||
await navigator.clipboard.writeText(apiLinkInput);
|
||||
let text = e.textContent;
|
||||
e.addEventListener("mouseout", function () {
|
||||
e.textContent = text;
|
||||
});
|
||||
e.textContent = "复制成功";
|
||||
} catch (err) {
|
||||
console.error("复制到剪贴板失败:", err);
|
||||
}
|
||||
}
|
||||
|
||||
function createRuleProvider() {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("input-group", "mb-2");
|
||||
div.innerHTML = `
|
||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Behavior">
|
||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Url">
|
||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Group">
|
||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Prepend">
|
||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Name">
|
||||
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function createReplace() {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("input-group", "mb-2");
|
||||
div.innerHTML = `
|
||||
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
|
||||
<input type="text" class="form-control" name="replace" placeholder="替换为(可为空)">
|
||||
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function createRule() {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("input-group", "mb-2");
|
||||
div.innerHTML = `
|
||||
<input type="text" class="form-control" name="rule" placeholder="Rule">
|
||||
<input type="text" class="form-control" name="rule" placeholder="Prepend">
|
||||
<input type="text" class="form-control" name="rule" placeholder="Group">
|
||||
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function addRuleProvider() {
|
||||
const div = createRuleProvider();
|
||||
document.getElementById("ruleProviderGroup").appendChild(div);
|
||||
}
|
||||
|
||||
function addRule() {
|
||||
const div = createRule();
|
||||
document.getElementById("ruleGroup").appendChild(div);
|
||||
}
|
||||
|
||||
function addReplace() {
|
||||
const div = createReplace();
|
||||
document.getElementById("replaceGroup").appendChild(div);
|
||||
}
|
||||
|
||||
function removeElement(button) {
|
||||
button.parentElement.remove();
|
||||
}
|
||||
|
||||
function generateURL() {
|
||||
const apiLink = document.getElementById("apiLink");
|
||||
let uri = generateURI();
|
||||
if (uri === "") {
|
||||
return;
|
||||
}
|
||||
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
|
||||
}
|
||||
|
||||
function generateShortLink() {
|
||||
const apiShortLink = document.getElementById("apiShortLink");
|
||||
const password = document.getElementById("password");
|
||||
let uri = generateURI();
|
||||
if (uri === "") {
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.post(
|
||||
"./short",
|
||||
{
|
||||
url: uri,
|
||||
password: password.value.trim(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
alert("生成短链失败,请重试!");
|
||||
});
|
||||
}
|
||||
7
build-frontend.sh
Normal file
7
build-frontend.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
export VITE_APP_VERSION=$1
|
||||
|
||||
cd server/frontend
|
||||
npm install
|
||||
npm run build
|
||||
77
common/database/database.go
Normal file
77
common/database/database.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func ConnectDB() (*Database, error) {
|
||||
path := filepath.Join("data", "sub2clash.db")
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{
|
||||
Logger: logger.Discard,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, common.NewDatabaseConnectError(err)
|
||||
}
|
||||
|
||||
if err = db.AutoMigrate(&model.ShortLink{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Database{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Database) FindShortLinkByID(id string) (model.ShortLink, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return gorm.G[model.ShortLink](d.db).Where("id = ?", id).First(ctx)
|
||||
}
|
||||
|
||||
func (d *Database) CreateShortLink(shortLink *model.ShortLink) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return gorm.G[model.ShortLink](d.db).Create(ctx, shortLink)
|
||||
}
|
||||
|
||||
func (d *Database) UpdataShortLink(id string, name string, value any) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_, err := gorm.G[model.ShortLink](d.db).Where("id = ?", id).Update(ctx, name, value)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) CheckShortLinkIDExists(id string) (bool, error) {
|
||||
_, err := d.FindShortLinkByID(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *Database) DeleteShortLink(id string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_, err := gorm.G[model.ShortLink](d.db).Where("id = ?", id).Delete(ctx)
|
||||
return err
|
||||
}
|
||||
192
common/errors.go
Normal file
192
common/errors.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CommonError represents a structured error type for the common package
|
||||
type CommonError struct {
|
||||
Code ErrorCode
|
||||
Message string
|
||||
Cause error
|
||||
}
|
||||
|
||||
// ErrorCode represents different types of errors
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
// Directory operation errors
|
||||
ErrDirCreation ErrorCode = "DIRECTORY_CREATION_FAILED"
|
||||
ErrDirAccess ErrorCode = "DIRECTORY_ACCESS_FAILED"
|
||||
|
||||
// File operation errors
|
||||
ErrFileNotFound ErrorCode = "FILE_NOT_FOUND"
|
||||
ErrFileRead ErrorCode = "FILE_READ_FAILED"
|
||||
ErrFileWrite ErrorCode = "FILE_WRITE_FAILED"
|
||||
ErrFileCreate ErrorCode = "FILE_CREATE_FAILED"
|
||||
|
||||
// Network operation errors
|
||||
ErrNetworkRequest ErrorCode = "NETWORK_REQUEST_FAILED"
|
||||
ErrNetworkResponse ErrorCode = "NETWORK_RESPONSE_FAILED"
|
||||
|
||||
// Template and configuration errors
|
||||
ErrTemplateLoad ErrorCode = "TEMPLATE_LOAD_FAILED"
|
||||
ErrTemplateParse ErrorCode = "TEMPLATE_PARSE_FAILED"
|
||||
ErrConfigInvalid ErrorCode = "CONFIG_INVALID"
|
||||
|
||||
// Subscription errors
|
||||
ErrSubscriptionLoad ErrorCode = "SUBSCRIPTION_LOAD_FAILED"
|
||||
ErrSubscriptionParse ErrorCode = "SUBSCRIPTION_PARSE_FAILED"
|
||||
|
||||
// Regex errors
|
||||
ErrRegexCompile ErrorCode = "REGEX_COMPILE_FAILED"
|
||||
ErrRegexInvalid ErrorCode = "REGEX_INVALID"
|
||||
|
||||
// Database errors
|
||||
ErrDatabaseConnect ErrorCode = "DATABASE_CONNECTION_FAILED"
|
||||
ErrDatabaseQuery ErrorCode = "DATABASE_QUERY_FAILED"
|
||||
ErrRecordNotFound ErrorCode = "RECORD_NOT_FOUND"
|
||||
|
||||
// Validation errors
|
||||
ErrValidation ErrorCode = "VALIDATION_FAILED"
|
||||
ErrInvalidInput ErrorCode = "INVALID_INPUT"
|
||||
)
|
||||
|
||||
// Error returns the string representation of the error
|
||||
func (e *CommonError) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error
|
||||
func (e *CommonError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// Is allows error comparison
|
||||
func (e *CommonError) Is(target error) bool {
|
||||
if t, ok := target.(*CommonError); ok {
|
||||
return e.Code == t.Code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NewError creates a new CommonError
|
||||
func NewError(code ErrorCode, message string, cause error) *CommonError {
|
||||
return &CommonError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSimpleError creates a new CommonError without a cause
|
||||
func NewSimpleError(code ErrorCode, message string) *CommonError {
|
||||
return &CommonError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience constructors for common error types
|
||||
|
||||
// Directory errors
|
||||
func NewDirCreationError(dirPath string, cause error) *CommonError {
|
||||
return NewError(ErrDirCreation, fmt.Sprintf("failed to create directory: %s", dirPath), cause)
|
||||
}
|
||||
|
||||
func NewDirAccessError(dirPath string, cause error) *CommonError {
|
||||
return NewError(ErrDirAccess, fmt.Sprintf("failed to access directory: %s", dirPath), cause)
|
||||
}
|
||||
|
||||
// File errors
|
||||
func NewFileNotFoundError(filePath string) *CommonError {
|
||||
return NewSimpleError(ErrFileNotFound, fmt.Sprintf("file not found: %s", filePath))
|
||||
}
|
||||
|
||||
func NewFileReadError(filePath string, cause error) *CommonError {
|
||||
return NewError(ErrFileRead, fmt.Sprintf("failed to read file: %s", filePath), cause)
|
||||
}
|
||||
|
||||
func NewFileWriteError(filePath string, cause error) *CommonError {
|
||||
return NewError(ErrFileWrite, fmt.Sprintf("failed to write file: %s", filePath), cause)
|
||||
}
|
||||
|
||||
func NewFileCreateError(filePath string, cause error) *CommonError {
|
||||
return NewError(ErrFileCreate, fmt.Sprintf("failed to create file: %s", filePath), cause)
|
||||
}
|
||||
|
||||
// Network errors
|
||||
func NewNetworkRequestError(url string, cause error) *CommonError {
|
||||
return NewError(ErrNetworkRequest, fmt.Sprintf("network request failed for URL: %s", url), cause)
|
||||
}
|
||||
|
||||
func NewNetworkResponseError(message string, cause error) *CommonError {
|
||||
return NewError(ErrNetworkResponse, message, cause)
|
||||
}
|
||||
|
||||
// Template errors
|
||||
func NewTemplateLoadError(template string, cause error) *CommonError {
|
||||
return NewError(ErrTemplateLoad, fmt.Sprintf("failed to load template: %s", template), cause)
|
||||
}
|
||||
|
||||
func NewTemplateParseError(data []byte, cause error) *CommonError {
|
||||
return NewError(ErrTemplateParse, fmt.Sprintf("failed to parse template: %s", data), cause)
|
||||
}
|
||||
|
||||
// Subscription errors
|
||||
func NewSubscriptionLoadError(url string, cause error) *CommonError {
|
||||
return NewError(ErrSubscriptionLoad, fmt.Sprintf("failed to load subscription: %s", url), cause)
|
||||
}
|
||||
|
||||
func NewSubscriptionParseError(data []byte, cause error) *CommonError {
|
||||
return NewError(ErrSubscriptionParse, fmt.Sprintf("failed to parse subscription: %s", string(data)), cause)
|
||||
}
|
||||
|
||||
// Regex errors
|
||||
func NewRegexCompileError(pattern string, cause error) *CommonError {
|
||||
return NewError(ErrRegexCompile, fmt.Sprintf("failed to compile regex pattern: %s", pattern), cause)
|
||||
}
|
||||
|
||||
func NewRegexInvalidError(paramName string, cause error) *CommonError {
|
||||
return NewError(ErrRegexInvalid, fmt.Sprintf("invalid regex in parameter: %s", paramName), cause)
|
||||
}
|
||||
|
||||
// Database errors
|
||||
func NewDatabaseConnectError(cause error) *CommonError {
|
||||
return NewError(ErrDatabaseConnect, "failed to connect to database", cause)
|
||||
}
|
||||
|
||||
func NewRecordNotFoundError(recordType string, id string) *CommonError {
|
||||
return NewSimpleError(ErrRecordNotFound, fmt.Sprintf("%s not found: %s", recordType, id))
|
||||
}
|
||||
|
||||
// Validation errors
|
||||
func NewValidationError(field string, message string) *CommonError {
|
||||
return NewSimpleError(ErrValidation, fmt.Sprintf("validation failed for %s: %s", field, message))
|
||||
}
|
||||
|
||||
func NewInvalidInputError(paramName string, value string) *CommonError {
|
||||
return NewSimpleError(ErrInvalidInput, fmt.Sprintf("invalid input for parameter %s: %s", paramName, value))
|
||||
}
|
||||
|
||||
// IsErrorCode checks if an error has a specific error code
|
||||
func IsErrorCode(err error, code ErrorCode) bool {
|
||||
var commonErr *CommonError
|
||||
if errors.As(err, &commonErr) {
|
||||
return commonErr.Code == code
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetErrorCode extracts the error code from an error
|
||||
func GetErrorCode(err error) (ErrorCode, bool) {
|
||||
var commonErr *CommonError
|
||||
if errors.As(err, &commonErr) {
|
||||
return commonErr.Code, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package utils
|
||||
package common
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -18,13 +17,13 @@ func MKDir(dir string) error {
|
||||
|
||||
func MkEssentialDir() error {
|
||||
if err := MKDir("subs"); err != nil {
|
||||
return errors.New("create subs dir failed" + err.Error())
|
||||
}
|
||||
if err := MKDir("templates"); err != nil {
|
||||
return errors.New("create templates dir failed" + err.Error())
|
||||
return NewDirCreationError("subs", err)
|
||||
}
|
||||
if err := MKDir("logs"); err != nil {
|
||||
return errors.New("create logs dir failed" + err.Error())
|
||||
return NewDirCreationError("logs", err)
|
||||
}
|
||||
if err := MKDir("data"); err != nil {
|
||||
return NewDirCreationError("data", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
96
common/proxy.go
Normal file
96
common/proxy.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
"github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func GetContryName(countryKey string) string {
|
||||
|
||||
countryMaps := []map[string]string{
|
||||
model.CountryFlag,
|
||||
model.CountryChineseName,
|
||||
model.CountryISO,
|
||||
model.CountryEnglishName,
|
||||
}
|
||||
|
||||
for i, countryMap := range countryMaps {
|
||||
if i == 2 {
|
||||
|
||||
splitChars := []string{"-", "_", " "}
|
||||
key := make([]string, 0)
|
||||
for _, splitChar := range splitChars {
|
||||
slic := strings.Split(countryKey, splitChar)
|
||||
for _, v := range slic {
|
||||
if len(v) == 2 {
|
||||
key = append(key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range key {
|
||||
|
||||
if country, ok := countryMap[strings.ToUpper(v)]; ok {
|
||||
return country
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range countryMap {
|
||||
if strings.Contains(countryKey, k) {
|
||||
return v
|
||||
}
|
||||
}
|
||||
}
|
||||
return "其他地区"
|
||||
}
|
||||
|
||||
func AddProxy(
|
||||
sub *model.Subscription, autotest bool,
|
||||
lazy bool, clashType model.ClashType, proxies ...proxy.Proxy,
|
||||
) {
|
||||
proxyTypes := model.GetSupportProxyTypes(clashType)
|
||||
|
||||
for _, proxy := range proxies {
|
||||
if !proxyTypes[proxy.Type] {
|
||||
continue
|
||||
}
|
||||
sub.Proxy = append(sub.Proxy, proxy)
|
||||
haveProxyGroup := false
|
||||
countryName := GetContryName(proxy.Name)
|
||||
for i := range sub.ProxyGroup {
|
||||
group := &sub.ProxyGroup[i]
|
||||
if group.Name == countryName {
|
||||
group.Proxies = append(group.Proxies, proxy.Name)
|
||||
group.Size++
|
||||
haveProxyGroup = true
|
||||
}
|
||||
}
|
||||
if !haveProxyGroup {
|
||||
var newGroup model.ProxyGroup
|
||||
if !autotest {
|
||||
newGroup = model.ProxyGroup{
|
||||
Name: countryName,
|
||||
Type: "select",
|
||||
Proxies: []string{proxy.Name},
|
||||
IsCountryGrop: true,
|
||||
Size: 1,
|
||||
}
|
||||
} else {
|
||||
newGroup = model.ProxyGroup{
|
||||
Name: countryName,
|
||||
Type: "url-test",
|
||||
Proxies: []string{proxy.Name},
|
||||
IsCountryGrop: true,
|
||||
Url: "http://www.gstatic.com/generate_204",
|
||||
Interval: 300,
|
||||
Tolerance: 50,
|
||||
Lazy: lazy,
|
||||
Size: 1,
|
||||
}
|
||||
}
|
||||
sub.ProxyGroup = append(sub.ProxyGroup, newGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package utils
|
||||
package common
|
||||
|
||||
import "math/rand"
|
||||
|
||||
func RandomString(length int) string {
|
||||
// 生成随机字符串
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var result []byte
|
||||
for i := 0; i < length; i++ {
|
||||
12
common/request.go
Normal file
12
common/request.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
func Request(retryTimes int) *resty.Client {
|
||||
client := resty.New()
|
||||
client.
|
||||
SetRetryCount(retryTimes)
|
||||
return client
|
||||
}
|
||||
51
common/rule.go
Normal file
51
common/rule.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
)
|
||||
|
||||
func PrependRuleProvider(
|
||||
sub *model.Subscription, providerName string, group string, provider model.RuleProvider,
|
||||
) {
|
||||
if sub.RuleProvider == nil {
|
||||
sub.RuleProvider = make(map[string]model.RuleProvider)
|
||||
}
|
||||
sub.RuleProvider[providerName] = provider
|
||||
PrependRules(
|
||||
sub,
|
||||
fmt.Sprintf("RULE-SET,%s,%s", providerName, group),
|
||||
)
|
||||
}
|
||||
|
||||
func AppenddRuleProvider(
|
||||
sub *model.Subscription, providerName string, group string, provider model.RuleProvider,
|
||||
) {
|
||||
if sub.RuleProvider == nil {
|
||||
sub.RuleProvider = make(map[string]model.RuleProvider)
|
||||
}
|
||||
sub.RuleProvider[providerName] = provider
|
||||
AppendRules(sub, fmt.Sprintf("RULE-SET,%s,%s", providerName, group))
|
||||
}
|
||||
|
||||
func PrependRules(sub *model.Subscription, rules ...string) {
|
||||
if sub.Rule == nil {
|
||||
sub.Rule = make([]string, 0)
|
||||
}
|
||||
sub.Rule = append(rules, sub.Rule...)
|
||||
}
|
||||
|
||||
func AppendRules(sub *model.Subscription, rules ...string) {
|
||||
if sub.Rule == nil {
|
||||
sub.Rule = make([]string, 0)
|
||||
}
|
||||
matchRule := sub.Rule[len(sub.Rule)-1]
|
||||
if strings.Contains(matchRule, "MATCH") {
|
||||
sub.Rule = append(sub.Rule[:len(sub.Rule)-1], rules...)
|
||||
sub.Rule = append(sub.Rule, matchRule)
|
||||
return
|
||||
}
|
||||
sub.Rule = append(sub.Rule, rules...)
|
||||
}
|
||||
395
common/sub.go
Normal file
395
common/sub.go
Normal file
@@ -0,0 +1,395 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/parser"
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var subsDir = "subs"
|
||||
var fileLock sync.RWMutex
|
||||
|
||||
func LoadSubscription(url string, refresh bool, userAgent string, cacheExpire int64, retryTimes int) ([]byte, error) {
|
||||
if refresh {
|
||||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||||
}
|
||||
hash := sha256.Sum224([]byte(url))
|
||||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||||
stat, err := os.Stat(fileName)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||||
}
|
||||
lastGetTime := stat.ModTime().Unix()
|
||||
if lastGetTime+cacheExpire > time.Now().Unix() {
|
||||
file, err := os.Open(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
if file != nil {
|
||||
_ = file.Close()
|
||||
}
|
||||
}(file)
|
||||
fileLock.RLock()
|
||||
defer fileLock.RUnlock()
|
||||
subContent, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return subContent, nil
|
||||
}
|
||||
return FetchSubscriptionFromAPI(url, userAgent, retryTimes)
|
||||
}
|
||||
|
||||
func FetchSubscriptionFromAPI(url string, userAgent string, retryTimes int) ([]byte, error) {
|
||||
hash := sha256.Sum224([]byte(url))
|
||||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||||
client := Request(retryTimes)
|
||||
defer client.Close()
|
||||
resp, err := client.R().SetHeader("User-Agent", userAgent).Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
file, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
if file != nil {
|
||||
_ = file.Close()
|
||||
}
|
||||
}(file)
|
||||
fileLock.Lock()
|
||||
defer fileLock.Unlock()
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func BuildSub(clashType model.ClashType, query model.ConvertConfig, template string, cacheExpire int64, retryTimes int) (
|
||||
*model.Subscription, error,
|
||||
) {
|
||||
var temp = &model.Subscription{}
|
||||
var sub = &model.Subscription{}
|
||||
var err error
|
||||
var templateBytes []byte
|
||||
|
||||
if query.Template != "" {
|
||||
template = query.Template
|
||||
}
|
||||
if strings.HasPrefix(template, "http") {
|
||||
templateBytes, err = LoadSubscription(template, query.Refresh, query.UserAgent, cacheExpire, retryTimes)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, NewTemplateLoadError(template, err)
|
||||
}
|
||||
} else {
|
||||
unescape, err := url.QueryUnescape(template)
|
||||
if err != nil {
|
||||
return nil, NewTemplateLoadError(template, err)
|
||||
}
|
||||
templateBytes, err = LoadTemplate(unescape)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load template failed", zap.String("template", template), zap.Error(err),
|
||||
)
|
||||
return nil, NewTemplateLoadError(unescape, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(templateBytes, &temp)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("parse template failed", zap.Error(err))
|
||||
return nil, NewTemplateParseError(templateBytes, err)
|
||||
}
|
||||
var proxyList []P.Proxy
|
||||
|
||||
for i := range query.Subs {
|
||||
data, err := LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent, cacheExpire, retryTimes)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
|
||||
)
|
||||
return nil, NewSubscriptionLoadError(query.Subs[i], err)
|
||||
}
|
||||
subName := ""
|
||||
if strings.Contains(query.Subs[i], "#") {
|
||||
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(data, &sub)
|
||||
var newProxies []P.Proxy
|
||||
if err != nil {
|
||||
reg, err := regexp.Compile("(" + strings.Join(parser.GetAllPrefixes(), "|") + ")://")
|
||||
if err != nil {
|
||||
logger.Logger.Debug("compile regex failed", zap.Error(err))
|
||||
return nil, NewRegexInvalidError("prefix", err)
|
||||
}
|
||||
if reg.Match(data) {
|
||||
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, strings.Split(string(data), "\n")...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newProxies = p
|
||||
} else {
|
||||
base64, err := utils.DecodeBase64(string(data), false)
|
||||
if err != nil {
|
||||
logger.Logger.Debug(
|
||||
"parse subscription failed", zap.String("url", query.Subs[i]),
|
||||
zap.String("data", string(data)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, NewSubscriptionParseError(data, err)
|
||||
}
|
||||
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, strings.Split(base64, "\n")...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newProxies = p
|
||||
}
|
||||
} else {
|
||||
newProxies = sub.Proxy
|
||||
}
|
||||
if subName != "" {
|
||||
for i := range newProxies {
|
||||
newProxies[i].SubName = subName
|
||||
}
|
||||
}
|
||||
proxyList = append(proxyList, newProxies...)
|
||||
}
|
||||
|
||||
if len(query.Proxies) != 0 {
|
||||
p, err := parser.ParseProxies(parser.ParseConfig{UseUDP: query.UseUDP}, query.Proxies...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
proxyList = append(proxyList, p...)
|
||||
}
|
||||
|
||||
for i := range proxyList {
|
||||
if proxyList[i].SubName != "" {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 去重
|
||||
proxies := make(map[string]*P.Proxy)
|
||||
newProxies := make([]P.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
yamlBytes, err := yaml.Marshal(proxyList[i])
|
||||
if err != nil {
|
||||
logger.Logger.Debug("marshal proxy failed", zap.Error(err))
|
||||
return nil, fmt.Errorf("marshal proxy failed: %w", err)
|
||||
}
|
||||
key := string(yamlBytes)
|
||||
if _, exist := proxies[key]; !exist {
|
||||
proxies[key] = &proxyList[i]
|
||||
newProxies = append(newProxies, proxyList[i])
|
||||
}
|
||||
}
|
||||
proxyList = newProxies
|
||||
|
||||
// 移除
|
||||
if strings.TrimSpace(query.Remove) != "" {
|
||||
newProxyList := make([]P.Proxy, 0, len(proxyList))
|
||||
for i := range proxyList {
|
||||
removeReg, err := regexp.Compile(query.Remove)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||||
return nil, NewRegexInvalidError("remove", err)
|
||||
}
|
||||
|
||||
if removeReg.MatchString(proxyList[i].Name) {
|
||||
continue
|
||||
}
|
||||
newProxyList = append(newProxyList, proxyList[i])
|
||||
}
|
||||
proxyList = newProxyList
|
||||
}
|
||||
|
||||
// 替换
|
||||
if len(query.Replace) != 0 {
|
||||
for k, v := range query.Replace {
|
||||
replaceReg, err := regexp.Compile(k)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
|
||||
return nil, NewRegexInvalidError("replace", err)
|
||||
}
|
||||
for i := range proxyList {
|
||||
if replaceReg.MatchString(proxyList[i].Name) {
|
||||
proxyList[i].Name = replaceReg.ReplaceAllString(
|
||||
proxyList[i].Name, v,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名有相同名称的节点
|
||||
names := make(map[string]int)
|
||||
for i := range proxyList {
|
||||
if _, exist := names[proxyList[i].Name]; exist {
|
||||
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
|
||||
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
|
||||
} else {
|
||||
names[proxyList[i].Name] = 0
|
||||
}
|
||||
}
|
||||
|
||||
for i := range proxyList {
|
||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
||||
}
|
||||
|
||||
var t = &model.Subscription{}
|
||||
AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||||
|
||||
// 排序
|
||||
switch query.Sort {
|
||||
case "sizeasc":
|
||||
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroup))
|
||||
case "sizedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroup)))
|
||||
case "nameasc":
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroup))
|
||||
case "namedesc":
|
||||
sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroup)))
|
||||
default:
|
||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroup))
|
||||
}
|
||||
|
||||
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
|
||||
|
||||
for _, v := range query.Rules {
|
||||
if v.Prepend {
|
||||
PrependRules(temp, v.Rule)
|
||||
} else {
|
||||
AppendRules(temp, v.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range query.RuleProviders {
|
||||
hash := sha256.Sum224([]byte(v.Url))
|
||||
name := hex.EncodeToString(hash[:])
|
||||
provider := model.RuleProvider{
|
||||
Type: "http",
|
||||
Behavior: v.Behavior,
|
||||
Url: v.Url,
|
||||
Path: "./" + name + ".yaml",
|
||||
Interval: 3600,
|
||||
}
|
||||
if v.Prepend {
|
||||
PrependRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
} else {
|
||||
AppenddRuleProvider(
|
||||
temp, v.Name, v.Group, provider,
|
||||
)
|
||||
}
|
||||
}
|
||||
return temp, nil
|
||||
}
|
||||
|
||||
func FetchSubscriptionUserInfo(url string, userAgent string, retryTimes int) (string, error) {
|
||||
client := Request(retryTimes)
|
||||
defer client.Close()
|
||||
resp, err := client.R().SetHeader("User-Agent", userAgent).Head(url)
|
||||
if err != nil {
|
||||
logger.Logger.Debug("创建 HEAD 请求失败", zap.Error(err))
|
||||
return "", NewNetworkRequestError(url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if userInfo := resp.Header().Get("subscription-userinfo"); userInfo != "" {
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
logger.Logger.Debug("subscription-userinfo header not found in response")
|
||||
return "", NewNetworkResponseError("subscription-userinfo header not found", nil)
|
||||
}
|
||||
|
||||
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
|
||||
var countryGroupNames []string
|
||||
for _, proxyGroup := range sub.ProxyGroup {
|
||||
if proxyGroup.IsCountryGrop {
|
||||
countryGroupNames = append(
|
||||
countryGroupNames, proxyGroup.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
var proxyNames []string
|
||||
for _, proxy := range sub.Proxy {
|
||||
proxyNames = append(proxyNames, proxy.Name)
|
||||
}
|
||||
|
||||
temp.Proxy = append(temp.Proxy, sub.Proxy...)
|
||||
|
||||
for i := range temp.ProxyGroup {
|
||||
if temp.ProxyGroup[i].IsCountryGrop {
|
||||
continue
|
||||
}
|
||||
newProxies := make([]string, 0)
|
||||
countryGroupMap := make(map[string]model.ProxyGroup)
|
||||
for _, v := range sub.ProxyGroup {
|
||||
if v.IsCountryGrop {
|
||||
countryGroupMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
for j := range temp.ProxyGroup[i].Proxies {
|
||||
reg := regexp.MustCompile("<(.*?)>")
|
||||
if reg.Match([]byte(temp.ProxyGroup[i].Proxies[j])) {
|
||||
key := reg.FindStringSubmatch(temp.ProxyGroup[i].Proxies[j])[1]
|
||||
switch key {
|
||||
case "all":
|
||||
newProxies = append(newProxies, proxyNames...)
|
||||
case "countries":
|
||||
if !igcg {
|
||||
newProxies = append(newProxies, countryGroupNames...)
|
||||
}
|
||||
default:
|
||||
if !igcg {
|
||||
if len(key) == 2 {
|
||||
newProxies = append(
|
||||
newProxies, countryGroupMap[GetContryName(key)].Proxies...,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newProxies = append(newProxies, temp.ProxyGroup[i].Proxies[j])
|
||||
}
|
||||
}
|
||||
temp.ProxyGroup[i].Proxies = newProxies
|
||||
}
|
||||
if !igcg {
|
||||
temp.ProxyGroup = append(temp.ProxyGroup, sub.ProxyGroup...)
|
||||
}
|
||||
}
|
||||
42
common/template.go
Normal file
42
common/template.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const templatesDir = "templates"
|
||||
|
||||
// LoadTemplate 只读取运行目录下的 templates 目录,防止其他文件内容泄漏
|
||||
func LoadTemplate(templateName string) ([]byte, error) {
|
||||
// 清理路径,防止目录遍历攻击
|
||||
cleanTemplateName := filepath.Clean(templateName)
|
||||
|
||||
// 检查是否尝试访问父目录
|
||||
if strings.HasPrefix(cleanTemplateName, "..") || strings.Contains(cleanTemplateName, string(filepath.Separator)+".."+string(filepath.Separator)) {
|
||||
return nil, NewFileNotFoundError(templateName) // 拒绝包含父目录的路径
|
||||
}
|
||||
|
||||
// 构建完整路径,确保只从 templates 目录读取
|
||||
fullPath := filepath.Join(templatesDir, cleanTemplateName)
|
||||
|
||||
if _, err := os.Stat(fullPath); err == nil {
|
||||
file, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
if file != nil {
|
||||
_ = file.Close()
|
||||
}
|
||||
}(file)
|
||||
result, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, NewFileNotFoundError(templateName)
|
||||
}
|
||||
11
compose.yml
Normal file
11
compose.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
name: sub2clash
|
||||
services:
|
||||
sub2clash:
|
||||
restart: unless-stopped
|
||||
image: nite07/sub2clash:latest
|
||||
ports:
|
||||
- "8011:8011"
|
||||
volumes:
|
||||
# - ./logs:/app/logs
|
||||
# - ./templates:/app/templates
|
||||
- ./data:/app/data
|
||||
10
config.example.json
Normal file
10
config.example.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"address": "0.0.0.0:8011",
|
||||
"meta_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml",
|
||||
"clash_template": "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml",
|
||||
"request_retry_times": 3,
|
||||
"request_max_file_size": 1048576,
|
||||
"cache_expire": 300,
|
||||
"log_level": "info",
|
||||
"short_link_length": 6
|
||||
}
|
||||
22
config.example.yaml
Normal file
22
config.example.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# Sub2Clash 配置文件示例
|
||||
# 复制此文件为 config.yaml 并根据需要修改配置
|
||||
|
||||
# 服务端口
|
||||
address: "0.0.0.0:8011"
|
||||
|
||||
# 模板配置
|
||||
meta_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml"
|
||||
clash_template: "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml"
|
||||
|
||||
# 请求配置
|
||||
request_retry_times: 3
|
||||
request_max_file_size: 1048576 # 1MB in bytes
|
||||
|
||||
# 缓存配置 (秒)
|
||||
cache_expire: 300 # 5 minutes
|
||||
|
||||
# 日志级别 (debug, info, warn, error)
|
||||
log_level: "info"
|
||||
|
||||
# 短链接长度
|
||||
short_link_length: 6
|
||||
131
config/config.go
131
config/config.go
@@ -1,91 +1,76 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int
|
||||
MetaTemplate string
|
||||
ClashTemplate string
|
||||
RequestRetryTimes int
|
||||
RequestMaxFileSize int64
|
||||
CacheExpire int64
|
||||
LogLevel string
|
||||
//BasePath string
|
||||
ShortLinkLength int
|
||||
Address string `mapstructure:"address"`
|
||||
MetaTemplate string `mapstructure:"meta_template"`
|
||||
ClashTemplate string `mapstructure:"clash_template"`
|
||||
RequestRetryTimes int `mapstructure:"request_retry_times"`
|
||||
RequestMaxFileSize int64 `mapstructure:"request_max_file_size"`
|
||||
CacheExpire int64 `mapstructure:"cache_expire"`
|
||||
LogLevel string `mapstructure:"log_level"`
|
||||
ShortLinkLength int `mapstructure:"short_link_length"`
|
||||
}
|
||||
|
||||
var Default *Config
|
||||
var Version string
|
||||
var GlobalConfig *Config
|
||||
var Dev string
|
||||
|
||||
func LoadConfig() error {
|
||||
Default = &Config{
|
||||
MetaTemplate: "template_meta.yaml",
|
||||
ClashTemplate: "template_clash.yaml",
|
||||
RequestRetryTimes: 3,
|
||||
RequestMaxFileSize: 1024 * 1024 * 1,
|
||||
Port: 8011,
|
||||
CacheExpire: 60 * 5,
|
||||
LogLevel: "info",
|
||||
//BasePath: "/",
|
||||
ShortLinkLength: 6,
|
||||
}
|
||||
_ = godotenv.Load()
|
||||
if os.Getenv("PORT") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("PORT"))
|
||||
if err != nil {
|
||||
return errors.New("PORT invalid")
|
||||
v := viper.New()
|
||||
|
||||
// 添加配置文件搜索路径
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("./config")
|
||||
v.AddConfigPath("/etc/sub2clash/")
|
||||
|
||||
// 设置默认值
|
||||
setDefaults(v)
|
||||
|
||||
// 设置环境变量前缀和自动绑定
|
||||
v.SetEnvPrefix("SUB2CLASH")
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
// 尝试按优先级加载不同格式的配置文件
|
||||
configLoaded := false
|
||||
configNames := []string{"config", "sub2clash"}
|
||||
configExts := []string{"yaml", "yml", "json"}
|
||||
|
||||
for _, name := range configNames {
|
||||
for _, ext := range configExts {
|
||||
v.SetConfigName(name)
|
||||
v.SetConfigType(ext)
|
||||
if err := v.ReadInConfig(); err == nil {
|
||||
configLoaded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
Default.Port = atoi
|
||||
}
|
||||
if os.Getenv("META_TEMPLATE") != "" {
|
||||
Default.MetaTemplate = os.Getenv("META_TEMPLATE")
|
||||
}
|
||||
if os.Getenv("CLASH_TEMPLATE") != "" {
|
||||
Default.ClashTemplate = os.Getenv("CLASH_TEMPLATE")
|
||||
}
|
||||
if os.Getenv("REQUEST_RETRY_TIMES") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES"))
|
||||
if err != nil {
|
||||
return errors.New("REQUEST_RETRY_TIMES invalid")
|
||||
if configLoaded {
|
||||
break
|
||||
}
|
||||
Default.RequestRetryTimes = atoi
|
||||
}
|
||||
if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE"))
|
||||
if err != nil {
|
||||
return errors.New("REQUEST_MAX_FILE_SIZE invalid")
|
||||
}
|
||||
Default.RequestMaxFileSize = int64(atoi)
|
||||
}
|
||||
if os.Getenv("CACHE_EXPIRE") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("CACHE_EXPIRE"))
|
||||
if err != nil {
|
||||
return errors.New("CACHE_EXPIRE invalid")
|
||||
}
|
||||
Default.CacheExpire = int64(atoi)
|
||||
}
|
||||
if os.Getenv("LOG_LEVEL") != "" {
|
||||
Default.LogLevel = os.Getenv("LOG_LEVEL")
|
||||
}
|
||||
//if os.Getenv("BASE_PATH") != "" {
|
||||
// Default.BasePath = os.Getenv("BASE_PATH")
|
||||
// if Default.BasePath[len(Default.BasePath)-1] != '/' {
|
||||
// Default.BasePath += "/"
|
||||
// }
|
||||
//}
|
||||
if os.Getenv("SHORT_LINK_LENGTH") != "" {
|
||||
atoi, err := strconv.Atoi(os.Getenv("SHORT_LINK_LENGTH"))
|
||||
if err != nil {
|
||||
return errors.New("SHORT_LINK_LENGTH invalid")
|
||||
}
|
||||
Default.ShortLinkLength = atoi
|
||||
|
||||
// 将配置解析到结构体
|
||||
GlobalConfig = &Config{}
|
||||
if err := v.Unmarshal(GlobalConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDefaults(v *viper.Viper) {
|
||||
v.SetDefault("address", "0.0.0.0:8011")
|
||||
v.SetDefault("meta_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_meta.yaml")
|
||||
v.SetDefault("clash_template", "https://raw.githubusercontent.com/bestnite/sub2clash/refs/heads/main/templates/template_clash.yaml")
|
||||
v.SetDefault("request_retry_times", 3)
|
||||
v.SetDefault("request_max_file_size", 1024*1024*1)
|
||||
v.SetDefault("cache_expire", 60*5)
|
||||
v.SetDefault("log_level", "info")
|
||||
v.SetDefault("short_link_length", 6)
|
||||
}
|
||||
|
||||
3
constant/version.go
Normal file
3
constant/version.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package constant
|
||||
|
||||
var Version = "dev"
|
||||
@@ -1,21 +0,0 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
sub2clash:
|
||||
container_name: sub2clash
|
||||
restart: unless-stopped
|
||||
image: ghcr.io/nitezs/sub2clash:latest
|
||||
ports:
|
||||
- "8011:8011"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./templates:/app/templates
|
||||
- ./data:/app/data
|
||||
# environment:
|
||||
# - PORT=8011
|
||||
# - META_TEMPLATE=template_meta.yaml
|
||||
# - PROXY_TEMPLATE=template_clash.yaml
|
||||
# - REQUEST_RETRY_TIMES=3
|
||||
# - REQUEST_MAX_FILE_SIZE=1048576
|
||||
# - CACHE_EXPIRE=300
|
||||
# - LOG_LEVEL=info
|
||||
153
go.mod
153
go.mod
@@ -1,49 +1,152 @@
|
||||
module sub2clash
|
||||
module github.com/bestnite/sub2clash
|
||||
|
||||
go 1.21
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/glebarez/sqlite v1.9.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/text v0.13.0
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/metacubex/mihomo v1.19.10
|
||||
github.com/spf13/viper v1.20.1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/text v0.30.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.25.4
|
||||
gorm.io/gorm v1.31.0
|
||||
resty.dev/v3 v3.0.0-beta.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.10.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/3andne/restls-client-go v0.1.6 // indirect
|
||||
github.com/RyuaNerin/go-krypto v1.3.0 // indirect
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/buger/jsonparser v1.1.1 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/ebitengine/purego v0.8.3 // indirect
|
||||
github.com/enfein/mieru/v3 v3.13.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.4 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.2 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.20.5 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.2 // indirect
|
||||
github.com/metacubex/fswatch v0.1.1 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect
|
||||
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 // indirect
|
||||
github.com/metacubex/randv2 v0.2.0 // indirect
|
||||
github.com/metacubex/sing v0.5.3 // indirect
|
||||
github.com/metacubex/sing-mux v0.3.2 // indirect
|
||||
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.10 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.4 // indirect
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c // indirect
|
||||
github.com/metacubex/sing-vmess v0.2.2 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f // indirect
|
||||
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 // indirect
|
||||
github.com/metacubex/utls v1.7.3 // indirect
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 // indirect
|
||||
github.com/miekg/dns v1.1.63 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||
github.com/openacid/low v0.1.21 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/samber/lo v1.50.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 // indirect
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
lukechampine.com/blake3 v1.3.0 // indirect
|
||||
modernc.org/libc v1.22.5 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
|
||||
385
go.sum
385
go.sum
@@ -1,120 +1,385 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
|
||||
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
|
||||
github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
|
||||
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
|
||||
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
|
||||
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
|
||||
github.com/RyuaNerin/testingutil v0.1.0/go.mod h1:yTqj6Ta/ycHMPJHRyO12Mz3VrvTloWOsy23WOZH19AA=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
|
||||
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
|
||||
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
|
||||
github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
|
||||
github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
|
||||
github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
|
||||
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
|
||||
github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs=
|
||||
github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.15.4 h1:zMXza4EpOdooxPel5xDqXEdXG5r+WggpvnAKMsalBjs=
|
||||
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
|
||||
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
|
||||
github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
|
||||
github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
|
||||
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
|
||||
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
|
||||
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
|
||||
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
|
||||
github.com/metacubex/mihomo v1.19.10 h1:GXCOA1rJNfU5qYSvo+UBUFksh61M0tjPfvlZ1OsYtfs=
|
||||
github.com/metacubex/mihomo v1.19.10/go.mod h1:ih7BKy1pfqSvPRqaCcuFFK4oNRIFyBotoHX0PbhF7SQ=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
|
||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
|
||||
github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
|
||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||
github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
github.com/metacubex/sing v0.5.3 h1:QWdN16WFKMk06x4nzkc8SvZ7y2x+TLQrpkPoHs+WSVM=
|
||||
github.com/metacubex/sing v0.5.3/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
|
||||
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
|
||||
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
|
||||
github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.10 h1:Pr7LDbjMANIQHl07zWgl1vDuhpsfDQUpZ8cX6DPabfg=
|
||||
github.com/metacubex/sing-shadowsocks v0.2.10/go.mod h1:MtRM0ZZjR0kaDOzy9zWSt6/4/UlrnsNBq+1FNAF4vBk=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.4 h1:Ec0x3hHR7xkld5Z09IGh16wtUUpBb2HgqZ9DExd8Q7s=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.4/go.mod h1:WP8+S0kqtnSbX1vlIpo5i8Irm/ijZITEPBcZ26B5unY=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c h1:Y6jk7AH5BEg9Dsvczrf/KokYsvxeKSZZlCLHg+hC4ro=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250524142129-9d110c0af70c/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U=
|
||||
github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
|
||||
github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
|
||||
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
|
||||
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
|
||||
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||
github.com/metacubex/utls v1.7.3 h1:yDcMEWojFh+t8rU9X0HPcZDPAoFze/rIIyssqivzj8A=
|
||||
github.com/metacubex/utls v1.7.3/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
|
||||
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
|
||||
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
|
||||
github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
|
||||
github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
|
||||
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
|
||||
github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
|
||||
github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
|
||||
github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I=
|
||||
github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do=
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
|
||||
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
|
||||
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
|
||||
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
|
||||
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
|
||||
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU=
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo=
|
||||
github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4=
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
|
||||
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y=
|
||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk=
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
|
||||
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
|
||||
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
@@ -124,4 +389,6 @@ modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
|
||||
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
|
||||
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -1,70 +1,52 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
Logger *zap.Logger
|
||||
lock sync.Mutex
|
||||
logLevel string
|
||||
)
|
||||
var Logger *zap.Logger
|
||||
|
||||
func InitLogger(level string) {
|
||||
logLevel = level
|
||||
buildLogger()
|
||||
go rotateLogs()
|
||||
func InitLogger(logLevel string) {
|
||||
logger := zap.New(buildZapCore(getZapLogLevel(logLevel)))
|
||||
Logger = logger
|
||||
}
|
||||
|
||||
func buildLogger() {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
var level zapcore.Level
|
||||
switch logLevel {
|
||||
case "error":
|
||||
level = zap.ErrorLevel
|
||||
func buildZapCore(logLevel zapcore.Level) zapcore.Core {
|
||||
fileWriter := zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: "logs/app.log",
|
||||
MaxSize: 500,
|
||||
MaxBackups: 3,
|
||||
MaxAge: 28,
|
||||
Compress: true,
|
||||
})
|
||||
consoleWriter := zapcore.AddSync(os.Stdout)
|
||||
|
||||
encoderConfig := zap.NewProductionEncoderConfig()
|
||||
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
|
||||
fileCore := zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), fileWriter, logLevel)
|
||||
consoleCore := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, logLevel)
|
||||
combinedCore := zapcore.NewTee(fileCore, consoleCore)
|
||||
return combinedCore
|
||||
}
|
||||
|
||||
func getZapLogLevel(logLevel string) zapcore.Level {
|
||||
switch strings.ToLower(logLevel) {
|
||||
case "debug":
|
||||
level = zap.DebugLevel
|
||||
return zap.DebugLevel
|
||||
case "warn":
|
||||
level = zap.WarnLevel
|
||||
return zap.WarnLevel
|
||||
case "error":
|
||||
return zap.ErrorLevel
|
||||
case "info":
|
||||
level = zap.InfoLevel
|
||||
return zap.InfoLevel
|
||||
default:
|
||||
level = zap.InfoLevel
|
||||
}
|
||||
zapConfig := zap.NewProductionConfig()
|
||||
zapConfig.Encoding = "console"
|
||||
zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
|
||||
zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
|
||||
zapConfig.OutputPaths = []string{"stdout", getLogFileName("info")}
|
||||
zapConfig.ErrorOutputPaths = []string{"stderr", getLogFileName("error")}
|
||||
zapConfig.Level = zap.NewAtomicLevelAt(level)
|
||||
var err error
|
||||
Logger, err = zapConfig.Build()
|
||||
if err != nil {
|
||||
panic("log failed" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 根据日期获得日志文件
|
||||
func getLogFileName(name string) string {
|
||||
return filepath.Join("logs", time.Now().Format("2006-01-02")+"-"+name+".log")
|
||||
}
|
||||
|
||||
func rotateLogs() {
|
||||
for {
|
||||
now := time.Now()
|
||||
nextMidnight := time.Date(
|
||||
now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location(),
|
||||
).Add(24 * time.Hour)
|
||||
durationUntilMidnight := nextMidnight.Sub(now)
|
||||
|
||||
time.Sleep(durationUntilMidnight)
|
||||
buildLogger()
|
||||
return zap.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
65
main.go
65
main.go
@@ -3,75 +3,44 @@ package main
|
||||
import (
|
||||
_ "embed"
|
||||
"io"
|
||||
"strconv"
|
||||
"sub2clash/api"
|
||||
"sub2clash/config"
|
||||
"sub2clash/logger"
|
||||
"sub2clash/utils"
|
||||
"sub2clash/utils/database"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
"github.com/bestnite/sub2clash/server"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
//go:embed templates/template_meta.yaml
|
||||
var templateMeta string
|
||||
|
||||
//go:embed templates/template_clash.yaml
|
||||
var templateClash string
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
// 创建文件夹
|
||||
err = utils.MkEssentialDir()
|
||||
|
||||
err = common.MkEssentialDir()
|
||||
if err != nil {
|
||||
logger.Logger.Panic("create essential dir failed", zap.Error(err))
|
||||
}
|
||||
// 加载配置
|
||||
|
||||
err = config.LoadConfig()
|
||||
// 初始化日志
|
||||
logger.InitLogger(config.Default.LogLevel)
|
||||
|
||||
logger.InitLogger(config.GlobalConfig.LogLevel)
|
||||
if err != nil {
|
||||
logger.Logger.Panic("load config failed", zap.Error(err))
|
||||
}
|
||||
// 检查更新
|
||||
if config.Dev != "true" {
|
||||
go func() {
|
||||
update, newVersion, err := utils.CheckUpdate()
|
||||
if err != nil {
|
||||
logger.Logger.Warn("check update failed", zap.Error(err))
|
||||
}
|
||||
if update {
|
||||
logger.Logger.Info("new version is available", zap.String("version", newVersion))
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
logger.Logger.Info("running in dev mode")
|
||||
}
|
||||
// 写入默认模板
|
||||
err = utils.WriteDefalutTemplate(templateMeta, templateClash)
|
||||
if err != nil {
|
||||
logger.Logger.Panic("write default template failed", zap.Error(err))
|
||||
}
|
||||
// 连接数据库
|
||||
err = database.ConnectDB()
|
||||
if err != nil {
|
||||
logger.Logger.Panic("database connect failed", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Logger.Info("database connect success")
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 设置运行模式
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
// 关闭 Gin 的日志输出
|
||||
|
||||
gin.DefaultWriter = io.Discard
|
||||
// 创建路由
|
||||
|
||||
r := gin.Default()
|
||||
// 设置路由
|
||||
api.SetRoute(r)
|
||||
logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port))
|
||||
err := r.Run(":" + strconv.Itoa(config.Default.Port))
|
||||
|
||||
server.SetRoute(r)
|
||||
logger.Logger.Info("server is running at " + config.GlobalConfig.Address)
|
||||
err := r.Run(config.GlobalConfig.Address)
|
||||
if err != nil {
|
||||
logger.Logger.Error("server running failed", zap.Error(err))
|
||||
return
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "github.com/bestnite/sub2clash/parser"
|
||||
|
||||
type ClashType int
|
||||
|
||||
const (
|
||||
@@ -8,24 +10,20 @@ const (
|
||||
)
|
||||
|
||||
func GetSupportProxyTypes(clashType ClashType) map[string]bool {
|
||||
if clashType == Clash {
|
||||
return map[string]bool{
|
||||
"ss": true,
|
||||
"ssr": true,
|
||||
"vmess": true,
|
||||
"trojan": true,
|
||||
supportProxyTypes := make(map[string]bool)
|
||||
|
||||
for _, parser := range parser.GetAllParsers() {
|
||||
switch clashType {
|
||||
case Clash:
|
||||
if parser.SupportClash() {
|
||||
supportProxyTypes[parser.GetType()] = true
|
||||
}
|
||||
case ClashMeta:
|
||||
if parser.SupportMeta() {
|
||||
supportProxyTypes[parser.GetType()] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if clashType == ClashMeta {
|
||||
return map[string]bool{
|
||||
"ss": true,
|
||||
"ssr": true,
|
||||
"vmess": true,
|
||||
"trojan": true,
|
||||
"vless": true,
|
||||
"hysteria": true,
|
||||
"hysteria2": true,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return supportProxyTypes
|
||||
}
|
||||
|
||||
92
model/convert_config.go
Normal file
92
model/convert_config.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ConvertConfig struct {
|
||||
ClashType ClashType `json:"clashType" binding:"required"`
|
||||
Subs []string `json:"subscriptions" binding:""`
|
||||
Proxies []string `json:"proxies" binding:""`
|
||||
Refresh bool `json:"refresh" binding:""`
|
||||
Template string `json:"template" binding:""`
|
||||
RuleProviders []RuleProviderStruct `json:"ruleProviders" binding:""`
|
||||
Rules []RuleStruct `json:"rules" binding:""`
|
||||
AutoTest bool `json:"autoTest" binding:""`
|
||||
Lazy bool `json:"lazy" binding:""`
|
||||
Sort string `json:"sort" binding:""`
|
||||
Remove string `json:"remove" binding:""`
|
||||
Replace map[string]string `json:"replace" binding:""`
|
||||
NodeListMode bool `json:"nodeList" binding:""`
|
||||
IgnoreCountryGrooup bool `json:"ignoreCountryGroup" binding:""`
|
||||
UserAgent string `json:"userAgent" binding:""`
|
||||
UseUDP bool `json:"useUDP" binding:""`
|
||||
}
|
||||
|
||||
type RuleProviderStruct struct {
|
||||
Behavior string `json:"behavior" binding:""`
|
||||
Url string `json:"url" binding:""`
|
||||
Group string `json:"group" binding:""`
|
||||
Prepend bool `json:"prepend" binding:""`
|
||||
Name string `json:"name" binding:""`
|
||||
}
|
||||
|
||||
type RuleStruct struct {
|
||||
Rule string `json:"rule" binding:""`
|
||||
Prepend bool `json:"prepend" binding:""`
|
||||
}
|
||||
|
||||
func ParseConvertQuery(c *gin.Context) (ConvertConfig, error) {
|
||||
config := c.Param("config")
|
||||
queryBytes, err := utils.DecodeBase64(config, true)
|
||||
if err != nil {
|
||||
return ConvertConfig{}, errors.New("参数错误: " + err.Error())
|
||||
}
|
||||
var query ConvertConfig
|
||||
err = json.Unmarshal([]byte(queryBytes), &query)
|
||||
if err != nil {
|
||||
return ConvertConfig{}, errors.New("参数错误: " + err.Error())
|
||||
}
|
||||
if len(query.Subs) == 0 && len(query.Proxies) == 0 {
|
||||
return ConvertConfig{}, errors.New("参数错误: sub 和 proxy 不能同时为空")
|
||||
}
|
||||
if len(query.Subs) > 0 {
|
||||
for i := range query.Subs {
|
||||
if !strings.HasPrefix(query.Subs[i], "http") {
|
||||
return ConvertConfig{}, errors.New("参数错误: sub 格式错误")
|
||||
}
|
||||
if _, err := url.ParseRequestURI(query.Subs[i]); err != nil {
|
||||
return ConvertConfig{}, errors.New("参数错误: " + err.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
query.Subs = nil
|
||||
}
|
||||
if query.Template != "" {
|
||||
if strings.HasPrefix(query.Template, "http") {
|
||||
uri, err := url.ParseRequestURI(query.Template)
|
||||
if err != nil {
|
||||
return ConvertConfig{}, err
|
||||
}
|
||||
query.Template = uri.String()
|
||||
}
|
||||
}
|
||||
if len(query.RuleProviders) > 0 {
|
||||
names := make(map[string]bool)
|
||||
for _, ruleProvider := range query.RuleProviders {
|
||||
if _, ok := names[ruleProvider.Name]; ok {
|
||||
return ConvertConfig{}, errors.New("参数错误: Rule-Provider 名称重复")
|
||||
}
|
||||
names[ruleProvider.Name] = true
|
||||
}
|
||||
} else {
|
||||
query.RuleProviders = nil
|
||||
}
|
||||
return query, nil
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
package model
|
||||
|
||||
// https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9F%9F%E6%8C%87%E7%A4%BA%E7%AC%A6
|
||||
// https://zh.wikipedia.org/zh-sg/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81
|
||||
|
||||
var CountryEnglishName = map[string]string{
|
||||
"Andorra": "安道尔(AD)",
|
||||
"United Arab Emirates": "阿联酋(AE)",
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package model
|
||||
|
||||
type Tags []struct {
|
||||
Name string `json:"name"`
|
||||
ZipballUrl string `json:"zipball_url"`
|
||||
TarballUrl string `json:"tarball_url"`
|
||||
Commit struct {
|
||||
Sha string `json:"sha"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
NodeId string `json:"node_id"`
|
||||
}
|
||||
70
model/group.go
Normal file
70
model/group.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type ProxyGroup struct {
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
IsCountryGrop bool `yaml:"-"`
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Tolerance int `yaml:"tolerance,omitempty"`
|
||||
Lazy bool `yaml:"lazy"`
|
||||
Size int `yaml:"-"`
|
||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||
Strategy string `yaml:"strategy,omitempty"`
|
||||
Icon string `yaml:"icon,omitempty"`
|
||||
Timeout int `yaml:"timeout,omitempty"`
|
||||
Use []string `yaml:"use,omitempty"`
|
||||
InterfaceName string `yaml:"interface-name,omitempty"`
|
||||
RoutingMark int `yaml:"routing-mark,omitempty"`
|
||||
IncludeAll bool `yaml:"include-all,omitempty"`
|
||||
IncludeAllProxies bool `yaml:"include-all-proxies,omitempty"`
|
||||
IncludeAllProviders bool `yaml:"include-all-providers,omitempty"`
|
||||
Filter string `yaml:"filter,omitempty"`
|
||||
ExcludeFilter string `yaml:"exclude-filter,omitempty"`
|
||||
ExpectedStatus int `yaml:"expected-status,omitempty"`
|
||||
Hidden bool `yaml:"hidden,omitempty"`
|
||||
}
|
||||
|
||||
type ProxyGroupsSortByName []ProxyGroup
|
||||
type ProxyGroupsSortBySize []ProxyGroup
|
||||
|
||||
func (p ProxyGroupsSortByName) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
func (p ProxyGroupsSortBySize) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortByName) Less(i, j int) bool {
|
||||
|
||||
tags := []language.Tag{
|
||||
language.English,
|
||||
language.Chinese,
|
||||
}
|
||||
matcher := language.NewMatcher(tags)
|
||||
|
||||
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||
|
||||
c := collate.New(bestMatch)
|
||||
return c.CompareString(p[i].Name, p[j].Name) < 0
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortBySize) Less(i, j int) bool {
|
||||
if p[i].Size == p[j].Size {
|
||||
return p[i].Name < p[j].Name
|
||||
}
|
||||
return p[i].Size < p[j].Size
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortByName) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
func (p ProxyGroupsSortBySize) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package model
|
||||
|
||||
type SmuxStruct struct {
|
||||
Enabled bool `yaml:"enable"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Server string `yaml:"server,omitempty"`
|
||||
Port int `yaml:"port,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Cipher string `yaml:"cipher,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
UUID string `yaml:"uuid,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
Flow string `yaml:"flow,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
Plugin string `yaml:"plugin,omitempty"`
|
||||
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
||||
Smux SmuxStruct `yaml:"smux,omitempty"`
|
||||
Sni string `yaml:"sni,omitempty"`
|
||||
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Alpn []string `yaml:"alpn,omitempty"`
|
||||
XUDP bool `yaml:"xudp,omitempty"`
|
||||
Servername string `yaml:"servername,omitempty"`
|
||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||
AlterID int `yaml:"alterId,omitempty"`
|
||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
ObfsParam string `yaml:"obfs-param,omitempty"`
|
||||
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
||||
Remarks []string `yaml:"remarks,omitempty"`
|
||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
||||
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
||||
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
||||
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
||||
SubName string `yaml:"-"`
|
||||
Up string `yaml:"up,omitempty"`
|
||||
Down string `yaml:"down,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
CWND int `yaml:"cwnd,omitempty"`
|
||||
Auth string `yaml:"auth,omitempty"`
|
||||
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `yaml:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
|
||||
FastOpen bool `yaml:"fast-open,omitempty"`
|
||||
HopInterval int `yaml:"hop-interval,omitempty"`
|
||||
}
|
||||
|
||||
func (p Proxy) MarshalYAML() (interface{}, error) {
|
||||
switch p.Type {
|
||||
case "vmess":
|
||||
return ProxyToVmess(p), nil
|
||||
case "ss":
|
||||
return ProxyToShadowSocks(p), nil
|
||||
case "ssr":
|
||||
return ProxyToShadowSocksR(p), nil
|
||||
case "vless":
|
||||
return ProxyToVless(p), nil
|
||||
case "trojan":
|
||||
return ProxyToTrojan(p), nil
|
||||
case "hysteria":
|
||||
return ProxyToHysteria(p), nil
|
||||
case "hysteria2":
|
||||
return ProxyToHysteria2(p), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
18
model/proxy/anytls.go
Normal file
18
model/proxy/anytls.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/anytls.go
|
||||
type Anytls struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
IdleSessionCheckInterval int `yaml:"idle-session-check-interval,omitempty"`
|
||||
IdleSessionTimeout int `yaml:"idle-session-timeout,omitempty"`
|
||||
MinIdleSession int `yaml:"min-idle-session,omitempty"`
|
||||
}
|
||||
29
model/proxy/hysteria.go
Normal file
29
model/proxy/hysteria.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/hysteria.go
|
||||
type Hysteria struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port,omitempty"`
|
||||
Ports string `yaml:"ports,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
ObfsProtocol string `yaml:"obfs-protocol,omitempty"` // compatible with Stash
|
||||
Up string `yaml:"up"`
|
||||
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
|
||||
Down string `yaml:"down"`
|
||||
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
|
||||
Auth string `yaml:"auth,omitempty"`
|
||||
AuthString string `yaml:"auth-str,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `yaml:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
|
||||
FastOpen bool `yaml:"fast-open,omitempty"`
|
||||
HopInterval int `yaml:"hop-interval,omitempty"`
|
||||
}
|
||||
29
model/proxy/hysteria2.go
Normal file
29
model/proxy/hysteria2.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/hysteria2.go
|
||||
type Hysteria2 struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port,omitempty"`
|
||||
Ports string `yaml:"ports,omitempty"`
|
||||
HopInterval int `yaml:"hop-interval,omitempty"`
|
||||
Up string `yaml:"up,omitempty"`
|
||||
Down string `yaml:"down,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
ObfsPassword string `yaml:"obfs-password,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
CWND int `yaml:"cwnd,omitempty"`
|
||||
UdpMTU int `yaml:"udp-mtu,omitempty"`
|
||||
|
||||
// quic-go special config
|
||||
InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window,omitempty"`
|
||||
MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window,omitempty"`
|
||||
InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window,omitempty"`
|
||||
MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window,omitempty"`
|
||||
}
|
||||
305
model/proxy/proxy.go
Normal file
305
model/proxy/proxy.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type IntOrString int
|
||||
|
||||
func (i *IntOrString) UnmarshalYAML(value *yaml.Node) error {
|
||||
intVal := 0
|
||||
err := yaml.Unmarshal([]byte(value.Value), &intVal)
|
||||
if err == nil {
|
||||
*i = IntOrString(intVal)
|
||||
}
|
||||
strVal := ""
|
||||
err = yaml.Unmarshal([]byte(value.Value), &strVal)
|
||||
if err == nil {
|
||||
_int, err := strconv.ParseInt(strVal, 10, 64)
|
||||
if err != nil {
|
||||
*i = IntOrString(_int)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type HTTPOptions struct {
|
||||
Method string `yaml:"method,omitempty"`
|
||||
Path []string `yaml:"path,omitempty"`
|
||||
Headers map[string][]string `yaml:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type HTTP2Options struct {
|
||||
Host []string `yaml:"host,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
}
|
||||
|
||||
type GrpcOptions struct {
|
||||
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
|
||||
}
|
||||
|
||||
type RealityOptions struct {
|
||||
PublicKey string `yaml:"public-key"`
|
||||
ShortID string `yaml:"short-id,omitempty"`
|
||||
}
|
||||
|
||||
type WSOptions struct {
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
MaxEarlyData int `yaml:"max-early-data,omitempty"`
|
||||
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
|
||||
}
|
||||
|
||||
type SmuxStruct struct {
|
||||
Enabled bool `yaml:"enable"`
|
||||
}
|
||||
|
||||
type WireGuardPeerOption struct {
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
PublicKey string `yaml:"public-key,omitempty"`
|
||||
PreSharedKey string `yaml:"pre-shared-key,omitempty"`
|
||||
Reserved []uint8 `yaml:"reserved,omitempty"`
|
||||
AllowedIPs []string `yaml:"allowed-ips,omitempty"`
|
||||
}
|
||||
|
||||
type ECHOptions struct {
|
||||
Enable bool `yaml:"enable,omitempty" obfs:"enable,omitempty"`
|
||||
Config string `yaml:"config,omitempty" obfs:"config,omitempty"`
|
||||
}
|
||||
|
||||
type Proxy struct {
|
||||
Type string
|
||||
Name string
|
||||
SubName string `yaml:"-"`
|
||||
Anytls
|
||||
Hysteria
|
||||
Hysteria2
|
||||
ShadowSocks
|
||||
ShadowSocksR
|
||||
Trojan
|
||||
Vless
|
||||
Vmess
|
||||
Socks
|
||||
}
|
||||
|
||||
func (p Proxy) MarshalYAML() (any, error) {
|
||||
switch p.Type {
|
||||
case "anytls":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Anytls `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Anytls: p.Anytls,
|
||||
}, nil
|
||||
case "hysteria":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Hysteria `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Hysteria: p.Hysteria,
|
||||
}, nil
|
||||
case "hysteria2":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Hysteria2 `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Hysteria2: p.Hysteria2,
|
||||
}, nil
|
||||
case "ss":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
ShadowSocks `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
ShadowSocks: p.ShadowSocks,
|
||||
}, nil
|
||||
case "ssr":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
ShadowSocksR `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
ShadowSocksR: p.ShadowSocksR,
|
||||
}, nil
|
||||
case "trojan":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Trojan `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Trojan: p.Trojan,
|
||||
}, nil
|
||||
case "vless":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Vless `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Vless: p.Vless,
|
||||
}, nil
|
||||
case "vmess":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Vmess `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Vmess: p.Vmess,
|
||||
}, nil
|
||||
case "socks5":
|
||||
return struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Socks `yaml:",inline"`
|
||||
}{
|
||||
Type: p.Type,
|
||||
Name: p.Name,
|
||||
Socks: p.Socks,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy type: %s", p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) UnmarshalYAML(node *yaml.Node) error {
|
||||
var temp struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
if err := node.Decode(&temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Type = temp.Type
|
||||
p.Name = temp.Name
|
||||
|
||||
switch temp.Type {
|
||||
case "anytls":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Anytls `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Anytls = data.Anytls
|
||||
|
||||
case "hysteria":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Hysteria `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Hysteria = data.Hysteria
|
||||
|
||||
case "hysteria2":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Hysteria2 `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Hysteria2 = data.Hysteria2
|
||||
|
||||
case "ss":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
ShadowSocks `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.ShadowSocks = data.ShadowSocks
|
||||
|
||||
case "ssr":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
ShadowSocksR `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.ShadowSocksR = data.ShadowSocksR
|
||||
|
||||
case "trojan":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Trojan `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Trojan = data.Trojan
|
||||
|
||||
case "vless":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Vless `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Vless = data.Vless
|
||||
|
||||
case "vmess":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Vmess `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Vmess = data.Vmess
|
||||
|
||||
case "socks5":
|
||||
var data struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Socks `yaml:",inline"`
|
||||
}
|
||||
if err := node.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
p.Socks = data.Socks
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported proxy type: %s", temp.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
15
model/proxy/shadowsocks.go
Normal file
15
model/proxy/shadowsocks.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/shadowsocks.go
|
||||
type ShadowSocks struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
Cipher string `yaml:"cipher"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
Plugin string `yaml:"plugin,omitempty"`
|
||||
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
||||
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
||||
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
}
|
||||
14
model/proxy/shadowsocksr.go
Normal file
14
model/proxy/shadowsocksr.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/shadowsocksr.go
|
||||
type ShadowSocksR struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
Cipher string `yaml:"cipher"`
|
||||
Obfs string `yaml:"obfs"`
|
||||
ObfsParam string `yaml:"obfs-param,omitempty"`
|
||||
Protocol string `yaml:"protocol"`
|
||||
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
}
|
||||
13
model/proxy/socks.go
Normal file
13
model/proxy/socks.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/socks5.go
|
||||
type Socks struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
UserName string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/trojan.go
|
||||
type Trojan struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
@@ -12,28 +11,16 @@ type Trojan struct {
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||
SSOpts TrojanSSOption `yaml:"ss-opts,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToTrojan(p Proxy) Trojan {
|
||||
return Trojan{
|
||||
Type: "trojan",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
Password: p.Password,
|
||||
ALPN: p.Alpn,
|
||||
SNI: p.Sni,
|
||||
SkipCertVerify: p.SkipCertVerify,
|
||||
Fingerprint: p.Fingerprint,
|
||||
UDP: p.UDP,
|
||||
Network: p.Network,
|
||||
RealityOpts: p.RealityOpts,
|
||||
GrpcOpts: p.GrpcOpts,
|
||||
WSOpts: p.WSOpts,
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
type TrojanSSOption struct {
|
||||
Enabled bool `yaml:"enabled,omitempty"`
|
||||
Method string `yaml:"method,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package model
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/vless.go
|
||||
type Vless struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
UUID string `yaml:"uuid"`
|
||||
Flow string `yaml:"flow,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
@@ -14,6 +13,7 @@ type Vless struct {
|
||||
XUDP bool `yaml:"xudp,omitempty"`
|
||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||
@@ -26,32 +26,3 @@ type Vless struct {
|
||||
ServerName string `yaml:"servername,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToVless(p Proxy) Vless {
|
||||
return Vless{
|
||||
Type: "vless",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
UUID: p.UUID,
|
||||
Flow: p.Flow,
|
||||
TLS: p.TLS,
|
||||
ALPN: p.Alpn,
|
||||
UDP: p.UDP,
|
||||
PacketAddr: p.PacketAddr,
|
||||
XUDP: p.XUDP,
|
||||
PacketEncoding: p.PacketEncoding,
|
||||
Network: p.Network,
|
||||
RealityOpts: p.RealityOpts,
|
||||
HTTPOpts: p.HTTPOpts,
|
||||
HTTP2Opts: p.HTTP2Opts,
|
||||
GrpcOpts: p.GrpcOpts,
|
||||
WSOpts: p.WSOpts,
|
||||
WSPath: p.WSOpts.Path,
|
||||
WSHeaders: p.WSOpts.Headers,
|
||||
SkipCertVerify: p.SkipCertVerify,
|
||||
Fingerprint: p.Fingerprint,
|
||||
ServerName: p.Servername,
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
29
model/proxy/vmess.go
Normal file
29
model/proxy/vmess.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package proxy
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/adapter/outbound/vmess.go
|
||||
type Vmess struct {
|
||||
Server string `yaml:"server"`
|
||||
Port IntOrString `yaml:"port"`
|
||||
UUID string `yaml:"uuid"`
|
||||
AlterID IntOrString `yaml:"alterId"`
|
||||
Cipher string `yaml:"cipher"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
ServerName string `yaml:"servername,omitempty"`
|
||||
ECHOpts ECHOptions `yaml:"ech-opts,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
||||
XUDP bool `yaml:"xudp,omitempty"`
|
||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
||||
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"golang.org/x/text/collate"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type ProxyGroup struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
IsCountryGrop bool `yaml:"-"`
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Tolerance int `yaml:"tolerance,omitempty"`
|
||||
Lazy bool `yaml:"lazy"`
|
||||
Size int `yaml:"-"`
|
||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||
Strategy string `yaml:"strategy,omitempty"`
|
||||
}
|
||||
|
||||
type SelectProxyGroup struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||
}
|
||||
|
||||
type UrlTestProxyGroup struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Tolerance int `yaml:"tolerance,omitempty"`
|
||||
Lazy bool `yaml:"lazy"`
|
||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||
}
|
||||
|
||||
type LoadBalanceProxyGroup struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Lazy bool `yaml:"lazy"`
|
||||
Strategy string `yaml:"strategy,omitempty"`
|
||||
}
|
||||
|
||||
type RelayProxyGroup struct {
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Type string `yaml:"type,omitempty"`
|
||||
Proxies []string `yaml:"proxies,omitempty"`
|
||||
}
|
||||
|
||||
func (p ProxyGroup) MarshalYAML() (interface{}, error) {
|
||||
switch p.Type {
|
||||
case "select":
|
||||
return SelectProxyGroup{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Proxies: p.Proxies,
|
||||
DisableUDP: p.DisableUDP,
|
||||
}, nil
|
||||
case "url-test", "fallback":
|
||||
return UrlTestProxyGroup{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Proxies: p.Proxies,
|
||||
Url: p.Url,
|
||||
Interval: p.Interval,
|
||||
Tolerance: p.Tolerance,
|
||||
Lazy: p.Lazy,
|
||||
DisableUDP: p.DisableUDP,
|
||||
}, nil
|
||||
case "load-balance":
|
||||
return LoadBalanceProxyGroup{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Proxies: p.Proxies,
|
||||
DisableUDP: p.DisableUDP,
|
||||
Url: p.Url,
|
||||
Interval: p.Interval,
|
||||
Lazy: p.Lazy,
|
||||
Strategy: p.Strategy,
|
||||
}, nil
|
||||
case "relay":
|
||||
return RelayProxyGroup{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Proxies: p.Proxies,
|
||||
}, nil
|
||||
default:
|
||||
return SelectProxyGroup{
|
||||
Name: p.Name,
|
||||
Type: p.Type,
|
||||
Proxies: p.Proxies,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type ProxyGroupsSortByName []ProxyGroup
|
||||
type ProxyGroupsSortBySize []ProxyGroup
|
||||
|
||||
func (p ProxyGroupsSortByName) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
func (p ProxyGroupsSortBySize) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortByName) Less(i, j int) bool {
|
||||
// 定义一组备选语言:首选英语,其次中文
|
||||
tags := []language.Tag{
|
||||
language.English,
|
||||
language.Chinese,
|
||||
}
|
||||
matcher := language.NewMatcher(tags)
|
||||
|
||||
// 假设我们的请求语言是 "zh"(中文),则使用匹配器找到最佳匹配的语言
|
||||
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||
// 使用最佳匹配的语言进行排序
|
||||
c := collate.New(bestMatch)
|
||||
return c.CompareString(p[i].Name, p[j].Name) < 0
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortBySize) Less(i, j int) bool {
|
||||
if p[i].Size == p[j].Size {
|
||||
return p[i].Name < p[j].Name
|
||||
}
|
||||
return p[i].Size < p[j].Size
|
||||
}
|
||||
|
||||
func (p ProxyGroupsSortByName) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
func (p ProxyGroupsSortBySize) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package model
|
||||
|
||||
type Hysteria struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port,omitempty"`
|
||||
Ports string `yaml:"ports,omitempty"`
|
||||
Protocol string `yaml:"protocol,omitempty"`
|
||||
ObfsProtocol string `yaml:"obfs-protocol,omitempty"` // compatible with Stash
|
||||
Up string `yaml:"up"`
|
||||
UpSpeed int `yaml:"up-speed,omitempty"` // compatible with Stash
|
||||
Down string `yaml:"down"`
|
||||
DownSpeed int `yaml:"down-speed,omitempty"` // compatible with Stash
|
||||
Auth string `yaml:"auth,omitempty"`
|
||||
AuthString string `yaml:"auth-str,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
ReceiveWindowConn int `yaml:"recv-window-conn,omitempty"`
|
||||
ReceiveWindow int `yaml:"recv-window,omitempty"`
|
||||
DisableMTUDiscovery bool `yaml:"disable-mtu-discovery,omitempty"`
|
||||
FastOpen bool `yaml:"fast-open,omitempty"`
|
||||
HopInterval int `yaml:"hop-interval,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToHysteria(p Proxy) Hysteria {
|
||||
return Hysteria{
|
||||
Type: "hysteria",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
Up: p.Up,
|
||||
Down: p.Down,
|
||||
Auth: p.Auth,
|
||||
Obfs: p.Obfs,
|
||||
SNI: p.Sni,
|
||||
SkipCertVerify: p.SkipCertVerify,
|
||||
Fingerprint: p.Fingerprint,
|
||||
ALPN: p.Alpn,
|
||||
CustomCA: p.CustomCA,
|
||||
CustomCAString: p.CustomCAString,
|
||||
ReceiveWindowConn: p.ReceiveWindowConn,
|
||||
ReceiveWindow: p.ReceiveWindow,
|
||||
DisableMTUDiscovery: p.DisableMTUDiscovery,
|
||||
FastOpen: p.FastOpen,
|
||||
HopInterval: p.HopInterval,
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package model
|
||||
|
||||
type Hysteria2 struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Up string `yaml:"up,omitempty"`
|
||||
Down string `yaml:"down,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
Obfs string `yaml:"obfs,omitempty"`
|
||||
ObfsPassword string `yaml:"obfs-password,omitempty"`
|
||||
SNI string `yaml:"sni,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
CustomCA string `yaml:"ca,omitempty"`
|
||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
||||
CWND int `yaml:"cwnd,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToHysteria2(p Proxy) Hysteria2 {
|
||||
return Hysteria2{
|
||||
Type: "hysteria2",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
Up: p.Up,
|
||||
Down: p.Down,
|
||||
Password: p.Password,
|
||||
Obfs: p.Obfs,
|
||||
ObfsPassword: p.ObfsParam,
|
||||
SNI: p.Sni,
|
||||
SkipCertVerify: p.SkipCertVerify,
|
||||
Fingerprint: p.Fingerprint,
|
||||
ALPN: p.Alpn,
|
||||
CustomCA: p.CustomCA,
|
||||
CustomCAString: p.CustomCAString,
|
||||
CWND: p.CWND,
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package model
|
||||
|
||||
type ShadowSocks struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
Cipher string `yaml:"cipher"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
Plugin string `yaml:"plugin,omitempty"`
|
||||
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
||||
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
||||
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToShadowSocks(p Proxy) ShadowSocks {
|
||||
return ShadowSocks{
|
||||
Type: "ss",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
Password: p.Password,
|
||||
Cipher: p.Cipher,
|
||||
UDP: p.UDP,
|
||||
Plugin: p.Plugin,
|
||||
PluginOpts: p.PluginOpts,
|
||||
UDPOverTCP: p.UDPOverTCP,
|
||||
UDPOverTCPVersion: p.UDPOverTCPVersion,
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package model
|
||||
|
||||
type ShadowSocksR struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
Cipher string `yaml:"cipher"`
|
||||
Obfs string `yaml:"obfs"`
|
||||
ObfsParam string `yaml:"obfs-param,omitempty"`
|
||||
Protocol string `yaml:"protocol"`
|
||||
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToShadowSocksR(p Proxy) ShadowSocksR {
|
||||
return ShadowSocksR{
|
||||
Type: "ssr",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
Password: p.Password,
|
||||
Cipher: p.Cipher,
|
||||
Obfs: p.Obfs,
|
||||
ObfsParam: p.ObfsParam,
|
||||
Protocol: p.Protocol,
|
||||
ProtocolParam: p.ProtocolParam,
|
||||
UDP: p.UDP,
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package model
|
||||
|
||||
type HTTPOptions struct {
|
||||
Method string `yaml:"method,omitempty"`
|
||||
Path []string `yaml:"path,omitempty"`
|
||||
Headers map[string][]string `yaml:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type HTTP2Options struct {
|
||||
Host []string `yaml:"host,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
}
|
||||
|
||||
type GrpcOptions struct {
|
||||
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
|
||||
}
|
||||
|
||||
type RealityOptions struct {
|
||||
PublicKey string `yaml:"public-key"`
|
||||
ShortID string `yaml:"short-id,omitempty"`
|
||||
}
|
||||
|
||||
type WSOptions struct {
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
MaxEarlyData int `yaml:"max-early-data,omitempty"`
|
||||
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
|
||||
}
|
||||
|
||||
type VmessJson struct {
|
||||
V string `json:"v"`
|
||||
Ps string `json:"ps"`
|
||||
Add string `json:"add"`
|
||||
Port interface{} `json:"port"`
|
||||
Id string `json:"id"`
|
||||
Aid interface{} `json:"aid"`
|
||||
Scy string `json:"scy"`
|
||||
Net string `json:"net"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Tls string `json:"tls"`
|
||||
Sni string `json:"sni"`
|
||||
Alpn string `json:"alpn"`
|
||||
Fp string `json:"fp"`
|
||||
}
|
||||
|
||||
type Vmess struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Server string `yaml:"server"`
|
||||
Port int `yaml:"port"`
|
||||
UUID string `yaml:"uuid"`
|
||||
AlterID int `yaml:"alterId"`
|
||||
Cipher string `yaml:"cipher"`
|
||||
UDP bool `yaml:"udp,omitempty"`
|
||||
Network string `yaml:"network,omitempty"`
|
||||
TLS bool `yaml:"tls,omitempty"`
|
||||
ALPN []string `yaml:"alpn,omitempty"`
|
||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||
ServerName string `yaml:"servername,omitempty"`
|
||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
||||
XUDP bool `yaml:"xudp,omitempty"`
|
||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
||||
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
func ProxyToVmess(p Proxy) Vmess {
|
||||
return Vmess{
|
||||
Type: "vmess",
|
||||
Name: p.Name,
|
||||
Server: p.Server,
|
||||
Port: p.Port,
|
||||
UUID: p.UUID,
|
||||
AlterID: p.AlterID,
|
||||
Cipher: p.Cipher,
|
||||
UDP: p.UDP,
|
||||
Network: p.Network,
|
||||
TLS: p.TLS,
|
||||
ALPN: p.Alpn,
|
||||
SkipCertVerify: p.SkipCertVerify,
|
||||
Fingerprint: p.Fingerprint,
|
||||
ServerName: p.Servername,
|
||||
RealityOpts: p.RealityOpts,
|
||||
HTTPOpts: p.HTTPOpts,
|
||||
HTTP2Opts: p.HTTP2Opts,
|
||||
GrpcOpts: p.GrpcOpts,
|
||||
WSOpts: p.WSOpts,
|
||||
PacketAddr: p.PacketAddr,
|
||||
XUDP: p.XUDP,
|
||||
PacketEncoding: p.PacketEncoding,
|
||||
GlobalPadding: p.GlobalPadding,
|
||||
AuthenticatedLength: p.AuthenticatedLength,
|
||||
ClientFingerprint: p.ClientFingerprint,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ type RuleProvider struct {
|
||||
Url string `yaml:"url,omitempty"`
|
||||
Path string `yaml:"path,omitempty"`
|
||||
Interval int `yaml:"interval,omitempty"`
|
||||
Format string `yaml:"format,omitempty"`
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package model
|
||||
|
||||
type ShortLink struct {
|
||||
Hash string `gorm:"primary_key"`
|
||||
Url string
|
||||
ID string `gorm:"unique"`
|
||||
Config ConvertConfig `gorm:"serializer:json"`
|
||||
Password string
|
||||
LastRequestTime int64
|
||||
}
|
||||
|
||||
18
model/sub.go
18
model/sub.go
@@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
type Subscription struct {
|
||||
Port int `yaml:"port,omitempty"`
|
||||
SocksPort int `yaml:"socks-port,omitempty"`
|
||||
AllowLan bool `yaml:"allow-lan"`
|
||||
Mode string `yaml:"mode,omitempty"`
|
||||
LogLevel string `yaml:"logger-level,omitempty"`
|
||||
ExternalController string `yaml:"external-controller,omitempty"`
|
||||
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
|
||||
Rules []string `yaml:"rules,omitempty"`
|
||||
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
|
||||
}
|
||||
|
||||
type NodeList struct {
|
||||
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||
}
|
||||
83
model/subscription.go
Normal file
83
model/subscription.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"github.com/bestnite/sub2clash/model/proxy"
|
||||
C "github.com/metacubex/mihomo/config"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
)
|
||||
|
||||
type NodeList struct {
|
||||
Proxy []proxy.Proxy `yaml:"proxies,omitempty" json:"proxies"`
|
||||
}
|
||||
|
||||
// https://github.com/MetaCubeX/mihomo/blob/Meta/config/config.go RawConfig
|
||||
type Subscription struct {
|
||||
Port proxy.IntOrString `yaml:"port,omitempty" json:"port"`
|
||||
SocksPort proxy.IntOrString `yaml:"socks-port,omitempty" json:"socks-port"`
|
||||
RedirPort proxy.IntOrString `yaml:"redir-port,omitempty" json:"redir-port"`
|
||||
TProxyPort proxy.IntOrString `yaml:"tproxy-port,omitempty" json:"tproxy-port"`
|
||||
MixedPort proxy.IntOrString `yaml:"mixed-port,omitempty" json:"mixed-port"`
|
||||
ShadowSocksConfig string `yaml:"ss-config,omitempty" json:"ss-config"`
|
||||
VmessConfig string `yaml:"vmess-config,omitempty" json:"vmess-config"`
|
||||
InboundTfo bool `yaml:"inbound-tfo,omitempty" json:"inbound-tfo"`
|
||||
InboundMPTCP bool `yaml:"inbound-mptcp,omitempty" json:"inbound-mptcp"`
|
||||
Authentication []string `yaml:"authentication,omitempty" json:"authentication"`
|
||||
SkipAuthPrefixes []netip.Prefix `yaml:"skip-auth-prefixes,omitempty" json:"skip-auth-prefixes"`
|
||||
LanAllowedIPs []netip.Prefix `yaml:"lan-allowed-ips,omitempty" json:"lan-allowed-ips"`
|
||||
LanDisAllowedIPs []netip.Prefix `yaml:"lan-disallowed-ips,omitempty" json:"lan-disallowed-ips"`
|
||||
AllowLan bool `yaml:"allow-lan,omitempty" json:"allow-lan"`
|
||||
BindAddress string `yaml:"bind-address,omitempty" json:"bind-address"`
|
||||
Mode string `yaml:"mode,omitempty" json:"mode"`
|
||||
UnifiedDelay bool `yaml:"unified-delay,omitempty" json:"unified-delay"`
|
||||
LogLevel string `yaml:"log-level,omitempty" json:"log-level"`
|
||||
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
|
||||
ExternalController string `yaml:"external-controller,omitempty" json:"external-controller"`
|
||||
ExternalControllerPipe string `yaml:"external-controller-pipe,omitempty" json:"external-controller-pipe"`
|
||||
ExternalControllerUnix string `yaml:"external-controller-unix,omitempty" json:"external-controller-unix"`
|
||||
ExternalControllerTLS string `yaml:"external-controller-tls,omitempty" json:"external-controller-tls"`
|
||||
ExternalControllerCors C.RawCors `yaml:"external-controller-cors,omitempty" json:"external-controller-cors"`
|
||||
ExternalUI string `yaml:"external-ui,omitempty" json:"external-ui"`
|
||||
ExternalUIURL string `yaml:"external-ui-url,omitempty" json:"external-ui-url"`
|
||||
ExternalUIName string `yaml:"external-ui-name,omitempty" json:"external-ui-name"`
|
||||
ExternalDohServer string `yaml:"external-doh-server,omitempty" json:"external-doh-server"`
|
||||
Secret string `yaml:"secret,omitempty" json:"secret"`
|
||||
Interface string `yaml:"interface-name,omitempty" json:"interface-name"`
|
||||
RoutingMark int `yaml:"routing-mark,omitempty" json:"routing-mark"`
|
||||
Tunnels []LC.Tunnel `yaml:"tunnels,omitempty" json:"tunnels"`
|
||||
GeoAutoUpdate bool `yaml:"geo-auto-update,omitempty" json:"geo-auto-update"`
|
||||
GeoUpdateInterval int `yaml:"geo-update-interval,omitempty" json:"geo-update-interval"`
|
||||
GeodataMode bool `yaml:"geodata-mode,omitempty" json:"geodata-mode"`
|
||||
GeodataLoader string `yaml:"geodata-loader,omitempty" json:"geodata-loader"`
|
||||
GeositeMatcher string `yaml:"geosite-matcher,omitempty" json:"geosite-matcher"`
|
||||
TCPConcurrent bool `yaml:"tcp-concurrent,omitempty" json:"tcp-concurrent"`
|
||||
FindProcessMode string `yaml:"find-process-mode,omitempty" json:"find-process-mode"`
|
||||
GlobalClientFingerprint string `yaml:"global-client-fingerprint,omitempty" json:"global-client-fingerprint"`
|
||||
GlobalUA string `yaml:"global-ua,omitempty" json:"global-ua"`
|
||||
ETagSupport bool `yaml:"etag-support,omitempty" json:"etag-support"`
|
||||
KeepAliveIdle int `yaml:"keep-alive-idle,omitempty" json:"keep-alive-idle"`
|
||||
KeepAliveInterval int `yaml:"keep-alive-interval,omitempty" json:"keep-alive-interval"`
|
||||
DisableKeepAlive bool `yaml:"disable-keep-alive,omitempty" json:"disable-keep-alive"`
|
||||
|
||||
ProxyProvider map[string]map[string]any `yaml:"proxy-providers,omitempty" json:"proxy-providers"`
|
||||
RuleProvider map[string]RuleProvider `yaml:"rule-providers,omitempty" json:"rule-providers"`
|
||||
Proxy []proxy.Proxy `yaml:"proxies,omitempty" json:"proxies"`
|
||||
ProxyGroup []ProxyGroup `yaml:"proxy-groups,omitempty" json:"proxy-groups"`
|
||||
Rule []string `yaml:"rules,omitempty" json:"rule"`
|
||||
SubRules map[string][]string `yaml:"sub-rules,omitempty" json:"sub-rules"`
|
||||
Listeners []map[string]any `yaml:"listeners,omitempty" json:"listeners"`
|
||||
Hosts map[string]any `yaml:"hosts,omitempty" json:"hosts"`
|
||||
DNS C.RawDNS `yaml:"dns,omitempty" json:"dns"`
|
||||
NTP C.RawNTP `yaml:"ntp,omitempty" json:"ntp"`
|
||||
Tun C.RawTun `yaml:"tun,omitempty" json:"tun"`
|
||||
TuicServer C.RawTuicServer `yaml:"tuic-server,omitempty" json:"tuic-server"`
|
||||
IPTables C.RawIPTables `yaml:"iptables,omitempty" json:"iptables"`
|
||||
Experimental C.RawExperimental `yaml:"experimental,omitempty" json:"experimental"`
|
||||
Profile C.RawProfile `yaml:"profile,omitempty" json:"profile"`
|
||||
GeoXUrl C.RawGeoXUrl `yaml:"geox-url,omitempty" json:"geox-url"`
|
||||
Sniffer C.RawSniffer `yaml:"sniffer,omitempty" json:"sniffer"`
|
||||
TLS C.RawTLS `yaml:"tls,omitempty" json:"tls"`
|
||||
|
||||
ClashForAndroid C.RawClashForAndroid `yaml:"clash-for-android,omitempty" json:"clash-for-android"`
|
||||
}
|
||||
83
parser/anytls.go
Normal file
83
parser/anytls.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
type AnytlsParser struct{}
|
||||
|
||||
func (p *AnytlsParser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) GetPrefixes() []string {
|
||||
return []string{"anytls://"}
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) GetType() string {
|
||||
return "anytls"
|
||||
}
|
||||
|
||||
func (p *AnytlsParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
|
||||
username := link.User.Username()
|
||||
password, exist := link.User.Password()
|
||||
if !exist {
|
||||
password = username
|
||||
}
|
||||
|
||||
query := link.Query()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
insecure, sni := query.Get("insecure"), query.Get("sni")
|
||||
insecureBool := insecure == "1"
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Anytls: P.Anytls{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
Password: password,
|
||||
SNI: sni,
|
||||
SkipCertVerify: insecureBool,
|
||||
UDP: config.UseUDP,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&AnytlsParser{})
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DecodeBase64(s string) (string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s)%4 != 0 {
|
||||
s += strings.Repeat("=", 4-len(s)%4)
|
||||
}
|
||||
decodeStr, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decodeStr), nil
|
||||
}
|
||||
82
parser/common.go
Normal file
82
parser/common.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
)
|
||||
|
||||
func hasPrefix(proxy string, prefixes []string) bool {
|
||||
hasPrefix := false
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
hasPrefix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return hasPrefix
|
||||
}
|
||||
|
||||
func ParsePort(portStr string) (int, error) {
|
||||
port, err := strconv.Atoi(portStr)
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return 0, errors.New("invaild port range")
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// isLikelyBase64 不严格判断是否是合法的 Base64, 很多分享链接不符合 Base64 规范
|
||||
func isLikelyBase64(s string) bool {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.Contains(strings.TrimSuffix(s, "="), "=") {
|
||||
s = strings.TrimSuffix(s, "=")
|
||||
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
for _, c := range s {
|
||||
if !strings.ContainsRune(chars, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decoded, err := utils.DecodeBase64(s, true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !utf8.ValidString(decoded) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type ParseConfig struct {
|
||||
UseUDP bool
|
||||
}
|
||||
|
||||
func ParseProxies(config ParseConfig, proxies ...string) ([]P.Proxy, error) {
|
||||
var result []P.Proxy
|
||||
for _, proxy := range proxies {
|
||||
if proxy != "" {
|
||||
var proxyItem P.Proxy
|
||||
var err error
|
||||
|
||||
proxyItem, err = ParseProxyWithRegistry(config, proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, proxyItem)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
15
parser/errors.go
Normal file
15
parser/errors.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package parser
|
||||
|
||||
type ParseErrorType string
|
||||
|
||||
const (
|
||||
ErrInvalidPrefix ParseErrorType = "invalid url prefix"
|
||||
ErrInvalidStruct ParseErrorType = "invalid struct"
|
||||
ErrInvalidPort ParseErrorType = "invalid port number"
|
||||
ErrCannotParseParams ParseErrorType = "cannot parse query parameters"
|
||||
ErrInvalidBase64 ParseErrorType = "invalid base64"
|
||||
)
|
||||
|
||||
func (e ParseErrorType) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
@@ -1,79 +1,95 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
//hysteria://host:port?protocol=udp&auth=123456&peer=sni.domain&insecure=1&upmbps=100&downmbps=100&alpn=hysteria&obfs=xplus&obfsParam=123456#remarks
|
||||
//
|
||||
//- host: hostname or IP address of the server to connect to (required)
|
||||
//- port: port of the server to connect to (required)
|
||||
//- protocol: protocol to use ("udp", "wechat-video", "faketcp") (optional, default: "udp")
|
||||
//- auth: authentication payload (string) (optional)
|
||||
//- peer: SNI for TLS (optional)
|
||||
//- insecure: ignore certificate errors (optional)
|
||||
//- upmbps: upstream bandwidth in Mbps (required)
|
||||
//- downmbps: downstream bandwidth in Mbps (required)
|
||||
//- alpn: QUIC ALPN (optional)
|
||||
//- obfs: Obfuscation mode (optional, empty or "xplus")
|
||||
//- obfsParam: Obfuscation password (optional)
|
||||
//- remarks: remarks (optional)
|
||||
type HysteriaParser struct{}
|
||||
|
||||
func ParseHysteria(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 hysteria:// 开头
|
||||
if !strings.HasPrefix(proxy, "hysteria://") {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
func (p *HysteriaParser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) GetPrefixes() []string {
|
||||
return []string{"hysteria://"}
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) GetType() string {
|
||||
return "hysteria"
|
||||
}
|
||||
|
||||
func (p *HysteriaParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
// 分割
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria://"), "?", 2)
|
||||
serverInfo := strings.SplitN(parts[0], ":", 2)
|
||||
if len(serverInfo) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
}
|
||||
params, err := url.ParseQuery(parts[1])
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
host := serverInfo[0]
|
||||
port, err := strconv.Atoi(serverInfo[1])
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
|
||||
}
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria Url")
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
protocol := params.Get("protocol")
|
||||
auth := params.Get("auth")
|
||||
peer := params.Get("peer")
|
||||
insecure := params.Get("insecure")
|
||||
upmbps := params.Get("upmbps")
|
||||
downmbps := params.Get("downmbps")
|
||||
alpn := params.Get("alpn")
|
||||
obfs := params.Get("obfs")
|
||||
obfsParam := params.Get("obfsParam")
|
||||
remarks := ""
|
||||
if strings.Contains(parts[1], "#") {
|
||||
r := strings.Split(parts[1], "#")
|
||||
remarks = r[len(r)-1]
|
||||
} else {
|
||||
remarks = serverInfo[0] + ":" + serverInfo[1]
|
||||
|
||||
query := link.Query()
|
||||
|
||||
protocol, auth, auth_str, insecure, upmbps, downmbps, obfs, alpnStr := query.Get("protocol"), query.Get("auth"), query.Get("auth-str"), query.Get("insecure"), query.Get("upmbps"), query.Get("downmbps"), query.Get("obfs"), query.Get("alpn")
|
||||
insecureBool, err := strconv.ParseBool(insecure)
|
||||
if err != nil {
|
||||
insecureBool = false
|
||||
}
|
||||
// 返回结果
|
||||
result := model.Proxy{
|
||||
Type: "hysteria",
|
||||
Name: remarks,
|
||||
Server: host,
|
||||
Port: port,
|
||||
Up: upmbps,
|
||||
Down: downmbps,
|
||||
Auth: auth,
|
||||
Obfs: obfs,
|
||||
Sni: peer,
|
||||
SkipCertVerify: insecure == "1",
|
||||
Alpn: strings.Split(alpn, ","),
|
||||
ObfsParam: obfsParam,
|
||||
Protocol: protocol,
|
||||
|
||||
var alpn []string
|
||||
alpnStr = strings.TrimSpace(alpnStr)
|
||||
if alpnStr != "" {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
}
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Hysteria: P.Hysteria{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
Up: upmbps,
|
||||
Down: downmbps,
|
||||
Auth: auth,
|
||||
AuthString: auth_str,
|
||||
Obfs: obfs,
|
||||
SkipCertVerify: insecureBool,
|
||||
ALPN: alpn,
|
||||
Protocol: protocol,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&HysteriaParser{})
|
||||
}
|
||||
|
||||
@@ -1,50 +1,84 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
// hysteria2://letmein@example.com/?insecure=1&obfs=salamander&obfs-password=gawrgura&pinSHA256=deadbeef&sni=real.example.com
|
||||
type Hysteria2Parser struct{}
|
||||
|
||||
func ParseHysteria2(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 hysteria2:// 开头
|
||||
if !strings.HasPrefix(proxy, "hysteria2://") && !strings.HasPrefix(proxy, "hy2://") {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
func (p *Hysteria2Parser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) GetPrefixes() []string {
|
||||
return []string{"hysteria2://", "hy2://"}
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) GetType() string {
|
||||
return "hysteria2"
|
||||
}
|
||||
|
||||
func (p *Hysteria2Parser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
// 分割
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2)
|
||||
// 分割
|
||||
serverInfo := strings.SplitN(parts[1], "/?", 2)
|
||||
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
|
||||
if len(serverAndPort) == 1 {
|
||||
serverAndPort = append(serverAndPort, "443")
|
||||
} else if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
}
|
||||
params, err := url.ParseQuery(serverInfo[1])
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
// 获取端口
|
||||
port, err := strconv.Atoi(serverAndPort[1])
|
||||
|
||||
username := link.User.Username()
|
||||
password, exist := link.User.Password()
|
||||
if !exist {
|
||||
password = username
|
||||
}
|
||||
|
||||
query := link.Query()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
// 返回结果
|
||||
result := model.Proxy{
|
||||
Type: "hysteria2",
|
||||
Name: params.Get("name"),
|
||||
Server: serverAndPort[0],
|
||||
Port: port,
|
||||
Password: parts[0],
|
||||
Obfs: params.Get("obfs"),
|
||||
ObfsParam: params.Get("obfs-password"),
|
||||
Sni: params.Get("sni"),
|
||||
SkipCertVerify: params.Get("insecure") == "1",
|
||||
obfs, obfsPassword, insecure, sni := query.Get("obfs"), query.Get("obfs-password"), query.Get("insecure"), query.Get("sni")
|
||||
insecureBool := insecure == "1"
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Hysteria2: P.Hysteria2{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
Password: password,
|
||||
Obfs: obfs,
|
||||
ObfsPassword: obfsPassword,
|
||||
SNI: sni,
|
||||
SkipCertVerify: insecureBool,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&Hysteria2Parser{})
|
||||
}
|
||||
|
||||
80
parser/registry.go
Normal file
80
parser/registry.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
type ProxyParser interface {
|
||||
Parse(config ParseConfig, proxy string) (P.Proxy, error)
|
||||
GetPrefixes() []string
|
||||
GetType() string
|
||||
SupportClash() bool
|
||||
SupportMeta() bool
|
||||
}
|
||||
|
||||
type parserRegistry struct {
|
||||
mu sync.RWMutex
|
||||
parsers map[string]ProxyParser
|
||||
}
|
||||
|
||||
var registry = &parserRegistry{
|
||||
parsers: make(map[string]ProxyParser),
|
||||
}
|
||||
|
||||
func RegisterParser(parser ProxyParser) {
|
||||
registry.mu.Lock()
|
||||
defer registry.mu.Unlock()
|
||||
|
||||
for _, prefix := range parser.GetPrefixes() {
|
||||
registry.parsers[prefix] = parser
|
||||
}
|
||||
}
|
||||
|
||||
func GetParser(prefix string) (ProxyParser, bool) {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
parser, exists := registry.parsers[prefix]
|
||||
return parser, exists
|
||||
}
|
||||
|
||||
func GetAllParsers() map[string]ProxyParser {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
result := make(map[string]ProxyParser)
|
||||
for k, v := range registry.parsers {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GetAllPrefixes() []string {
|
||||
registry.mu.RLock()
|
||||
defer registry.mu.RUnlock()
|
||||
|
||||
prefixes := make([]string, 0, len(registry.parsers))
|
||||
for prefix := range registry.parsers {
|
||||
prefixes = append(prefixes, prefix)
|
||||
}
|
||||
return prefixes
|
||||
}
|
||||
|
||||
func ParseProxyWithRegistry(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
proxy = strings.TrimSpace(proxy)
|
||||
if proxy == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "empty proxy string")
|
||||
}
|
||||
|
||||
for prefix, parser := range registry.parsers {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
return parser.Parse(config, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, "unsupported protocol")
|
||||
}
|
||||
121
parser/shadowsocks.go
Normal file
121
parser/shadowsocks.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
)
|
||||
|
||||
// ShadowsocksParser Shadowsocks协议解析器
|
||||
type ShadowsocksParser struct{}
|
||||
|
||||
func (p *ShadowsocksParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ShadowsocksParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPrefixes 返回支持的协议前缀
|
||||
func (p *ShadowsocksParser) GetPrefixes() []string {
|
||||
return []string{"ss://"}
|
||||
}
|
||||
|
||||
// GetType 返回协议类型
|
||||
func (p *ShadowsocksParser) GetType() string {
|
||||
return "ss"
|
||||
}
|
||||
|
||||
// Parse 解析Shadowsocks代理
|
||||
func (p *ShadowsocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
|
||||
if !strings.Contains(proxy, "@") {
|
||||
s := strings.SplitN(proxy, "#", 2)
|
||||
for _, prefix := range p.GetPrefixes() {
|
||||
if strings.HasPrefix(s[0], prefix) {
|
||||
s[0] = strings.TrimPrefix(s[0], prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
d, err := utils.DecodeBase64(s[0], true)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
if len(s) == 2 {
|
||||
proxy = "ss://" + d + "#" + s[1]
|
||||
} else {
|
||||
proxy = "ss://" + d
|
||||
}
|
||||
}
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
|
||||
method := link.User.Username()
|
||||
password, hasPassword := link.User.Password()
|
||||
|
||||
if !hasPassword && isLikelyBase64(method) {
|
||||
decodedStr, err := utils.DecodeBase64(method, true)
|
||||
if err == nil {
|
||||
methodAndPass := strings.SplitN(decodedStr, ":", 2)
|
||||
if len(methodAndPass) == 2 {
|
||||
method = methodAndPass[0]
|
||||
password = methodAndPass[1]
|
||||
} else {
|
||||
method = decodedStr
|
||||
}
|
||||
}
|
||||
}
|
||||
if password != "" && isLikelyBase64(password) {
|
||||
password, err = utils.DecodeBase64(password, true)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
ShadowSocks: P.ShadowSocks{
|
||||
Cipher: method,
|
||||
Password: password,
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
UDP: config.UseUDP,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 注册解析器
|
||||
func init() {
|
||||
RegisterParser(&ShadowsocksParser{})
|
||||
}
|
||||
135
parser/shadowsocksr.go
Normal file
135
parser/shadowsocksr.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
)
|
||||
|
||||
type ShadowsocksRParser struct{}
|
||||
|
||||
func (p *ShadowsocksRParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) GetPrefixes() []string {
|
||||
return []string{"ssr://"}
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) GetType() string {
|
||||
return "ssr"
|
||||
}
|
||||
|
||||
func (p *ShadowsocksRParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
|
||||
for _, prefix := range p.GetPrefixes() {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
proxy = strings.TrimPrefix(proxy, prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
proxy, err := utils.DecodeBase64(proxy, true)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error())
|
||||
}
|
||||
serverInfoAndParams := strings.SplitN(proxy, "/?", 2)
|
||||
if len(serverInfoAndParams) != 2 {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, proxy)
|
||||
}
|
||||
parts := SplitNRight(serverInfoAndParams[0], ":", 6)
|
||||
if len(parts) < 6 {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, proxy)
|
||||
}
|
||||
server := parts[0]
|
||||
protocol := parts[2]
|
||||
method := parts[3]
|
||||
obfs := parts[4]
|
||||
password, err := utils.DecodeBase64(parts[5], true)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
port, err := ParsePort(parts[1])
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
|
||||
var obfsParam string
|
||||
var protoParam string
|
||||
var remarks string
|
||||
if len(serverInfoAndParams) == 2 {
|
||||
params, err := url.ParseQuery(serverInfoAndParams[1])
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrCannotParseParams, err.Error())
|
||||
}
|
||||
if params.Get("obfsparam") != "" {
|
||||
obfsParam, err = utils.DecodeBase64(params.Get("obfsparam"), true)
|
||||
}
|
||||
if params.Get("protoparam") != "" {
|
||||
protoParam, err = utils.DecodeBase64(params.Get("protoparam"), true)
|
||||
}
|
||||
if params.Get("remarks") != "" {
|
||||
remarks, err = utils.DecodeBase64(params.Get("remarks"), true)
|
||||
} else {
|
||||
remarks = server + ":" + strconv.Itoa(port)
|
||||
}
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
result := P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
ShadowSocksR: P.ShadowSocksR{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
Protocol: protocol,
|
||||
Cipher: method,
|
||||
Obfs: obfs,
|
||||
Password: password,
|
||||
ObfsParam: obfsParam,
|
||||
ProtocolParam: protoParam,
|
||||
UDP: config.UseUDP,
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&ShadowsocksRParser{})
|
||||
}
|
||||
|
||||
func SplitNRight(s, sep string, n int) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
if n == 1 {
|
||||
return []string{s}
|
||||
}
|
||||
parts := strings.Split(s, sep)
|
||||
if len(parts) <= n {
|
||||
return parts
|
||||
}
|
||||
result := make([]string, n)
|
||||
for i, j := len(parts)-1, 0; i >= 0; i, j = i-1, j+1 {
|
||||
if j < n-1 {
|
||||
result[n-j-1] = parts[len(parts)-j-1]
|
||||
} else {
|
||||
result[0] = strings.Join(parts[:i+1], sep)
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
93
parser/socks.go
Normal file
93
parser/socks.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
)
|
||||
|
||||
type SocksParser struct{}
|
||||
|
||||
func (p *SocksParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
func (p *SocksParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *SocksParser) GetPrefixes() []string {
|
||||
return []string{"socks://", "socks5://"}
|
||||
}
|
||||
|
||||
func (p *SocksParser) GetType() string {
|
||||
return "socks5"
|
||||
}
|
||||
|
||||
func (p *SocksParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
|
||||
}
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
var username, password string
|
||||
|
||||
username = link.User.Username()
|
||||
password, hasPassword := link.User.Password()
|
||||
|
||||
if !hasPassword && isLikelyBase64(username) {
|
||||
decodedStr, err := utils.DecodeBase64(username, true)
|
||||
if err == nil {
|
||||
usernameAndPassword := strings.SplitN(decodedStr, ":", 2)
|
||||
if len(usernameAndPassword) == 2 {
|
||||
username = usernameAndPassword[0]
|
||||
password = usernameAndPassword[1]
|
||||
} else {
|
||||
username = decodedStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tls, udp := link.Query().Get("tls"), link.Query().Get("udp")
|
||||
|
||||
return P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Socks: P.Socks{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
UserName: username,
|
||||
Password: password,
|
||||
TLS: tls == "true",
|
||||
UDP: udp == "true",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&SocksParser{})
|
||||
}
|
||||
67
parser/ss.go
67
parser/ss.go
@@ -1,67 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
)
|
||||
|
||||
// ParseSS 解析 SS(Shadowsocks)Url
|
||||
func ParseSS(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 ss:// 开头
|
||||
if !strings.HasPrefix(proxy, "ss://") {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
// 分割
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
if !strings.Contains(parts[0], ":") {
|
||||
// 解码
|
||||
decoded, err := DecodeBase64(parts[0])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
|
||||
}
|
||||
parts[0] = decoded
|
||||
}
|
||||
credentials := strings.SplitN(parts[0], ":", 2)
|
||||
if len(credentials) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
// 分割
|
||||
serverInfo := strings.SplitN(parts[1], "#", 2)
|
||||
serverAndPort := strings.SplitN(serverInfo[0], ":", 2)
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, errors.New("invalid ss Url")
|
||||
}
|
||||
// 转换端口字符串为数字
|
||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
|
||||
}
|
||||
// 返回结果
|
||||
result := model.Proxy{
|
||||
Type: "ss",
|
||||
Cipher: strings.TrimSpace(credentials[0]),
|
||||
Password: strings.TrimSpace(credentials[1]),
|
||||
Server: strings.TrimSpace(serverAndPort[0]),
|
||||
Port: port,
|
||||
UDP: true,
|
||||
Name: serverAndPort[0],
|
||||
}
|
||||
// 如果有节点名称
|
||||
if len(serverInfo) == 2 {
|
||||
unescape, err := url.QueryUnescape(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid ss Url" + err.Error())
|
||||
}
|
||||
result.Name = strings.TrimSpace(unescape)
|
||||
} else {
|
||||
result.Name = strings.TrimSpace(serverAndPort[0])
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
)
|
||||
|
||||
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 ssr:// 开头
|
||||
if !strings.HasPrefix(proxy, "ssr://") {
|
||||
return model.Proxy{}, fmt.Errorf("invalid ssr Url")
|
||||
}
|
||||
var err error
|
||||
proxy = strings.TrimPrefix(proxy, "ssr://")
|
||||
if !strings.Contains(proxy, ":") {
|
||||
proxy, err = DecodeBase64(strings.TrimPrefix(proxy, "ssr://"))
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
}
|
||||
// 分割
|
||||
detailsAndParams := strings.SplitN(proxy, "/?", 2)
|
||||
parts := strings.Split(detailsAndParams[0], ":")
|
||||
params, err := url.ParseQuery(detailsAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
// 处理端口
|
||||
port, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
var obfsParam string
|
||||
var protoParam string
|
||||
var remarks string
|
||||
if params.Get("obfsparam") != "" {
|
||||
obfsParam, err = DecodeBase64(params.Get("obfsparam"))
|
||||
}
|
||||
if params.Get("protoparam") != "" {
|
||||
protoParam, err = DecodeBase64(params.Get("protoparam"))
|
||||
}
|
||||
if params.Get("remarks") != "" {
|
||||
remarks, err = DecodeBase64(params.Get("remarks"))
|
||||
}
|
||||
|
||||
result := model.Proxy{
|
||||
Name: remarks,
|
||||
Type: "ssr",
|
||||
Server: parts[0],
|
||||
Port: port,
|
||||
Protocol: parts[2],
|
||||
Cipher: parts[3],
|
||||
Obfs: parts[4],
|
||||
Password: parts[5],
|
||||
ObfsParam: obfsParam,
|
||||
ProtocolParam: protoParam,
|
||||
}
|
||||
|
||||
if result.Name == "" {
|
||||
result.Name = result.Server
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
158
parser/trojan.go
158
parser/trojan.go
@@ -3,51 +3,121 @@ package parser
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 trojan:// 开头
|
||||
if !strings.HasPrefix(proxy, "trojan://") {
|
||||
return model.Proxy{}, fmt.Errorf("invalid trojan Url")
|
||||
}
|
||||
// 分割
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return model.Proxy{}, fmt.Errorf("invalid trojan Url")
|
||||
}
|
||||
// 分割
|
||||
serverInfo := strings.SplitN(parts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, fmt.Errorf("invalid trojan")
|
||||
}
|
||||
// 处理端口
|
||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
// 返回结果
|
||||
result := model.Proxy{
|
||||
Type: "trojan",
|
||||
Server: strings.TrimSpace(serverAndPort[0]),
|
||||
Port: port,
|
||||
UDP: true,
|
||||
Password: strings.TrimSpace(parts[0]),
|
||||
Sni: params.Get("sni"),
|
||||
}
|
||||
// 如果有节点名称
|
||||
if len(serverInfo) == 2 {
|
||||
result.Name, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
|
||||
} else {
|
||||
result.Name = serverAndPort[0]
|
||||
}
|
||||
return result, nil
|
||||
type TrojanParser struct{}
|
||||
|
||||
func (p *TrojanParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *TrojanParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *TrojanParser) GetPrefixes() []string {
|
||||
return []string{"trojan://"}
|
||||
}
|
||||
|
||||
func (p *TrojanParser) GetType() string {
|
||||
return "trojan"
|
||||
}
|
||||
|
||||
func (p *TrojanParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
|
||||
password := link.User.Username()
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
portStr := link.Port()
|
||||
if portStr == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server port")
|
||||
}
|
||||
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
query := link.Query()
|
||||
network, security, alpnStr, sni, pbk, sid, fp, path, host, serviceName, udp, insecure := query.Get("type"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("pbk"), query.Get("sid"), query.Get("fp"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("udp"), query.Get("allowInsecure")
|
||||
|
||||
insecureBool := insecure == "1"
|
||||
result := P.Trojan{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
Password: password,
|
||||
Network: network,
|
||||
UDP: udp == "true",
|
||||
SkipCertVerify: insecureBool,
|
||||
}
|
||||
|
||||
var alpn []string
|
||||
if strings.Contains(alpnStr, ",") {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
result.ALPN = alpn
|
||||
}
|
||||
|
||||
if fp != "" {
|
||||
result.ClientFingerprint = fp
|
||||
}
|
||||
|
||||
if sni != "" {
|
||||
result.SNI = sni
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
result.RealityOpts = P.RealityOptions{
|
||||
PublicKey: pbk,
|
||||
ShortID: sid,
|
||||
}
|
||||
}
|
||||
|
||||
if network == "ws" {
|
||||
result.Network = "ws"
|
||||
result.WSOpts = P.WSOptions{
|
||||
Path: path,
|
||||
Headers: map[string]string{
|
||||
"Host": host,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if network == "grpc" {
|
||||
result.GrpcOpts = P.GrpcOptions{
|
||||
GrpcServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
return P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Trojan: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&TrojanParser{})
|
||||
}
|
||||
|
||||
212
parser/vless.go
212
parser/vless.go
@@ -3,85 +3,141 @@ package parser
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
)
|
||||
|
||||
func ParseVless(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 vless:// 开头
|
||||
if !strings.HasPrefix(proxy, "vless://") {
|
||||
return model.Proxy{}, fmt.Errorf("invalid vless Url")
|
||||
}
|
||||
// 分割
|
||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2)
|
||||
if len(parts) != 2 {
|
||||
return model.Proxy{}, fmt.Errorf("invalid vless Url")
|
||||
}
|
||||
// 分割
|
||||
serverInfo := strings.SplitN(parts[1], "#", 2)
|
||||
serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2)
|
||||
serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2)
|
||||
params, err := url.ParseQuery(serverAndPortAndParams[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
if len(serverAndPort) != 2 {
|
||||
return model.Proxy{}, fmt.Errorf("invalid vless")
|
||||
}
|
||||
// 处理端口
|
||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
// 返回结果
|
||||
result := model.Proxy{
|
||||
Type: "vless",
|
||||
Server: strings.TrimSpace(serverAndPort[0]),
|
||||
Port: port,
|
||||
UUID: strings.TrimSpace(parts[0]),
|
||||
UDP: true,
|
||||
Sni: params.Get("sni"),
|
||||
Network: params.Get("type"),
|
||||
TLS: params.Get("security") == "reality",
|
||||
Flow: params.Get("flow"),
|
||||
ClientFingerprint: params.Get("fp"),
|
||||
Servername: params.Get("sni"),
|
||||
RealityOpts: model.RealityOptions{
|
||||
PublicKey: params.Get("pbk"),
|
||||
},
|
||||
}
|
||||
if params.Get("alpn") != "" {
|
||||
result.Alpn = strings.Split(params.Get("alpn"), ",")
|
||||
}
|
||||
if params.Get("type") == "ws" {
|
||||
result.WSOpts = model.WSOptions{
|
||||
Path: params.Get("path"),
|
||||
Headers: map[string]string{
|
||||
"Host": params.Get("host"),
|
||||
},
|
||||
}
|
||||
}
|
||||
if params.Get("type") == "grpc" {
|
||||
result.GrpcOpts = model.GrpcOptions{
|
||||
GrpcServiceName: params.Get("serviceName"),
|
||||
}
|
||||
}
|
||||
// 如果有节点名称
|
||||
if len(serverInfo) == 2 {
|
||||
if strings.Contains(serverInfo[1], "|") {
|
||||
result.Name = strings.SplitN(serverInfo[1], "|", 2)[1]
|
||||
} else {
|
||||
result.Name, err = url.QueryUnescape(serverInfo[1])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.Name, err = url.QueryUnescape(serverAndPort[0])
|
||||
if err != nil {
|
||||
return model.Proxy{}, err
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
type VlessParser struct{}
|
||||
|
||||
func (p *VlessParser) SupportClash() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *VlessParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *VlessParser) GetPrefixes() []string {
|
||||
return []string{"vless://"}
|
||||
}
|
||||
|
||||
func (p *VlessParser) GetType() string {
|
||||
return "vless"
|
||||
}
|
||||
|
||||
func (p *VlessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
|
||||
link, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
|
||||
server := link.Hostname()
|
||||
if server == "" {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, "missing server host")
|
||||
}
|
||||
portStr := link.Port()
|
||||
port, err := ParsePort(portStr)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
|
||||
query := link.Query()
|
||||
uuid := link.User.Username()
|
||||
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type, udp := query.Get("flow"), query.Get("security"), query.Get("alpn"), query.Get("sni"), query.Get("allowInsecure"), query.Get("fp"), query.Get("pbk"), query.Get("sid"), query.Get("path"), query.Get("host"), query.Get("serviceName"), query.Get("type"), query.Get("udp")
|
||||
|
||||
insecureBool := insecure == "1"
|
||||
var alpn []string
|
||||
if strings.Contains(alpnStr, ",") {
|
||||
alpn = strings.Split(alpnStr, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
|
||||
remarks := link.Fragment
|
||||
if remarks == "" {
|
||||
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||
}
|
||||
remarks = strings.TrimSpace(remarks)
|
||||
|
||||
result := P.Vless{
|
||||
Server: server,
|
||||
Port: P.IntOrString(port),
|
||||
UUID: uuid,
|
||||
Flow: flow,
|
||||
UDP: udp == "true",
|
||||
SkipCertVerify: insecureBool,
|
||||
}
|
||||
|
||||
if len(alpn) > 0 {
|
||||
result.ALPN = alpn
|
||||
}
|
||||
|
||||
if fp != "" {
|
||||
result.ClientFingerprint = fp
|
||||
}
|
||||
|
||||
if sni != "" {
|
||||
result.ServerName = sni
|
||||
}
|
||||
|
||||
if security == "tls" {
|
||||
result.TLS = true
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
result.TLS = true
|
||||
result.RealityOpts = P.RealityOptions{
|
||||
PublicKey: pbk,
|
||||
ShortID: sid,
|
||||
}
|
||||
}
|
||||
|
||||
if _type == "ws" {
|
||||
result.Network = "ws"
|
||||
result.WSOpts = P.WSOptions{
|
||||
Path: path,
|
||||
}
|
||||
if host != "" {
|
||||
result.WSOpts.Headers = make(map[string]string)
|
||||
result.WSOpts.Headers["Host"] = host
|
||||
}
|
||||
}
|
||||
|
||||
if _type == "grpc" {
|
||||
result.Network = "grpc"
|
||||
result.GrpcOpts = P.GrpcOptions{
|
||||
GrpcServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
if _type == "http" {
|
||||
result.HTTPOpts = P.HTTPOptions{}
|
||||
result.HTTPOpts.Headers = map[string][]string{}
|
||||
|
||||
result.HTTPOpts.Path = strings.Split(path, ",")
|
||||
|
||||
hosts, err := url.QueryUnescape(host)
|
||||
if err != nil {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrCannotParseParams, err.Error())
|
||||
}
|
||||
result.Network = "http"
|
||||
if hosts != "" {
|
||||
result.HTTPOpts.Headers["host"] = strings.Split(host, ",")
|
||||
}
|
||||
}
|
||||
|
||||
return P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: remarks,
|
||||
Vless: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&VlessParser{})
|
||||
}
|
||||
|
||||
171
parser/vmess.go
171
parser/vmess.go
@@ -2,89 +2,174 @@ package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sub2clash/model"
|
||||
|
||||
P "github.com/bestnite/sub2clash/model/proxy"
|
||||
"github.com/bestnite/sub2clash/utils"
|
||||
)
|
||||
|
||||
func ParseVmess(proxy string) (model.Proxy, error) {
|
||||
// 判断是否以 vmess:// 开头
|
||||
if !strings.HasPrefix(proxy, "vmess://") {
|
||||
return model.Proxy{}, errors.New("invalid vmess url")
|
||||
type VmessJson struct {
|
||||
V any `json:"v"`
|
||||
Ps string `json:"ps"`
|
||||
Add string `json:"add"`
|
||||
Port any `json:"port"`
|
||||
Id string `json:"id"`
|
||||
Aid any `json:"aid"`
|
||||
Scy string `json:"scy"`
|
||||
Net string `json:"net"`
|
||||
Type string `json:"type"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Tls string `json:"tls"`
|
||||
Sni string `json:"sni"`
|
||||
Alpn string `json:"alpn"`
|
||||
Fp string `json:"fp"`
|
||||
}
|
||||
|
||||
type VmessParser struct{}
|
||||
|
||||
func (p *VmessParser) SupportClash() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *VmessParser) SupportMeta() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *VmessParser) GetPrefixes() []string {
|
||||
return []string{"vmess://"}
|
||||
}
|
||||
|
||||
func (p *VmessParser) GetType() string {
|
||||
return "vmess"
|
||||
}
|
||||
|
||||
func (p *VmessParser) Parse(config ParseConfig, proxy string) (P.Proxy, error) {
|
||||
if !hasPrefix(proxy, p.GetPrefixes()) {
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPrefix, proxy)
|
||||
}
|
||||
// 解码
|
||||
base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://"))
|
||||
|
||||
for _, prefix := range p.GetPrefixes() {
|
||||
if strings.HasPrefix(proxy, prefix) {
|
||||
proxy = strings.TrimPrefix(proxy, prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
base64, err := utils.DecodeBase64(proxy, true)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidBase64, err.Error())
|
||||
}
|
||||
// 解析
|
||||
var vmess model.VmessJson
|
||||
|
||||
var vmess VmessJson
|
||||
err = json.Unmarshal([]byte(base64), &vmess)
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
// 解析端口
|
||||
port := 0
|
||||
|
||||
var port int
|
||||
switch vmess.Port.(type) {
|
||||
case string:
|
||||
port, err = strconv.Atoi(vmess.Port.(string))
|
||||
port, err = ParsePort(vmess.Port.(string))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidPort, err.Error())
|
||||
}
|
||||
case float64:
|
||||
port = int(vmess.Port.(float64))
|
||||
}
|
||||
// 解析Aid
|
||||
|
||||
aid := 0
|
||||
switch vmess.Aid.(type) {
|
||||
case string:
|
||||
aid, err = strconv.Atoi(vmess.Aid.(string))
|
||||
if err != nil {
|
||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
||||
return P.Proxy{}, fmt.Errorf("%w: %s", ErrInvalidStruct, err.Error())
|
||||
}
|
||||
case float64:
|
||||
aid = int(vmess.Aid.(float64))
|
||||
}
|
||||
// 设置默认值
|
||||
|
||||
if vmess.Scy == "" {
|
||||
vmess.Scy = "auto"
|
||||
}
|
||||
if vmess.Net == "ws" && vmess.Path == "" {
|
||||
vmess.Path = "/"
|
||||
}
|
||||
if vmess.Net == "ws" && vmess.Host == "" {
|
||||
vmess.Host = vmess.Add
|
||||
}
|
||||
|
||||
name, err := url.QueryUnescape(vmess.Ps)
|
||||
if err != nil {
|
||||
name = vmess.Ps
|
||||
}
|
||||
// 返回结果
|
||||
result := model.Proxy{
|
||||
Name: name,
|
||||
Type: "vmess",
|
||||
Server: vmess.Add,
|
||||
Port: port,
|
||||
UUID: vmess.Id,
|
||||
AlterID: aid,
|
||||
Cipher: vmess.Scy,
|
||||
UDP: true,
|
||||
TLS: vmess.Tls == "tls",
|
||||
Fingerprint: vmess.Fp,
|
||||
ClientFingerprint: "chrome",
|
||||
SkipCertVerify: true,
|
||||
Servername: vmess.Add,
|
||||
Network: vmess.Net,
|
||||
|
||||
var alpn []string
|
||||
if strings.Contains(vmess.Alpn, ",") {
|
||||
alpn = strings.Split(vmess.Alpn, ",")
|
||||
} else {
|
||||
alpn = nil
|
||||
}
|
||||
|
||||
result := P.Vmess{
|
||||
Server: vmess.Add,
|
||||
Port: P.IntOrString(port),
|
||||
UUID: vmess.Id,
|
||||
AlterID: P.IntOrString(aid),
|
||||
Cipher: vmess.Scy,
|
||||
UDP: config.UseUDP,
|
||||
}
|
||||
|
||||
if len(alpn) > 0 {
|
||||
result.ALPN = alpn
|
||||
}
|
||||
|
||||
if vmess.Fp != "" {
|
||||
result.ClientFingerprint = vmess.Fp
|
||||
}
|
||||
|
||||
if vmess.Sni != "" {
|
||||
result.ServerName = vmess.Sni
|
||||
}
|
||||
|
||||
if vmess.Tls == "tls" {
|
||||
result.TLS = true
|
||||
}
|
||||
|
||||
if vmess.Net == "ws" {
|
||||
result.WSOpts = model.WSOptions{
|
||||
if vmess.Path == "" {
|
||||
vmess.Path = "/"
|
||||
}
|
||||
if vmess.Host == "" {
|
||||
vmess.Host = vmess.Add
|
||||
}
|
||||
result.Network = "ws"
|
||||
result.WSOpts = P.WSOptions{
|
||||
Path: vmess.Path,
|
||||
Headers: map[string]string{
|
||||
"Host": vmess.Host,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
||||
if vmess.Net == "grpc" {
|
||||
result.GrpcOpts = P.GrpcOptions{
|
||||
GrpcServiceName: vmess.Path,
|
||||
}
|
||||
result.Network = "grpc"
|
||||
}
|
||||
|
||||
if vmess.Net == "h2" {
|
||||
result.HTTP2Opts = P.HTTP2Options{
|
||||
Host: strings.Split(vmess.Host, ","),
|
||||
Path: vmess.Path,
|
||||
}
|
||||
result.Network = "h2"
|
||||
}
|
||||
|
||||
return P.Proxy{
|
||||
Type: p.GetType(),
|
||||
Name: name,
|
||||
Vmess: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterParser(&VmessParser{})
|
||||
}
|
||||
|
||||
24
server/frontend/.gitignore
vendored
Normal file
24
server/frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
17
server/frontend/index.html
Normal file
17
server/frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>sub2clash</title>
|
||||
<link rel="stylesheet" href="./src/index.css" />
|
||||
<script type="module" src="/src/app.ts"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<sub2clash-app>
|
||||
</sub2clash-app>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
2030
server/frontend/package-lock.json
generated
Normal file
2030
server/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
server/frontend/package.json
Normal file
22
server/frontend/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "sub2clash-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"axios": "^1.12.2",
|
||||
"daisyui": "^5.3.7",
|
||||
"lit": "^3.3.1",
|
||||
"tailwindcss": "^4.1.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
712
server/frontend/src/app.ts
Normal file
712
server/frontend/src/app.ts
Normal file
@@ -0,0 +1,712 @@
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import globalStyles from "./index.css?inline";
|
||||
import { type Config, type Rule, type RuleProvider } from "./interface.js";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { base64EncodeUnicode, base64decodeUnicode } from "./utils.js";
|
||||
import "./components/rule-provider-input.js";
|
||||
import "./components/rule-input.js";
|
||||
import "./components/rename-input.js";
|
||||
|
||||
@customElement("sub2clash-app")
|
||||
export class Sub2clashApp extends LitElement {
|
||||
static styles = [unsafeCSS(globalStyles)];
|
||||
|
||||
private _config: Config = {
|
||||
clashType: 2,
|
||||
subscriptions: [],
|
||||
proxies: [],
|
||||
refresh: false,
|
||||
autoTest: false,
|
||||
lazy: false,
|
||||
ignoreCountryGroup: false,
|
||||
useUDP: false,
|
||||
template: "",
|
||||
sort: "nameasc",
|
||||
remove: "",
|
||||
nodeList: false,
|
||||
ruleProviders: [],
|
||||
replace: undefined,
|
||||
rules: [],
|
||||
};
|
||||
|
||||
@state()
|
||||
set config(value: Config) {
|
||||
console.log(JSON.stringify(value));
|
||||
if (
|
||||
(value.subscriptions == null || value.subscriptions.length == 0) &&
|
||||
(value.proxies == null || value.proxies.length == 0)
|
||||
) {
|
||||
this.configUrl = "";
|
||||
return;
|
||||
}
|
||||
const oldValue = this._config;
|
||||
this.configUrl = `${
|
||||
window.location.origin
|
||||
}${window.location.pathname.replace(
|
||||
/\/$/,
|
||||
""
|
||||
)}/convert/${base64EncodeUnicode(JSON.stringify(value))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")}`;
|
||||
this._config = value;
|
||||
this.requestUpdate("config", oldValue);
|
||||
}
|
||||
|
||||
get config(): Config {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
@state({
|
||||
hasChanged(value: boolean) {
|
||||
localStorage.setItem("theme", value ? "dark" : "light");
|
||||
document
|
||||
.querySelector("html")
|
||||
?.setAttribute("data-theme", value ? "dark" : "light");
|
||||
return true;
|
||||
},
|
||||
})
|
||||
darkTheme: boolean = this.initTheme();
|
||||
|
||||
initTheme(): boolean {
|
||||
const savedTheme = localStorage.getItem("theme");
|
||||
if (savedTheme != null) {
|
||||
return savedTheme === "dark" ? true : false;
|
||||
}
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
return prefersDark;
|
||||
}
|
||||
|
||||
@state()
|
||||
reverseUrl: string = "";
|
||||
|
||||
@state()
|
||||
dialogMessage: string = "";
|
||||
|
||||
@state()
|
||||
dialogTitle: string = "";
|
||||
|
||||
@query("dialog#my_modal")
|
||||
dialog!: HTMLDialogElement;
|
||||
|
||||
showDialog(title: string, message: string): void {
|
||||
if (title.trim() === "") {
|
||||
title = "警告";
|
||||
}
|
||||
this.dialogTitle = title;
|
||||
this.dialogMessage = message;
|
||||
this.dialog.showModal();
|
||||
}
|
||||
|
||||
@state()
|
||||
configUrl: string = "";
|
||||
|
||||
@state()
|
||||
shortLinkID: string = "";
|
||||
|
||||
@state()
|
||||
shortLinkPasswd: string = "";
|
||||
|
||||
async copyToClipboard(content: string, e: HTMLButtonElement) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
let text = e.textContent;
|
||||
e.addEventListener("mouseout", function () {
|
||||
e.textContent = text;
|
||||
});
|
||||
e.textContent = "复制成功";
|
||||
} catch (err) {
|
||||
console.error("复制到剪贴板失败:", err);
|
||||
}
|
||||
}
|
||||
|
||||
generateShortLink() {
|
||||
if (this.configUrl === "") {
|
||||
this.showDialog("", "还未填写配置");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.post(
|
||||
"./short",
|
||||
{
|
||||
config: this.config,
|
||||
password: this.shortLinkPasswd,
|
||||
id: this.shortLinkID,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then((response) => {
|
||||
// 设置返回的短链ID和密码
|
||||
this.shortLinkID = response.data.id;
|
||||
this.shortLinkPasswd = response.data.password;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response && error.response.data) {
|
||||
this.showDialog("", "生成短链失败:" + error.response.data);
|
||||
} else {
|
||||
this.showDialog("", "生成短链失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateShortLink() {
|
||||
if (this.shortLinkID.trim() === "") {
|
||||
this.showDialog("", "请输入ID");
|
||||
return;
|
||||
}
|
||||
if (this.shortLinkPasswd.trim() === "") {
|
||||
this.showDialog("", "请输入密码");
|
||||
return;
|
||||
}
|
||||
if (this.configUrl === "") {
|
||||
this.showDialog("", "还未填写配置");
|
||||
return;
|
||||
}
|
||||
axios
|
||||
.put(
|
||||
"./short",
|
||||
{
|
||||
id: this.shortLinkID,
|
||||
config: this.config,
|
||||
password: this.shortLinkPasswd,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
this.showDialog("成功", "更新成功");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
this.showDialog("", "密码错误");
|
||||
} else if (error.response && error.response.data) {
|
||||
this.showDialog("", "更新短链失败:" + error.response.data);
|
||||
} else {
|
||||
this.showDialog("", "更新短链失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteShortLink() {
|
||||
if (this.shortLinkID.trim() === "") {
|
||||
this.showDialog("", "请输入ID");
|
||||
return;
|
||||
}
|
||||
if (this.shortLinkPasswd.trim() === "") {
|
||||
this.showDialog("", "请输入密码");
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams();
|
||||
params.append("password", this.shortLinkPasswd);
|
||||
axios
|
||||
.delete(`./short/${this.shortLinkID}?${params.toString()}`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.showDialog("成功", "删除成功");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.response && error.response.status === 401) {
|
||||
this.showDialog("", "短链不存在或密码错误");
|
||||
} else if (error.response && error.response.data) {
|
||||
this.showDialog("", "删除短链失败:" + error.response.data);
|
||||
} else {
|
||||
this.showDialog("", "删除短链失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getRawConfigFromShortLink() {
|
||||
const s = this.reverseUrl.split("/s/");
|
||||
if (s.length != 2) {
|
||||
this.showDialog("", "解析失败");
|
||||
return;
|
||||
}
|
||||
|
||||
axios
|
||||
.get(`./short/${s[1]}`)
|
||||
.then((resp) => {
|
||||
this.config = resp.data;
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (err.response && err.response.status == 401) {
|
||||
this.showDialog("", "短链不存在或密码错误");
|
||||
} else if (err.response && err.response.data) {
|
||||
this.showDialog("", "获取配置失败:" + err.response.data);
|
||||
} else {
|
||||
this.showDialog("", "获取配置失败");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parseConfig() {
|
||||
if (this.reverseUrl.trim() === "") {
|
||||
this.showDialog("", "无法解析,链接为空");
|
||||
}
|
||||
if (this.reverseUrl.indexOf("/s/") != -1) {
|
||||
this.getRawConfigFromShortLink();
|
||||
return;
|
||||
}
|
||||
let url = new URL(this.reverseUrl);
|
||||
const pathSections = url.pathname.split("/");
|
||||
if (pathSections.length < 2) {
|
||||
this.showDialog("", "无法解析,链接格式错误");
|
||||
}
|
||||
if (pathSections[pathSections.length - 2] == "convert") {
|
||||
let base64Data = pathSections[pathSections.length - 1];
|
||||
base64Data = base64Data.replace(/-/g, "+").replace(/_/g, "/");
|
||||
try {
|
||||
const configData = base64decodeUnicode(base64Data);
|
||||
this.config = JSON.parse(configData) as Config;
|
||||
} catch (e: any) {
|
||||
this.showDialog("", "无法解析 Base64,配置格式错误");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
this.showDialog("", "无法解析,链接格式错误");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<dialog id="my_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="text-lg font-bold">${this.dialogTitle}</h3>
|
||||
<p class="py-4">${this.dialogMessage}</p>
|
||||
<div class="modal-action">
|
||||
<form method="dialog">
|
||||
<button class="btn">关闭</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<div class="max-w-4xl mx-auto p-4 flex flex-col items-center">
|
||||
<form class="w-full max-w-2xl bg-base-100">
|
||||
<fieldset class="fieldset mb-6">
|
||||
<div class="flex flex-row justify-between items-center my-6">
|
||||
<legend
|
||||
class="fieldset-legend text-2xl font-semibold inline-block m-0 p-0">
|
||||
sub2clash
|
||||
</legend>
|
||||
<label class="swap swap-rotate h-7 w-7">
|
||||
<!-- this hidden checkbox controls the state -->
|
||||
<input
|
||||
type="checkbox"
|
||||
class="theme-controller"
|
||||
.checked="${!this.darkTheme}"
|
||||
@change="${() => (this.darkTheme = !this.darkTheme)}" />
|
||||
|
||||
<!-- sun icon -->
|
||||
<svg
|
||||
class="swap-off h-7 w-7 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
|
||||
</svg>
|
||||
|
||||
<!-- moon icon -->
|
||||
<svg
|
||||
class="swap-on h-7 w-7 fill-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Input URL -->
|
||||
<div class="form-control mb-5">
|
||||
<label class="label mb-1 pl-1">解析链接</label>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
class="input input-bordered w-full join-item"
|
||||
type="text"
|
||||
@change="${(e: Event) => {
|
||||
this.reverseUrl = (e.target as HTMLInputElement).value;
|
||||
}}"
|
||||
placeholder="通过生成的链接重新填写下方设置" />
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
@click="${this.parseConfig}"
|
||||
type="button">
|
||||
解析
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Endpoint -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="endpoint">客户端类型</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
name="endpoint"
|
||||
.value="${this.config.clashType == 1 ? "1" : "2"}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
clashType: Number((e.target as HTMLInputElement).value),
|
||||
};
|
||||
}}">
|
||||
<option value="1">Clash</option>
|
||||
<option value="2" selected>Clash.Meta</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Template -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="template">模板链接</label>
|
||||
<input
|
||||
class="input input-bordered w-full"
|
||||
name="template"
|
||||
placeholder="输入模板链接"
|
||||
type="text"
|
||||
.value="${this.config.template ?? ""}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
template: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
}}" />
|
||||
</div>
|
||||
|
||||
<!-- Subscription Link -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="sub">订阅链接</label>
|
||||
<div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered h-24 w-full"
|
||||
name="sub"
|
||||
placeholder="每行输入一个订阅链接"
|
||||
.value="${this.config.subscriptions
|
||||
? this.config.subscriptions.join("\n")
|
||||
: ""}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
subscriptions: (e.target as HTMLInputElement).value
|
||||
.split("\n")
|
||||
.filter((e) => e.trim() !== ""),
|
||||
};
|
||||
}}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proxy Link -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="proxy">节点分享链接</label>
|
||||
<div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered h-24 w-full"
|
||||
name="proxy"
|
||||
placeholder="每行输入一个节点分享链接"
|
||||
.value="${this.config.proxies
|
||||
? this.config.proxies.join("\n")
|
||||
: ""}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
proxies: (e.target as HTMLInputElement).value
|
||||
.split("\n")
|
||||
.filter((e) => e.trim() !== ""),
|
||||
};
|
||||
}}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Agent -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="user-agent">UA 标识</label>
|
||||
<div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered h-20 w-full"
|
||||
name="user-agent"
|
||||
placeholder="用于获取订阅的 http 请求中的 User-Agent 标识"
|
||||
.value="${this.config.userAgent ?? ""}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
userAgent: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
}}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="sort">
|
||||
国家策略组排序规则
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
name="sort"
|
||||
.value="${this.config.sort ?? "nameasc"}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
sort: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
}}">
|
||||
<option value="nameasc">名称(升序)</option>
|
||||
<option value="namedesc">名称(降序)</option>
|
||||
<option value="sizeasc">节点数量(升序)</option>
|
||||
<option value="sizedesc">节点数量(降序)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Remove -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1" for="remove">
|
||||
<span class="label-text">排除节点</span>
|
||||
</label>
|
||||
<input
|
||||
class="input input-bordered w-full"
|
||||
type="text"
|
||||
name="remove"
|
||||
placeholder="正则表达式"
|
||||
.value="${this.config.remove ?? ""}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
remove: (e.target as HTMLInputElement).value,
|
||||
};
|
||||
}}" />
|
||||
</div>
|
||||
|
||||
<!-- Checkboxes -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="refresh"
|
||||
class="checkbox"
|
||||
.checked="${this.config.refresh ?? false}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
refresh: (e.target as HTMLInputElement).checked,
|
||||
};
|
||||
}}" />
|
||||
强制重新获取订阅
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mb-3">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="nodeList"
|
||||
class="checkbox"
|
||||
.checked="${this.config.nodeList ?? false}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
nodeList: (e.target as HTMLInputElement).checked,
|
||||
};
|
||||
}}" />
|
||||
输出为 Node List
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mb-3">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="autoTest"
|
||||
class="checkbox"
|
||||
.checked="${this.config.autoTest ?? false}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
autoTest: (e.target as HTMLInputElement).checked,
|
||||
};
|
||||
}}" />
|
||||
国家策略组自动测速
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mb-3">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="lazy"
|
||||
class="checkbox"
|
||||
.checked="${this.config.lazy ?? false}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
lazy: (e.target as HTMLInputElement).checked,
|
||||
};
|
||||
}}" />
|
||||
自动测速启用 lazy 模式
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mb-3">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="igcg"
|
||||
class="checkbox"
|
||||
.checked="${this.config.ignoreCountryGroup ?? false}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
ignoreCountryGroup: (e.target as HTMLInputElement)
|
||||
.checked,
|
||||
};
|
||||
}}" />
|
||||
不输出国家策略组
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control mb-5">
|
||||
<label class="label cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="useUDP"
|
||||
class="checkbox"
|
||||
.checked="${this.config.useUDP ?? false}"
|
||||
@change="${(e: Event) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
useUDP: (e.target as HTMLInputElement).checked,
|
||||
};
|
||||
}}" />
|
||||
使用 UDP
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<rule-provider-input
|
||||
@change="${(e: CustomEvent<Array<RuleProvider>>) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
ruleProviders: e.detail,
|
||||
};
|
||||
}}"></rule-provider-input>
|
||||
|
||||
<rule-input
|
||||
@change="${(e: CustomEvent<Array<Rule>>) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
rules: e.detail,
|
||||
};
|
||||
}}"></rule-input>
|
||||
<rename-input
|
||||
@change="${(e: CustomEvent<{ [key: string]: string }>) => {
|
||||
this.config = {
|
||||
...this.config,
|
||||
replace: e.detail,
|
||||
};
|
||||
}}"></rename-input>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="fieldset mb-8">
|
||||
<legend
|
||||
class="fieldset-legend text-2xl font-semibold mb-4 text-center">
|
||||
输出配置
|
||||
</legend>
|
||||
|
||||
<!-- Display the API Link -->
|
||||
<div class="form-control mb-5">
|
||||
<div class="join w-full mb-2">
|
||||
<input
|
||||
class="input input-bordered w-full join-item cursor-not-allowed"
|
||||
type="text"
|
||||
placeholder="链接"
|
||||
.value="${this.configUrl}"
|
||||
readonly />
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
@click="${(e: Event) => {
|
||||
this.copyToClipboard(
|
||||
this.configUrl,
|
||||
e.target as HTMLButtonElement
|
||||
);
|
||||
}}"
|
||||
type="button">
|
||||
复制链接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-2">
|
||||
<div class="join w-full">
|
||||
<input
|
||||
class="input input-bordered w-1/2 join-item"
|
||||
type="text"
|
||||
placeholder="ID(可选)"
|
||||
.value="${this.shortLinkID}"
|
||||
@change="${(e: Event) => {
|
||||
this.shortLinkID = (e.target as HTMLInputElement).value;
|
||||
}}" />
|
||||
<input
|
||||
class="input input-bordered w-1/2 join-item"
|
||||
type="text"
|
||||
placeholder="密码"
|
||||
.value="${this.shortLinkPasswd}"
|
||||
@change="${(e: Event) => {
|
||||
this.shortLinkPasswd = (e.target as HTMLInputElement).value;
|
||||
}}" />
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
type="button"
|
||||
@click="${this.generateShortLink}">
|
||||
生成短链
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
@click="${this.updateShortLink}"
|
||||
type="button">
|
||||
更新短链
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
@click="${this.deleteShortLink}"
|
||||
type="button">
|
||||
删除短链
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-primary join-item"
|
||||
type="button"
|
||||
@click="${(e: Event) => {
|
||||
this.copyToClipboard(
|
||||
`${window.location.origin}${window.location.pathname}s/${this.shortLinkID}?password=${this.shortLinkPasswd}`,
|
||||
e.target as HTMLButtonElement
|
||||
);
|
||||
}}">
|
||||
复制短链
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer class="footer footer-horizontal footer-center mb-8">
|
||||
<aside>
|
||||
<p>
|
||||
Powered by
|
||||
<a class="link" href="https://github.com/bestnite/sub2clash"
|
||||
>sub2clash</a
|
||||
>
|
||||
</p>
|
||||
<p>Version: ${import.meta.env.VITE_APP_VERSION ?? "dev"}</p>
|
||||
</aside>
|
||||
</footer>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"sub2clash-app": Sub2clashApp;
|
||||
}
|
||||
}
|
||||
101
server/frontend/src/components/rename-input.ts
Normal file
101
server/frontend/src/components/rename-input.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import globalStyles from "../index.css?inline";
|
||||
import type { Rename } from "../interface";
|
||||
|
||||
@customElement("rename-input")
|
||||
export class RenameInput extends LitElement {
|
||||
static styles = [unsafeCSS(globalStyles)];
|
||||
|
||||
private _replaceArray: Array<Rename> = [];
|
||||
|
||||
@state()
|
||||
set replaceArray(value: Array<Rename>) {
|
||||
this._replaceArray = value;
|
||||
let updatedReplaceMap: { [key: string]: string } = {};
|
||||
value.forEach((e) => {
|
||||
updatedReplaceMap[e.old] = e.new;
|
||||
});
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: updatedReplaceMap,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get replaceArray(): Array<Rename> {
|
||||
return this._replaceArray;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<!-- Rename -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1">
|
||||
<span class="label-text">节点名称替换</span>
|
||||
<button
|
||||
class="btn btn-primary btn-xs"
|
||||
type="button"
|
||||
@click="${() => {
|
||||
let updatedReplaceArray = [...this.replaceArray];
|
||||
updatedReplaceArray.push({ old: "", new: "" });
|
||||
this.replaceArray = updatedReplaceArray;
|
||||
}}">
|
||||
+
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
${this.replaceArray.map((_, i) => this.RenameTemplate(i))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
RenameTemplate(index: number) {
|
||||
const replaceItem = this.replaceArray[index];
|
||||
return html`<div class="join mb-1">
|
||||
<input
|
||||
class="input join-item"
|
||||
placeholder="旧名称 (正则表达式)"
|
||||
.value="${replaceItem?.old ?? ""}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedReplaceArray = [...this.replaceArray];
|
||||
updatedReplaceArray[index] = {
|
||||
...updatedReplaceArray[index],
|
||||
old: target.value,
|
||||
};
|
||||
this.replaceArray = updatedReplaceArray;
|
||||
}}" />
|
||||
<input
|
||||
class="input join-item"
|
||||
placeholder="新名称"
|
||||
.value="${replaceItem?.new ?? ""}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedReplaceArray = [...this.replaceArray];
|
||||
updatedReplaceArray[index] = {
|
||||
...updatedReplaceArray[index],
|
||||
new: target.value,
|
||||
};
|
||||
this.replaceArray = updatedReplaceArray;
|
||||
}}" />
|
||||
<button
|
||||
class="btn join-item bg-error"
|
||||
type="button"
|
||||
@click="${() => {
|
||||
let updatedReplaceArray = this.replaceArray.filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
this.replaceArray = updatedReplaceArray;
|
||||
}}">
|
||||
删除
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rename-input": RenameInput;
|
||||
}
|
||||
}
|
||||
92
server/frontend/src/components/rule-input.ts
Normal file
92
server/frontend/src/components/rule-input.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import type { Rule } from "../interface";
|
||||
import globalStyles from "../index.css?inline";
|
||||
|
||||
@customElement("rule-input")
|
||||
export class RuleInput extends LitElement {
|
||||
static styles = [unsafeCSS(globalStyles)];
|
||||
|
||||
_rules: Array<Rule> = [];
|
||||
@state()
|
||||
set rules(value: Array<Rule>) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: value,
|
||||
})
|
||||
);
|
||||
this._rules = value;
|
||||
}
|
||||
get rules() {
|
||||
return this._rules;
|
||||
}
|
||||
render() {
|
||||
return html`<!-- Rule -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1">
|
||||
<span class="label-text">规则</span>
|
||||
<button
|
||||
class="btn btn-primary btn-xs"
|
||||
type="button"
|
||||
@click="${() => {
|
||||
let updatedRules = this.rules ? [...this.rules] : [];
|
||||
updatedRules?.push({
|
||||
rule: "",
|
||||
prepend: false,
|
||||
});
|
||||
this.rules = updatedRules;
|
||||
}}">
|
||||
+
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
${this.rules?.map((_, i) => this.RuleTemplate(i))}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
RuleTemplate(index: number) {
|
||||
return html`<div class="join mb-1">
|
||||
<input
|
||||
class="input join-item"
|
||||
placeholder="规则"
|
||||
.value="${this.rules![index].rule}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRules = this.rules;
|
||||
updatedRules![index].rule = target.value;
|
||||
this.rules = updatedRules;
|
||||
}}" />
|
||||
<div class="tooltip" data-tip="是否置于规则列表最前">
|
||||
<select
|
||||
class="select join-item w-fit"
|
||||
.value="${String(this.rules![index].prepend)}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRules = this.rules;
|
||||
updatedRules![index].prepend = Boolean(target.value);
|
||||
this.rules = updatedRules;
|
||||
}}">
|
||||
<option value="true">是</option>
|
||||
<option value="false" selected>否</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="btn join-item bg-error"
|
||||
type="button"
|
||||
@click="${() => {
|
||||
let updatedRules = this.rules?.filter((_, i) => i !== index);
|
||||
this.rules = updatedRules;
|
||||
}}">
|
||||
删除
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rule-input": RuleInput;
|
||||
}
|
||||
}
|
||||
143
server/frontend/src/components/rule-provider-input.ts
Normal file
143
server/frontend/src/components/rule-provider-input.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { LitElement, html, unsafeCSS } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import type { RuleProvider } from "../interface";
|
||||
import globalStyles from "../index.css?inline";
|
||||
|
||||
@customElement("rule-provider-input")
|
||||
export class RuleProviderInput extends LitElement {
|
||||
static styles = [unsafeCSS(globalStyles)];
|
||||
|
||||
_ruleProviders: Array<RuleProvider> = [];
|
||||
|
||||
@state()
|
||||
set ruleProviders(value) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("change", {
|
||||
detail: value,
|
||||
})
|
||||
);
|
||||
this._ruleProviders = value;
|
||||
}
|
||||
|
||||
get ruleProviders() {
|
||||
return this._ruleProviders;
|
||||
}
|
||||
|
||||
RuleProviderTemplate(index: number) {
|
||||
return html`
|
||||
<div class="join mb-1">
|
||||
<div class="tooltip" data-tip="不能重复">
|
||||
<input
|
||||
class="input join-item"
|
||||
placeholder="名称"
|
||||
.value="${this.ruleProviders![index].name}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRuleProviders = this.ruleProviders;
|
||||
updatedRuleProviders![index].name = target.value;
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}" />
|
||||
</div>
|
||||
<div class="tooltip" data-tip="类型">
|
||||
<select
|
||||
class="select join-item w-fit"
|
||||
.value="${this.ruleProviders![index].behavior}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRuleProviders = this.ruleProviders;
|
||||
updatedRuleProviders![index].behavior = target.value;
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}">
|
||||
<option value="classical" selected>classical</option>
|
||||
<option value="domain">domain</option>
|
||||
<option value="ipcidr">ipcidr</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
class="input join-item"
|
||||
placeholder="Url"
|
||||
.value="${this.ruleProviders![index].url}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRuleProviders = this.ruleProviders;
|
||||
updatedRuleProviders![index].url = target.value;
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}" />
|
||||
</div>
|
||||
<input
|
||||
class="input join-item"
|
||||
placeholder="出站策略组"
|
||||
.value="${this.ruleProviders![index].group}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRuleProviders = this.ruleProviders;
|
||||
updatedRuleProviders![index].group = target.value;
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}" />
|
||||
<div class="tooltip" data-tip="是否置于规则列表最前">
|
||||
<select
|
||||
class="select join-item w-fit"
|
||||
.value="${String(this.ruleProviders![index].prepend)}"
|
||||
@change="${(e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
let updatedRuleProviders = this.ruleProviders;
|
||||
updatedRuleProviders![index].prepend = Boolean(target.value);
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}">
|
||||
<option value="true">是</option>
|
||||
<option value="false" selected>否</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="btn join-item bg-error"
|
||||
type="button"
|
||||
@click="${() => {
|
||||
let updatedRuleProviders = this.ruleProviders?.filter(
|
||||
(_, i) => i !== index
|
||||
);
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}">
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <!-- Rule Provider -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label mb-1 pl-1">
|
||||
<span class="label-text">Rule Provider</span>
|
||||
<button
|
||||
class="btn btn-primary btn-xs"
|
||||
type="button"
|
||||
@click="${() => {
|
||||
let updatedRuleProviders = this.ruleProviders
|
||||
? [...this.ruleProviders]
|
||||
: [];
|
||||
updatedRuleProviders.push({
|
||||
behavior: "classical",
|
||||
url: "",
|
||||
name: "",
|
||||
prepend: false,
|
||||
group: "",
|
||||
});
|
||||
this.ruleProviders = updatedRuleProviders;
|
||||
}}">
|
||||
+
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
${this.ruleProviders?.map((_, i) => this.RuleProviderTemplate(i))}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rule-provider-input": RuleProviderInput;
|
||||
}
|
||||
}
|
||||
72
server/frontend/src/index.css
Normal file
72
server/frontend/src/index.css
Normal file
@@ -0,0 +1,72 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "light";
|
||||
default: false;
|
||||
prefersdark: false;
|
||||
color-scheme: "light";
|
||||
--color-base-100: oklch(98% 0.001 106.423);
|
||||
--color-base-200: oklch(97% 0.001 106.424);
|
||||
--color-base-300: oklch(92% 0.003 48.717);
|
||||
--color-base-content: oklch(21% 0.006 56.043);
|
||||
--color-primary: oklch(54% 0.281 293.009);
|
||||
--color-primary-content: oklch(96% 0.016 293.756);
|
||||
--color-secondary: oklch(57% 0.245 27.325);
|
||||
--color-secondary-content: oklch(97% 0.013 17.38);
|
||||
--color-accent: oklch(59% 0.249 0.584);
|
||||
--color-accent-content: oklch(97% 0.014 343.198);
|
||||
--color-neutral: oklch(14% 0.004 49.25);
|
||||
--color-neutral-content: oklch(98% 0.001 106.423);
|
||||
--color-info: oklch(78% 0.154 211.53);
|
||||
--color-info-content: oklch(30% 0.056 229.695);
|
||||
--color-success: oklch(79% 0.209 151.711);
|
||||
--color-success-content: oklch(26% 0.065 152.934);
|
||||
--color-warning: oklch(82% 0.189 84.429);
|
||||
--color-warning-content: oklch(27% 0.077 45.635);
|
||||
--color-error: oklch(71% 0.194 13.428);
|
||||
--color-error-content: oklch(27% 0.105 12.094);
|
||||
--radius-selector: 1rem;
|
||||
--radius-field: 1rem;
|
||||
--radius-box: 1rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1.5px;
|
||||
--depth: 0;
|
||||
--noise: 0;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: "dark";
|
||||
default: true;
|
||||
prefersdark: true;
|
||||
color-scheme: "dark";
|
||||
--color-base-100: oklch(21% 0.006 285.885);
|
||||
--color-base-200: oklch(21% 0.006 285.885);
|
||||
--color-base-300: oklch(27% 0.006 286.033);
|
||||
--color-base-content: oklch(96% 0.001 286.375);
|
||||
--color-primary: oklch(55% 0.288 302.321);
|
||||
--color-primary-content: oklch(97% 0.014 308.299);
|
||||
--color-secondary: oklch(44% 0.03 256.802);
|
||||
--color-secondary-content: oklch(98% 0.002 247.839);
|
||||
--color-accent: oklch(59% 0.249 0.584);
|
||||
--color-accent-content: oklch(97% 0.014 343.198);
|
||||
--color-neutral: oklch(37% 0.013 285.805);
|
||||
--color-neutral-content: oklch(98% 0 0);
|
||||
--color-info: oklch(54% 0.245 262.881);
|
||||
--color-info-content: oklch(97% 0.014 254.604);
|
||||
--color-success: oklch(64% 0.2 131.684);
|
||||
--color-success-content: oklch(98% 0.031 120.757);
|
||||
--color-warning: oklch(66% 0.179 58.318);
|
||||
--color-warning-content: oklch(98% 0.022 95.277);
|
||||
--color-error: oklch(58% 0.253 17.585);
|
||||
--color-error-content: oklch(96% 0.015 12.422);
|
||||
--radius-selector: 1rem;
|
||||
--radius-field: 1rem;
|
||||
--radius-box: 1rem;
|
||||
--size-selector: 0.25rem;
|
||||
--size-field: 0.25rem;
|
||||
--border: 1px;
|
||||
--depth: 0;
|
||||
--noise: 1;
|
||||
}
|
||||
36
server/frontend/src/interface.ts
Normal file
36
server/frontend/src/interface.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export interface RuleProvider {
|
||||
behavior: string;
|
||||
url: string;
|
||||
group: string;
|
||||
prepend: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
rule: string;
|
||||
prepend: boolean;
|
||||
}
|
||||
|
||||
export interface Rename {
|
||||
old: string;
|
||||
new: string;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
clashType: number;
|
||||
subscriptions?: string[];
|
||||
proxies?: string[];
|
||||
userAgent?: string;
|
||||
refresh?: boolean;
|
||||
autoTest?: boolean;
|
||||
lazy?: boolean;
|
||||
nodeList?: boolean;
|
||||
ignoreCountryGroup?: boolean;
|
||||
useUDP?: boolean;
|
||||
template?: string;
|
||||
ruleProviders?: RuleProvider[];
|
||||
rules?: Rule[];
|
||||
sort?: string;
|
||||
remove?: string;
|
||||
replace?: { [key: string]: string };
|
||||
}
|
||||
15
server/frontend/src/utils.ts
Normal file
15
server/frontend/src/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function base64EncodeUnicode(str: string) {
|
||||
const bytes = new TextEncoder().encode(str);
|
||||
let binary = "";
|
||||
bytes.forEach((b) => (binary += String.fromCharCode(b)));
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
export function base64decodeUnicode(str: string) {
|
||||
const binaryString = atob(str);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
31
server/frontend/tsconfig.json
Normal file
31
server/frontend/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"types": [
|
||||
"vite/client"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
6
server/frontend/vite.config.js
Normal file
6
server/frontend/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
});
|
||||
61
server/handler/convert.go
Normal file
61
server/handler/convert.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
M "github.com/bestnite/sub2clash/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func ConvertHandler() func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
query, err := M.ParseConvertQuery(c)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
template := ""
|
||||
switch query.ClashType {
|
||||
case model.Clash:
|
||||
template = config.GlobalConfig.ClashTemplate
|
||||
case model.ClashMeta:
|
||||
template = config.GlobalConfig.MetaTemplate
|
||||
}
|
||||
sub, err := common.BuildSub(query.ClashType, query, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(query.Subs) == 1 {
|
||||
userInfoHeader, err := common.FetchSubscriptionUserInfo(query.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
|
||||
if err == nil {
|
||||
c.Header("subscription-userinfo", userInfoHeader)
|
||||
}
|
||||
}
|
||||
|
||||
if query.NodeListMode {
|
||||
nodelist := M.NodeList{}
|
||||
nodelist.Proxy = sub.Proxy
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
||||
}
|
||||
253
server/handler/short_link.go
Normal file
253
server/handler/short_link.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/common"
|
||||
"github.com/bestnite/sub2clash/common/database"
|
||||
"github.com/bestnite/sub2clash/config"
|
||||
"github.com/bestnite/sub2clash/model"
|
||||
M "github.com/bestnite/sub2clash/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type shortLinkGenRequset struct {
|
||||
Config model.ConvertConfig `form:"config" binding:"required"`
|
||||
Password string `form:"password"`
|
||||
ID string `form:"id"`
|
||||
}
|
||||
|
||||
type shortLinkUpdateRequest struct {
|
||||
Config model.ConvertConfig `form:"config" binding:"required"`
|
||||
Password string `form:"password" binding:"required"`
|
||||
ID string `form:"id" binding:"required"`
|
||||
}
|
||||
|
||||
var DB *database.Database
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
DB, err = database.ConnectDB()
|
||||
if err != nil {
|
||||
log.Printf("failed to connect to database: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateLinkHandler(c *gin.Context) {
|
||||
var params shortLinkGenRequset
|
||||
if err := c.ShouldBind(¶ms); err != nil {
|
||||
c.String(http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
var password string
|
||||
var err error
|
||||
|
||||
if params.ID != "" {
|
||||
// 检查自定义ID是否已存在
|
||||
exists, err := DB.CheckShortLinkIDExists(params.ID)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
c.String(http.StatusBadRequest, "短链已存在")
|
||||
return
|
||||
}
|
||||
id = params.ID
|
||||
password = params.Password
|
||||
} else {
|
||||
// 自动生成短链ID和密码
|
||||
id, err = generateUniqueHash(config.GlobalConfig.ShortLinkLength)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "生成短链失败")
|
||||
return
|
||||
}
|
||||
if params.Password == "" {
|
||||
password = common.RandomString(8) // 生成8位随机密码
|
||||
} else {
|
||||
password = params.Password
|
||||
}
|
||||
}
|
||||
|
||||
shortLink := model.ShortLink{
|
||||
ID: id,
|
||||
Config: params.Config,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
if err := DB.CreateShortLink(&shortLink); err != nil {
|
||||
c.String(http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 返回生成的短链ID和密码
|
||||
response := map[string]string{
|
||||
"id": id,
|
||||
"password": password,
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func generateUniqueHash(length int) (string, error) {
|
||||
for {
|
||||
hash := common.RandomString(length)
|
||||
exists, err := DB.CheckShortLinkIDExists(hash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !exists {
|
||||
return hash, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateLinkHandler(c *gin.Context) {
|
||||
var params shortLinkUpdateRequest
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
c.String(http.StatusBadRequest, "参数错误: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 先获取原有的短链
|
||||
existingLink, err := DB.FindShortLinkByID(params.ID)
|
||||
if err != nil {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if existingLink.Password != params.Password {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(params.Config)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "配置格式错误")
|
||||
return
|
||||
}
|
||||
if err := DB.UpdataShortLink(params.ID, "config", jsonData); err != nil {
|
||||
c.String(http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, "短链更新成功")
|
||||
}
|
||||
|
||||
func GetRawConfHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
password := c.Query("password")
|
||||
|
||||
if strings.TrimSpace(id) == "" {
|
||||
c.String(http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
shortLink, err := DB.FindShortLinkByID(id)
|
||||
if err != nil {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = DB.UpdataShortLink(shortLink.ID, "last_request_time", time.Now().Unix())
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "数据库错误")
|
||||
return
|
||||
}
|
||||
|
||||
template := ""
|
||||
switch shortLink.Config.ClashType {
|
||||
case model.Clash:
|
||||
template = config.GlobalConfig.ClashTemplate
|
||||
case model.ClashMeta:
|
||||
template = config.GlobalConfig.MetaTemplate
|
||||
}
|
||||
sub, err := common.BuildSub(shortLink.Config.ClashType, shortLink.Config, template, config.GlobalConfig.CacheExpire, config.GlobalConfig.RequestRetryTimes)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if len(shortLink.Config.Subs) == 1 {
|
||||
userInfoHeader, err := common.FetchSubscriptionUserInfo(shortLink.Config.Subs[0], "clash", config.GlobalConfig.RequestRetryTimes)
|
||||
if err == nil {
|
||||
c.Header("subscription-userinfo", userInfoHeader)
|
||||
}
|
||||
}
|
||||
|
||||
if shortLink.Config.NodeListMode {
|
||||
nodelist := M.NodeList{}
|
||||
nodelist.Proxy = sub.Proxy
|
||||
marshal, err := yaml.Marshal(nodelist)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
return
|
||||
}
|
||||
marshal, err := yaml.Marshal(sub)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.String(http.StatusOK, string(marshal))
|
||||
}
|
||||
|
||||
func GetRawConfUriHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
password := c.Query("password")
|
||||
|
||||
if strings.TrimSpace(id) == "" {
|
||||
c.String(http.StatusBadRequest, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
shortLink, err := DB.FindShortLinkByID(id)
|
||||
if err != nil {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
if shortLink.Password != "" && shortLink.Password != password {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, shortLink.Config)
|
||||
}
|
||||
|
||||
func DeleteShortLinkHandler(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
password := c.Query("password")
|
||||
shortLink, err := DB.FindShortLinkByID(id)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
if shortLink.Password != password {
|
||||
c.String(http.StatusUnauthorized, "短链不存在或密码错误")
|
||||
return
|
||||
}
|
||||
|
||||
err = DB.DeleteShortLink(id)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "删除失败", err)
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,10 @@ package middleware
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sub2clash/logger"
|
||||
"time"
|
||||
|
||||
"github.com/bestnite/sub2clash/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
32
server/route.go
Normal file
32
server/route.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/bestnite/sub2clash/server/handler"
|
||||
"github.com/bestnite/sub2clash/server/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed frontend/dist
|
||||
var staticFiles embed.FS
|
||||
|
||||
func SetRoute(r *gin.Engine) {
|
||||
r.GET("/convert/:config", middleware.ZapLogger(), handler.ConvertHandler())
|
||||
r.GET("/s/:id", middleware.ZapLogger(), handler.GetRawConfHandler)
|
||||
r.POST("/short", middleware.ZapLogger(), handler.GenerateLinkHandler)
|
||||
r.PUT("/short", middleware.ZapLogger(), handler.UpdateLinkHandler)
|
||||
r.GET("/short/:id", middleware.ZapLogger(), handler.GetRawConfUriHandler)
|
||||
r.DELETE("/short/:id", middleware.ZapLogger(), handler.DeleteShortLinkHandler)
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.FileFromFS("frontend/dist/", http.FS(staticFiles))
|
||||
})
|
||||
r.GET(
|
||||
"/assets/*filepath", func(c *gin.Context) {
|
||||
c.FileFromFS("frontend/dist/assets/"+c.Param("filepath"), http.FS(staticFiles))
|
||||
},
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user