126 lines
3.6 KiB
Go
126 lines
3.6 KiB
Go
package app
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
const coreReleaseEndpoint = "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest"
|
||
|
||
var stableVersionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)$`)
|
||
|
||
type coreRelease struct {
|
||
Tag string `json:"tag_name"`
|
||
Published string `json:"published_at"`
|
||
Draft bool `json:"draft"`
|
||
Prerelease bool `json:"prerelease"`
|
||
}
|
||
|
||
func stableVersion(version string) ([3]int, bool) {
|
||
var parts [3]int
|
||
match := stableVersionPattern.FindStringSubmatch(strings.TrimSpace(version))
|
||
if len(match) != 4 {
|
||
return parts, false
|
||
}
|
||
for i := range parts {
|
||
n, err := strconv.Atoi(match[i+1])
|
||
if err != nil {
|
||
return parts, false
|
||
}
|
||
parts[i] = n
|
||
}
|
||
return parts, true
|
||
}
|
||
|
||
func newerStableVersion(current, latest string) *bool {
|
||
a, currentOK := stableVersion(current)
|
||
b, latestOK := stableVersion(latest)
|
||
if !currentOK || !latestOK {
|
||
return nil
|
||
}
|
||
newer := false
|
||
for i := range a {
|
||
if b[i] != a[i] {
|
||
newer = b[i] > a[i]
|
||
break
|
||
}
|
||
}
|
||
return &newer
|
||
}
|
||
|
||
func fetchCoreRelease(ctx context.Context, client *http.Client) (*coreRelease, error) {
|
||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||
defer cancel()
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, coreReleaseEndpoint, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("Accept", "application/vnd.github+json")
|
||
req.Header.Set("User-Agent", "ClashWeb-update-check")
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return nil, errors.New("无法连接 GitHub,请检查网络后重试")
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests {
|
||
return nil, errors.New("GitHub 更新检查暂时受限,请稍后重试")
|
||
}
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("更新检查失败(HTTP %d)", resp.StatusCode)
|
||
}
|
||
data, err := io.ReadAll(io.LimitReader(resp.Body, (2<<20)+1))
|
||
if err != nil || len(data) > 2<<20 {
|
||
return nil, errors.New("版本信息读取失败")
|
||
}
|
||
var release coreRelease
|
||
if err := json.Unmarshal(data, &release); err != nil {
|
||
return nil, errors.New("版本信息格式错误")
|
||
}
|
||
if _, ok := stableVersion(release.Tag); !ok || release.Draft || release.Prerelease {
|
||
return nil, errors.New("未获取到有效的 Mihomo 稳定版本")
|
||
}
|
||
return &release, nil
|
||
}
|
||
|
||
func (a *App) latestCoreRelease(ctx context.Context) (*coreRelease, time.Time, error) {
|
||
a.updatesMu.Lock()
|
||
defer a.updatesMu.Unlock()
|
||
if a.updateCache != nil && time.Since(a.updateTime) < 10*time.Minute {
|
||
return a.updateCache, a.updateTime, nil
|
||
}
|
||
release, err := fetchCoreRelease(ctx, a.download)
|
||
if err != nil {
|
||
return nil, time.Time{}, err
|
||
}
|
||
a.updateCache, a.updateTime = release, time.Now()
|
||
return release, a.updateTime, nil
|
||
}
|
||
|
||
func (a *App) coreUpdates(w http.ResponseWriter, r *http.Request) {
|
||
release, checked, err := a.latestCoreRelease(r.Context())
|
||
if err != nil {
|
||
writeError(w, http.StatusBadGateway, err.Error())
|
||
return
|
||
}
|
||
var version struct {
|
||
Version string `json:"version"`
|
||
}
|
||
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||
defer cancel()
|
||
_ = a.core.Request(ctx, http.MethodGet, "/version", nil, &version)
|
||
jsonResponse(w, http.StatusOK, map[string]any{
|
||
"currentVersion": version.Version, "latestVersion": release.Tag,
|
||
"updateAvailable": newerStableVersion(version.Version, release.Tag),
|
||
"checkedAt": checked.Format(time.RFC3339), "publishedAt": release.Published,
|
||
"releaseURL": "https://github.com/MetaCubeX/mihomo/releases/tag/" + release.Tag,
|
||
})
|
||
}
|