aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-09-27 00:01:22 +0700
committerShulhan <ms@kilabit.info>2023-09-27 00:54:10 +0700
commitd1b393b0d4ca75c8d81d051a332597b7aa3f0ab0 (patch)
tree68adc1e0bfee5c6e2056110dc094cb520a5b969a
parent59968a1665735990187c05547faab3c5310c4be1 (diff)
downloadawwan-d1b393b0d4ca75c8d81d051a332597b7aa3f0ab0.tar.xz
all: make .Vars, .Val, and .Vals panic if values is empty
This is to prevent copying or executing command with value that are not defined or typo which make the result empty and may result in undefined behaviour. For example if we have "app_dir = /data/app" and command in the script that remove that directory recursively, sudo rm -r {{.Val "::app_dir}}/bin will result removing "/bin" entirely.
-rw-r--r--awwan.go4
-rw-r--r--awwan_test.go58
-rw-r--r--script.go21
-rw-r--r--script_example_test.go2
-rw-r--r--session.go71
-rw-r--r--testdata/local/local.aww4
-rw-r--r--testdata/local/local.data (renamed from testdata/local/test.data)8
-rw-r--r--testdata/local/missing_val_encrypted.txt2
-rw-r--r--testdata/local/plain.txt1
-rw-r--r--testdata/local/put.aww5
-rw-r--r--testdata/local/put.data16
11 files changed, 117 insertions, 75 deletions
diff --git a/awwan.go b/awwan.go
index 4c20d52..e506ca5 100644
--- a/awwan.go
+++ b/awwan.go
@@ -209,7 +209,7 @@ func (aww *Awwan) Local(req *Request) (err error) {
if len(req.Content) == 0 {
req.script, err = NewScript(ses, req.scriptPath)
} else {
- req.script, err = ParseScript(ses, req.Content)
+ req.script, err = ParseScript(ses, req.scriptPath, req.Content)
}
if err != nil {
return fmt.Errorf("%s: %w", logp, err)
@@ -288,7 +288,7 @@ func (aww *Awwan) Play(req *Request) (err error) {
if len(req.Content) == 0 {
req.script, err = NewScript(ses, req.scriptPath)
} else {
- req.script, err = ParseScript(ses, req.Content)
+ req.script, err = ParseScript(ses, req.scriptPath, req.Content)
}
if err != nil {
return fmt.Errorf("%s: %w", logp, err)
diff --git a/awwan_test.go b/awwan_test.go
index 349072b..acf54f6 100644
--- a/awwan_test.go
+++ b/awwan_test.go
@@ -163,7 +163,7 @@ func TestAwwanLocal_withEncryption(t *testing.T) {
err error
)
- tdata, err = test.LoadData(`testdata/local/test.data`)
+ tdata, err = test.LoadData(`testdata/local/local.data`)
if err != nil {
t.Fatal(err)
}
@@ -216,58 +216,63 @@ func TestAwwanLocal_withEncryption(t *testing.T) {
func TestAwwanLocalPut(t *testing.T) {
type testCase struct {
- desc string
- passphrase string
- lineRange string
- fileDest string
- tdataStdout string
- tdataFileOut string
- expError string
+ desc string
+ passphrase string
+ lineRange string
+ fileDest string
+ expError string
+ expStderr string
+ expContent string
}
// Load the test data output.
var (
baseDir = filepath.Join(`testdata`, `local`)
+ script = filepath.Join(baseDir, `put.aww`)
tdata *test.Data
err error
)
- tdata, err = test.LoadData(filepath.Join(baseDir, `test.data`))
+
+ tdata, err = test.LoadData(filepath.Join(baseDir, `put.data`))
if err != nil {
t.Fatal(err)
}
var cases = []testCase{{
- desc: `With text file`,
- lineRange: `3`,
- fileDest: filepath.Join(baseDir, `tmp`, `plain.txt`),
- tdataFileOut: `tmp/plain.txt`,
+ desc: `With text file`,
+ lineRange: `1`,
+ fileDest: filepath.Join(baseDir, `tmp`, `plain.txt`),
+ expContent: string(tdata.Output[`tmp/plain.txt`]),
+ }, {
+
+ desc: `With text file, one of value is encrypted`,
+ lineRange: `3`,
+ expStderr: string(tdata.Output[`missing_val_encrypted`]),
}, {
- desc: `With encrypted file`,
- lineRange: `5`,
- fileDest: filepath.Join(baseDir, `tmp`, `decrypted.txt`),
- tdataFileOut: `tmp/decrypted.txt`,
- passphrase: "s3cret\r",
+ desc: `With encrypted file`,
+ lineRange: `5`,
+ passphrase: "s3cret\r",
+ fileDest: filepath.Join(baseDir, `tmp`, `decrypted.txt`),
+ expContent: string(tdata.Output[`tmp/decrypted.txt`]),
}, {
desc: `With encrypted file, empty passphrase`,
- expError: "!!! Copy: generateFileInput: private key is missing or not loaded\n",
lineRange: `5`,
+ expStderr: string(tdata.Output[`encrypted_empty_passphrase.stderr`]),
}, {
desc: `With encrypted file, invalid passphrase`,
passphrase: "invalid\r",
lineRange: `5`,
- expError: `Local: NewSession: loadEnvFromPaths: LoadPrivateKeyInteractive: x509: decryption password incorrect`,
+ expError: string(tdata.Output[`encrypted_invalid_passphrase`]),
}}
var (
- script = filepath.Join(baseDir, `local.aww`)
mockout = bytes.Buffer{}
mockerr = bytes.Buffer{}
mockrw = mock.ReadWriter{}
aww *Awwan
c testCase
- expContent []byte
gotContent []byte
)
for _, c = range cases {
@@ -296,14 +301,14 @@ func TestAwwanLocalPut(t *testing.T) {
err = aww.Local(req)
if err != nil {
- test.Assert(t, c.desc, c.expError, err.Error())
- return
+ test.Assert(t, `Local error`, c.expError, err.Error())
+ continue
}
// The stdout cannot be asserted since its print dynamic
// paths.
- test.Assert(t, `stderr`, c.expError, mockerr.String())
+ test.Assert(t, `stderr`, c.expStderr, mockerr.String())
if len(c.fileDest) != 0 {
gotContent, err = os.ReadFile(c.fileDest)
@@ -311,8 +316,7 @@ func TestAwwanLocalPut(t *testing.T) {
t.Fatal(err)
}
- expContent = tdata.Output[c.tdataFileOut]
- test.Assert(t, `content`, string(expContent), string(gotContent))
+ test.Assert(t, `content`, c.expContent, string(gotContent))
}
}
}
diff --git a/script.go b/script.go
index cb25e16..8b50bd7 100644
--- a/script.go
+++ b/script.go
@@ -7,7 +7,6 @@ import (
"bytes"
"fmt"
"os"
- "text/template"
)
// Script define the content of ".aww" file, line by line.
@@ -31,7 +30,7 @@ func NewScript(ses *Session, path string) (script *Script, err error) {
return nil, fmt.Errorf("%s: %w", logp, err)
}
- script, err = ParseScript(ses, content)
+ script, err = ParseScript(ses, path, content)
if err != nil {
return nil, fmt.Errorf("%s: %w", logp, err)
}
@@ -40,12 +39,10 @@ func NewScript(ses *Session, path string) (script *Script, err error) {
// ParseScript parse the script content by applying the session
// variables and splitting it into Statement.
-func ParseScript(ses *Session, content []byte) (script *Script, err error) {
+func ParseScript(ses *Session, path string, content []byte) (script *Script, err error) {
var (
logp = `ParseScript`
- tmpl *template.Template
- buf bytes.Buffer
stmt *Statement
line []byte
raw []byte
@@ -56,21 +53,11 @@ func ParseScript(ses *Session, content []byte) (script *Script, err error) {
x int
)
- // Apply the session variables.
- tmpl = template.New("aww")
-
- tmpl, err = tmpl.Parse(string(content))
+ raw, err = ses.render(path, content)
if err != nil {
- return nil, fmt.Errorf("%s: %w", logp, err)
+ return nil, fmt.Errorf(`%s: %w`, logp, err)
}
- err = tmpl.Execute(&buf, ses)
- if err != nil {
- return nil, fmt.Errorf("%s: %w", logp, err)
- }
-
- raw = buf.Bytes()
-
raw = bytes.TrimRight(raw, " \t\r\n\v")
splits = bytes.Split(raw, newLine)
diff --git a/script_example_test.go b/script_example_test.go
index e0156af..05793e4 100644
--- a/script_example_test.go
+++ b/script_example_test.go
@@ -33,7 +33,7 @@ end;
log.Fatal(err)
}
- s, err = ParseScript(ses, []byte(scriptContent))
+ s, err = ParseScript(ses, `scriptContent`, []byte(scriptContent))
if err != nil {
log.Fatal(err)
}
diff --git a/session.go b/session.go
index af308db..1936b24 100644
--- a/session.go
+++ b/session.go
@@ -85,18 +85,36 @@ func (ses *Session) Subs(secName string) (subs []*ini.Section) {
}
// Vars return all variables in section and/or subsection as map of string.
+// It will panic if the no variables found.
func (ses *Session) Vars(path string) (vars map[string]string) {
- return ses.vars.Vars(path)
+ vars = ses.vars.Vars(path)
+ if len(vars) == 0 {
+ var msg = fmt.Sprintf(`%q is empty`, path)
+ panic(msg)
+ }
+ return vars
}
// Val return the last variable value defined in key path.
-func (ses *Session) Val(keyPath string) string {
- return ses.vars.Val(keyPath)
+// It will panic if the value is empty.
+func (ses *Session) Val(keyPath string) (val string) {
+ val = ses.vars.Val(keyPath)
+ if len(val) == 0 {
+ var msg = fmt.Sprintf(`%q is empty`, keyPath)
+ panic(msg)
+ }
+ return val
}
// Vals return all variable values as slice of string.
-func (ses *Session) Vals(keyPath string) []string {
- return ses.vars.Vals(keyPath)
+// It will panic if the no variables found.
+func (ses *Session) Vals(keyPath string) (list []string) {
+ list = ses.vars.Vals(keyPath)
+ if len(list) == 0 {
+ var msg = fmt.Sprintf(`%q is empty`, keyPath)
+ panic(msg)
+ }
+ return list
}
// Copy file in local system.
@@ -453,16 +471,9 @@ func (ses *Session) generateFileInput(in string) (out string, isVault bool, err
return ``, false, fmt.Errorf(`%s: %w`, logp, err)
}
- var tmpl = template.New(in)
+ var contentOut []byte
- tmpl, err = tmpl.Parse(string(contentInput))
- if err != nil {
- return ``, false, fmt.Errorf(`%s: %w`, logp, err)
- }
-
- var contentOut bytes.Buffer
-
- err = tmpl.Execute(&contentOut, ses)
+ contentOut, err = ses.render(in, contentInput)
if err != nil {
return ``, false, fmt.Errorf(`%s: %w`, logp, err)
}
@@ -479,7 +490,7 @@ func (ses *Session) generateFileInput(in string) (out string, isVault bool, err
out = filepath.Join(outDir, base)
- err = os.WriteFile(out, contentOut.Bytes(), 0600)
+ err = os.WriteFile(out, contentOut, 0600)
if err != nil {
return ``, false, fmt.Errorf(`%s: %s: %w`, logp, out, err)
}
@@ -648,3 +659,33 @@ func (ses *Session) loadRawEnv(content []byte) (err error) {
return nil
}
+
+// render apply the session and environment variables into input stream `in`
+// and return the result.
+// It will return an error if the input cannot be parsed or one variable
+// is not exists.
+func (ses *Session) render(path string, in []byte) (out []byte, err error) {
+ var relpath string
+
+ relpath, err = filepath.Rel(ses.BaseDir, path)
+ if err != nil {
+ relpath = path
+ }
+
+ var tmpl = template.New(relpath)
+
+ tmpl, err = tmpl.Parse(string(in))
+ if err != nil {
+ return nil, err
+ }
+
+ var buf bytes.Buffer
+ err = tmpl.Execute(&buf, ses)
+ if err != nil {
+ return nil, err
+ }
+
+ out = buf.Bytes()
+
+ return out, nil
+}
diff --git a/testdata/local/local.aww b/testdata/local/local.aww
index a77ec42..d9127db 100644
--- a/testdata/local/local.aww
+++ b/testdata/local/local.aww
@@ -1,5 +1 @@
echo {{.Val "secret::pass"}}
-
-#put: {{.ScriptDir}}/plain.txt {{.ScriptDir}}/tmp/plain.txt
-
-#put: {{.ScriptDir}}/encrypted.txt {{.ScriptDir}}/tmp/decrypted.txt
diff --git a/testdata/local/test.data b/testdata/local/local.data
index 330e646..9ecdb9f 100644
--- a/testdata/local/test.data
+++ b/testdata/local/local.data
@@ -7,11 +7,3 @@ this_is_a_secret
--> local: 1: echo this_is_a_secret_in_sub
this_is_a_secret_in_sub
-
-<<< tmp/plain.txt
-The host name is encrypt.
-The secret password is .
-
-<<< tmp/decrypted.txt
-The host name is encrypt.
-The secret password is this_is_a_secret.
diff --git a/testdata/local/missing_val_encrypted.txt b/testdata/local/missing_val_encrypted.txt
new file mode 100644
index 0000000..de6797e
--- /dev/null
+++ b/testdata/local/missing_val_encrypted.txt
@@ -0,0 +1,2 @@
+The host name is {{.Val "host::name"}}.
+The secret password is {{.Val "secret::pass"}}.
diff --git a/testdata/local/plain.txt b/testdata/local/plain.txt
index de6797e..5067c47 100644
--- a/testdata/local/plain.txt
+++ b/testdata/local/plain.txt
@@ -1,2 +1 @@
The host name is {{.Val "host::name"}}.
-The secret password is {{.Val "secret::pass"}}.
diff --git a/testdata/local/put.aww b/testdata/local/put.aww
new file mode 100644
index 0000000..5fd6b4d
--- /dev/null
+++ b/testdata/local/put.aww
@@ -0,0 +1,5 @@
+#put: {{.ScriptDir}}/plain.txt {{.ScriptDir}}/tmp/plain.txt
+
+#put: {{.ScriptDir}}/missing_val_encrypted.txt {{.ScriptDir}}/tmp/missing_val_encrypted.txt
+
+#put: {{.ScriptDir}}/encrypted.txt {{.ScriptDir}}/tmp/decrypted.txt
diff --git a/testdata/local/put.data b/testdata/local/put.data
new file mode 100644
index 0000000..2018f7a
--- /dev/null
+++ b/testdata/local/put.data
@@ -0,0 +1,16 @@
+<<< tmp/plain.txt
+The host name is encrypt.
+
+
+<<< missing_val_encrypted
+!!! Copy: generateFileInput: template: missing_val_encrypted.txt:2:25: executing "missing_val_encrypted.txt" at <.Val>: error calling Val: "secret::pass" is empty
+
+<<< tmp/decrypted.txt
+The host name is encrypt.
+The secret password is this_is_a_secret.
+
+<<< encrypted_empty_passphrase.stderr
+!!! Copy: generateFileInput: private key is missing or not loaded
+
+<<< encrypted_invalid_passphrase
+Local: NewSession: loadEnvFromPaths: LoadPrivateKeyInteractive: x509: decryption password incorrect