diff --git a/actions/download_action.go b/actions/download_action.go index 954eb3f..f69dafd 100644 --- a/actions/download_action.go +++ b/actions/download_action.go @@ -29,14 +29,22 @@ See the 'Unpack' action for more information. - compression -- optional hint for unpack allowing to use proper compression method. See the 'Unpack' action for more information. + +- sha256sum -- optional expected SHA256 sum of the downloaded file; provided directly as a 64 characters hexadecimal string */ package actions import ( + "crypto/sha256" + "encoding/hex" "fmt" - "github.com/go-debos/debos" + "io" + "log" "net/url" + "os" "path" + + "github.com/go-debos/debos" ) type DownloadAction struct { @@ -45,6 +53,7 @@ type DownloadAction struct { Filename string // File name, overrides the name from URL. Unpack bool // Unpack downloaded file to directory dedicated for download Compression string // compression type + Sha256sum string // Expected SHA256 sum of the downloaded file Name string // exporting path to file or directory(in case of unpack) } @@ -120,6 +129,15 @@ func (d *DownloadAction) Verify(context *debos.DebosContext) error { return err } } + if len(d.Sha256sum) > 0 { + if len(d.Sha256sum) != 64 { + return fmt.Errorf("invalid length for property 'sha256sum'; expected 64 characters, got %d", len(d.Sha256sum)) + } + _, err := hex.DecodeString(d.Sha256sum) + if err != nil { + return fmt.Errorf("invalid characters in 'sha256sum' property: %v", err) + } + } return nil } @@ -147,6 +165,27 @@ func (d *DownloadAction) Run(context *debos.DebosContext) error { return fmt.Errorf("unsupported URL provided: '%s'", url.String()) } + file, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to open downloaded file %s: %v", filename, err) + } + defer file.Close() + hasher := sha256.New() + _, err = io.Copy(hasher, file) + if err != nil { + return fmt.Errorf("failed to hash file %s: %v", filename, err) + } + + actualSha256sum := hex.EncodeToString(hasher.Sum(nil)) + log.Printf("Downloaded file '%s': sha256sum = %s", filename, actualSha256sum) + + if len(d.Sha256sum) > 0 { + if actualSha256sum != d.Sha256sum { + os.Remove(filename) + return fmt.Errorf("SHA256 sum mismatch for %s. Expected %s but got %s", filename, d.Sha256sum, actualSha256sum) + } + } + if d.Unpack { archive, err := d.archive(filename) if err != nil { diff --git a/actions/download_action_test.go b/actions/download_action_test.go new file mode 100644 index 0000000..92cf5a6 --- /dev/null +++ b/actions/download_action_test.go @@ -0,0 +1,115 @@ +package actions_test + +import ( + "crypto/sha256" + "encoding/hex" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/go-debos/debos" + "github.com/go-debos/debos/actions" + "github.com/stretchr/testify/assert" +) + +func TestDownloadActionSha256sum(t *testing.T) { + // Test HTTP server to serve files with this content + testFileContent := []byte("This is a test file for sha256sum verification.") + hasher := sha256.New() + hasher.Write(testFileContent) + expectedSha256sum := hex.EncodeToString(hasher.Sum(nil)) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(testFileContent) + })) + defer ts.Close() + + // Temporary scratch directory + tmpdir, err := os.MkdirTemp("", "debos-test-") + assert.NoError(t, err) + defer os.RemoveAll(tmpdir) + + context := &debos.DebosContext{ + CommonContext: &debos.CommonContext{ + Origins: make(map[string]string), + Scratchdir: tmpdir, + }, + Architecture: "amd64", + } + + // Test case 1: Correct sha256sum + action1 := actions.DownloadAction{ + Url: ts.URL + "/test-action1", + Name: "test-file-correct", + Sha256sum: expectedSha256sum, + } + + err = action1.Verify(context) + assert.NoError(t, err, "Verify should pass for correct sha256sum") + + err = action1.Run(context) + assert.NoError(t, err, "Run should pass for correct sha256sum") + + downloadedPath1, ok := context.Origins[action1.Name] + assert.True(t, ok, "Origin path should be set") + _, err = os.Stat(downloadedPath1) + assert.NoError(t, err, "Downloaded file should exist") + + // Test case 2: Incorrect sha256sum + action2 := actions.DownloadAction{ + Url: ts.URL + "/test-action2", + Name: "test-file-incorrect", + Sha256sum: "a" + expectedSha256sum[1:], // Mismatched SHA256 sum + } + + err = action2.Verify(context) + assert.NoError(t, err, "Verify should pass even with incorrect sum (runtime check)") + + err = action2.Run(context) + assert.Error(t, err, "Run should fail for incorrect sha256sum") + assert.Contains(t, err.Error(), "SHA256 sum mismatch") + + _, missing := context.Origins[action2.Name] + assert.False(t, missing, "Origin path should not be set on failure") + downloadedPath2 := tmpdir + "/" + action2.Name + _, err = os.Stat(downloadedPath2) + assert.True(t, os.IsNotExist(err), "Downloaded file should be removed on SHA256 sum mismatch") + + // Test case 3: Invalid sha256sum length in Verify + action3 := actions.DownloadAction{ + Url: ts.URL + "/test-action3", + Name: "test-file-invalid-len", + Sha256sum: "abc", // Invalid length + } + err = action3.Verify(context) + assert.Error(t, err, "Verify should fail for invalid sha256sum length") + assert.Contains(t, err.Error(), "invalid length for property 'sha256sum'") + + // Test case 4: Invalid hex characters in Verify + action4 := actions.DownloadAction{ + Url: ts.URL + "/test-action4", + Name: "test-file-invalid-hex", + Sha256sum: expectedSha256sum[:63] + "Z", // Invalid hex character + } + err = action4.Verify(context) + assert.Error(t, err, "Verify should fail for invalid hex characters") + assert.Contains(t, err.Error(), "invalid characters in 'sha256sum' property") + + // Test case 5: No sha256sum provided + action5 := actions.DownloadAction{ + Url: ts.URL + "/test-action5", + Name: "test-file-no-sum", + } + + err = action5.Verify(context) + assert.NoError(t, err, "Verify should pass when no sha256sum is provided") + + err = action5.Run(context) + assert.NoError(t, err, "Run should pass when no sha256sum is provided") + + downloadedPath5, ok := context.Origins[action5.Name] + assert.True(t, ok, "Origin path should be set") + _, err = os.Stat(downloadedPath5) + assert.NoError(t, err, "Downloaded file should exist") +}