Initial commit.
parent
0515fc10c2
commit
082cfca0c3
|
@ -1,2 +1,5 @@
|
||||||
# am
|
# 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