Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions docs/content/configuration/http_hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ Additionally, for the `send-after-fail` hooks, these environment variables will
- `ERROR_EXIT_CODE` containing the exit code of the command line that failed
- `ERROR_STDERR` containing any message that the failed command sent to the standard error (stderr)

URL encoding is applayed for variables `ERROR`, `ERROR_COMMANDLINE` and `ERROR_STDERR` if they are used in URL.

The `send-finally` hooks are also getting the environment of `send-after-fail` when any previous operation has failed (except any `send` operation).

Failures in any `send-*` are logged but do not influence environment or return code.
Expand Down
42 changes: 36 additions & 6 deletions monitor/hook/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
urlpkg "net/url"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -74,8 +75,8 @@ func (s *Sender) Send(cfg config.SendMonitoringSection, ctx Context) error {
if cfg.URL.Value() == "" {
return errors.New("URL field is empty")
}
url := resolve(cfg.URL.Value(), ctx)
publicUrl := resolve(cfg.URL.String(), ctx)
url := resolveURL(cfg.URL.Value(), ctx)
publicUrl := resolveURL(cfg.URL.String(), ctx)
method := cfg.Method
if method == "" {
method = http.MethodGet
Expand All @@ -93,7 +94,7 @@ func (s *Sender) Send(cfg config.SendMonitoringSection, ctx Context) error {
bodyReader = bytes.NewBufferString(body)
}
if cfg.Body != "" {
body = resolve(cfg.Body, ctx)
body = resolveBody(cfg.Body, ctx)
bodyReader = bytes.NewBufferString(body)
}

Expand Down Expand Up @@ -202,8 +203,8 @@ func getRootCAs(certificates []string) *x509.CertPool {
return caCertPool
}

func resolve(body string, ctx Context) string {
body = os.Expand(body, func(s string) string {
func resolveBody(body string, ctx Context) string {
return os.Expand(body, func(s string) string {
switch s {
case constants.EnvProfileName:
return ctx.ProfileName
Expand All @@ -230,7 +231,36 @@ func resolve(body string, ctx Context) string {
return os.Getenv(s)
}
})
return body
}

func resolveURL(url string, ctx Context) string {
return os.Expand(url, func(s string) string {
switch s {
case constants.EnvProfileName:
return ctx.ProfileName

case constants.EnvProfileCommand:
return ctx.ProfileCommand

case constants.EnvError:
return urlpkg.QueryEscape(ctx.Error.Message)

case constants.EnvErrorCommandLine:
return urlpkg.QueryEscape(ctx.Error.CommandLine)

case constants.EnvErrorExitCode:
return ctx.Error.ExitCode

case constants.EnvErrorStderr:
return urlpkg.QueryEscape(ctx.Error.Stderr)

case "$":
return "$" // allow to escape "$" as "$$"

default:
return os.Getenv(s)
}
})
}

func loadBodyTemplate(filename string, ctx Context) (string, error) {
Expand Down
52 changes: 52 additions & 0 deletions monitor/hook/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/creativeprojects/resticprofile/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/creativeprojects/resticprofile/constants"
)

func TestSend(t *testing.T) {
Expand Down Expand Up @@ -273,6 +274,57 @@ func TestConfidentialURL(t *testing.T) {
assert.Equal(t, 1, calls)
}

func TestURLEncoding(t *testing.T) {
ctx := Context{
ProfileName: "unencoded/name",
ProfileCommand: "unencoded/command",
Error: ErrorContext{
Message: "some/error/message",
CommandLine: "some < tricky || command & line",
ExitCode: "1",
Stderr: "some\nmultiline\nerror\nwith strange &/~!^., characters",
},
Stdout: "unused",
}

calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()

assert.Equal(t, fmt.Sprintf("/%s-%s", ctx.ProfileName, ctx.ProfileCommand), r.URL.Path)

assert.Equal(t, ctx.Error.Message, query.Get("message"))
assert.Equal(t, ctx.Error.CommandLine, query.Get("command_line"))
assert.Equal(t, ctx.Error.ExitCode, query.Get("exit_code"))
assert.Equal(t, ctx.Error.Stderr, query.Get("stderr"))

assert.Equal(t, "$TEST_MONITOR_URL", query.Get("escaped"))

calls++
}))
defer server.Close()

// test if env vars are untouched
t.Setenv("TEST_MONITOR_URL", server.URL)

serverURL := fmt.Sprintf(
"$TEST_MONITOR_URL/$%s-$%s?message=$%s&command_line=$%s&exit_code=$%s&stderr=$%s&escaped=$$TEST_MONITOR_URL",
constants.EnvProfileName,
constants.EnvProfileCommand,
constants.EnvError,
constants.EnvErrorCommandLine,
constants.EnvErrorExitCode,
constants.EnvErrorStderr,
)

sender := NewSender(nil, "", 300*time.Millisecond, false)
err := sender.Send(config.SendMonitoringSection{
URL: config.NewConfidentialValue(serverURL),
}, ctx)
assert.NoError(t, err)
assert.Equal(t, 1, calls)
}

func TestConfidentialHeader(t *testing.T) {
clog.SetTestLog(t)
defer clog.CloseTestLog()
Expand Down
Loading