mirror of
https://github.com/outbackdingo/debos.git
synced 2026-01-27 10:18:47 +00:00
feat: sha256sum for download action
Unconditionally log SHA256 sum of downloads and add an optional sha256sum property. Setting this property will ensure verification of the downloaded file and delete it on mismatch. Signed-off-by: Loïc Minier <loic.minier@oss.qualcomm.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
115
actions/download_action_test.go
Normal file
115
actions/download_action_test.go
Normal file
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user