Browse Source

Handle binary messages for UnifiedPush

Philipp Heckel 4 years ago
parent
commit
4ceb058a40
3 changed files with 88 additions and 25 deletions
  1. 42 25
      server/server.go
  2. 45 0
      server/server_test.go
  3. 1 0
      server/types.go

+ 42 - 25
server/server.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"embed"
+	"encoding/base64"
 	"encoding/json"
 	"errors"
 	firebase "firebase.google.com/go"
@@ -95,6 +96,7 @@ const (
 	firebaseControlTopic     = "~control"                // See Android if changed
 	emptyMessageBody         = "triggered"               // Used if message body is empty
 	defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
+	encodingBase64           = "base64"
 )
 
 // WebSocket constants
@@ -415,11 +417,11 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
 		return err
 	}
 	m := newDefaultMessage(t.ID, "")
-	cache, firebase, email, err := s.parsePublishParams(r, v, m)
+	cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m)
 	if err != nil {
 		return err
 	}
-	if err := s.handlePublishBody(r, v, m, body); err != nil {
+	if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
 		return err
 	}
 	if m.Message == "" {
@@ -461,7 +463,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
 	return nil
 }
 
-func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, err error) {
+func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
 	cache = readBoolParam(r, true, "x-cache", "cache")
 	firebase = readBoolParam(r, true, "x-firebase", "firebase")
 	m.Title = readParam(r, "x-title", "title", "t")
@@ -476,7 +478,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	}
 	if attach != "" {
 		if !attachURLRegex.MatchString(attach) {
-			return false, false, "", errHTTPBadRequestAttachmentURLInvalid
+			return false, false, "", false, errHTTPBadRequestAttachmentURLInvalid
 		}
 		m.Attachment.URL = attach
 		if m.Attachment.Name == "" {
@@ -495,11 +497,11 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
 	if email != "" {
 		if err := v.EmailAllowed(); err != nil {
-			return false, false, "", errHTTPTooManyRequestsLimitEmails
+			return false, false, "", false, errHTTPTooManyRequestsLimitEmails
 		}
 	}
 	if s.mailer == nil && email != "" {
-		return false, false, "", errHTTPBadRequestEmailDisabled
+		return false, false, "", false, errHTTPBadRequestEmailDisabled
 	}
 	messageStr := readParam(r, "x-message", "message", "m")
 	if messageStr != "" {
@@ -507,7 +509,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	}
 	m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
 	if err != nil {
-		return false, false, "", errHTTPBadRequestPriorityInvalid
+		return false, false, "", false, errHTTPBadRequestPriorityInvalid
 	}
 	tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
 	if tagsStr != "" {
@@ -519,50 +521,65 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
 	if delayStr != "" {
 		if !cache {
-			return false, false, "", errHTTPBadRequestDelayNoCache
+			return false, false, "", false, errHTTPBadRequestDelayNoCache
 		}
 		if email != "" {
-			return false, false, "", errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
+			return false, false, "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
 		}
 		delay, err := util.ParseFutureTime(delayStr, time.Now())
 		if err != nil {
-			return false, false, "", errHTTPBadRequestDelayCannotParse
+			return false, false, "", false, errHTTPBadRequestDelayCannotParse
 		} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
-			return false, false, "", errHTTPBadRequestDelayTooSmall
+			return false, false, "", false, errHTTPBadRequestDelayTooSmall
 		} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
-			return false, false, "", errHTTPBadRequestDelayTooLarge
+			return false, false, "", false, errHTTPBadRequestDelayTooLarge
 		}
 		m.Time = delay.Unix()
 	}
-	unifiedpush := readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
+	unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
 	if unifiedpush {
 		firebase = false
+		unifiedpush = true
 	}
-	return cache, firebase, email, nil
+	return cache, firebase, email, unifiedpush, nil
 }
 
 // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
 //
-// 1. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
+// 1. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
+//    If body is binary, encode as base64, if not do not encode
+// 2. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
 //    Body must be a message, because we attached an external URL
-// 2. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
+// 3. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
 //    Body must be attachment, because we passed a filename
-// 3. curl -T file.txt ntfy.sh/mytopic
-//    If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
 // 4. curl -T file.txt ntfy.sh/mytopic
+//    If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
+// 5. 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.PeakedReadCloser) error {
-	if m.Attachment != nil && m.Attachment.URL != "" {
-		return s.handleBodyAsMessage(m, body) // Case 1
+func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser, unifiedpush bool) error {
+	if unifiedpush {
+		return s.handleBodyAsMessageAutoDetect(m, body) // Case 1
+	} else if m.Attachment != nil && m.Attachment.URL != "" {
+		return s.handleBodyAsTextMessage(m, body) // Case 2
 	} else if m.Attachment != nil && m.Attachment.Name != "" {
-		return s.handleBodyAsAttachment(r, v, m, body) // Case 2
+		return s.handleBodyAsAttachment(r, v, m, body) // Case 3
 	} else if !body.LimitReached && utf8.Valid(body.PeakedBytes) {
-		return s.handleBodyAsMessage(m, body) // Case 3
+		return s.handleBodyAsTextMessage(m, body) // Case 4
 	}
-	return s.handleBodyAsAttachment(r, v, m, body) // Case 4
+	return s.handleBodyAsAttachment(r, v, m, body) // Case 5
+}
+
+func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeakedReadCloser) error {
+	if utf8.Valid(body.PeakedBytes) {
+		m.Message = string(body.PeakedBytes) // Do not trim
+	} else {
+		m.Message = base64.StdEncoding.EncodeToString(body.PeakedBytes)
+		m.Encoding = encodingBase64
+	}
+	return nil
 }
 
-func (s *Server) handleBodyAsMessage(m *message, body *util.PeakedReadCloser) error {
+func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeakedReadCloser) error {
 	if !utf8.Valid(body.PeakedBytes) {
 		return errHTTPBadRequestMessageNotUTF8
 	}

+ 45 - 0
server/server_test.go

@@ -3,10 +3,12 @@ package server
 import (
 	"bufio"
 	"context"
+	"encoding/base64"
 	"encoding/json"
 	"fmt"
 	"github.com/stretchr/testify/require"
 	"heckel.io/ntfy/util"
+	"math/rand"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -623,6 +625,49 @@ func TestServer_UnifiedPushDiscovery(t *testing.T) {
 	require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String())
 }
 
+func TestServer_PublishUnifiedPushBinary(t *testing.T) {
+	b := make([]byte, 12) // Max length
+	_, err := rand.Read(b)
+	require.Nil(t, err)
+
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "PUT", "/mytopic?up=1", string(b), nil)
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "base64", m.Encoding)
+	b2, err := base64.StdEncoding.DecodeString(m.Message)
+	require.Nil(t, err)
+	require.Equal(t, b, b2)
+}
+
+func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) {
+	b := make([]byte, 5000) // Longer than max length
+	_, err := rand.Read(b)
+	require.Nil(t, err)
+
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "PUT", "/mytopic?up=1", string(b), nil)
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "base64", m.Encoding)
+	b2, err := base64.StdEncoding.DecodeString(m.Message)
+	require.Nil(t, err)
+	require.Equal(t, 4096, len(b2))
+	require.Equal(t, b[:4096], b2)
+}
+
+func TestServer_PublishUnifiedPushText(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+	response := request(t, s, "PUT", "/mytopic?up=1", "this is a unifiedpush text message", nil)
+	require.Equal(t, 200, response.Code)
+
+	m := toMessage(t, response.Body.String())
+	require.Equal(t, "", m.Encoding)
+	require.Equal(t, "this is a unifiedpush text message", m.Message)
+}
+
 func TestServer_PublishAttachment(t *testing.T) {
 	content := util.RandomString(5000) // > 4096
 	s := newTestServer(t, newTestConfig(t))

+ 1 - 0
server/types.go

@@ -29,6 +29,7 @@ type message struct {
 	Attachment *attachment `json:"attachment,omitempty"`
 	Title      string      `json:"title,omitempty"`
 	Message    string      `json:"message,omitempty"`
+	Encoding   string      `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
 }
 
 type attachment struct {