easybridge/web.go

236 lines
5.4 KiB
Go

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)
}
}
}