implement scheduled message unpublishing via /topic/{messageId}

This commit is contained in:
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 2024-06-30 00:46:42 -04:00
parent 9d3fc20e58
commit 37468f38c2
No known key found for this signature in database
GPG Key ID: 924A5F6AF051E87C
2 changed files with 83 additions and 2 deletions

View File

@ -77,6 +77,7 @@ 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)$`)
messagePathRegex = regexp.MustCompile(`^/[-A-Za-z-0-9]{1,64}/([A-Za-z0-9]{12})$`)
webConfigPath = "/config.js" webConfigPath = "/config.js"
webManifestPath = "/manifest.webmanifest" webManifestPath = "/manifest.webmanifest"
@ -537,6 +538,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
return s.limitRequests(s.authorizeTopicRead(s.handleTopicAuth))(w, r, v) return s.limitRequests(s.authorizeTopicRead(s.handleTopicAuth))(w, r, v)
} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) { } else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
return s.ensureWebEnabled(s.handleTopic)(w, r, v) return s.ensureWebEnabled(s.handleTopic)(w, r, v)
} else if r.Method == http.MethodDelete && (messagePathRegex.MatchString(r.URL.Path)) {
return s.limitRequestsWithTopic(s.authorizeTopicWrite(s.handleMessageUnpublish))(w, r, v)
} }
return errHTTPNotFound return errHTTPNotFound
} }
@ -558,6 +561,25 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request, v *visitor)
return s.handleStatic(w, r, v) return s.handleStatic(w, r, v)
} }
func (s *Server) handleMessageUnpublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
segments := strings.Split(r.URL.Path, "/")
msg, err := s.messageCache.Message(segments[len(segments)-1])
if err != nil {
if err == errMessageNotFound {
return errHTTPNotFound
}
return err
}
if time.Now().Unix() >= msg.Time {
httpErr := errHTTPBadRequest
alreadyPublished := httpErr.Wrap("Message \"%s\" has already been published", msg.ID)
s.handleError(w, r, v, alreadyPublished)
return alreadyPublished
}
s.messageCache.DeleteMessages(msg.ID)
return s.writeJSON(w, msg)
}
func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error { func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
return nil return nil
} }

View File

@ -7,8 +7,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/user"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -22,6 +20,9 @@ import (
"testing" "testing"
"time" "time"
"golang.org/x/crypto/bcrypt"
"heckel.io/ntfy/v2/user"
"github.com/SherClockHolmes/webpush-go" "github.com/SherClockHolmes/webpush-go"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/log" "heckel.io/ntfy/v2/log"
@ -2853,6 +2854,64 @@ template ""}}`,
} }
} }
func TestServer_Message_Unpublish_Existing(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
response1 := request(t, s, "PUT", "/mytopic", "hello universe", map[string]string{
"In": "1m",
})
msg1 := toMessage(t, response1.Body.String())
time.Sleep(500)
response2 := request(t, s, "DELETE", fmt.Sprintf("/mytopic/%s", msg1.ID), "", nil)
require.Equal(t, 200, response2.Code)
msg2 := toMessage(t, response2.Body.String())
require.Equal(t, msg1.ID, msg2.ID)
}
func TestServer_Message_Unpublish_Nonexistent(t *testing.T) {
t.Parallel()
s := newTestServer(t, newTestConfig(t))
response2 := request(t, s, "DELETE", "/mytopic/n0nexist3nt1", "", nil)
require.Equal(t, 404, response2.Code)
}
func TestServer_Message_Unpublish_Protected_Fail_Unauthorized(t *testing.T) {
c := newTestConfigWithAuthFile(t)
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
response1 := request(t, s, "PUT", "/announcements", "hello universe", map[string]string{
"In": "1m",
"Authorization": util.BasicAuth("phil", "phil"),
})
msg1 := toMessage(t, response1.Body.String())
response2 := request(t, s, "DELETE", fmt.Sprintf("/announcements/%s", msg1.ID), "", nil)
require.Equal(t, 403, response2.Code)
}
func TestServer_Message_Unpublish_Private(t *testing.T) {
c := newTestConfigWithAuthFile(t)
c.AuthDefault = user.PermissionDenyAll
s := newTestServer(t, c)
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleAdmin))
require.Nil(t, s.userManager.AllowAccess("phil", "announcements", user.PermissionReadWrite))
h := map[string]string{
"In": "1m",
"Authorization": util.BasicAuth("phil", "phil"),
}
response1 := request(t, s, "PUT", "/announcements", "hello universe", h)
msg1 := toMessage(t, response1.Body.String())
response2 := request(t, s, "DELETE", fmt.Sprintf("/announcements/%s", msg1.ID), "", h)
msg2 := toMessage(t, response2.Body.String())
require.Equal(t, 200, response2.Code)
require.Equal(t, msg1.ID, msg2.ID)
}
func newTestConfig(t *testing.T) *Config { func newTestConfig(t *testing.T) *Config {
conf := NewConfig() conf := NewConfig()
conf.BaseURL = "http://127.0.0.1:12345" conf.BaseURL = "http://127.0.0.1:12345"