Files
Havline/internal/cloudflared/binary.go
T

209 lines
5.9 KiB
Go
Raw Normal View History

package cloudflared
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
const minBinarySize = 10 << 20
// Mirror 是 cloudflared 二进制下载镜像。
type Mirror struct {
ID string `json:"id"`
Name string `json:"name"`
Base string `json:"base"`
}
// DefaultMirrors 返回内置镜像表;Base 为空表示 GitHub 官方源。
func DefaultMirrors() []Mirror {
return []Mirror{
{ID: "official", Name: "GitHub 官方源"},
{ID: "gh-proxy-org", Name: "gh-proxy.org", Base: "https://gh-proxy.org/"},
{ID: "gh-proxy-hk", Name: "gh-proxy 香港", Base: "https://hk.gh-proxy.org/"},
{ID: "gh-proxy-cdn", Name: "gh-proxy CDN", Base: "https://cdn.gh-proxy.org/"},
{ID: "gh-proxy-edgeone", Name: "gh-proxy EdgeOne", Base: "https://edgeone.gh-proxy.org/"},
}
}
func findMirror(id string) Mirror {
id = strings.TrimSpace(id)
for _, mirror := range DefaultMirrors() {
if mirror.ID == id {
return mirror
}
}
return DefaultMirrors()[0]
}
func binaryFileName() string {
name := "cloudflared-" + runtime.GOOS + "-" + runtime.GOARCH
if runtime.GOOS == "windows" {
name += ".exe"
}
return name
}
// DownloadURL 生成指定镜像的下载地址;version 为空表示 latest。
func DownloadURL(mirror Mirror, version string) string {
official := "https://github.com/cloudflare/cloudflared/releases/latest/download/" + binaryFileName()
if strings.TrimSpace(version) != "" {
official = "https://github.com/cloudflare/cloudflared/releases/download/" + strings.TrimSpace(version) + "/" + binaryFileName()
}
if strings.TrimSpace(mirror.Base) == "" {
return official
}
return mirror.Base + official
}
func (s *Service) binaryPath() string {
name := "cloudflared"
if runtime.GOOS == "windows" {
name += ".exe"
}
return filepath.Join(s.cfg.DataDir, "cloudflared", "bin", name)
}
// BinaryVersion 返回本机 cloudflared 版本;未安装返回空串。
func (s *Service) BinaryVersion() string {
path := s.binaryPath()
if _, err := os.Stat(path); err != nil {
return ""
}
out, err := exec.Command(path, "--version").CombinedOutput()
if err != nil {
return ""
}
return strings.TrimSpace(string(out))
}
// DownloadBinary 下载并替换本机 cloudflared 二进制。
func (s *Service) DownloadBinary(ctx context.Context, mirrorID string) (string, error) {
return s.DownloadBinaryVersion(ctx, mirrorID, "")
}
// DownloadBinaryVersion 下载指定版本;version 为空表示最新版。
func (s *Service) DownloadBinaryVersion(ctx context.Context, mirrorID, version string) (string, error) {
mirror := findMirror(mirrorID)
target := s.binaryPath()
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return "", err
}
tmp := target + ".download"
_ = os.Remove(tmp)
if err := fetchFile(ctx, DownloadURL(mirror, version), tmp); err != nil {
_ = os.Remove(tmp)
return "", err
}
info, err := os.Stat(tmp)
if err != nil || info.Size() < minBinarySize {
_ = os.Remove(tmp)
return "", fmt.Errorf("下载文件过小,可能被镜像源截断")
}
if runtime.GOOS != "windows" {
_ = os.Chmod(tmp, 0o755)
}
if out, err := exec.Command(tmp, "--version").CombinedOutput(); err != nil {
_ = os.Remove(tmp)
return "", fmt.Errorf("cloudflared 二进制不可执行: %s", strings.TrimSpace(string(out)))
}
previous := target + ".previous"
if _, err := os.Stat(target); err == nil {
_ = os.Remove(previous)
if err := os.Rename(target, previous); err != nil {
_ = os.Remove(tmp)
return "", err
}
}
if err := os.Rename(tmp, target); err != nil {
_ = os.Rename(previous, target)
_ = os.Remove(tmp)
return "", err
}
return s.BinaryVersion(), nil
}
// LatestBinaryVersion 查询 Cloudflare 官方 GitHub Release 的最新版本。
func (s *Service) LatestBinaryVersion(ctx context.Context) (string, error) {
client := &http.Client{Timeout: 20 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/cloudflare/cloudflared/releases/latest", nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("读取最新版本失败:HTTP %d", resp.StatusCode)
}
var payload struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&payload); err != nil {
return "", err
}
version := strings.TrimSpace(payload.TagName)
if version == "" {
return "", fmt.Errorf("GitHub 未返回 cloudflared 版本")
}
return version, nil
}
// RollbackBinary 恢复上一版 cloudflared 二进制。
func (s *Service) RollbackBinary() (string, error) {
target := s.binaryPath()
previous := target + ".previous"
if _, err := os.Stat(previous); err != nil {
return "", fmt.Errorf("没有可回滚的 cloudflared 版本")
}
failed := target + ".failed"
_ = os.Remove(failed)
if _, err := os.Stat(target); err == nil {
if err := os.Rename(target, failed); err != nil {
return "", err
}
}
if err := os.Rename(previous, target); err != nil {
_ = os.Rename(failed, target)
return "", err
}
_ = os.Remove(failed)
return s.BinaryVersion(), nil
}
func fetchFile(ctx context.Context, url, path string) error {
client := &http.Client{Timeout: 5 * time.Minute}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("下载 cloudflared 失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("下载 cloudflared 失败: HTTP %d", resp.StatusCode)
}
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(file, resp.Body); err != nil {
return fmt.Errorf("写入 cloudflared 失败: %w", err)
}
return nil
}