From 6d6c1936cc1b132ba7865a448c4392446a3e8c1c Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Sun, 6 Nov 2016 13:47:13 +1000 Subject: [PATCH] Handle db better, fix bug, update db schema, rest --- CHANGELOG.md | 2 ++ irc/database.go | 59 +++++++++++++++++++++++++++++++++++++-- irc/registration.go | 14 +++++----- irc/rest_api.go | 68 +++++++++++++++++++++++++++++++++------------ irc/server.go | 22 +++++++++++++-- oragono.go | 4 +++ 6 files changed, 141 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a237ab..933addbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ New release of Oragono! ### Added * Added ability to ban IP addresses and networks from the server with `DLINE`. +* Added REST API for use with web interface to manage accounts, DLINEs, etc. ### Changed +* Database upgraded to make handling accounts simpler. ### Removed diff --git a/irc/database.go b/irc/database.go index 298d0fa8..45741a4f 100644 --- a/irc/database.go +++ b/irc/database.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "os" + "strings" "github.com/tidwall/buntdb" ) @@ -15,6 +16,8 @@ import ( const ( // 'version' of the database schema keySchemaVersion = "db.version" + // latest schema of the db + latestDbSchema = "2" // key for the primary salt used by the ircd keySalt = "crypto.salt" ) @@ -40,16 +43,68 @@ func InitDB(path string) { tx.Set(keySalt, encodedSalt, nil) // set schema version - tx.Set(keySchemaVersion, "1", nil) + tx.Set(keySchemaVersion, "2", nil) return nil }) if err != nil { - log.Fatal("Could not save bunt store:", err.Error()) + log.Fatal("Could not save datastore:", err.Error()) } } // UpgradeDB upgrades the datastore to the latest schema. func UpgradeDB(path string) { + store, err := buntdb.Open(path) + if err != nil { + log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error())) + } + defer store.Close() + + err = store.Update(func(tx *buntdb.Tx) error { + version, _ := tx.Get(keySchemaVersion) + + // == version 1 -> 2 == + // account key changes and account.verified key bugfix. + if version == "1" { + log.Println("Updating store v1 to v2") + + var keysToRemove []string + newKeys := make(map[string]string) + + tx.AscendKeys("account *", func(key, value string) bool { + keysToRemove = append(keysToRemove, key) + splitkey := strings.Split(key, " ") + + // work around bug + if splitkey[2] == "exists" { + // manually create new verified key + newVerifiedKey := fmt.Sprintf("%s.verified %s", splitkey[0], splitkey[1]) + newKeys[newVerifiedKey] = "1" + } else if splitkey[1] == "%s" { + return true + } + + newKey := fmt.Sprintf("%s.%s %s", splitkey[0], splitkey[2], splitkey[1]) + newKeys[newKey] = value + + return true + }) + + for _, key := range keysToRemove { + tx.Delete(key) + } + for key, value := range newKeys { + tx.Set(key, value, nil) + } + + tx.Set(keySchemaVersion, "2", nil) + } + + return nil + }) + if err != nil { + log.Fatal("Could not update datastore:", err.Error()) + } + return } diff --git a/irc/registration.go b/irc/registration.go index 2667a3d5..2c7138fc 100644 --- a/irc/registration.go +++ b/irc/registration.go @@ -17,11 +17,11 @@ import ( ) const ( - keyAccountExists = "account %s exists" - keyAccountVerified = "account %s verified" - keyAccountName = "account %s name" // stores the 'preferred name' of the account, not casemapped - keyAccountRegTime = "account %s registered.time" - keyAccountCredentials = "account %s credentials" + keyAccountExists = "account.exists %s" + keyAccountVerified = "account.verified %s" + keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped + keyAccountRegTime = "account.registered.time %s" + keyAccountCredentials = "account.credentials %s" keyCertToAccount = "account.creds.certfp %s" ) @@ -80,7 +80,7 @@ func regHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } // removeFailedRegCreateData removes the data created by REG CREATE if the account creation fails early. -func removeFailedRegCreateData(store buntdb.DB, account string) { +func removeFailedRegCreateData(store *buntdb.DB, account string) { // error is ignored here, we can't do much about it anyways store.Update(func(tx *buntdb.Tx) error { tx.Delete(fmt.Sprintf(keyAccountExists, account)) @@ -250,7 +250,7 @@ func regCreateHandler(server *Server, client *Client, msg ircmsg.IrcMessage) boo // automatically complete registration if callbackNamespace == "*" { err = server.store.Update(func(tx *buntdb.Tx) error { - tx.Set(keyAccountVerified, "1", nil) + tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil) // load acct info inside store tx account := ClientAccount{ diff --git a/irc/rest_api.go b/irc/rest_api.go index a869b0f3..ede4bca9 100644 --- a/irc/rest_api.go +++ b/irc/rest_api.go @@ -8,11 +8,13 @@ package irc import ( "encoding/json" "net/http" + "strconv" "time" "fmt" "github.com/gorilla/mux" + "github.com/tidwall/buntdb" ) const restErr = "{\"error\":\"An unknown error occurred\"}" @@ -21,7 +23,10 @@ const restErr = "{\"error\":\"An unknown error occurred\"}" // way to do it, given how HTTP handlers dispatch and work. var restAPIServer *Server -type restVersionResp struct { +type restInfoResp struct { + ServerName string `json:"server-name"` + NetworkName string `json:"network-name"` + Version string `json:"version"` } @@ -36,13 +41,13 @@ type restDLinesResp struct { } type restAcct struct { - Name string + Name string `json:"name"` RegisteredAt time.Time `json:"registered-at"` - Clients int + Clients int `json:"clients"` } type restAccountsResp struct { - Accounts map[string]restAcct `json:"accounts"` + Verified map[string]restAcct `json:"verified"` } type restRehashResp struct { @@ -51,9 +56,11 @@ type restRehashResp struct { Time time.Time `json:"time"` } -func restVersion(w http.ResponseWriter, r *http.Request) { - rs := restVersionResp{ - Version: SemVer, +func restInfo(w http.ResponseWriter, r *http.Request) { + rs := restInfoResp{ + Version: SemVer, + ServerName: restAPIServer.name, + NetworkName: restAPIServer.networkName, } b, err := json.Marshal(rs) if err != nil { @@ -91,17 +98,44 @@ func restGetDLines(w http.ResponseWriter, r *http.Request) { func restGetAccounts(w http.ResponseWriter, r *http.Request) { rs := restAccountsResp{ - Accounts: make(map[string]restAcct), + Verified: make(map[string]restAcct), } - // get accts - for key, info := range restAPIServer.accounts { - rs.Accounts[key] = restAcct{ - Name: info.Name, - RegisteredAt: info.RegisteredAt, - Clients: len(info.Clients), - } - } + // get accounts + err := restAPIServer.store.View(func(tx *buntdb.Tx) error { + tx.AscendKeys("account.exists *", func(key, value string) bool { + key = key[len("account.exists "):] + _, err := tx.Get(fmt.Sprintf(keyAccountVerified, key)) + verified := err == nil + fmt.Println(fmt.Sprintf(keyAccountVerified, key)) + + // get other details + name, _ := tx.Get(fmt.Sprintf(keyAccountName, key)) + regTimeStr, _ := tx.Get(fmt.Sprintf(keyAccountRegTime, key)) + regTimeInt, _ := strconv.ParseInt(regTimeStr, 10, 64) + regTime := time.Unix(regTimeInt, 0) + + var clients int + acct := restAPIServer.accounts[key] + if acct != nil { + clients = len(acct.Clients) + } + + if verified { + rs.Verified[key] = restAcct{ + Name: name, + RegisteredAt: regTime, + Clients: clients, + } + } else { + //TODO(dan): Add to unverified list + } + + return true // true to continue I guess? + }) + + return nil + }) b, err := json.Marshal(rs) if err != nil { @@ -139,7 +173,7 @@ func (s *Server) startRestAPI() { // GET methods rg := r.Methods("GET").Subrouter() - rg.HandleFunc("/version", restVersion) + rg.HandleFunc("/info", restInfo) rg.HandleFunc("/status", restStatus) rg.HandleFunc("/dlines", restGetDLines) rg.HandleFunc("/accounts", restGetAccounts) diff --git a/irc/server.go b/irc/server.go index c0797f11..b6605684 100644 --- a/irc/server.go +++ b/irc/server.go @@ -9,6 +9,7 @@ import ( "bufio" "crypto/tls" "encoding/base64" + "errors" "fmt" "log" "net" @@ -32,6 +33,8 @@ var ( bannedFromServerMsg = ircmsg.MakeMessage(nil, "", "ERROR", "You are banned from this server (%s)") bannedFromServerBytes, _ = bannedFromServerMsg.Line() + + errDbOutOfDate = errors.New("Database schema is old.") ) // Limits holds the maximum limits for various things such as topic lengths @@ -102,7 +105,7 @@ type Server struct { rehashSignal chan os.Signal restAPI *RestAPIConfig signals chan os.Signal - store buntdb.DB + store *buntdb.DB whoWas *WhoWasList } @@ -194,7 +197,22 @@ func NewServer(configFilename string, config *Config) *Server { if err != nil { log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error())) } - server.store = *db + server.store = db + + // check db version + err = server.store.View(func(tx *buntdb.Tx) error { + version, _ := tx.Get(keySchemaVersion) + if version != latestDbSchema { + log.Println(fmt.Sprintf("Database must be updated. Expected schema v%s, got v%s.", latestDbSchema, version)) + return errDbOutOfDate + } + return nil + }) + if err != nil { + // close the db + db.Close() + return nil + } // load dlines server.loadDLines() diff --git a/oragono.go b/oragono.go index eeb1d8a4..61977e48 100644 --- a/oragono.go +++ b/oragono.go @@ -84,6 +84,10 @@ Options: } else if arguments["run"].(bool) { irc.Log.SetLevel(config.Server.Log) server := irc.NewServer(configfile, config) + if server == nil { + log.Println("Could not load server") + return + } if !arguments["--quiet"].(bool) { log.Println(fmt.Sprintf("Oragono v%s running", irc.SemVer)) defer log.Println(irc.SemVer, "exiting")