More docs docs docs

pull/114/head
Philipp Heckel 2022-02-01 23:39:57 -05:00
parent c3a2331b59
commit 1552d8103e
7 changed files with 211 additions and 121 deletions

View File

@ -5,6 +5,7 @@ import (
"bufio" "bufio"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
"io" "io"
@ -105,13 +106,13 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response %d from server", resp.StatusCode)
}
b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes)) b, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBytes))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if resp.StatusCode != http.StatusOK {
return nil, errors.New(strings.TrimSpace(string(b)))
}
m, err := toMessage(string(b), topicURL, "") m, err := toMessage(string(b), topicURL, "")
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -2,6 +2,7 @@ package client
import ( import (
"fmt" "fmt"
"heckel.io/ntfy/util"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -70,6 +71,11 @@ func WithEmail(email string) PublishOption {
return WithHeader("X-Email", email) return WithHeader("X-Email", email)
} }
// WithBasicAuth adds the Authorization header for basic auth to the request
func WithBasicAuth(user, pass string) PublishOption {
return WithHeader("Authorization", util.BasicAuth(user, pass))
}
// WithNoCache instructs the server not to cache the message server-side // WithNoCache instructs the server not to cache the message server-side
func WithNoCache() PublishOption { func WithNoCache() PublishOption {
return WithHeader("X-Cache", "no") return WithHeader("X-Cache", "no")

View File

@ -8,12 +8,6 @@ import (
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
) )
/*
*/
const ( const (
userEveryone = "everyone" userEveryone = "everyone"
) )
@ -46,7 +40,8 @@ Usage:
ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC ntfy access USERNAME TOPIC PERMISSION # Allow/deny access for USERNAME to TOPIC
Arguments: Arguments:
USERNAME an existing user, as created with 'ntfy user add' USERNAME an existing user, as created with 'ntfy user add', or "everyone"/"*"
to define access rules for anonymous/unauthenticated clients
TOPIC name of a topic with optional wildcards, e.g. "mytopic*" TOPIC name of a topic with optional wildcards, e.g. "mytopic*"
PERMISSION one of the following: PERMISSION one of the following:
- read-write (alias: rw) - read-write (alias: rw)

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"heckel.io/ntfy/client" "heckel.io/ntfy/client"
"heckel.io/ntfy/util"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -15,23 +16,25 @@ var cmdPublish = &cli.Command{
Name: "publish", Name: "publish",
Aliases: []string{"pub", "send", "trigger"}, Aliases: []string{"pub", "send", "trigger"},
Usage: "Send message via a ntfy server", Usage: "Send message via a ntfy server",
UsageText: "ntfy send [OPTIONS..] TOPIC [MESSAGE]", UsageText: "ntfy send [OPTIONS..] TOPIC [MESSAGE]\n NTFY_TOPIC=.. ntfy send [OPTIONS..] -P [MESSAGE]",
Action: execPublish, Action: execPublish,
Category: categoryClient, Category: categoryClient,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"}, &cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"},
&cli.StringFlag{Name: "title", Aliases: []string{"t"}, Usage: "message title"}, &cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"},
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"}, &cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, Usage: "comma separated list of tags and emojis"}, &cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, Usage: "delay/schedule message"}, &cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
&cli.StringFlag{Name: "click", Aliases: []string{"U"}, Usage: "URL to open when notification is clicked"}, &cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, Usage: "URL to send as an external attachment"}, &cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, Usage: "Filename for the attachment"}, &cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "Filename for the attachment"},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "File to upload as an attachment"}, &cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "File to upload as an attachment"},
&cli.StringFlag{Name: "email", Aliases: []string{"e-mail", "mail", "e"}, Usage: "also send to e-mail address"}, &cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, Usage: "do not cache message server-side"}, &cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, Usage: "do not forward message to Firebase"}, &cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Usage: "do print message"}, &cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do print message"},
}, },
Description: `Publish a message to a ntfy server. Description: `Publish a message to a ntfy server.
@ -46,6 +49,9 @@ Examples:
ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked ntfy pub --click="https://reddit.com" redd 'New msg' # Opens Reddit when notification is clicked
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
ntfy pub -u phil:mypass secret Psst # Publish with username/password
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
NTFY_TOPIC=mytopic ntfy pub -P "some message"" # Use NTFY_TOPIC variable as topic
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
ntfy trigger mywebhook # Sending without message, useful for webhooks ntfy trigger mywebhook # Sending without message, useful for webhooks
@ -57,9 +63,6 @@ or ~/.config/ntfy/client.yml for all other users.`,
} }
func execPublish(c *cli.Context) error { func execPublish(c *cli.Context) error {
if c.NArg() < 1 {
return errors.New("must specify topic, type 'ntfy publish --help' for help")
}
conf, err := loadConfig(c) conf, err := loadConfig(c)
if err != nil { if err != nil {
return err return err
@ -73,14 +76,26 @@ func execPublish(c *cli.Context) error {
filename := c.String("filename") filename := c.String("filename")
file := c.String("file") file := c.String("file")
email := c.String("email") email := c.String("email")
user := c.String("user")
noCache := c.Bool("no-cache") noCache := c.Bool("no-cache")
noFirebase := c.Bool("no-firebase") noFirebase := c.Bool("no-firebase")
envTopic := c.Bool("env-topic")
quiet := c.Bool("quiet") quiet := c.Bool("quiet")
topic := c.Args().Get(0) var topic, message string
message := "" if envTopic {
topic = os.Getenv("NTFY_TOPIC")
if c.NArg() > 0 {
message = strings.Join(c.Args().Slice(), " ")
}
} else {
if c.NArg() < 1 {
return errors.New("must specify topic, type 'ntfy publish --help' for help")
}
topic = c.Args().Get(0)
if c.NArg() > 1 { if c.NArg() > 1 {
message = strings.Join(c.Args().Slice()[1:], " ") message = strings.Join(c.Args().Slice()[1:], " ")
} }
}
var options []client.PublishOption var options []client.PublishOption
if title != "" { if title != "" {
options = append(options, client.WithTitle(title)) options = append(options, client.WithTitle(title))
@ -112,6 +127,23 @@ func execPublish(c *cli.Context) error {
if noFirebase { if noFirebase {
options = append(options, client.WithNoFirebase()) options = append(options, client.WithNoFirebase())
} }
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))
}
var body io.Reader var body io.Reader
if file == "" { if file == "" {
body = strings.NewReader(message) body = strings.NewReader(message)

View File

@ -11,29 +11,11 @@ import (
"strings" "strings"
) )
/*
---
dabbling for CLI
ntfy user allow phil mytopic
ntfy user allow phil mytopic --read-only
ntfy user deny phil mytopic
ntfy user list
phil (admin)
- read-write access to everything
ben (user)
- read-write access to a topic alerts
- read access to
everyone (no user)
- read-only access to topic announcements
*/
var flagsUser = userCommandFlags() var flagsUser = userCommandFlags()
var cmdUser = &cli.Command{ var cmdUser = &cli.Command{
Name: "user", Name: "user",
Usage: "Manage users and access to topics", Usage: "Manage/show users",
UsageText: "ntfy user [add|del|...] ...", UsageText: "ntfy user [list|add|remove|change-pass|change-role] ...",
Flags: flagsUser, Flags: flagsUser,
Before: initConfigFileInputSource("config", flagsUser), Before: initConfigFileInputSource("config", flagsUser),
Category: categoryServer, Category: categoryServer,
@ -41,37 +23,96 @@ var cmdUser = &cli.Command{
{ {
Name: "add", Name: "add",
Aliases: []string{"a"}, Aliases: []string{"a"},
Usage: "add user to auth database", Usage: "add user",
UsageText: "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"},
}, },
Description: `Add a new user to the ntfy user database.
A user can be either a regular user, or an admin. A regular user has no read or write access (unless
granted otherwise by the auth-default-access setting). An admin user has read and write access to all
topics.
Examples:
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
`,
}, },
{ {
Name: "remove", Name: "remove",
Aliases: []string{"del", "rm"}, Aliases: []string{"del", "rm"},
Usage: "remove user from auth database", Usage: "remove user",
UsageText: "ntfy user remove USERNAME",
Action: execUserDel, Action: execUserDel,
Description: `Remove a user from the ntfy user database.
Example:
ntfy user del phil
`,
}, },
{ {
Name: "change-pass", Name: "change-pass",
Aliases: []string{"chp"}, Aliases: []string{"chp"},
Usage: "change user password", Usage: "change user password",
UsageText: "ntfy user change-pass USERNAME",
Action: execUserChangePass, Action: execUserChangePass,
Description: `Change the password for the given user.
The new password will be read from STDIN, and it'll be confirmed by typing
it twice.
Example:
ntfy user change-pass phil
`,
}, },
{ {
Name: "change-role", Name: "change-role",
Aliases: []string{"chr"}, Aliases: []string{"chr"},
Usage: "change user role", Usage: "change user role",
UsageText: "ntfy user change-role USERNAME ROLE",
Action: execUserChangeRole, Action: execUserChangeRole,
Description: `Change the role for the given user to admin or user.
This command can be used to change the role of a user either from a regular user
to an admin user, or the other way around:
- admin: an admin has read/write access to all topics
- user: a regular user only has access to what was explicitly granted via 'ntfy access'
When changing the role of a user to "admin", all access control entries for that
user are removed, since they are no longer necessary.
Example:
ntfy user change-role phil admin # Make user phil an admin
ntfy user change-role phil user # Remove admin role from user phil
`,
}, },
{ {
Name: "list", Name: "list",
Aliases: []string{"chr"}, Aliases: []string{"l"},
Usage: "change user role", Usage: "list users",
Action: execUserList, Action: execUserList,
}, },
}, },
Description: `Manage users of the ntfy server.
This is a server-only command. It directly manages the user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined. Please also refer
to the related command 'ntfy access'.
The command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles.
Examples:
ntfy user list # Shows list of users
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
ntfy user change-role phil admin # Make user phil an admin
`,
} }
func execUserAdd(c *cli.Context) error { func execUserAdd(c *cli.Context) error {
@ -79,6 +120,8 @@ func execUserAdd(c *cli.Context) error {
role := auth.Role(c.String("role")) role := auth.Role(c.String("role"))
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 {
return errors.New("username not allowed")
} else if !auth.AllowedRole(role) { } else if !auth.AllowedRole(role) {
return errors.New("role must be either 'user' or 'admin'") return errors.New("role must be either 'user' or 'admin'")
} }
@ -101,6 +144,8 @@ func execUserDel(c *cli.Context) error {
username := c.Args().Get(0) username := c.Args().Get(0)
if username == "" { if username == "" {
return errors.New("username expected, type 'ntfy user del --help' for help") return errors.New("username expected, type 'ntfy user del --help' for help")
} else if username == userEveryone {
return errors.New("username not allowed")
} }
manager, err := createAuthManager(c) manager, err := createAuthManager(c)
if err != nil { if err != nil {
@ -117,6 +162,8 @@ func execUserChangePass(c *cli.Context) error {
username := c.Args().Get(0) username := c.Args().Get(0)
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 {
return errors.New("username not allowed")
} }
password, err := readPassword(c) password, err := readPassword(c)
if err != nil { if err != nil {
@ -138,6 +185,8 @@ func execUserChangeRole(c *cli.Context) error {
role := auth.Role(c.Args().Get(1)) role := auth.Role(c.Args().Get(1))
if username == "" || !auth.AllowedRole(role) { if username == "" || !auth.AllowedRole(role) {
return errors.New("username and new role expected, type 'ntfy user change-role --help' for help") return errors.New("username and new role expected, type 'ntfy user change-role --help' for help")
} else if username == userEveryone {
return errors.New("username not allowed")
} }
manager, err := createAuthManager(c) manager, err := createAuthManager(c)
if err != nil { if err != nil {
@ -169,11 +218,11 @@ func createAuthManager(c *cli.Context) (auth.Manager, error) {
return nil, errors.New("option auth-file not set; auth is unconfigured for this server") return nil, errors.New("option auth-file not set; auth is unconfigured for this server")
} else if !util.FileExists(authFile) { } else if !util.FileExists(authFile) {
return nil, errors.New("auth-file does not exist; please start the server at least once to create it") return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
} else if !util.InStringList([]string{"read-write", "read-only", "deny-all"}, authDefaultAccess) { } else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'") return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only' or 'deny-all'")
} }
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only" authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
authDefaultWrite := authDefaultAccess == "read-write" authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
return auth.NewSQLiteAuth(authFile, authDefaultRead, authDefaultWrite) return auth.NewSQLiteAuth(authFile, authDefaultRead, authDefaultWrite)
} }

View File

@ -122,7 +122,7 @@ Please also refer to the [rate limiting](#rate-limiting) settings below, specifi
and `visitor-attachment-daily-bandwidth-limit`. Setting these conservatively is necessary to avoid abuse. and `visitor-attachment-daily-bandwidth-limit`. Setting these conservatively is necessary to avoid abuse.
## Access control ## Access control
By default, the ntfy server is open for everyone, meaning everyone can read and write to any topic. To restrict access By default, the ntfy server is open for everyone, meaning **everyone can read and write to any topic**. To restrict access
to your own server, you can optionally configure authentication and authorization. to your own server, you can optionally configure authentication and authorization.
ntfy's auth is implemented with a simple SQLite-based backend. It implements two roles (`user` and `admin`) and per-topic ntfy's auth is implemented with a simple SQLite-based backend. It implements two roles (`user` and `admin`) and per-topic
@ -135,10 +135,13 @@ To set up auth, simply configure the following two options:
* `auth-default-access` defines the default/fallback access if no access control entry is found; it can be * `auth-default-access` defines the default/fallback access if no access control entry is found; it can be
set to `read-write` (default), `read-only`, `write-only` or `deny-all`. set to `read-write` (default), `read-only`, `write-only` or `deny-all`.
Once configured, you can use the `ntfy user` command to add/modify/delete users (with either a `user` or an `admin` role). ### Managing users + access
To control granular access to specific topics, you can use the `ntfy access` command to modify the access control list. Once configured, you can use the `ntfy user` command to add/modify/delete users, and the `ntfy access` command
to modify the access control list to allow/deny access to specific topic or topic patterns.
### Example: private instance XXXXXXXXXXXXXXXXXXXx
### Example: Private instance
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`: The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`:
``` yaml ``` yaml
@ -156,72 +159,70 @@ User phil added with role admin
``` ```
Once you've done that, you can publish and subscribe using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication) Once you've done that, you can publish and subscribe using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
with the given username/password. Here's a simple example: with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password. Here's a simple example:
=== "Command line (curl)" === "Command line (curl)"
``` ```
curl \ curl \
-u phil:mypass \ -u phil:mypass \
-d "Look ma, with auth" \ -d "Look ma, with auth" \
ntfy.example.com/secrets https://ntfy.example.com/mysecrets
``` ```
=== "ntfy CLI" === "ntfy CLI"
``` ```
ntfy publish ntfy.example.com/mytopic "Look ma, with auth" ntfy publish \
-u phil:mypass \
XXXXXXXXXXX ntfy.example.com/mysecrets \
XXXXXXXXXXX "Look ma, with auth"
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
XXXXXXXXXXX
``` ```
=== "HTTP" === "HTTP"
``` http ``` http
POST /mytopic HTTP/1.1 POST /mysecrets HTTP/1.1
Host: ntfy.sh Host: ntfy.example.com
Authorization: Basic cGhpbDpteXBhc3M= Authorization: Basic cGhpbDpteXBhc3M=
Backup successful 😀 Look ma, with auth
``` ```
=== "JavaScript" === "JavaScript"
``` javascript ``` javascript
fetch('https://ntfy.sh/mytopic', { fetch('https://ntfy.example.com/mysecrets', {
method: 'POST', // PUT works too method: 'POST', // PUT works too
body: 'Backup successful 😀' body: 'Look ma, with auth',
headers: {
'Authorization': 'Basic cGhpbDpteXBhc3M='
}
}) })
``` ```
=== "Go" === "Go"
``` go ``` go
http.Post("https://ntfy.sh/mytopic", "text/plain", req, _ := http.NewRequest("POST", "https://ntfy.example.com/mysecrets",
strings.NewReader("Backup successful 😀")) strings.NewReader("Look ma, with auth"))
req.Header.Set("Authorization", "Basic cGhpbDpteXBhc3M=")
http.DefaultClient.Do(req)
``` ```
=== "Python" === "Python"
``` python ``` python
requests.post("https://ntfy.sh/mytopic", requests.post("https://ntfy.example.com/mysecrets",
data="Backup successful 😀".encode(encoding='utf-8')) data="Look ma, with auth",
headers={
"Authorization": "Basic cGhpbDpteXBhc3M="
})
``` ```
=== "PHP" === "PHP"
``` php-inline ``` php-inline
file_get_contents('https://ntfy.sh/mytopic', false, stream_context_create([ file_get_contents('https://ntfy.example.com/mysecrets', false, stream_context_create([
'http' => [ 'http' => [
'method' => 'POST', // PUT also works 'method' => 'POST', // PUT also works
'header' => 'Content-Type: text/plain', 'header' =>
'content' => 'Backup successful 😀' 'Content-Type: text/plain\r\n' .
'Authorization: Basic cGhpbDpteXBhc3M=',
'content' => 'Look ma, with auth'
] ]
])); ]));
``` ```

View File

@ -1,6 +1,7 @@
package util package util
import ( import (
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
@ -240,3 +241,8 @@ func ReadPassword(in io.Reader) ([]byte, error) {
return password, nil return password, nil
} }
// BasicAuth encodes the Authorization header value for basic auth
func BasicAuth(user, pass string) string {
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, pass))))
}