diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/mattermost/server/build_fs.sh b/mattermost/server/build_fs.sh new file mode 100755 index 0000000..e2b8f05 --- /dev/null +++ b/mattermost/server/build_fs.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_fs.go DEFS=fsnotify ./build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/mattermost/server/build_fs.sh b/mattermost/server/build_fs.sh new file mode 100755 index 0000000..e2b8f05 --- /dev/null +++ b/mattermost/server/build_fs.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_fs.go DEFS=fsnotify ./build.sh diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh new file mode 100755 index 0000000..7ccb125 --- /dev/null +++ b/mattermost/server/build_web.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_web.go ./build.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/mattermost/server/build_fs.sh b/mattermost/server/build_fs.sh new file mode 100755 index 0000000..e2b8f05 --- /dev/null +++ b/mattermost/server/build_fs.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_fs.go DEFS=fsnotify ./build.sh diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh new file mode 100755 index 0000000..7ccb125 --- /dev/null +++ b/mattermost/server/build_web.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go new file mode 100644 index 0000000..6384df5 --- /dev/null +++ b/mattermost/server/config_fs.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/mattermost/server/build_fs.sh b/mattermost/server/build_fs.sh new file mode 100755 index 0000000..e2b8f05 --- /dev/null +++ b/mattermost/server/build_fs.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_fs.go DEFS=fsnotify ./build.sh diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh new file mode 100755 index 0000000..7ccb125 --- /dev/null +++ b/mattermost/server/build_web.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go new file mode 100644 index 0000000..6384df5 --- /dev/null +++ b/mattermost/server/config_fs.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go new file mode 100644 index 0000000..c72b5c8 --- /dev/null +++ b/mattermost/server/config_web.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/mattermost/server/build_fs.sh b/mattermost/server/build_fs.sh new file mode 100755 index 0000000..e2b8f05 --- /dev/null +++ b/mattermost/server/build_fs.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_fs.go DEFS=fsnotify ./build.sh diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh new file mode 100755 index 0000000..7ccb125 --- /dev/null +++ b/mattermost/server/build_web.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go new file mode 100644 index 0000000..6384df5 --- /dev/null +++ b/mattermost/server/config_fs.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go new file mode 100644 index 0000000..c72b5c8 --- /dev/null +++ b/mattermost/server/config_web.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = false diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json new file mode 100644 index 0000000..1de221a --- /dev/null +++ b/mattermost/server/plugin_templ.json @@ -0,0 +1,18 @@ +{ + "id": "com.mattermost.bathroom", + "name": "Bathroom Monitor", + "server": { + "executable": "bathroom-linux-amd64" + }, + "settings_schema": { + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, +#ifdef fsnotify + {"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"}, +#else + {"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"}, +#endif + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"} + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1377554 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.swp diff --git a/mattermost/server/.gitignore b/mattermost/server/.gitignore deleted file mode 100644 index 1377554..0000000 --- a/mattermost/server/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.swp diff --git a/mattermost/server/bathroom-linux-amd64 b/mattermost/server/bathroom-linux-amd64 index 2f3a056..b43b3ed 100755 --- a/mattermost/server/bathroom-linux-amd64 +++ b/mattermost/server/bathroom-linux-amd64 Binary files differ diff --git a/mattermost/server/bathroom.go b/mattermost/server/bathroom.go index 87150be..52bafbe 100644 --- a/mattermost/server/bathroom.go +++ b/mattermost/server/bathroom.go @@ -8,11 +8,11 @@ "github.com/pkg/errors" "sync" "reflect" - "github.com/google/go-cmp/cmp" + _ "github.com/google/go-cmp/cmp" "os" - "github.com/fatih/structs" + _ "github.com/fatih/structs" "github.com/hashicorp/go-multierror" - "github.com/kr/pretty" + _ "github.com/kr/pretty" "strings" "regexp" "strconv" @@ -21,63 +21,173 @@ "io/ioutil" "html" "encoding/json" + "encoding/pem" + "crypto/rsa" + "crypto/x509" + "path" ) var userSplit *regexp.Regexp = regexp.MustCompile(`\s+(^|[^,])|\s*,\s*`) -type Config struct { - NumDoors string - WatchPath string - AdminUsers string - numDoors uint8 -} +const ( + Unknown = iota + Open + Closed +) -func (c *Config) configMap() map[string]interface{} { - var configMap map[string]interface{} = structs.Map(c) - var lowerConfigMap map[string]interface{} = make(map[string]interface{}) - for k, v := range(configMap) { - lowerConfigMap[strings.ToLower(k)] = fmt.Sprintf("%v", v) +func statusName(status int) (string, error) { + switch status { + case Unknown: + return "unknown", nil + case Open: + return "open", nil + case Closed: + return "closed", nil } - return lowerConfigMap + 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 - adminUsers []string configLock sync.RWMutex configChanged chan struct{} - doors map[uint8]bool 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.doors = make(map[uint8]bool) + p.doorLock.Lock() + defer p.doorLock.Unlock() + + p.doors = make(map[uint8]*Door) for i := uint8(0); i < p.config.numDoors; i++ { - p.doors[i] = false + 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) init() *BathroomMonitorPlugin { - p.config = &Config{NumDoors: "1", WatchPath: "./", numDoors: 1, AdminUsers:""} - p.configChanged = make(chan struct{}) +func (p *BathroomMonitorPlugin) setDoorStatus(id uint8, status int, report bool) error { + p.doorLock.Lock() + defer p.doorLock.Unlock() - p.initDoors() + 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 { - val := "open" - if s { - val = "closed" - } - write[fmt.Sprintf("%d", d + 1)] = val + statusStr, _ := statusName(s.status) + write[fmt.Sprintf("%d", d + 1)] = statusStr } if output, err := json.Marshal(write); err == nil { fmt.Fprint(w, string(output)) @@ -86,6 +196,8 @@ } return } + if r.URL.Path == "/status-update" { + } http.NotFound(w, r) } @@ -96,32 +208,14 @@ } } -func (p *BathroomMonitorPlugin) checkCorrectConfig() { - var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - var goodConfig map[string]interface{} = p.config.configMap() - if !cmp.Equal(currentConfig, goodConfig) { - p.API.LogInfo(fmt.Sprintf("%d: Saving un-matching config\n%# v\n%# v", p.configUpdates, pretty.Formatter(currentConfig), pretty.Formatter(goodConfig))) - p.API.SavePluginConfig(goodConfig) - } -} - -func (p *BathroomMonitorPlugin) getLowerCasePluginConfig() map[string]interface{} { - var currentConfig map[string]interface{} = p.API.GetPluginConfig() - var lowerConfig map[string]interface{} = make(map[string]interface{}) - for k, v := range(currentConfig) { - lowerConfig[strings.ToLower(k)] = fmt.Sprintf("%v", v) - } - return lowerConfig -} - 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.adminUsers)) - for _, un := range(p.adminUsers) { + 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) @@ -172,14 +266,6 @@ p.configLock.Lock() defer p.configLock.Unlock() - //var currentConfig map[string]interface{} = p.getLowerCasePluginConfig() - //if cmp.Equal(currentConfig, p.config.configMap()) { - // p.API.LogInfo(fmt.Sprintf("%d: Skipping matching config", p.configUpdates)) - // return nil - //} - - //defer p.checkCorrectConfig() - 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)) @@ -187,46 +273,47 @@ return newErr } - //p.API.LogInfo(fmt.Sprintf("Loaded %t %t", newConfig != nil, p.config != nil)) - if newConfig == nil || newConfig == p.config || reflect.ValueOf(*newConfig).NumField() == 0 { p.API.LogInfo("Passed same config, or empty?") return nil; } - //p.API.LogInfo("Checked dupe") var configErr error = nil - if _, err := os.Stat(newConfig.WatchPath); err != nil { - newConfig.WatchPath = "./" - configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid watch path")) + + 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")) + } } - //p.API.LogInfo("Checked path") numDoors, err := strconv.ParseUint(newConfig.NumDoors, 10, 8) if err != nil { - //p.API.LogError("Bad doors! " + err.Error()) newConfig.NumDoors = "1" newConfig.numDoors = 1 configErr = multierror.Append(configErr, errors.Wrap(err, "Invalid number of doors")) } else { - //p.API.LogError(fmt.Sprintf("Good doors! %d", numDoors)) newConfig.numDoors = uint8(numDoors) } - p.config = newConfig - - p.adminUsers = make([]string, 0, 4) + newConfig.adminUsers = make([]string, 0, 4) split := userSplit.Split(newConfig.AdminUsers, -1) for _, un := range split { trimmed := strings.Trim(un, ", \t\n") if trimmed != "" { - p.adminUsers = append(p.adminUsers, trimmed) + newConfig.adminUsers = append(newConfig.adminUsers, trimmed) } } + p.config = newConfig p.configUpdates++ p.initDoors() @@ -242,7 +329,7 @@ return configErr } -func watchLoop(p *BathroomMonitorPlugin) { +func fileNotifyBasedLoop(p *BathroomMonitorPlugin) { doorFile, err := regexp.Compile(`(^|/)door(\d+)$`) if err != nil { p.API.LogError(errors.Wrap(err, "Couldn't compile regex").Error()) @@ -280,28 +367,51 @@ run = false } else { match := doorFile.FindStringSubmatch(event.Name) - if match != nil { - id64, err := strconv.ParseUint(match[2], 10, 8) - _ = id64 - if err == nil { - id := uint8(id64) - if id >= 1 && id <= numDoors { - if event.Op & fsnotify.Write == fsnotify.Write || event.Op & fsnotify.Create == fsnotify.Create { - if statusBytes, err := ioutil.ReadFile(event.Name); err == nil { - status := string(statusBytes) - if len(status) > 0 { - p.postAdminChannel("STATUS: " + event.Name + " = " + status) - doorSensor := strings.Trim(status, " \t\n") == "1" - p.doors[id - 1] = doorSensor - } - } else { - p.API.LogError(errors.Wrap(err, "Couldn't read door file").Error()) - } - } - } - } else { - p.API.LogError(errors.Wrap(err, "ParseUint error").Error()) - } + 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: @@ -316,7 +426,11 @@ func (p *BathroomMonitorPlugin) OnActivate() error { - go watchLoop(p) + if USE_FSNOTIFY { + go fileNotifyBasedLoop(p) + } else { + } + return nil } diff --git a/mattermost/server/bathroom.tar.gz b/mattermost/server/bathroom.tar.gz index 407d374..a51c02d 100644 --- a/mattermost/server/bathroom.tar.gz +++ b/mattermost/server/bathroom.tar.gz Binary files differ diff --git a/mattermost/server/build.sh b/mattermost/server/build.sh index 161a39f..916a360 100755 --- a/mattermost/server/build.sh +++ b/mattermost/server/build.sh @@ -1,3 +1,4 @@ #! /usr/bin/env bash -GOOS=linux GOARCH=amd64 go build -o bathroom-linux-amd64 bathroom.go +filepp $( [[ ! -z "$DEFS" ]] && echo -D$DEFS ) plugin_templ.json > plugin.json +GOOS=linux GOARCH=amd64 go build $( [[ ! -z "$DEFS" ]] && echo -tags $DEFS ) -o bathroom-linux-amd64 bathroom.go $FILES diff --git a/mattermost/server/build_fs.sh b/mattermost/server/build_fs.sh new file mode 100755 index 0000000..e2b8f05 --- /dev/null +++ b/mattermost/server/build_fs.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_fs.go DEFS=fsnotify ./build.sh diff --git a/mattermost/server/build_web.sh b/mattermost/server/build_web.sh new file mode 100755 index 0000000..7ccb125 --- /dev/null +++ b/mattermost/server/build_web.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash + +FILES=config_web.go ./build.sh diff --git a/mattermost/server/config_fs.go b/mattermost/server/config_fs.go new file mode 100644 index 0000000..6384df5 --- /dev/null +++ b/mattermost/server/config_fs.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = true diff --git a/mattermost/server/config_web.go b/mattermost/server/config_web.go new file mode 100644 index 0000000..c72b5c8 --- /dev/null +++ b/mattermost/server/config_web.go @@ -0,0 +1,3 @@ +package main + +const USE_FSNOTIFY = false diff --git a/mattermost/server/plugin_templ.json b/mattermost/server/plugin_templ.json new file mode 100644 index 0000000..1de221a --- /dev/null +++ b/mattermost/server/plugin_templ.json @@ -0,0 +1,18 @@ +{ + "id": "com.mattermost.bathroom", + "name": "Bathroom Monitor", + "server": { + "executable": "bathroom-linux-amd64" + }, + "settings_schema": { + "settings": [ + {"key":"NumDoors", "display_name":"Number of Door Sensors", "type":"text", "default":"1", "help_text":"How many Pis"}, +#ifdef fsnotify + {"key":"WatchPath", "display_name":"Pi status folder", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"}, +#else + {"key":"KeyPath", "display_name":"", "type":"text", "default":"./", "help_text":"Path to watch for doorX files updated from PHP"}, +#endif + {"key":"AdminUsers", "display_name":"Plugin admin users", "type":"text", "default":"", "help_text":"Space- or comma-separated list of users to notify with plugin debug message"} + ] + } +} diff --git a/mattermost/webapp/.gitignore b/mattermost/webapp/.gitignore index 1bd7226..3c3629e 100644 --- a/mattermost/webapp/.gitignore +++ b/mattermost/webapp/.gitignore @@ -1,2 +1 @@ node_modules -*.swp