package main import ( "crypto/rand" "html/template" "log" "net/http" "strings" "github.com/gorilla/mux" "github.com/gorilla/sessions" "golang.org/x/crypto/argon2" "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() { session_key := make([]byte, 32) n, err := rand.Read(session_key) if err != nil || n != 32 { log.Fatal(err) } sessionsStore = sessions.NewCookieStore(session_key) r := mux.NewRouter() r.HandleFunc("/", handleHome) r.HandleFunc("/logout", handleLogout) 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) go func() { err = http.ListenAndServe(config.WebBindAddr, logRequest(r)) if err != nil { log.Fatal("Cannot start http server: ", err) } }() } 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"] if ok { login_info = &LoginInfo{ MxId: mxid.(string), } } } if login_info == nil { login_info = handleLogin(w, r) } return login_info } // ---- type HomeData struct { Login *LoginInfo Accounts map[string]*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 } accountsLock.Lock() defer accountsLock.Unlock() templateHome.Execute(w, &HomeData{ Login: login, Accounts: registeredAccounts[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 syncDbAccounts(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 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 syncDbAccounts(mxid string, key *[32]byte) { accountsLock.Lock() defer accountsLock.Unlock() // 1. Save all accounts that we have var accounts map[string]*Account if accts, ok := registeredAccounts[mxid]; ok { accounts = accts for name, acct := range accts { var entry DbAccountConfig db.Where(&DbAccountConfig{ MxUserID: mxid, Name: name, }).Assign(&DbAccountConfig{ Protocol: acct.Protocol, Config: encryptAccountConfig(acct.Config, key), }).FirstOrCreate(&entry) } } else { accounts = make(map[string]*Account) registeredAccounts[mxid] = accounts } // 2. Load and start missing accounts var allAccounts []DbAccountConfig db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts) for _, acct := range allAccounts { if _, ok := accounts[acct.Name]; !ok { config, err := decryptAccountConfig(acct.Config, key) if err != nil { ezbrSystemSendf("Could not decrypt stored configuration for account %s", acct.Name) continue } conn := createConnector(acct.Protocol) if conn == nil { ezbrSystemSendf("Could not create connector for protocol %s", acct.Protocol) continue } account := &Account{ MatrixUser: mxid, AccountName: acct.Name, Protocol: acct.Protocol, Config: config, Conn: conn, JoinedRooms: map[connector.RoomID]bool{}, } conn.SetHandler(account) accounts[acct.Name] = account go account.connect(config) } } }