540 lines
15 KiB
Go
540 lines
15 KiB
Go
package frp
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"archive/tar"
|
|||
|
|
"archive/zip"
|
|||
|
|
"bytes"
|
|||
|
|
"compress/gzip"
|
|||
|
|
"context"
|
|||
|
|
"crypto/sha256"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"net/http"
|
|||
|
|
"os"
|
|||
|
|
"path/filepath"
|
|||
|
|
"runtime"
|
|||
|
|
"sort"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
githubReleasesURL = "https://api.github.com/repos/fatedier/frp/releases"
|
|||
|
|
maxArchiveSize = 150 << 20
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var downloadProxyOptions = map[string]struct{}{
|
|||
|
|
"": {},
|
|||
|
|
"https://gh-proxy.org/": {},
|
|||
|
|
"https://v4.gh-proxy.org/": {},
|
|||
|
|
"https://cdn.gh-proxy.org/": {},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var releaseProxyOrder = []string{
|
|||
|
|
"https://gh-proxy.org/",
|
|||
|
|
"https://v4.gh-proxy.org/",
|
|||
|
|
"https://cdn.gh-proxy.org/",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type githubRelease struct {
|
|||
|
|
TagName string `json:"tag_name"`
|
|||
|
|
Draft bool `json:"draft"`
|
|||
|
|
Prerelease bool `json:"prerelease"`
|
|||
|
|
PublishedAt time.Time `json:"published_at"`
|
|||
|
|
Assets []githubAsset `json:"assets"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type githubAsset struct {
|
|||
|
|
ID int64 `json:"id"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
Size int64 `json:"size"`
|
|||
|
|
Digest string `json:"digest"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type downloadReporter func(status, message string, downloaded, total int64, failure string)
|
|||
|
|
|
|||
|
|
func runtimePlatform() string {
|
|||
|
|
return runtime.GOOS + "/" + runtime.GOARCH
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func fetchOfficialReleases(ctx context.Context, selectedProxy string) ([]Release, error) {
|
|||
|
|
var failures []string
|
|||
|
|
for _, url := range releaseListURLs(selectedProxy) {
|
|||
|
|
releases, err := fetchOfficialReleasesAt(ctx, url)
|
|||
|
|
if err == nil {
|
|||
|
|
return releases, nil
|
|||
|
|
}
|
|||
|
|
failures = append(failures, err.Error())
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("获取 FRP 官方版本列表失败:%s", strings.Join(failures, ";"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func fetchOfficialReleasesAt(ctx context.Context, url string) ([]Release, error) {
|
|||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
request.Header.Set("Accept", "application/vnd.github+json")
|
|||
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|||
|
|
response, err := client.Do(request)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer response.Body.Close()
|
|||
|
|
if response.StatusCode != http.StatusOK {
|
|||
|
|
return nil, fmt.Errorf("HTTP %d", response.StatusCode)
|
|||
|
|
}
|
|||
|
|
var source []githubRelease
|
|||
|
|
if err := json.NewDecoder(io.LimitReader(response.Body, 2<<20)).Decode(&source); err != nil {
|
|||
|
|
return nil, fmt.Errorf("响应解析失败:%w", err)
|
|||
|
|
}
|
|||
|
|
return supportedReleases(source), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func releaseListURLs(selectedProxy string) []string {
|
|||
|
|
urls := []string{}
|
|||
|
|
add := func(proxy string) {
|
|||
|
|
url := githubReleasesURL
|
|||
|
|
if proxy != "" {
|
|||
|
|
url = proxy + githubReleasesURL
|
|||
|
|
}
|
|||
|
|
for _, item := range urls {
|
|||
|
|
if item == url {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
urls = append(urls, url)
|
|||
|
|
}
|
|||
|
|
add(selectedProxy)
|
|||
|
|
add("")
|
|||
|
|
for _, proxy := range releaseProxyOrder {
|
|||
|
|
add(proxy)
|
|||
|
|
}
|
|||
|
|
return urls
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func supportedReleases(source []githubRelease) []Release {
|
|||
|
|
releases := make([]Release, 0, len(source))
|
|||
|
|
for _, item := range source {
|
|||
|
|
if item.Draft || item.Prerelease {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
asset, ok := findArchiveAsset(item)
|
|||
|
|
if !ok {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
releases = append(releases, Release{Version: item.TagName, PublishedAt: item.PublishedAt.UTC().Format(time.RFC3339), AssetName: asset.Name, Size: asset.Size, Checksum: strings.TrimPrefix(asset.Digest, "sha256:"), AssetID: asset.ID})
|
|||
|
|
}
|
|||
|
|
return releases
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func findArchiveAsset(release githubRelease) (githubAsset, bool) {
|
|||
|
|
version := strings.TrimPrefix(release.TagName, "v")
|
|||
|
|
prefix := "frp_" + version + "_" + runtime.GOOS + "_" + runtime.GOARCH
|
|||
|
|
for _, asset := range release.Assets {
|
|||
|
|
if asset.Name == prefix+".zip" || asset.Name == prefix+".tar.gz" {
|
|||
|
|
return asset, true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return githubAsset{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func findRelease(releases []Release, version string) (Release, bool) {
|
|||
|
|
for _, release := range releases {
|
|||
|
|
if release.Version == version {
|
|||
|
|
return release, true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return Release{}, false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func downloadOfficialBinary(ctx context.Context, frpDir string, release Release, proxy string, report downloadReporter) (string, error) {
|
|||
|
|
checksum := release.Checksum
|
|||
|
|
if checksum == "" {
|
|||
|
|
report("queued", "正在获取官方 SHA-256 校验信息", 0, 0, "")
|
|||
|
|
var err error
|
|||
|
|
checksum, err = fetchChecksum(ctx, release)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
archivePath, err := downloadArchive(ctx, frpDir, release, checksum, proxy, report)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
defer os.Remove(archivePath)
|
|||
|
|
report("extracting", "SHA-256 校验通过,正在解压 frpc", 0, 0, "")
|
|||
|
|
return extractClientBinary(archivePath, frpDir, release)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func fetchChecksum(ctx context.Context, release Release) (string, error) {
|
|||
|
|
version := strings.TrimPrefix(release.Version, "v")
|
|||
|
|
name := "frp_" + version + "_sha256sums.txt"
|
|||
|
|
url := officialAssetURL(release.Version, name)
|
|||
|
|
data, err := fetchBytes(ctx, url, 1<<20)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("下载 FRP 官方校验文件失败:%w", err)
|
|||
|
|
}
|
|||
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|||
|
|
fields := strings.Fields(line)
|
|||
|
|
if len(fields) >= 2 && strings.TrimPrefix(fields[1], "*") == release.AssetName {
|
|||
|
|
return fields[0], nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return "", fmt.Errorf("FRP 官方校验文件中未找到 %s", release.AssetName)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func downloadArchive(ctx context.Context, frpDir string, release Release, checksum, proxy string, report downloadReporter) (string, error) {
|
|||
|
|
report("downloading", "正在下载 FRP 官方安装包", 0, release.Size, "")
|
|||
|
|
data, err := fetchBytesWithProgress(ctx, releaseDownloadURL(release, proxy), maxArchiveSize, func(downloaded, total int64) {
|
|||
|
|
if total <= 0 {
|
|||
|
|
total = release.Size
|
|||
|
|
}
|
|||
|
|
report("downloading", "正在下载 FRP 官方安装包", downloaded, total, "")
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return "", fmt.Errorf("下载 FRP 官方安装包失败:%w;请检查网络连接或设置 HTTPS_PROXY", err)
|
|||
|
|
}
|
|||
|
|
report("verifying", "安装包下载完成,正在校验 SHA-256", int64(len(data)), int64(len(data)), "")
|
|||
|
|
sum := fmt.Sprintf("%x", sha256.Sum256(data))
|
|||
|
|
if !strings.EqualFold(sum, checksum) {
|
|||
|
|
return "", fmt.Errorf("FRP 安装包 SHA-256 校验失败")
|
|||
|
|
}
|
|||
|
|
dir := filepath.Join(frpDir, "downloads")
|
|||
|
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
path := filepath.Join(dir, release.AssetName)
|
|||
|
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
return path, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func fetchBytes(ctx context.Context, url string, limit int64) ([]byte, error) {
|
|||
|
|
return fetchBytesWithProgress(ctx, url, limit, nil)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func fetchBytesWithProgress(ctx context.Context, url string, limit int64, progress func(int64, int64)) ([]byte, error) {
|
|||
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
request.Header.Set("User-Agent", "Fonu-FRP-Downloader")
|
|||
|
|
if strings.HasPrefix(url, "https://api.github.com/repos/fatedier/frp/releases/assets/") {
|
|||
|
|
request.Header.Set("Accept", "application/octet-stream")
|
|||
|
|
request.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
|||
|
|
}
|
|||
|
|
client := &http.Client{Timeout: 5 * time.Minute}
|
|||
|
|
response, err := client.Do(request)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer response.Body.Close()
|
|||
|
|
if response.StatusCode != http.StatusOK {
|
|||
|
|
return nil, fmt.Errorf("HTTP %d", response.StatusCode)
|
|||
|
|
}
|
|||
|
|
return readResponseBody(response.Body, response.ContentLength, limit, progress)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func readResponseBody(reader io.Reader, total, limit int64, progress func(int64, int64)) ([]byte, error) {
|
|||
|
|
var output bytes.Buffer
|
|||
|
|
buffer := make([]byte, 64*1024)
|
|||
|
|
var downloaded int64
|
|||
|
|
for {
|
|||
|
|
count, err := reader.Read(buffer)
|
|||
|
|
if count > 0 {
|
|||
|
|
downloaded += int64(count)
|
|||
|
|
if downloaded > limit {
|
|||
|
|
return nil, fmt.Errorf("下载内容超过大小限制")
|
|||
|
|
}
|
|||
|
|
_, _ = output.Write(buffer[:count])
|
|||
|
|
if progress != nil {
|
|||
|
|
progress(downloaded, total)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if err == io.EOF {
|
|||
|
|
return output.Bytes(), nil
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func officialAssetURL(version, name string) string {
|
|||
|
|
return "https://github.com/fatedier/frp/releases/download/" + version + "/" + name
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func releaseDownloadURL(release Release, proxy string) string {
|
|||
|
|
if proxy != "" {
|
|||
|
|
return proxy + officialAssetURL(release.Version, release.AssetName)
|
|||
|
|
}
|
|||
|
|
if release.AssetID > 0 {
|
|||
|
|
return fmt.Sprintf("https://api.github.com/repos/fatedier/frp/releases/assets/%d", release.AssetID)
|
|||
|
|
}
|
|||
|
|
return officialAssetURL(release.Version, release.AssetName)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func downloadProxyPath(frpDir string) string {
|
|||
|
|
return filepath.Join(frpDir, "download-proxy.json")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func readDownloadProxy(frpDir string) (string, error) {
|
|||
|
|
data, err := os.ReadFile(downloadProxyPath(frpDir))
|
|||
|
|
if os.IsNotExist(err) {
|
|||
|
|
return "", nil
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
var state struct {
|
|||
|
|
Proxy string `json:"proxy"`
|
|||
|
|
}
|
|||
|
|
if err := json.Unmarshal(data, &state); err != nil {
|
|||
|
|
return "", fmt.Errorf("FRP 下载代理配置无效")
|
|||
|
|
}
|
|||
|
|
if !validDownloadProxy(state.Proxy) {
|
|||
|
|
return "", fmt.Errorf("FRP 下载代理配置无效")
|
|||
|
|
}
|
|||
|
|
return state.Proxy, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func writeDownloadProxy(frpDir, proxy string) error {
|
|||
|
|
if !validDownloadProxy(proxy) {
|
|||
|
|
return fmt.Errorf("不支持的 FRP 下载加速代理")
|
|||
|
|
}
|
|||
|
|
if err := os.MkdirAll(frpDir, 0o700); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
data, err := json.Marshal(struct {
|
|||
|
|
Proxy string `json:"proxy"`
|
|||
|
|
}{Proxy: proxy})
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return os.WriteFile(downloadProxyPath(frpDir), data, 0o600)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func validDownloadProxy(proxy string) bool {
|
|||
|
|
_, ok := downloadProxyOptions[proxy]
|
|||
|
|
return ok
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func extractClientBinary(archivePath, frpDir string, release Release) (string, error) {
|
|||
|
|
data, err := os.ReadFile(archivePath)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
binary, err := extractBinary(data, release.AssetName)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
path := filepath.Join(frpDir, "bin", release.Version, clientBinaryName())
|
|||
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
if err := os.WriteFile(path, binary, 0o700); err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
return path, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func extractBinary(data []byte, assetName string) ([]byte, error) {
|
|||
|
|
if strings.HasSuffix(assetName, ".zip") {
|
|||
|
|
return extractZipBinary(data)
|
|||
|
|
}
|
|||
|
|
return extractTarBinary(data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func extractZipBinary(data []byte) ([]byte, error) {
|
|||
|
|
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("FRP ZIP 安装包无效:%w", err)
|
|||
|
|
}
|
|||
|
|
for _, file := range reader.File {
|
|||
|
|
if filepath.Base(file.Name) != clientBinaryName() || file.FileInfo().IsDir() {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
return readZipFile(file)
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("FRP 安装包中未找到 %s", clientBinaryName())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func readZipFile(file *zip.File) ([]byte, error) {
|
|||
|
|
if file.UncompressedSize64 > maxArchiveSize {
|
|||
|
|
return nil, fmt.Errorf("FRP 客户端文件超过大小限制")
|
|||
|
|
}
|
|||
|
|
reader, err := file.Open()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
defer reader.Close()
|
|||
|
|
return boundedRead(reader, maxArchiveSize)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func extractTarBinary(data []byte) ([]byte, error) {
|
|||
|
|
gzipReader, err := gzip.NewReader(bytes.NewReader(data))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("FRP TAR.GZ 安装包无效:%w", err)
|
|||
|
|
}
|
|||
|
|
defer gzipReader.Close()
|
|||
|
|
reader := tar.NewReader(gzipReader)
|
|||
|
|
for {
|
|||
|
|
header, err := reader.Next()
|
|||
|
|
if err == io.EOF {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if header.Typeflag != tar.TypeReg || filepath.Base(header.Name) != clientBinaryName() {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if header.Size > maxArchiveSize {
|
|||
|
|
return nil, fmt.Errorf("FRP 客户端文件超过大小限制")
|
|||
|
|
}
|
|||
|
|
return boundedRead(reader, maxArchiveSize)
|
|||
|
|
}
|
|||
|
|
return nil, fmt.Errorf("FRP 安装包中未找到 %s", clientBinaryName())
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func boundedRead(reader io.Reader, limit int64) ([]byte, error) {
|
|||
|
|
data, err := io.ReadAll(io.LimitReader(reader, limit+1))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if int64(len(data)) > limit {
|
|||
|
|
return nil, fmt.Errorf("FRP 客户端文件超过大小限制")
|
|||
|
|
}
|
|||
|
|
return data, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func clientBinaryName() string {
|
|||
|
|
if runtime.GOOS == "windows" {
|
|||
|
|
return "frpc.exe"
|
|||
|
|
}
|
|||
|
|
return "frpc"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func listBinaries(frpDir, active string) ([]Binary, error) {
|
|||
|
|
root := filepath.Join(frpDir, "bin")
|
|||
|
|
entries, err := os.ReadDir(root)
|
|||
|
|
if os.IsNotExist(err) {
|
|||
|
|
return []Binary{}, nil
|
|||
|
|
}
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
binaries := make([]Binary, 0, len(entries))
|
|||
|
|
for _, entry := range entries {
|
|||
|
|
binary, ok := binaryFromEntry(root, entry, active)
|
|||
|
|
if ok {
|
|||
|
|
binaries = append(binaries, binary)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
sort.Slice(binaries, func(i, j int) bool { return binaries[i].Version > binaries[j].Version })
|
|||
|
|
return binaries, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func binaryFromEntry(root string, entry os.DirEntry, active string) (Binary, bool) {
|
|||
|
|
if !entry.IsDir() || !validVersion(entry.Name()) {
|
|||
|
|
return Binary{}, false
|
|||
|
|
}
|
|||
|
|
path := filepath.Join(root, entry.Name(), clientBinaryName())
|
|||
|
|
info, err := os.Stat(path)
|
|||
|
|
if err != nil || info.IsDir() {
|
|||
|
|
return Binary{}, false
|
|||
|
|
}
|
|||
|
|
return Binary{Version: entry.Name(), Path: path, Size: info.Size(), InstalledAt: info.ModTime().UTC().Format(time.RFC3339), Active: filepath.Clean(path) == filepath.Clean(active)}, true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func binaryPathForVersion(frpDir, version string) (string, error) {
|
|||
|
|
if !validVersion(version) {
|
|||
|
|
return "", fmt.Errorf("FRP 版本格式无效")
|
|||
|
|
}
|
|||
|
|
path := filepath.Join(frpDir, "bin", version, clientBinaryName())
|
|||
|
|
if !fileExists(path) {
|
|||
|
|
return "", fmt.Errorf("FRP %s 尚未下载", version)
|
|||
|
|
}
|
|||
|
|
return path, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func removeBinaryVersion(frpDir, version, binaryPath string) error {
|
|||
|
|
root, err := filepath.Abs(filepath.Join(frpDir, "bin"))
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
target, err := filepath.Abs(filepath.Dir(binaryPath))
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
relative, err := filepath.Rel(root, target)
|
|||
|
|
if err != nil || filepath.Clean(relative) != version {
|
|||
|
|
return fmt.Errorf("FRP 客户端目录无效")
|
|||
|
|
}
|
|||
|
|
return os.RemoveAll(target)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func validVersion(version string) bool {
|
|||
|
|
if version == "" || len(version) > 32 || strings.ContainsAny(version, `/\\`) {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
for _, char := range version {
|
|||
|
|
if (char < '0' || char > '9') && char != '.' && char != 'v' && char != '-' {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func activeBinaryPath(frpDir string) string {
|
|||
|
|
return filepath.Join(frpDir, "bin", "active.json")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func readActiveBinary(frpDir string) (string, error) {
|
|||
|
|
data, err := os.ReadFile(activeBinaryPath(frpDir))
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
var state struct {
|
|||
|
|
Path string `json:"path"`
|
|||
|
|
}
|
|||
|
|
if err := json.Unmarshal(data, &state); err != nil || versionFromBinaryPath(frpDir, state.Path) == "" || !fileExists(state.Path) {
|
|||
|
|
return "", fmt.Errorf("已启用的 FRP 客户端无效")
|
|||
|
|
}
|
|||
|
|
return state.Path, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func writeActiveBinary(frpDir, path string) error {
|
|||
|
|
if err := os.MkdirAll(filepath.Dir(activeBinaryPath(frpDir)), 0o700); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
data, err := json.Marshal(struct {
|
|||
|
|
Path string `json:"path"`
|
|||
|
|
}{Path: path})
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return os.WriteFile(activeBinaryPath(frpDir), data, 0o600)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func versionFromBinaryPath(frpDir, path string) string {
|
|||
|
|
root := filepath.Join(frpDir, "bin")
|
|||
|
|
relative, err := filepath.Rel(root, path)
|
|||
|
|
if err != nil || strings.HasPrefix(relative, "..") {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
parts := strings.Split(filepath.ToSlash(relative), "/")
|
|||
|
|
if len(parts) != 2 || !validVersion(parts[0]) {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return parts[0]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func fileExists(path string) bool {
|
|||
|
|
_, err := os.Stat(path)
|
|||
|
|
return err == nil
|
|||
|
|
}
|