mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-20 12:30:04 +00:00
* feat: add browseros-cli self-updater * fix: address review comments for 0327-cli_self_updater * fix: address PR review comments for 0327-cli_self_updater * fix: replace goreleaser with Makefile-based release build Remove .goreleaser.yml (required Pro license for monorepo field) and consolidate cross-compilation into `make release`. CI now uses the same Makefile target, fixing a bug where POSTHOG_API_KEY was missing from release ldflags. * fix: address critical self-updater bugs from code review - Fix SHA256 checksum mismatch: verify archive checksum before extraction instead of verifying extracted binary against archive hash (was always failing). Add VerifyChecksum() and integration test. - Fix JSON field name mismatch: TypeScript was emitting camelCase (publishedAt, archiveFormat) but Go expected snake_case (published_at, archive_format). Manifest parsing was silently broken. - Add decompression size limit (256 MB) to prevent zip/gzip bombs. - Don't update LastCheckedAt on transient errors so retry happens on next CLI invocation instead of waiting 24h.
139 lines
2.9 KiB
Go
139 lines
2.9 KiB
Go
package update
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
)
|
|
|
|
const maxAssetSize = 64 << 20
|
|
const maxBinarySize = 256 << 20
|
|
|
|
func DownloadAsset(ctx context.Context, client *http.Client, asset Asset) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, asset.URL, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("update download returned HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
return readAssetBytes(resp.Body)
|
|
}
|
|
|
|
func readAssetBytes(reader io.Reader) ([]byte, error) {
|
|
limited := io.LimitReader(reader, maxAssetSize+1)
|
|
data, err := io.ReadAll(limited)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(data) > maxAssetSize {
|
|
return nil, fmt.Errorf("update asset exceeds %d bytes", maxAssetSize)
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func ExtractBinary(archive []byte, format string) ([]byte, error) {
|
|
switch format {
|
|
case "tar.gz":
|
|
return extractTarGzBinary(archive)
|
|
case "zip":
|
|
return extractZipBinary(archive)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported archive format %q", format)
|
|
}
|
|
}
|
|
|
|
func extractTarGzBinary(archive []byte) ([]byte, error) {
|
|
gzipReader, err := gzip.NewReader(bytes.NewReader(archive))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gzipReader.Close()
|
|
|
|
tarReader := tar.NewReader(gzipReader)
|
|
return readTarBinary(tarReader)
|
|
}
|
|
|
|
func readTarBinary(reader *tar.Reader) ([]byte, error) {
|
|
var binary []byte
|
|
|
|
for {
|
|
header, err := reader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if header.Typeflag != tar.TypeReg {
|
|
continue
|
|
}
|
|
if binary != nil {
|
|
return nil, fmt.Errorf("archive contains multiple files; expected exactly one binary")
|
|
}
|
|
|
|
binary, err = io.ReadAll(io.LimitReader(reader, maxBinarySize+1))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(binary) > maxBinarySize {
|
|
return nil, fmt.Errorf("extracted binary exceeds %d bytes", maxBinarySize)
|
|
}
|
|
}
|
|
|
|
if binary == nil {
|
|
return nil, fmt.Errorf("archive does not contain a file")
|
|
}
|
|
|
|
return binary, nil
|
|
}
|
|
|
|
func extractZipBinary(archive []byte) ([]byte, error) {
|
|
reader, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var binary []byte
|
|
for _, file := range reader.File {
|
|
if file.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
if binary != nil {
|
|
return nil, fmt.Errorf("archive contains multiple files; expected exactly one binary")
|
|
}
|
|
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
binary, err = io.ReadAll(io.LimitReader(rc, maxBinarySize+1))
|
|
rc.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(binary) > maxBinarySize {
|
|
return nil, fmt.Errorf("extracted binary exceeds %d bytes", maxBinarySize)
|
|
}
|
|
}
|
|
|
|
if binary == nil {
|
|
return nil, fmt.Errorf("archive does not contain a file")
|
|
}
|
|
|
|
return binary, nil
|
|
}
|