mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-07-20 10:04:08 +00:00
Add some limits
This commit is contained in:
parent
57df16dd62
commit
dde07adbdc
9 changed files with 77 additions and 13 deletions
|
@ -969,15 +969,20 @@ To learn the basics of Go's templating language, please see [template syntax](#t
|
||||||
### Pre-defined templates
|
### Pre-defined templates
|
||||||
|
|
||||||
When `X-Template: <name>` (aliases: `Template: <name>`, `Tpl: <name>`) or `?template=<name>` is set, ntfy will transform the
|
When `X-Template: <name>` (aliases: `Template: <name>`, `Tpl: <name>`) or `?template=<name>` is set, ntfy will transform the
|
||||||
message and/or title based on one of the built-in pre-defined templates
|
message and/or title based on one of the built-in pre-defined templates.
|
||||||
|
|
||||||
The following **pre-defined templates** are available:
|
The following **pre-defined templates** are available:
|
||||||
|
|
||||||
* `github`: Formats a subset of [GitHub webhook](https://docs.github.com/en/webhooks/about-webhooks) payloads (PRs, issues, new star, new watcher, new comment)
|
* `github`: Formats a subset of [GitHub webhook](https://docs.github.com/en/webhooks/about-webhooks) payloads (PRs, issues, new star, new watcher, new comment). See [github.yml](https://github.com/binwiederhier/ntfy/blob/main/server/templates/github.yml).
|
||||||
* `grafana`: Formats [Grafana webhook](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/) payloads (firing/resolved alerts)
|
* `grafana`: Formats [Grafana webhook](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/) payloads (firing/resolved alerts). See [grafana.yml](https://github.com/binwiederhier/ntfy/blob/main/server/templates/grafana.yml).
|
||||||
* `alertmanager`: Formats [Alertmanager webhook](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) payloads (firing/resolved alerts)
|
* `alertmanager`: Formats [Alertmanager webhook](https://prometheus.io/docs/alerting/latest/configuration/#webhook_config) payloads (firing/resolved alerts). See [alertmanager.yml](https://github.com/binwiederhier/ntfy/blob/main/server/templates/alertmanager.yml).
|
||||||
|
|
||||||
Here's an example of how to use the pre-defined `github` template: First, configure the webhook in GitHub to send a webhook to your ntfy topic, e.g. `https://ntfy.sh/mytopic?template=github`.
|
To override the pre-defined templates, you can place a file with the same name in the template directory (defaults to `/etc/ntfy/templates`,
|
||||||
|
can be overridden with `template-dir`). See [custom templates](#custom-templates) for more details.
|
||||||
|
|
||||||
|
Here's an example of how to use the **pre-defined `github` template**:
|
||||||
|
|
||||||
|
First, configure the webhook in GitHub to send a webhook to your ntfy topic, e.g. `https://ntfy.sh/mytopic?template=github`.
|
||||||
<figure markdown>
|
<figure markdown>
|
||||||
{ width=600 }
|
{ width=600 }
|
||||||
<figcaption>GitHub webhook configuration</figcaption>
|
<figcaption>GitHub webhook configuration</figcaption>
|
||||||
|
|
|
@ -123,7 +123,6 @@ var (
|
||||||
errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
errHTTPBadRequestTemplateDisallowedFunctionCalls = &errHTTP{40044, http.StatusBadRequest, "invalid request: template contains disallowed function calls, e.g. template, call, or define", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||||
errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
errHTTPBadRequestTemplateExecuteFailed = &errHTTP{40045, http.StatusBadRequest, "invalid request: template execution failed", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||||
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
|
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
|
||||||
errHTTPBadRequestTemplateDirectoryNotConfigured = &errHTTP{40046, http.StatusBadRequest, "invalid request: template directory not configured", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
|
||||||
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||||
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
|
||||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||||
|
|
|
@ -145,6 +145,7 @@ const (
|
||||||
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
|
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
|
||||||
messagesHistoryMax = 10 // Number of message count values to keep in memory
|
messagesHistoryMax = 10 // Number of message count values to keep in memory
|
||||||
templateMaxExecutionTime = 100 * time.Millisecond // Maximum time a template can take to execute, used to prevent DoS attacks
|
templateMaxExecutionTime = 100 * time.Millisecond // Maximum time a template can take to execute, used to prevent DoS attacks
|
||||||
|
templateMaxOutputBytes = 1024 * 1024 // Maximum number of bytes a template can output, used to prevent DoS attacks
|
||||||
templateFileExtension = ".yml" // Template files must end with this extension
|
templateFileExtension = ".yml" // Template files must end with this extension
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1127,7 +1128,7 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, template templateM
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(m.Message) > s.config.MessageSizeLimit {
|
if len(m.Title) > s.config.MessageSizeLimit || len(m.Message) > s.config.MessageSizeLimit {
|
||||||
return errHTTPBadRequestTemplateMessageTooLarge
|
return errHTTPBadRequestTemplateMessageTooLarge
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -1188,7 +1189,8 @@ func (s *Server) replaceTemplate(tpl string, source string) (string, error) {
|
||||||
return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error())
|
return "", errHTTPBadRequestTemplateInvalid.Wrap("%s", err.Error())
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil {
|
limitWriter := util.NewLimitWriter(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), util.NewFixedLimiter(templateMaxOutputBytes))
|
||||||
|
if err := t.Execute(limitWriter, data); err != nil {
|
||||||
return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error())
|
return "", errHTTPBadRequestTemplateExecuteFailed.Wrap("%s", err.Error())
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(buf.String()), nil
|
return strings.TrimSpace(buf.String()), nil
|
||||||
|
|
|
@ -3143,6 +3143,42 @@ message: |
|
||||||
require.Equal(t, "Custom message 1391", m.Message)
|
require.Equal(t, "Custom message 1391", m.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_Repeat9999_TooLarge(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||||
|
"X-Message": `{{ repeat 9999 "mystring" }}`,
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code)
|
||||||
|
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "message or title is too large after replacing template")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_Repeat10001_TooLarge(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||||
|
"X-Message": `{{ repeat 10001 "mystring" }}`,
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code)
|
||||||
|
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "repeat count 10001 exceeds limit of 10000")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_Until100_000(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "POST", "/mytopic", `{}`, map[string]string{
|
||||||
|
"X-Message": `{{ range $i, $e := until 100_000 }}{{end}}`,
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, 40045, toHTTPError(t, response.Body.String()).Code)
|
||||||
|
require.Contains(t, toHTTPError(t, response.Body.String()).Message, "too many iterations")
|
||||||
|
}
|
||||||
|
|
||||||
func newTestConfig(t *testing.T) *Config {
|
func newTestConfig(t *testing.T) *Config {
|
||||||
conf := NewConfig()
|
conf := NewConfig()
|
||||||
conf.BaseURL = "http://127.0.0.1:12345"
|
conf.BaseURL = "http://127.0.0.1:12345"
|
||||||
|
|
|
@ -132,7 +132,7 @@ func toRawJSON(v any) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return string(output)
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters.
|
// mustToRawJSON encodes an item into a JSON string with no escaping of HTML characters.
|
||||||
|
|
|
@ -14,6 +14,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
loopExecutionLimit = 10_000 // Limit the number of loop executions to prevent execution from taking too long
|
||||||
|
stringLengthLimit = 100_000 // Limit the length of strings to prevent memory issues
|
||||||
|
)
|
||||||
|
|
||||||
// TxtFuncMap produces the function map.
|
// TxtFuncMap produces the function map.
|
||||||
//
|
//
|
||||||
// Use this to pass the functions into the template engine:
|
// Use this to pass the functions into the template engine:
|
||||||
|
@ -58,7 +63,7 @@ var genericMap = map[string]any{
|
||||||
},
|
},
|
||||||
"substr": substring,
|
"substr": substring,
|
||||||
// Switch order so that "foo" | repeat 5
|
// Switch order so that "foo" | repeat 5
|
||||||
"repeat": func(count int, str string) string { return strings.Repeat(str, count) },
|
"repeat": repeat,
|
||||||
"trimAll": func(a, b string) string { return strings.Trim(b, a) },
|
"trimAll": func(a, b string) string { return strings.Trim(b, a) },
|
||||||
"trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) },
|
"trimSuffix": func(a, b string) string { return strings.TrimSuffix(b, a) },
|
||||||
"trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) },
|
"trimPrefix": func(a, b string) string { return strings.TrimPrefix(b, a) },
|
||||||
|
|
|
@ -127,7 +127,15 @@ func until(count int) []int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func untilStep(start, stop, step int) []int {
|
func untilStep(start, stop, step int) []int {
|
||||||
v := []int{}
|
var v []int
|
||||||
|
if step == 0 {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
iterations := math.Abs(float64(stop)-float64(start)) / float64(step)
|
||||||
|
if iterations > loopExecutionLimit {
|
||||||
|
panic(fmt.Sprintf("too many iterations in untilStep; max allowed is %d, got %f", loopExecutionLimit, iterations))
|
||||||
|
}
|
||||||
|
|
||||||
if stop < start {
|
if stop < start {
|
||||||
if step >= 0 {
|
if step >= 0 {
|
||||||
|
|
|
@ -187,3 +187,12 @@ func substring(start, end int, s string) string {
|
||||||
}
|
}
|
||||||
return s[start:end]
|
return s[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func repeat(count int, str string) string {
|
||||||
|
if count > loopExecutionLimit {
|
||||||
|
panic(fmt.Sprintf("repeat count %d exceeds limit of %d", count, loopExecutionLimit))
|
||||||
|
} else if count*len(str) >= stringLengthLimit {
|
||||||
|
panic(fmt.Sprintf("repeat count %d with string length %d exceeds limit of %d", count, len(str), stringLengthLimit))
|
||||||
|
}
|
||||||
|
return strings.Repeat(str, count)
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrWriteTimeout is returned when a write timed out
|
// ErrWriteTimeout is returned when a write timed out
|
||||||
var ErrWriteTimeout = errors.New("write operation failed due to timeout since creation")
|
var ErrWriteTimeout = errors.New("write operation failed due to timeout")
|
||||||
|
|
||||||
// TimeoutWriter wraps an io.Writer that will time out after the given timeout
|
// TimeoutWriter wraps an io.Writer that will time out after the given timeout
|
||||||
type TimeoutWriter struct {
|
type TimeoutWriter struct {
|
||||||
|
@ -28,7 +28,7 @@ func NewTimeoutWriter(w io.Writer, timeout time.Duration) *TimeoutWriter {
|
||||||
// Write implements the io.Writer interface, failing if called after the timeout period from creation.
|
// Write implements the io.Writer interface, failing if called after the timeout period from creation.
|
||||||
func (tw *TimeoutWriter) Write(p []byte) (n int, err error) {
|
func (tw *TimeoutWriter) Write(p []byte) (n int, err error) {
|
||||||
if time.Since(tw.start) > tw.timeout {
|
if time.Since(tw.start) > tw.timeout {
|
||||||
return 0, errors.New("write operation failed due to timeout since creation")
|
return 0, ErrWriteTimeout
|
||||||
}
|
}
|
||||||
return tw.writer.Write(p)
|
return tw.writer.Write(p)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue