97 lines
3.5 KiB
Go
Executable File
97 lines
3.5 KiB
Go
Executable File
// Summon: download a source tarball with hash verification.
|
|
//
|
|
// Replaces the original Bash `summon` which looped `wget` calls. The Go
|
|
// version uses net/http so we get redirects and retries.
|
|
// The downloaded bytes are streamed through a sha512 hasher in parallel
|
|
// with the file write, so we never read the source twice.
|
|
//
|
|
// If the expected hash is empty we just warn (some DETAILS files omit
|
|
// SOURCE_HASH). If it's present and doesn't match, we delete the file
|
|
// and fail the cast.
|
|
package cast
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha512"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"dcos.net/sorcery-go/pkg/eventbus"
|
|
)
|
|
|
|
// Summon downloads url to destPath and verifies it against expectedHash.
|
|
// expectedHash is in the form "sha512:<hex>" or "" to skip verification.
|
|
func Summon(ctx context.Context, url, destPath, expectedHash string, bus *eventbus.Bus, taskID string) error {
|
|
if bus != nil {
|
|
bus.Log(taskID, "↓ Summoning "+url)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("User-Agent", "Sorcery-Go/1.0 (Sovereign Coven)")
|
|
// NOTE: http.DefaultClient has no per-request timeout of its own.
|
|
// The caller is expected to pass a context with a deadline (e.g.,
|
|
// context.WithTimeout) that bounds this request. This is appropriate
|
|
// because source tarball sizes vary widely — a single fixed timeout
|
|
// would be wrong for both tiny configs and multi-GB kernel sources.
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("summon: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("summon: HTTP %d for %s", resp.StatusCode, url)
|
|
}
|
|
|
|
out, err := os.Create(destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
hasher := sha512.New()
|
|
mw := io.MultiWriter(out, hasher)
|
|
if _, err := io.Copy(mw, resp.Body); err != nil {
|
|
return fmt.Errorf("summon: copy: %w", err)
|
|
}
|
|
|
|
actualHash := "sha512:" + hex.EncodeToString(hasher.Sum(nil))
|
|
if expectedHash == "" {
|
|
if bus != nil {
|
|
bus.Log(taskID, " (no SOURCE_HASH in DETAILS — skipping verification)")
|
|
}
|
|
return nil
|
|
}
|
|
// Some SMGL hashes have uppercase hex or no prefix — normalise.
|
|
if !hashEqual(actualHash, expectedHash) {
|
|
_ = os.Remove(destPath)
|
|
return fmt.Errorf("summon: hash mismatch (expected %s, got %s) — file deleted",
|
|
expectedHash, actualHash)
|
|
}
|
|
if bus != nil {
|
|
bus.Log(taskID, "✓ Hash verified")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hashEqual compares two "sha512:<hex>" strings case-insensitively.
|
|
// If the expected value lacks the "sha512:" prefix we add it.
|
|
func hashEqual(actual, expected string) bool {
|
|
actual = strings.ToLower(actual)
|
|
expected = strings.ToLower(expected)
|
|
if !strings.HasPrefix(expected, "sha512:") {
|
|
expected = "sha512:" + expected
|
|
}
|
|
return actual == expected
|
|
}
|