Release 3.8.0.1: unify apphost, harden lifecycle and core SHA checks.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
package coredl
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReleaseAsset is one GitHub release file (optional SHA-256 digest).
|
||||
type ReleaseAsset struct {
|
||||
Name string
|
||||
BrowserDownloadURL string
|
||||
SHA256 string // lowercase hex, empty if unknown
|
||||
}
|
||||
|
||||
type ghRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Assets []struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
Digest string `json:"digest"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
// FetchLatestAssets loads assets for a repo's latest GitHub release.
|
||||
func FetchLatestAssets(apiLatestURL string) (tag string, assets []ReleaseAsset, err error) {
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
req, err := http.NewRequest(http.MethodGet, apiLatestURL, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", "navis-vpnclient")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("github api: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return "", nil, fmt.Errorf("github api: %s: %s", resp.Status, string(body))
|
||||
}
|
||||
var rel ghRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
out := make([]ReleaseAsset, 0, len(rel.Assets))
|
||||
for _, a := range rel.Assets {
|
||||
out = append(out, ReleaseAsset{
|
||||
Name: a.Name,
|
||||
BrowserDownloadURL: a.BrowserDownloadURL,
|
||||
SHA256: parseDigestSHA256(a.Digest),
|
||||
})
|
||||
}
|
||||
return rel.TagName, out, nil
|
||||
}
|
||||
|
||||
func parseDigestSHA256(digest string) string {
|
||||
digest = strings.TrimSpace(strings.ToLower(digest))
|
||||
if digest == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(digest, "sha256:") {
|
||||
return strings.TrimPrefix(digest, "sha256:")
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DownloadFile writes url to path (size-capped).
|
||||
func DownloadFile(path, url string, maxBytes int64) error {
|
||||
if maxBytes <= 0 {
|
||||
maxBytes = 120 << 20
|
||||
}
|
||||
client := &http.Client{Timeout: 15 * time.Minute}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download %s: %s", url, resp.Status)
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = io.Copy(f, io.LimitReader(resp.Body, maxBytes))
|
||||
return err
|
||||
}
|
||||
|
||||
// FileSHA256 returns lowercase hex digest of path.
|
||||
func FileSHA256(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, io.LimitReader(f, 200<<20)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// VerifySHA256 compares file digest to want (lowercase hex). Empty want is an error.
|
||||
func VerifySHA256(path, want string) error {
|
||||
want = strings.TrimSpace(strings.ToLower(want))
|
||||
if want == "" {
|
||||
return fmt.Errorf("coredl: пустой sha256 — загрузка отклонена")
|
||||
}
|
||||
got, err := FileSHA256(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if got != want {
|
||||
return fmt.Errorf("coredl: sha256 mismatch: got %s want %s", got, want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequireSHA returns sha or an error if the asset has none.
|
||||
func RequireSHA(a ReleaseAsset) (string, error) {
|
||||
if a.SHA256 != "" {
|
||||
return a.SHA256, nil
|
||||
}
|
||||
return "", fmt.Errorf("coredl: у ассета %s нет sha256 в GitHub API — установка отклонена", a.Name)
|
||||
}
|
||||
|
||||
// ResolveSHA picks digest from the asset or companion .dgst/.sha256 assets in the same release.
|
||||
func ResolveSHA(a ReleaseAsset, all []ReleaseAsset) (string, error) {
|
||||
if a.SHA256 != "" {
|
||||
return a.SHA256, nil
|
||||
}
|
||||
companions := []string{a.Name + ".dgst", a.Name + ".sha256", "SHA256SUMS", "sha256sums"}
|
||||
for _, name := range companions {
|
||||
for _, o := range all {
|
||||
if !strings.EqualFold(o.Name, name) {
|
||||
continue
|
||||
}
|
||||
sum, err := fetchChecksumFile(o.BrowserDownloadURL, a.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if sum != "" {
|
||||
return sum, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return RequireSHA(a)
|
||||
}
|
||||
|
||||
func fetchChecksumFile(url, assetName string) (string, error) {
|
||||
client := &http.Client{Timeout: 60 * time.Second}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("checksum download: %s", resp.Status)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := string(body)
|
||||
lowerName := strings.ToLower(assetName)
|
||||
lines := strings.Split(text, "\n")
|
||||
var shaLine string
|
||||
for _, line := range lines {
|
||||
l := strings.TrimSpace(strings.ToLower(line))
|
||||
if strings.HasPrefix(l, "sha256:") {
|
||||
shaLine = strings.TrimSpace(line[len("sha256:"):])
|
||||
// keep scanning — prefer line that also mentions the asset
|
||||
if strings.Contains(l, lowerName) {
|
||||
return strings.ToLower(strings.Fields(shaLine)[0]), nil
|
||||
}
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
sum := strings.ToLower(fields[0])
|
||||
file := strings.TrimPrefix(fields[len(fields)-1], "*")
|
||||
if len(sum) == 64 && strings.EqualFold(file, assetName) {
|
||||
return sum, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if shaLine != "" {
|
||||
parts := strings.Fields(shaLine)
|
||||
if len(parts) > 0 && len(parts[0]) == 64 {
|
||||
return strings.ToLower(parts[0]), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// DownloadAndVerify downloads url to path and checks sha256.
|
||||
func DownloadAndVerify(path, url, wantSHA256 string) error {
|
||||
if err := DownloadFile(path, url, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := VerifySHA256(path, wantSHA256); err != nil {
|
||||
_ = os.Remove(path)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user