diff --git a/server/server.go b/server/server.go index 5af493e6..56afa8ba 100644 --- a/server/server.go +++ b/server/server.go @@ -739,7 +739,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e return nil, err } m := newDefaultMessage(t.ID, "") - cache, firebase, email, call, messageTemplate, titleTemplate, unifiedpush, e := s.parsePublishParams(r, m) + cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m) if e != nil { return nil, e.With(t) } @@ -770,7 +770,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e if cache { m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix() } - if err := s.handlePublishBody(r, v, m, body, messageTemplate, titleTemplate, unifiedpush); err != nil { + if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil { return nil, err } if m.Message == "" { @@ -925,7 +925,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { } } -func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, messageTemplate string, titleTemplate string, unifiedpush bool, err *errHTTP) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) { cache = readBoolParam(r, true, "x-cache", "cache") firebase = readBoolParam(r, true, "x-firebase", "firebase") m.Title = readParam(r, "x-title", "title", "t") @@ -941,7 +941,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if attach != "" { if !urlRegex.MatchString(attach) { - return false, false, "", "", "", "", false, errHTTPBadRequestAttachmentURLInvalid + return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid } m.Attachment.URL = attach if m.Attachment.Name == "" { @@ -959,20 +959,20 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } if icon != "" { if !urlRegex.MatchString(icon) { - return false, false, "", "", "", "", false, errHTTPBadRequestIconURLInvalid + return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid } m.Icon = icon } email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") if s.smtpSender == nil && email != "" { - return false, false, "", "", "", "", false, errHTTPBadRequestEmailDisabled + return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled } call = readParam(r, "x-call", "call") if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) { print("call: %s", call) - return false, false, "", "", "", "", false, errHTTPBadRequestPhoneCallsDisabled + return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled } else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) { - return false, false, "", "", "", "", false, errHTTPBadRequestPhoneNumberInvalid + return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { @@ -981,27 +981,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi var e error m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) if e != nil { - return false, false, "", "", "", "", false, errHTTPBadRequestPriorityInvalid + return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid } m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta") delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") if delayStr != "" { if !cache { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayNoCache + return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache } if email != "" { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) + return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet) } if call != "" { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) + return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet) } delay, err := util.ParseFutureTime(delayStr, time.Now()) if err != nil { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayCannotParse + return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse } else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayTooSmall + return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall } else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() { - return false, false, "", "", "", "", false, errHTTPBadRequestDelayTooLarge + return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge } m.Time = delay.Unix() } @@ -1009,15 +1009,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi if actionsStr != "" { m.Actions, e = parseActions(actionsStr) if e != nil { - return false, false, "", "", "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) + return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error()) } } contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md") if markdown || strings.ToLower(contentType) == "text/markdown" { m.ContentType = "text/markdown" } - messageTemplate = readParam(r, "x-template-message", "template-message", "tpl-m") - titleTemplate = readParam(r, "x-template-title", "template-title", "tpl-t") + template = readBoolParam(r, false, "x-template", "template", "tpl") unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! if unifiedpush { firebase = false @@ -1029,7 +1028,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi cache = false email = "" } - return cache, firebase, email, call, messageTemplate, titleTemplate, unifiedpush, nil + return cache, firebase, email, call, template, unifiedpush, nil } // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message. @@ -1046,17 +1045,17 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi // If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message // 6. curl -T file.txt ntfy.sh/mytopic // If file.txt is > message limit, treat it as an attachment -func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, messageTemplate string, titleTemplate string, unifiedpush bool) error { +func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template bool, unifiedpush bool) error { if m.Event == pollRequestEvent { // Case 1 return s.handleBodyDiscard(body) } else if unifiedpush { return s.handleBodyAsMessageAutoDetect(m, body) // Case 2 } else if m.Attachment != nil && m.Attachment.URL != "" { - return s.handleBodyAsTextMessage(m, body, messageTemplate, titleTemplate) // Case 3 + return s.handleBodyAsTextMessage(m, body, template) // Case 3 } else if m.Attachment != nil && m.Attachment.Name != "" { return s.handleBodyAsAttachment(r, v, m, body) // Case 4 } else if !body.LimitReached && utf8.Valid(body.PeekedBytes) { - return s.handleBodyAsTextMessage(m, body, messageTemplate, titleTemplate) // Case 5 + return s.handleBodyAsTextMessage(m, body, template) // Case 5 } return s.handleBodyAsAttachment(r, v, m, body) // Case 6 } @@ -1077,39 +1076,36 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeekedRead return nil } -func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, messageTemplate string, titleTemplate string) error { +func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser, template bool) error { if !utf8.Valid(body.PeekedBytes) { return errHTTPBadRequestMessageNotUTF8.With(m) } if len(body.PeekedBytes) > 0 { // Empty body should not override message (publish via GET!) peakedBody := strings.TrimSpace(string(body.PeekedBytes)) // Truncates the message to the peek limit if required - // Replace JSON paths in messageTemplate - if messageTemplate != "" && gjson.Valid(peakedBody) { - m.Message = messageTemplate + if template && gjson.Valid(peakedBody) { + // Replace JSON paths in message r := regexp.MustCompile(`\${([^}]+)}`) - messageMatches := r.FindAllStringSubmatch(messageTemplate, -1) - for _, v := range messageMatches { + matches := r.FindAllStringSubmatch(m.Message, -1) + for _, v := range matches { query := v[1] result := gjson.Get(peakedBody, query) if result.Exists() { m.Message = strings.ReplaceAll(m.Message, fmt.Sprintf("${%s}", query), result.String()) } } - } else { - m.Message = peakedBody - } - // Replace JSON paths in titleTemplate - if titleTemplate != "" && gjson.Valid(peakedBody) { - m.Title = titleTemplate - r := regexp.MustCompile(`\${([^}]+)}`) - titleMatches := r.FindAllStringSubmatch(titleTemplate, -1) - for _, v := range titleMatches { + + // Replace JSON paths in title + r = regexp.MustCompile(`\${([^}]+)}`) + matches = r.FindAllStringSubmatch(m.Title, -1) + for _, v := range matches { query := v[1] result := gjson.Get(peakedBody, query) if result.Exists() { m.Title = strings.ReplaceAll(m.Title, fmt.Sprintf("${%s}", query), result.String()) } } + } else { + m.Message = peakedBody } } if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" { diff --git a/server/server_test.go b/server/server_test.go index 4f634360..35ea2c54 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -2628,8 +2628,9 @@ func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) { func TestServer_MessageTemplate(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Template-Message": "${foo}", - "X-Template-Title": "${nested.title}", + "X-Message": "${foo}", + "X-Title": "${nested.title}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2641,8 +2642,9 @@ func TestServer_MessageTemplate(t *testing.T) { func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "Template-Message": "${foo} is ${foo}", - "Template-Title": "${nested.title} is ${nested.title}", + "Message": "${foo} is ${foo}", + "Title": "${nested.title} is ${nested.title}", + "Template": "1", }) require.Equal(t, 200, response.Code) @@ -2655,8 +2657,9 @@ func TestServer_MessageTemplate_JSONBody(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}` response := request(t, s, "PUT", "/", body, map[string]string{ - "tpl-m": "${foo}", - "tpl-t": "${nested.title}", + "m": "${foo}", + "t": "${nested.title}", + "tpl": "1", }) require.Equal(t, 200, response.Code) @@ -2669,21 +2672,23 @@ func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) { s := newTestServer(t, newTestConfig(t)) body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}` response := request(t, s, "PUT", "/", body, map[string]string{ - "X-Template-Message": "${foo}", - "X-Template-Title": "${nested.title}", + "X-Message": "${foo}", + "X-Title": "${nested.title}", + "X-Template": "1", }) require.Equal(t, 200, response.Code, "Got %s", response) m := toMessage(t, response.Body.String()) require.Equal(t, "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID", m.Message) - require.Equal(t, "", m.Title) + require.Equal(t, "${nested.title}", m.Title) } func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Template-Message": "${food}", - "X-Template-Title": "${nested.titl}", + "X-Message": "${food}", + "X-Title": "${nested.titl}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2695,7 +2700,8 @@ func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) { func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) { s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{ - "X-Template-Message": "${foo} is ${nested.title}", + "X-Message": "${foo} is ${nested.title}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2708,7 +2714,8 @@ func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) { // i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar" s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}}`, map[string]string{ - "X-Template-Message": "${${nested.bar}}", + "X-Message": "${${nested.bar}}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2723,7 +2730,8 @@ func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) { // included by the regex, so it is still there after replacing the placeholder, thus giving you "works!}" s := newTestServer(t, newTestConfig(t)) response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}, "${nested":{"bar":"works!"}}`, map[string]string{ - "X-Template-Message": "${${nested.bar}}", + "X-Message": "${${nested.bar}}", + "X-Template": "1", }) require.Equal(t, 200, response.Code) @@ -2735,8 +2743,9 @@ func TestServer_MessageTemplate_FancyGJSON(t *testing.T) { s := newTestServer(t, newTestConfig(t)) jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}` response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{ - "X-Template-Message": `${errors.#(level=="severe")#.url}`, - "X-Template-Title": `${errors.#(level=="severe")#|#} Severe Errors`, + "X-Message": `${errors.#(level=="severe")#.url}`, + "X-Title": `${errors.#(level=="severe")#|#} Severe Errors`, + "X-Template": "1", }) require.Equal(t, 200, response.Code)