Skip to content

Commit 461710b

Browse files
authored
fix(http-hooks): apply url encoding for ERROR_* env vars (#538)
* fix(http-hooks): apply url encoding for ERROR_* env vars * docs(http-hooks): clarify url encoding conditions * test(http-hooks): improve test coverage
1 parent 720d7ee commit 461710b

File tree

3 files changed

+90
-6
lines changed

3 files changed

+90
-6
lines changed

docs/content/configuration/http_hooks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ Additionally, for the `send-after-fail` hooks, these environment variables will
300300
- `ERROR_EXIT_CODE` containing the exit code of the command line that failed
301301
- `ERROR_STDERR` containing any message that the failed command sent to the standard error (stderr)
302302

303+
URL encoding is applayed for variables `ERROR`, `ERROR_COMMANDLINE` and `ERROR_STDERR` if they are used in URL.
304+
303305
The `send-finally` hooks are also getting the environment of `send-after-fail` when any previous operation has failed (except any `send` operation).
304306

305307
Failures in any `send-*` are logged but do not influence environment or return code.

monitor/hook/sender.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"net/http"
12+
urlpkg "net/url"
1213
"os"
1314
"path/filepath"
1415
"regexp"
@@ -74,8 +75,8 @@ func (s *Sender) Send(cfg config.SendMonitoringSection, ctx Context) error {
7475
if cfg.URL.Value() == "" {
7576
return errors.New("URL field is empty")
7677
}
77-
url := resolve(cfg.URL.Value(), ctx)
78-
publicUrl := resolve(cfg.URL.String(), ctx)
78+
url := resolveURL(cfg.URL.Value(), ctx)
79+
publicUrl := resolveURL(cfg.URL.String(), ctx)
7980
method := cfg.Method
8081
if method == "" {
8182
method = http.MethodGet
@@ -93,7 +94,7 @@ func (s *Sender) Send(cfg config.SendMonitoringSection, ctx Context) error {
9394
bodyReader = bytes.NewBufferString(body)
9495
}
9596
if cfg.Body != "" {
96-
body = resolve(cfg.Body, ctx)
97+
body = resolveBody(cfg.Body, ctx)
9798
bodyReader = bytes.NewBufferString(body)
9899
}
99100

@@ -202,8 +203,8 @@ func getRootCAs(certificates []string) *x509.CertPool {
202203
return caCertPool
203204
}
204205

205-
func resolve(body string, ctx Context) string {
206-
body = os.Expand(body, func(s string) string {
206+
func resolveBody(body string, ctx Context) string {
207+
return os.Expand(body, func(s string) string {
207208
switch s {
208209
case constants.EnvProfileName:
209210
return ctx.ProfileName
@@ -230,7 +231,36 @@ func resolve(body string, ctx Context) string {
230231
return os.Getenv(s)
231232
}
232233
})
233-
return body
234+
}
235+
236+
func resolveURL(url string, ctx Context) string {
237+
return os.Expand(url, func(s string) string {
238+
switch s {
239+
case constants.EnvProfileName:
240+
return ctx.ProfileName
241+
242+
case constants.EnvProfileCommand:
243+
return ctx.ProfileCommand
244+
245+
case constants.EnvError:
246+
return urlpkg.QueryEscape(ctx.Error.Message)
247+
248+
case constants.EnvErrorCommandLine:
249+
return urlpkg.QueryEscape(ctx.Error.CommandLine)
250+
251+
case constants.EnvErrorExitCode:
252+
return ctx.Error.ExitCode
253+
254+
case constants.EnvErrorStderr:
255+
return urlpkg.QueryEscape(ctx.Error.Stderr)
256+
257+
case "$":
258+
return "$" // allow to escape "$" as "$$"
259+
260+
default:
261+
return os.Getenv(s)
262+
}
263+
})
234264
}
235265

236266
func loadBodyTemplate(filename string, ctx Context) (string, error) {

monitor/hook/sender_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/creativeprojects/resticprofile/config"
1919
"github.com/stretchr/testify/assert"
2020
"github.com/stretchr/testify/require"
21+
"github.com/creativeprojects/resticprofile/constants"
2122
)
2223

2324
func TestSend(t *testing.T) {
@@ -273,6 +274,57 @@ func TestConfidentialURL(t *testing.T) {
273274
assert.Equal(t, 1, calls)
274275
}
275276

277+
func TestURLEncoding(t *testing.T) {
278+
ctx := Context{
279+
ProfileName: "unencoded/name",
280+
ProfileCommand: "unencoded/command",
281+
Error: ErrorContext{
282+
Message: "some/error/message",
283+
CommandLine: "some < tricky || command & line",
284+
ExitCode: "1",
285+
Stderr: "some\nmultiline\nerror\nwith strange &/~!^., characters",
286+
},
287+
Stdout: "unused",
288+
}
289+
290+
calls := 0
291+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
292+
query := r.URL.Query()
293+
294+
assert.Equal(t, fmt.Sprintf("/%s-%s", ctx.ProfileName, ctx.ProfileCommand), r.URL.Path)
295+
296+
assert.Equal(t, ctx.Error.Message, query.Get("message"))
297+
assert.Equal(t, ctx.Error.CommandLine, query.Get("command_line"))
298+
assert.Equal(t, ctx.Error.ExitCode, query.Get("exit_code"))
299+
assert.Equal(t, ctx.Error.Stderr, query.Get("stderr"))
300+
301+
assert.Equal(t, "$TEST_MONITOR_URL", query.Get("escaped"))
302+
303+
calls++
304+
}))
305+
defer server.Close()
306+
307+
// test if env vars are untouched
308+
t.Setenv("TEST_MONITOR_URL", server.URL)
309+
310+
serverURL := fmt.Sprintf(
311+
"$TEST_MONITOR_URL/$%s-$%s?message=$%s&command_line=$%s&exit_code=$%s&stderr=$%s&escaped=$$TEST_MONITOR_URL",
312+
constants.EnvProfileName,
313+
constants.EnvProfileCommand,
314+
constants.EnvError,
315+
constants.EnvErrorCommandLine,
316+
constants.EnvErrorExitCode,
317+
constants.EnvErrorStderr,
318+
)
319+
320+
sender := NewSender(nil, "", 300*time.Millisecond, false)
321+
err := sender.Send(config.SendMonitoringSection{
322+
URL: config.NewConfidentialValue(serverURL),
323+
}, ctx)
324+
assert.NoError(t, err)
325+
assert.Equal(t, 1, calls)
326+
}
327+
276328
func TestConfidentialHeader(t *testing.T) {
277329
clog.SetTestLog(t)
278330
defer clog.CloseTestLog()

0 commit comments

Comments
 (0)