Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always copy PHP and legacy CLI files if they have changed #94

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ jobs:
touch internal/legacy/archives/php_linux_arm64
touch internal/legacy/archives/php_darwin_amd64
touch internal/legacy/archives/php_darwin_arm64
touch internal/legacy/archives/platform.phar.sha256
touch internal/legacy/archives/php_windows_amd64.sha256
touch internal/legacy/archives/php_linux_amd64.sha256
touch internal/legacy/archives/php_linux_arm64.sha256
touch internal/legacy/archives/php_darwin_amd64.sha256
touch internal/legacy/archives/php_darwin_arm64.sha256

- name: Run linter
uses: golangci/golangci-lint-action@v3
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ internal/legacy/archives/cacert.pem:
mkdir -p internal/legacy/archives
wget https://curl.se/ca/cacert.pem -O internal/legacy/archives/cacert.pem

php: $(PHP_BINARY_PATH)
.PHONY: archive-hashes
archive-hashes:
cd internal/legacy/archives
for f in $(ls -1 | grep -v sha256); do shasum -a 256 "$f" | awk '{print $1}' > "$f".sha256; done
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should prefer MD5 here, the hash is used for integrity check and not for tamper protection and the faster MD5 algorithm should be enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was aiming for hashes not to need computing at runtime at all (#94 (comment)), the performance difference is negligible, and some people like to run automated scanners looking for usage of bad hash algorithms, but I'm fine with MD5 if you prefer.


php: $(PHP_BINARY_PATH) archive-hashes

single: internal/legacy/archives/platform.phar php
command -v goreleaser >/dev/null || go install github.com/goreleaser/goreleaser@latest
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ require (
github.com/gofrs/flock v0.8.1
github.com/mattn/go-isatty v0.0.16
github.com/platformsh/platformify v0.1.2
github.com/spf13/afero v1.9.5
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.15.0
github.com/stretchr/testify v1.8.1
github.com/wk8/go-ordered-map/v2 v2.1.7
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
)
Expand All @@ -20,6 +22,7 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
Expand All @@ -35,8 +38,8 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
Expand Down
9 changes: 6 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
Expand Down Expand Up @@ -260,7 +260,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
Expand Down Expand Up @@ -330,6 +330,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
Expand Down Expand Up @@ -386,6 +387,7 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -409,6 +411,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
Expand Down
101 changes: 101 additions & 0 deletions internal/file/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package file

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"os"

"github.com/spf13/afero"
pjcdawkins marked this conversation as resolved.
Show resolved Hide resolved
)

const hashExt = ".sha256"

// Copy copies a file from the given bytes to destination.
func Copy(destination string, fin []byte) error {
if _, err := os.Stat(destination); err != nil && !os.IsNotExist(err) {
pjcdawkins marked this conversation as resolved.
Show resolved Hide resolved
return fmt.Errorf("could not stat file: %w", err)
}

fout, err := os.Create(destination)
if err != nil {
return fmt.Errorf("could not create file: %w", err)
}
defer fout.Close()

r := bytes.NewReader(fin)

if _, err := io.Copy(fout, r); err != nil {
return fmt.Errorf("could copy file: %w", err)
}

return nil
}

// CheckSize checks if a file exists and has an exact size.
func CheckSize(filename string, size int) (bool, error) {
stat, err := os.Stat(filename)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("could not stat file: %w", err)
}
return stat.Size() == int64(size), nil
}

var testableFS = afero.NewOsFs()

// CheckHash checks if a file has the given SHA256 hash.
// It supports reading the file's current hash from a static file saved next to it with the hashExt extension.
func CheckHash(filename, hash string) (bool, error) {
if fh, err := afero.ReadFile(testableFS, filename+hashExt); err == nil {
return string(fh) == hash, nil
}
fh, err := sha256Sum(testableFS, filename)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
return fh == hash, nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hash should be computed from the file itself, otherwise the hash can be tampered in the same way that the file was tampered.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not used for tamper protection as you say - the pregenerated hashes could almost just be random numbers - I went for real hashes just so there's the possibility of checking against accidental corruption of the copied files themselves and the fallback of being able to recompute when the hash file wasn't copied. But broadly I was going for not having to compute hashes at runtime at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recomputing the hash from the file on every run would theoretically use time and memory that we don't want or need to use - the purpose of this PR is to make sure we have updated files when we change them, the user is free to break their own installation (and it can be fixed again by deleting the main file or hash).


func SaveHash(filename, hash string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to maintain the hash, as the file containing the hash can be tampered with as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see other comments #94 (comment)

return Copy(filename+hashExt, []byte(hash))
}

// sha256Sum calculates the SHA256 hash of a file.
func sha256Sum(fs afero.Fs, filename string) (string, error) {
f, err := fs.Open(filename)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}

// CopyIfChanged copies source data to a destination filename if it has changed.
func CopyIfChanged(destFilename string, source []byte, sourceHash string) error {
sizeOK, err := CheckSize(destFilename, len(source))
if err != nil {
return err
}
if sizeOK {
hashOK, err := CheckHash(destFilename, sourceHash)
if hashOK || err != nil {
return err
}
}
if err := Copy(destFilename, source); err != nil {
return err
}
return SaveHash(destFilename, sourceHash)
}
78 changes: 78 additions & 0 deletions internal/file/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package file

import (
"os"
"testing"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCheckHash(t *testing.T) {
// Temporarily swap to a memory filesystem.
testableFS = afero.NewMemMapFs()
defer func() {
testableFS = afero.NewOsFs()
}()
fs := testableFS

mockContent := "hello world\n"
mockContentHash := "a948904f2f0f479b8f8197694b30184b0d2ed1c1cd2a1ec0fb85d299a192a447"
diffContent := "hello world?\n"
diffContentHash := "d441ffff4b6663c3f150bda9c519a58c0685e34cf13d26e881d7e004f704eeba"

cases := []struct {
name string
content string
writeHash string

checkHash string
shouldFail bool
}{
{
name: "static_hash_good",
writeHash: mockContentHash,
checkHash: mockContentHash,
},
{
name: "static_hash_bad",
writeHash: diffContentHash,
checkHash: mockContentHash,
shouldFail: true,
},
{
name: "dynamic_hash_good",
content: diffContent,
checkHash: diffContentHash,
},
{
name: "dynamic_hash_bad",
content: mockContent,
checkHash: diffContentHash,
shouldFail: true,
},
}

filename := "test"
for _, c := range cases {
require.NoError(t, removeIfExists(fs, filename))
require.NoError(t, removeIfExists(fs, filename+hashExt))
t.Run(c.name, func(t *testing.T) {
require.NoError(t, afero.WriteFile(fs, filename, []byte(c.content), 0o644))
if c.writeHash != "" {
require.NoError(t, afero.WriteFile(fs, filename+hashExt, []byte(c.writeHash), 0o644))
}
hashOK, err := CheckHash(filename, c.checkHash)
assert.NoError(t, err)
assert.Equal(t, !c.shouldFail, hashOK)
})
}
}

func removeIfExists(fs afero.Fs, filename string) error {
if err := fs.Remove(filename); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
61 changes: 9 additions & 52 deletions internal/legacy/legacy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package legacy

import (
"bytes"
"context"
_ "embed"
"fmt"
Expand All @@ -12,11 +11,16 @@ import (
"path"

"github.com/gofrs/flock"

"github.com/platformsh/cli/internal/file"
)

//go:embed archives/platform.phar
var pshCLI []byte

//go:embed archives/platform.phar.sha256
var pshCLIHash string

var (
PSHVersion = "0.0.0"
PHPVersion = "0.0.0"
Expand All @@ -27,27 +31,6 @@ const prefix = "psh-go"
var phpPath = fmt.Sprintf("php-%s", PHPVersion)
var pshPath = fmt.Sprintf("psh-%s", PSHVersion)

// copyFile from the given bytes to destination
func copyFile(destination string, fin []byte) error {
if _, err := os.Stat(destination); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("could not stat file: %w", err)
}

fout, err := os.Create(destination)
if err != nil {
return fmt.Errorf("could not create file: %w", err)
}
defer fout.Close()

r := bytes.NewReader(fin)

if _, err := io.Copy(fout, r); err != nil {
return fmt.Errorf("could copy file: %w", err)
}

return nil
}

// CLIWrapper wraps the legacy CLI
type CLIWrapper struct {
Stdout io.Writer
Expand Down Expand Up @@ -78,28 +61,11 @@ func (c *CLIWrapper) Init() error {
//nolint:errcheck
defer fileLock.Unlock()

if _, err := os.Stat(c.PSHPath()); os.IsNotExist(err) {
if c.CustomPshCliPath != "" {
return fmt.Errorf("given PSH phar path does not exist: %w", err)
}

c.debugLog("PSH .phar file does not exist, copying: %s", c.PSHPath())
if err := c.copyPSH(); err != nil {
return fmt.Errorf("could not copy files: %w", err)
}
if err := c.copyPSH(); err != nil {
return fmt.Errorf("could not copy files: %w", err)
}

if _, err := os.Stat(c.PHPPath()); os.IsNotExist(err) {
c.debugLog("PHP binary does not exist, copying: %s", c.PHPPath())
if err := c.copyPHP(); err != nil {
return fmt.Errorf("could not copy files: %w", err)
}
if err := os.Chmod(c.PHPPath(), 0o700); err != nil {
return fmt.Errorf("could not make PHP executable: %w", err)
}
}

return nil
return c.copyPHP()
}

// Exec a legacy CLI command with the given arguments
Expand Down Expand Up @@ -156,16 +122,7 @@ func (c *CLIWrapper) PSHPath() string {

// copyPSH to destination, if it does not exist
func (c *CLIWrapper) copyPSH() error {
// Do not copy the file, if a custom path was given
if c.CustomPshCliPath != "" {
return nil
}

if err := copyFile(c.PSHPath(), pshCLI); err != nil {
return fmt.Errorf("could not copy legacy Platform.sh CLI: %w", err)
}

return nil
return file.CopyIfChanged(c.PSHPath(), pshCLI, pshCLIHash)
}

// debugLog logs a debugging message, if debug is enabled
Expand Down
3 changes: 3 additions & 0 deletions internal/legacy/php_darwin_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ import (

//go:embed archives/php_darwin_amd64
var phpCLI []byte

//go:embed archives/php_darwin_amd64.sha256
var phpCLIHash string
3 changes: 3 additions & 0 deletions internal/legacy/php_darwin_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ import (

//go:embed archives/php_darwin_arm64
var phpCLI []byte

//go:embed archives/php_darwin_arm64.sha256
var phpCLIHash string
Loading