aboutsummaryrefslogblamecommitdiff
path: root/web.go
blob: 9983855e9fb29c67e027e43dba2177be20da2752 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11




                       
                  
                 



                                     
                                        
                                    
 
                                                           





                                                       
                                     











                                                            


                                                       

















































                                                                                
                           









                                                                                                          

                                          
                                                   




















































                                                                                                            



                                                                                                                


                                         
 





















                                                                                  
 





























































                                                                                                              
                 
         
 






















































                                                                                                               

                 




                                                           























                                                                                                              
 
package main

import (
	"crypto/rand"
	"html/template"
	"net/http"
	"strconv"
	"strings"

	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"
	log "github.com/sirupsen/logrus"
	"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)
	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)
	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 []*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

		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],
	}
	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
			data.Config[field] = strings.Join(r.Form[field], "")
			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)
}