Merge branch 'main' into done

This commit is contained in:
Philipp Heckel 2022-06-20 20:34:00 -04:00
commit c40338c146
5 changed files with 107 additions and 25 deletions

View file

@ -6,11 +6,13 @@ import (
"crypto/subtle" "crypto/subtle"
"errors" "errors"
"fmt" "fmt"
"os"
"strings"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/auth" "heckel.io/ntfy/auth"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
"strings"
) )
func init() { func init() {
@ -36,7 +38,7 @@ var cmdUser = &cli.Command{
Name: "add", Name: "add",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "Adds a new user", Usage: "Adds a new user",
UsageText: "ntfy user add [--role=admin|user] USERNAME", UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME",
Action: execUserAdd, Action: execUserAdd,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(auth.RoleUser), Usage: "user role"}, &cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(auth.RoleUser), Usage: "user role"},
@ -50,6 +52,10 @@ topics.
Examples: Examples:
ntfy user add phil # Add regular user phil ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil ntfy user add --role=admin phil # Add admin user phil
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if
you are creating users via scripts.
`, `,
}, },
{ {
@ -68,7 +74,7 @@ Example:
Name: "change-pass", Name: "change-pass",
Aliases: []string{"chp"}, Aliases: []string{"chp"},
Usage: "Changes a user's password", Usage: "Changes a user's password",
UsageText: "ntfy user change-pass USERNAME", UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME",
Action: execUserChangePass, Action: execUserChangePass,
Description: `Change the password for the given user. Description: `Change the password for the given user.
@ -77,6 +83,11 @@ it twice.
Example: Example:
ntfy user change-pass phil ntfy user change-pass phil
NTFY_PASSWORD=.. ntfy user change-pass phil
You may set the NTFY_PASSWORD environment variable to pass the new password. This is
useful if you are updating users via scripts.
`, `,
}, },
{ {
@ -127,16 +138,22 @@ passwords or roles.
Examples: Examples:
ntfy user list # Shows list of users (alias: 'ntfy access') ntfy user list # Shows list of users (alias: 'ntfy access')
ntfy user add phil # Add regular user phil ntfy user add phil # Add regular user phil
NTFY_PASSWORD=... ntfy user add phil # As above, using env variable to set password (for scripts)
ntfy user add --role=admin phil # Add admin user phil ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil ntfy user change-pass phil # Change password for user phil
NTFY_PASSWORD=.. ntfy user change-pass phil # As above, using env variable to set password (for scripts)
ntfy user change-role phil admin # Make user phil an admin ntfy user change-role phil admin # Make user phil an admin
For the 'ntfy user add' and 'ntfy user change-pass' commands, you may set the NTFY_PASSWORD environment
variable to pass the new password. This is useful if you are creating/updating users via scripts.
`, `,
} }
func execUserAdd(c *cli.Context) error { func execUserAdd(c *cli.Context) error {
username := c.Args().Get(0) username := c.Args().Get(0)
role := auth.Role(c.String("role")) role := auth.Role(c.String("role"))
password := os.Getenv("NTFY_PASSWORD")
if username == "" { if username == "" {
return errors.New("username expected, type 'ntfy user add --help' for help") return errors.New("username expected, type 'ntfy user add --help' for help")
} else if username == userEveryone { } else if username == userEveryone {
@ -151,10 +168,14 @@ func execUserAdd(c *cli.Context) error {
if user, _ := manager.User(username); user != nil { if user, _ := manager.User(username); user != nil {
return fmt.Errorf("user %s already exists", username) return fmt.Errorf("user %s already exists", username)
} }
password, err := readPasswordAndConfirm(c) if password == "" {
p, err := readPasswordAndConfirm(c)
if err != nil { if err != nil {
return err return err
} }
password = p
}
if err := manager.AddUser(username, password, role); err != nil { if err := manager.AddUser(username, password, role); err != nil {
return err return err
} }
@ -185,6 +206,7 @@ func execUserDel(c *cli.Context) error {
func execUserChangePass(c *cli.Context) error { func execUserChangePass(c *cli.Context) error {
username := c.Args().Get(0) username := c.Args().Get(0)
password := os.Getenv("NTFY_PASSWORD")
if username == "" { if username == "" {
return errors.New("username expected, type 'ntfy user change-pass --help' for help") return errors.New("username expected, type 'ntfy user change-pass --help' for help")
} else if username == userEveryone { } else if username == userEveryone {
@ -197,10 +219,12 @@ func execUserChangePass(c *cli.Context) error {
if _, err := manager.User(username); err == auth.ErrNotFound { if _, err := manager.User(username); err == auth.ErrNotFound {
return fmt.Errorf("user %s does not exist", username) return fmt.Errorf("user %s does not exist", username)
} }
password, err := readPasswordAndConfirm(c) if password == "" {
password, err = readPasswordAndConfirm(c)
if err != nil { if err != nil {
return err return err
} }
}
if err := manager.ChangePassword(username, password); err != nil { if err := manager.ChangePassword(username, password); err != nil {
return err return err
} }

View file

@ -9,11 +9,14 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Features:** **Features:**
* Trace: Log entire HTTP request to simplify debugging (no ticket) * Trace: Log entire HTTP request to simplify debugging (no ticket)
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
**Bugs:** **Bugs:**
* Return HTTP 500 for GET /_matrix/push/v1/notify when base-url is not configured (no ticket) * Return HTTP 500 for GET /_matrix/push/v1/notify when base-url is not configured (no ticket)
* Disallow setting `upstream-base-url` to the same value as `base-url` ([#334](https://github.com/binwiederhier/ntfy/issues/334), thanks to [@oester](https://github.com/oester) for reporting) * Disallow setting `upstream-base-url` to the same value as `base-url` ([#334](https://github.com/binwiederhier/ntfy/issues/334), thanks to [@oester](https://github.com/oester) for reporting)
* Fix `since=<id>` implementation for multiple topics ([#336](https://github.com/binwiederhier/ntfy/issues/336), thanks to [@karmanyaahm](https://github.com/karmanyaahm) for reporting)
## ntfy Android app v1.14.0 (UNRELEASED) ## ntfy Android app v1.14.0 (UNRELEASED)

View file

@ -49,7 +49,7 @@ const (
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1` pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?` selectRowIDFromMessageID = `SELECT id FROM messages WHERE mid = ?` // Do not include topic, see #336 and TestServer_PollSinceID_MultipleTopics
selectMessagesSinceTimeQuery = ` selectMessagesSinceTimeQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, encoding
FROM messages FROM messages
@ -294,7 +294,7 @@ func (c *messageCache) messagesSinceTime(topic string, since sinceMarker, schedu
} }
func (c *messageCache) messagesSinceID(topic string, since sinceMarker, scheduled bool) ([]*message, error) { func (c *messageCache) messagesSinceID(topic string, since sinceMarker, scheduled bool) ([]*message, error) {
idrows, err := c.db.Query(selectRowIDFromMessageID, topic, since.ID()) idrows, err := c.db.Query(selectRowIDFromMessageID, since.ID())
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -16,6 +16,7 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -972,21 +973,28 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
return return
} }
// sendOldMessages selects old messages from the messageCache and calls sub for each of them. It uses since as the
// marker, returning only messages that are newer than the marker.
func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, v *visitor, sub subscriber) error { func (s *Server) sendOldMessages(topics []*topic, since sinceMarker, scheduled bool, v *visitor, sub subscriber) error {
if since.IsNone() { if since.IsNone() {
return nil return nil
} }
messages := make([]*message, 0)
for _, t := range topics { for _, t := range topics {
messages, err := s.messageCache.Messages(t.ID, since, scheduled) topicMessages, err := s.messageCache.Messages(t.ID, since, scheduled)
if err != nil { if err != nil {
return err return err
} }
messages = append(messages, topicMessages...)
}
sort.Slice(messages, func(i, j int) bool {
return messages[i].Time < messages[j].Time
})
for _, m := range messages { for _, m := range messages {
if err := sub(v, m); err != nil { if err := sub(v, m); err != nil {
return err return err
} }
} }
}
return nil return nil
} }

View file

@ -437,6 +437,53 @@ func TestServer_PublishAndPollSince(t *testing.T) {
require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code) require.Equal(t, 40008, toHTTPError(t, response.Body.String()).Code)
} }
func newMessageWithTimestamp(topic, message string, timestamp int64) *message {
m := newDefaultMessage(topic, message)
m.Time = timestamp
return m
}
func TestServer_PollSinceID_MultipleTopics(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 1", 1655740277)))
markerMessage := newMessageWithTimestamp("mytopic2", "test 2", 1655740283)
require.Nil(t, s.messageCache.AddMessage(markerMessage))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289)))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293)))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297)))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303)))
response := request(t, s, "GET", fmt.Sprintf("/mytopic1,mytopic2/json?poll=1&since=%s", markerMessage.ID), "", nil)
messages := toMessages(t, response.Body.String())
require.Equal(t, 4, len(messages))
require.Equal(t, "test 3", messages[0].Message)
require.Equal(t, "mytopic1", messages[0].Topic)
require.Equal(t, "test 4", messages[1].Message)
require.Equal(t, "mytopic2", messages[1].Topic)
require.Equal(t, "test 5", messages[2].Message)
require.Equal(t, "mytopic1", messages[2].Topic)
require.Equal(t, "test 6", messages[3].Message)
require.Equal(t, "mytopic2", messages[3].Topic)
}
func TestServer_PollSinceID_MultipleTopics_IDDoesNotMatch(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 3", 1655740289)))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 4", 1655740293)))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic1", "test 5", 1655740297)))
require.Nil(t, s.messageCache.AddMessage(newMessageWithTimestamp("mytopic2", "test 6", 1655740303)))
response := request(t, s, "GET", "/mytopic1,mytopic2/json?poll=1&since=NoMatchForID", "", nil)
messages := toMessages(t, response.Body.String())
require.Equal(t, 4, len(messages))
require.Equal(t, "test 3", messages[0].Message)
require.Equal(t, "test 4", messages[1].Message)
require.Equal(t, "test 5", messages[2].Message)
require.Equal(t, "test 6", messages[3].Message)
}
func TestServer_PublishViaGET(t *testing.T) { func TestServer_PublishViaGET(t *testing.T) {
s := newTestServer(t, newTestConfig(t)) s := newTestServer(t, newTestConfig(t))