mirror of
https://github.com/bestnite/sub2clash.git
synced 2025-04-10 17:15:55 +08:00
Compare commits
65 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
![]() |
c159f2b417 | ||
fd7c460f95 | |||
68269fd499 | |||
d8a81e44b6 | |||
70f5c63fe2 | |||
679102fa3b | |||
![]() |
3309a0c208 |
@ -5,4 +5,3 @@ REQUEST_RETRY_TIMES=3
|
|||||||
REQUEST_MAX_FILE_SIZE=1048576
|
REQUEST_MAX_FILE_SIZE=1048576
|
||||||
CACHE_EXPIRE=300
|
CACHE_EXPIRE=300
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
BASE_PATH=/
|
|
||||||
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
90
.github/workflows/docker.yml
vendored
90
.github/workflows/docker.yml
vendored
@ -1,69 +1,55 @@
|
|||||||
name: Build and Push to GHCR
|
name: docker
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- "v*"
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
prepare-and-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Docker meta
|
||||||
uses: docker/login-action@v1
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
nite07/sub2clash
|
||||||
|
ghcr.io/nitezs/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:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set tag name
|
- name: Build and push
|
||||||
id: set_tag
|
uses: docker/build-push-action@v5
|
||||||
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
|
|
||||||
with:
|
with:
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
build-args: |
|
build-args: |
|
||||||
dev=true
|
"version=${{ github.ref_name }}"
|
||||||
version=${{ github.sha }}
|
|
||||||
push: true
|
push: true
|
||||||
tags: ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
- name: Build and push Docker image for tags
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
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
|
|
||||||
|
|
||||||
|
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
|
|
||||||
|
|
28
.github/workflows/release.yml
vendored
Normal file
28
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
goreleaser:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v5
|
||||||
|
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
|
.idea
|
||||||
dist
|
dist
|
||||||
subs
|
subs
|
||||||
test
|
|
||||||
logs
|
logs
|
||||||
sub2clash.db
|
data
|
||||||
.env
|
.env
|
||||||
|
.vscode/settings.json
|
||||||
|
test
|
||||||
|
*test.go
|
@ -1,17 +1,32 @@
|
|||||||
#before:
|
project_name: sub2clash
|
||||||
# hooks:
|
|
||||||
# - go mod tidy
|
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- windows
|
||||||
- windows
|
- linux
|
||||||
- darwin
|
- darwin
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
ldflags:
|
- arm
|
||||||
- -s -w -X sub2clash/config.Version={{ .Version }}
|
- "386"
|
||||||
no_unique_dist_dir: true
|
goarm:
|
||||||
binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}"
|
- "6"
|
||||||
|
- "7"
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X sub2clash/constant.Version={{ .Version }}
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
archives:
|
||||||
|
- format: tar.gz
|
||||||
|
format_overrides:
|
||||||
|
- format: zip
|
||||||
|
goos: windows
|
||||||
|
wrap_in_directory: true
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- README.md
|
||||||
|
- templates
|
||||||
|
release:
|
||||||
|
draft: true
|
||||||
|
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@ -5,9 +5,9 @@
|
|||||||
"type": "go",
|
"type": "go",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "debug",
|
"mode": "debug",
|
||||||
"program": "${workspaceFolder}/main.go",
|
"program": "${workspaceFolder}",
|
||||||
"output": "${workspaceFolder}/dist/main.exe",
|
"output": "${workspaceFolder}/dist/main.exe",
|
||||||
"buildFlags": "-ldflags '-X sub2clash/config.Dev=true -X sub2clash/config.Version=dev'"
|
"buildFlags": "-ldflags '-X sub2clash/constant.Version=dev'"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
47
API.md
Normal file
47
API.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# `GET /clash`, `GET /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` | 只输出节点 |
|
||||||
|
|
||||||
|
# `POST /short`
|
||||||
|
|
||||||
|
获取短链,Content-Type 为 `application/json`
|
||||||
|
具体参考使用可以参考 [api\templates\index.html](api/static/index.html)
|
||||||
|
|
||||||
|
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||||
|
| --------- | ------ | -------- | ------ | ------------------------- |
|
||||||
|
| url | string | 是 | - | 需要转换的 Query 参数部分 |
|
||||||
|
| password | string | 否 | - | 短链密码 |
|
||||||
|
|
||||||
|
# `GET /s/:hash`
|
||||||
|
|
||||||
|
短链跳转
|
||||||
|
`hash` 为动态路由参数,可以通过 `/short` 接口获取
|
||||||
|
|
||||||
|
| Query 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||||
|
| ---------- | ------ | -------- | ------ | -------- |
|
||||||
|
| password | string | 否 | - | 短链密码 |
|
||||||
|
|
||||||
|
# `PUT /short`
|
||||||
|
|
||||||
|
更新短链,Content-Type 为 `application/json`
|
||||||
|
|
||||||
|
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||||
|
| --------- | ------ | -------- | ------ | ------------------------- |
|
||||||
|
| url | string | 是 | - | 需要转换的 Query 参数部分 |
|
||||||
|
| password | string | 否 | - | 短链密码 |
|
||||||
|
| hash | string | 是 | - | 短链 hash |
|
@ -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 | 否 | - | 短链密码 |
|
|
20
Dockerfile
20
Dockerfile
@ -1,28 +1,12 @@
|
|||||||
# 使用官方 Golang 镜像作为构建环境
|
|
||||||
FROM golang:1.21-alpine as builder
|
FROM golang:1.21-alpine as builder
|
||||||
LABEL authors="nite07"
|
LABEL authors="nite07"
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制源代码到工作目录
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# 获取参数
|
|
||||||
ARG version
|
ARG version
|
||||||
ARG dev
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X sub2clash/constant.Version=${version}" -o sub2clash .
|
||||||
|
|
||||||
# 使用 -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
|
FROM alpine:latest
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 从 builder 镜像中复制出编译好的二进制文件
|
|
||||||
COPY --from=builder /app/sub2clash /app/sub2clash
|
COPY --from=builder /app/sub2clash /app/sub2clash
|
||||||
|
ENTRYPOINT ["/app/sub2clash"]
|
||||||
# 设置容器的默认启动命令
|
|
||||||
ENTRYPOINT ["/app/sub2clash"]
|
|
46
README.md
46
README.md
@ -7,19 +7,21 @@
|
|||||||
|
|
||||||
- 开箱即用的规则、策略组配置
|
- 开箱即用的规则、策略组配置
|
||||||
- 自动根据节点名称按国家划分策略组
|
- 自动根据节点名称按国家划分策略组
|
||||||
- 支持多订阅合并
|
- 多订阅合并
|
||||||
- 支持添加自定义 Rule Provider、Rule
|
- 自定义 Rule Provider、Rule
|
||||||
- 支持多种协议
|
- 支持多种协议
|
||||||
- Shadowsocks
|
- Shadowsocks
|
||||||
- ShadowsocksR
|
- ShadowsocksR
|
||||||
- Vmess
|
- Vmess
|
||||||
- Vless (Clash.Meta)
|
- Vless (Clash.Meta)
|
||||||
- Trojan
|
- Trojan
|
||||||
- Hysteria2 (Clash.Meta)
|
- Hysteria (Clash.Meta)
|
||||||
|
- Hysteria2 (Clash.Meta)
|
||||||
|
- Socks5
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
### 运行
|
### 部署
|
||||||
|
|
||||||
- [docker compose](./docker-compose.yml)
|
- [docker compose](./docker-compose.yml)
|
||||||
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
|
- 运行[二进制文件](https://github.com/nitezs/sub2clash/releases/latest)
|
||||||
@ -28,25 +30,25 @@
|
|||||||
|
|
||||||
可以通过编辑 .env 文件来修改默认配置,docker 直接添加环境变量
|
可以通过编辑 .env 文件来修改默认配置,docker 直接添加环境变量
|
||||||
|
|
||||||
| 变量名 | 说明 | 默认值 |
|
| 变量名 | 说明 | 默认值 |
|
||||||
|-----------------------|----------------------------------------|-----------------------|
|
| --------------------- | ---------------------------------------------- | --------------------- |
|
||||||
| PORT | 端口 | `8011` |
|
| PORT | 端口 | `8011` |
|
||||||
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
|
| META_TEMPLATE | 默认 meta 模板文件名 | `template_meta.yaml` |
|
||||||
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
|
| CLASH_TEMPLATE | 默认 clash 模板文件名 | `template_clash.yaml` |
|
||||||
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
|
| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` |
|
||||||
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` |
|
| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` |
|
||||||
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
|
| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` |
|
||||||
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
|
| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` |
|
||||||
| SHORT_LINK_LENGTH | 短链长度 | `6` |
|
| SHORT_LINK_LENGTH | 短链长度 | `6` |
|
||||||
|
|
||||||
### API
|
### API
|
||||||
|
|
||||||
[API 文档](./API_README.md)
|
[API 文档](./API.md)
|
||||||
|
|
||||||
### 模板
|
### 模板
|
||||||
|
|
||||||
可以通过变量自定义模板中的策略组代理节点
|
可以通过变量自定义模板中的策略组代理节点
|
||||||
解释的不太清楚,可以参考下方默认模板
|
具体参考下方默认模板
|
||||||
|
|
||||||
- `<all>` 为添加所有节点
|
- `<all>` 为添加所有节点
|
||||||
- `<countries>` 为添加所有国家策略组
|
- `<countries>` 为添加所有国家策略组
|
||||||
@ -56,7 +58,3 @@
|
|||||||
|
|
||||||
- [Clash](./templates/template_clash.yaml)
|
- [Clash](./templates/template_clash.yaml)
|
||||||
- [Clash.Meta](./templates/template_meta.yaml)
|
- [Clash.Meta](./templates/template_meta.yaml)
|
||||||
|
|
||||||
## 已知问题
|
|
||||||
|
|
||||||
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
|
|
||||||
|
@ -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))
|
|
||||||
}
|
|
@ -1,17 +1,18 @@
|
|||||||
package controller
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sub2clash/config"
|
|
||||||
"sub2clash/model"
|
"github.com/nitezs/sub2clash/config"
|
||||||
"sub2clash/validator"
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
"github.com/nitezs/sub2clash/validator"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SubmodHandler(c *gin.Context) {
|
func SubmodHandler(c *gin.Context) {
|
||||||
// 从请求中获取参数
|
|
||||||
query, err := validator.ParseQuery(c)
|
query, err := validator.ParseQuery(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusBadRequest, err.Error())
|
c.String(http.StatusBadRequest, err.Error())
|
||||||
@ -22,7 +23,7 @@ func SubmodHandler(c *gin.Context) {
|
|||||||
c.String(http.StatusInternalServerError, err.Error())
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 输出
|
|
||||||
if query.NodeListMode {
|
if query.NodeListMode {
|
||||||
nodelist := model.NodeList{}
|
nodelist := model.NodeList{}
|
||||||
nodelist.Proxies = sub.Proxies
|
nodelist.Proxies = sub.Proxies
|
@ -1,4 +1,4 @@
|
|||||||
package controller
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@ -9,11 +9,12 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/logger"
|
|
||||||
"sub2clash/model"
|
"github.com/nitezs/sub2clash/common"
|
||||||
"sub2clash/parser"
|
"github.com/nitezs/sub2clash/logger"
|
||||||
"sub2clash/utils"
|
"github.com/nitezs/sub2clash/model"
|
||||||
"sub2clash/validator"
|
"github.com/nitezs/sub2clash/parser"
|
||||||
|
"github.com/nitezs/sub2clash/validator"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -22,17 +23,17 @@ import (
|
|||||||
func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) (
|
func BuildSub(clashType model.ClashType, query validator.SubValidator, template string) (
|
||||||
*model.Subscription, error,
|
*model.Subscription, error,
|
||||||
) {
|
) {
|
||||||
// 定义变量
|
|
||||||
var temp = &model.Subscription{}
|
var temp = &model.Subscription{}
|
||||||
var sub = &model.Subscription{}
|
var sub = &model.Subscription{}
|
||||||
var err error
|
var err error
|
||||||
var templateBytes []byte
|
var templateBytes []byte
|
||||||
// 加载模板
|
|
||||||
if query.Template != "" {
|
if query.Template != "" {
|
||||||
template = query.Template
|
template = query.Template
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(template, "http") {
|
if strings.HasPrefix(template, "http") {
|
||||||
templateBytes, err = utils.LoadSubscription(template, query.Refresh)
|
templateBytes, err = common.LoadSubscription(template, query.Refresh, query.UserAgent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Debug(
|
logger.Logger.Debug(
|
||||||
"load template failed", zap.String("template", template), zap.Error(err),
|
"load template failed", zap.String("template", template), zap.Error(err),
|
||||||
@ -44,7 +45,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("加载模板失败: " + err.Error())
|
return nil, errors.New("加载模板失败: " + err.Error())
|
||||||
}
|
}
|
||||||
templateBytes, err = utils.LoadTemplate(unescape)
|
templateBytes, err = common.LoadTemplate(unescape)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Debug(
|
logger.Logger.Debug(
|
||||||
"load template failed", zap.String("template", template), zap.Error(err),
|
"load template failed", zap.String("template", template), zap.Error(err),
|
||||||
@ -52,16 +53,16 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
return nil, errors.New("加载模板失败: " + err.Error())
|
return nil, errors.New("加载模板失败: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 解析模板
|
|
||||||
err = yaml.Unmarshal(templateBytes, &temp)
|
err = yaml.Unmarshal(templateBytes, &temp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Debug("parse template failed", zap.Error(err))
|
logger.Logger.Debug("parse template failed", zap.Error(err))
|
||||||
return nil, errors.New("解析模板失败: " + err.Error())
|
return nil, errors.New("解析模板失败: " + err.Error())
|
||||||
}
|
}
|
||||||
var proxyList []model.Proxy
|
var proxyList []model.Proxy
|
||||||
// 加载订阅
|
|
||||||
for i := range query.Subs {
|
for i := range query.Subs {
|
||||||
data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
|
data, err := common.LoadSubscription(query.Subs[i], query.Refresh, query.UserAgent)
|
||||||
subName := ""
|
subName := ""
|
||||||
if strings.Contains(query.Subs[i], "#") {
|
if strings.Contains(query.Subs[i], "#") {
|
||||||
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
|
subName = query.Subs[i][strings.LastIndex(query.Subs[i], "#")+1:]
|
||||||
@ -72,16 +73,16 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
)
|
)
|
||||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||||
}
|
}
|
||||||
// 解析订阅
|
|
||||||
err = yaml.Unmarshal(data, &sub)
|
err = yaml.Unmarshal(data, &sub)
|
||||||
newProxies := make([]model.Proxy, 0)
|
var newProxies []model.Proxy
|
||||||
if err != nil {
|
if err != nil {
|
||||||
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria)://")
|
reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless|hysteria|hy2|hysteria2)://")
|
||||||
if reg.Match(data) {
|
if reg.Match(data) {
|
||||||
p := utils.ParseProxy(strings.Split(string(data), "\n")...)
|
p := common.ParseProxy(strings.Split(string(data), "\n")...)
|
||||||
newProxies = p
|
newProxies = p
|
||||||
} else {
|
} else {
|
||||||
// 如果无法直接解析,尝试Base64解码
|
|
||||||
base64, err := parser.DecodeBase64(string(data))
|
base64, err := parser.DecodeBase64(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Debug(
|
logger.Logger.Debug(
|
||||||
@ -91,7 +92,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
)
|
)
|
||||||
return nil, errors.New("加载订阅失败: " + err.Error())
|
return nil, errors.New("加载订阅失败: " + err.Error())
|
||||||
}
|
}
|
||||||
p := utils.ParseProxy(strings.Split(base64, "\n")...)
|
p := common.ParseProxy(strings.Split(base64, "\n")...)
|
||||||
newProxies = p
|
newProxies = p
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -104,28 +105,31 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
}
|
}
|
||||||
proxyList = append(proxyList, newProxies...)
|
proxyList = append(proxyList, newProxies...)
|
||||||
}
|
}
|
||||||
// 添加自定义节点
|
|
||||||
if len(query.Proxies) != 0 {
|
if len(query.Proxies) != 0 {
|
||||||
proxyList = append(proxyList, utils.ParseProxy(query.Proxies...)...)
|
proxyList = append(proxyList, common.ParseProxy(query.Proxies...)...)
|
||||||
}
|
}
|
||||||
// 给节点添加订阅名称
|
|
||||||
for i := range proxyList {
|
for i := range proxyList {
|
||||||
if proxyList[i].SubName != "" {
|
if proxyList[i].SubName != "" {
|
||||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
proxyList[i].Name = strings.TrimSpace(proxyList[i].SubName) + " " + strings.TrimSpace(proxyList[i].Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 去掉配置相同的节点
|
|
||||||
proxies := make(map[string]*model.Proxy)
|
proxies := make(map[string]*model.Proxy)
|
||||||
newProxies := make([]model.Proxy, 0, len(proxyList))
|
newProxies := make([]model.Proxy, 0, len(proxyList))
|
||||||
for i := range proxyList {
|
for i := range proxyList {
|
||||||
key := proxyList[i].Server + ":" + strconv.Itoa(proxyList[i].Port) + ":" + proxyList[i].Type
|
key := proxyList[i].Server + strconv.Itoa(proxyList[i].Port) + proxyList[i].Type + proxyList[i].UUID + proxyList[i].Password
|
||||||
|
if proxyList[i].Network == "ws" {
|
||||||
|
key += proxyList[i].WSOpts.Path + proxyList[i].WSOpts.Headers["Host"]
|
||||||
|
}
|
||||||
if _, exist := proxies[key]; !exist {
|
if _, exist := proxies[key]; !exist {
|
||||||
proxies[key] = &proxyList[i]
|
proxies[key] = &proxyList[i]
|
||||||
newProxies = append(newProxies, proxyList[i])
|
newProxies = append(newProxies, proxyList[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxyList = newProxies
|
proxyList = newProxies
|
||||||
// 删除节点
|
|
||||||
if strings.TrimSpace(query.Remove) != "" {
|
if strings.TrimSpace(query.Remove) != "" {
|
||||||
newProxyList := make([]model.Proxy, 0, len(proxyList))
|
newProxyList := make([]model.Proxy, 0, len(proxyList))
|
||||||
for i := range proxyList {
|
for i := range proxyList {
|
||||||
@ -134,17 +138,17 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
|
||||||
return nil, errors.New("remove 参数非法: " + err.Error())
|
return nil, errors.New("remove 参数非法: " + err.Error())
|
||||||
}
|
}
|
||||||
// 删除匹配到的节点
|
|
||||||
if removeReg.MatchString(proxyList[i].Name) {
|
if removeReg.MatchString(proxyList[i].Name) {
|
||||||
continue // 如果匹配到要删除的元素,跳过该元素,不添加到新切片中
|
continue
|
||||||
}
|
}
|
||||||
newProxyList = append(newProxyList, proxyList[i]) // 将要保留的元素添加到新切片中
|
newProxyList = append(newProxyList, proxyList[i])
|
||||||
}
|
}
|
||||||
proxyList = newProxyList
|
proxyList = newProxyList
|
||||||
}
|
}
|
||||||
// 重命名
|
|
||||||
if len(query.ReplaceKeys) != 0 {
|
if len(query.ReplaceKeys) != 0 {
|
||||||
// 创建重命名正则表达式
|
|
||||||
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
|
replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
|
||||||
for _, v := range query.ReplaceKeys {
|
for _, v := range query.ReplaceKeys {
|
||||||
replaceReg, err := regexp.Compile(v)
|
replaceReg, err := regexp.Compile(v)
|
||||||
@ -155,12 +159,8 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
replaceRegs = append(replaceRegs, replaceReg)
|
replaceRegs = append(replaceRegs, replaceReg)
|
||||||
}
|
}
|
||||||
for i := range proxyList {
|
for i := range proxyList {
|
||||||
// 重命名匹配到的节点
|
|
||||||
for j, v := range replaceRegs {
|
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) {
|
if v.MatchString(proxyList[i].Name) {
|
||||||
proxyList[i].Name = v.ReplaceAllString(
|
proxyList[i].Name = v.ReplaceAllString(
|
||||||
proxyList[i].Name, query.ReplaceTo[j],
|
proxyList[i].Name, query.ReplaceTo[j],
|
||||||
@ -169,22 +169,24 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 重名检测
|
|
||||||
names := make(map[string]int)
|
names := make(map[string]int)
|
||||||
for i := range proxyList {
|
for i := range proxyList {
|
||||||
if _, exist := names[proxyList[i].Name]; exist {
|
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])
|
proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
|
||||||
|
} else {
|
||||||
|
names[proxyList[i].Name] = 0
|
||||||
}
|
}
|
||||||
names[proxyList[i].Name] = names[proxyList[i].Name] + 1
|
|
||||||
}
|
}
|
||||||
// trim
|
|
||||||
for i := range proxyList {
|
for i := range proxyList {
|
||||||
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
|
||||||
}
|
}
|
||||||
// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
|
|
||||||
var t = &model.Subscription{}
|
var t = &model.Subscription{}
|
||||||
utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
common.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
|
||||||
// 排序策略组
|
|
||||||
switch query.Sort {
|
switch query.Sort {
|
||||||
case "sizeasc":
|
case "sizeasc":
|
||||||
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
|
sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups))
|
||||||
@ -197,17 +199,17 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
default:
|
default:
|
||||||
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups))
|
||||||
}
|
}
|
||||||
// 合并新节点和模板
|
|
||||||
MergeSubAndTemplate(temp, t)
|
MergeSubAndTemplate(temp, t, query.IgnoreCountryGrooup)
|
||||||
// 处理自定义规则
|
|
||||||
for _, v := range query.Rules {
|
for _, v := range query.Rules {
|
||||||
if v.Prepend {
|
if v.Prepend {
|
||||||
utils.PrependRules(temp, v.Rule)
|
common.PrependRules(temp, v.Rule)
|
||||||
} else {
|
} else {
|
||||||
utils.AppendRules(temp, v.Rule)
|
common.AppendRules(temp, v.Rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 处理自定义 ruleProvider
|
|
||||||
for _, v := range query.RuleProviders {
|
for _, v := range query.RuleProviders {
|
||||||
hash := sha256.Sum224([]byte(v.Url))
|
hash := sha256.Sum224([]byte(v.Url))
|
||||||
name := hex.EncodeToString(hash[:])
|
name := hex.EncodeToString(hash[:])
|
||||||
@ -219,11 +221,11 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
Interval: 3600,
|
Interval: 3600,
|
||||||
}
|
}
|
||||||
if v.Prepend {
|
if v.Prepend {
|
||||||
utils.PrependRuleProvider(
|
common.PrependRuleProvider(
|
||||||
temp, v.Name, v.Group, provider,
|
temp, v.Name, v.Group, provider,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
utils.AppenddRuleProvider(
|
common.AppenddRuleProvider(
|
||||||
temp, v.Name, v.Group, provider,
|
temp, v.Name, v.Group, provider,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -231,9 +233,8 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
|
|||||||
return temp, nil
|
return temp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
|
func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription, igcg bool) {
|
||||||
// 只合并节点、策略组
|
|
||||||
// 统计所有国家策略组名称
|
|
||||||
var countryGroupNames []string
|
var countryGroupNames []string
|
||||||
for _, proxyGroup := range sub.ProxyGroups {
|
for _, proxyGroup := range sub.ProxyGroups {
|
||||||
if proxyGroup.IsCountryGrop {
|
if proxyGroup.IsCountryGrop {
|
||||||
@ -246,9 +247,9 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
|
|||||||
for _, proxy := range sub.Proxies {
|
for _, proxy := range sub.Proxies {
|
||||||
proxyNames = append(proxyNames, proxy.Name)
|
proxyNames = append(proxyNames, proxy.Name)
|
||||||
}
|
}
|
||||||
// 将订阅中的节点添加到模板中
|
|
||||||
temp.Proxies = append(temp.Proxies, sub.Proxies...)
|
temp.Proxies = append(temp.Proxies, sub.Proxies...)
|
||||||
// 将订阅中的策略组添加到模板中
|
|
||||||
for i := range temp.ProxyGroups {
|
for i := range temp.ProxyGroups {
|
||||||
if temp.ProxyGroups[i].IsCountryGrop {
|
if temp.ProxyGroups[i].IsCountryGrop {
|
||||||
continue
|
continue
|
||||||
@ -268,12 +269,16 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
|
|||||||
case "all":
|
case "all":
|
||||||
newProxies = append(newProxies, proxyNames...)
|
newProxies = append(newProxies, proxyNames...)
|
||||||
case "countries":
|
case "countries":
|
||||||
newProxies = append(newProxies, countryGroupNames...)
|
if !igcg {
|
||||||
|
newProxies = append(newProxies, countryGroupNames...)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if len(key) == 2 {
|
if !igcg {
|
||||||
newProxies = append(
|
if len(key) == 2 {
|
||||||
newProxies, countryGroupMap[utils.GetContryName(key)].Proxies...,
|
newProxies = append(
|
||||||
)
|
newProxies, countryGroupMap[common.GetContryName(key)].Proxies...,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -282,5 +287,7 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
|
|||||||
}
|
}
|
||||||
temp.ProxyGroups[i].Proxies = newProxies
|
temp.ProxyGroups[i].Proxies = newProxies
|
||||||
}
|
}
|
||||||
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
|
if !igcg {
|
||||||
|
temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,18 +1,19 @@
|
|||||||
package controller
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sub2clash/config"
|
|
||||||
"sub2clash/model"
|
"github.com/nitezs/sub2clash/config"
|
||||||
"sub2clash/validator"
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
"github.com/nitezs/sub2clash/validator"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SubHandler(c *gin.Context) {
|
func SubHandler(c *gin.Context) {
|
||||||
// 从请求中获取参数
|
|
||||||
query, err := validator.ParseQuery(c)
|
query, err := validator.ParseQuery(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusBadRequest, err.Error())
|
c.String(http.StatusBadRequest, err.Error())
|
||||||
@ -23,7 +24,7 @@ func SubHandler(c *gin.Context) {
|
|||||||
c.String(http.StatusInternalServerError, err.Error())
|
c.String(http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 输出
|
|
||||||
if query.NodeListMode {
|
if query.NodeListMode {
|
||||||
nodelist := model.NodeList{}
|
nodelist := model.NodeList{}
|
||||||
nodelist.Proxies = sub.Proxies
|
nodelist.Proxies = sub.Proxies
|
199
api/handler/short_link.go
Normal file
199
api/handler/short_link.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/common"
|
||||||
|
"github.com/nitezs/sub2clash/common/database"
|
||||||
|
"github.com/nitezs/sub2clash/config"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
"github.com/nitezs/sub2clash/validator"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func respondWithError(c *gin.Context, code int, message string) {
|
||||||
|
c.String(code, message)
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateLinkHandler(c *gin.Context) {
|
||||||
|
var params validator.ShortLinkGenValidator
|
||||||
|
if err := c.ShouldBind(¶ms); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(params.Url) == "" {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "URL 不能为空")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash string
|
||||||
|
var password string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if params.CustomID != "" {
|
||||||
|
// 检查自定义ID是否已存在
|
||||||
|
exists, err := database.CheckShortLinkHashExists(params.CustomID)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "短链已存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash = params.CustomID
|
||||||
|
password = params.Password
|
||||||
|
} else {
|
||||||
|
// 自动生成短链ID和密码
|
||||||
|
hash, err = generateUniqueHash()
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "生成短链接失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if params.Password == "" {
|
||||||
|
password = common.RandomString(8) // 生成8位随机密码
|
||||||
|
} else {
|
||||||
|
password = params.Password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shortLink := model.ShortLink{
|
||||||
|
Hash: hash,
|
||||||
|
Url: params.Url,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.SaveShortLink(&shortLink); err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回生成的短链ID和密码
|
||||||
|
response := map[string]string{
|
||||||
|
"hash": hash,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateUniqueHash() (string, error) {
|
||||||
|
for {
|
||||||
|
hash := common.RandomString(config.Default.ShortLinkLength)
|
||||||
|
exists, err := database.CheckShortLinkHashExists(hash)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return hash, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateLinkHandler(c *gin.Context) {
|
||||||
|
var params validator.ShortLinkUpdateValidator
|
||||||
|
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "参数错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先获取原有的短链接
|
||||||
|
existingLink, err := database.FindShortLinkByHash(params.Hash)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusNotFound, "未找到短链接")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
if existingLink.Password != params.Password {
|
||||||
|
respondWithError(c, http.StatusUnauthorized, "密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新URL,但保持原密码不变
|
||||||
|
shortLink := model.ShortLink{
|
||||||
|
Hash: params.Hash,
|
||||||
|
Url: params.Url,
|
||||||
|
Password: existingLink.Password, // 保持原密码不变
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := database.SaveShortLink(&shortLink); err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusOK, "短链接更新成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRawConfHandler(c *gin.Context) {
|
||||||
|
|
||||||
|
hash := c.Param("hash")
|
||||||
|
password := c.Query("password")
|
||||||
|
|
||||||
|
if strings.TrimSpace(hash) == "" {
|
||||||
|
c.String(http.StatusBadRequest, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shortLink, err := database.FindShortLinkByHash(hash)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shortLink.Password != "" && shortLink.Password != password {
|
||||||
|
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shortLink.LastRequestTime = time.Now().Unix()
|
||||||
|
err = database.SaveShortLink(shortLink)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "数据库错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "请求错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
all, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
respondWithError(c, http.StatusInternalServerError, "读取错误: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusOK, string(all))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRawConfUriHandler(c *gin.Context) {
|
||||||
|
|
||||||
|
hash := c.Query("hash")
|
||||||
|
password := c.Query("password")
|
||||||
|
|
||||||
|
if strings.TrimSpace(hash) == "" {
|
||||||
|
c.String(http.StatusBadRequest, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shortLink, err := database.FindShortLinkByHash(hash)
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if shortLink.Password != "" && shortLink.Password != password {
|
||||||
|
c.String(http.StatusNotFound, "未找到短链接或密码错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusOK, shortLink.Url)
|
||||||
|
}
|
39
api/route.go
39
api/route.go
@ -5,9 +5,10 @@ import (
|
|||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sub2clash/api/controller"
|
|
||||||
"sub2clash/config"
|
"github.com/nitezs/sub2clash/api/handler"
|
||||||
"sub2clash/middleware"
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/middleware"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -18,7 +19,6 @@ var staticFiles embed.FS
|
|||||||
func SetRoute(r *gin.Engine) {
|
func SetRoute(r *gin.Engine) {
|
||||||
r.Use(middleware.ZapLogger())
|
r.Use(middleware.ZapLogger())
|
||||||
|
|
||||||
// 使用内嵌的模板文件
|
|
||||||
tpl, err := template.ParseFS(staticFiles, "static/*")
|
tpl, err := template.ParseFS(staticFiles, "static/*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error parsing templates: %v", err)
|
log.Fatalf("Error parsing templates: %v", err)
|
||||||
@ -32,10 +32,7 @@ func SetRoute(r *gin.Engine) {
|
|||||||
)
|
)
|
||||||
r.GET(
|
r.GET(
|
||||||
"/", func(c *gin.Context) {
|
"/", func(c *gin.Context) {
|
||||||
version := config.Version
|
version := constant.Version
|
||||||
if len(config.Version) > 7 {
|
|
||||||
version = config.Version[:7]
|
|
||||||
}
|
|
||||||
c.HTML(
|
c.HTML(
|
||||||
200, "index.html", gin.H{
|
200, "index.html", gin.H{
|
||||||
"Version": version,
|
"Version": version,
|
||||||
@ -43,24 +40,10 @@ func SetRoute(r *gin.Engine) {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
r.GET(
|
r.GET("/clash", handler.SubmodHandler)
|
||||||
"/clash", func(c *gin.Context) {
|
r.GET("/meta", handler.SubHandler)
|
||||||
controller.SubmodHandler(c)
|
r.GET("/s/:hash", handler.GetRawConfHandler)
|
||||||
},
|
r.POST("/short", handler.GenerateLinkHandler)
|
||||||
)
|
r.PUT("/short", handler.UpdateLinkHandler)
|
||||||
r.GET(
|
r.GET("/short", handler.GetRawConfUriHandler)
|
||||||
"/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 +1,185 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8" />
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||||
<title>sub2clash</title>
|
<title>sub2clash</title>
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link
|
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||||
crossorigin="anonymous"
|
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" rel="stylesheet" />
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
|
||||||
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<!-- Bootstrap JS -->
|
<!-- Bootstrap JS -->
|
||||||
<script
|
<script crossorigin="anonymous" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
||||||
crossorigin="anonymous"
|
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
|
||||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
|
||||||
></script>
|
|
||||||
<!-- Axios -->
|
<!-- Axios -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>
|
||||||
<script src="./static/index.js"></script>
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-xs {
|
.btn-xs {
|
||||||
padding: 2px 2px; /* 调整内边距以减小按钮大小 */
|
padding: 2px 2px;
|
||||||
font-size: 10px; /* 设置字体大小 */
|
/* 调整内边距以减小按钮大小 */
|
||||||
line-height: 1.2; /* 调整行高 */
|
font-size: 10px;
|
||||||
border-radius: 3px; /* 可选的边框半径调整 */
|
/* 设置字体大小 */
|
||||||
|
line-height: 1.2;
|
||||||
|
/* 调整行高 */
|
||||||
|
border-radius: 3px;
|
||||||
|
/* 可选的边框半径调整 */
|
||||||
height: 25px;
|
height: 25px;
|
||||||
width: 25px;
|
width: 25px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2>sub2clash</h2>
|
<h2>sub2clash</h2>
|
||||||
<span class="text-muted fst-italic"
|
<span class="text-muted fst-italic">通用订阅链接转 Clash(Meta) 配置工具
|
||||||
>通用订阅链接转 Clash(Meta) 配置工具
|
<a href="https://github.com/nitezs/sub2clash#clash-meta" target="_blank">使用文档</a></span><br /><span
|
||||||
<a
|
class="text-muted fst-italic">注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span>
|
||||||
href="https://github.com/nitezs/sub2clash#clash-meta"
|
</div>
|
||||||
target="_blank"
|
|
||||||
>使用文档</a
|
|
||||||
></span
|
|
||||||
><br><span class="text-muted fst-italic"
|
|
||||||
>注意:本程序非纯前端程序,输入的订阅将被后端缓存,请确保您信任当前站点</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Input URL -->
|
<!-- Input URL -->
|
||||||
<div class="form-group mb-5">
|
<div class="form-group mb-5">
|
||||||
<label for="apiLink">解析链接:</label>
|
<label for="apiLink">解析链接:</label>
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input
|
<input class="form-control" id="urlInput" type="text" placeholder="通过生成的链接重新填写下方设置" />
|
||||||
class="form-control"
|
<button class="btn btn-primary" onclick="parseInputURL()" type="button">
|
||||||
id="urlInput"
|
解析
|
||||||
type="text"
|
</button>
|
||||||
placeholder="通过生成的链接重新填写下方设置"
|
</div>
|
||||||
/>
|
</div>
|
||||||
<button
|
<!-- API Endpoint -->
|
||||||
class="btn btn-primary"
|
<div class="form-group mb-3">
|
||||||
onclick="parseInputURL()"
|
<label for="endpoint">客户端类型:</label>
|
||||||
type="button"
|
<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>
|
||||||
|
<!-- User Agent -->
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="user-agent">ua标识:</label>
|
||||||
|
<textarea class="form-control" id="user-agent" name="user-agent"
|
||||||
|
placeholder="用于获取订阅的http请求中的user-agent标识(可选)" rows="3"></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>
|
||||||
|
<!-- IgnoreCountryGroup -->
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" id="igcg" name="igcg" type="checkbox" />
|
||||||
|
<label class="form-check-label" for="igcg">不输出国家策略组</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>
|
</button>
|
||||||
</div>
|
</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 -->
|
<!-- Display the API Link -->
|
||||||
<div class="form-group mb-5">
|
<div class="form-group mb-5">
|
||||||
<label for="apiLink">配置链接:</label>
|
<label for="apiLink">配置链接:</label>
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
<input
|
<input class="form-control bg-light" id="apiLink" type="text" placeholder="链接" readonly style="cursor: not-allowed;" />
|
||||||
class="form-control"
|
<button class="btn btn-primary" onclick="copyToClipboard('apiLink',this)" type="button">
|
||||||
id="apiLink"
|
复制链接
|
||||||
readonly
|
</button>
|
||||||
type="text"
|
</div>
|
||||||
/>
|
<div class="input-group mb-2">
|
||||||
<button
|
<input class="form-control" id="customId" type="text" placeholder="短链ID(可选)" />
|
||||||
class="btn btn-primary"
|
<input class="form-control" id="password" type="text" placeholder="密码(可选)" />
|
||||||
onclick="generateURL()"
|
<button class="btn btn-primary" onclick="generateShortLink()" type="button">
|
||||||
type="button"
|
生成短链
|
||||||
>
|
</button>
|
||||||
生成链接
|
<button class="btn btn-primary" onclick="copyToClipboard('apiShortLink',this)" type="button">
|
||||||
</button>
|
复制短链
|
||||||
<button
|
</button>
|
||||||
class="btn btn-primary"
|
</div>
|
||||||
onclick="copyToClipboard('apiLink',this)"
|
<div class="input-group">
|
||||||
type="button"
|
<input class="form-control bg-light" id="apiShortLink" type="text" placeholder="短链接" readonly style="cursor: not-allowed;" />
|
||||||
>
|
<button class="btn btn-primary" onclick="updateShortLink()" type="button">
|
||||||
复制链接
|
更新短链
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</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>
|
</body>
|
||||||
</html>
|
<script src="./static/index.js"></script>
|
||||||
|
|
||||||
|
</html>
|
@ -1,335 +1,414 @@
|
|||||||
function clearExistingValues() {
|
function setInputReadOnly(input, readonly) {
|
||||||
// 清除简单输入框和复选框的值
|
if (readonly) {
|
||||||
document.getElementById("endpoint").value = "clash";
|
input.readOnly = true;
|
||||||
document.getElementById("sub").value = "";
|
input.classList.add('bg-light');
|
||||||
document.getElementById("proxy").value = "";
|
input.style.cursor = 'not-allowed';
|
||||||
document.getElementById("refresh").checked = false;
|
} else {
|
||||||
document.getElementById("autoTest").checked = false;
|
input.readOnly = false;
|
||||||
document.getElementById("lazy").checked = false;
|
input.classList.remove('bg-light');
|
||||||
document.getElementById("template").value = "";
|
input.style.cursor = 'auto';
|
||||||
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 创建的所有额外输入组
|
function clearExistingValues() {
|
||||||
clearInputGroup("ruleProviderGroup");
|
// 清除简单输入框和复选框的值
|
||||||
clearInputGroup("replaceGroup");
|
document.getElementById("endpoint").value = "clash";
|
||||||
clearInputGroup("ruleGroup");
|
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("igcg").checked = false;
|
||||||
|
document.getElementById("template").value = "";
|
||||||
|
document.getElementById("sort").value = "nameasc";
|
||||||
|
document.getElementById("remove").value = "";
|
||||||
|
document.getElementById("apiLink").value = "";
|
||||||
|
document.getElementById("apiShortLink").value = "";
|
||||||
|
|
||||||
|
// 恢复短链ID和密码输入框状态
|
||||||
|
const customIdInput = document.getElementById("customId");
|
||||||
|
const passwordInput = document.getElementById("password");
|
||||||
|
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
|
||||||
|
|
||||||
|
customIdInput.value = "";
|
||||||
|
setInputReadOnly(customIdInput, false);
|
||||||
|
|
||||||
|
passwordInput.value = "";
|
||||||
|
setInputReadOnly(passwordInput, false);
|
||||||
|
|
||||||
|
// 恢复生成短链按钮状态
|
||||||
|
generateButton.disabled = false;
|
||||||
|
generateButton.classList.remove('btn-secondary');
|
||||||
|
generateButton.classList.add('btn-primary');
|
||||||
|
|
||||||
|
document.getElementById("nodeList").checked = false;
|
||||||
|
|
||||||
|
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
|
||||||
|
clearInputGroup("ruleProviderGroup");
|
||||||
|
clearInputGroup("replaceGroup");
|
||||||
|
clearInputGroup("ruleGroup");
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateURI() {
|
function generateURI() {
|
||||||
const queryParams = [];
|
const queryParams = [];
|
||||||
|
|
||||||
// 获取 API Endpoint
|
// 获取 API Endpoint
|
||||||
const endpoint = document.getElementById("endpoint").value;
|
const endpoint = document.getElementById("endpoint").value;
|
||||||
|
|
||||||
// 获取并组合订阅链接
|
// 获取并组合订阅链接
|
||||||
let subLines = document
|
let subLines = document
|
||||||
.getElementById("sub")
|
.getElementById("sub")
|
||||||
.value.split("\n")
|
.value.split("\n")
|
||||||
.filter((line) => line.trim() !== "");
|
.filter((line) => line.trim() !== "");
|
||||||
let noSub = false;
|
let noSub = false;
|
||||||
// 去除 subLines 中空元素
|
// 去除 subLines 中空元素
|
||||||
subLines = subLines.map((item) => {
|
subLines = subLines.map((item) => {
|
||||||
if (item !== "") {
|
if (item !== "") {
|
||||||
return item;
|
return item;
|
||||||
}
|
|
||||||
});
|
|
||||||
if (subLines.length > 0) {
|
|
||||||
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
|
|
||||||
} else {
|
|
||||||
noSub = true;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
if (subLines.length > 0) {
|
||||||
|
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
|
||||||
|
} else {
|
||||||
|
noSub = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 获取并组合节点分享链接
|
// 获取并组合节点分享链接
|
||||||
let proxyLines = document
|
let proxyLines = document
|
||||||
.getElementById("proxy")
|
.getElementById("proxy")
|
||||||
.value.split("\n")
|
.value.split("\n")
|
||||||
.filter((line) => line.trim() !== "");
|
.filter((line) => line.trim() !== "");
|
||||||
let noProxy = false;
|
let noProxy = false;
|
||||||
// 去除 proxyLines 中空元素
|
// 去除 proxyLines 中空元素
|
||||||
proxyLines = proxyLines.map((item) => {
|
proxyLines = proxyLines.map((item) => {
|
||||||
if (item !== "") {
|
if (item !== "") {
|
||||||
return item;
|
return item;
|
||||||
}
|
|
||||||
});
|
|
||||||
if (proxyLines.length > 0) {
|
|
||||||
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
|
|
||||||
} else {
|
|
||||||
noProxy = true;
|
|
||||||
}
|
}
|
||||||
if (noSub && noProxy) {
|
});
|
||||||
alert("订阅链接和节点分享链接不能同时为空!");
|
if (proxyLines.length > 0) {
|
||||||
|
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
|
||||||
|
} else {
|
||||||
|
noProxy = true;
|
||||||
|
}
|
||||||
|
if (noSub && noProxy) {
|
||||||
|
// alert("订阅链接和节点分享链接不能同时为空!");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取订阅user-agent标识
|
||||||
|
const userAgent = document.getElementById("user-agent").value;
|
||||||
|
queryParams.push(`userAgent=${encodeURIComponent(userAgent)}`);
|
||||||
|
|
||||||
|
// 获取复选框的值
|
||||||
|
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 igcg = document.getElementById("igcg").checked;
|
||||||
|
queryParams.push(`ignoreCountryGroup=${igcg ? "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 / 2; i++) {
|
||||||
|
if (rules[i * 2].value.trim() !== "") {
|
||||||
|
let rule = rules[i * 2].value;
|
||||||
|
let prepend = rules[i * 2 + 1].value;
|
||||||
|
// 是否存在空值
|
||||||
|
if (rule.trim() === "" || prepend.trim() === "") {
|
||||||
|
// alert("Rule 中存在空值,请检查后重试!");
|
||||||
return "";
|
return "";
|
||||||
|
}
|
||||||
|
ruleList.push(`[${rule},${prepend}]`);
|
||||||
}
|
}
|
||||||
// 获取复选框的值
|
}
|
||||||
const refresh = document.getElementById("refresh").checked;
|
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
|
||||||
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;
|
const sort = document.getElementById("sort").value;
|
||||||
if (template.trim() !== "") {
|
queryParams.push(`sort=${sort}`);
|
||||||
queryParams.push(`template=${encodeURIComponent(template)}`);
|
|
||||||
|
// 获取删除节点的正则表达式
|
||||||
|
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(","))}`);
|
||||||
|
|
||||||
// 获取Rule Provider和规则
|
return `${endpoint}?${queryParams.join("&")}`;
|
||||||
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 解析为参数
|
// 将输入框中的 URL 解析为参数
|
||||||
function parseInputURL() {
|
async function parseInputURL() {
|
||||||
// 获取输入框中的 URL
|
// 获取输入框中的 URL
|
||||||
const inputURL = document.getElementById("urlInput").value;
|
const inputURL = document.getElementById("urlInput").value;
|
||||||
|
// 清除现有的输入框值
|
||||||
|
clearExistingValues();
|
||||||
|
if (!inputURL) {
|
||||||
|
alert("请输入有效的链接!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!inputURL) {
|
let url;
|
||||||
alert("请输入有效的链接!");
|
try {
|
||||||
return;
|
url = new URL(inputURL);
|
||||||
|
} catch (_) {
|
||||||
|
alert("无效的链接!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (url.pathname.includes("/s/")) {
|
||||||
|
let hash = url.pathname.substring(url.pathname.lastIndexOf("/s/") + 3);
|
||||||
|
let q = new URLSearchParams();
|
||||||
|
let password = url.searchParams.get("password");
|
||||||
|
if (password === null) {
|
||||||
|
alert("仅可解析加密短链");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
q.append("hash", hash);
|
||||||
let url;
|
q.append("password", password);
|
||||||
try {
|
try {
|
||||||
url = new URL(inputURL);
|
const response = await axios.get("./short?" + q.toString());
|
||||||
} catch (_) {
|
url = new URL(window.location.href + response.data);
|
||||||
alert("无效的链接!");
|
|
||||||
return;
|
// 回显配置链接
|
||||||
|
const apiLinkInput = document.querySelector("#apiLink");
|
||||||
|
apiLinkInput.value = `${window.location.origin}${window.location.pathname}${response.data}`;
|
||||||
|
setInputReadOnly(apiLinkInput, true);
|
||||||
|
|
||||||
|
// 回显短链相关信息
|
||||||
|
const apiShortLinkInput = document.querySelector("#apiShortLink");
|
||||||
|
apiShortLinkInput.value = inputURL;
|
||||||
|
setInputReadOnly(apiShortLinkInput, true);
|
||||||
|
|
||||||
|
// 设置短链ID和密码,并设置为只读
|
||||||
|
const customIdInput = document.querySelector("#customId");
|
||||||
|
const passwordInput = document.querySelector("#password");
|
||||||
|
const generateButton = document.querySelector('button[onclick="generateShortLink()"]');
|
||||||
|
|
||||||
|
customIdInput.value = hash;
|
||||||
|
setInputReadOnly(customIdInput, true);
|
||||||
|
|
||||||
|
passwordInput.value = password;
|
||||||
|
setInputReadOnly(passwordInput, true);
|
||||||
|
|
||||||
|
// 禁用生成短链按钮
|
||||||
|
generateButton.disabled = true;
|
||||||
|
generateButton.classList.add('btn-secondary');
|
||||||
|
generateButton.classList.remove('btn-primary');
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
alert("获取短链失败,请检查密码!");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
let params = new URLSearchParams(url.search);
|
||||||
|
|
||||||
// 清除现有的输入框值
|
// 分配值到对应的输入框
|
||||||
clearExistingValues();
|
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")) {
|
||||||
const params = new URLSearchParams(url.search);
|
document.getElementById("sub").value = decodeURIComponent(params.get("sub"))
|
||||||
|
.split(",")
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
// 分配值到对应的输入框
|
if (params.has("proxy")) {
|
||||||
const pathSections = url.pathname.split("/");
|
document.getElementById("proxy").value = decodeURIComponent(
|
||||||
const lastSection = pathSections[pathSections.length - 1];
|
params.get("proxy")
|
||||||
const clientTypeSelect = document.getElementById("endpoint");
|
)
|
||||||
switch (lastSection.toLowerCase()) {
|
.split(",")
|
||||||
case "meta":
|
.join("\n");
|
||||||
clientTypeSelect.value = "meta";
|
}
|
||||||
break;
|
|
||||||
case "clash":
|
|
||||||
default:
|
|
||||||
clientTypeSelect.value = "clash";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.has("sub")) {
|
if (params.has("refresh")) {
|
||||||
document.getElementById("sub").value = decodeURIComponent(
|
document.getElementById("refresh").checked =
|
||||||
params.get("sub")
|
params.get("refresh") === "true";
|
||||||
)
|
}
|
||||||
.split(",")
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.has("proxy")) {
|
if (params.has("autoTest")) {
|
||||||
document.getElementById("proxy").value = decodeURIComponent(
|
document.getElementById("autoTest").checked =
|
||||||
params.get("proxy")
|
params.get("autoTest") === "true";
|
||||||
)
|
}
|
||||||
.split(",")
|
|
||||||
.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.has("refresh")) {
|
if (params.has("lazy")) {
|
||||||
document.getElementById("refresh").checked =
|
document.getElementById("lazy").checked = params.get("lazy") === "true";
|
||||||
params.get("refresh") === "true";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (params.has("autoTest")) {
|
if (params.has("template")) {
|
||||||
document.getElementById("autoTest").checked =
|
document.getElementById("template").value = decodeURIComponent(
|
||||||
params.get("autoTest") === "true";
|
params.get("template")
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (params.has("lazy")) {
|
if (params.has("sort")) {
|
||||||
document.getElementById("lazy").checked = params.get("lazy") === "true";
|
document.getElementById("sort").value = params.get("sort");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.has("template")) {
|
if (params.has("remove")) {
|
||||||
document.getElementById("template").value = decodeURIComponent(
|
document.getElementById("remove").value = decodeURIComponent(
|
||||||
params.get("template")
|
params.get("remove")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.has("sort")) {
|
if (params.has("userAgent")) {
|
||||||
document.getElementById("sort").value = params.get("sort");
|
document.getElementById("user-agent").value = decodeURIComponent(
|
||||||
}
|
params.get("userAgent")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (params.has("remove")) {
|
if (params.has("ignoreCountryGroup")) {
|
||||||
document.getElementById("remove").value = decodeURIComponent(
|
document.getElementById("igcg").checked =
|
||||||
params.get("remove")
|
params.get("ignoreCountryGroup") === "true";
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (params.has("replace")) {
|
if (params.has("replace")) {
|
||||||
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
|
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.has("ruleProvider")) {
|
if (params.has("ruleProvider")) {
|
||||||
parseAndFillRuleProviderParams(
|
parseAndFillRuleProviderParams(
|
||||||
decodeURIComponent(params.get("ruleProvider"))
|
decodeURIComponent(params.get("ruleProvider"))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.has("rule")) {
|
if (params.has("rule")) {
|
||||||
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
|
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.has("nodeList")) {
|
if (params.has("nodeList")) {
|
||||||
document.getElementById("nodeList").checked =
|
document.getElementById("nodeList").checked =
|
||||||
params.get("nodeList") === "true";
|
params.get("nodeList") === "true";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearInputGroup(groupId) {
|
function clearInputGroup(groupId) {
|
||||||
// 清空第二个之后的child
|
// 清空第二个之后的child
|
||||||
const group = document.getElementById(groupId);
|
const group = document.getElementById(groupId);
|
||||||
while (group.children.length > 2) {
|
while (group.children.length > 2) {
|
||||||
group.removeChild(group.lastChild);
|
group.removeChild(group.lastChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAndFillReplaceParams(replaceParams) {
|
function parseAndFillReplaceParams(replaceParams) {
|
||||||
const replaceGroup = document.getElementById("replaceGroup");
|
const replaceGroup = document.getElementById("replaceGroup");
|
||||||
let matches;
|
let matches;
|
||||||
const regex = /\[(<.*?>),(<.*?>)\]/g;
|
const regex = /\[(<.*?>),(<.*?>)\]/g;
|
||||||
const str = decodeURIComponent(replaceParams);
|
const str = decodeURIComponent(replaceParams);
|
||||||
while ((matches = regex.exec(str)) !== null) {
|
while ((matches = regex.exec(str)) !== null) {
|
||||||
const div = createReplace();
|
const div = createReplace();
|
||||||
const original = matches[1].slice(1, -1); // Remove < and >
|
const original = matches[1].slice(1, -1); // Remove < and >
|
||||||
const replacement = matches[2].slice(1, -1); // Remove < and >
|
const replacement = matches[2].slice(1, -1); // Remove < and >
|
||||||
|
|
||||||
div.children[0].value = original;
|
div.children[0].value = original;
|
||||||
div.children[1].value = replacement;
|
div.children[1].value = replacement;
|
||||||
replaceGroup.appendChild(div);
|
replaceGroup.appendChild(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAndFillRuleProviderParams(ruleProviderParams) {
|
function parseAndFillRuleProviderParams(ruleProviderParams) {
|
||||||
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
|
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
|
||||||
let matches;
|
let matches;
|
||||||
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
|
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
|
||||||
const str = decodeURIComponent(ruleProviderParams);
|
const str = decodeURIComponent(ruleProviderParams);
|
||||||
while ((matches = regex.exec(str)) !== null) {
|
while ((matches = regex.exec(str)) !== null) {
|
||||||
const div = createRuleProvider();
|
const div = createRuleProvider();
|
||||||
div.children[0].value = matches[1];
|
div.children[0].value = matches[1];
|
||||||
div.children[1].value = matches[2];
|
div.children[1].value = matches[2];
|
||||||
div.children[2].value = matches[3];
|
div.children[2].value = matches[3];
|
||||||
div.children[3].value = matches[4];
|
div.children[3].value = matches[4];
|
||||||
div.children[4].value = matches[5];
|
div.children[4].value = matches[5];
|
||||||
ruleProviderGroup.appendChild(div);
|
ruleProviderGroup.appendChild(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAndFillRuleParams(ruleParams) {
|
function parseAndFillRuleParams(ruleParams) {
|
||||||
const ruleGroup = document.getElementById("ruleGroup");
|
const ruleGroup = document.getElementById("ruleGroup");
|
||||||
let matches;
|
let matches;
|
||||||
const regex = /\[(.*?),(.*?),(.*?)\]/g;
|
const regex = /\[(.*?),(.*?)\]/g;
|
||||||
const str = decodeURIComponent(ruleParams);
|
const str = decodeURIComponent(ruleParams);
|
||||||
while ((matches = regex.exec(str)) !== null) {
|
while ((matches = regex.exec(str)) !== null) {
|
||||||
const div = createRule();
|
const div = createRule();
|
||||||
div.children[0].value = matches[1];
|
div.children[0].value = matches[1];
|
||||||
div.children[1].value = matches[2];
|
div.children[1].value = matches[2];
|
||||||
div.children[2].value = matches[3];
|
ruleGroup.appendChild(div);
|
||||||
ruleGroup.appendChild(div);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyToClipboard(elem, e) {
|
async function copyToClipboard(elem, e) {
|
||||||
const apiLinkInput = document.querySelector(`#${elem}`).value;
|
const apiLinkInput = document.querySelector(`#${elem}`).value;
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(apiLinkInput);
|
await navigator.clipboard.writeText(apiLinkInput);
|
||||||
let text = e.textContent;
|
let text = e.textContent;
|
||||||
e.addEventListener("mouseout", function () {
|
e.addEventListener("mouseout", function () {
|
||||||
e.textContent = text;
|
e.textContent = text;
|
||||||
});
|
});
|
||||||
e.textContent = "复制成功";
|
e.textContent = "复制成功";
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("复制到剪贴板失败:", err);
|
console.error("复制到剪贴板失败:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRuleProvider() {
|
function createRuleProvider() {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.classList.add("input-group", "mb-2");
|
div.classList.add("input-group", "mb-2");
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Behavior">
|
<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="Url">
|
||||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Group">
|
<input type="text" class="form-control" name="ruleProvider" placeholder="Group">
|
||||||
@ -337,85 +416,168 @@ function createRuleProvider() {
|
|||||||
<input type="text" class="form-control" name="ruleProvider" placeholder="Name">
|
<input type="text" class="form-control" name="ruleProvider" placeholder="Name">
|
||||||
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||||
`;
|
`;
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReplace() {
|
function createReplace() {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.classList.add("input-group", "mb-2");
|
div.classList.add("input-group", "mb-2");
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
|
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
|
||||||
<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>
|
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||||
`;
|
`;
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRule() {
|
function createRule() {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.classList.add("input-group", "mb-2");
|
div.classList.add("input-group", "mb-2");
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<input type="text" class="form-control" name="rule" placeholder="Rule">
|
<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="Prepend">
|
||||||
<input type="text" class="form-control" name="rule" placeholder="Group">
|
|
||||||
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
|
||||||
`;
|
`;
|
||||||
return div;
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listenInput() {
|
||||||
|
let selectElements = document.querySelectorAll("select");
|
||||||
|
let inputElements = document.querySelectorAll("input");
|
||||||
|
let textAreaElements = document.querySelectorAll("textarea");
|
||||||
|
inputElements.forEach(function (element) {
|
||||||
|
element.addEventListener("input", function () {
|
||||||
|
generateURL();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
textAreaElements.forEach(function (element) {
|
||||||
|
element.addEventListener("input", function () {
|
||||||
|
generateURL();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
selectElements.forEach(function (element) {
|
||||||
|
element.addEventListener("change", function () {
|
||||||
|
generateURL();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRuleProvider() {
|
function addRuleProvider() {
|
||||||
const div = createRuleProvider();
|
const div = createRuleProvider();
|
||||||
document.getElementById("ruleProviderGroup").appendChild(div);
|
document.getElementById("ruleProviderGroup").appendChild(div);
|
||||||
|
listenInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRule() {
|
function addRule() {
|
||||||
const div = createRule();
|
const div = createRule();
|
||||||
document.getElementById("ruleGroup").appendChild(div);
|
document.getElementById("ruleGroup").appendChild(div);
|
||||||
|
listenInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addReplace() {
|
function addReplace() {
|
||||||
const div = createReplace();
|
const div = createReplace();
|
||||||
document.getElementById("replaceGroup").appendChild(div);
|
document.getElementById("replaceGroup").appendChild(div);
|
||||||
|
listenInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeElement(button) {
|
function removeElement(button) {
|
||||||
button.parentElement.remove();
|
button.parentElement.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateURL() {
|
function generateURL() {
|
||||||
const apiLink = document.getElementById("apiLink");
|
const apiLink = document.getElementById("apiLink");
|
||||||
let uri = generateURI();
|
let uri = generateURI();
|
||||||
if (uri === "") {
|
if (uri === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
|
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
|
||||||
|
setInputReadOnly(apiLink, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateShortLink() {
|
function generateShortLink() {
|
||||||
const apiShortLink = document.getElementById("apiShortLink");
|
const apiShortLink = document.getElementById("apiShortLink");
|
||||||
const password = document.getElementById("password");
|
const password = document.getElementById("password");
|
||||||
let uri = generateURI();
|
const customId = document.getElementById("customId");
|
||||||
if (uri === "") {
|
let uri = generateURI();
|
||||||
return;
|
if (uri === "") {
|
||||||
}
|
return;
|
||||||
axios
|
}
|
||||||
.post(
|
|
||||||
"./short",
|
axios
|
||||||
{
|
.post(
|
||||||
url: uri,
|
"./short",
|
||||||
password: password.value.trim(),
|
{
|
||||||
},
|
url: uri,
|
||||||
{
|
password: password.value.trim(),
|
||||||
headers: {
|
customId: customId.value.trim()
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
},
|
{
|
||||||
}
|
headers: {
|
||||||
)
|
"Content-Type": "application/json",
|
||||||
.then((response) => {
|
},
|
||||||
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
|
}
|
||||||
})
|
)
|
||||||
.catch((error) => {
|
.then((response) => {
|
||||||
console.log(error);
|
// 设置返回的短链ID和密码
|
||||||
alert("生成短链失败,请重试!");
|
customId.value = response.data.hash;
|
||||||
});
|
password.value = response.data.password;
|
||||||
|
// 生成完整的短链接
|
||||||
|
const shortLink = `${window.location.origin}${window.location.pathname}s/${response.data.hash}?password=${response.data.password}`;
|
||||||
|
apiShortLink.value = shortLink;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
alert(error.response.data);
|
||||||
|
} else {
|
||||||
|
alert("生成短链失败,请重试!");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateShortLink() {
|
||||||
|
const password = document.getElementById("password");
|
||||||
|
const apiShortLink = document.getElementById("apiShortLink");
|
||||||
|
let hash = apiShortLink.value;
|
||||||
|
if (hash.startsWith("http")) {
|
||||||
|
let u = new URL(hash);
|
||||||
|
hash = u.pathname.substring(u.pathname.lastIndexOf("/s/") + 3);
|
||||||
|
}
|
||||||
|
if (password.value.trim() === "") {
|
||||||
|
alert("请输入原密码进行验证!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let uri = generateURI();
|
||||||
|
if (uri === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios
|
||||||
|
.put(
|
||||||
|
"./short",
|
||||||
|
{
|
||||||
|
hash: hash,
|
||||||
|
url: uri,
|
||||||
|
password: password.value.trim(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((response) => {
|
||||||
|
alert(`短链 ${hash} 更新成功!`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
alert("密码错误,请输入正确的原密码!");
|
||||||
|
} else if (error.response && error.response.data) {
|
||||||
|
alert(error.response.data);
|
||||||
|
} else {
|
||||||
|
alert("更新短链失败,请重试!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listenInput();
|
||||||
|
69
common/database/database.go
Normal file
69
common/database/database.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var DB *bbolt.DB
|
||||||
|
|
||||||
|
func ConnectDB() error {
|
||||||
|
path := filepath.Join("data", "sub2clash.db")
|
||||||
|
|
||||||
|
db, err := bbolt.Open(path, 0600, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
DB = db
|
||||||
|
|
||||||
|
return db.Update(func(tx *bbolt.Tx) error {
|
||||||
|
_, err := tx.CreateBucketIfNotExists([]byte("ShortLinks"))
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindShortLinkByHash(hash string) (*model.ShortLink, error) {
|
||||||
|
var shortLink model.ShortLink
|
||||||
|
err := DB.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("ShortLinks"))
|
||||||
|
v := b.Get([]byte(hash))
|
||||||
|
if v == nil {
|
||||||
|
return errors.New("ShortLink not found")
|
||||||
|
}
|
||||||
|
return json.Unmarshal(v, &shortLink)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &shortLink, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveShortLink(shortLink *model.ShortLink) error {
|
||||||
|
return DB.Update(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("ShortLinks"))
|
||||||
|
encoded, err := json.Marshal(shortLink)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return b.Put([]byte(shortLink.Hash), encoded)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckShortLinkHashExists(hash string) (bool, error) {
|
||||||
|
exists := false
|
||||||
|
err := DB.View(func(tx *bbolt.Tx) error {
|
||||||
|
b := tx.Bucket([]byte("ShortLinks"))
|
||||||
|
v := b.Get([]byte(hash))
|
||||||
|
exists = v != nil
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return exists, nil
|
||||||
|
}
|
60
common/get.go
Normal file
60
common/get.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetConfig struct {
|
||||||
|
userAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetOption func(*GetConfig)
|
||||||
|
|
||||||
|
func WithUserAgent(userAgent string) GetOption {
|
||||||
|
return func(config *GetConfig) {
|
||||||
|
config.userAgent = userAgent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Get(url string, options ...GetOption) (resp *http.Response, err error) {
|
||||||
|
retryTimes := config.Default.RequestRetryTimes
|
||||||
|
haveTried := 0
|
||||||
|
retryDelay := time.Second
|
||||||
|
getConfig := GetConfig{}
|
||||||
|
for _, option := range options {
|
||||||
|
option(&getConfig)
|
||||||
|
}
|
||||||
|
var req *http.Request
|
||||||
|
var get *http.Response
|
||||||
|
for haveTried < retryTimes {
|
||||||
|
client := &http.Client{}
|
||||||
|
//client.Timeout = time.Second * 10
|
||||||
|
req, err = http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
haveTried++
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if getConfig.userAgent != "" {
|
||||||
|
req.Header.Set("User-Agent", getConfig.userAgent)
|
||||||
|
}
|
||||||
|
get, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
haveTried++
|
||||||
|
time.Sleep(retryDelay)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
|
||||||
|
return nil, errors.New("文件过大")
|
||||||
|
}
|
||||||
|
return get, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("请求失败:%v", err)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -26,5 +26,8 @@ func MkEssentialDir() error {
|
|||||||
if err := MKDir("logs"); err != nil {
|
if err := MKDir("logs"); err != nil {
|
||||||
return errors.New("create logs dir failed" + err.Error())
|
return errors.New("create logs dir failed" + err.Error())
|
||||||
}
|
}
|
||||||
|
if err := MKDir("data"); err != nil {
|
||||||
|
return errors.New("create data dir failed" + err.Error())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
@ -1,16 +1,18 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/logger"
|
|
||||||
"sub2clash/model"
|
"github.com/nitezs/sub2clash/constant"
|
||||||
"sub2clash/parser"
|
"github.com/nitezs/sub2clash/logger"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
"github.com/nitezs/sub2clash/parser"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetContryName(countryKey string) string {
|
func GetContryName(countryKey string) string {
|
||||||
// 创建一个切片包含所有的国家映射
|
|
||||||
countryMaps := []map[string]string{
|
countryMaps := []map[string]string{
|
||||||
model.CountryFlag,
|
model.CountryFlag,
|
||||||
model.CountryChineseName,
|
model.CountryChineseName,
|
||||||
@ -18,11 +20,9 @@ func GetContryName(countryKey string) string {
|
|||||||
model.CountryEnglishName,
|
model.CountryEnglishName,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 对每一个映射进行检查
|
|
||||||
for i, countryMap := range countryMaps {
|
for i, countryMap := range countryMaps {
|
||||||
if i == 2 {
|
if i == 2 {
|
||||||
// 对ISO匹配做特殊处理
|
|
||||||
// 根据常用分割字符分割字符串
|
|
||||||
splitChars := []string{"-", "_", " "}
|
splitChars := []string{"-", "_", " "}
|
||||||
key := make([]string, 0)
|
key := make([]string, 0)
|
||||||
for _, splitChar := range splitChars {
|
for _, splitChar := range splitChars {
|
||||||
@ -33,9 +33,9 @@ func GetContryName(countryKey string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 对每一个分割后的字符串进行检查
|
|
||||||
for _, v := range key {
|
for _, v := range key {
|
||||||
// 如果匹配到了国家
|
|
||||||
if country, ok := countryMap[strings.ToUpper(v)]; ok {
|
if country, ok := countryMap[strings.ToUpper(v)]; ok {
|
||||||
return country
|
return country
|
||||||
}
|
}
|
||||||
@ -55,7 +55,7 @@ func AddProxy(
|
|||||||
lazy bool, clashType model.ClashType, proxies ...model.Proxy,
|
lazy bool, clashType model.ClashType, proxies ...model.Proxy,
|
||||||
) {
|
) {
|
||||||
proxyTypes := model.GetSupportProxyTypes(clashType)
|
proxyTypes := model.GetSupportProxyTypes(clashType)
|
||||||
// 添加节点
|
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if !proxyTypes[proxy.Type] {
|
if !proxyTypes[proxy.Type] {
|
||||||
continue
|
continue
|
||||||
@ -65,17 +65,11 @@ func AddProxy(
|
|||||||
countryName := GetContryName(proxy.Name)
|
countryName := GetContryName(proxy.Name)
|
||||||
for i := range sub.ProxyGroups {
|
for i := range sub.ProxyGroups {
|
||||||
group := &sub.ProxyGroups[i]
|
group := &sub.ProxyGroups[i]
|
||||||
|
|
||||||
if group.Name == countryName {
|
if group.Name == countryName {
|
||||||
group.Proxies = append(group.Proxies, proxy.Name)
|
group.Proxies = append(group.Proxies, proxy.Name)
|
||||||
group.Size++
|
group.Size++
|
||||||
haveProxyGroup = true
|
haveProxyGroup = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if group.Name == "手动切换" {
|
|
||||||
group.Proxies = append(group.Proxies, proxy.Name)
|
|
||||||
group.Size++
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !haveProxyGroup {
|
if !haveProxyGroup {
|
||||||
var newGroup model.ProxyGroup
|
var newGroup model.ProxyGroup
|
||||||
@ -111,25 +105,31 @@ func ParseProxy(proxies ...string) []model.Proxy {
|
|||||||
if proxy != "" {
|
if proxy != "" {
|
||||||
var proxyItem model.Proxy
|
var proxyItem model.Proxy
|
||||||
var err error
|
var err error
|
||||||
// 解析节点
|
|
||||||
if strings.HasPrefix(proxy, "ss://") {
|
if strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
|
||||||
proxyItem, err = parser.ParseSS(proxy)
|
proxyItem, err = parser.ParseShadowsocks(proxy)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(proxy, "trojan://") {
|
if strings.HasPrefix(proxy, constant.TrojanPrefix) {
|
||||||
proxyItem, err = parser.ParseTrojan(proxy)
|
proxyItem, err = parser.ParseTrojan(proxy)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(proxy, "vmess://") {
|
if strings.HasPrefix(proxy, constant.VMessPrefix) {
|
||||||
proxyItem, err = parser.ParseVmess(proxy)
|
proxyItem, err = parser.ParseVmess(proxy)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(proxy, "vless://") {
|
if strings.HasPrefix(proxy, constant.VLESSPrefix) {
|
||||||
proxyItem, err = parser.ParseVless(proxy)
|
proxyItem, err = parser.ParseVless(proxy)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(proxy, "ssr://") {
|
if strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
|
||||||
proxyItem, err = parser.ParseShadowsocksR(proxy)
|
proxyItem, err = parser.ParseShadowsocksR(proxy)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(proxy, "hysteria2://") {
|
if strings.HasPrefix(proxy, constant.Hysteria2Prefix1) || strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
|
||||||
proxyItem, err = parser.ParseHysteria2(proxy)
|
proxyItem, err = parser.ParseHysteria2(proxy)
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(proxy, constant.HysteriaPrefix) {
|
||||||
|
proxyItem, err = parser.ParseHysteria(proxy)
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(proxy, constant.SocksPrefix) {
|
||||||
|
proxyItem, err = parser.ParseSocks(proxy)
|
||||||
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
result = append(result, proxyItem)
|
result = append(result, proxyItem)
|
||||||
} else {
|
} else {
|
@ -1,9 +1,9 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import "math/rand"
|
import "math/rand"
|
||||||
|
|
||||||
func RandomString(length int) string {
|
func RandomString(length int) string {
|
||||||
// 生成随机字符串
|
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
var result []byte
|
var result []byte
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
@ -1,9 +1,10 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/model"
|
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func PrependRuleProvider(
|
func PrependRuleProvider(
|
@ -1,23 +1,25 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sub2clash/config"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var subsDir = "subs"
|
var subsDir = "subs"
|
||||||
var fileLock sync.RWMutex
|
var fileLock sync.RWMutex
|
||||||
|
|
||||||
func LoadSubscription(url string, refresh bool) ([]byte, error) {
|
func LoadSubscription(url string, refresh bool, userAgent string) ([]byte, error) {
|
||||||
if refresh {
|
if refresh {
|
||||||
return FetchSubscriptionFromAPI(url)
|
return FetchSubscriptionFromAPI(url, userAgent)
|
||||||
}
|
}
|
||||||
hash := sha256.Sum224([]byte(url))
|
hash := sha256.Sum224([]byte(url))
|
||||||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||||||
@ -26,9 +28,9 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
|
|||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return FetchSubscriptionFromAPI(url)
|
return FetchSubscriptionFromAPI(url, userAgent)
|
||||||
}
|
}
|
||||||
lastGetTime := stat.ModTime().Unix() // 单位是秒
|
lastGetTime := stat.ModTime().Unix()
|
||||||
if lastGetTime+config.Default.CacheExpire > time.Now().Unix() {
|
if lastGetTime+config.Default.CacheExpire > time.Now().Unix() {
|
||||||
file, err := os.Open(fileName)
|
file, err := os.Open(fileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,21 +49,21 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
return subContent, nil
|
return subContent, nil
|
||||||
}
|
}
|
||||||
return FetchSubscriptionFromAPI(url)
|
return FetchSubscriptionFromAPI(url, userAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchSubscriptionFromAPI(url string) ([]byte, error) {
|
func FetchSubscriptionFromAPI(url string, userAgent string) ([]byte, error) {
|
||||||
hash := sha256.Sum224([]byte(url))
|
hash := sha256.Sum224([]byte(url))
|
||||||
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:]))
|
||||||
resp, err := Get(url)
|
resp, err := Get(url, WithUserAgent(userAgent))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer func(Body io.ReadCloser) {
|
defer func(resp *http.Response) {
|
||||||
if Body != nil {
|
if resp != nil && resp.Body != nil {
|
||||||
_ = Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
}(resp.Body)
|
}(resp)
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
@ -81,8 +83,5 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
|
return nil, fmt.Errorf("failed to write to sub.yaml: %w", err)
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
|
|
||||||
}
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -7,8 +7,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LoadTemplate 加载模板
|
|
||||||
// templates 模板文件名
|
|
||||||
func LoadTemplate(template string) ([]byte, error) {
|
func LoadTemplate(template string) ([]byte, error) {
|
||||||
tPath := filepath.Join("templates", template)
|
tPath := filepath.Join("templates", template)
|
||||||
if _, err := os.Stat(tPath); err == nil {
|
if _, err := os.Stat(tPath); err == nil {
|
||||||
@ -25,9 +23,6 @@ func LoadTemplate(template string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("模板文件不存在")
|
return nil, errors.New("模板文件不存在")
|
@ -1,9 +1,10 @@
|
|||||||
package utils
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sub2clash/config"
|
|
||||||
|
"github.com/nitezs/sub2clash/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeTemplate(path string, template string) error {
|
func writeTemplate(path string, template string) error {
|
@ -21,7 +21,6 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var Default *Config
|
var Default *Config
|
||||||
var Version string
|
|
||||||
var Dev string
|
var Dev string
|
||||||
|
|
||||||
func LoadConfig() error {
|
func LoadConfig() error {
|
||||||
|
13
constant/prefix.go
Normal file
13
constant/prefix.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
HysteriaPrefix string = "hysteria://"
|
||||||
|
Hysteria2Prefix1 string = "hysteria2://"
|
||||||
|
Hysteria2Prefix2 string = "hy2://"
|
||||||
|
ShadowsocksPrefix string = "ss://"
|
||||||
|
ShadowsocksRPrefix string = "ssr://"
|
||||||
|
TrojanPrefix string = "trojan://"
|
||||||
|
VLESSPrefix string = "vless://"
|
||||||
|
VMessPrefix string = "vmess://"
|
||||||
|
SocksPrefix string = "socks"
|
||||||
|
)
|
3
constant/version.go
Normal file
3
constant/version.go
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
var Version = "dev"
|
@ -4,7 +4,7 @@ services:
|
|||||||
sub2clash:
|
sub2clash:
|
||||||
container_name: sub2clash
|
container_name: sub2clash
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
image: ghcr.io/nitezs/sub2clash:latest
|
image: nite07/sub2clash:latest
|
||||||
ports:
|
ports:
|
||||||
- "8011:8011"
|
- "8011:8011"
|
||||||
volumes:
|
volumes:
|
||||||
|
53
go.mod
53
go.mod
@ -1,51 +1,42 @@
|
|||||||
module sub2clash
|
module github.com/nitezs/sub2clash
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/glebarez/sqlite v1.9.0
|
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
go.uber.org/zap v1.26.0
|
go.etcd.io/bbolt v1.3.9
|
||||||
golang.org/x/text v0.13.0
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/text v0.14.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/gorm v1.25.4
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.10.1 // indirect
|
github.com/bytedance/sonic v1.11.5 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
github.com/cloudwego/base64x v0.1.3 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.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.19.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
github.com/google/go-cmp v0.6.0 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.2.4 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.5.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
golang.org/x/crypto v0.13.0 // indirect
|
golang.org/x/crypto v0.22.0 // indirect
|
||||||
golang.org/x/net v0.15.0 // indirect
|
golang.org/x/net v0.24.0 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.19.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
|
||||||
modernc.org/mathutil v1.5.0 // indirect
|
|
||||||
modernc.org/memory v1.5.0 // indirect
|
|
||||||
modernc.org/sqlite v1.23.1 // indirect
|
|
||||||
)
|
)
|
||||||
|
125
go.sum
125
go.sum
@ -1,127 +1,102 @@
|
|||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k=
|
||||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw=
|
||||||
github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc=
|
github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY=
|
||||||
github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
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/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
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-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 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
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.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
|
||||||
github.com/go-playground/validator/v10 v10.15.4/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
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/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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
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/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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
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/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
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.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
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.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.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.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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/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.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.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.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
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/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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||||
|
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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
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/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
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.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||||
golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
|
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||||
|
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
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=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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=
|
|
||||||
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=
|
|
||||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
|
||||||
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
|
|
||||||
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=
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
|
@ -1,70 +1,52 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"os"
|
||||||
"sync"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"go.uber.org/zap/zapcore"
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var Logger *zap.Logger
|
||||||
Logger *zap.Logger
|
|
||||||
lock sync.Mutex
|
|
||||||
logLevel string
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitLogger(level string) {
|
func InitLogger(logLevel string) {
|
||||||
logLevel = level
|
logger := zap.New(buildZapCore(getZapLogLevel(logLevel)))
|
||||||
buildLogger()
|
Logger = logger
|
||||||
go rotateLogs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildLogger() {
|
func buildZapCore(logLevel zapcore.Level) zapcore.Core {
|
||||||
lock.Lock()
|
fileWriter := zapcore.AddSync(&lumberjack.Logger{
|
||||||
defer lock.Unlock()
|
Filename: "logs/app.log",
|
||||||
var level zapcore.Level
|
MaxSize: 500,
|
||||||
switch logLevel {
|
MaxBackups: 3,
|
||||||
case "error":
|
MaxAge: 28,
|
||||||
level = zap.ErrorLevel
|
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":
|
case "debug":
|
||||||
level = zap.DebugLevel
|
return zap.DebugLevel
|
||||||
case "warn":
|
case "warn":
|
||||||
level = zap.WarnLevel
|
return zap.WarnLevel
|
||||||
|
case "error":
|
||||||
|
return zap.ErrorLevel
|
||||||
case "info":
|
case "info":
|
||||||
level = zap.InfoLevel
|
return zap.InfoLevel
|
||||||
default:
|
default:
|
||||||
level = zap.InfoLevel
|
return 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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
main.go
47
main.go
@ -4,11 +4,12 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sub2clash/api"
|
|
||||||
"sub2clash/config"
|
"github.com/nitezs/sub2clash/api"
|
||||||
"sub2clash/logger"
|
"github.com/nitezs/sub2clash/common"
|
||||||
"sub2clash/utils"
|
"github.com/nitezs/sub2clash/common/database"
|
||||||
"sub2clash/utils/database"
|
"github.com/nitezs/sub2clash/config"
|
||||||
|
"github.com/nitezs/sub2clash/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -22,38 +23,24 @@ var templateClash string
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
// 创建文件夹
|
|
||||||
err = utils.MkEssentialDir()
|
err = common.MkEssentialDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Panic("create essential dir failed", zap.Error(err))
|
logger.Logger.Panic("create essential dir failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
// 加载配置
|
|
||||||
err = config.LoadConfig()
|
err = config.LoadConfig()
|
||||||
// 初始化日志
|
|
||||||
logger.InitLogger(config.Default.LogLevel)
|
logger.InitLogger(config.Default.LogLevel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Panic("load config failed", zap.Error(err))
|
logger.Logger.Panic("load config failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
// 检查更新
|
|
||||||
if config.Dev != "true" {
|
err = common.WriteDefalutTemplate(templateMeta, templateClash)
|
||||||
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 {
|
if err != nil {
|
||||||
logger.Logger.Panic("write default template failed", zap.Error(err))
|
logger.Logger.Panic("write default template failed", zap.Error(err))
|
||||||
}
|
}
|
||||||
// 连接数据库
|
|
||||||
err = database.ConnectDB()
|
err = database.ConnectDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Panic("database connect failed", zap.Error(err))
|
logger.Logger.Panic("database connect failed", zap.Error(err))
|
||||||
@ -62,13 +49,13 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 设置运行模式
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
// 关闭 Gin 的日志输出
|
|
||||||
gin.DefaultWriter = io.Discard
|
gin.DefaultWriter = io.Discard
|
||||||
// 创建路由
|
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
// 设置路由
|
|
||||||
api.SetRoute(r)
|
api.SetRoute(r)
|
||||||
logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port))
|
logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port))
|
||||||
err := r.Run(":" + strconv.Itoa(config.Default.Port))
|
err := r.Run(":" + strconv.Itoa(config.Default.Port))
|
||||||
|
@ -2,9 +2,10 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sub2clash/logger"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
@ -14,6 +14,7 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool {
|
|||||||
"ssr": true,
|
"ssr": true,
|
||||||
"vmess": true,
|
"vmess": true,
|
||||||
"trojan": true,
|
"trojan": true,
|
||||||
|
"socks5": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if clashType == ClashMeta {
|
if clashType == ClashMeta {
|
||||||
@ -23,7 +24,9 @@ func GetSupportProxyTypes(clashType ClashType) map[string]bool {
|
|||||||
"vmess": true,
|
"vmess": true,
|
||||||
"trojan": true,
|
"trojan": true,
|
||||||
"vless": true,
|
"vless": true,
|
||||||
|
"hysteria": true,
|
||||||
"hysteria2": true,
|
"hysteria2": true,
|
||||||
|
"socks5": true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
package model
|
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{
|
var CountryEnglishName = map[string]string{
|
||||||
"Andorra": "安道尔(AD)",
|
"Andorra": "安道尔(AD)",
|
||||||
"United Arab Emirates": "阿联酋(AE)",
|
"United Arab Emirates": "阿联酋(AE)",
|
||||||
|
125
model/proxy.go
125
model/proxy.go
@ -5,53 +5,83 @@ type SmuxStruct struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
Name string `yaml:"name,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Server string `yaml:"server,omitempty"`
|
Server string `yaml:"server,omitempty"`
|
||||||
Port int `yaml:"port,omitempty"`
|
Port int `yaml:"port,omitempty"`
|
||||||
Type string `yaml:"type,omitempty"`
|
Type string `yaml:"type,omitempty"`
|
||||||
Cipher string `yaml:"cipher,omitempty"`
|
Cipher string `yaml:"cipher,omitempty"`
|
||||||
Password string `yaml:"password,omitempty"`
|
Username string `yaml:"username,omitempty"`
|
||||||
UDP bool `yaml:"udp,omitempty"`
|
Password string `yaml:"password,omitempty"`
|
||||||
UUID string `yaml:"uuid,omitempty"`
|
UDP bool `yaml:"udp,omitempty"`
|
||||||
Network string `yaml:"network,omitempty"`
|
UUID string `yaml:"uuid,omitempty"`
|
||||||
Flow string `yaml:"flow,omitempty"`
|
Network string `yaml:"network,omitempty"`
|
||||||
TLS bool `yaml:"tls,omitempty"`
|
Flow string `yaml:"flow,omitempty"`
|
||||||
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
TLS bool `yaml:"tls,omitempty"`
|
||||||
Plugin string `yaml:"plugin,omitempty"`
|
ClientFingerprint string `yaml:"client-fingerprint,omitempty"`
|
||||||
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
Plugin string `yaml:"plugin,omitempty"`
|
||||||
Smux SmuxStruct `yaml:"smux,omitempty"`
|
PluginOpts map[string]any `yaml:"plugin-opts,omitempty"`
|
||||||
Sni string `yaml:"sni,omitempty"`
|
Smux SmuxStruct `yaml:"smux,omitempty"`
|
||||||
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
|
Sni string `yaml:"sni,omitempty"`
|
||||||
Fingerprint string `yaml:"fingerprint,omitempty"`
|
AllowInsecure bool `yaml:"allow-insecure,omitempty"`
|
||||||
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
Fingerprint string `yaml:"fingerprint,omitempty"`
|
||||||
Alpn []string `yaml:"alpn,omitempty"`
|
SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"`
|
||||||
XUDP bool `yaml:"xudp,omitempty"`
|
Alpn []string `yaml:"alpn,omitempty"`
|
||||||
Servername string `yaml:"servername,omitempty"`
|
XUDP bool `yaml:"xudp,omitempty"`
|
||||||
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
Servername string `yaml:"servername,omitempty"`
|
||||||
AlterID int `yaml:"alterId,omitempty"`
|
WSOpts WSOptions `yaml:"ws-opts,omitempty"`
|
||||||
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
AlterID int `yaml:"alterId,omitempty"`
|
||||||
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
GrpcOpts GrpcOptions `yaml:"grpc-opts,omitempty"`
|
||||||
Protocol string `yaml:"protocol,omitempty"`
|
RealityOpts RealityOptions `yaml:"reality-opts,omitempty"`
|
||||||
Obfs string `yaml:"obfs,omitempty"`
|
Protocol string `yaml:"protocol,omitempty"`
|
||||||
ObfsParam string `yaml:"obfs-param,omitempty"`
|
Obfs string `yaml:"obfs,omitempty"`
|
||||||
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
ObfsParam string `yaml:"obfs-param,omitempty"`
|
||||||
Remarks []string `yaml:"remarks,omitempty"`
|
ProtocolParam string `yaml:"protocol-param,omitempty"`
|
||||||
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
Remarks []string `yaml:"remarks,omitempty"`
|
||||||
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
HTTPOpts HTTPOptions `yaml:"http-opts,omitempty"`
|
||||||
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
HTTP2Opts HTTP2Options `yaml:"h2-opts,omitempty"`
|
||||||
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
PacketAddr bool `yaml:"packet-addr,omitempty"`
|
||||||
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
PacketEncoding string `yaml:"packet-encoding,omitempty"`
|
||||||
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
GlobalPadding bool `yaml:"global-padding,omitempty"`
|
||||||
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
AuthenticatedLength bool `yaml:"authenticated-length,omitempty"`
|
||||||
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
UDPOverTCP bool `yaml:"udp-over-tcp,omitempty"`
|
||||||
SubName string `yaml:"-"`
|
UDPOverTCPVersion int `yaml:"udp-over-tcp-version,omitempty"`
|
||||||
Up string `yaml:"up,omitempty"`
|
SubName string `yaml:"-"`
|
||||||
Down string `yaml:"down,omitempty"`
|
Up string `yaml:"up,omitempty"`
|
||||||
CustomCA string `yaml:"ca,omitempty"`
|
Down string `yaml:"down,omitempty"`
|
||||||
CustomCAString string `yaml:"ca-str,omitempty"`
|
CustomCA string `yaml:"ca,omitempty"`
|
||||||
CWND int `yaml:"cwnd,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"`
|
||||||
|
Ports string `yaml:"ports,omitempty"`
|
||||||
|
AuthStringOLD string `yaml:"auth_str,omitempty"`
|
||||||
|
AuthString string `yaml:"auth-str,omitempty"`
|
||||||
|
Ip string `yaml:"ip,omitempty"`
|
||||||
|
Ipv6 string `yaml:"ipv6,omitempty"`
|
||||||
|
PrivateKey string `yaml:"private-key,omitempty"`
|
||||||
|
Workers int `yaml:"workers,omitempty"`
|
||||||
|
MTU int `yaml:"mtu,omitempty"`
|
||||||
|
PersistentKeepalive int `yaml:"persistent-keepalive,omitempty"`
|
||||||
|
Peers []WireGuardPeerOption `yaml:"peers,omitempty"`
|
||||||
|
RemoteDnsResolve bool `yaml:"remote-dns-resolve,omitempty"`
|
||||||
|
Dns []string `yaml:"dns,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 _Proxy Proxy
|
||||||
|
|
||||||
func (p Proxy) MarshalYAML() (interface{}, error) {
|
func (p Proxy) MarshalYAML() (interface{}, error) {
|
||||||
switch p.Type {
|
switch p.Type {
|
||||||
case "vmess":
|
case "vmess":
|
||||||
@ -64,8 +94,11 @@ func (p Proxy) MarshalYAML() (interface{}, error) {
|
|||||||
return ProxyToVless(p), nil
|
return ProxyToVless(p), nil
|
||||||
case "trojan":
|
case "trojan":
|
||||||
return ProxyToTrojan(p), nil
|
return ProxyToTrojan(p), nil
|
||||||
|
case "hysteria":
|
||||||
|
return ProxyToHysteria(p), nil
|
||||||
case "hysteria2":
|
case "hysteria2":
|
||||||
return ProxyToHysteria2(p), nil
|
return ProxyToHysteria2(p), nil
|
||||||
|
default:
|
||||||
|
return _Proxy(p), nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
@ -6,98 +6,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ProxyGroup struct {
|
type ProxyGroup struct {
|
||||||
Name string `yaml:"name,omitempty"`
|
Type string `yaml:"type,omitempty"`
|
||||||
Type string `yaml:"type,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Proxies []string `yaml:"proxies,omitempty"`
|
Proxies []string `yaml:"proxies,omitempty"`
|
||||||
IsCountryGrop bool `yaml:"-"`
|
IsCountryGrop bool `yaml:"-"`
|
||||||
Url string `yaml:"url,omitempty"`
|
Url string `yaml:"url,omitempty"`
|
||||||
Interval int `yaml:"interval,omitempty"`
|
Interval int `yaml:"interval,omitempty"`
|
||||||
Tolerance int `yaml:"tolerance,omitempty"`
|
Tolerance int `yaml:"tolerance,omitempty"`
|
||||||
Lazy bool `yaml:"lazy"`
|
Lazy bool `yaml:"lazy"`
|
||||||
Size int `yaml:"-"`
|
Size int `yaml:"-"`
|
||||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
||||||
Strategy string `yaml:"strategy,omitempty"`
|
Strategy string `yaml:"strategy,omitempty"`
|
||||||
}
|
Icon string `yaml:"icon,omitempty"`
|
||||||
|
Timeout int `yaml:"timeout,omitempty"`
|
||||||
type SelectProxyGroup struct {
|
Use []string `yaml:"use,omitempty"`
|
||||||
Name string `yaml:"name,omitempty"`
|
InterfaceName string `yaml:"interface-name,omitempty"`
|
||||||
Type string `yaml:"type,omitempty"`
|
RoutingMark int `yaml:"routing-mark,omitempty"`
|
||||||
Proxies []string `yaml:"proxies,omitempty"`
|
IncludeAll bool `yaml:"include-all,omitempty"`
|
||||||
DisableUDP bool `yaml:"disable-udp,omitempty"`
|
IncludeAllProxies bool `yaml:"include-all-proxies,omitempty"`
|
||||||
}
|
IncludeAllProviders bool `yaml:"include-all-providers,omitempty"`
|
||||||
|
Filter string `yaml:"filter,omitempty"`
|
||||||
type UrlTestProxyGroup struct {
|
ExcludeFilter string `yaml:"exclude-filter,omitempty"`
|
||||||
Name string `yaml:"name,omitempty"`
|
ExpectedStatus int `yaml:"expected-status,omitempty"`
|
||||||
Type string `yaml:"type,omitempty"`
|
Hidden bool `yaml:"hidden,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 ProxyGroupsSortByName []ProxyGroup
|
||||||
@ -111,16 +42,15 @@ func (p ProxyGroupsSortBySize) Len() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p ProxyGroupsSortByName) Less(i, j int) bool {
|
func (p ProxyGroupsSortByName) Less(i, j int) bool {
|
||||||
// 定义一组备选语言:首选英语,其次中文
|
|
||||||
tags := []language.Tag{
|
tags := []language.Tag{
|
||||||
language.English,
|
language.English,
|
||||||
language.Chinese,
|
language.Chinese,
|
||||||
}
|
}
|
||||||
matcher := language.NewMatcher(tags)
|
matcher := language.NewMatcher(tags)
|
||||||
|
|
||||||
// 假设我们的请求语言是 "zh"(中文),则使用匹配器找到最佳匹配的语言
|
|
||||||
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
bestMatch, _, _ := matcher.Match(language.Make("zh"))
|
||||||
// 使用最佳匹配的语言进行排序
|
|
||||||
c := collate.New(bestMatch)
|
c := collate.New(bestMatch)
|
||||||
return c.CompareString(p[i].Name, p[j].Name) < 0
|
return c.CompareString(p[i].Name, p[j].Name) < 0
|
||||||
}
|
}
|
||||||
|
58
model/proxy_hysteria.go
Normal file
58
model/proxy_hysteria.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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"`
|
||||||
|
Up string `yaml:"up"`
|
||||||
|
UpSpeed int `yaml:"up-speed,omitempty"`
|
||||||
|
Down string `yaml:"down"`
|
||||||
|
DownSpeed int `yaml:"down-speed,omitempty"`
|
||||||
|
Auth string `yaml:"auth,omitempty"`
|
||||||
|
AuthStringOLD string `yaml:"auth_str,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,
|
||||||
|
Ports: p.Ports,
|
||||||
|
Protocol: p.Protocol,
|
||||||
|
Up: p.Up,
|
||||||
|
Down: p.Down,
|
||||||
|
Auth: p.Auth,
|
||||||
|
AuthStringOLD: p.AuthStringOLD,
|
||||||
|
AuthString: p.AuthString,
|
||||||
|
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,18 +1,18 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type HTTPOptions struct {
|
type HTTPOptions struct {
|
||||||
Method string `proxy:"method,omitempty"`
|
Method string `yaml:"method,omitempty"`
|
||||||
Path []string `proxy:"path,omitempty"`
|
Path []string `yaml:"path,omitempty"`
|
||||||
Headers map[string][]string `proxy:"headers,omitempty"`
|
Headers map[string][]string `yaml:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP2Options struct {
|
type HTTP2Options struct {
|
||||||
Host []string `proxy:"host,omitempty"`
|
Host []string `yaml:"host,omitempty"`
|
||||||
Path string `proxy:"path,omitempty"`
|
Path string `yaml:"path,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GrpcOptions struct {
|
type GrpcOptions struct {
|
||||||
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
|
GrpcServiceName string `yaml:"grpc-service-name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RealityOptions struct {
|
type RealityOptions struct {
|
||||||
@ -21,10 +21,10 @@ type RealityOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WSOptions struct {
|
type WSOptions struct {
|
||||||
Path string `proxy:"path,omitempty"`
|
Path string `yaml:"path,omitempty"`
|
||||||
Headers map[string]string `proxy:"headers,omitempty"`
|
Headers map[string]string `yaml:"headers,omitempty"`
|
||||||
MaxEarlyData int `proxy:"max-early-data,omitempty"`
|
MaxEarlyData int `yaml:"max-early-data,omitempty"`
|
||||||
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
|
EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type VmessJson struct {
|
type VmessJson struct {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type ShortLink struct {
|
type ShortLink struct {
|
||||||
Hash string `gorm:"primary_key"`
|
Hash string
|
||||||
Url string
|
Url string
|
||||||
Password string
|
Password string
|
||||||
LastRequestTime int64
|
LastRequestTime int64
|
||||||
|
218
model/sub.go
218
model/sub.go
@ -1,18 +1,210 @@
|
|||||||
package model
|
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 {
|
type NodeList struct {
|
||||||
Proxies []Proxy `yaml:"proxies,omitempty"`
|
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subscription struct {
|
||||||
|
Port int `yaml:"port,omitempty" json:"port"`
|
||||||
|
SocksPort int `yaml:"socks-port,omitempty" json:"socks-port"`
|
||||||
|
RedirPort int `yaml:"redir-port,omitempty" json:"redir-port"`
|
||||||
|
TProxyPort int `yaml:"tproxy-port,omitempty" json:"tproxy-port"`
|
||||||
|
MixedPort int `yaml:"mixed-port,omitempty" json:"mixed-port"`
|
||||||
|
ShadowSocksConfig string `yaml:"ss-config,omitempty"`
|
||||||
|
VmessConfig string `yaml:"vmess-config,omitempty"`
|
||||||
|
InboundTfo bool `yaml:"inbound-tfo,omitempty"`
|
||||||
|
InboundMPTCP bool `yaml:"inbound-mptcp,omitempty"`
|
||||||
|
Authentication []string `yaml:"authentication,omitempty" json:"authentication"`
|
||||||
|
SkipAuthPrefixes []string `yaml:"skip-auth-prefixes,omitempty"`
|
||||||
|
LanAllowedIPs []string `yaml:"lan-allowed-ips,omitempty"`
|
||||||
|
LanDisAllowedIPs []string `yaml:"lan-disallowed-ips,omitempty"`
|
||||||
|
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"`
|
||||||
|
ExternalControllerTLS string `yaml:"external-controller-tls,omitempty"`
|
||||||
|
ExternalUI string `yaml:"external-ui,omitempty"`
|
||||||
|
ExternalUIURL string `yaml:"external-ui-url,omitempty" json:"external-ui-url"`
|
||||||
|
ExternalUIName string `yaml:"external-ui-name,omitempty" json:"external-ui-name"`
|
||||||
|
Secret string `yaml:"secret,omitempty"`
|
||||||
|
Interface string `yaml:"interface-name,omitempty"`
|
||||||
|
RoutingMark int `yaml:"routing-mark,omitempty"`
|
||||||
|
//Tunnels []LC.Tunnel `yaml:"tunnels,omitempty"`
|
||||||
|
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"`
|
||||||
|
GlobalUA string `yaml:"global-ua,omitempty"`
|
||||||
|
KeepAliveInterval int `yaml:"keep-alive-interval,omitempty"`
|
||||||
|
|
||||||
|
Sniffer RawSniffer `yaml:"sniffer,omitempty" json:"sniffer"`
|
||||||
|
ProxyProvider map[string]map[string]any `yaml:"proxy-providers,omitempty"`
|
||||||
|
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty"`
|
||||||
|
Hosts map[string]any `yaml:"hosts,omitempty" json:"hosts"`
|
||||||
|
NTP RawNTP `yaml:"ntp,omitempty" json:"ntp"`
|
||||||
|
DNS RawDNS `yaml:"dns,omitempty" json:"dns"`
|
||||||
|
Tun RawTun `yaml:"tun,omitempty"`
|
||||||
|
TuicServer RawTuicServer `yaml:"tuic-server,omitempty"`
|
||||||
|
EBpf EBpf `yaml:"ebpf,omitempty"`
|
||||||
|
IPTables IPTables `yaml:"iptables,omitempty"`
|
||||||
|
Experimental Experimental `yaml:"experimental,omitempty"`
|
||||||
|
Profile Profile `yaml:"profile,omitempty"`
|
||||||
|
GeoXUrl GeoXUrl `yaml:"geox-url,omitempty"`
|
||||||
|
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||||
|
ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"`
|
||||||
|
Rules []string `yaml:"rules,omitempty"`
|
||||||
|
SubRules map[string][]string `yaml:"sub-rules,omitempty"`
|
||||||
|
RawTLS TLS `yaml:"tls,omitempty"`
|
||||||
|
Listeners []map[string]any `yaml:"listeners,omitempty"`
|
||||||
|
|
||||||
|
ClashForAndroid RawClashForAndroid `yaml:"clash-for-android,omitempty" json:"clash-for-android"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawClashForAndroid struct {
|
||||||
|
AppendSystemDNS bool `yaml:"append-system-dns,omitempty" json:"append-system-dns"`
|
||||||
|
UiSubtitlePattern string `yaml:"ui-subtitle-pattern,omitempty" json:"ui-subtitle-pattern"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TLS struct {
|
||||||
|
Certificate string `yaml:"certificate,omitempty"`
|
||||||
|
PrivateKey string `yaml:"private-key,omitempty"`
|
||||||
|
CustomTrustCert []string `yaml:"custom-certifactes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeoXUrl struct {
|
||||||
|
GeoIp string `yaml:"geoip,omitempty" json:"geoip"`
|
||||||
|
Mmdb string `yaml:"mmdb,omitempty" json:"mmdb"`
|
||||||
|
GeoSite string `yaml:"geosite,omitempty" json:"geosite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Experimental struct {
|
||||||
|
Fingerprints []string `yaml:"fingerprints,omitempty"`
|
||||||
|
QUICGoDisableGSO bool `yaml:"quic-go-disable-gso,omitempty"`
|
||||||
|
QUICGoDisableECN bool `yaml:"quic-go-disable-ecn,omitempty"`
|
||||||
|
IP4PEnable bool `yaml:"dialer-ip4p-convert,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Profile struct {
|
||||||
|
StoreSelected bool `yaml:"store-selected,omitempty"`
|
||||||
|
StoreFakeIP bool `yaml:"store-fake-ip,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPTables struct {
|
||||||
|
Enable bool `yaml:"enable,omitempty" json:"enable"`
|
||||||
|
InboundInterface string `yaml:"inbound-interface,omitempty" json:"inbound-interface"`
|
||||||
|
Bypass []string `yaml:"bypass,omitempty" json:"bypass"`
|
||||||
|
DnsRedirect bool `yaml:"dns-redirect,omitempty" json:"dns-redirect"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EBpf struct {
|
||||||
|
RedirectToTun []string `yaml:"redirect-to-tun,omitempty" json:"redirect-to-tun"`
|
||||||
|
AutoRedir []string `yaml:"auto-redir,omitempty" json:"auto-redir"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawSniffer struct {
|
||||||
|
Enable bool `yaml:"enable,omitempty" json:"enable"`
|
||||||
|
OverrideDest bool `yaml:"override-destination,omitempty" json:"override-destination"`
|
||||||
|
Sniffing []string `yaml:"sniffing,omitempty" json:"sniffing"`
|
||||||
|
ForceDomain []string `yaml:"force-domain,omitempty" json:"force-domain"`
|
||||||
|
SkipDomain []string `yaml:"skip-domain,omitempty" json:"skip-domain"`
|
||||||
|
Ports []string `yaml:"port-whitelist,omitempty" json:"port-whitelist"`
|
||||||
|
ForceDnsMapping bool `yaml:"force-dns-mapping,omitempty" json:"force-dns-mapping"`
|
||||||
|
ParsePureIp bool `yaml:"parse-pure-ip,omitempty" json:"parse-pure-ip"`
|
||||||
|
Sniff map[string]RawSniffingConfig `yaml:"sniff,omitempty" json:"sniff"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawSniffingConfig struct {
|
||||||
|
Ports []string `yaml:"ports,omitempty" json:"ports"`
|
||||||
|
OverrideDest *bool `yaml:"override-destination,omitempty" json:"override-destination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawNTP struct {
|
||||||
|
Enable bool `yaml:"enable,omitempty"`
|
||||||
|
Server string `yaml:"server,omitempty"`
|
||||||
|
ServerPort int `yaml:"server-port,omitempty"`
|
||||||
|
Interval int `yaml:"interval,omitempty"`
|
||||||
|
DialerProxy string `yaml:"dialer-proxy,omitempty"`
|
||||||
|
WriteToSystem bool `yaml:"write-to-system,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawDNS struct {
|
||||||
|
Enable bool `yaml:"enable,omitempty" json:"enable"`
|
||||||
|
PreferH3 bool `yaml:"prefer-h3,omitempty" json:"prefer-h3"`
|
||||||
|
IPv6 bool `yaml:"ipv6,omitempty" json:"ipv6"`
|
||||||
|
IPv6Timeout uint `yaml:"ipv6-timeout,omitempty" json:"ipv6-timeout"`
|
||||||
|
UseHosts bool `yaml:"use-hosts,omitempty" json:"use-hosts"`
|
||||||
|
NameServer []string `yaml:"nameserver,omitempty" json:"nameserver"`
|
||||||
|
Fallback []string `yaml:"fallback,omitempty" json:"fallback"`
|
||||||
|
FallbackFilter RawFallbackFilter `yaml:"fallback-filter,omitempty" json:"fallback-filter"`
|
||||||
|
Listen string `yaml:"listen,omitempty" json:"listen"`
|
||||||
|
EnhancedMode string `yaml:"enhanced-mode,omitempty" json:"enhanced-mode"`
|
||||||
|
FakeIPRange string `yaml:"fake-ip-range,omitempty" json:"fake-ip-range"`
|
||||||
|
FakeIPFilter []string `yaml:"fake-ip-filter,omitempty" json:"fake-ip-filter"`
|
||||||
|
DefaultNameserver []string `yaml:"default-nameserver,omitempty" json:"default-nameserver"`
|
||||||
|
CacheAlgorithm string `yaml:"cache-algorithm,omitempty" json:"cache-algorithm"`
|
||||||
|
//NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy,omitempty" json:"nameserver-policy"`
|
||||||
|
ProxyServerNameserver []string `yaml:"proxy-server-nameserver,omitempty" json:"proxy-server-nameserver"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawFallbackFilter struct {
|
||||||
|
GeoIP bool `yaml:"geoip,omitempty" json:"geoip"`
|
||||||
|
GeoIPCode string `yaml:"geoip-code,omitempty" json:"geoip-code"`
|
||||||
|
IPCIDR []string `yaml:"ipcidr,omitempty" json:"ipcidr"`
|
||||||
|
Domain []string `yaml:"domain,omitempty" json:"domain"`
|
||||||
|
GeoSite []string `yaml:"geosite,omitempty" json:"geosite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawTun struct {
|
||||||
|
Enable bool `yaml:"enable,omitempty" json:"enable"`
|
||||||
|
Device string `yaml:"device,omitempty" json:"device"`
|
||||||
|
Stack string `yaml:"stack,omitempty" json:"stack"`
|
||||||
|
DNSHijack []string `yaml:"dns-hijack,omitempty" json:"dns-hijack"`
|
||||||
|
AutoRoute bool `yaml:"auto-route,omitempty" json:"auto-route"`
|
||||||
|
AutoDetectInterface bool `yaml:"auto-detect-interface,omitempty"`
|
||||||
|
RedirectToTun []string `yaml:"-,omitempty" json:"-"`
|
||||||
|
|
||||||
|
MTU uint32 `yaml:"mtu,omitempty" json:"mtu,omitempty"`
|
||||||
|
GSO bool `yaml:"gso,omitempty" json:"gso,omitempty"`
|
||||||
|
GSOMaxSize uint32 `yaml:"gso-max-size,omitempty" json:"gso-max-size,omitempty"`
|
||||||
|
//Inet4Address []netip.Prefix `yaml:"inet4-address,omitempty" json:"inet4_address,omitempty"`
|
||||||
|
Inet6Address []uint32 `yaml:"inet6-address,omitempty" json:"inet6_address,omitempty"`
|
||||||
|
StrictRoute bool `yaml:"strict-route,omitempty" json:"strict_route,omitempty"`
|
||||||
|
Inet4RouteAddress []uint32 `yaml:"inet4-route-address,omitempty" json:"inet4_route_address,omitempty"`
|
||||||
|
Inet6RouteAddress []uint32 `yaml:"inet6-route-address,omitempty" json:"inet6_route_address,omitempty"`
|
||||||
|
Inet4RouteExcludeAddress []uint32 `yaml:"inet4-route-exclude-address,omitempty" json:"inet4_route_exclude_address,omitempty"`
|
||||||
|
Inet6RouteExcludeAddress []uint32 `yaml:"inet6-route-exclude-address,omitempty" json:"inet6_route_exclude_address,omitempty"`
|
||||||
|
IncludeInterface []string `yaml:"include-interface,omitempty" json:"include-interface,omitempty"`
|
||||||
|
ExcludeInterface []string `yaml:"exclude-interface,omitempty" json:"exclude-interface,omitempty"`
|
||||||
|
IncludeUID []uint32 `yaml:"include-uid,omitempty" json:"include_uid,omitempty"`
|
||||||
|
IncludeUIDRange []string `yaml:"include-uid-range,omitempty" json:"include_uid_range,omitempty"`
|
||||||
|
ExcludeUID []uint32 `yaml:"exclude-uid,omitempty" json:"exclude_uid,omitempty"`
|
||||||
|
ExcludeUIDRange []string `yaml:"exclude-uid-range,omitempty" json:"exclude_uid_range,omitempty"`
|
||||||
|
IncludeAndroidUser []int `yaml:"include-android-user,omitempty" json:"include_android_user,omitempty"`
|
||||||
|
IncludePackage []string `yaml:"include-package,omitempty" json:"include_package,omitempty"`
|
||||||
|
ExcludePackage []string `yaml:"exclude-package,omitempty" json:"exclude_package,omitempty"`
|
||||||
|
EndpointIndependentNat bool `yaml:"endpoint-independent-nat,omitempty" json:"endpoint_independent_nat,omitempty"`
|
||||||
|
UDPTimeout int64 `yaml:"udp-timeout,omitempty" json:"udp_timeout,omitempty"`
|
||||||
|
FileDescriptor int `yaml:"file-descriptor,omitempty" json:"file-descriptor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RawTuicServer struct {
|
||||||
|
Enable bool `yaml:"enable,omitempty" json:"enable"`
|
||||||
|
Listen string `yaml:"listen,omitempty" json:"listen"`
|
||||||
|
Token []string `yaml:"token,omitempty" json:"token"`
|
||||||
|
Users map[string]string `yaml:"users,omitempty" json:"users,omitempty"`
|
||||||
|
Certificate string `yaml:"certificate,omitempty" json:"certificate"`
|
||||||
|
PrivateKey string `yaml:"private-key,omitempty" json:"private-key"`
|
||||||
|
CongestionController string `yaml:"congestion-controller,omitempty" json:"congestion-controller,omitempty"`
|
||||||
|
MaxIdleTime int `yaml:"max-idle-time,omitempty" json:"max-idle-time,omitempty"`
|
||||||
|
AuthenticationTimeout int `yaml:"authentication-timeout,omitempty" json:"authentication-timeout,omitempty"`
|
||||||
|
ALPN []string `yaml:"alpn,omitempty" json:"alpn,omitempty"`
|
||||||
|
MaxUdpRelayPacketSize int `yaml:"max-udp-relay-packet-size,omitempty" json:"max-udp-relay-packet-size,omitempty"`
|
||||||
|
CWND int `yaml:"cwnd,omitempty" json:"cwnd,omitempty"`
|
||||||
|
}
|
||||||
|
@ -7,6 +7,11 @@ import (
|
|||||||
|
|
||||||
func DecodeBase64(s string) (string, error) {
|
func DecodeBase64(s string) (string, error) {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
|
|
||||||
|
if strings.Contains(s, "-") || strings.Contains(s, "_") {
|
||||||
|
s = strings.Replace(s, "-", "+", -1)
|
||||||
|
s = strings.Replace(s, "_", "/", -1)
|
||||||
|
}
|
||||||
if len(s)%4 != 0 {
|
if len(s)%4 != 0 {
|
||||||
s += strings.Repeat("=", 4-len(s)%4)
|
s += strings.Repeat("=", 4-len(s)%4)
|
||||||
}
|
}
|
||||||
|
24
parser/error.go
Normal file
24
parser/error.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
type ParseError struct {
|
||||||
|
Type ParseErrorType
|
||||||
|
Message string
|
||||||
|
Raw string
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *ParseError) Error() string {
|
||||||
|
if e.Message != "" {
|
||||||
|
return string(e.Type) + ": " + e.Message + " \"" + e.Raw + "\""
|
||||||
|
}
|
||||||
|
return string(e.Type)
|
||||||
|
}
|
88
parser/hysteria.go
Normal file
88
parser/hysteria.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseHysteria(proxy string) (model.Proxy, error) {
|
||||||
|
if !strings.HasPrefix(proxy, constant.HysteriaPrefix) {
|
||||||
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := url.Parse(proxy)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server host",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portStr := link.Port()
|
||||||
|
if portStr == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server port",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := ParsePort(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidPort,
|
||||||
|
Message: err.Error(),
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := link.Query()
|
||||||
|
|
||||||
|
protocol, auth, insecure, upmbps, downmbps, obfs, alpnStr := query.Get("protocol"), query.Get("auth"), 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := model.Proxy{
|
||||||
|
Type: "hysteria",
|
||||||
|
Name: remarks,
|
||||||
|
Server: server,
|
||||||
|
Port: port,
|
||||||
|
Up: upmbps,
|
||||||
|
Down: downmbps,
|
||||||
|
Auth: auth,
|
||||||
|
Obfs: obfs,
|
||||||
|
SkipCertVerify: insecureBool,
|
||||||
|
Alpn: alpn,
|
||||||
|
Protocol: protocol,
|
||||||
|
AllowInsecure: insecureBool,
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
@ -1,51 +1,80 @@
|
|||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/model"
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseHysteria2(proxy string) (model.Proxy, error) {
|
func ParseHysteria2(proxy string) (model.Proxy, error) {
|
||||||
// 判断是否以 hysteria2:// 开头
|
if !strings.HasPrefix(proxy, constant.Hysteria2Prefix1) &&
|
||||||
if !strings.HasPrefix(proxy, "hysteria2://") {
|
!strings.HasPrefix(proxy, constant.Hysteria2Prefix2) {
|
||||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
}
|
}
|
||||||
// 分割
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "hysteria2://"), "@", 2)
|
link, err := url.Parse(proxy)
|
||||||
if len(parts) != 2 {
|
|
||||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
|
||||||
}
|
|
||||||
// 分割
|
|
||||||
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])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 获取端口
|
|
||||||
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 model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server host",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portStr := link.Port()
|
||||||
|
if portStr == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server port",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port, err := ParsePort(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, errors.New("invalid hysteria2 Url")
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidPort,
|
||||||
|
Raw: portStr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 返回结果
|
network, obfs, obfsPassword, pinSHA256, insecure, sni := query.Get("network"), query.Get("obfs"), query.Get("obfs-password"), query.Get("pinSHA256"), query.Get("insecure"), query.Get("sni")
|
||||||
|
enableTLS := pinSHA256 != "" || sni != ""
|
||||||
|
insecureBool := insecure == "1"
|
||||||
|
remarks := link.Fragment
|
||||||
|
if remarks == "" {
|
||||||
|
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||||
|
}
|
||||||
|
remarks = strings.TrimSpace(remarks)
|
||||||
|
|
||||||
result := model.Proxy{
|
result := model.Proxy{
|
||||||
Type: "hysteria2",
|
Type: "hysteria2",
|
||||||
Name: params.Get("name"),
|
Name: remarks,
|
||||||
Server: serverAndPort[0],
|
Server: server,
|
||||||
Port: port,
|
Port: port,
|
||||||
Password: parts[0],
|
Password: password,
|
||||||
Obfs: params.Get("obfs"),
|
Obfs: obfs,
|
||||||
ObfsParam: params.Get("obfs-password"),
|
ObfsParam: obfsPassword,
|
||||||
Sni: params.Get("sni"),
|
Sni: sni,
|
||||||
SkipCertVerify: params.Get("insecure") == "1",
|
SkipCertVerify: insecureBool,
|
||||||
|
TLS: enableTLS,
|
||||||
|
Network: network,
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
18
parser/port.go
Normal file
18
parser/port.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
120
parser/shadowsocks.go
Normal file
120
parser/shadowsocks.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseShadowsocks(proxy string) (model.Proxy, error) {
|
||||||
|
if !strings.HasPrefix(proxy, constant.ShadowsocksPrefix) {
|
||||||
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
|
}
|
||||||
|
if !strings.Contains(proxy, "@") {
|
||||||
|
s := strings.SplitN(proxy, "#", 2)
|
||||||
|
d, err := DecodeBase64(strings.TrimPrefix(s[0], "ss://"))
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s) == 2 {
|
||||||
|
proxy = "ss://" + d + "#" + s[1]
|
||||||
|
} else {
|
||||||
|
proxy = "ss://" + d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
link, err := url.Parse(proxy)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server host",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
portStr := link.Port()
|
||||||
|
if portStr == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server port",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port, err := ParsePort(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
method := link.User.Username()
|
||||||
|
password, _ := link.User.Password()
|
||||||
|
|
||||||
|
if password == "" {
|
||||||
|
user, err := DecodeBase64(method)
|
||||||
|
if err == nil {
|
||||||
|
methodAndPass := strings.SplitN(user, ":", 2)
|
||||||
|
if len(methodAndPass) == 2 {
|
||||||
|
method = methodAndPass[0]
|
||||||
|
password = methodAndPass[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isLikelyBase64(password) {
|
||||||
|
password, err = DecodeBase64(password)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "password decode error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remarks := link.Fragment
|
||||||
|
if remarks == "" {
|
||||||
|
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||||
|
}
|
||||||
|
remarks = strings.TrimSpace(remarks)
|
||||||
|
|
||||||
|
result := model.Proxy{
|
||||||
|
Type: "ss",
|
||||||
|
Cipher: method,
|
||||||
|
Password: password,
|
||||||
|
Server: server,
|
||||||
|
Port: port,
|
||||||
|
Name: remarks,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLikelyBase64(s string) bool {
|
||||||
|
if len(s)%4 == 0 && strings.HasSuffix(s, "=") && !strings.Contains(strings.TrimSuffix(s, "="), "=") {
|
||||||
|
s = strings.TrimSuffix(s, "=")
|
||||||
|
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||||
|
for _, c := range s {
|
||||||
|
if !strings.ContainsRune(chars, c) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
94
parser/shadowsocksr.go
Normal file
94
parser/shadowsocksr.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseShadowsocksR(proxy string) (model.Proxy, error) {
|
||||||
|
if !strings.HasPrefix(proxy, constant.ShadowsocksRPrefix) {
|
||||||
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy = strings.TrimPrefix(proxy, constant.ShadowsocksRPrefix)
|
||||||
|
proxy, err := DecodeBase64(proxy)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidBase64,
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverInfoAndParams := strings.SplitN(proxy, "/?", 2)
|
||||||
|
parts := strings.Split(serverInfoAndParams[0], ":")
|
||||||
|
server := parts[0]
|
||||||
|
protocol := parts[2]
|
||||||
|
method := parts[3]
|
||||||
|
obfs := parts[4]
|
||||||
|
password, err := DecodeBase64(parts[5])
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Raw: proxy,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port, err := ParsePort(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidPort,
|
||||||
|
Message: err.Error(),
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var obfsParam string
|
||||||
|
var protoParam string
|
||||||
|
var remarks string
|
||||||
|
if len(serverInfoAndParams) == 2 {
|
||||||
|
params, err := url.ParseQuery(serverInfoAndParams[1])
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrCannotParseParams,
|
||||||
|
Raw: proxy,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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"))
|
||||||
|
} else {
|
||||||
|
remarks = server + ":" + strconv.Itoa(port)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Raw: proxy,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := model.Proxy{
|
||||||
|
Name: remarks,
|
||||||
|
Type: "ssr",
|
||||||
|
Server: server,
|
||||||
|
Port: port,
|
||||||
|
Protocol: protocol,
|
||||||
|
Cipher: method,
|
||||||
|
Obfs: obfs,
|
||||||
|
Password: password,
|
||||||
|
ObfsParam: obfsParam,
|
||||||
|
ProtocolParam: protoParam,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
79
parser/socks.go
Normal file
79
parser/socks.go
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseSocks(proxy string) (model.Proxy, error) {
|
||||||
|
if !strings.HasPrefix(proxy, constant.SocksPrefix) {
|
||||||
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
|
}
|
||||||
|
link, err := url.Parse(proxy)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server host",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portStr := link.Port()
|
||||||
|
if portStr == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server port",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port, err := ParsePort(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidPort,
|
||||||
|
Raw: portStr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remarks := link.Fragment
|
||||||
|
if remarks == "" {
|
||||||
|
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||||
|
}
|
||||||
|
remarks = strings.TrimSpace(remarks)
|
||||||
|
|
||||||
|
encodeStr := link.User.Username()
|
||||||
|
var username, password string
|
||||||
|
if encodeStr != "" {
|
||||||
|
decodeStr, err := DecodeBase64(encodeStr)
|
||||||
|
splitStr := strings.Split(decodeStr, ":")
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
username = splitStr[0]
|
||||||
|
if len(splitStr) == 2 {
|
||||||
|
password = splitStr[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return model.Proxy{
|
||||||
|
Type: "socks5",
|
||||||
|
Name: remarks,
|
||||||
|
Server: server,
|
||||||
|
Port: port,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
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
|
|
||||||
}
|
|
120
parser/trojan.go
120
parser/trojan.go
@ -3,51 +3,109 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/model"
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseTrojan(proxy string) (model.Proxy, error) {
|
func ParseTrojan(proxy string) (model.Proxy, error) {
|
||||||
// 判断是否以 trojan:// 开头
|
if !strings.HasPrefix(proxy, constant.TrojanPrefix) {
|
||||||
if !strings.HasPrefix(proxy, "trojan://") {
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
return model.Proxy{}, fmt.Errorf("invalid trojan Url")
|
|
||||||
}
|
}
|
||||||
// 分割
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2)
|
link, err := url.Parse(proxy)
|
||||||
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 {
|
if err != nil {
|
||||||
return model.Proxy{}, err
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(serverAndPort) != 2 {
|
|
||||||
return model.Proxy{}, fmt.Errorf("invalid trojan")
|
password := link.User.Username()
|
||||||
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server host",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 处理端口
|
portStr := link.Port()
|
||||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
if portStr == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server port",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
port, err := ParsePort(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, err
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidPort,
|
||||||
|
Message: err.Error(),
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 返回结果
|
|
||||||
|
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 := 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")
|
||||||
|
|
||||||
|
var alpn []string
|
||||||
|
if strings.Contains(alpnStr, ",") {
|
||||||
|
alpn = strings.Split(alpnStr, ",")
|
||||||
|
} else {
|
||||||
|
alpn = nil
|
||||||
|
}
|
||||||
|
|
||||||
result := model.Proxy{
|
result := model.Proxy{
|
||||||
Type: "trojan",
|
Type: "trojan",
|
||||||
Server: strings.TrimSpace(serverAndPort[0]),
|
Server: server,
|
||||||
Port: port,
|
Port: port,
|
||||||
UDP: true,
|
Password: password,
|
||||||
Password: strings.TrimSpace(parts[0]),
|
Name: remarks,
|
||||||
Sni: params.Get("sni"),
|
Network: network,
|
||||||
}
|
}
|
||||||
// 如果有节点名称
|
|
||||||
if len(serverInfo) == 2 {
|
if security == "xtls" || security == "tls" {
|
||||||
result.Name, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1]))
|
result.Alpn = alpn
|
||||||
} else {
|
result.Sni = sni
|
||||||
result.Name = serverAndPort[0]
|
result.TLS = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if security == "reality" {
|
||||||
|
result.TLS = true
|
||||||
|
result.Sni = sni
|
||||||
|
result.RealityOpts = model.RealityOptions{
|
||||||
|
PublicKey: pbk,
|
||||||
|
ShortID: sid,
|
||||||
|
}
|
||||||
|
result.Fingerprint = fp
|
||||||
|
}
|
||||||
|
|
||||||
|
if network == "ws" {
|
||||||
|
result.Network = "ws"
|
||||||
|
result.WSOpts = model.WSOptions{
|
||||||
|
Path: path,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Host": host,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if network == "grpc" {
|
||||||
|
result.GrpcOpts = model.GrpcOptions{
|
||||||
|
GrpcServiceName: serviceName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
168
parser/vless.go
168
parser/vless.go
@ -3,79 +3,125 @@ package parser
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/model"
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseVless(proxy string) (model.Proxy, error) {
|
func ParseVless(proxy string) (model.Proxy, error) {
|
||||||
// 判断是否以 vless:// 开头
|
if !strings.HasPrefix(proxy, constant.VLESSPrefix) {
|
||||||
if !strings.HasPrefix(proxy, "vless://") {
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
return model.Proxy{}, fmt.Errorf("invalid vless Url")
|
|
||||||
}
|
}
|
||||||
// 分割
|
|
||||||
parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2)
|
link, err := url.Parse(proxy)
|
||||||
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 {
|
if err != nil {
|
||||||
return model.Proxy{}, err
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "url parse error",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(serverAndPort) != 2 {
|
|
||||||
return model.Proxy{}, fmt.Errorf("invalid vless")
|
server := link.Hostname()
|
||||||
|
if server == "" {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidStruct,
|
||||||
|
Message: "missing server host",
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 处理端口
|
portStr := link.Port()
|
||||||
port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1]))
|
port, err := ParsePort(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, err
|
return model.Proxy{}, &ParseError{
|
||||||
}
|
Type: ErrInvalidPort,
|
||||||
// 返回结果
|
Message: err.Error(),
|
||||||
result := model.Proxy{
|
Raw: 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{
|
query := link.Query()
|
||||||
GrpcServiceName: params.Get("serviceName"),
|
uuid := link.User.Username()
|
||||||
}
|
flow, security, alpnStr, sni, insecure, fp, pbk, sid, path, host, serviceName, _type := 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")
|
||||||
}
|
|
||||||
// 如果有节点名称
|
insecureBool := insecure == "1"
|
||||||
if len(serverInfo) == 2 {
|
var alpn []string
|
||||||
if strings.Contains(serverInfo[1], "|") {
|
if strings.Contains(alpnStr, ",") {
|
||||||
result.Name = strings.SplitN(serverInfo[1], "|", 2)[1]
|
alpn = strings.Split(alpnStr, ",")
|
||||||
} else {
|
|
||||||
result.Name = serverInfo[1]
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
result.Name = serverAndPort[0]
|
alpn = nil
|
||||||
}
|
}
|
||||||
|
remarks := link.Fragment
|
||||||
|
if remarks == "" {
|
||||||
|
remarks = fmt.Sprintf("%s:%s", server, portStr)
|
||||||
|
}
|
||||||
|
remarks = strings.TrimSpace(remarks)
|
||||||
|
|
||||||
|
result := model.Proxy{
|
||||||
|
Type: "vless",
|
||||||
|
Server: server,
|
||||||
|
Name: remarks,
|
||||||
|
Port: port,
|
||||||
|
UUID: uuid,
|
||||||
|
Flow: flow,
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == "tls" {
|
||||||
|
result.TLS = true
|
||||||
|
result.Alpn = alpn
|
||||||
|
result.Sni = sni
|
||||||
|
result.AllowInsecure = insecureBool
|
||||||
|
result.ClientFingerprint = fp
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == "reality" {
|
||||||
|
result.TLS = true
|
||||||
|
result.Servername = sni
|
||||||
|
result.RealityOpts = model.RealityOptions{
|
||||||
|
PublicKey: pbk,
|
||||||
|
ShortID: sid,
|
||||||
|
}
|
||||||
|
result.ClientFingerprint = fp
|
||||||
|
}
|
||||||
|
|
||||||
|
if _type == "ws" {
|
||||||
|
result.Network = "ws"
|
||||||
|
result.WSOpts = model.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 = model.GrpcOptions{
|
||||||
|
GrpcServiceName: serviceName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _type == "http" {
|
||||||
|
result.HTTPOpts = model.HTTPOptions{}
|
||||||
|
result.HTTPOpts.Headers = map[string][]string{}
|
||||||
|
|
||||||
|
result.HTTPOpts.Path = strings.Split(path, ",")
|
||||||
|
|
||||||
|
hosts, err := url.QueryUnescape(host)
|
||||||
|
if err != nil {
|
||||||
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrCannotParseParams,
|
||||||
|
Raw: proxy,
|
||||||
|
Message: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.Network = "http"
|
||||||
|
if hosts != "" {
|
||||||
|
result.HTTPOpts.Headers["host"] = strings.Split(host, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
109
parser/vmess.go
109
parser/vmess.go
@ -2,78 +2,97 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sub2clash/model"
|
|
||||||
|
"github.com/nitezs/sub2clash/constant"
|
||||||
|
"github.com/nitezs/sub2clash/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParseVmess(proxy string) (model.Proxy, error) {
|
func ParseVmess(proxy string) (model.Proxy, error) {
|
||||||
// 判断是否以 vmess:// 开头
|
if !strings.HasPrefix(proxy, constant.VMessPrefix) {
|
||||||
if !strings.HasPrefix(proxy, "vmess://") {
|
return model.Proxy{}, &ParseError{Type: ErrInvalidPrefix, Raw: proxy}
|
||||||
return model.Proxy{}, errors.New("invalid vmess url")
|
|
||||||
}
|
}
|
||||||
// 解码
|
|
||||||
base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://"))
|
proxy = strings.TrimPrefix(proxy, constant.VMessPrefix)
|
||||||
|
base64, err := DecodeBase64(proxy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
return model.Proxy{}, &ParseError{Type: ErrInvalidBase64, Raw: proxy, Message: err.Error()}
|
||||||
}
|
}
|
||||||
// 解析
|
|
||||||
var vmess model.VmessJson
|
var vmess model.VmessJson
|
||||||
err = json.Unmarshal([]byte(base64), &vmess)
|
err = json.Unmarshal([]byte(base64), &vmess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||||
}
|
}
|
||||||
// 解析端口
|
|
||||||
port := 0
|
var port int
|
||||||
switch vmess.Port.(type) {
|
switch vmess.Port.(type) {
|
||||||
case string:
|
case string:
|
||||||
port, err = strconv.Atoi(vmess.Port.(string))
|
port, err = ParsePort(vmess.Port.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
return model.Proxy{}, &ParseError{
|
||||||
|
Type: ErrInvalidPort,
|
||||||
|
Message: err.Error(),
|
||||||
|
Raw: proxy,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case float64:
|
case float64:
|
||||||
port = int(vmess.Port.(float64))
|
port = int(vmess.Port.(float64))
|
||||||
}
|
}
|
||||||
// 解析Aid
|
|
||||||
aid := 0
|
aid := 0
|
||||||
switch vmess.Aid.(type) {
|
switch vmess.Aid.(type) {
|
||||||
case string:
|
case string:
|
||||||
aid, err = strconv.Atoi(vmess.Aid.(string))
|
aid, err = strconv.Atoi(vmess.Aid.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return model.Proxy{}, errors.New("invalid vmess url" + err.Error())
|
return model.Proxy{}, &ParseError{Type: ErrInvalidStruct, Raw: proxy, Message: err.Error()}
|
||||||
}
|
}
|
||||||
case float64:
|
case float64:
|
||||||
aid = int(vmess.Aid.(float64))
|
aid = int(vmess.Aid.(float64))
|
||||||
}
|
}
|
||||||
// 设置默认值
|
|
||||||
if vmess.Scy == "" {
|
if vmess.Scy == "" {
|
||||||
vmess.Scy = "auto"
|
vmess.Scy = "auto"
|
||||||
}
|
}
|
||||||
if vmess.Net == "ws" && vmess.Path == "" {
|
|
||||||
vmess.Path = "/"
|
name, err := url.QueryUnescape(vmess.Ps)
|
||||||
|
if err != nil {
|
||||||
|
name = vmess.Ps
|
||||||
}
|
}
|
||||||
if vmess.Net == "ws" && vmess.Host == "" {
|
|
||||||
vmess.Host = vmess.Add
|
|
||||||
}
|
|
||||||
// 返回结果
|
|
||||||
result := model.Proxy{
|
result := model.Proxy{
|
||||||
Name: vmess.Ps,
|
Name: name,
|
||||||
Type: "vmess",
|
Type: "vmess",
|
||||||
Server: vmess.Add,
|
Server: vmess.Add,
|
||||||
Port: port,
|
Port: port,
|
||||||
UUID: vmess.Id,
|
UUID: vmess.Id,
|
||||||
AlterID: aid,
|
AlterID: aid,
|
||||||
Cipher: vmess.Scy,
|
Cipher: vmess.Scy,
|
||||||
UDP: true,
|
|
||||||
TLS: vmess.Tls == "tls",
|
|
||||||
Fingerprint: vmess.Fp,
|
|
||||||
ClientFingerprint: "chrome",
|
|
||||||
SkipCertVerify: true,
|
|
||||||
Servername: vmess.Add,
|
|
||||||
Network: vmess.Net,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vmess.Tls == "tls" {
|
||||||
|
var alpn []string
|
||||||
|
if strings.Contains(vmess.Alpn, ",") {
|
||||||
|
alpn = strings.Split(vmess.Alpn, ",")
|
||||||
|
} else {
|
||||||
|
alpn = nil
|
||||||
|
}
|
||||||
|
result.TLS = true
|
||||||
|
result.Fingerprint = vmess.Fp
|
||||||
|
result.Alpn = alpn
|
||||||
|
result.Servername = vmess.Sni
|
||||||
|
}
|
||||||
|
|
||||||
if vmess.Net == "ws" {
|
if vmess.Net == "ws" {
|
||||||
|
if vmess.Path == "" {
|
||||||
|
vmess.Path = "/"
|
||||||
|
}
|
||||||
|
if vmess.Host == "" {
|
||||||
|
vmess.Host = vmess.Add
|
||||||
|
}
|
||||||
|
result.Network = "ws"
|
||||||
result.WSOpts = model.WSOptions{
|
result.WSOpts = model.WSOptions{
|
||||||
Path: vmess.Path,
|
Path: vmess.Path,
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
@ -81,5 +100,21 @@ func ParseVmess(proxy string) (model.Proxy, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if vmess.Net == "grpc" {
|
||||||
|
result.GrpcOpts = model.GrpcOptions{
|
||||||
|
GrpcServiceName: vmess.Path,
|
||||||
|
}
|
||||||
|
result.Network = "grpc"
|
||||||
|
}
|
||||||
|
|
||||||
|
if vmess.Net == "h2" {
|
||||||
|
result.HTTP2Opts = model.HTTP2Options{
|
||||||
|
Host: strings.Split(vmess.Host, ","),
|
||||||
|
Path: vmess.Path,
|
||||||
|
}
|
||||||
|
result.Network = "h2"
|
||||||
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,127 +1,126 @@
|
|||||||
port: 7890
|
mixed-port: 7890
|
||||||
socks-port: 7891
|
|
||||||
allow-lan: true
|
allow-lan: true
|
||||||
mode: Rule
|
mode: Rule
|
||||||
log-level: info
|
log-level: info
|
||||||
proxies:
|
proxies:
|
||||||
proxy-groups:
|
proxy-groups:
|
||||||
- name: 节点选择
|
- name: 节点选择
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: 手动切换
|
- name: 手动切换
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- <all>
|
- <all>
|
||||||
- name: 游戏平台(中国)
|
- name: 游戏平台(中国)
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: 游戏平台(全球)
|
- name: 游戏平台(全球)
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: 巴哈姆特
|
- name: 巴哈姆特
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: 哔哩哔哩
|
- name: 哔哩哔哩
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: Telegram
|
- name: Telegram
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: OpenAI
|
- name: OpenAI
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: Youtube
|
- name: Youtube
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: Microsoft
|
- name: Microsoft
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: Onedrive
|
- name: Onedrive
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: Apple
|
- name: Apple
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: Netflix
|
- name: Netflix
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: 广告拦截
|
- name: 广告拦截
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- REJECT
|
- REJECT
|
||||||
- DIRECT
|
- DIRECT
|
||||||
- name: 漏网之鱼
|
- name: 漏网之鱼
|
||||||
type: select
|
type: select
|
||||||
proxies:
|
proxies:
|
||||||
- 节点选择
|
- 节点选择
|
||||||
- <countries>
|
- <countries>
|
||||||
- 手动切换
|
- 手动切换
|
||||||
- DIRECT
|
- DIRECT
|
||||||
rules:
|
rules:
|
||||||
- GEOSITE,private,DIRECT,no-resolve
|
- GEOSITE,private,DIRECT,no-resolve
|
||||||
- GEOIP,private,DIRECT
|
- GEOIP,private,DIRECT
|
||||||
- GEOSITE,category-ads-all,广告拦截
|
- GEOSITE,category-ads-all,广告拦截
|
||||||
- GEOSITE,microsoft,Microsoft
|
- GEOSITE,microsoft,Microsoft
|
||||||
- GEOSITE,apple,Apple
|
- GEOSITE,apple,Apple
|
||||||
- GEOSITE,netflix,Netflix
|
- GEOSITE,netflix,Netflix
|
||||||
- GEOIP,netflix,Netflix
|
- GEOIP,netflix,Netflix
|
||||||
- GEOSITE,onedrive,Onedrive
|
- GEOSITE,onedrive,Onedrive
|
||||||
- GEOSITE,youtube,Youtube
|
- GEOSITE,youtube,Youtube
|
||||||
- GEOSITE,telegram,Telegram
|
- GEOSITE,telegram,Telegram
|
||||||
- GEOIP,telegram,Telegram
|
- GEOIP,telegram,Telegram
|
||||||
- GEOSITE,openai,OpenAI
|
- GEOSITE,openai,OpenAI
|
||||||
- GEOSITE,bilibili,哔哩哔哩
|
- GEOSITE,bilibili,哔哩哔哩
|
||||||
- GEOSITE,bahamut,巴哈姆特
|
- GEOSITE,bahamut,巴哈姆特
|
||||||
- GEOSITE,category-games@cn,游戏平台(中国)
|
- GEOSITE,category-games@cn,游戏平台(中国)
|
||||||
- GEOSITE,category-games,游戏平台(全球)
|
- GEOSITE,category-games,游戏平台(全球)
|
||||||
- GEOSITE,geolocation-!cn,节点选择
|
- GEOSITE,geolocation-!cn,节点选择
|
||||||
- GEOSITE,CN,DIRECT
|
- GEOSITE,CN,DIRECT
|
||||||
- GEOIP,CN,DIRECT
|
- GEOIP,CN,DIRECT
|
||||||
- MATCH,漏网之鱼
|
- MATCH,漏网之鱼
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"sub2clash/config"
|
|
||||||
"sub2clash/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CheckUpdate() (bool, string, error) {
|
|
||||||
get, err := Get("https://api.github.com/repos/nitezs/sub2clash/tags")
|
|
||||||
if err != nil {
|
|
||||||
return false, "", errors.New("get version info failed" + err.Error())
|
|
||||||
|
|
||||||
}
|
|
||||||
var version model.Tags
|
|
||||||
all, err := io.ReadAll(get.Body)
|
|
||||||
if err != nil {
|
|
||||||
return false, "", errors.New("get version info failed" + err.Error())
|
|
||||||
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(all, &version)
|
|
||||||
if err != nil {
|
|
||||||
return false, "", errors.New("get version info failed" + err.Error())
|
|
||||||
|
|
||||||
}
|
|
||||||
if version[0].Name == config.Version {
|
|
||||||
return false, "", nil
|
|
||||||
} else {
|
|
||||||
return true, version[0].Name, nil
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"sub2clash/logger"
|
|
||||||
"sub2clash/model"
|
|
||||||
"sub2clash/utils"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DB *gorm.DB
|
|
||||||
|
|
||||||
func ConnectDB() error {
|
|
||||||
// 用上面的数据库连接初始化 gorm
|
|
||||||
err := utils.MKDir("data")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
db, err := gorm.Open(
|
|
||||||
sqlite.Open(filepath.Join("data", "sub2clash.db")), &gorm.Config{
|
|
||||||
Logger: nil,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
DB = db
|
|
||||||
err = db.AutoMigrate(&model.ShortLink{})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindShortLinkByUrl(url string, shortLink *model.ShortLink) *gorm.DB {
|
|
||||||
logger.Logger.Debug("find short link by url", zap.String("url", url))
|
|
||||||
return DB.Where("url = ?", url).First(&shortLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindShortLinkByHash(hash string, shortLink *model.ShortLink) *gorm.DB {
|
|
||||||
logger.Logger.Debug("find short link by hash", zap.String("hash", hash))
|
|
||||||
return DB.Where("hash = ?", hash).First(&shortLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveShortLink(shortLink *model.ShortLink) {
|
|
||||||
logger.Logger.Debug("save short link", zap.String("hash", shortLink.Hash))
|
|
||||||
DB.Save(shortLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FirstOrCreateShortLink(shortLink *model.ShortLink) {
|
|
||||||
logger.Logger.Debug("first or create short link", zap.String("hash", shortLink.Hash))
|
|
||||||
DB.FirstOrCreate(shortLink)
|
|
||||||
}
|
|
38
utils/get.go
38
utils/get.go
@ -1,38 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"sub2clash/config"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Get(url string) (resp *http.Response, err error) {
|
|
||||||
retryTimes := config.Default.RequestRetryTimes
|
|
||||||
haveTried := 0
|
|
||||||
retryDelay := time.Second // 延迟1秒再重试
|
|
||||||
for haveTried < retryTimes {
|
|
||||||
client := &http.Client{}
|
|
||||||
//client.Timeout = time.Second * 10
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
haveTried++
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
get, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
haveTried++
|
|
||||||
time.Sleep(retryDelay)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
// 如果文件大小大于设定,直接返回错误
|
|
||||||
if get != nil && get.ContentLength > config.Default.RequestMaxFileSize {
|
|
||||||
return nil, errors.New("文件过大")
|
|
||||||
}
|
|
||||||
return get, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
@ -3,9 +3,16 @@ package validator
|
|||||||
type ShortLinkGenValidator struct {
|
type ShortLinkGenValidator struct {
|
||||||
Url string `form:"url" binding:"required"`
|
Url string `form:"url" binding:"required"`
|
||||||
Password string `form:"password"`
|
Password string `form:"password"`
|
||||||
|
CustomID string `form:"customId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ShortLinkGetValidator struct {
|
type GetUrlValidator struct {
|
||||||
Hash string `form:"hash" binding:"required"`
|
Hash string `form:"hash" binding:"required"`
|
||||||
Password string `form:"password"`
|
Password string `form:"password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShortLinkUpdateValidator struct {
|
||||||
|
Hash string `form:"hash" binding:"required"`
|
||||||
|
Url string `form:"url" binding:"required"`
|
||||||
|
Password string `form:"password" binding:"required"`
|
||||||
|
}
|
||||||
|
@ -12,24 +12,26 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type SubValidator struct {
|
type SubValidator struct {
|
||||||
Sub string `form:"sub" binding:""`
|
Sub string `form:"sub" binding:""`
|
||||||
Subs []string `form:"-" binding:""`
|
Subs []string `form:"-" binding:""`
|
||||||
Proxy string `form:"proxy" binding:""`
|
Proxy string `form:"proxy" binding:""`
|
||||||
Proxies []string `form:"-" binding:""`
|
Proxies []string `form:"-" binding:""`
|
||||||
Refresh bool `form:"refresh,default=false" binding:""`
|
Refresh bool `form:"refresh,default=false" binding:""`
|
||||||
Template string `form:"template" binding:""`
|
Template string `form:"template" binding:""`
|
||||||
RuleProvider string `form:"ruleProvider" binding:""`
|
RuleProvider string `form:"ruleProvider" binding:""`
|
||||||
RuleProviders []RuleProviderStruct `form:"-" binding:""`
|
RuleProviders []RuleProviderStruct `form:"-" binding:""`
|
||||||
Rule string `form:"rule" binding:""`
|
Rule string `form:"rule" binding:""`
|
||||||
Rules []RuleStruct `form:"-" binding:""`
|
Rules []RuleStruct `form:"-" binding:""`
|
||||||
AutoTest bool `form:"autoTest,default=false" binding:""`
|
AutoTest bool `form:"autoTest,default=false" binding:""`
|
||||||
Lazy bool `form:"lazy,default=false" binding:""`
|
Lazy bool `form:"lazy,default=false" binding:""`
|
||||||
Sort string `form:"sort" binding:""`
|
Sort string `form:"sort" binding:""`
|
||||||
Remove string `form:"remove" binding:""`
|
Remove string `form:"remove" binding:""`
|
||||||
Replace string `form:"replace" binding:""`
|
Replace string `form:"replace" binding:""`
|
||||||
ReplaceKeys []string `form:"-" binding:""`
|
ReplaceKeys []string `form:"-" binding:""`
|
||||||
ReplaceTo []string `form:"-" binding:""`
|
ReplaceTo []string `form:"-" binding:""`
|
||||||
NodeListMode bool `form:"nodeList,default=false" binding:""`
|
NodeListMode bool `form:"nodeList,default=false" binding:""`
|
||||||
|
IgnoreCountryGrooup bool `form:"ignoreCountryGroup,default=false" binding:""`
|
||||||
|
UserAgent string `form:"userAgent" binding:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleProviderStruct struct {
|
type RuleProviderStruct struct {
|
||||||
@ -109,7 +111,7 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// 校验 Rule-Provider 是否有重名
|
|
||||||
names := make(map[string]bool)
|
names := make(map[string]bool)
|
||||||
for _, ruleProvider := range query.RuleProviders {
|
for _, ruleProvider := range query.RuleProviders {
|
||||||
if _, ok := names[ruleProvider.Name]; ok {
|
if _, ok := names[ruleProvider.Name]; ok {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user