Compare commits
4 Commits
b5ac60961c
...
a39b4725d3
| Author | SHA1 | Date |
|---|---|---|
|
|
a39b4725d3 | |
|
|
30301c8a7f | |
|
|
7b470a7f6f | |
|
|
37468f38c2 |
40
README.md
40
README.md
|
|
@ -56,20 +56,18 @@ For announcements of new releases and cutting-edge beta versions, please subscri
|
|||
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
|
||||
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
|
||||
|
||||
## Contributing
|
||||
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
||||
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
||||
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/ntfy/">
|
||||
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Sponsors
|
||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
|
||||
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
||||
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks who have sponsored ntfy in the past, or are still sponsoring ntfy:
|
||||
If you'd like to support the ntfy maintainers, please consider donating to [GitHub Sponsors](https://github.com/sponsors/binwiederhier) or
|
||||
and [Liberapay](https://liberapay.com/ntfy). We would be humbled if you helped carry the server and developer
|
||||
account costs. Even small donations are very much appreciated.
|
||||
|
||||
Thank you to our commercial sponsors, who help keep the service running and the development going:
|
||||
|
||||
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
||||
|
||||
<a href="https://www.magicbell.com/?utm_source=ntfy"><img src="assets/sponsors/magicbell.png" width="180px"></a>
|
||||
|
||||
And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy:
|
||||
|
||||
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||
|
|
@ -210,13 +208,21 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
|
|||
<a href="https://github.com/user8446"><img src="https://github.com/user8446.png" width="40px" /></a>
|
||||
<a href="https://github.com/cdf-eagles"><img src="https://github.com/cdf-eagles.png" width="40px" /></a>
|
||||
|
||||
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
|
||||
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
||||
## Contributing
|
||||
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
|
||||
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
|
||||
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
|
||||
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
|
||||
|
||||
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
|
||||
<a href="https://hosted.weblate.org/engage/ntfy/">
|
||||
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Code of Conduct
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for
|
||||
everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity
|
||||
and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste,
|
||||
color, religion, or sexual identity and orientation.
|
||||
|
||||
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -77,6 +77,7 @@ 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)$`)
|
||||
messagePathRegex = regexp.MustCompile(`^/[-A-Za-z-0-9]{1,64}/([A-Za-z0-9]{12})$`)
|
||||
|
||||
webConfigPath = "/config.js"
|
||||
webManifestPath = "/manifest.webmanifest"
|
||||
|
|
@ -540,6 +541,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
|||
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)) {
|
||||
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
|
||||
}
|
||||
|
|
@ -561,6 +564,25 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request, v *visitor)
|
|||
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 {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
|
@ -22,6 +20,9 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
|
|
@ -2940,6 +2941,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 {
|
||||
conf := NewConfig()
|
||||
conf.BaseURL = "http://127.0.0.1:12345"
|
||||
|
|
|
|||
Loading…
Reference in New Issue