From 29628a66a68063002a19ab99cac52bbc2a98b5fd Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 25 Dec 2021 11:56:02 +0100 Subject: [PATCH 1/3] Initial --- server/server.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/server.go b/server/server.go index 0c0f0f0e..122b2a0f 100644 --- a/server/server.go +++ b/server/server.go @@ -253,6 +253,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { return s.handleHome(w, r) } else if r.Method == http.MethodGet && r.URL.Path == "/example.html" { return s.handleExample(w, r) + } else if r.Method == http.MethodGet && r.URL.Path == "/up" { + return s.handleUnifiedPush(w, r) } else if r.Method == http.MethodHead && r.URL.Path == "/" { return s.handleEmpty(w, r) } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { @@ -293,6 +295,13 @@ func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error { return err } +func (s *Server) handleUnifiedPush(w http.ResponseWriter, r *http.Request) error { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests + _, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`) + return err +} + func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error { http.FileServer(http.FS(webStaticFsCached)).ServeHTTP(w, r) return nil From 63a29380a94ed5f080bfee607cde9ab8941c4124 Mon Sep 17 00:00:00 2001 From: Karmanyaah Malhotra Date: Sat, 25 Dec 2021 10:26:18 -0600 Subject: [PATCH 2/3] up testing --- server/message.go | 20 ++++++++++-------- server/server.go | 53 ++++++++++++++++++++++++++--------------------- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/server/message.go b/server/message.go index ad870e09..1afa34f6 100644 --- a/server/message.go +++ b/server/message.go @@ -1,8 +1,9 @@ package server import ( - "heckel.io/ntfy/util" "time" + + "heckel.io/ntfy/util" ) // List of possible events @@ -18,14 +19,15 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - Time int64 `json:"time"` // Unix time in seconds - Event string `json:"event"` // One of the above - Topic string `json:"topic"` - Priority int `json:"priority,omitempty"` - Tags []string `json:"tags,omitempty"` - Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` + ID string `json:"id"` // Random message ID + Time int64 `json:"time"` // Unix time in seconds + Event string `json:"event"` // One of the above + Topic string `json:"topic"` + Priority int `json:"priority,omitempty"` + Tags []string `json:"tags,omitempty"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` + UnifiedPush bool `json:"unifiedpush,omitempty"` //this could be 'up' } // messageEncoder is a function that knows how to encode a message diff --git a/server/server.go b/server/server.go index 122b2a0f..5a4b4cbf 100644 --- a/server/server.go +++ b/server/server.go @@ -5,11 +5,7 @@ import ( "context" "embed" "encoding/json" - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" "fmt" - "google.golang.org/api/option" - "heckel.io/ntfy/util" "html/template" "io" "log" @@ -20,6 +16,11 @@ import ( "strings" "sync" "time" + + firebase "firebase.google.com/go" + "firebase.google.com/go/messaging" + "google.golang.org/api/option" + "heckel.io/ntfy/util" ) // TODO add "max messages in a topic" limit @@ -253,8 +254,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { return s.handleHome(w, r) } else if r.Method == http.MethodGet && r.URL.Path == "/example.html" { return s.handleExample(w, r) - } else if r.Method == http.MethodGet && r.URL.Path == "/up" { - return s.handleUnifiedPush(w, r) } else if r.Method == http.MethodHead && r.URL.Path == "/" { return s.handleEmpty(w, r) } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { @@ -295,13 +294,6 @@ func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error { return err } -func (s *Server) handleUnifiedPush(w http.ResponseWriter, r *http.Request) error { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests - _, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`) - return err -} - func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error { http.FileServer(http.FS(webStaticFsCached)).ServeHTTP(w, r) return nil @@ -323,15 +315,25 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } m := newDefaultMessage(t.ID, strings.TrimSpace(string(b))) - cache, firebase, email, err := s.parseParams(r, m) + cache, firebase, email, unifiedpush, err := s.parseParams(r, m) if err != nil { return err } + + if r.Method == http.MethodGet && unifiedpush { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests + _, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`) + return err + } + if email != "" { if err := v.EmailAllowed(); err != nil { return err } } + + m.UnifiedPush = unifiedpush if s.mailer == nil && email != "" { return errHTTPBadRequest } @@ -344,20 +346,21 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } } - if s.firebase != nil && firebase && !delayed { + if s.firebase != nil && firebase && !delayed && !unifiedpush { go func() { if err := s.firebase(m); err != nil { log.Printf("Unable to publish to Firebase: %v", err.Error()) } }() } - if s.mailer != nil && email != "" && !delayed { + if s.mailer != nil && email != "" && !delayed && !unifiedpush { go func() { if err := s.mailer.Send(v.ip, email, m); err != nil { log.Printf("Unable to send email: %v", err.Error()) } }() } + if cache { if err := s.cache.AddMessage(m); err != nil { return err @@ -365,6 +368,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests + if err := json.NewEncoder(w).Encode(m); err != nil { return err } @@ -372,10 +376,11 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return nil } -func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase bool, email string, err error) { +func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) { cache = readParam(r, "x-cache", "cache") != "no" firebase = readParam(r, "x-firebase", "firebase") != "no" email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") + unifiedpush = readParam(r, "up", "unifiedpush") == "1" m.Title = readParam(r, "x-title", "title", "t") messageStr := readParam(r, "x-message", "message", "m") if messageStr != "" { @@ -383,7 +388,7 @@ func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase } m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p")) if err != nil { - return false, false, "", errHTTPBadRequest + return false, false, "", false, errHTTPBadRequest } tagsStr := readParam(r, "x-tags", "tags", "tag", "ta") if tagsStr != "" { @@ -395,22 +400,22 @@ func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in") if delayStr != "" { if !cache { - return false, false, "", errHTTPBadRequest + return false, false, "", false, errHTTPBadRequest } if email != "" { - return false, false, "", errHTTPBadRequest // we cannot store the email address (yet) + return false, false, "", false, errHTTPBadRequest // we cannot store the email address (yet) } delay, err := util.ParseFutureTime(delayStr, time.Now()) if err != nil { - return false, false, "", errHTTPBadRequest + return false, false, "", false, errHTTPBadRequest } else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() { - return false, false, "", errHTTPBadRequest + return false, false, "", false, errHTTPBadRequest } else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() { - return false, false, "", errHTTPBadRequest + return false, false, "", false, errHTTPBadRequest } m.Time = delay.Unix() } - return cache, firebase, email, nil + return cache, firebase, email, unifiedpush, nil } func readParam(r *http.Request, names ...string) string { From d6762276f58f9095956819a82ec051d368079670 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 25 Dec 2021 22:07:55 +0100 Subject: [PATCH 3/3] Test --- server/message.go | 20 +++++++++---------- server/server.go | 46 ++++++++++++++++++++++--------------------- server/server_test.go | 7 +++++++ 3 files changed, 40 insertions(+), 33 deletions(-) diff --git a/server/message.go b/server/message.go index 1afa34f6..ad870e09 100644 --- a/server/message.go +++ b/server/message.go @@ -1,9 +1,8 @@ package server import ( - "time" - "heckel.io/ntfy/util" + "time" ) // List of possible events @@ -19,15 +18,14 @@ const ( // message represents a message published to a topic type message struct { - ID string `json:"id"` // Random message ID - Time int64 `json:"time"` // Unix time in seconds - Event string `json:"event"` // One of the above - Topic string `json:"topic"` - Priority int `json:"priority,omitempty"` - Tags []string `json:"tags,omitempty"` - Title string `json:"title,omitempty"` - Message string `json:"message,omitempty"` - UnifiedPush bool `json:"unifiedpush,omitempty"` //this could be 'up' + ID string `json:"id"` // Random message ID + Time int64 `json:"time"` // Unix time in seconds + Event string `json:"event"` // One of the above + Topic string `json:"topic"` + Priority int `json:"priority,omitempty"` + Tags []string `json:"tags,omitempty"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` } // messageEncoder is a function that knows how to encode a message diff --git a/server/server.go b/server/server.go index 7ef59394..78715d20 100644 --- a/server/server.go +++ b/server/server.go @@ -5,7 +5,11 @@ import ( "context" "embed" "encoding/json" + firebase "firebase.google.com/go" + "firebase.google.com/go/messaging" "fmt" + "google.golang.org/api/option" + "heckel.io/ntfy/util" "html/template" "io" "log" @@ -16,11 +20,6 @@ import ( "strings" "sync" "time" - - firebase "firebase.google.com/go" - "firebase.google.com/go/messaging" - "google.golang.org/api/option" - "heckel.io/ntfy/util" ) // TODO add "max messages in a topic" limit @@ -288,7 +287,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { } else if r.Method == http.MethodOptions { return s.handleOptions(w, r) } else if r.Method == http.MethodGet && topicRegex.MatchString(r.URL.Path) { - return s.handleHome(w, r) + return s.handleTopic(w, r) } else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) { return s.withRateLimit(w, r, s.handlePublish) } else if r.Method == http.MethodGet && sendRegex.MatchString(r.URL.Path) { @@ -310,6 +309,17 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error { }) } +func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error { + unifiedpush := readParam(r, "x-unifiedpush", "unifiedpush", "up") == "1" // see PUT/POST too! + if unifiedpush { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests + _, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`+"\n") + return err + } + return s.handleHome(w, r) +} + func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request) error { return nil } @@ -340,25 +350,15 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } m := newDefaultMessage(t.ID, strings.TrimSpace(string(b))) - cache, firebase, email, err := s.parseParams(r, m) + cache, firebase, email, err := s.parsePublishParams(r, m) if err != nil { return err } - - if r.Method == http.MethodGet && unifiedpush { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests - _, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`) - return err - } - if email != "" { if err := v.EmailAllowed(); err != nil { return errHTTPTooManyRequestsLimitEmails } } - - m.UnifiedPush = unifiedpush if s.mailer == nil && email != "" { return errHTTPBadRequestEmailDisabled } @@ -371,21 +371,20 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return err } } - if s.firebase != nil && firebase && !delayed && !unifiedpush { + if s.firebase != nil && firebase && !delayed { go func() { if err := s.firebase(m); err != nil { log.Printf("Unable to publish to Firebase: %v", err.Error()) } }() } - if s.mailer != nil && email != "" && !delayed && !unifiedpush { + if s.mailer != nil && email != "" && !delayed { go func() { if err := s.mailer.Send(v.ip, email, m); err != nil { log.Printf("Unable to send email: %v", err.Error()) } }() } - if cache { if err := s.cache.AddMessage(m); err != nil { return err @@ -393,7 +392,6 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito } w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests - if err := json.NewEncoder(w).Encode(m); err != nil { return err } @@ -401,7 +399,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito return nil } -func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase bool, email string, err error) { +func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email string, err error) { cache = readParam(r, "x-cache", "cache") != "no" firebase = readParam(r, "x-firebase", "firebase") != "no" email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") @@ -439,6 +437,10 @@ func (s *Server) parseParams(r *http.Request, m *message) (cache bool, firebase } m.Time = delay.Unix() } + unifiedpush := readParam(r, "x-unifiedpush", "unifiedpush", "up") == "1" // see GET too! + if unifiedpush { + firebase = false + } return cache, firebase, email, nil } diff --git a/server/server_test.go b/server/server_test.go index cf3fc58c..589b982c 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -583,6 +583,13 @@ func TestServer_PublishEmailNoMailer_Fail(t *testing.T) { require.Equal(t, 400, response.Code) } +func TestServer_UnifiedPushDiscovery(t *testing.T) { + s := newTestServer(t, newTestConfig(t)) + response := request(t, s, "GET", "/mytopic?up=1", "", nil) + require.Equal(t, 200, response.Code) + require.Equal(t, `{"unifiedpush":{"version":1}}`+"\n", response.Body.String()) +} + func newTestConfig(t *testing.T) *Config { conf := NewConfig() conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")