An easy to set-up simple pupetting bridge for talking to all your IM contacts through Matrix
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

357 lines
8.2 KiB

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