236 lines
4.7 KiB
Go
236 lines
4.7 KiB
Go
|
package store
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
|
||
|
"git.t-juice.club/torjus/apiary/models"
|
||
|
bolt "go.etcd.io/bbolt"
|
||
|
)
|
||
|
|
||
|
// var _ LoginAttemptStore = &BBoltStore{}
|
||
|
|
||
|
var bktKeyLogins []byte = []byte("logins")
|
||
|
|
||
|
type BBoltStore struct {
|
||
|
db *bolt.DB
|
||
|
}
|
||
|
|
||
|
func NewBBoltStore(path string) (*BBoltStore, error) {
|
||
|
db, err := bolt.Open(path, 0o666, nil)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error opening database: %w", err)
|
||
|
}
|
||
|
|
||
|
var store BBoltStore
|
||
|
store.db = db
|
||
|
|
||
|
err = db.Update(func(tx *bolt.Tx) error {
|
||
|
_, err := tx.CreateBucketIfNotExists(bktKeyLogins)
|
||
|
return err
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error creating database bucket: %w", err)
|
||
|
}
|
||
|
|
||
|
return &store, nil
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) Close() error {
|
||
|
return s.db.Close()
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) AddAttempt(l *models.LoginAttempt) error {
|
||
|
data, err := json.Marshal(l)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return s.db.Update(func(tx *bolt.Tx) error {
|
||
|
bkt := tx.Bucket(bktKeyLogins)
|
||
|
seq, err := bkt.NextSequence()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
key := itob(seq)
|
||
|
return bkt.Put(key, data)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) All() (<-chan models.LoginAttempt, error) {
|
||
|
ch := make(chan models.LoginAttempt)
|
||
|
|
||
|
go func() {
|
||
|
_ = s.db.View(func(tx *bolt.Tx) error {
|
||
|
bkt := tx.Bucket(bktKeyLogins)
|
||
|
|
||
|
c := bkt.Cursor()
|
||
|
|
||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||
|
var l models.LoginAttempt
|
||
|
if err := json.Unmarshal(v, &l); err != nil {
|
||
|
close(ch)
|
||
|
panic(err)
|
||
|
}
|
||
|
ch <- l
|
||
|
}
|
||
|
|
||
|
close(ch)
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
}()
|
||
|
|
||
|
return ch, nil
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) Stats(statType LoginStats, limit int) ([]StatsResult, error) {
|
||
|
if statType == LoginStatsTotals {
|
||
|
return s.statTotals()
|
||
|
}
|
||
|
|
||
|
counts := make(map[string]int)
|
||
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||
|
bkt := tx.Bucket(bktKeyLogins)
|
||
|
|
||
|
c := bkt.Cursor()
|
||
|
|
||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||
|
var l models.LoginAttempt
|
||
|
if err := json.Unmarshal(v, &l); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
switch statType {
|
||
|
case LoginStatsPasswords:
|
||
|
counts[l.Password]++
|
||
|
case LoginStatsCountry:
|
||
|
counts[l.Country]++
|
||
|
case LoginStatsIP:
|
||
|
counts[l.RemoteIP.String()]++
|
||
|
case LoginStatsUsername:
|
||
|
counts[l.Username]++
|
||
|
default:
|
||
|
return fmt.Errorf("invalid stat type")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("error generating stats: %w", err)
|
||
|
}
|
||
|
|
||
|
if limit < 1 {
|
||
|
return toResults(counts), nil
|
||
|
}
|
||
|
if limit >= len(counts) {
|
||
|
return toResults(counts), nil
|
||
|
}
|
||
|
|
||
|
var si StatItems
|
||
|
for key := range counts {
|
||
|
si = append(si, StatItem{Key: key, Count: counts[key]})
|
||
|
}
|
||
|
sort.Sort(si)
|
||
|
|
||
|
output := make(map[string]int)
|
||
|
for i := len(si) - 1; i > len(si)-limit-1; i-- {
|
||
|
output[si[i].Key] = si[i].Count
|
||
|
}
|
||
|
|
||
|
return toResults(output), nil
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) statTotals() ([]StatsResult, error) {
|
||
|
passwords := make(map[string]int)
|
||
|
usernames := make(map[string]int)
|
||
|
ips := make(map[string]int)
|
||
|
countries := make(map[string]int)
|
||
|
var count int
|
||
|
|
||
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||
|
bkt := tx.Bucket(bktKeyLogins)
|
||
|
|
||
|
c := bkt.Cursor()
|
||
|
|
||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||
|
var l models.LoginAttempt
|
||
|
if err := json.Unmarshal(v, &l); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
passwords[l.Password] += 1
|
||
|
usernames[l.Username] += 1
|
||
|
ips[l.RemoteIP.String()] += 1
|
||
|
countries[l.Country] += 1
|
||
|
count++
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
stats := []StatsResult{
|
||
|
{Name: "UniquePasswords", Count: len(passwords)},
|
||
|
{Name: "UniqueUsernames", Count: len(usernames)},
|
||
|
{Name: "UniqueIPs", Count: len(ips)},
|
||
|
{Name: "UniqueCountries", Count: len(countries)},
|
||
|
{Name: "TotalLoginAttempts", Count: count},
|
||
|
}
|
||
|
return stats, nil
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) Query(query AttemptQuery) ([]models.LoginAttempt, error) {
|
||
|
var results []models.LoginAttempt
|
||
|
err := s.db.View(func(tx *bolt.Tx) error {
|
||
|
bkt := tx.Bucket(bktKeyLogins)
|
||
|
|
||
|
c := bkt.Cursor()
|
||
|
|
||
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||
|
var l models.LoginAttempt
|
||
|
if err := json.Unmarshal(v, &l); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
switch query.QueryType {
|
||
|
case AttemptQueryTypeIP:
|
||
|
if l.RemoteIP.String() == query.Query {
|
||
|
results = append(results, l)
|
||
|
}
|
||
|
case AttemptQueryTypePassword:
|
||
|
if strings.Contains(l.Password, query.Query) {
|
||
|
results = append(results, l)
|
||
|
}
|
||
|
case AttemptQueryTypeUsername:
|
||
|
if strings.Contains(l.Username, query.Query) {
|
||
|
results = append(results, l)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return results, nil
|
||
|
}
|
||
|
|
||
|
func (s *BBoltStore) IsHealthy() error {
|
||
|
// TODO: Do actual healthcheck
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func itob(v uint64) []byte {
|
||
|
b := make([]byte, 8)
|
||
|
binary.BigEndian.PutUint64(b, v)
|
||
|
return b
|
||
|
}
|