diff options
author | Alex Auvolat <alex@adnab.me> | 2020-02-26 22:49:27 +0100 |
---|---|---|
committer | Alex Auvolat <alex@adnab.me> | 2020-02-26 22:49:27 +0100 |
commit | 8a5ed3f507d37c52e2a68a23ced6942cc752221d (patch) | |
tree | 14e85d2f6031d6b38ad34bb7b360df3918c1c9e4 | |
parent | 775fc7b2172a632587e82cd44b9d7400ca4f4f74 (diff) | |
download | easybridge-8a5ed3f507d37c52e2a68a23ced6942cc752221d.tar.gz easybridge-8a5ed3f507d37c52e2a68a23ced6942cc752221d.zip |
Initial ability to configure accounts from web interface
-rw-r--r-- | account.go | 22 | ||||
-rw-r--r-- | connector/config.go | 21 | ||||
-rw-r--r-- | connector/irc/config.go | 32 | ||||
-rw-r--r-- | connector/irc/irc.go | 24 | ||||
-rw-r--r-- | connector/mattermost/config.go | 52 | ||||
-rw-r--r-- | connector/mattermost/mattermost.go | 6 | ||||
-rw-r--r-- | connector/xmpp/config.go | 38 | ||||
-rw-r--r-- | connector/xmpp/xmpp.go | 13 | ||||
-rw-r--r-- | templates/config.html | 71 | ||||
-rw-r--r-- | templates/home.html | 43 | ||||
-rw-r--r-- | web.go | 139 |
11 files changed, 422 insertions, 39 deletions
@@ -34,6 +34,9 @@ func SetAccount(mxid string, name string, protocol string, config map[string]str accounts := registeredAccounts[mxid] if prev_acct, ok := accounts[name]; ok { + prev_acct.Conn.Close() + prev_acct.JoinedRooms = map[RoomID]bool{} + if protocol != prev_acct.Protocol { return fmt.Errorf("Wrong protocol") } @@ -112,6 +115,8 @@ func RemoveAccount(mxUser string, name string) { } } +// ---- + func SaveDbAccounts(mxid string, key *[32]byte) { accountsLock.Lock() defer accountsLock.Unlock() @@ -130,6 +135,23 @@ func SaveDbAccounts(mxid string, key *[32]byte) { } } +func LoadDbAccounts(mxid string, key *[32]byte) { + var allAccounts []DbAccountConfig + db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts) + for _, acct := range allAccounts { + config, err := decryptAccountConfig(acct.Config, key) + if err != nil { + ezbrSystemSendf("Could not decrypt stored configuration for account %s", acct.Name) + continue + } + + err = SetAccount(mxid, acct.Name, acct.Protocol, config) + if err != nil { + ezbrSystemSendf("Could not setup account %s: %s", acct.Name, err.Error()) + } + } +} + // ---- func (a *Account) ezbrMessagef(format string, args ...interface{}) { diff --git a/connector/config.go b/connector/config.go index e0fcf17..97e4556 100644 --- a/connector/config.go +++ b/connector/config.go @@ -43,3 +43,24 @@ func (c Configuration) GetBool(k string, deflt ...bool) (bool, error) { } return false, fmt.Errorf("Missing configuration key: %s", k) } + +// ---- + +type ConfigSchema []*ConfigEntry + +type ConfigEntry struct { + Name string + Description string + Default string + FixedValue string + Required bool + IsPassword bool + IsNumeric bool + IsBoolean bool +} + +var Protocols = map[string]ConfigSchema{} + +func Register(name string, schema ConfigSchema) { + Protocols[name] = schema +} diff --git a/connector/irc/config.go b/connector/irc/config.go new file mode 100644 index 0000000..26d9a63 --- /dev/null +++ b/connector/irc/config.go @@ -0,0 +1,32 @@ +package irc + +import ( + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" +) + +func init() { + Register("irc", ConfigSchema{ + &ConfigEntry{ + Name: "nick", + Description: "Nickname", + Required: true, + }, + &ConfigEntry{ + Name: "server", + Description: "Server", + Required: true, + }, + &ConfigEntry{ + Name: "port", + Description: "Port", + IsNumeric: true, + Default: "6667", + }, + &ConfigEntry{ + Name: "ssl", + Description: "Use SSL", + IsBoolean: true, + Default: "false", + }, + }) +} diff --git a/connector/irc/irc.go b/connector/irc/irc.go index 2ed3923..d69884e 100644 --- a/connector/irc/irc.go +++ b/connector/irc/irc.go @@ -127,6 +127,10 @@ func (irc *IRC) SetUserInfo(info *UserInfo) error { } func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error { + if irc.conn == nil { + return fmt.Errorf("Not connected") + } + ch, err := irc.checkRoomId(roomId) if err != nil { return err @@ -145,6 +149,10 @@ func (irc *IRC) SetRoomInfo(roomId RoomID, info *RoomInfo) error { } func (irc *IRC) Join(roomId RoomID) error { + if irc.conn == nil { + return fmt.Errorf("Not connected") + } + ch, err := irc.checkRoomId(roomId) if err != nil { return err @@ -155,6 +163,10 @@ func (irc *IRC) Join(roomId RoomID) error { } func (irc *IRC) Invite(userId UserID, roomId RoomID) error { + if irc.conn == nil { + return fmt.Errorf("Not connected") + } + who, err := irc.checkUserId(userId) if err != nil { return err @@ -174,6 +186,10 @@ func (irc *IRC) Invite(userId UserID, roomId RoomID) error { } func (irc *IRC) Leave(roomId RoomID) { + if irc.conn == nil { + return + } + ch, err := irc.checkRoomId(roomId) if err != nil { return @@ -183,6 +199,10 @@ func (irc *IRC) Leave(roomId RoomID) { } func (irc *IRC) Send(event *Event) error { + if irc.conn == nil { + return fmt.Errorf("Not connected") + } + // Workaround girc bug if event.Text[0] == ':' { event.Text = " " + event.Text @@ -231,7 +251,9 @@ func (irc *IRC) Send(event *Event) error { func (irc *IRC) Close() { conn := irc.conn irc.conn = nil - conn.Close() + if conn != nil { + conn.Close() + } } func (irc *IRC) connectLoop(c *girc.Client) { diff --git a/connector/mattermost/config.go b/connector/mattermost/config.go new file mode 100644 index 0000000..b7c4ba8 --- /dev/null +++ b/connector/mattermost/config.go @@ -0,0 +1,52 @@ +package mattermost + +import ( + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" +) + +func init() { + Register("mattermost", ConfigSchema{ + &ConfigEntry{ + Name: "server", + Description: "Server", + Required: true, + }, + &ConfigEntry{ + Name: "username", + Description: "Username", + Required: true, + }, + &ConfigEntry{ + Name: "password", + Description: "Password", + IsPassword: true, + }, + &ConfigEntry{ + Name: "token", + Description: "Authentification token (replaces password if set)", + }, + &ConfigEntry{ + Name: "teams", + Description: "Comma-separated list of teams to follow", + Required: true, + }, + &ConfigEntry{ + Name: "no_tls", + Description: "Disable SSL/TLS", + IsBoolean: true, + Default: "false", + }, + &ConfigEntry{ + Name: "initial_backlog", + Description: "Maximum number of messages to load when joining a channel", + IsNumeric: true, + Default: "1000", + }, + &ConfigEntry{ + Name: "initial_members", + Description: "Maximum number of members to load when joining a channel", + IsNumeric: true, + Default: "100", + }, + }) +} diff --git a/connector/mattermost/mattermost.go b/connector/mattermost/mattermost.go index 0b863fb..e3a6429 100644 --- a/connector/mattermost/mattermost.go +++ b/connector/mattermost/mattermost.go @@ -69,7 +69,7 @@ func (mm *Mattermost) Configure(c Configuration) error { return err } - mm.initial_members, err = c.GetInt("initial_members", 1000) + mm.initial_members, err = c.GetInt("initial_members", 100) if err != nil { return err } @@ -312,7 +312,9 @@ func (mm *Mattermost) Send(event *Event) error { } func (mm *Mattermost) Close() { - mm.conn.WsQuit = true + if mm.conn != nil { + mm.conn.WsQuit = true + } if mm.handlerStopChan != nil { mm.handlerStopChan <- true mm.handlerStopChan = nil diff --git a/connector/xmpp/config.go b/connector/xmpp/config.go new file mode 100644 index 0000000..6fd5f9b --- /dev/null +++ b/connector/xmpp/config.go @@ -0,0 +1,38 @@ +package xmpp + +import ( + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" +) + +func init() { + Register("xmpp", ConfigSchema{ + &ConfigEntry{ + Name: "jid", + Description: "JID", + Required: true, + }, + &ConfigEntry{ + Name: "password", + Description: "Password", + Required: true, + IsPassword: true, + }, + &ConfigEntry{ + Name: "nickname", + Description: "Nickname in MUCs", + Required: true, + }, + &ConfigEntry{ + Name: "port", + Description: "Port", + IsNumeric: true, + Default: "6667", + }, + &ConfigEntry{ + Name: "ssl", + Description: "Use SSL", + IsBoolean: true, + Default: "true", + }, + }) +} diff --git a/connector/xmpp/xmpp.go b/connector/xmpp/xmpp.go index 698016f..f1a75b2 100644 --- a/connector/xmpp/xmpp.go +++ b/connector/xmpp/xmpp.go @@ -55,11 +55,6 @@ func (xm *XMPP) Configure(c Configuration) error { // Parse and validate configuration var err error - xm.server, err = c.GetString("server") - if err != nil { - return err - } - xm.port, err = c.GetInt("port", 5222) if err != nil { return err @@ -78,9 +73,7 @@ func (xm *XMPP) Configure(c Configuration) error { if len(jid_parts) != 2 { return fmt.Errorf("Invalid JID: %s", xm.jid) } - if jid_parts[1] != xm.server { - return fmt.Errorf("JID %s not on server %s", xm.jid, xm.server) - } + xm.server = jid_parts[1] xm.jid_localpart = jid_parts[0] xm.nickname, _ = c.GetString("nickname", xm.jid_localpart) @@ -353,7 +346,9 @@ func (xm *XMPP) Send(event *Event) error { } func (xm *XMPP) Close() { - xm.conn.Close() + if xm.conn != nil { + xm.conn.Close() + } xm.conn = nil xm.connectorLoopNum += 1 } diff --git a/templates/config.html b/templates/config.html new file mode 100644 index 0000000..2d64444 --- /dev/null +++ b/templates/config.html @@ -0,0 +1,71 @@ +{{define "title"}}Account configuration |{{end}} + +{{define "body"}} +<div class="d-flex"> + <h4>Configure account</h4> + <a class="ml-auto btn btn-info" href="/">Go back</a> +</div> + +{{if .ErrorMessage}} + <div class="alert alert-danger mt-4">An error occurred. + <div style="font-size: 0.8em">{{ .ErrorMessage }}</div> + </div> +{{end}} + +<form method="POST" class="mt-4"> + <div class="form-group"> + <label for="name">Account name:</label> + <input type="text" {{if .NameEditable}}{{else}}disabled="disabled"{{end}} id="name" name="name" class="form-control" value="{{ .Name }}" /> + {{if .InvalidName}} + <div class="alert alert-warning">Invalid name (must not be empty)</div> + {{end}} + </div> + <div class="form-group"> + <label>Protocol:</label> + <input type="text" disabled="disabled" class="form-control" value="{{ .Protocol }}" /> + </div> + {{$config := .Config}} + {{$errors := .Errors}} + {{range $i, $schema := .Schema}} + <div class="form-group"> + <label for="{{$schema.Name}}">{{$schema.Description}}</label> + {{if $schema.FixedValue}} + <input type="text" + disabled="disabled" + class="form-control" + name="{{$schema.Name}}" + id="{{$schema.Name}}" + value="{{index $config $schema.Name}}" /> + {{else if $schema.IsBoolean}} + {{$value := index $config $schema.Name}} + <label for="{{$schema.Name}}-true"> + <input type="radio" name="{{$schema.Name}}" id="{{$schema.Name}}-true" value="true" {{if eq $value "true"}}checked="checked"{{end}} /> + Yes + </label> + <label for="{{$schema.Name}}-false"> + <input type="radio" name="{{$schema.Name}}" id="{{$schema.Name}}-false" value="false" {{if eq $value "false"}}checked="checked"{{end}} /> + No + </label> + {{else if $schema.IsPassword}} + <input type="password" + class="form-control" + name="{{$schema.Name}}" + id="{{$schema.Name}}" + value="{{index $config $schema.Name}}" /> + {{else}} + <input type="text" + class="form-control" + name="{{$schema.Name}}" + id="{{$schema.Name}}" + value="{{index $config $schema.Name}}" /> + {{end}} + {{$error := index $errors $schema.Name}} + {{if $error}} + <div class="alert alert-warning mt-2">{{$error}}</div> + {{end}} + </div> + {{end}} + <button type="submit" class="btn btn-primary">Save configuration</button> +</form> + +{{end}} diff --git a/templates/home.html b/templates/home.html index 40a0e5c..da3e478 100644 --- a/templates/home.html +++ b/templates/home.html @@ -8,23 +8,34 @@ <a class="ml-auto btn btn-sm btn-dark" href="/logout">Log out</a> </div> -<table class="table mt-4"> - <thead> - <tr> - <th>Account name</th> - <th>Protocol</th> - <th></th> - </tr> - </thead> - <tbody> - {{range $name, $acc := .Accounts}} +{{ if .Accounts }} + <table class="table mt-4"> + <thead> <tr> - <td>{{ $name }}</td> - <td>{{ $acc.Protocol }}</td> - <td>Modifier etc</td> + <th>Account name</th> + <th>Protocol</th> + <th></th> </tr> - {{end}} - </tbody> -</table> + </thead> + <tbody> + {{range $i, $acc := .Accounts}} + <tr> + <td>{{ $acc.AccountName }}</td> + <td>{{ $acc.Protocol }}</td> + <td> + <a class="btn btn-sm btn-primary" href="/edit/{{ $acc.AccountName }}">Modify</a> + <a class="btn btn-sm btn-danger ml-4" href="/delete/{{ $acc.AccountName }}">Delete</a> + </td> + </tr> + {{end}} + </tbody> + </table> +{{end}} + +<h5 class="mt-4">Add account</h5> + +<a class="btn btn-sm btn-dark" href="/add/irc">IRC</a> +<a class="btn btn-sm btn-warning ml-4" href="/add/xmpp">XMPP</a> +<a class="btn btn-sm btn-info ml-4" href="/add/mattermost">Mattermost</a> {{end}} @@ -5,12 +5,14 @@ import ( "html/template" "log" "net/http" + "strconv" "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" ) @@ -30,6 +32,9 @@ func StartWeb() { 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)) @@ -178,19 +183,131 @@ func handleLogin(w http.ResponseWriter, r *http.Request) *LoginInfo { } } -func LoadDbAccounts(mxid string, key *[32]byte) { - var allAccounts []DbAccountConfig - db.Where(&DbAccountConfig{MxUserID: mxid}).Find(&allAccounts) - for _, acct := range allAccounts { - config, err := decryptAccountConfig(acct.Config, key) - if err != nil { - ezbrSystemSendf("Could not decrypt stored configuration for account %s", acct.Name) - continue +// ---- + +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 } + } - err = SetAccount(mxid, acct.Name, acct.Protocol, config) - if err != nil { - ezbrSystemSendf("Could not setup account %s: %s", acct.Name, err.Error()) + 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) { } |