2020-02-26 16:45:25 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-03-04 16:44:22 +00:00
|
|
|
"context"
|
2020-02-26 16:45:25 +00:00
|
|
|
"html/template"
|
2020-03-04 16:44:22 +00:00
|
|
|
"net"
|
2020-02-26 16:45:25 +00:00
|
|
|
"net/http"
|
2020-02-26 21:49:27 +00:00
|
|
|
"strconv"
|
2020-02-26 16:45:25 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/gorilla/sessions"
|
2020-02-26 22:08:25 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2020-02-26 19:21:32 +00:00
|
|
|
"golang.org/x/crypto/argon2"
|
2020-02-28 16:20:32 +00:00
|
|
|
"golang.org/x/crypto/blake2b"
|
2020-02-26 16:45:25 +00:00
|
|
|
|
2020-02-26 21:49:27 +00:00
|
|
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/connector"
|
2020-02-26 16:45:25 +00:00
|
|
|
"git.deuxfleurs.fr/Deuxfleurs/easybridge/mxlib"
|
|
|
|
)
|
|
|
|
|
|
|
|
const SESSION_NAME = "easybridge_session"
|
|
|
|
|
|
|
|
var sessionsStore sessions.Store = nil
|
2020-02-26 19:21:32 +00:00
|
|
|
var userKeys = map[string]*[32]byte{}
|
2020-02-26 16:45:25 +00:00
|
|
|
|
2020-03-04 16:44:22 +00:00
|
|
|
func StartWeb(errch chan error, ctx context.Context) *http.Server {
|
2020-02-28 16:20:32 +00:00
|
|
|
session_key := blake2b.Sum256([]byte(config.SessionKey))
|
|
|
|
sessionsStore = sessions.NewCookieStore(session_key[:])
|
2020-02-26 16:45:25 +00:00
|
|
|
|
|
|
|
r := mux.NewRouter()
|
|
|
|
r.HandleFunc("/", handleHome)
|
|
|
|
r.HandleFunc("/logout", handleLogout)
|
2020-02-26 21:49:27 +00:00
|
|
|
r.HandleFunc("/add/{protocol}", handleAdd)
|
|
|
|
r.HandleFunc("/edit/{account}", handleEdit)
|
|
|
|
r.HandleFunc("/delete/{account}", handleDelete)
|
2020-02-26 16:45:25 +00:00
|
|
|
|
|
|
|
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)
|
2020-03-02 20:51:13 +00:00
|
|
|
web_server := &http.Server{
|
|
|
|
Addr: config.WebBindAddr,
|
|
|
|
Handler: logRequest(r),
|
2020-03-04 16:44:22 +00:00
|
|
|
BaseContext: func(net.Listener) context.Context {
|
|
|
|
return ctx
|
|
|
|
},
|
2020-03-02 20:51:13 +00:00
|
|
|
}
|
2020-02-26 16:45:25 +00:00
|
|
|
go func() {
|
2020-03-02 20:51:13 +00:00
|
|
|
err := web_server.ListenAndServe()
|
2020-02-26 16:45:25 +00:00
|
|
|
if err != nil {
|
2020-02-28 16:42:15 +00:00
|
|
|
errch <- err
|
2020-02-26 16:45:25 +00:00
|
|
|
}
|
|
|
|
}()
|
2020-03-02 20:51:13 +00:00
|
|
|
|
|
|
|
return web_server
|
2020-02-26 16:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2020-02-28 16:20:32 +00:00
|
|
|
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)
|
|
|
|
}
|
2020-02-26 16:45:25 +00:00
|
|
|
login_info = &LoginInfo{
|
2020-02-28 16:20:32 +00:00
|
|
|
MxId: mxid,
|
2020-02-26 16:45:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if login_info == nil {
|
|
|
|
login_info = handleLogin(w, r)
|
|
|
|
}
|
|
|
|
|
|
|
|
return login_info
|
|
|
|
}
|
|
|
|
|
|
|
|
// ----
|
|
|
|
|
|
|
|
type HomeData struct {
|
|
|
|
Login *LoginInfo
|
2020-02-26 20:36:35 +00:00
|
|
|
Accounts []*Account
|
2020-02-26 16:45:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
2020-02-26 20:36:35 +00:00
|
|
|
Accounts: ListAccounts(login.MxId),
|
2020-02-26 16:45:25 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-02-26 19:21:32 +00:00
|
|
|
key := new([32]byte)
|
|
|
|
key_slice := argon2.IDKey([]byte(password), []byte("EZBRIDGE account store"), 3, 64*1024, 4, 32)
|
2020-02-28 16:20:32 +00:00
|
|
|
copy(key[:], key_slice)
|
2020-02-26 19:21:32 +00:00
|
|
|
userKeys[mxid] = key
|
2020-02-26 20:36:35 +00:00
|
|
|
|
|
|
|
SaveDbAccounts(mxid, key)
|
|
|
|
LoadDbAccounts(mxid, key)
|
2020-02-26 19:21:32 +00:00
|
|
|
|
2020-02-26 16:45:25 +00:00
|
|
|
// 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
|
2020-02-28 16:20:32 +00:00
|
|
|
session.Values["login_user_key"] = key_slice
|
2020-02-26 16:45:25 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-02-26 19:21:32 +00:00
|
|
|
|
2020-02-26 21:49:27 +00:00
|
|
|
// ----
|
|
|
|
|
|
|
|
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{},
|
2020-02-28 09:18:47 +00:00
|
|
|
Schema: connector.Protocols[protocol].Schema,
|
2020-02-26 21:49:27 +00:00
|
|
|
}
|
|
|
|
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
|
2020-02-26 20:36:35 +00:00
|
|
|
}
|
2020-02-26 21:49:27 +00:00
|
|
|
}
|
2020-02-26 19:21:32 +00:00
|
|
|
|
2020-02-26 21:49:27 +00:00
|
|
|
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
|
2020-02-27 09:40:52 +00:00
|
|
|
old_value := data.Config[field]
|
2020-02-26 21:49:27 +00:00
|
|
|
data.Config[field] = strings.Join(r.Form[field], "")
|
2020-02-27 09:40:52 +00:00
|
|
|
if schema.IsPassword {
|
|
|
|
if data.Config[field] == "" {
|
|
|
|
data.Config[field] = old_value
|
|
|
|
}
|
|
|
|
} else if data.Config[field] == "" {
|
2020-02-26 21:49:27 +00:00
|
|
|
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()
|
2020-02-26 19:21:32 +00:00
|
|
|
}
|
|
|
|
}
|
2020-02-26 21:49:27 +00:00
|
|
|
|
|
|
|
templateConfig.Execute(w, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleDelete(w http.ResponseWriter, r *http.Request) {
|
2020-02-26 22:08:25 +00:00
|
|
|
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)
|
2020-02-26 19:21:32 +00:00
|
|
|
}
|