diff --git a/server/errors.go b/server/errors.go index 8e565197..862fa9d7 100644 --- a/server/errors.go +++ b/server/errors.go @@ -106,6 +106,7 @@ var ( errHTTPBadRequestNotAPaidUser = &errHTTP{40027, http.StatusBadRequest, "invalid request: not a paid user", "", nil} errHTTPBadRequestBillingRequestInvalid = &errHTTP{40028, http.StatusBadRequest, "invalid request: not a valid billing request", "", nil} errHTTPBadRequestBillingSubscriptionExists = &errHTTP{40029, http.StatusBadRequest, "invalid request: billing subscription already exists", "", nil} + errHTTPBadRequestInvalidMimeHeader = &errHTTP{40030, http.StatusBadRequest, "invalid request: invalid MIME encoding of header", "", 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} diff --git a/server/server.go b/server/server.go index ebbe147e..cae74e55 100644 --- a/server/server.go +++ b/server/server.go @@ -843,10 +843,17 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) { func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email string, 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") m.Click = readParam(r, "x-click", "click") icon := readParam(r, "x-icon", "icon") filename := readParam(r, "x-filename", "filename", "file", "f") + title := readParam(r, "x-title", "title", "t") + if title != "" { + title, err := mimeDecoder.DecodeHeader(title) + if err != nil { + return false, false, "", false, errHTTPBadRequestInvalidMimeHeader.Wrap("invalid X-Title header: %s", err.Error()) + } + m.Title = title + } attach := readParam(r, "x-attach", "attach", "a") if attach != "" || filename != "" { m.Attachment = &attachment{} @@ -884,6 +891,10 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi } messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n") if messageStr != "" { + messageStr, err := mimeDecoder.DecodeHeader(messageStr) + if err != nil { + return false, false, "", false, errHTTPBadRequestInvalidMimeHeader.Wrap("invalid X-Message header: %s", err.Error()) + } m.Message = messageStr } var e error diff --git a/server/server_test.go b/server/server_test.go index ceee187e..0dc34632 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -21,8 +21,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "heckel.io/ntfy/log" "heckel.io/ntfy/util" @@ -2106,8 +2104,8 @@ func TestServer_PublishWhileUpdatingStatsWithLotsOfMessages(t *testing.T) { start = time.Now() response := request(t, s, "PUT", "/mytopic", "some body", nil) m := toMessage(t, response.Body.String()) - assert.Equal(t, "some body", m.Message) - assert.True(t, time.Since(start) < 100*time.Millisecond) + require.Equal(t, "some body", m.Message) + require.True(t, time.Since(start) < 100*time.Millisecond) log.Info("Done: Publishing message; took %s", time.Since(start).Round(time.Millisecond)) // Wait for all goroutines @@ -2469,6 +2467,21 @@ func TestServer_MessageCountPersistence(t *testing.T) { require.Equal(t, int64(1234), s.messages) } +func TestServer_PublishWithUTF8MimeHeader(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + + response := request(t, s, "POST", "/mytopic", "some attachment", map[string]string{ + "X-Filename": "some attachment.txt", + "X-Message": "=?UTF-8?B?8J+HqfCfh6o=?=", + "X-Title": "=?UTF-8?B?bnRmeSDlvojmo5I=?=, no really I mean it! =?UTF-8?Q?This is q=C3=BC=C3=B6ted-print=C3=A4ble.?=", + }) + require.Equal(t, 200, response.Code) + m := toMessage(t, response.Body.String()) + require.Equal(t, "🇩🇪", m.Message) + require.Equal(t, "ntfy 很棒, no really I mean it! This is qüöted-printäble.", m.Title) + require.Equal(t, "some attachment.txt", m.Attachment.Name) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.BaseURL = "http://127.0.0.1:12345" diff --git a/server/util.go b/server/util.go index 390e7fb1..98a76630 100644 --- a/server/util.go +++ b/server/util.go @@ -5,11 +5,14 @@ import ( "fmt" "heckel.io/ntfy/util" "io" + "mime" "net/http" "net/netip" "strings" ) +var mimeDecoder mime.WordDecoder + func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool { value := strings.ToLower(readParam(r, names...)) if value == "" {