Просмотр исходного кода

publish messages with a custom sequence ID

Hunter Kehoe 4 месяцев назад
Родитель
Сommit
83b5470bc5
3 измененных файлов с 99 добавлено и 1 удалено
  1. 1 0
      server/errors.go
  2. 30 1
      server/server.go
  3. 68 0
      server/server_test.go

+ 1 - 0
server/errors.go

@@ -125,6 +125,7 @@ var (
 	errHTTPBadRequestInvalidUsername                 = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
 	errHTTPBadRequestTemplateFileNotFound            = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
 	errHTTPBadRequestTemplateFileInvalid             = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
+	errHTTPBadRequestSIDInvalid                      = &errHTTP{40049, http.StatusBadRequest, "invalid request: SID invalid", "https://ntfy.sh/docs/publish/#TODO", 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}

+ 30 - 1
server/server.go

@@ -79,6 +79,8 @@ var (
 	wsPathRegex            = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
 	authPathRegex          = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
 	publishPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/(publish|send|trigger)$`)
+	sidRegex               = topicRegex
+	updatePathRegex        = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}/[-_A-Za-z0-9]{1,64}$`)
 
 	webConfigPath                                        = "/config.js"
 	webManifestPath                                      = "/manifest.webmanifest"
@@ -542,7 +544,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 		return s.transformBodyJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish)))(w, r, v)
 	} else if r.Method == http.MethodPost && r.URL.Path == matrixPushPath {
 		return s.transformMatrixJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublishMatrix)))(w, r, v)
-	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
+	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && (topicPathRegex.MatchString(r.URL.Path) || updatePathRegex.MatchString(r.URL.Path)) {
 		return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
 	} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
@@ -955,6 +957,24 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
 }
 
 func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template templateMode, unifiedpush bool, err *errHTTP) {
+	if r.Method != http.MethodGet && updatePathRegex.MatchString(r.URL.Path) {
+		pathSID, err := s.sidFromPath(r.URL.Path)
+		if err != nil {
+			return false, false, "", "", "", false, err
+		}
+		m.SID = pathSID
+	} else {
+		sid := readParam(r, "x-sequence-id", "sequence-id", "sid")
+		if sid != "" {
+			if sidRegex.MatchString(sid) {
+				m.SID = sid
+			} else {
+				return false, false, "", "", "", false, errHTTPBadRequestSIDInvalid
+			}
+		} else {
+			m.SID = m.ID
+		}
+	}
 	cache = readBoolParam(r, true, "x-cache", "cache")
 	firebase = readBoolParam(r, true, "x-firebase", "firebase")
 	m.Title = readParam(r, "x-title", "title", "t")
@@ -1693,6 +1713,15 @@ func (s *Server) topicsFromPath(path string) ([]*topic, string, error) {
 	return topics, parts[1], nil
 }
 
+// sidFromPath returns the SID from a POST path like /mytopic/sidHere
+func (s *Server) sidFromPath(path string) (string, *errHTTP) {
+	parts := strings.Split(path, "/")
+	if len(parts) != 3 {
+		return "", errHTTPBadRequestSIDInvalid
+	}
+	return parts[2], nil
+}
+
 // topicsFromIDs returns the topics with the given IDs, creating them if they don't exist.
 func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
 	s.mu.Lock()

+ 68 - 0
server/server_test.go

@@ -703,6 +703,74 @@ func TestServer_PublishInvalidTopic(t *testing.T) {
 	require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code)
 }
 
+func TestServer_PublishWithSIDInPath(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "POST", "/mytopic/sid", "message", nil)
+	msg := toMessage(t, response.Body.String())
+	require.NotEmpty(t, msg.ID)
+	require.Equal(t, "sid", msg.SID)
+}
+
+func TestServer_PublishWithSIDInHeader(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "POST", "/mytopic", "message", map[string]string{
+		"sid": "sid",
+	})
+	msg := toMessage(t, response.Body.String())
+	require.NotEmpty(t, msg.ID)
+	require.Equal(t, "sid", msg.SID)
+}
+
+func TestServer_PublishWithSIDInPathAndHeader(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "PUT", "/mytopic/sid1", "message", map[string]string{
+		"sid": "sid2",
+	})
+	msg := toMessage(t, response.Body.String())
+	require.NotEmpty(t, msg.ID)
+	require.Equal(t, "sid1", msg.SID) // SID in path has priority over SID in header
+}
+
+func TestServer_PublishWithSIDInQuery(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "PUT", "/mytopic?sid=sid1", "message", nil)
+	msg := toMessage(t, response.Body.String())
+	require.NotEmpty(t, msg.ID)
+	require.Equal(t, "sid1", msg.SID)
+}
+
+func TestServer_PublishWithSIDViaGet(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "GET", "/mytopic/publish?sid=sid1", "message", nil)
+	msg := toMessage(t, response.Body.String())
+	require.NotEmpty(t, msg.ID)
+	require.Equal(t, "sid1", msg.SID)
+}
+
+func TestServer_PublishWithInvalidSIDInPath(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "POST", "/mytopic/.", "message", nil)
+
+	require.Equal(t, 404, response.Code)
+}
+
+func TestServer_PublishWithInvalidSIDInHeader(t *testing.T) {
+	s := newTestServer(t, newTestConfig(t))
+
+	response := request(t, s, "POST", "/mytopic", "message", map[string]string{
+		"X-Sequence-ID": "*&?",
+	})
+
+	require.Equal(t, 400, response.Code)
+	require.Equal(t, 40049, toHTTPError(t, response.Body.String()).Code)
+}
+
 func TestServer_PollWithQueryFilters(t *testing.T) {
 	s := newTestServer(t, newTestConfig(t))