Commit Diff


commit - /dev/null
commit + e20d6baf639b035ce416f106ffd196070cd3e372
blob - /dev/null
blob + ebf0db795e2a08b39cc7d6fdca8e5d8d25f327c4 (mode 644)
--- /dev/null
+++ LICENSE
@@ -0,0 +1,13 @@
+Copyright (c) 2021 Oliver Lowe <o@olowe.co>
+
+Permission to use, copy, modify, and distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
blob - /dev/null
blob + 6ec9128c06dcf0348aa57840072b871e618e595d (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,3 @@
+This repository contains the Go pushover package,
+the pover command-line utility,
+and example Icinga2 configuration and scripts to send notifications from Icinga2 using pover.
blob - /dev/null
blob + a4f6a780df90013537749f443140089eabd54784 (mode 644)
--- /dev/null
+++ cmd/pover/README.md
@@ -0,0 +1,10 @@
+## Tests
+Tests are run by building and running pover
+
+First, build pover:
+
+        go build
+
+Create a valid credentials file, then run test.sh
+
+        ./test.sh
blob - /dev/null
blob + c5667b15545813d8a6b4478c35bb5649d4340a65 (mode 644)
--- /dev/null
+++ cmd/pover/config.go
@@ -0,0 +1,51 @@
+package main
+
+import (
+	"os"
+	"bufio"
+	"strings"
+	"fmt"
+)
+
+type config struct {
+	user string
+	token string
+}
+
+func configFromFile(name string) (config, error) {
+	f, err := os.Open(name)
+	if err != nil {
+		f.Close()
+		return config{}, err
+	}
+	defer f.Close()
+	sc := bufio.NewScanner(f)
+	i := 0
+	c := config{}
+	for sc.Scan() {
+		i++
+		s := sc.Text()
+		// skip comments
+		if strings.HasPrefix(s, "#") {
+			continue
+		}
+		slice := strings.Split(s, " ")
+		if len(slice) > 2 {
+			return config{}, fmt.Errorf("%s:%d: too many values", f.Name(), i)
+		}
+		switch slice[0] {
+		case "user":
+			c.user = slice[1]
+		case "token":
+			c.token = slice[1]
+		default:
+			return config{}, fmt.Errorf("%s:%d: unknown key %s", f.Name(), i, slice[0])
+		}
+	}
+	if c.user == "" {
+		return config{}, fmt.Errorf("no user")
+	} else if c.token == "" {
+		return config{}, fmt.Errorf("no token")
+	}
+	return c, nil
+}
blob - /dev/null
blob + 1770264cf6764b635d4331c0e1d77ffb8a894978 (mode 644)
--- /dev/null
+++ cmd/pover/icinga2.conf
@@ -0,0 +1,33 @@
+object NotificationCommand "pushover" {
+	command = [ ConfigDir + "/scripts/pushover-icinga2.sh" ]
+	arguments = {
+		"-f" = "$pushover_config$"
+	}
+
+	env = {
+		HOSTADDRESS = "$address$"
+ 		HOSTDISPLAYNAME = "$host.display_name$"
+		LONGDATETIME = "$icinga.long_date_time$"
+		NOTIFICATIONAUTHORNAME = "$notification.author$"
+		NOTIFICATIONCOMMENT = "$notification.comment$"
+		NOTIFICATIONTYPE = "$notification.type$"
+		SERVICEDESC = "$service.name$"
+		SERVICEDISPLAYNAME = "$service.display_name$"
+		SERVICEOUTPUT = "$service.output$"
+		SERVICESTATE = "$service.state$"
+	}
+}
+
+object User "otl" {
+	display_name = "Oliver Lowe"
+	groups = [ "icingaadmins" ]
+	email = "otl@example.com"
+	vars.pushover_config = "/path/to/credentials/file"
+}
+
+apply Notification "olly-notification" to Service {
+	users = [ "otl" ]
+	command = "pushover"
+	/* Notify for every Service except for rdiff-backup services. */
+	assign where !match("rdiff-backup*", service.name)
+}
blob - /dev/null
blob + ce39ebe5e8a53724ab810944b803b3a5f46100b3 (mode 644)
--- /dev/null
+++ cmd/pover/pover.1
@@ -0,0 +1,52 @@
+.TH POVER 1
+.SH NAME
+pover \- push a notification to Pushover
+.SH SYNOPSIS
+.B pover
+[
+.B -d
+]
+[
+.B -f
+.I file
+]
+.SH DESCRIPTION
+.I Pover
+pushes a notification to Pushover using text read from standard input as the message body.
+The
+.B -d
+flag enables debugging output.
+The
+.B -f
+flag sets credentials to be read from
+.IR file .
+.PP
+Credentials must be present in a credentials file.
+A credentials file is a newline-delimited text file.
+Lines beginning with "#" are treated as comments and ignored.
+The recognised keys in the credentials file are:
+.TP
+.B user
+Pushover account User key.
+.TP
+.B token
+API token.
+.SH EXAMPLES
+An example credentials file
+.EX
+	# for pushover application "shell"
+	user abcd12345
+	token zxcvbnm98765
+.EE
+.PP
+Send a message "Hello world",
+reading credentials from a non-default path
+.EX
+	echo "Hello world" | pover -f /tmp/creds
+.EE
+.SH FILES
+.B $HOME/.config/pover
+.TP
+default credentials file
+.SH SOURCE
+.B github.com/ollytom/pover
blob - /dev/null
blob + d040cd863fe646a8b7440a699a252e0ee516143b (mode 644)
--- /dev/null
+++ cmd/pover/pover.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"io"
+	"io/ioutil"
+	"path/filepath"
+
+	"git.sr.ht/~otl/pushover"
+)
+
+const usage string = "usage: pover [-d] [-f file]"
+
+var debug *bool
+var configflag *string
+
+func init() {
+	debug = flag.Bool("d", false, "debug")
+	configflag = flag.String("f", "", "path to configuration file")
+	flag.Parse()
+}
+
+func main() {
+	var configpath string
+	configpath = *configflag
+	if *configflag == "" {
+		s, err := os.UserConfigDir()
+		if err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		}
+		configpath = filepath.Join(s, "pover")
+	}
+	config, err := configFromFile(configpath)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "load configuration: %v\n", err)
+		os.Exit(1)
+	}
+
+	lr := io.LimitReader(os.Stdin, pushover.MaxMsgLength)
+	b, err := ioutil.ReadAll(lr)
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+	if len(b) == pushover.MaxMsgLength {
+		fmt.Fprintf(os.Stderr, "max message length (%d) reached\n", pushover.MaxMsgLength)
+	}
+	if *debug {
+		fmt.Fprint(os.Stderr, string(b))
+	}
+
+	msg := pushover.Message{
+		User: config.user,
+		Token: config.token,
+		Message: string(b),
+	}
+	if err := pushover.Push(msg); err != nil {
+		fmt.Fprintf(os.Stderr, "push message: %v\n", err)
+		os.Exit(1)
+	}
+}
blob - /dev/null
blob + 1b4a447771810022d7edab07c1354ae8b73534a9 (mode 644)
--- /dev/null
+++ cmd/pover/pover.mdoc
@@ -0,0 +1,75 @@
+.Dd $Mdocdate$
+.Dt pover 1
+.Os
+.Sh NAME
+.Nm pover
+.Nd send a notification to Pushover
+.Sh SYNOPSIS
+.Nm
+.Op Fl d
+.Op Fl f Ar file
+.Op Fl t Ar title
+.Sh DESCRIPTION
+.Nm
+sends a notification to Pushover using text read from standard input as the message body.
+.Pp
+The options are:
+.Bl -tag -width Ds
+.It Fl d
+Write debugging output to standard error.
+.It Fl f Ar file
+Sets configuration to be read from
+.Ar file .
+.It Fl t Ar title
+Sets the message title to
+.Ar title .
+By default there is no title.
+.El
+.Pp
+Credentials must be present in a configuration file.
+A configuration file is a newline-delimited text file.
+Lines beginning with
+.Dq #
+are treated as comments and ignored.
+Configuration is a series of key-value pairs separated by whitespace,
+one per line.
+The recognised keys in the credentials file are:
+.Bl -tag -width Ds
+.It user
+Pushover account user key.
+.It token
+API token.
+.El
+.Sh EXIT STATUS
+.Ex
+.Sh EXAMPLES
+An example configuration file:
+.Pp
+.Bd -literal -offset indent -compact
+# for pushover application "shell"
+user abcd12345
+token zxcvbnm98765
+.Ed
+.Pp
+Send the current date as a notification:
+.Pp
+.Dl date | pover
+.Pp
+Send a hello world notification, reading configuration from
+.Pa /etc/pover :
+.Pp
+.Dl echo 'hello world' | pover -f /etc/pover
+.Sh FILES
+The default configuration file location is as returned from Go's os.UserConfigDir().
+.Bl -tag -width Ds
+.It Pa $HOME/.config/pover
+On Unix.
+.It Pa $HOME/Library/Application\ Support/pover
+On Darwin.
+.It Pa %AppData%\\\pover
+On Windows.
+.It Pa $home/lib/pover
+On Plan 9.
+.El
+.Sh SEE ALSO
+.Lk "Pushover Message API documentation" https://pushover.net/api
blob - /dev/null
blob + 368000d1915882c1f0102f46d516958cede7dc9f (mode 644)
--- /dev/null
+++ cmd/pover/pushover-icinga2.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+while getopt 'f:' flag
+do
+	case $flag in
+	f) config=$OPTARG ;;
+	*) echo "unknown flag" $flag ;;
+	esac
+done
+
+pover -f $config <<EOF
+$NOTIFICATIONTYPE
+
+$SERVICEDISPLAYNAME - $SERVICEDESC - on $HOSTDISPLAYNAME ($HOSTADDRESS) is $SERVICESTATE
+Time: $LONGDATETIME
+Output: $SERVICEOUTPUT
+Comments: [$NOTIFICATIONAUTHORNAME] $NOTIFICATIONCOMMENT
+EOF
blob - /dev/null
blob + c28e45c3d2e5e452bf9cb8e644b8e79a79eaba94 (mode 755)
--- /dev/null
+++ cmd/pover/test.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+
+if date -n | cmd/pover/pover
+then
+	echo 'date sent ok'
+else
+	echo 'date failed'
+fi
+
+if /bin/dd if=/dev/urandom of=/dev/stdout count=1024 | cmd/pover/pover
+then
+	echo 'max length message ok'
+else
+	echo 'max length message failed'
+fi
+
+# we expect pover to print a warning to standard error but send a truncated message anyway.
+if /bin/dd if=/dev/urandom of=/dev/stdout count=2048 | cmd/pover/pover
+then
+	echo 'too long message ok'
+else
+	echo 'too long message failed'
+fi
+
+if ! echo | cmd/pover/pover
+then
+	echo 'blank message ok'
+else
+	echo 'blank message failed'
+fi
+
+if ! cmd/pover/pover -f /dev/null
+then
+	echo "empty config ok"
+else
+	echo "empty config failed"
+fi
+
+badconfig=`mktemp`
+echo 'badconfig' > $badconfig
+if ! cmd/pover/pover -f $badconfig
+then
+	echo "bad config ok"
+else
+	echo "bad config failed"
+fi
+rm $badconfig
blob - /dev/null
blob + 42431be192c97fc0ec146dd88f0e9d6dd6875add (mode 644)
--- /dev/null
+++ go.mod
@@ -0,0 +1,3 @@
+module git.sr.ht/~otl/pushover
+
+go 1.16
blob - /dev/null
blob + 41119f9322acb87af84f93940a73f20f78e98dc2 (mode 644)
--- /dev/null
+++ pushover.go
@@ -0,0 +1,72 @@
+package pushover
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+const apiurl = "https://api.pushover.net/1/messages.json"
+const MaxMsgLength = 1024
+const MaxTitleLength = 250
+
+// Message represents a message in the Pushover Message API.
+type Message struct {
+	User string
+	Token string
+	Title string
+	Message string
+	Priority int
+}
+
+type response struct {
+	Status int
+	Request string
+	Errors errors
+}
+
+type errors []string
+
+func (e errors) Error() string {
+	return strings.Join(e, ", ")
+}
+
+func (m *Message) validate() error {
+	nchar := strings.Count(m.Message, "")
+	if nchar > MaxMsgLength {
+		return fmt.Errorf("%d character message too long, allowed %d characters", nchar, MaxMsgLength)
+	}
+	nchar = strings.Count(m.Title, "")
+	if nchar > MaxTitleLength {
+		return fmt.Errorf("%d-character title too long, allowed %d characters", nchar, MaxTitleLength)
+	}
+	return nil
+}
+
+// Push sends the Message m to Pushover.
+func Push(m Message) error {
+	if err := m.validate(); err != nil {
+		return err
+	}
+	req := url.Values{}
+	req.Add("token", m.Token)
+	req.Add("user", m.User)
+	req.Add("title", m.Title)
+	req.Add("message", m.Message)
+	resp, err := http.PostForm(apiurl, req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	if resp.StatusCode == http.StatusOK {
+		return nil
+	}
+
+	var presp response
+	if err := json.NewDecoder(resp.Body).Decode(&presp); err != nil {
+		return fmt.Errorf("decode error response: %v", err)
+	}
+	return presp.Errors
+}