Merge pull request #659 from wunter8/653-default-token
allow default-token and per-subscription tokens in client.yml
This commit is contained in:
commit
3f754f2d02
11 changed files with 598 additions and 77 deletions
|
@ -5,10 +5,12 @@
|
||||||
#
|
#
|
||||||
# default-host: https://ntfy.sh
|
# default-host: https://ntfy.sh
|
||||||
|
|
||||||
# Default username and password will be used with "ntfy publish" if no credentials are provided on command line
|
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
|
||||||
# Default username and password will be used with "ntfy subscribe" if no credentials are provided in subscription below
|
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
|
||||||
# For an empty password, use empty double-quotes ("")
|
# use empty double-quotes ("")
|
||||||
#
|
|
||||||
|
# default-token:
|
||||||
|
|
||||||
# default-user:
|
# default-user:
|
||||||
# default-password:
|
# default-password:
|
||||||
|
|
||||||
|
@ -30,6 +32,8 @@
|
||||||
# command: 'notify-send "$m"'
|
# command: 'notify-send "$m"'
|
||||||
# user: phill
|
# user: phill
|
||||||
# password: mypass
|
# password: mypass
|
||||||
|
# - topic: token_topic
|
||||||
|
# token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
#
|
#
|
||||||
# Variables:
|
# Variables:
|
||||||
# Variable Aliases Description
|
# Variable Aliases Description
|
||||||
|
|
|
@ -12,17 +12,22 @@ const (
|
||||||
|
|
||||||
// Config is the config struct for a Client
|
// Config is the config struct for a Client
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DefaultHost string `yaml:"default-host"`
|
DefaultHost string `yaml:"default-host"`
|
||||||
DefaultUser string `yaml:"default-user"`
|
DefaultUser string `yaml:"default-user"`
|
||||||
DefaultPassword *string `yaml:"default-password"`
|
DefaultPassword *string `yaml:"default-password"`
|
||||||
DefaultCommand string `yaml:"default-command"`
|
DefaultToken string `yaml:"default-token"`
|
||||||
Subscribe []struct {
|
DefaultCommand string `yaml:"default-command"`
|
||||||
Topic string `yaml:"topic"`
|
Subscribe []Subscribe `yaml:"subscribe"`
|
||||||
User string `yaml:"user"`
|
}
|
||||||
Password *string `yaml:"password"`
|
|
||||||
Command string `yaml:"command"`
|
// Subscribe is the struct for a Subscription within Config
|
||||||
If map[string]string `yaml:"if"`
|
type Subscribe struct {
|
||||||
} `yaml:"subscribe"`
|
Topic string `yaml:"topic"`
|
||||||
|
User string `yaml:"user"`
|
||||||
|
Password *string `yaml:"password"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
Command string `yaml:"command"`
|
||||||
|
If map[string]string `yaml:"if"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig creates a new Config struct for a Client
|
// NewConfig creates a new Config struct for a Client
|
||||||
|
@ -31,6 +36,7 @@ func NewConfig() *Config {
|
||||||
DefaultHost: DefaultBaseURL,
|
DefaultHost: DefaultBaseURL,
|
||||||
DefaultUser: "",
|
DefaultUser: "",
|
||||||
DefaultPassword: nil,
|
DefaultPassword: nil,
|
||||||
|
DefaultToken: "",
|
||||||
DefaultCommand: "",
|
DefaultCommand: "",
|
||||||
Subscribe: nil,
|
Subscribe: nil,
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,3 +116,25 @@ subscribe:
|
||||||
require.Equal(t, "phil", conf.Subscribe[0].User)
|
require.Equal(t, "phil", conf.Subscribe[0].User)
|
||||||
require.Nil(t, conf.Subscribe[0].Password)
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_DefaultToken(t *testing.T) {
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(`
|
||||||
|
default-host: http://localhost
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
`), 0600))
|
||||||
|
|
||||||
|
conf, err := client.LoadConfig(filename)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "http://localhost", conf.DefaultHost)
|
||||||
|
require.Equal(t, "", conf.DefaultUser)
|
||||||
|
require.Nil(t, conf.DefaultPassword)
|
||||||
|
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
|
||||||
|
require.Equal(t, 1, len(conf.Subscribe))
|
||||||
|
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
|
||||||
|
require.Equal(t, "", conf.Subscribe[0].User)
|
||||||
|
require.Nil(t, conf.Subscribe[0].Password)
|
||||||
|
require.Equal(t, "", conf.Subscribe[0].Token)
|
||||||
|
}
|
||||||
|
|
|
@ -154,25 +154,28 @@ func execPublish(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
if token != "" {
|
if token != "" {
|
||||||
options = append(options, client.WithBearerAuth(token))
|
options = append(options, client.WithBearerAuth(token))
|
||||||
}
|
} else {
|
||||||
if user != "" {
|
if user != "" {
|
||||||
var pass string
|
var pass string
|
||||||
parts := strings.SplitN(user, ":", 2)
|
parts := strings.SplitN(user, ":", 2)
|
||||||
if len(parts) == 2 {
|
if len(parts) == 2 {
|
||||||
user = parts[0]
|
user = parts[0]
|
||||||
pass = parts[1]
|
pass = parts[1]
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
|
fmt.Fprint(c.App.ErrWriter, "Enter Password: ")
|
||||||
p, err := util.ReadPassword(c.App.Reader)
|
p, err := util.ReadPassword(c.App.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
pass = string(p)
|
||||||
|
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
||||||
}
|
}
|
||||||
pass = string(p)
|
options = append(options, client.WithBasicAuth(user, pass))
|
||||||
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
|
} else if conf.DefaultToken != "" {
|
||||||
|
options = append(options, client.WithBearerAuth(conf.DefaultToken))
|
||||||
|
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||||
|
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
||||||
}
|
}
|
||||||
options = append(options, client.WithBasicAuth(user, pass))
|
|
||||||
} else if token == "" && conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
|
||||||
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
|
|
||||||
}
|
}
|
||||||
if pid > 0 {
|
if pid > 0 {
|
||||||
newMessage, err := waitForProcess(pid)
|
newMessage, err := waitForProcess(pid)
|
||||||
|
|
|
@ -5,8 +5,11 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/test"
|
"heckel.io/ntfy/test"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -130,7 +133,7 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||||
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
|
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
|
||||||
|
|
||||||
// Tests with NTFY_TOPIC set ////
|
// Tests with NTFY_TOPIC set ////
|
||||||
require.Nil(t, os.Setenv("NTFY_TOPIC", topic))
|
t.Setenv("NTFY_TOPIC", topic)
|
||||||
|
|
||||||
// Test: Successful command with NTFY_TOPIC
|
// Test: Successful command with NTFY_TOPIC
|
||||||
app, _, stdout, _ = newTestApp()
|
app, _, stdout, _ = newTestApp()
|
||||||
|
@ -147,3 +150,151 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||||
m = toMessage(t, stdout.String())
|
m = toMessage(t, stdout.String())
|
||||||
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
|
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Default_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: philipp
|
||||||
|
default-password: mypass
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "triggered", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Default_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "triggered", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: philipp
|
||||||
|
default-password: mypass
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "triggered", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "triggered", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "triggered", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: philipp
|
||||||
|
default-password: fakepass
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "triggered", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
|
||||||
|
app, _, _, _ := newTestApp()
|
||||||
|
err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, "cannot set both --user and --token", err.Error())
|
||||||
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ var flagsSubscribe = append(
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
|
||||||
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
|
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
|
||||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||||
|
&cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"},
|
||||||
&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
|
&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
|
||||||
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
|
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
|
||||||
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
|
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
|
||||||
|
@ -97,11 +98,18 @@ func execSubscribe(c *cli.Context) error {
|
||||||
cl := client.New(conf)
|
cl := client.New(conf)
|
||||||
since := c.String("since")
|
since := c.String("since")
|
||||||
user := c.String("user")
|
user := c.String("user")
|
||||||
|
token := c.String("token")
|
||||||
poll := c.Bool("poll")
|
poll := c.Bool("poll")
|
||||||
scheduled := c.Bool("scheduled")
|
scheduled := c.Bool("scheduled")
|
||||||
fromConfig := c.Bool("from-config")
|
fromConfig := c.Bool("from-config")
|
||||||
topic := c.Args().Get(0)
|
topic := c.Args().Get(0)
|
||||||
command := c.Args().Get(1)
|
command := c.Args().Get(1)
|
||||||
|
|
||||||
|
// Checks
|
||||||
|
if user != "" && token != "" {
|
||||||
|
return errors.New("cannot set both --user and --token")
|
||||||
|
}
|
||||||
|
|
||||||
if !fromConfig {
|
if !fromConfig {
|
||||||
conf.Subscribe = nil // wipe if --from-config not passed
|
conf.Subscribe = nil // wipe if --from-config not passed
|
||||||
}
|
}
|
||||||
|
@ -109,6 +117,9 @@ func execSubscribe(c *cli.Context) error {
|
||||||
if since != "" {
|
if since != "" {
|
||||||
options = append(options, client.WithSince(since))
|
options = append(options, client.WithSince(since))
|
||||||
}
|
}
|
||||||
|
if token != "" {
|
||||||
|
options = append(options, client.WithBearerAuth(token))
|
||||||
|
}
|
||||||
if user != "" {
|
if user != "" {
|
||||||
var pass string
|
var pass string
|
||||||
parts := strings.SplitN(user, ":", 2)
|
parts := strings.SplitN(user, ":", 2)
|
||||||
|
@ -145,6 +156,9 @@ func execSubscribe(c *cli.Context) error {
|
||||||
|
|
||||||
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
|
||||||
for _, s := range conf.Subscribe { // may be nil
|
for _, s := range conf.Subscribe { // may be nil
|
||||||
|
if auth := maybeAddAuthHeader(s, conf); auth != nil {
|
||||||
|
options = append(options, auth)
|
||||||
|
}
|
||||||
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
|
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -175,21 +189,11 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||||
for filter, value := range s.If {
|
for filter, value := range s.If {
|
||||||
topicOptions = append(topicOptions, client.WithFilter(filter, value))
|
topicOptions = append(topicOptions, client.WithFilter(filter, value))
|
||||||
}
|
}
|
||||||
var user string
|
|
||||||
var password *string
|
if auth := maybeAddAuthHeader(s, conf); auth != nil {
|
||||||
if s.User != "" {
|
topicOptions = append(topicOptions, auth)
|
||||||
user = s.User
|
|
||||||
} else if conf.DefaultUser != "" {
|
|
||||||
user = conf.DefaultUser
|
|
||||||
}
|
|
||||||
if s.Password != nil {
|
|
||||||
password = s.Password
|
|
||||||
} else if conf.DefaultPassword != nil {
|
|
||||||
password = conf.DefaultPassword
|
|
||||||
}
|
|
||||||
if user != "" && password != nil {
|
|
||||||
topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
|
||||||
if s.Command != "" {
|
if s.Command != "" {
|
||||||
cmds[subscriptionID] = s.Command
|
cmds[subscriptionID] = s.Command
|
||||||
|
@ -214,6 +218,25 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
|
||||||
|
// check for subscription token then subscription user:pass
|
||||||
|
if s.Token != "" {
|
||||||
|
return client.WithBearerAuth(s.Token)
|
||||||
|
}
|
||||||
|
if s.User != "" && s.Password != nil {
|
||||||
|
return client.WithBasicAuth(s.User, *s.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no subscription token nor subscription user:pass, check for default token then default user:pass
|
||||||
|
if conf.DefaultToken != "" {
|
||||||
|
return client.WithBearerAuth(conf.DefaultToken)
|
||||||
|
}
|
||||||
|
if conf.DefaultUser != "" && conf.DefaultPassword != nil {
|
||||||
|
return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
|
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
|
||||||
if command != "" {
|
if command != "" {
|
||||||
runCommand(c, command, m)
|
runCommand(c, command, m)
|
||||||
|
|
312
cmd/subscribe_test.go
Normal file
312
cmd/subscribe_test.go
Normal file
|
@ -0,0 +1,312 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_UserPass_Subscription_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: philipp
|
||||||
|
default-password: mypass
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Token_Subscription_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
user: philipp
|
||||||
|
password: mypass
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Token_Subscription_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_UserPass_Subscription_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: fake
|
||||||
|
default-password: password
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
user: philipp
|
||||||
|
password: mypass
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Token_Subscription_Empty(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-user: philipp
|
||||||
|
default-password: mypass
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Empty_Subscription_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Empty_Subscription_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
user: philipp
|
||||||
|
password: mypass
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Token_CLI_Token(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_FAKETOKEN0123456789FAKETOKEN
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic"}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Token_CLI_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass", "mytopic"}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass(t *testing.T) {
|
||||||
|
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
require.Equal(t, "/mytopic/json", r.URL.Path)
|
||||||
|
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
filename := filepath.Join(t.TempDir(), "client.yml")
|
||||||
|
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
|
||||||
|
default-host: %s
|
||||||
|
default-token: tk_FAKETOKEN01234567890FAKETOKEN
|
||||||
|
subscribe:
|
||||||
|
- topic: mytopic
|
||||||
|
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
|
||||||
|
`, server.URL)), 0600))
|
||||||
|
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass"}))
|
||||||
|
|
||||||
|
require.Equal(t, message, strings.TrimSpace(stdout.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
|
||||||
|
app, _, _, _ := newTestApp()
|
||||||
|
err := app.Run([]string{"ntfy", "subscribe", "--poll", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, "cannot set both --user and --token", err.Error())
|
||||||
|
}
|
|
@ -2,6 +2,27 @@
|
||||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||||
|
|
||||||
|
## ntfy server v2.1.3 (UNRELEASED)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* You can now use tokens in `client.yml` for publishing and subscribing ([#653](https://github.com/binwiederhier/ntfy/issues/653), thanks to [@wunter8](https://github.com/wunter8))
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* `ntfy sub --poll --from-config` will now include authentication headers from client.yml (if applicable) ([#658](https://github.com/binwiederhier/ntfy/issues/658), thanks to [@wunter8](https://github.com/wunter8))
|
||||||
|
|
||||||
|
## ntfy Android app v1.16.1 (UNRELEASED)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing))
|
||||||
|
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
|
||||||
|
|
||||||
|
**Bug fixes + maintenance:**
|
||||||
|
|
||||||
|
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing))
|
||||||
|
|
||||||
## ntfy server v2.1.2
|
## ntfy server v2.1.2
|
||||||
Released March 4, 2023
|
Released March 4, 2023
|
||||||
|
|
||||||
|
|
|
@ -254,13 +254,13 @@ I hope this shows how powerful this command is. Here's a short video that demons
|
||||||
<figcaption>Execute all the things</figcaption>
|
<figcaption>Execute all the things</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
If most (or all) of your subscription usernames, passwords, and commands are the same, you can specify a `default-user`, `default-password`, and `default-command` at the top of the
|
If most (or all) of your subscriptions use the same credentials, you can set defaults in `client.yml`. Use `default-user` and `default-password` or `default-token` (but not both).
|
||||||
`client.yml`. If a subscription does not specify a username/password to use or does not have a command, the defaults will be used, otherwise, the subscription settings will
|
You can also specify a `default-command` that will run when a message is received. If a subscription does not include credentials to use or does not have a command, the defaults
|
||||||
override the defaults.
|
will be used, otherwise, the subscription settings will override the defaults.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
Because the `default-user` and `default-password` will be sent for each topic that does not have its own username/password (even if the topic does not require authentication),
|
Because the `default-user`, `default-password`, and `default-token` will be sent for each topic that does not have its own username/password (even if the topic does not
|
||||||
be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
require authentication), be sure that the servers/topics you subscribe to use HTTPS to prevent leaking the username and password.
|
||||||
|
|
||||||
### Using the systemd service
|
### Using the systemd service
|
||||||
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
You can use the `ntfy-client` systemd service (see [ntfy-client.service](https://github.com/binwiederhier/ntfy/blob/main/client/ntfy-client.service))
|
||||||
|
|
|
@ -1622,6 +1622,7 @@ func (s *Server) autorizeTopic(next handleFunc, perm user.Permission) handleFunc
|
||||||
// maybeAuthenticate reads the "Authorization" header and will try to authenticate the user
|
// maybeAuthenticate reads the "Authorization" header and will try to authenticate the user
|
||||||
// if it is set.
|
// if it is set.
|
||||||
//
|
//
|
||||||
|
// - If auth-db is not configured, immediately return an IP-based visitor
|
||||||
// - If the header is not set or not supported (anything non-Basic and non-Bearer),
|
// - If the header is not set or not supported (anything non-Basic and non-Bearer),
|
||||||
// an IP-based visitor is returned
|
// an IP-based visitor is returned
|
||||||
// - If the header is set, authenticate will be called to check the username/password (Basic auth),
|
// - If the header is set, authenticate will be called to check the username/password (Basic auth),
|
||||||
|
@ -1633,13 +1634,14 @@ func (s *Server) maybeAuthenticate(r *http.Request) (*visitor, error) {
|
||||||
// Read "Authorization" header value, and exit out early if it's not set
|
// Read "Authorization" header value, and exit out early if it's not set
|
||||||
ip := extractIPAddress(r, s.config.BehindProxy)
|
ip := extractIPAddress(r, s.config.BehindProxy)
|
||||||
vip := s.visitor(ip, nil)
|
vip := s.visitor(ip, nil)
|
||||||
|
if s.userManager == nil {
|
||||||
|
return vip, nil
|
||||||
|
}
|
||||||
header, err := readAuthHeader(r)
|
header, err := readAuthHeader(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return vip, err
|
return vip, err
|
||||||
} else if !supportedAuthHeader(header) {
|
} else if !supportedAuthHeader(header) {
|
||||||
return vip, nil
|
return vip, nil
|
||||||
} else if s.userManager == nil {
|
|
||||||
return vip, errHTTPUnauthorized
|
|
||||||
}
|
}
|
||||||
// If we're trying to auth, check the rate limiter first
|
// If we're trying to auth, check the rate limiter first
|
||||||
if !vip.AuthAllowed() {
|
if !vip.AuthAllowed() {
|
||||||
|
|
|
@ -133,29 +133,6 @@ func TestManager_AddUser_And_Query(t *testing.T) {
|
||||||
require.Equal(t, u.ID, u3.ID)
|
require.Equal(t, u.ID, u3.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_Authenticate_Timing(t *testing.T) {
|
|
||||||
a := newTestManagerFromFile(t, filepath.Join(t.TempDir(), "user.db"), "", PermissionDenyAll, DefaultUserPasswordBcryptCost, DefaultUserStatsQueueWriterInterval)
|
|
||||||
require.Nil(t, a.AddUser("user", "pass", RoleAdmin))
|
|
||||||
|
|
||||||
// Timing a correct attempt
|
|
||||||
start := time.Now().UnixMilli()
|
|
||||||
_, err := a.Authenticate("user", "pass")
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
|
||||||
|
|
||||||
// Timing an incorrect attempt
|
|
||||||
start = time.Now().UnixMilli()
|
|
||||||
_, err = a.Authenticate("user", "INCORRECT")
|
|
||||||
require.Equal(t, ErrUnauthenticated, err)
|
|
||||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
|
||||||
|
|
||||||
// Timing a non-existing user attempt
|
|
||||||
start = time.Now().UnixMilli()
|
|
||||||
_, err = a.Authenticate("DOES-NOT-EXIST", "hithere")
|
|
||||||
require.Equal(t, ErrUnauthenticated, err)
|
|
||||||
require.GreaterOrEqual(t, time.Now().UnixMilli()-start, minBcryptTimingMillis)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
|
func TestManager_MarkUserRemoved_RemoveDeletedUsers(t *testing.T) {
|
||||||
a := newTestManager(t, PermissionDenyAll)
|
a := newTestManager(t, PermissionDenyAll)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue