package main import ( "context" "html/template" "net" "net/http" "strconv" "strings" "github.com/gorilla/mux" "github.com/gorilla/sessions" log "github.com/sirupsen/logrus" "golang.org/x/crypto/argon2" "golang.org/x/crypto/blake2b" "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" "git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib" ) const SESSION_NAME = "easybridge_session" var sessionsStore sessions.Store = nil var userKeys = map[string]*[32]byte{} func StartWeb(errch chan error, ctx context.Context) *http.Server { session_key := blake2b.Sum256([]byte(config.SessionKey)) sessionsStore = sessions.NewCookieStore(session_key[:]) r := mux.NewRouter() r.HandleFunc("/", handleHome) r.HandleFunc("/logout", handleLogout) r.HandleFunc("/add/{protocol}", handleAdd) r.HandleFunc("/edit/{account}", handleEdit) r.HandleFunc("/delete/{account}", handleDelete) staticfiles := http.FileServer(http.Dir("static")) r.Handle("/static/{file:.*}", http.StripPrefix("/static/", staticfiles)) log.Printf("Starting web UI HTTP server on %s", config.WebBindAddr) web_server := &http.Server{ Addr: config.WebBindAddr, Handler: logRequest(r), BaseContext: func(net.Listener) context.Context { return ctx }, } go func() { err := web_server.ListenAndServe() if err != nil { errch <- err } }() return web_server } func logRequest(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL) handler.ServeHTTP(w, r) }) } // ---- type LoginInfo struct { MxId string } func checkLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { var login_info *LoginInfo session, err := sessionsStore.Get(r, SESSION_NAME) if err == nil { mxid, ok := session.Values["login_mxid"].(string) user_key, ok2 := session.Values["login_user_key"].([]byte) if ok && ok2 { if _, had_key := userKeys[mxid]; !had_key && len(user_key) == 32 { key := new([32]byte) copy(key[:], user_key) userKeys[mxid] = key LoadDbAccounts(mxid, key) } login_info = &LoginInfo{ MxId: mxid, } } } if login_info == nil { login_info = handleLogin(w, r) } return login_info } // ---- type HomeData struct { Login *LoginInfo Accounts []*Account } func handleHome(w http.ResponseWriter, r *http.Request) { templateHome := template.Must(template.ParseFiles("templates/layout.html", "templates/home.html")) login := checkLogin(w, r) if login == nil { return } templateHome.Execute(w, &HomeData{ Login: login, Accounts: ListAccounts(login.MxId), }) } func handleLogout(w http.ResponseWriter, r *http.Request) { session, err := sessionsStore.Get(r, SESSION_NAME) if err != nil { session, _ = sessionsStore.New(r, SESSION_NAME) } delete(session.Values, "login_mxid") err = session.Save(r, w) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/", http.StatusFound) } type LoginFormData struct { Username string WrongPass bool ErrorMessage string MatrixDomain string } func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { templateLogin := template.Must(template.ParseFiles("templates/layout.html", "templates/login.html")) data := &LoginFormData{ MatrixDomain: config.MatrixDomain, } if r.Method == "GET" { templateLogin.Execute(w, data) return nil } else if r.Method == "POST" { r.ParseForm() username := strings.Join(r.Form["username"], "") password := strings.Join(r.Form["password"], "") cli := mxlib.NewClient(config.Server, "") mxid, err := cli.PasswordLogin(username, password, "EZBRIDGE", "Easybridge") if err != nil { data.Username = username data.ErrorMessage = err.Error() templateLogin.Execute(w, data) return nil } key := new([32]byte) key_slice := argon2.IDKey([]byte(password), []byte("EZBRIDGE account store"), 3, 64*1024, 4, 32) copy(key[:], key_slice) userKeys[mxid] = key SaveDbAccounts(mxid, key) LoadDbAccounts(mxid, key) // Successfully logged in, save it to session session, err := sessionsStore.Get(r, SESSION_NAME) if err != nil { session, _ = sessionsStore.New(r, SESSION_NAME) } session.Values["login_mxid"] = mxid session.Values["login_user_key"] = key_slice err = session.Save(r, w) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return nil } return &LoginInfo{ MxId: mxid, } } else { http.Error(w, "Unsupported method", http.StatusBadRequest) return nil } } // ---- func handleAdd(w http.ResponseWriter, r *http.Request) { login := checkLogin(w, r) if login == nil { return } protocol := mux.Vars(r)["protocol"] configForm(w, r, login, "", protocol, map[string]string{}) } func handleEdit(w http.ResponseWriter, r *http.Request) { login := checkLogin(w, r) if login == nil { return } account := mux.Vars(r)["account"] acct := FindAccount(login.MxId, account) if acct == nil { http.Error(w, "No such account", http.StatusNotFound) return } configForm(w, r, login, account, acct.Protocol, acct.Config) } type ConfigFormData struct { ErrorMessage string Name string NameEditable bool InvalidName bool Protocol string Config map[string]string Errors map[string]string Schema connector.ConfigSchema } func configForm(w http.ResponseWriter, r *http.Request, login *LoginInfo, name string, protocol string, prevConfig map[string]string) { templateConfig := template.Must(template.ParseFiles("templates/layout.html", "templates/config.html")) data := &ConfigFormData{ Name: name, NameEditable: (name == ""), Protocol: protocol, Config: map[string]string{}, Errors: map[string]string{}, Schema: connector.Protocols[protocol].Schema, } for k, v := range prevConfig { data.Config[k] = v } for _, sch := range data.Schema { if _, ok := data.Config[sch.Name]; !ok && sch.Default != "" { data.Config[sch.Name] = sch.Default } } if r.Method == "POST" { ok := true r.ParseForm() if data.NameEditable { data.Name = strings.Join(r.Form["name"], "") if data.Name == "" { ok = false data.InvalidName = true } } for _, schema := range data.Schema { field := schema.Name old_value := data.Config[field] data.Config[field] = strings.Join(r.Form[field], "") if schema.IsPassword { if data.Config[field] == "" { data.Config[field] = old_value } } else if data.Config[field] == "" { if schema.Required { ok = false data.Errors[field] = "This field is required" } } else if schema.FixedValue != "" { if data.Config[field] != schema.FixedValue { ok = false data.Errors[field] = "This field must be equal to " + schema.FixedValue } } else if schema.IsBoolean { if data.Config[field] != "false" && data.Config[field] != "true" { ok = false data.Errors[field] = "This field must be 'true' or 'false'" } } else if schema.IsNumeric { _, err := strconv.Atoi(data.Config[field]) if err != nil { ok = false data.Errors[field] = "This field must be a valid number" } } } if ok { var entry DbAccountConfig db.Where(&DbAccountConfig{ MxUserID: login.MxId, Name: data.Name, }).Assign(&DbAccountConfig{ Protocol: protocol, Config: encryptAccountConfig(data.Config, userKeys[login.MxId]), }).FirstOrCreate(&entry) err := SetAccount(login.MxId, data.Name, protocol, data.Config) if err == nil { http.Redirect(w, r, "/", http.StatusFound) return } data.ErrorMessage = err.Error() } } templateConfig.Execute(w, data) } func handleDelete(w http.ResponseWriter, r *http.Request) { templateDelete := template.Must(template.ParseFiles("templates/layout.html", "templates/delete.html")) login := checkLogin(w, r) if login == nil { return } account := mux.Vars(r)["account"] if r.Method == "POST" { r.ParseForm() del := strings.Join(r.Form["delete"], "") if del == "Yes" { RemoveAccount(login.MxId, account) db.Where(&DbAccountConfig{ MxUserID: login.MxId, Name: account, }).Delete(&DbAccountConfig{}) http.Redirect(w, r, "/", http.StatusFound) return } } templateDelete.Execute(w, account) }