diff --git a/client/client.yml b/client/client.yml index 9f62990a..56733a14 100644 --- a/client/client.yml +++ b/client/client.yml @@ -16,6 +16,10 @@ # command: 'echo "$message"' # if: # priority: high,urgent +# - topic: secret +# command: 'notify-send "$m"' +# user: phill +# password: mypass # # Variables: # Variable Aliases Description diff --git a/client/config.go b/client/config.go index c44fac6c..0866cd1b 100644 --- a/client/config.go +++ b/client/config.go @@ -14,9 +14,11 @@ const ( type Config struct { DefaultHost string `yaml:"default-host"` Subscribe []struct { - Topic string `yaml:"topic"` - Command string `yaml:"command"` - If map[string]string `yaml:"if"` + Topic string `yaml:"topic"` + User string `yaml:"user"` + Password string `yaml:"password"` + Command string `yaml:"command"` + If map[string]string `yaml:"if"` } `yaml:"subscribe"` } diff --git a/client/config_test.go b/client/config_test.go index 8d322111..d601cdb4 100644 --- a/client/config_test.go +++ b/client/config_test.go @@ -13,7 +13,9 @@ func TestConfig_Load(t *testing.T) { require.Nil(t, os.WriteFile(filename, []byte(` default-host: http://localhost subscribe: - - topic: no-command + - topic: no-command-with-auth + user: phil + password: mypass - topic: echo-this command: 'echo "Message received: $message"' - topic: alerts @@ -26,8 +28,10 @@ subscribe: require.Nil(t, err) require.Equal(t, "http://localhost", conf.DefaultHost) require.Equal(t, 3, len(conf.Subscribe)) - require.Equal(t, "no-command", conf.Subscribe[0].Topic) + require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic) require.Equal(t, "", conf.Subscribe[0].Command) + require.Equal(t, "phil", conf.Subscribe[0].User) + require.Equal(t, "mypass", conf.Subscribe[0].Password) require.Equal(t, "echo-this", conf.Subscribe[1].Topic) require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command) require.Equal(t, "alerts", conf.Subscribe[2].Topic) diff --git a/cmd/subscribe.go b/cmd/subscribe.go index b5a56933..9000a163 100644 --- a/cmd/subscribe.go +++ b/cmd/subscribe.go @@ -23,6 +23,7 @@ var cmdSubscribe = &cli.Command{ Flags: []cli.Flag{ &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: "user", Aliases: []string{"u"}, Usage: "username[:password] used to auth against the server"}, &cli.BoolFlag{Name: "from-config", Aliases: []string{"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: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"}, @@ -40,6 +41,7 @@ ntfy subscribe TOPIC ntfy subscribe mytopic # Prints JSON for incoming messages for ntfy.sh/mytopic ntfy sub home.lan/backups # Subscribe to topic on different server ntfy sub --poll home.lan/backups # Just query for latest messages and exit + ntfy sub -u phil:mypass secret # Subscribe with username/password ntfy subscribe TOPIC COMMAND This executes COMMAND for every incoming messages. The message fields are passed to the @@ -81,6 +83,7 @@ func execSubscribe(c *cli.Context) error { } cl := client.New(conf) since := c.String("since") + user := c.String("user") poll := c.Bool("poll") scheduled := c.Bool("scheduled") fromConfig := c.Bool("from-config") @@ -93,6 +96,23 @@ func execSubscribe(c *cli.Context) error { if since != "" { options = append(options, client.WithSince(since)) } + if user != "" { + var pass string + parts := strings.SplitN(user, ":", 2) + if len(parts) == 2 { + user = parts[0] + pass = parts[1] + } else { + fmt.Fprint(c.App.ErrWriter, "Enter Password: ") + p, err := util.ReadPassword(c.App.Reader) + if err != nil { + return err + } + pass = string(p) + fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20)) + } + options = append(options, client.WithBasicAuth(user, pass)) + } if poll { options = append(options, client.WithPoll()) } @@ -142,6 +162,9 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic, for filter, value := range s.If { topicOptions = append(topicOptions, client.WithFilter(filter, value)) } + if s.User != "" && s.Password != "" { + topicOptions = append(topicOptions, client.WithBasicAuth(s.User, s.Password)) + } subscriptionID := cl.Subscribe(s.Topic, topicOptions...) commands[subscriptionID] = s.Command } diff --git a/docs/subscribe/cli.md b/docs/subscribe/cli.md index 2d3f83b4..52e005c0 100644 --- a/docs/subscribe/cli.md +++ b/docs/subscribe/cli.md @@ -196,3 +196,27 @@ EOF sudo systemctl daemon-reload sudo systemctl restart ntfy-client ``` + + +### Authentication +Depending on whether the server is configured to support [access control](../config.md#access-control), some topics +may be read/write protected so that only users with the correct credentials can subscribe or publish to them. +To publish/subscribe to protected topics, you can use [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) +with a valid username/password. For your self-hosted server, **be sure to use HTTPS to avoid eavesdropping** and exposing +your password. + +You can either add your username and password to the configuration file: +=== "~/.config/ntfy/client.yml" + ```yaml + - topic: secret + command: 'notify-send "$m"' + user: phill + password: mypass + ``` + +Or with the `ntfy subscibe` command: +``` +ntfy subscribe \ + -u phil:mypass \ + ntfy.example.com/mysecrets +```