Files
ARTEX/selfupdate/github.go
T
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

144 lines
4.8 KiB
Go

package selfupdate
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
// Repo is the release source. Hardcoded rather than configurable: a configurable
// update source hands anyone who can change the config a remote code execution
// channel — a door a penetration testing platform must never open.
const Repo = "Autumn-27/artex"
// latestURL is GitHub's "latest stable release" endpoint. It automatically skips prereleases and drafts.
const latestURL = "https://api.github.com/repos/" + Repo + "/releases/latest"
// allowedHosts restricts the domains the upgrade chain may visit. Together with
// checkRedirect below, any hop redirected to a host outside this list fails
// immediately — this is the first gate against DNS poisoning / MITM swapping out
// the binary; the second is the SHA256SUMS comparison.
var allowedHosts = map[string]bool{
"api.github.com": true,
"github.com": true,
"objects.githubusercontent.com": true, // the object storage where release assets actually land
"release-assets.githubusercontent.com": true,
"raw.githubusercontent.com": true,
}
// Release holds the fields we care about in a GitHub Release.
type Release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
PublishedAt time.Time `json:"published_at"`
HTMLURL string `json:"html_url"`
Assets []Asset `json:"assets"`
}
// Asset is a file attached to a Release.
type Asset struct {
Name string `json:"name"`
URL string `json:"browser_download_url"`
Size int64 `json:"size"`
}
// NewClient builds an HTTP client that only trusts GitHub domains. Direct
// connection when proxy is empty.
//
// Deliberately does not reuse the default Transport: the upgrade chain must force
// TLS with certificate verification, and must not be affected by things like
// InsecureSkipVerify set elsewhere.
func NewClient(proxy string) *http.Client {
tr := &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 15 * time.Second,
}
if p := strings.TrimSpace(proxy); p != "" {
if pu, err := url.Parse(p); err == nil {
tr.Proxy = http.ProxyURL(pu)
}
}
return &http.Client{
Transport: tr,
Timeout: 30 * time.Minute, // downloading a full package must not be killed by a request-level timeout
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("too many redirects")
}
return checkURL(req.URL)
},
}
}
// checkURL enforces https + a domain whitelist.
func checkURL(u *url.URL) error {
if u.Scheme != "https" {
return fmt.Errorf("refusing non-HTTPS address: %s", u.Scheme+"://"+u.Host)
}
if !allowedHosts[strings.ToLower(u.Hostname())] {
return fmt.Errorf("refusing non-GitHub domain: %s", u.Hostname())
}
return nil
}
// FetchLatest queries the latest stable release.
func FetchLatest(ctx context.Context, c *http.Client) (*Release, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, latestURL, nil)
if err != nil {
return nil, err
}
if err := checkURL(req.URL); err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "artex-selfupdate")
resp, err := c.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to reach GitHub (a global proxy can be configured in system settings): %w", err)
}
defer resp.Body.Close()
switch {
case resp.StatusCode == http.StatusForbidden, resp.StatusCode == http.StatusTooManyRequests:
// Unauthenticated GitHub API allows 60 requests per IP per hour; easy to hit when sharing an egress IP.
return nil, fmt.Errorf("GitHub API rate limit (60 per hour), please try again later")
case resp.StatusCode == http.StatusNotFound:
return nil, fmt.Errorf("repository %s has no stable release yet", Repo)
case resp.StatusCode != http.StatusOK:
return nil, fmt.Errorf("GitHub returned %d", resp.StatusCode)
}
var rel Release
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return nil, fmt.Errorf("failed to parse Release: %w", err)
}
if strings.TrimSpace(rel.TagName) == "" {
return nil, fmt.Errorf("Release is missing a tag")
}
return &rel, nil
}
// AssetName returns the release package name for the current platform, matching
// build.sh's package_binary: artex-<version>-<os>-<arch>.zip (version without the v prefix).
func AssetName(tag, goos, goarch string) string {
return fmt.Sprintf("artex-%s-%s-%s.zip", strings.TrimPrefix(tag, "v"), goos, goarch)
}
// FindAsset finds an asset by name in the Release.
func (r *Release) FindAsset(name string) (Asset, bool) {
for _, a := range r.Assets {
if strings.EqualFold(a.Name, name) {
return a, true
}
}
return Asset{}, false
}