catbase/plugins/admin/web.go

196 lines
4.7 KiB
Go

package admin
import (
"crypto/md5"
"crypto/rand"
"embed"
"encoding/json"
"fmt"
bh "github.com/timshannon/bolthold"
"github.com/velour/catbase/config"
"io/ioutil"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"
"golang.org/x/crypto/bcrypt"
)
//go:embed *.html
var embeddedFS embed.FS
func (p *AdminPlugin) registerWeb() {
r := chi.NewRouter()
r.HandleFunc("/api", p.handleVarsAPI)
r.HandleFunc("/", p.handleVars)
p.bot.RegisterWebName(r, "/vars", "Variables")
r = chi.NewRouter()
r.HandleFunc("/verify", p.handleAppPassCheck)
r.HandleFunc("/api", p.handleAppPassAPI)
r.HandleFunc("/", p.handleAppPass)
p.bot.RegisterWebName(r, "/apppass", "App Pass")
}
func (p *AdminPlugin) handleAppPass(w http.ResponseWriter, r *http.Request) {
index, _ := embeddedFS.ReadFile("apppass.html")
w.Write(index)
}
type PassEntry struct {
ID int64 `json:"id"`
Secret string `json:"secret"`
// Should be null unless inserting a new entry
Pass string `json:"pass"`
EncodedPass []byte `db:"encoded_pass" json:"encodedPass"`
Cost int `json:"cost"`
}
func (p *PassEntry) EncodePass() {
encoded, err := bcrypt.GenerateFromPassword([]byte(p.Pass), p.Cost)
if err != nil {
log.Error().Err(err).Msg("could not hash password")
}
p.EncodedPass = encoded
}
func (p *PassEntry) Compare(pass string) bool {
if err := bcrypt.CompareHashAndPassword(p.EncodedPass, []byte(pass)); err != nil {
log.Error().Err(err).Msg("failure to match password")
return false
}
return true
}
func (p *AdminPlugin) handleAppPassCheck(w http.ResponseWriter, r *http.Request) {
req := struct {
Secret string `json:"secret"`
Password string `json:"password"`
}{}
body, _ := ioutil.ReadAll(r.Body)
_ = json.Unmarshal(body, &req)
if p.bot.CheckPassword(req.Secret, req.Password) {
w.WriteHeader(204)
} else {
w.WriteHeader(403)
}
}
func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
req := struct {
Password string `json:"password"`
PassEntry PassEntry `json:"passEntry"`
}{}
body, _ := ioutil.ReadAll(r.Body)
_ = json.Unmarshal(body, &req)
if req.PassEntry.Secret == "" {
writeErr(w, fmt.Errorf("missing secret"))
return
}
if req.Password == "" || !p.bot.CheckPassword(req.PassEntry.Secret, req.Password) {
writeErr(w, fmt.Errorf("missing or incorrect password"))
return
}
switch r.Method {
case http.MethodPut:
if req.PassEntry.Secret == "" {
writeErr(w, fmt.Errorf("missing secret"))
return
}
if string(req.PassEntry.Pass) == "" {
c := 10
b := make([]byte, c)
_, err := rand.Read(b)
if err != nil {
fmt.Println("error:", err)
return
}
req.PassEntry.Pass = fmt.Sprintf("%x", md5.Sum(b))
}
req.PassEntry.EncodePass()
check := bcrypt.CompareHashAndPassword(req.PassEntry.EncodedPass, []byte(req.PassEntry.Pass))
entry := PassEntry{
Secret: req.PassEntry.Secret,
EncodedPass: req.PassEntry.EncodedPass,
Cost: req.PassEntry.Cost,
Pass: "",
}
log.Debug().
Str("secret", req.PassEntry.Secret).
Str("encoded", string(req.PassEntry.EncodedPass)).
Str("password", string(req.PassEntry.Pass)).
Interface("check", check).
Msg("debug pass creation")
err := p.store.Insert(bh.NextSequence(), &entry)
if err != nil {
writeErr(w, err)
return
}
j, _ := json.Marshal(req.PassEntry)
fmt.Fprint(w, string(j))
return
case http.MethodDelete:
if req.PassEntry.ID <= 0 {
writeErr(w, fmt.Errorf("missing ID"))
return
}
err := p.store.Delete(req.PassEntry.ID, PassEntry{})
if err != nil {
writeErr(w, err)
return
}
}
passEntries := []PassEntry{}
err := p.store.Find(&passEntries, bh.Where("secret").Eq(req.PassEntry.Secret))
if err != nil {
writeErr(w, err)
return
}
j, _ := json.Marshal(passEntries)
_, _ = fmt.Fprint(w, string(j))
}
func writeErr(w http.ResponseWriter, err error) {
log.Error().Err(err).Msg("apppass error")
j, _ := json.Marshal(struct {
Err string `json:"err"`
}{
err.Error(),
})
w.WriteHeader(400)
fmt.Fprint(w, string(j))
}
func (p *AdminPlugin) handleVars(w http.ResponseWriter, r *http.Request) {
index, _ := embeddedFS.ReadFile("vars.html")
w.Write(index)
}
func (p *AdminPlugin) handleVarsAPI(w http.ResponseWriter, r *http.Request) {
configEntries := []config.Value{}
err := p.store.Find(&configEntries, &bh.Query{})
if err != nil {
log.Error().
Err(err).
Msg("Error getting config entries.")
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
for i, e := range configEntries {
if strings.Contains(e.Value, ";;") {
e.Value = strings.ReplaceAll(e.Value, ";;", ", ")
e.Value = fmt.Sprintf("[%s]", e.Value)
configEntries[i] = e
}
}
j, _ := json.Marshal(configEntries)
fmt.Fprintf(w, "%s", j)
}