binwiederhier 1 anno fa
parent
commit
b9c176ddba
4 ha cambiato i file con 67 aggiunte e 68 eliminazioni
  1. 2 0
      server/errors.go
  2. 12 15
      server/server.go
  3. 19 53
      server/server_test.go
  4. 34 0
      util/timeout_writer.go

+ 2 - 0
server/errors.go

@@ -119,6 +119,8 @@ var (
 	errHTTPBadRequestWebPushTopicCountTooHigh        = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
 	errHTTPBadRequestTemplatedMessageTooLarge        = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil}
 	errHTTPBadRequestTemplatedMessageNotJSON         = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", nil}
+	errHTTPBadRequestTemplateInvalid                 = &errHTTP{40043, http.StatusBadRequest, "invalid request: could not parse template", "", nil}
+	errHTTPBadRequestTemplateExecutionFailed         = &errHTTP{40044, http.StatusBadRequest, "invalid request: template execution failed", "", nil}
 	errHTTPNotFound                                  = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
 	errHTTPUnauthorized                              = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
 	errHTTPForbidden                                 = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}

+ 12 - 15
server/server.go

@@ -135,6 +135,7 @@ const (
 	unifiedPushTopicPrefix   = "up"                      // Temporarily, we rate limit all "up*" topics based on the subscriber
 	unifiedPushTopicLength   = 14                        // Length of UnifiedPush topics, including the "up" part
 	messagesHistoryMax       = 10                        // Number of message count values to keep in memory
+	templateMaxExecutionTime = 100 * time.Millisecond
 )
 
 // WebSocket constants
@@ -1102,34 +1103,30 @@ func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedR
 		return errHTTPEntityTooLargeJSONBody
 	}
 	peekedBody := strings.TrimSpace(string(body.PeekedBytes))
-	m.Message = replaceTemplate(m.Message, peekedBody)
-	m.Title = replaceTemplate(m.Title, peekedBody)
+	if m.Message, err = replaceTemplate(m.Message, peekedBody); err != nil {
+		return err
+	}
+	if m.Title, err = replaceTemplate(m.Title, peekedBody); err != nil {
+		return err
+	}
 	if len(m.Message) > s.config.MessageSizeLimit {
 		return errHTTPBadRequestTemplatedMessageTooLarge
 	}
 	return nil
 }
 
-func replaceTemplate(tpl string, source string) string {
-	rendered, err := replaceTemplateInternal(tpl, source)
-	if err != nil {
-		return "<invalid template>"
-	}
-	return rendered
-}
-
-func replaceTemplateInternal(tpl string, source string) (string, error) {
+func replaceTemplate(tpl string, source string) (string, error) {
 	var data any
 	if err := json.Unmarshal([]byte(source), &data); err != nil {
-		return "", err
+		return "", errHTTPBadRequestTemplatedMessageNotJSON
 	}
 	t, err := template.New("").Parse(tpl)
 	if err != nil {
-		return "", err
+		return "", errHTTPBadRequestTemplateInvalid
 	}
 	var buf bytes.Buffer
-	if err := t.Execute(&buf, data); err != nil {
-		return "", err
+	if err := t.Execute(util.NewTimeoutWriter(&buf, templateMaxExecutionTime), data); err != nil {
+		return "", errHTTPBadRequestTemplateExecutionFailed
 	}
 	return buf.String(), nil
 }

File diff suppressed because it is too large
+ 19 - 53
server/server_test.go


+ 34 - 0
util/timeout_writer.go

@@ -0,0 +1,34 @@
+package util
+
+import (
+	"errors"
+	"io"
+	"time"
+)
+
+// ErrWriteTimeout is returned when a write timed out
+var ErrWriteTimeout = errors.New("write operation failed due to timeout since creation")
+
+// TimeoutWriter wraps an io.Writer that will time out after the given timeout
+type TimeoutWriter struct {
+	writer  io.Writer
+	timeout time.Duration
+	start   time.Time
+}
+
+// NewTimeoutWriter creates a new TimeoutWriter
+func NewTimeoutWriter(w io.Writer, timeout time.Duration) *TimeoutWriter {
+	return &TimeoutWriter{
+		writer:  w,
+		timeout: timeout,
+		start:   time.Now(),
+	}
+}
+
+// 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) {
+	if time.Since(tw.start) > tw.timeout {
+		return 0, errors.New("write operation failed due to timeout since creation")
+	}
+	return tw.writer.Write(p)
+}

Some files were not shown because too many files changed in this diff