package main import ( "fmt" "net/http" "github.com/mattermost/mattermost-server/plugin" "github.com/mattermost/mattermost-server/model" "github.com/pkg/errors" "sync" "reflect" _ "github.com/google/go-cmp/cmp" "os" _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" _ "github.com/kr/pretty" "strings" "regexp" "strconv" "time" "github.com/fsnotify/fsnotify" "io/ioutil" "html" "encoding/json" "encoding/pem" "crypto/rsa" "crypto/x509" "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) const ( Unknown = iota Open Closed ) func statusName(status int) (string, error) { switch status { case Unknown: return "unknown", nil case Open: return "open", nil case Closed: return "closed", nil } return "", errors.New(fmt.Sprintf("Invalid status %d", status)) } type DoorRequest struct { ip string time int verify string } type Door struct { id uint8 status int pubKey *rsa.PublicKey lastRequest *DoorRequest } type Config struct { NumDoors string numDoors uint8 WatchPath string AdminUsers string adminUsers []string KeyPath string } type BathroomMonitorPlugin struct { plugin.MattermostPlugin config *Config configLock sync.RWMutex configChanged chan struct{} configUpdates int doorLock sync.RWMutex doors map[uint8]*Door lastReport *string } func (p *BathroomMonitorPlugin) getKeyFile(id uint8) (*rsa.PublicKey, error) { keyFile, err := ioutil.ReadFile(path.Join(p.config.KeyPath, fmt.Sprintf("public%d.pem", id))) if err != nil { return nil, err } data, _ := pem.Decode(keyFile) if data == nil { return nil, errors.New("No PEM formatted data in file") } key, err := x509.ParsePKIXPublicKey(data.Bytes) if err != nil { return nil, err } switch pub := key.(type) { case *rsa.PublicKey: return pub, nil } return nil, errors.New(fmt.Sprintf("Keyfile wasn't a public RSA PKIX key, it was %T", key)) } func (p *BathroomMonitorPlugin) initDoors() { p.doorLock.Lock() defer p.doorLock.Unlock() p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { id := i + 1 var pub *rsa.PublicKey = nil if !USE_FSNOTIFY { var err error pub, err = p.getKeyFile(id) if err != nil { p.API.LogError(errors.Wrap(err, fmt.Sprintf("Unable to load key file for id %d", id)).Error()) pub = nil } } p.doors[i] = &Door { id: id, status: Unknown, pubKey: pub, } } } func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { p.doorLock.Lock() defer p.doorLock.Unlock() if id < 1 || id > p.config.numDoors { return errors.New(fmt.Sprintf("Invalid door id %d", id)) } if status < Unknown || status > Closed { return errors.New(fmt.Sprintf("Invalid door status %d", status)) } if p.doors == nil || len(p.doors) < int(p.config.numDoors) { return errors.New("Doors not inited yet") } if p.doors[id - 1].status != status { p.doors[id - 1].status = status if report { statusStr, _ := statusName(status) p.postAdminChannel(fmt.Sprintf("STATUS: door %d = %s", id, statusStr)) } } return nil } func (p *BathroomMonitorPlugin) init() *BathroomMonitorPlugin { p.config = &Config { NumDoors: "1", numDoors: 1, WatchPath: "./", KeyPath: "./", AdminUsers: "", } p.configChanged = make(chan struct{}) return p; } func (p *BathroomMonitorPlugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/status" { p.doorLock.Lock() defer p.doorLock.Unlock() var write map[string]string = make(map[string]string) for d, s := range p.doors { statusStr, _ := statusName(s.status) write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) } else { fmt.Fprint(w, html.EscapeString(err.Error())) } return } if r.URL.Path == "/status-update" { } http.NotFound(w, r) } func (p *BathroomMonitorPlugin) confChangedEvent() { select { case p.configChanged <- struct{}{}: default: } } func (p *BathroomMonitorPlugin) postAdminChannel(text string) { p.API.LogError(text) if p.lastReport != nil && *p.lastReport == text { return } p.lastReport = &text var users []*model.User = make([]*model.User, 0, len(p.config.adminUsers)) for _, un := range(p.config.adminUsers) { u, _ := p.API.GetUserByUsername(un) if u != nil { users = append(users, u) } } if len(users) > 0 { admin := users[0] bots, _ := p.API.GetBots(&model.BotGetOptions{Page:0, PerPage:1000}) var bathroom_bot *model.Bot = nil if bots != nil { for _, b := range(bots) { if b.Username == "bathroom-bot" { bathroom_bot = b break } } } if bathroom_bot == nil { created_bathroom_bot, err := p.API.CreateBot(&model.Bot{Username:"bathroom-bot", OwnerId:admin.Id, DisplayName:"Bathroom Bot", Description:"Tracks Bathroom Status"}) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't create bathroom-bot bot").Error()) return } bathroom_bot = created_bathroom_bot } if bathroom_bot == nil { p.API.LogError("Really couldn't create bathroom-bot bot") return } for _, u := range(users) { channel, err := p.API.GetDirectChannel(bathroom_bot.UserId, u.Id); if err != nil { p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't get direct channel to user %s", u.Username)).Error()) continue } p.API.CreatePost(&model.Post{UserId: bathroom_bot.UserId, ChannelId:channel.Id, Message:text, MessageSource:text}) } } else { p.API.LogError("No admin users?") } } func (p *BathroomMonitorPlugin) OnConfigurationChange() error { p.configLock.Lock() defer p.configLock.Unlock() var newConfig *Config = new(Config); if err := p.API.LoadPluginConfiguration(newConfig); err != nil { newErr := errors.Wrap(err, fmt.Sprintf("%d: Failed to load configuration", p.configUpdates)) p.postAdminChannel(newErr.Error()) return newErr } if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } var configErr error = nil if USE_FSNOTIFY { if info, err := os.Stat(newConfig.WatchPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { newConfig.WatchPath = "./" configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) } } else { if info, err := os.Stat(newConfig.KeyPath); err != nil || info.Mode() & os.ModeDir != os.ModeDir { newConfig.KeyPath = "./" configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid key path")) } } numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { newConfig.numDoors = uint8(numDoors) } newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } p.config = newConfig p.configUpdates++ p.initDoors() p.API.LogInfo(fmt.Sprintf("%d: Config: %d %s", p.configUpdates, p.config.numDoors, p.config.WatchPath)) if configErr != nil { p.postAdminChannel(configErr.Error()) } p.confChangedEvent() return configErr } func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) return } for { p.configLock.Lock() numDoors := p.config.numDoors watchPath := p.config.WatchPath p.configLock.Unlock() _ = numDoors watcher, err := fsnotify.NewWatcher() if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't make watcher").Error()) time.Sleep(60) continue } err = watcher.Add(watchPath) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't watch WatchPath").Error()) } run := true for ;run; { select { case event, ok := <- watcher.Events: if (!ok) { p.API.LogError(errors.Wrap(err, "Couldn't get fsnotify event").Error()) time.Sleep(60) run = false } else { match := doorFile.FindStringSubmatch(event.Name) if match == nil { continue } id64, err := strconv.ParseUint(match[2], 10, 8) if err != nil { p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) continue } id := uint8(id64) if id < 1 || id > numDoors { continue } if event.Op & fsnotify.Write != fsnotify.Write && event.Op & fsnotify.Create != fsnotify.Create { continue } statusBytes, err := ioutil.ReadFile(event.Name) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) } status := strings.Trim(string(statusBytes), " \n\t") if len(status) <= 0 { continue } statusInt, err := strconv.ParseInt(status, 10, 32) if err != nil { p.API.LogError(errors.Wrap(err, "Status invalid " + status).Error()) continue } newStatus := Open if statusInt == 1 { newStatus = Closed } err = p.setDoorStatus(id, newStatus, true) if err != nil { p.API.LogError(errors.Wrap(err, fmt.Sprintf("Couldn't set door status %d %d", id, statusInt)).Error()) continue } } case <- p.configChanged: p.API.LogInfo(fmt.Sprintf("CONFIG CHANGED, RESTARTING %d", p.configUpdates)) run = false } } watcher.Close() } } func (p *BathroomMonitorPlugin) OnActivate() error { if USE_FSNOTIFY { go fileNotifyBasedLoop(p) } else { } return nil } func main() { plugin.ClientMain((&BathroomMonitorPlugin{}).init()) }