Initial commit.
parent
0515fc10c2
commit
082cfca0c3
|
@ -1,2 +1,5 @@
|
|||
# am
|
||||
|
||||
* X Go client
|
||||
* X Command line client: `am https://_:<api_key>@addr <action> <log or alert>`
|
||||
* alert timeout not updated
|
|
@ -0,0 +1,44 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func run(cmd, name, text string) {
|
||||
c := exec.Command(cmd, name, text)
|
||||
c.Stderr = os.Stderr
|
||||
if err := c.Run(); err != nil {
|
||||
log.Printf("Failed to run command in shell `%s`: %v", cmd, err)
|
||||
}
|
||||
}
|
||||
|
||||
func runLogAction(action, sourceName, text string) {
|
||||
log.Printf("[%s] %s", sourceName, text)
|
||||
|
||||
if action == "" {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get working directory: %v", err)
|
||||
return
|
||||
}
|
||||
action = filepath.Join(dir, "log-action")
|
||||
}
|
||||
|
||||
run(action, sourceName, text)
|
||||
}
|
||||
|
||||
func runAlertAction(action, sourceName, text string) {
|
||||
log.Printf("[%s] ALERT %s", sourceName, text)
|
||||
if action == "" {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Printf("Failed to get working directory: %v", err)
|
||||
return
|
||||
}
|
||||
action = filepath.Join(dir, "alert-action")
|
||||
}
|
||||
run(action, sourceName, text)
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package amclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
addr string
|
||||
apiKey string
|
||||
}
|
||||
|
||||
// addr is the full address, for example: https://x.io/report.
|
||||
func New(addr, apiKey string) Client {
|
||||
return Client{addr, apiKey}
|
||||
}
|
||||
|
||||
func (cl Client) post(values url.Values) error {
|
||||
resp, err := http.PostForm(cl.addr, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("POST failed: [%d] %s", resp.StatusCode, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cl Client) Ping() error {
|
||||
return cl.post(url.Values{
|
||||
"key": []string{cl.apiKey},
|
||||
"action": []string{"ping"},
|
||||
})
|
||||
}
|
||||
|
||||
func (cl Client) Log(text string) error {
|
||||
return cl.post(url.Values{
|
||||
"key": []string{cl.apiKey},
|
||||
"action": []string{"log"},
|
||||
"text": []string{text},
|
||||
})
|
||||
}
|
||||
|
||||
func (cl Client) Alert(text string) error {
|
||||
return cl.post(url.Values{
|
||||
"key": []string{cl.apiKey},
|
||||
"action": []string{"alert"},
|
||||
"text": []string{text},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.crumpington.com/public/am/amclient"
|
||||
)
|
||||
|
||||
func main() {
|
||||
usage := func() {
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"\n\n%s <URL> <api_key> <action> [text]\n\n"+
|
||||
"Action is one of `ping`, `log`, or `alert`.\n"+
|
||||
"The `log` and `alert` actions require `text`.\n\n",
|
||||
os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(os.Args) < 4 {
|
||||
usage()
|
||||
}
|
||||
|
||||
url := os.Args[1]
|
||||
apiKey := os.Args[2]
|
||||
|
||||
cl := amclient.New(url, apiKey)
|
||||
|
||||
action := os.Args[3]
|
||||
|
||||
var err error
|
||||
|
||||
switch action {
|
||||
case "ping":
|
||||
err = cl.Ping()
|
||||
|
||||
case "log":
|
||||
if len(os.Args) < 5 {
|
||||
usage()
|
||||
}
|
||||
err = cl.Log(strings.Join(os.Args[4:], " "))
|
||||
|
||||
case "alert":
|
||||
if len(os.Args) < 5 {
|
||||
usage()
|
||||
}
|
||||
err = cl.Alert(strings.Join(os.Args[4:], " "))
|
||||
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Error: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package main
|
||||
|
||||
import "git.crumpington.com/public/am"
|
||||
|
||||
func main() {
|
||||
am.Main()
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
paths, err := filepath.Glob("templates/*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
contents := []string{}
|
||||
for _, path := range paths {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
contents = append(contents, string(data))
|
||||
}
|
||||
|
||||
s := "package am\n\nvar tmpls = `\n" + strings.Join(contents, "\n") + "\n`\n"
|
||||
if err := ioutil.WriteFile("tmpl_gen.go", []byte(s), 0644); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func newUUID() string {
|
||||
buf := make([]byte, 16)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
s := hex.EncodeToString(buf)
|
||||
return s[:8] + "-" +
|
||||
s[8:12] + "-" +
|
||||
s[12:16] + "-" +
|
||||
s[16:20] + "-" +
|
||||
s[20:32]
|
||||
}
|
||||
|
||||
func newCSRF() string {
|
||||
buf := make([]byte, 16)
|
||||
_, err := rand.Read(buf)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(buf)
|
||||
}
|
|
@ -0,0 +1,373 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type dbal struct {
|
||||
sql.DB
|
||||
}
|
||||
|
||||
func (*dbal) logf(s string, args ...interface{}) {
|
||||
log.Printf("db: "+s, args...)
|
||||
}
|
||||
|
||||
func newDB(dbPath string) (*dbal, error) {
|
||||
_db, err := sql.Open("sqlite3", dbPath)
|
||||
if err != nil {
|
||||
log.Printf("Failed to open database %s: %v", dbPath, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db := &dbal{*_db}
|
||||
|
||||
if _, err := db.Exec(migration); err != nil {
|
||||
log.Printf("Failed to apply database migration: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_ = db.UserInsert(User{
|
||||
Username: `root`,
|
||||
Admin: true,
|
||||
}, `root`)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
/*********
|
||||
* Users *
|
||||
*********/
|
||||
|
||||
func (db *dbal) UserInsert(u User, pwd string) error {
|
||||
if err := validateName(u.Username); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validatePwd(pwd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bHash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(pwd),
|
||||
bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
db.logf("Failed to hash password: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(
|
||||
`INSERT INTO users(Username,Password,Admin)VALUES(?,?,?)`,
|
||||
u.Username, string(bHash), u.Admin)
|
||||
if err != nil {
|
||||
db.logf("Failed to insert user %s: %v", u.Username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) UserGet(username string) (u User, err error) {
|
||||
u.Username = username
|
||||
err = db.QueryRow(
|
||||
`SELECT Admin FROM users WHERE Username=?`,
|
||||
username).Scan(&u.Admin)
|
||||
if err != nil {
|
||||
db.logf("Failed to get user %s: %v", username, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *dbal) UserGetWithPwd(username, pwd string) (u User, err error) {
|
||||
u.Username = username
|
||||
pwdHash := ""
|
||||
err = db.QueryRow(
|
||||
`SELECT Password,Admin FROM users WHERE Username=?`,
|
||||
username).Scan(&pwdHash, &u.Admin)
|
||||
if err != nil {
|
||||
db.logf("Failed to get user %s: %v", username, err)
|
||||
return u, err
|
||||
}
|
||||
err = bcrypt.CompareHashAndPassword([]byte(pwdHash), []byte(pwd))
|
||||
if err != nil {
|
||||
db.logf("Password comparison failed for user %s: %v", username, err)
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
func (db *dbal) UserList() (l []User, err error) {
|
||||
rows, err := db.Query(`SELECT Username, Admin FROM users ORDER BY Username`)
|
||||
if err != nil {
|
||||
db.logf("Failed to list users: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
u := User{}
|
||||
if err := rows.Scan(&u.Username, &u.Admin); err != nil {
|
||||
db.logf("Failed to scan user: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, u)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (db *dbal) UserUpdatePwd(username, pwd string) error {
|
||||
if err := validatePwd(pwd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bHash, err := bcrypt.GenerateFromPassword(
|
||||
[]byte(pwd),
|
||||
bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
db.logf("Failed to hash password: %v", err)
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(
|
||||
`UPDATE users SET Password=? WHERE Username=?`,
|
||||
string(bHash), username)
|
||||
if err != nil {
|
||||
db.logf("Failed to update password for user %s: %v", username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) UserUpdateAdmin(username string, b bool) error {
|
||||
if username == `root` {
|
||||
return errors.New("Root user is always an admin.")
|
||||
}
|
||||
_, err := db.Exec(
|
||||
`UPDATE users SET Admin=? WHERE Username=?`,
|
||||
b, username)
|
||||
if err != nil {
|
||||
db.logf("Failed to update admin for user %s: %v", username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) UserDelete(username string) error {
|
||||
if username == `root` {
|
||||
return errors.New("Root user cannot be deleted.")
|
||||
}
|
||||
_, err := db.Exec(`DELETE FROM users WHERE username=?`, username)
|
||||
if err != nil {
|
||||
db.logf("Failed to delete user %s: %v", username, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/***********
|
||||
* Sources *
|
||||
***********/
|
||||
|
||||
func (db *dbal) SourceInsert(s *Source) error {
|
||||
if err := validateName(s.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.SourceID = newUUID()
|
||||
s.APIKey = newUUID()
|
||||
s.LastSeenAt = time.Now().UTC()
|
||||
s.AlertedAt = s.LastSeenAt
|
||||
|
||||
_, err := db.Exec(`INSERT INTO sources(`+
|
||||
` SourceID,Name,APIKey,Description,`+
|
||||
` LastSeenAt,AlertTimeout,AlertedAt,`+
|
||||
` Ignore,LogAction,AlertAction`+
|
||||
`)VALUES(?,?,?,?,?,?,?,?,?,?)`,
|
||||
s.SourceID, s.Name, s.APIKey, s.Description,
|
||||
s.LastSeenAt, s.AlertTimeout, s.AlertedAt,
|
||||
s.Ignore, s.LogAction, s.AlertAction)
|
||||
if err != nil {
|
||||
db.logf("Failed to insert source: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
const sourceCols = `SourceID,Name,APIKey,Description,` +
|
||||
`LastSeenAt,AlertTimeout,AlertedAt,` +
|
||||
`Ignore,LogAction,AlertAction`
|
||||
|
||||
func (db *dbal) scanSource(
|
||||
row interface{ Scan(...interface{}) error },
|
||||
) (s Source, err error) {
|
||||
err = row.Scan(
|
||||
&s.SourceID, &s.Name, &s.APIKey, &s.Description,
|
||||
&s.LastSeenAt, &s.AlertTimeout, &s.AlertedAt,
|
||||
&s.Ignore, &s.LogAction, &s.AlertAction)
|
||||
if err != nil {
|
||||
db.logf("Failed to scan source: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *dbal) SourceGet(id string) (s Source, err error) {
|
||||
return db.scanSource(
|
||||
db.QueryRow(
|
||||
`SELECT `+sourceCols+` FROM sources WHERE SourceID=?`, id))
|
||||
}
|
||||
|
||||
func (db *dbal) SourceGetByKey(key string) (s Source, err error) {
|
||||
return db.scanSource(
|
||||
db.QueryRow(
|
||||
`SELECT `+sourceCols+` FROM sources WHERE APIKey=?`, key))
|
||||
}
|
||||
|
||||
func (db *dbal) SourceList() (l []Source, err error) {
|
||||
rows, err := db.Query(`SELECT ` + sourceCols + ` FROM sources ORDER BY Name`)
|
||||
if err != nil {
|
||||
db.logf("Failed to list sources: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
u, err := db.scanSource(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, u)
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// Updates APIKey, Description, LogAction, AlertAction.
|
||||
func (db *dbal) SourceUpdate(s Source) error {
|
||||
if err := validateAPIKey(s.APIKey); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := db.Exec(`UPDATE sources `+
|
||||
`SET APIKey=?,Description=?,AlertTimeout=?,LogAction=?,AlertAction=? `+
|
||||
`WHERE SourceID=?`,
|
||||
s.APIKey, s.Description, s.AlertTimeout,
|
||||
s.LogAction, s.AlertAction, s.SourceID)
|
||||
if err != nil {
|
||||
db.logf("Failed to update source %s: %v", s.Name, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) SourceUpdateIgnore(id string, b bool) error {
|
||||
_, err := db.Exec(`UPDATE sources SET Ignore=? WHERE SourceID=?`, b, id)
|
||||
if err != nil {
|
||||
db.logf("Failed to update source ignore %s: %v", id, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) SourceUpdateLastSeenAt(id string) error {
|
||||
t := time.Now()
|
||||
_, err := db.Exec(`UPDATE sources SET LastSeenAt=? WHERE SourceID=?`, t, id)
|
||||
if err != nil {
|
||||
db.logf("Failed to update source last seen at %s: %v", id, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) SourceUpdateAlertedAt(id string) error {
|
||||
t := time.Now()
|
||||
_, err := db.Exec(`UPDATE sources SET AlertedAt=? WHERE SourceID=?`, t, id)
|
||||
if err != nil {
|
||||
db.logf("Failed to update source alerted at %s: %v", id, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *dbal) SourceDelete(id string) error {
|
||||
_, err := db.Exec(`DELETE FROM sources WHERE SourceID=?`, id)
|
||||
if err != nil {
|
||||
db.logf("Failed to delete source %s: %v", id, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
/*******
|
||||
* Log *
|
||||
*******/
|
||||
|
||||
func (db *dbal) LogInsert(e Entry) error {
|
||||
e.TS = time.Now()
|
||||
_, err := db.Exec(`INSERT INTO log`+
|
||||
`(SourceID,TS,Alert,Text)VALUES(?,?,?,?)`,
|
||||
e.SourceID, e.TS, e.Alert, e.Text)
|
||||
if err != nil {
|
||||
db.logf("Failed to insert log entry: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
const logCols = `LogID,SourceID,TS,Alert,Text`
|
||||
|
||||
func (db *dbal) scanLog(
|
||||
row interface{ Scan(...interface{}) error },
|
||||
) (e Entry, err error) {
|
||||
err = row.Scan(&e.LogID, &e.SourceID, &e.TS, &e.Alert, &e.Text)
|
||||
if err != nil {
|
||||
db.logf("Failed to scan log entry: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type LogListArgs struct {
|
||||
BeforeID int64
|
||||
Alert *bool
|
||||
SourceID string
|
||||
Limit int64
|
||||
}
|
||||
|
||||
func (db *dbal) LogList(args LogListArgs) (l []EntryListRow, err error) {
|
||||
if args.Limit <= 0 {
|
||||
args.Limit = 100
|
||||
}
|
||||
|
||||
qArgs := []interface{}{}
|
||||
query := `SELECT ` +
|
||||
`l.LogID,s.Name,l.TS,l.Alert,l.Text ` +
|
||||
`FROM log l JOIN sources s ON l.SourceID=s.SourceID WHERE 1`
|
||||
if args.BeforeID != 0 {
|
||||
query += " AND LogID<?"
|
||||
qArgs = append(qArgs, args.BeforeID)
|
||||
}
|
||||
|
||||
if args.Alert != nil {
|
||||
query += " AND l.Alert=?"
|
||||
qArgs = append(qArgs, *args.Alert)
|
||||
}
|
||||
|
||||
if args.SourceID != "" {
|
||||
query += " AND l.SourceID=?"
|
||||
qArgs = append(qArgs, args.SourceID)
|
||||
}
|
||||
|
||||
query += ` ORDER BY TS DESC LIMIT ` + strconv.FormatInt(args.Limit, 10)
|
||||
|
||||
rows, err := db.Query(query, qArgs...)
|
||||
if err != nil {
|
||||
db.logf("Failed to list log: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
e := EntryListRow{}
|
||||
err := rows.Scan(&e.LogID, &e.SourceName, &e.TS, &e.Alert, &e.Text)
|
||||
if err != nil {
|
||||
db.logf("Failed to scan entry list row: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
l = append(l, e)
|
||||
}
|
||||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (db *dbal) LogDeleteBefore(ts time.Time) error {
|
||||
_, err := db.Exec("DELETE FROM log WHERE TS < ?", ts)
|
||||
if err != nil {
|
||||
db.logf("Failed to delete log entries: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package am
|
||||
|
||||
import "testing"
|
||||
|
||||
func NewDBForTesting() *dbal {
|
||||
db, err := newDB(":memory:")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestDBUserInsert(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
|
||||
err := db.UserInsert(User{Username: `test`, Admin: false}, `123`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.UserInsert(User{Username: `test`, Admin: false}, `123`)
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.UserInsert(User{Username: `root`, Admin: false}, `root`)
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBUserGet(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
|
||||
err := db.UserInsert(User{Username: `test`, Admin: false}, `123`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
u, err := db.UserGet(`test`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u.Username != `test` || u.Admin {
|
||||
t.Fatal(u)
|
||||
}
|
||||
|
||||
_, err = db.UserGet(`test2`)
|
||||
if err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBUserGetWithPwd(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
|
||||
u, err := db.UserGetWithPwd("root", "root")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if u.Username != "root" || !u.Admin {
|
||||
t.Fatal(u)
|
||||
}
|
||||
if _, err = db.UserGetWithPwd("root", "root2"); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBUserList(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
|
||||
err := db.UserInsert(User{Username: `test`, Admin: false}, `123`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
l, err := db.UserList()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(l) != 2 {
|
||||
t.Fatal(l)
|
||||
}
|
||||
if l[0].Username != "root" || l[1].Username != "test" {
|
||||
t.Fatal(l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBUserUpdatePwd(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
|
||||
if err := db.UserUpdatePwd("root", "xyz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := db.UserGetWithPwd("root", "xyz"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.UserGetWithPwd("root", "root"); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBUserUpdateAdmin(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
|
||||
err := db.UserInsert(User{Username: `test`, Admin: false}, `123`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := db.UserUpdateAdmin("test", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u, err := db.UserGet("test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !u.Admin {
|
||||
t.Fatal(u)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBUserDelete(t *testing.T) {
|
||||
db := NewDBForTesting()
|
||||
if err := db.UserDelete("root"); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := db.UserInsert(User{Username: `test`, Admin: false}, `123`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := db.UserDelete("test"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := db.UserGet("test"); err == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
//go:generate go run cmd/generate/templates.go
|
||||
|
||||
/***********
|
||||
* Globals *
|
||||
***********/
|
||||
|
||||
var db *dbal
|
||||
var tmpl *template.Template
|
||||
|
||||
func Main() {
|
||||
var listenAddr string
|
||||
flag.StringVar(&listenAddr, "listen", "localhost:8888",
|
||||
"Listen address, <host>:<port>")
|
||||
flag.Parse()
|
||||
|
||||
l := strings.Split(listenAddr, ":")
|
||||
if len(l) != 2 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
host := l[0]
|
||||
port := l[1]
|
||||
|
||||
var err error
|
||||
|
||||
// Initialize globals.
|
||||
tmpl = template.Must(template.New("").Parse(tmpls))
|
||||
if db, err = newDB("database.sqlite3"); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
go processTimeouts()
|
||||
go purgeOldEntries()
|
||||
|
||||
// Root.
|
||||
http.HandleFunc("/", handleRoot)
|
||||
|
||||
// API requests.
|
||||
http.HandleFunc("/report", handleReport)
|
||||
|
||||
// User routes.
|
||||
routeAdmin("/user/insert", handleUserInsert)
|
||||
routeUser("/user/list", handleUserList)
|
||||
routeUser("/user/view/", handleUserView)
|
||||
routeAdmin("/user/update/", handleUserUpdate)
|
||||
routeAdmin("/user/delete/", handleUserDelete)
|
||||
|
||||
routeAdmin("/source/insert", handleSourceInsert)
|
||||
routeUser("/source/list", handleSourceList)
|
||||
routeUser("/source/view/", handleSourceView)
|
||||
routeAdmin("/source/update/", handleSourceUpdate)
|
||||
routeAdmin("/source/delete/", handleSourceDelete)
|
||||
|
||||
routeUser("/log/list", handleLogList)
|
||||
|
||||
if port == "443" {
|
||||
http.Serve(autocert.NewListener(host), http.DefaultServeMux)
|
||||
} else {
|
||||
http.ListenAndServe(listenAddr, nil)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package am
|
||||
|
||||
var migration = `
|
||||
CREATE TABLE IF NOT EXISTS users(
|
||||
Username TEXT NOT NULL PRIMARY KEY,
|
||||
Password TEXT NOT NULL,
|
||||
Admin BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sources(
|
||||
SourceID TEXT NOT NULL PRIMARY KEY,
|
||||
Name TEXT NOT NULL UNIQUE,
|
||||
APIKey TEXT NOT NULL UNIQUE,
|
||||
Description TEXT NOT NULL,
|
||||
LastSeenAt TIMESTAMP NOT NULL,
|
||||
AlertTimeout BIGINT NOT NULL,
|
||||
AlertedAt TIMESTAMP NOT NULL,
|
||||
Ignore BOOLEAN NOT NULL,
|
||||
LogAction TEXT NOT NULL,
|
||||
AlertAction TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS log(
|
||||
LogID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
SourceID TEXT NOT NULL REFERENCES sources(SourceID) ON DELETE CASCADE,
|
||||
TS TIMESTAMP NOT NULL,
|
||||
Alert BOOLEAN NOT NULL,
|
||||
Text TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS log_ts_idx ON log(TS);
|
||||
CREATE INDEX IF NOT EXISTS log_source_ts_idx ON log(SourceID, TS);
|
||||
`
|
|
@ -0,0 +1,55 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func respondReportError(w http.ResponseWriter, msg string) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(msg))
|
||||
}
|
||||
|
||||
func handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
|
||||
apiKey := r.Form.Get("key")
|
||||
|
||||
src, err := db.SourceGetByKey(apiKey)
|
||||
if err != nil {
|
||||
respondNotAuthorized(w)
|
||||
return
|
||||
}
|
||||
|
||||
// Always update last seen timestamp.
|
||||
_ = db.SourceUpdateLastSeenAt(src.SourceID)
|
||||
|
||||
action := r.Form.Get("action")
|
||||
var actionFunc func(string, string, string)
|
||||
|
||||
switch action {
|
||||
case "ping":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
|
||||
case "log":
|
||||
actionFunc = runLogAction
|
||||
|
||||
case "alert":
|
||||
actionFunc = runAlertAction
|
||||
|
||||
default:
|
||||
respondReportError(w, "Unknown action: "+action)
|
||||
return
|
||||
}
|
||||
|
||||
e := Entry{
|
||||
SourceID: src.SourceID,
|
||||
Text: r.Form.Get("text"),
|
||||
Alert: action == "alert",
|
||||
}
|
||||
|
||||
_ = db.LogInsert(e)
|
||||
actionFunc(src.LogAction, src.Name, e.Text)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{{define "PageStart" -}}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>am</title>
|
||||
<style>{{template "CSS"}}</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ul class="top-menu">
|
||||
<li><a href="/log/list">Log</a></li>
|
||||
<li><a href="/source/list">Sources</a></li>
|
||||
<li><a href="/user/list">Users</a></li>
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{define "PageEnd" -}}
|
||||
</body>
|
||||
</html>
|
||||
{{- end}}
|
|
@ -0,0 +1,12 @@
|
|||
|
||||
{{define "Error" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1>Error</h1>
|
||||
|
||||
<section>
|
||||
<p>{{.}}</p>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
|
@ -0,0 +1,58 @@
|
|||
|
||||
-------------------------------------------------------------------------------
|
||||
-- List
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "LogList" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1>Log</h1>
|
||||
|
||||
<section>
|
||||
<form>
|
||||
<select Name="Type">
|
||||
<option value="all" {{if (eq .Args.Type "all")}}selected{{end}}>Type: All</option>
|
||||
<option value="alert" {{if (eq .Args.Type "alert")}}selected{{end}}>Type: Alerts</option>
|
||||
<option value="log" {{if (eq .Args.Type "log")}}selected{{end}}>Type: Non-alerts</option>
|
||||
</select>
|
||||
<select Name="SourceID">
|
||||
<option value="">Source: All</option>
|
||||
{{range .Sources}}
|
||||
<option value="{{.SourceID}}" {{if (eq .SourceID $.Args.SourceID)}}selected{{end}}>
|
||||
Source: {{.Name}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{{if .Args.BeforeID}}
|
||||
<p><a href="javascript:history.back()">Back</a></p>
|
||||
{{end}}
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th> </th>
|
||||
<th>Source</th>
|
||||
<th>Text</th>
|
||||
</tr>
|
||||
|
||||
{{range .Entries}}
|
||||
<tr>
|
||||
<td>{{.TS.Format "2006-01-02 15:04"}}
|
||||
</td>
|
||||
<td>{{if .Alert}}<b>!</b>{{end}}</td>
|
||||
<td>{{.SourceName}}</td>
|
||||
<td style="width:100%;">{{.Text}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
{{if .NextURL}}
|
||||
<p><a href="{{.NextURL}}">More</a></p>{{end}}
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
|
@ -0,0 +1,204 @@
|
|||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Insert
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceInsert" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> / Insert
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<label for="Name">Name:</label>
|
||||
<input type="text" id="Name" name="Name" required>
|
||||
</li>
|
||||
<li>
|
||||
<label for="Description">Description</label>
|
||||
<textarea id="Description" name="Description" rows=8></textarea>
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertTimeout">Alert Timeout:</label>
|
||||
<input type="number" id="AlertTimeout" name="AlertTimeout">
|
||||
</li>
|
||||
<li>
|
||||
<label for="LogAction">Log Action:</label>
|
||||
<input type="text" id="LogAction" name="LogAction">
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertAction">Alert Action:</label>
|
||||
<input type="text" id="AlertAction" name="AlertAction">
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Insert">
|
||||
</li>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- List
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceList" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">Sources</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/source/insert">Insert</a></li>
|
||||
</ul>
|
||||
|
||||
<section>
|
||||
<ul class="user-list">
|
||||
{{range . -}}
|
||||
<li>
|
||||
<a href="/source/view/{{.SourceID}}">
|
||||
{{.Name}}
|
||||
{{if .TimedOut}}✖{{end}}
|
||||
{{if .Ignore}}∥{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{- end}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- View
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceView" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> / {{.Name}}
|
||||
</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/source/update/{{.SourceID}}">Update</a></li>
|
||||
<li><a href="/source/delete/{{.SourceID}}">Delete</a></li>
|
||||
</ul>
|
||||
|
||||
<section>
|
||||
<dl>
|
||||
<dt>API Key</dt>
|
||||
<dd>{{.APIKey}}</dd>
|
||||
|
||||
<dt>Description</dt>
|
||||
<dd class="multiline">{{.Description}}</dd>
|
||||
|
||||
<dt>Last Seen</dt>
|
||||
<dd>{{.LastSeenAt.Format "2006-01-02 15:04"}}</dd>
|
||||
|
||||
<dt>Alert Timeout (sec)</dt>
|
||||
<dd>{{.AlertTimeout}}</dd>
|
||||
|
||||
<dt>Alerted At</dt>
|
||||
<dd>{{.AlertedAt.Format "2006-01-02 15:04"}}</dd>
|
||||
|
||||
<dt>Ignore</dt>
|
||||
<dd>{{if .Ignore}}True{{else}}False{{end}}</dd>
|
||||
|
||||
<dt>Log Action</dt>
|
||||
<dd>{{.LogAction}}</dd>
|
||||
|
||||
<dt>Alert Action</dt>
|
||||
<dd>{{.AlertAction}}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Update
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceUpdate" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> /
|
||||
<a href="/source/view/{{.Source.SourceID}}">{{.Source.Name}}</a> /
|
||||
Update
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<input type="hidden" name="SourceID" value="{{.Source.SourceID}}">
|
||||
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<input type="checkbox" name="Ignore" id="Ignore" {{if .Source.Ignore}}Checked{{end}}>
|
||||
<label for="Ignore">Ignore</label>
|
||||
</li>
|
||||
<li>
|
||||
<label for="APIKey">API Key:</label>
|
||||
<input type="text" id="APIKey" name="APIKey" value="{{.Source.APIKey}}">
|
||||
</li>
|
||||
<li>
|
||||
<label for="Description">Description</label>
|
||||
<textarea id="Description" name="Description" rows=8>
|
||||
{{- .Source.Description -}}
|
||||
</textarea>
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertTimeout">Alert Timeout:</label>
|
||||
<input type="number" id="AlertTimeout" name="AlertTimeout" value="{{.Source.AlertTimeout}}">
|
||||
</li>
|
||||
<li>
|
||||
<label for="LogAction">Log Action:</label>
|
||||
<input type="text" id="LogAction" name="LogAction" value="{{.Source.LogAction}}">
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertAction">Alert Action:</label>
|
||||
<input type="text" id="AlertAction" name="AlertAction" value="{{.Source.AlertAction}}">
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Update">
|
||||
</li>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Delete
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceDelete" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> /
|
||||
<a href="/source/view/{{.Source.SourceID}}">{{.Source.Name}}</a> /
|
||||
Delete
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<p>Really delete source {{.Source.Name}}?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<input type="submit" value="Delete">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
|
@ -0,0 +1,172 @@
|
|||
{{define "CSS"}}
|
||||
|
||||
/*********
|
||||
* Links *
|
||||
*********/
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/************
|
||||
* Top Menu *
|
||||
************/
|
||||
|
||||
.top-menu {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #999999;
|
||||
}
|
||||
|
||||
.top-menu li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.top-menu li a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/****************
|
||||
* Section Menu *
|
||||
****************/
|
||||
|
||||
.section-menu {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-menu li {
|
||||
float: left;
|
||||
border: 1px solid #999999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.section-menu li a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/***********
|
||||
* Section *
|
||||
***********/
|
||||
|
||||
section {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
/********************
|
||||
* Definition Lists *
|
||||
********************/
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin: 8px 0;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0 0 0 32px;
|
||||
padding: 8px 0 8px 16px;
|
||||
min-height: 20px;
|
||||
max-width: 480px;
|
||||
border-left: 1px solid #999999;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/*********
|
||||
* Table *
|
||||
*********/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tr:nth-child(even){
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/*********
|
||||
* Forms *
|
||||
*********/
|
||||
|
||||
.form-list {
|
||||
max-width: 400px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-list li {
|
||||
display: block;
|
||||
list-style: none;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.form-list label{
|
||||
margin:0 0 4px 0;
|
||||
padding:0px;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.form-list input,.form-list textarea,.form-liat select{
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-list input[type=checkbox] {
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-list input + label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.form-list input[type=submit] {
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
width: auto;
|
||||
background: none;
|
||||
border: 1px solid #999999;
|
||||
color: #444;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*************
|
||||
* User List *
|
||||
*************/
|
||||
|
||||
.user-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-list li {
|
||||
}
|
||||
|
||||
.user-list li a {
|
||||
margin: 6px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
{{end}}
|
|
@ -0,0 +1,156 @@
|
|||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Insert
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserInsert" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> / Insert
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST" autocomplete="off">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<label for="Username">Username:</label>
|
||||
<input type="text" name="Username" autocomplete="off" required>
|
||||
</li>
|
||||
<li>
|
||||
<label for="Password">Password:</label>
|
||||
<input type="password" name="Password" autocomplete="off" required>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" name="Admin" id="Admin">
|
||||
<label for="Admin">Admin</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Insert">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- List
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserList" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">Users</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/user/insert">Insert</a></li>
|
||||
</ul>
|
||||
|
||||
<section>
|
||||
<ul class="user-list">
|
||||
{{range . -}}
|
||||
<li>
|
||||
<a href="/user/view/{{.Username}}">
|
||||
{{.Username}}{{if .Admin}} ✪{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{- end}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- View
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserView" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> / {{.Username}}
|
||||
</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/user/update/{{.Username}}">Update</a></li>
|
||||
<li><a href="/user/delete/{{.Username}}">Delete</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<section>
|
||||
<dl>
|
||||
<dt>Admin</dt>
|
||||
<dd>{{if .Admin}}True{{else}}False{{end}}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Update
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserUpdate" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> /
|
||||
<a href="/user/view/{{.User.Username}}">{{.User.Username}}</a> /
|
||||
Update
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<label for="NewPassword">Password:</label>
|
||||
<input type="password" name="NewPassword" autocomplete="off">
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" name="Admin" id="Admin" {{if .User.Admin}}Checked{{end}}>
|
||||
<label for="Admin">Admin</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Update">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Delete
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserDelete" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> /
|
||||
<a href="/user/view/{{.User.Username}}">{{.User.Username}}</a> /
|
||||
Delete
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<p>Really delete user {{.User.Username}}?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<input type="submit" value="Delete">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
|
@ -0,0 +1,34 @@
|
|||
package am
|
||||
|
||||
import "time"
|
||||
|
||||
func processTimeouts() {
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
l, _ := db.SourceList()
|
||||
|
||||
for _, src := range l {
|
||||
if !src.RequiresAlertForTimeout() {
|
||||
continue
|
||||
}
|
||||
|
||||
e := Entry{
|
||||
SourceID: src.SourceID,
|
||||
Alert: true,
|
||||
Text: "Source hasn't reported in.",
|
||||
}
|
||||
|
||||
_ = db.LogInsert(e)
|
||||
runAlertAction(src.AlertAction, src.Name, e.Text)
|
||||
_ = db.SourceUpdateAlertedAt(src.SourceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func purgeOldEntries() {
|
||||
for {
|
||||
_ = db.LogDeleteBefore(time.Now().Add(-90 * 24 * time.Hour))
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,632 @@
|
|||
package am
|
||||
|
||||
var tmpls = `
|
||||
{{define "PageStart" -}}
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>am</title>
|
||||
<style>{{template "CSS"}}</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<ul class="top-menu">
|
||||
<li><a href="/log/list">Log</a></li>
|
||||
<li><a href="/source/list">Sources</a></li>
|
||||
<li><a href="/user/list">Users</a></li>
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
{{define "PageEnd" -}}
|
||||
</body>
|
||||
</html>
|
||||
{{- end}}
|
||||
|
||||
|
||||
{{define "Error" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1>Error</h1>
|
||||
|
||||
<section>
|
||||
<p>{{.}}</p>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- List
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "LogList" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1>Log</h1>
|
||||
|
||||
<section>
|
||||
<form>
|
||||
<select Name="Type">
|
||||
<option value="all" {{if (eq .Args.Type "all")}}selected{{end}}>Type: All</option>
|
||||
<option value="alert" {{if (eq .Args.Type "alert")}}selected{{end}}>Type: Alerts</option>
|
||||
<option value="log" {{if (eq .Args.Type "log")}}selected{{end}}>Type: Non-alerts</option>
|
||||
</select>
|
||||
<select Name="SourceID">
|
||||
<option value="">Source: All</option>
|
||||
{{range .Sources}}
|
||||
<option value="{{.SourceID}}" {{if (eq .SourceID $.Args.SourceID)}}selected{{end}}>
|
||||
Source: {{.Name}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
|
||||
{{if .Args.BeforeID}}
|
||||
<p><a href="javascript:history.back()">Back</a></p>
|
||||
{{end}}
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th> </th>
|
||||
<th>Source</th>
|
||||
<th>Text</th>
|
||||
</tr>
|
||||
|
||||
{{range .Entries}}
|
||||
<tr>
|
||||
<td>{{.TS.Format "2006-01-02 15:04"}}
|
||||
</td>
|
||||
<td>{{if .Alert}}<b>!</b>{{end}}</td>
|
||||
<td>{{.SourceName}}</td>
|
||||
<td style="width:100%;">{{.Text}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
|
||||
{{if .NextURL}}
|
||||
<p><a href="{{.NextURL}}">More</a></p>{{end}}
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Insert
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceInsert" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> / Insert
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<label for="Name">Name:</label>
|
||||
<input type="text" id="Name" name="Name" required>
|
||||
</li>
|
||||
<li>
|
||||
<label for="Description">Description</label>
|
||||
<textarea id="Description" name="Description" rows=8></textarea>
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertTimeout">Alert Timeout:</label>
|
||||
<input type="number" id="AlertTimeout" name="AlertTimeout">
|
||||
</li>
|
||||
<li>
|
||||
<label for="LogAction">Log Action:</label>
|
||||
<input type="text" id="LogAction" name="LogAction">
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertAction">Alert Action:</label>
|
||||
<input type="text" id="AlertAction" name="AlertAction">
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Insert">
|
||||
</li>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- List
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceList" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">Sources</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/source/insert">Insert</a></li>
|
||||
</ul>
|
||||
|
||||
<section>
|
||||
<ul class="user-list">
|
||||
{{range . -}}
|
||||
<li>
|
||||
<a href="/source/view/{{.SourceID}}">
|
||||
{{.Name}}
|
||||
{{if .TimedOut}}✖{{end}}
|
||||
{{if .Ignore}}∥{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{- end}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- View
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceView" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> / {{.Name}}
|
||||
</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/source/update/{{.SourceID}}">Update</a></li>
|
||||
<li><a href="/source/delete/{{.SourceID}}">Delete</a></li>
|
||||
</ul>
|
||||
|
||||
<section>
|
||||
<dl>
|
||||
<dt>API Key</dt>
|
||||
<dd>{{.APIKey}}</dd>
|
||||
|
||||
<dt>Description</dt>
|
||||
<dd class="multiline">{{.Description}}</dd>
|
||||
|
||||
<dt>Last Seen</dt>
|
||||
<dd>{{.LastSeenAt.Format "2006-01-02 15:04"}}</dd>
|
||||
|
||||
<dt>Alert Timeout (sec)</dt>
|
||||
<dd>{{.AlertTimeout}}</dd>
|
||||
|
||||
<dt>Alerted At</dt>
|
||||
<dd>{{.AlertedAt.Format "2006-01-02 15:04"}}</dd>
|
||||
|
||||
<dt>Ignore</dt>
|
||||
<dd>{{if .Ignore}}True{{else}}False{{end}}</dd>
|
||||
|
||||
<dt>Log Action</dt>
|
||||
<dd>{{.LogAction}}</dd>
|
||||
|
||||
<dt>Alert Action</dt>
|
||||
<dd>{{.AlertAction}}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Update
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceUpdate" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> /
|
||||
<a href="/source/view/{{.Source.SourceID}}">{{.Source.Name}}</a> /
|
||||
Update
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<input type="hidden" name="SourceID" value="{{.Source.SourceID}}">
|
||||
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<input type="checkbox" name="Ignore" id="Ignore" {{if .Source.Ignore}}Checked{{end}}>
|
||||
<label for="Ignore">Ignore</label>
|
||||
</li>
|
||||
<li>
|
||||
<label for="APIKey">API Key:</label>
|
||||
<input type="text" id="APIKey" name="APIKey" value="{{.Source.APIKey}}">
|
||||
</li>
|
||||
<li>
|
||||
<label for="Description">Description</label>
|
||||
<textarea id="Description" name="Description" rows=8>
|
||||
{{- .Source.Description -}}
|
||||
</textarea>
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertTimeout">Alert Timeout:</label>
|
||||
<input type="number" id="AlertTimeout" name="AlertTimeout" value="{{.Source.AlertTimeout}}">
|
||||
</li>
|
||||
<li>
|
||||
<label for="LogAction">Log Action:</label>
|
||||
<input type="text" id="LogAction" name="LogAction" value="{{.Source.LogAction}}">
|
||||
</li>
|
||||
<li>
|
||||
<label for="AlertAction">Alert Action:</label>
|
||||
<input type="text" id="AlertAction" name="AlertAction" value="{{.Source.AlertAction}}">
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Update">
|
||||
</li>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Delete
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "SourceDelete" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/source/list">Sources</a> /
|
||||
<a href="/source/view/{{.Source.SourceID}}">{{.Source.Name}}</a> /
|
||||
Delete
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<p>Really delete source {{.Source.Name}}?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<input type="submit" value="Delete">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
{{define "CSS"}}
|
||||
|
||||
/*********
|
||||
* Links *
|
||||
*********/
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/************
|
||||
* Top Menu *
|
||||
************/
|
||||
|
||||
.top-menu {
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #999999;
|
||||
}
|
||||
|
||||
.top-menu li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.top-menu li a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/****************
|
||||
* Section Menu *
|
||||
****************/
|
||||
|
||||
.section-menu {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-menu li {
|
||||
float: left;
|
||||
border: 1px solid #999999;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.section-menu li a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
/***********
|
||||
* Section *
|
||||
***********/
|
||||
|
||||
section {
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
/********************
|
||||
* Definition Lists *
|
||||
********************/
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
margin: 8px 0;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0 0 0 32px;
|
||||
padding: 8px 0 8px 16px;
|
||||
min-height: 20px;
|
||||
max-width: 480px;
|
||||
border-left: 1px solid #999999;
|
||||
}
|
||||
|
||||
.multiline {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/*********
|
||||
* Table *
|
||||
*********/
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tr:nth-child(even){
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/*********
|
||||
* Forms *
|
||||
*********/
|
||||
|
||||
.form-list {
|
||||
max-width: 400px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-list li {
|
||||
display: block;
|
||||
list-style: none;
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
|
||||
.form-list label{
|
||||
margin:0 0 4px 0;
|
||||
padding:0px;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.form-list input,.form-list textarea,.form-liat select{
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-list input[type=checkbox] {
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-list input + label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.form-list input[type=submit] {
|
||||
padding: 8px;
|
||||
margin: 0px;
|
||||
width: auto;
|
||||
background: none;
|
||||
border: 1px solid #999999;
|
||||
color: #444;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*************
|
||||
* User List *
|
||||
*************/
|
||||
|
||||
.user-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.user-list li {
|
||||
}
|
||||
|
||||
.user-list li a {
|
||||
margin: 6px 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
{{end}}
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Insert
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserInsert" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> / Insert
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST" autocomplete="off">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<label for="Username">Username:</label>
|
||||
<input type="text" name="Username" autocomplete="off" required>
|
||||
</li>
|
||||
<li>
|
||||
<label for="Password">Password:</label>
|
||||
<input type="password" name="Password" autocomplete="off" required>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" name="Admin" id="Admin">
|
||||
<label for="Admin">Admin</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Insert">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- List
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserList" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">Users</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/user/insert">Insert</a></li>
|
||||
</ul>
|
||||
|
||||
<section>
|
||||
<ul class="user-list">
|
||||
{{range . -}}
|
||||
<li>
|
||||
<a href="/user/view/{{.Username}}">
|
||||
{{.Username}}{{if .Admin}} ✪{{end}}
|
||||
</a>
|
||||
</li>
|
||||
{{- end}}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- View
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserView" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> / {{.Username}}
|
||||
</h1>
|
||||
|
||||
<ul class="section-menu">
|
||||
<li><a href="/user/update/{{.Username}}">Update</a></li>
|
||||
<li><a href="/user/delete/{{.Username}}">Delete</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
<section>
|
||||
<dl>
|
||||
<dt>Admin</dt>
|
||||
<dd>{{if .Admin}}True{{else}}False{{end}}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Update
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserUpdate" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> /
|
||||
<a href="/user/view/{{.User.Username}}">{{.User.Username}}</a> /
|
||||
Update
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<label for="NewPassword">Password:</label>
|
||||
<input type="password" name="NewPassword" autocomplete="off">
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" name="Admin" id="Admin" {{if .User.Admin}}Checked{{end}}>
|
||||
<label for="Admin">Admin</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="submit" value="Update">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Delete
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
{{define "UserDelete" -}}
|
||||
{{template "PageStart"}}
|
||||
|
||||
<h1 class="breadcrumbs">
|
||||
<a href="/user/list">Users</a> /
|
||||
<a href="/user/view/{{.User.Username}}">{{.User.Username}}</a> /
|
||||
Delete
|
||||
</h1>
|
||||
|
||||
<section>
|
||||
<p>Really delete user {{.User.Username}}?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="CSRF" value="{{.CSRF}}">
|
||||
<ul class="form-list">
|
||||
<li>
|
||||
<input type="submit" value="Delete">
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{{template "PageEnd"}}
|
||||
{{- end}}
|
||||
|
||||
`
|
|
@ -0,0 +1,45 @@
|
|||
package am
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Admin bool
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
SourceID string
|
||||
Name string
|
||||
APIKey string
|
||||
Description string
|
||||
LastSeenAt time.Time
|
||||
AlertTimeout int64 // In seconds.
|
||||
AlertedAt time.Time // Timeout alert time.
|
||||
Ignore bool // Don't trigger alerts.
|
||||
LogAction string // Override log action.
|
||||
AlertAction string // Override alert action.
|
||||
}
|
||||
|
||||
func (s Source) TimedOut() bool {
|
||||
return time.Since(s.LastSeenAt) > time.Duration(s.AlertTimeout)*time.Second
|
||||
}
|
||||
|
||||
func (s Source) RequiresAlertForTimeout() bool {
|
||||
return s.TimedOut() && s.AlertedAt.Before(s.LastSeenAt)
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
LogID int64
|
||||
SourceID string
|
||||
TS time.Time
|
||||
Alert bool
|
||||
Text string
|
||||
}
|
||||
|
||||
type EntryListRow struct {
|
||||
LogID int64
|
||||
SourceName string
|
||||
TS time.Time
|
||||
Alert bool
|
||||
Text string
|
||||
}
|
|
@ -0,0 +1,384 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func formGetInt(r *http.Request, name string) int64 {
|
||||
s := r.Form.Get(name)
|
||||
i, _ := strconv.ParseInt(s, 10, 64)
|
||||
return i
|
||||
}
|
||||
|
||||
func execTmpl(w http.ResponseWriter, name string, data interface{}) {
|
||||
if err := tmpl.ExecuteTemplate(w, name, data); err != nil {
|
||||
log.Printf("Failed to execute template %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func respondNotAuthorized(w http.ResponseWriter) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="am"`)
|
||||
w.WriteHeader(401)
|
||||
w.Write([]byte("Unauthorised.\n"))
|
||||
}
|
||||
|
||||
func respondInvalidCSRF(w http.ResponseWriter) {
|
||||
w.WriteHeader(403)
|
||||
w.Write([]byte("Forbidden.\n"))
|
||||
}
|
||||
|
||||
func respondRedirect(w http.ResponseWriter, r *http.Request, url string, args ...interface{}) {
|
||||
http.Redirect(w, r, fmt.Sprintf(url, args...), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func getReqUser(w http.ResponseWriter, r *http.Request) (User, bool) {
|
||||
username, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
return User{}, false
|
||||
}
|
||||
|
||||
user, err := db.UserGetWithPwd(username, pass)
|
||||
if err != nil {
|
||||
return user, false
|
||||
}
|
||||
|
||||
return user, true
|
||||
}
|
||||
|
||||
func getCSRF(w http.ResponseWriter, r *http.Request) string {
|
||||
cookie, err := r.Cookie("am_csrf")
|
||||
if err != nil || len(cookie.Value) != 32 {
|
||||
token := newCSRF()
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "am_csrf",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
})
|
||||
return token
|
||||
}
|
||||
return cookie.Value
|
||||
}
|
||||
|
||||
func checkCSRF(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.Method != http.MethodPost {
|
||||
return true
|
||||
}
|
||||
|
||||
cookieVal := getCSRF(w, r)
|
||||
|
||||
r.ParseForm()
|
||||
formVal := r.Form.Get("CSRF")
|
||||
if formVal == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return formVal == cookieVal
|
||||
}
|
||||
|
||||
// TODO: Rename function.
|
||||
func routeUser(path string, h func(w http.ResponseWriter, r *http.Request)) {
|
||||
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := getReqUser(w, r); !ok {
|
||||
respondNotAuthorized(w)
|
||||
return
|
||||
}
|
||||
if !checkCSRF(w, r) {
|
||||
respondInvalidCSRF(w)
|
||||
return
|
||||
}
|
||||
h(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Rename function.
|
||||
func routeAdmin(path string, h func(w http.ResponseWriter, r *http.Request)) {
|
||||
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
if u, ok := getReqUser(w, r); !ok || !u.Admin {
|
||||
respondNotAuthorized(w)
|
||||
return
|
||||
}
|
||||
if !checkCSRF(w, r) {
|
||||
respondInvalidCSRF(w)
|
||||
return
|
||||
}
|
||||
h(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
respondRedirect(w, r, "/user/list")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func handleUserInsert(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
execTmpl(w, "UserInsert", struct{ CSRF string }{getCSRF(w, r)})
|
||||
return
|
||||
}
|
||||
|
||||
user := User{
|
||||
Username: r.Form.Get("Username"),
|
||||
Admin: r.Form.Get("Admin") != "",
|
||||
}
|
||||
|
||||
if err := db.UserInsert(user, r.Form.Get("Password")); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
respondRedirect(w, r, "/user/view/%s", user.Username)
|
||||
}
|
||||
|
||||
func handleUserList(w http.ResponseWriter, r *http.Request) {
|
||||
l, err := db.UserList()
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
execTmpl(w, "UserList", l)
|
||||
}
|
||||
|
||||
func handleUserView(w http.ResponseWriter, r *http.Request) {
|
||||
name := filepath.Base(r.URL.Path)
|
||||
u, err := db.UserGet(name)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
execTmpl(w, "UserView", u)
|
||||
}
|
||||
|
||||
func handleUserUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
name := filepath.Base(r.URL.Path)
|
||||
u, err := db.UserGet(name)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
execTmpl(w, "UserUpdate", struct {
|
||||
User User
|
||||
CSRF string
|
||||
}{u, getCSRF(w, r)})
|
||||
return
|
||||
}
|
||||
|
||||
admin := r.Form.Get("Admin") != ""
|
||||
if u.Admin != admin {
|
||||
if err := db.UserUpdateAdmin(u.Username, admin); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
pwd := r.Form.Get("NewPassword")
|
||||
if pwd != "" {
|
||||
if err := db.UserUpdatePwd(u.Username, pwd); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
respondRedirect(w, r, "/user/view/%s", u.Username)
|
||||
}
|
||||
|
||||
func handleUserDelete(w http.ResponseWriter, r *http.Request) {
|
||||
name := filepath.Base(r.URL.Path)
|
||||
u, err := db.UserGet(name)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
execTmpl(w, "UserDelete", struct {
|
||||
User User
|
||||
CSRF string
|
||||
}{u, getCSRF(w, r)})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.UserDelete(name); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondRedirect(w, r, "/user/list")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func handleSourceInsert(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
execTmpl(w, "SourceInsert", struct{ CSRF string }{getCSRF(w, r)})
|
||||
return
|
||||
}
|
||||
|
||||
s := Source{
|
||||
Name: r.Form.Get("Name"),
|
||||
Description: r.Form.Get("Description"),
|
||||
AlertTimeout: formGetInt(r, "AlertTimeout"),
|
||||
LogAction: r.Form.Get("LogAction"),
|
||||
AlertAction: r.Form.Get("AlertAction"),
|
||||
}
|
||||
|
||||
if err := db.SourceInsert(&s); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondRedirect(w, r, "/source/view/%s", s.SourceID)
|
||||
}
|
||||
|
||||
func handleSourceList(w http.ResponseWriter, r *http.Request) {
|
||||
l, err := db.SourceList()
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
execTmpl(w, "SourceList", l)
|
||||
}
|
||||
|
||||
func handleSourceView(w http.ResponseWriter, r *http.Request) {
|
||||
id := filepath.Base(r.URL.Path)
|
||||
s, err := db.SourceGet(id)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
execTmpl(w, "SourceView", s)
|
||||
}
|
||||
|
||||
func handleSourceUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
id := filepath.Base(r.URL.Path)
|
||||
s, err := db.SourceGet(id)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet {
|
||||
execTmpl(w, "SourceUpdate", struct {
|
||||
Source Source
|
||||
CSRF string
|
||||
}{s, getCSRF(w, r)})
|
||||
return
|
||||
}
|
||||
|
||||
s.APIKey = r.Form.Get("APIKey")
|
||||
s.Description = r.Form.Get("Description")
|
||||
s.AlertTimeout = formGetInt(r, "AlertTimeout")
|
||||
s.LogAction = r.Form.Get("LogAction")
|
||||
s.AlertAction = r.Form.Get("AlertAction")
|
||||
|
||||
if err := db.SourceUpdate(s); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
ignore := r.Form.Get("Ignore") != ""
|
||||
if ignore != s.Ignore {
|
||||
if err := db.SourceUpdateIgnore(s.SourceID, ignore); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
respondRedirect(w, r, "/source/view/%s", s.SourceID)
|
||||
}
|
||||
|
||||
func handleSourceDelete(w http.ResponseWriter, r *http.Request) {
|
||||
id := filepath.Base(r.URL.Path)
|
||||
u, err := db.SourceGet(id)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodGet {
|
||||
execTmpl(w, "SourceDelete", struct {
|
||||
Source Source
|
||||
CSRF string
|
||||
}{u, getCSRF(w, r)})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.SourceDelete(id); err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondRedirect(w, r, "/source/list")
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type logListArgs struct {
|
||||
BeforeID int64
|
||||
SourceID string
|
||||
Type string // One of "all", "alert", "log".
|
||||
Limit int64
|
||||
}
|
||||
|
||||
func handleLogList(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
|
||||
args := logListArgs{
|
||||
BeforeID: formGetInt(r, "BeforeID"),
|
||||
Limit: formGetInt(r, "Limit"),
|
||||
SourceID: r.Form.Get("SourceID"),
|
||||
Type: r.Form.Get("Type"),
|
||||
}
|
||||
if args.Limit < 1 {
|
||||
args.Limit = 100
|
||||
}
|
||||
|
||||
listArgs := LogListArgs{
|
||||
BeforeID: args.BeforeID,
|
||||
Limit: args.Limit + 1,
|
||||
SourceID: args.SourceID,
|
||||
}
|
||||
|
||||
switch args.Type {
|
||||
case "alert":
|
||||
b := true
|
||||
listArgs.Alert = &b
|
||||
|
||||
case "log":
|
||||
b := false
|
||||
listArgs.Alert = &b
|
||||
}
|
||||
|
||||
l, err := db.LogList(listArgs)
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
nextURL := ""
|
||||
|
||||
if len(l) > int(args.Limit) {
|
||||
l = l[:len(l)-1]
|
||||
nextURL = fmt.Sprintf("?Limit=%d&BeforeID=%d&SourceID=%s&Type=%s",
|
||||
args.Limit, l[len(l)-1].LogID, args.SourceID, args.Type)
|
||||
}
|
||||
|
||||
sources, err := db.SourceList()
|
||||
if err != nil {
|
||||
execTmpl(w, "Error", err)
|
||||
return
|
||||
}
|
||||
|
||||
execTmpl(w, "LogList", struct {
|
||||
Entries []EntryListRow
|
||||
Sources []Source
|
||||
Args logListArgs
|
||||
NextURL string
|
||||
}{l, sources, args, nextURL})
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package am
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var namePattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
|
||||
|
||||
func validateName(s string) error {
|
||||
if len(s) < 2 || !namePattern.MatchString(s) {
|
||||
return fmt.Errorf("Invalid name: %s", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePwd(pwd string) error {
|
||||
if len(pwd) < 8 {
|
||||
return errors.New("Password must be at least 8 characters.")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func validateAPIKey(key string) error {
|
||||
if len(key) < 16 {
|
||||
return errors.New("API key must be at least 16 characters.")
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue