publish messages with a custom sequence ID
This commit is contained in:
@@ -125,6 +125,7 @@ var (
|
|||||||
errHTTPBadRequestInvalidUsername = &errHTTP{40046, http.StatusBadRequest, "invalid request: invalid username", "", nil}
|
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}
|
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}
|
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}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", 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}
|
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ var (
|
|||||||
wsPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
|
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$`)
|
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)$`)
|
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"
|
webConfigPath = "/config.js"
|
||||||
webManifestPath = "/manifest.webmanifest"
|
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)
|
return s.transformBodyJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish)))(w, r, v)
|
||||||
} else if r.Method == http.MethodPost && r.URL.Path == matrixPushPath {
|
} else if r.Method == http.MethodPost && r.URL.Path == matrixPushPath {
|
||||||
return s.transformMatrixJSON(s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublishMatrix)))(w, r, v)
|
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)
|
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handlePublish))(w, r, v)
|
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) {
|
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")
|
cache = readBoolParam(r, true, "x-cache", "cache")
|
||||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||||
m.Title = readParam(r, "x-title", "title", "t")
|
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
|
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.
|
// topicsFromIDs returns the topics with the given IDs, creating them if they don't exist.
|
||||||
func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
|
func (s *Server) topicsFromIDs(ids ...string) ([]*topic, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|||||||
@@ -703,6 +703,74 @@ func TestServer_PublishInvalidTopic(t *testing.T) {
|
|||||||
require.Equal(t, 40010, toHTTPError(t, response.Body.String()).Code)
|
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) {
|
func TestServer_PollWithQueryFilters(t *testing.T) {
|
||||||
s := newTestServer(t, newTestConfig(t))
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user