diff options
| author | Shulhan <ms@kilabit.info> | 2023-09-27 00:01:22 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2023-09-27 00:54:10 +0700 |
| commit | d1b393b0d4ca75c8d81d051a332597b7aa3f0ab0 (patch) | |
| tree | 68adc1e0bfee5c6e2056110dc094cb520a5b969a | |
| parent | 59968a1665735990187c05547faab3c5310c4be1 (diff) | |
| download | awwan-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.go | 4 | ||||
| -rw-r--r-- | awwan_test.go | 58 | ||||
| -rw-r--r-- | script.go | 21 | ||||
| -rw-r--r-- | script_example_test.go | 2 | ||||
| -rw-r--r-- | session.go | 71 | ||||
| -rw-r--r-- | testdata/local/local.aww | 4 | ||||
| -rw-r--r-- | testdata/local/local.data (renamed from testdata/local/test.data) | 8 | ||||
| -rw-r--r-- | testdata/local/missing_val_encrypted.txt | 2 | ||||
| -rw-r--r-- | testdata/local/plain.txt | 1 | ||||
| -rw-r--r-- | testdata/local/put.aww | 5 | ||||
| -rw-r--r-- | testdata/local/put.data | 16 |
11 files changed, 117 insertions, 75 deletions
@@ -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)) } } } @@ -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) } @@ -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 |
