// 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:" 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:" 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 }