diff --git a/config/config.go b/config/config.go index abfbdaa..ab61451 100644 --- a/config/config.go +++ b/config/config.go @@ -22,7 +22,16 @@ import ( type Config struct { *sqlx.DB - DBFile string + DBFile string + secrets map[string]Secret +} + +// Secret is a config value that is loaded permanently and not ever displayed +type Secret struct { + // Key is the key field of the table + Key string `db:"key"` + // Value represents the secret that must not be shared + Value string `db:"value"` } // GetFloat64 returns the config value for a string key @@ -85,6 +94,9 @@ func (c *Config) GetString(key, fallback string) string { if v, found := os.LookupEnv(envkey(key)); found { return v } + if v, found := c.secrets[key]; found { + return v.Value + } var configValue string q := `select value from config where key=?` err := c.DB.Get(&configValue, q, key) @@ -162,6 +174,33 @@ func (c *Config) Set(key, value string) error { return nil } +func (c *Config) RefreshSecrets() error { + q := `select key, value from secrets` + var secrets []Secret + err := c.Select(&secrets, q) + if err != nil { + return err + } + secretMap := map[string]Secret{} + for _, s := range secrets { + secretMap[s.Key] = s + } + c.secrets = secretMap + return nil +} + +func (c *Config) GetAllSecrets() map[string]Secret { + return c.secrets +} + +func (c *Config) SecretKeys() []string { + keys := []string{} + for k := range c.secrets { + keys = append(keys, k) + } + return keys +} + func (c *Config) SetMap(key string, values map[string]string) error { b, err := json.Marshal(values) if err != nil { @@ -199,7 +238,8 @@ func ReadConfig(dbpath string) *Config { log.Fatal().Err(err) } c := Config{ - DBFile: dbpath, + DBFile: dbpath, + secrets: map[string]Secret{}, } c.DB = sqlDB @@ -208,7 +248,19 @@ func ReadConfig(dbpath string) *Config { value string, primary key (key) );`); err != nil { - panic(err) + log.Fatal().Err(err).Msgf("failed to initialize config") + } + + if _, err := c.Exec(`create table if not exists secrets ( + key string, + value string, + primary key (key) + );`); err != nil { + log.Fatal().Err(err).Msgf("failed to initialize config") + } + + if err := c.RefreshSecrets(); err != nil { + log.Fatal().Err(err).Msgf("failed to initialize config") } log.Info().Msgf("catbase is running.") diff --git a/main.go b/main.go index cd0004d..063438c 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/velour/catbase/plugins/giphy" "github.com/velour/catbase/plugins/last" "github.com/velour/catbase/plugins/rest" + "github.com/velour/catbase/plugins/secrets" "github.com/velour/catbase/plugins/achievements" "github.com/velour/catbase/plugins/aoc" @@ -121,6 +122,7 @@ func main() { b := bot.New(c, client) b.AddPlugin(admin.New(b)) + b.AddPlugin(secrets.New(b)) b.AddPlugin(giphy.New(b)) b.AddPlugin(emojifyme.New(b)) b.AddPlugin(last.New(b)) diff --git a/plugins/rest/rest.go b/plugins/rest/rest.go index 6f29eed..2950f83 100644 --- a/plugins/rest/rest.go +++ b/plugins/rest/rest.go @@ -15,6 +15,7 @@ import ( "text/template" "github.com/itchyny/gojq" + "github.com/rs/zerolog/log" "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot" @@ -235,9 +236,14 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler { return false } values := bot.RegexValues{} + for _, s := range p.b.Config().GetAllSecrets() { + values[s.Key] = s.Value + } + log.Debug().Interface("values", values).Msgf("secrets") for k := range r.Values { values[k] = url.QueryEscape(r.Values[k]) } + log.Debug().Interface("values", values).Msgf("r.Values") urlStr := w.URL.String() parse, err := template.New(urlStr).Parse(urlStr) if p.handleErr(err, r) { @@ -249,6 +255,10 @@ func (p *RestPlugin) mkHandler(w *wire) bot.ResponseHandler { return true } newURL, err := url.Parse(buf.String()) + log.Debug(). + Interface("values", values). + Str("URL", buf.String()). + Msg("Querying URL with values") if p.handleErr(err, r) { return true } diff --git a/plugins/secrets/secrets.go b/plugins/secrets/secrets.go new file mode 100644 index 0000000..a3adf70 --- /dev/null +++ b/plugins/secrets/secrets.go @@ -0,0 +1,154 @@ +package secrets + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/jmoiron/sqlx" + "github.com/rs/zerolog/log" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/config" +) + +type SecretsPlugin struct { + b bot.Bot + c *config.Config + db *sqlx.DB +} + +func New(b bot.Bot) *SecretsPlugin { + p := &SecretsPlugin{ + b: b, + c: b.Config(), + db: b.DB(), + } + p.registerWeb() + return p +} + +func (p *SecretsPlugin) registerWeb() { + http.HandleFunc("/secrets/add", p.handleRegister) + http.HandleFunc("/secrets/remove", p.handleRemove) + http.HandleFunc("/secrets/all", p.handleAll) + http.HandleFunc("/secrets/test", func(w http.ResponseWriter, r *http.Request) { + value := r.URL.Query().Get("test") + j, _ := json.Marshal(map[string]string{"value": value}) + w.Write(j) + }) + http.HandleFunc("/secrets", p.handleIndex) + p.b.RegisterWeb("/secrets", "Secrets") +} + +func (p *SecretsPlugin) registerSecret(key, value string) error { + q := `insert into secrets (key, value) values (?, ?)` + _, err := p.db.Exec(q, key, value) + if err != nil { + return err + } + return p.c.RefreshSecrets() +} + +func (p *SecretsPlugin) removeSecret(key string) error { + q := `delete from secrets where key=?` + _, err := p.db.Exec(q, key) + if err != nil { + return err + } + return p.c.RefreshSecrets() +} + +func (p *SecretsPlugin) updateSecret(key, value string) error { + q := `update secrets set value=? where key=?)` + _, err := p.db.Exec(q, value, key) + if err != nil { + return err + } + return p.c.RefreshSecrets() +} + +func mkCheckError(w http.ResponseWriter) func(error) bool { + return func(err error) bool { + if err != nil { + log.Error().Stack().Err(err).Msgf("secret failed") + w.WriteHeader(500) + e, _ := json.Marshal(err) + w.Write(e) + return true + } + return false + } +} + +func checkMethod(method string, w http.ResponseWriter, r *http.Request) bool { + if r.Method != method { + w.WriteHeader(405) + fmt.Fprintf(w, "Incorrect HTTP method") + return true + } + return false +} + +func (p *SecretsPlugin) sendKeys(w http.ResponseWriter, r *http.Request) { + checkError := mkCheckError(w) + log.Debug().Msgf("Keys before refresh: %v", p.c.SecretKeys()) + err := p.c.RefreshSecrets() + log.Debug().Msgf("Keys after refresh: %v", p.c.SecretKeys()) + if checkError(err) { + return + } + keys, err := json.Marshal(p.c.SecretKeys()) + if checkError(err) { + return + } + w.WriteHeader(200) + w.Write(keys) +} + +func (p *SecretsPlugin) handleAll(w http.ResponseWriter, r *http.Request) { + p.sendKeys(w, r) +} + +func (p *SecretsPlugin) handleRegister(w http.ResponseWriter, r *http.Request) { + log.Debug().Msgf("handleRegister") + if checkMethod(http.MethodPost, w, r) { + log.Debug().Msgf("failed post %s", r.Method) + return + } + checkError := mkCheckError(w) + decoder := json.NewDecoder(r.Body) + secret := config.Secret{} + err := decoder.Decode(&secret) + log.Debug().Msgf("decoding: %s", err) + if checkError(err) { + return + } + log.Debug().Msgf("Secret: %s", secret) + err = p.registerSecret(secret.Key, secret.Value) + if checkError(err) { + return + } + p.sendKeys(w, r) +} + +func (p *SecretsPlugin) handleRemove(w http.ResponseWriter, r *http.Request) { + if checkMethod(http.MethodDelete, w, r) { + return + } + checkError := mkCheckError(w) + decoder := json.NewDecoder(r.Body) + secret := config.Secret{} + err := decoder.Decode(&secret) + if checkError(err) { + return + } + err = p.removeSecret(secret.Key) + if checkError(err) { + return + } + p.sendKeys(w, r) +} + +func (p *SecretsPlugin) handleIndex(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(indexTpl)) +} diff --git a/plugins/secrets/web.go b/plugins/secrets/web.go new file mode 100644 index 0000000..207142f --- /dev/null +++ b/plugins/secrets/web.go @@ -0,0 +1,125 @@ +package secrets + +var indexTpl = ` + + +
+ + + + + + + + + + + + + + +