From 69cdad45acb0621e7b2d47cb4f9c02fbc62d6b56 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Thu, 6 Mar 2014 11:56:32 -0800 Subject: [PATCH 01/11] partially-working sqlite-based usermasks --- ergonomadic.go | 1 + irc/channel.go | 12 +-- irc/commands.go | 4 +- irc/reply.go | 18 ++++- irc/server.go | 202 +++++++++++++++++++++++++++++++++++++++++------- irc/types.go | 52 +------------ 6 files changed, 200 insertions(+), 89 deletions(-) diff --git a/ergonomadic.go b/ergonomadic.go index dbd45ddb..2c4cce76 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -45,5 +45,6 @@ func main() { irc.DEBUG_CHANNEL = config.Debug.Channel irc.DEBUG_SERVER = config.Debug.Server + log.Println(irc.SEM_VER, "running") irc.NewServer(config).Run() } diff --git a/irc/channel.go b/irc/channel.go index bab45747..d7992af8 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -8,7 +8,7 @@ import ( type Channel struct { flags ChannelModeSet - lists map[ChannelMode][]UserMask + lists map[ChannelMode]UserMaskSet key string members MemberSet name string @@ -26,10 +26,10 @@ func IsChannel(target string) bool { func NewChannel(s *Server, name string) *Channel { channel := &Channel{ flags: make(ChannelModeSet), - lists: map[ChannelMode][]UserMask{ - BanMask: []UserMask{}, - ExceptMask: []UserMask{}, - InviteMask: []UserMask{}, + lists: map[ChannelMode]UserMaskSet{ + BanMask: make(UserMaskSet), + ExceptMask: make(UserMaskSet), + InviteMask: make(UserMaskSet), }, members: make(MemberSet), name: strings.ToLower(name), @@ -307,7 +307,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo case BanMask, ExceptMask, InviteMask: // TODO add/remove - for _, mask := range channel.lists[change.mode] { + for mask := range channel.lists[change.mode] { client.RplMaskList(change.mode, channel, mask) } client.RplEndOfMaskList(change.mode, channel) diff --git a/irc/commands.go b/irc/commands.go index 7f7a14bb..0fa347aa 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -656,7 +656,7 @@ func (msg *WhoisCommand) String() string { type WhoCommand struct { BaseCommand - mask Mask + mask string operatorOnly bool } @@ -665,7 +665,7 @@ func NewWhoCommand(args []string) (editableCommand, error) { cmd := &WhoCommand{} if len(args) > 0 { - cmd.mask = Mask(args[0]) + cmd.mask = args[0] } if (len(args) > 1) && (args[1] == "o") { diff --git a/irc/reply.go b/irc/reply.go index 3e69b746..27b39fa8 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -196,6 +196,16 @@ func (target *Client) RplYoureOper() { ":You are now an IRC operator") } +func (target *Client) RplWhois(client *Client) { + target.RplWhoisUser(client) + if client.flags[Operator] { + target.RplWhoisOperator(client) + } + target.RplWhoisIdle(client) + target.RplWhoisChannels(client) + target.RplEndOfWhois() +} + func (target *Client) RplWhoisUser(client *Client) { target.NumericReply(RPL_WHOISUSER, "%s %s %s * :%s", client.Nick(), client.username, client.hostname, @@ -258,7 +268,7 @@ func (target *Client) RplEndOfWho(name string) { "%s :End of WHO list", name) } -func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask UserMask) { +func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask string) { switch mode { case BanMask: target.RplBanList(channel, mask) @@ -284,7 +294,7 @@ func (target *Client) RplEndOfMaskList(mode ChannelMode, channel *Channel) { } } -func (target *Client) RplBanList(channel *Channel, mask UserMask) { +func (target *Client) RplBanList(channel *Channel, mask string) { target.NumericReply(RPL_BANLIST, "%s %s", channel, mask) } @@ -294,7 +304,7 @@ func (target *Client) RplEndOfBanList(channel *Channel) { "%s :End of channel ban list", channel) } -func (target *Client) RplExceptList(channel *Channel, mask UserMask) { +func (target *Client) RplExceptList(channel *Channel, mask string) { target.NumericReply(RPL_EXCEPTLIST, "%s %s", channel, mask) } @@ -304,7 +314,7 @@ func (target *Client) RplEndOfExceptList(channel *Channel) { "%s :End of channel exception list", channel) } -func (target *Client) RplInviteList(channel *Channel, mask UserMask) { +func (target *Client) RplInviteList(channel *Channel, mask string) { target.NumericReply(RPL_INVITELIST, "%s %s", channel, mask) } diff --git a/irc/server.go b/irc/server.go index ba2ed34c..fa186b0d 100644 --- a/irc/server.go +++ b/irc/server.go @@ -3,6 +3,7 @@ package irc import ( "bufio" "database/sql" + "errors" "fmt" "log" "net" @@ -18,7 +19,7 @@ import ( type Server struct { channels ChannelNameMap - clients ClientNameMap + clients *ClientLookupSet commands chan Command ctime time.Time db *sql.DB @@ -35,7 +36,7 @@ type Server struct { func NewServer(config *Config) *Server { server := &Server{ channels: make(ChannelNameMap), - clients: make(ClientNameMap), + clients: NewClientLookupSet(), commands: make(chan Command, 16), ctime: time.Now(), db: OpenDB(config.Server.Database), @@ -136,7 +137,7 @@ func (server *Server) processCommand(cmd Command) { func (server *Server) Shutdown() { server.db.Close() - for _, client := range server.clients { + for _, client := range server.clients.byNick { client.Reply(RplNotice(server, client, "shutting down")) } } @@ -556,19 +557,14 @@ func (m *WhoisCommand) HandleServer(server *Server) { // TODO implement target query for _, mask := range m.masks { - // TODO implement wildcard matching - mclient := server.clients.Get(mask) - if mclient == nil { + matches := server.clients.FindAll(mask) + if len(matches) == 0 { client.ErrNoSuchNick(mask) continue } - client.RplWhoisUser(mclient) - if mclient.flags[Operator] { - client.RplWhoisOperator(mclient) + for mclient := range matches { + client.RplWhois(mclient) } - client.RplWhoisIdle(mclient) - client.RplWhoisChannels(mclient) - client.RplEndOfWhois() } } @@ -583,9 +579,9 @@ func (msg *ChannelModeCommand) HandleServer(server *Server) { channel.Mode(client, msg.changes) } -func whoChannel(client *Client, channel *Channel) { +func whoChannel(client *Client, channel *Channel, friends ClientSet) { for member := range channel.members { - if !client.flags[Invisible] { + if !client.flags[Invisible] || friends[client] { client.RplWhoReply(channel, member) } } @@ -593,27 +589,21 @@ func whoChannel(client *Client, channel *Channel) { func (msg *WhoCommand) HandleServer(server *Server) { client := msg.Client() + friends := client.Friends() + mask := msg.mask - // TODO implement wildcard matching - mask := string(msg.mask) if mask == "" { for _, channel := range server.channels { - for member := range channel.members { - if !client.flags[Invisible] { - client.RplWhoReply(channel, member) - } - } + whoChannel(client, channel, friends) } } else if IsChannel(mask) { + // TODO implement wildcard matching channel := server.channels.Get(mask) if channel != nil { - for member := range channel.members { - client.RplWhoReply(channel, member) - } + whoChannel(client, channel, friends) } } else { - mclient := server.clients.Get(mask) - if mclient != nil { + for mclient := range server.clients.FindAll(mask) { client.RplWhoReply(nil, mclient) } } @@ -853,3 +843,163 @@ func (msg *KillCommand) HandleServer(server *Server) { quitMsg := fmt.Sprintf("KILLed by %s: %s", client.Nick(), msg.comment) target.Quit(quitMsg) } + +// +// keeping track of clients +// + +type ClientLookupSet struct { + byNick map[string]*Client + db *ClientDB +} + +func NewClientLookupSet() *ClientLookupSet { + return &ClientLookupSet{ + byNick: make(map[string]*Client), + db: NewClientDB(), + } +} + +var ( + ErrNickMissing = errors.New("nick missing") + ErrNicknameInUse = errors.New("nickname in use") + ErrNicknameMismatch = errors.New("nickname mismatch") +) + +func (clients *ClientLookupSet) Get(nick string) *Client { + return clients.byNick[strings.ToLower(nick)] +} + +func (clients *ClientLookupSet) Add(client *Client) error { + if !client.HasNick() { + return ErrNickMissing + } + if clients.Get(client.nick) != nil { + return ErrNicknameInUse + } + clients.byNick[strings.ToLower(client.nick)] = client + clients.db.Add(client) + return nil +} + +func (clients *ClientLookupSet) Remove(client *Client) error { + if !client.HasNick() { + return ErrNickMissing + } + if clients.Get(client.nick) != client { + return ErrNicknameMismatch + } + delete(clients.byNick, strings.ToLower(client.nick)) + clients.db.Remove(client) + return nil +} + +func ExpandUserHost(userhost string) (expanded string) { + expanded = userhost + // fill in missing wildcards for nicks + if !strings.Contains(expanded, "!") { + expanded += "!*" + } + if !strings.Contains(expanded, "@") { + expanded += "@*" + } + return +} + +func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { + userhost = ExpandUserHost(userhost) + set = make(ClientSet) + rows, err := clients.db.db.Query( + `SELECT nickname FROM client + WHERE userhost LIKE ? ESCAPE '\'`, + QuoteLike(userhost)) + if err != nil { + return + } + for rows.Next() { + var nickname string + err := rows.Scan(&nickname) + if err != nil { + return + } + client := clients.Get(nickname) + if client != nil { + set.Add(client) + } + } + return +} + +func (clients *ClientLookupSet) Find(userhost string) *Client { + userhost = ExpandUserHost(userhost) + row := clients.db.db.QueryRow( + `SELECT nickname FROM client + WHERE userhost LIKE ? ESCAPE \ + LIMIT 1`, + QuoteLike(userhost)) + var nickname string + err := row.Scan(&nickname) + if err != nil { + log.Println("ClientLookupSet.Find: ", err) + return nil + } + return clients.Get(nickname) +} + +// +// client db +// + +type ClientDB struct { + db *sql.DB +} + +func NewClientDB() *ClientDB { + db := &ClientDB{ + db: OpenDB(":memory:"), + } + _, err := db.db.Exec(` + CREATE TABLE client ( + nickname TEXT NOT NULL UNIQUE, + userhost TEXT NOT NULL)`) + if err != nil { + log.Fatal(err) + } + _, err = db.db.Exec(` + CREATE UNIQUE INDEX nickname_index ON client (nickname)`) + if err != nil { + log.Fatal(err) + } + return db +} + +func (db *ClientDB) Add(client *Client) { + _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, + client.Nick(), client.UserHost()) + if err != nil { + log.Println(err) + } +} + +func (db *ClientDB) Remove(client *Client) { + _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, + client.Nick()) + if err != nil { + log.Println(err) + } +} + +func QuoteLike(userhost string) (like string) { + like = userhost + // escape escape char + like = strings.Replace(like, `\`, `\\`, -1) + // escape meta-many + like = strings.Replace(like, `%`, `\%`, -1) + // escape meta-one + like = strings.Replace(like, `_`, `\_`, -1) + // swap meta-many + like = strings.Replace(like, `*`, `%`, -1) + // swap meta-one + like = strings.Replace(like, `?`, `_`, -1) + return +} diff --git a/irc/types.go b/irc/types.go index 6cb49a60..9539c186 100644 --- a/irc/types.go +++ b/irc/types.go @@ -1,7 +1,6 @@ package irc import ( - "errors" "fmt" "strings" ) @@ -10,8 +9,7 @@ import ( // simple types // -// a string with wildcards -type Mask string +type UserMaskSet map[string]bool // add, remove, list modes type ModeOp rune @@ -74,40 +72,6 @@ func (channels ChannelNameMap) Remove(channel *Channel) error { return nil } -type ClientNameMap map[string]*Client - -var ( - ErrNickMissing = errors.New("nick missing") - ErrNicknameInUse = errors.New("nickname in use") - ErrNicknameMismatch = errors.New("nickname mismatch") -) - -func (clients ClientNameMap) Get(nick string) *Client { - return clients[strings.ToLower(nick)] -} - -func (clients ClientNameMap) Add(client *Client) error { - if !client.HasNick() { - return ErrNickMissing - } - if clients.Get(client.nick) != nil { - return ErrNicknameInUse - } - clients[strings.ToLower(client.nick)] = client - return nil -} - -func (clients ClientNameMap) Remove(client *Client) error { - if !client.HasNick() { - return ErrNickMissing - } - if clients.Get(client.nick) != client { - return ErrNicknameMismatch - } - delete(clients, strings.ToLower(client.nick)) - return nil -} - type ChannelModeSet map[ChannelMode]bool func (set ChannelModeSet) String() string { @@ -209,17 +173,3 @@ type RegServerCommand interface { Command HandleRegServer(*Server) } - -// -// structs -// - -type UserMask struct { - nickname Mask - username Mask - hostname Mask -} - -func (mask *UserMask) String() string { - return fmt.Sprintf("%s!%s@%s", mask.nickname, mask.username, mask.hostname) -} From b2055595e1a6ebe5e6e4bd27bc921c06d27a54a2 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Thu, 6 Mar 2014 12:14:21 -0800 Subject: [PATCH 02/11] WHOWAS command --- irc/commands.go | 24 ++++++++++++++++++++++++ irc/constants.go | 1 + irc/reply.go | 15 +++++++++++++++ irc/server.go | 9 +++++++++ 4 files changed, 49 insertions(+) diff --git a/irc/commands.go b/irc/commands.go index 0fa347aa..a0963687 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -54,6 +54,7 @@ var ( VERSION: NewVersionCommand, WHO: NewWhoCommand, WHOIS: NewWhoisCommand, + WHOWAS: NewWhoWasCommand, } ) @@ -968,3 +969,26 @@ func NewKillCommand(args []string) (editableCommand, error) { comment: args[1], }, nil } + +type WhoWasCommand struct { + BaseCommand + nicknames []string + count int64 + target string +} + +func NewWhoWasCommand(args []string) (editableCommand, error) { + if len(args) < 1 { + return nil, NotEnoughArgsError + } + cmd := &WhoWasCommand{ + nicknames: strings.Split(args[0], ","), + } + if len(args) > 1 { + cmd.count, _ = strconv.ParseInt(args[1], 10, 64) + } + if len(args) > 2 { + cmd.target = args[2] + } + return cmd, nil +} diff --git a/irc/constants.go b/irc/constants.go index 6b4a89ec..bab17e6b 100644 --- a/irc/constants.go +++ b/irc/constants.go @@ -61,6 +61,7 @@ const ( VERSION StringCode = "VERSION" WHO StringCode = "WHO" WHOIS StringCode = "WHOIS" + WHOWAS StringCode = "WHOWAS" // numeric codes RPL_WELCOME NumericCode = 1 diff --git a/irc/reply.go b/irc/reply.go index 27b39fa8..a32a5765 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -394,6 +394,16 @@ func (target *Client) RplTime() { "%s :%s", target.server.name, time.Now().Format(time.RFC1123)) } +func (target *Client) RplWhoWasUser(nickname, username, hostname, realname string) { + target.NumericReply(RPL_WHOWASUSER, + "%s %s %s * :%s", nickname, username, hostname, realname) +} + +func (target *Client) RplEndOfWhoWas(nickname string) { + target.NumericReply(RPL_ENDOFWHOWAS, + "%s :End of WHOWAS", nickname) +} + // // errors (also numeric) // @@ -512,3 +522,8 @@ func (target *Client) ErrChannelIsFull(channel *Channel) { target.NumericReply(ERR_CHANNELISFULL, "%s :Cannot join channel (+l)", channel) } + +func (target *Client) ErrWasNoSuchNick(nickname string) { + target.NumericReply(ERR_WASNOSUCHNICK, + "%s :There was no such nickname", nickname) +} diff --git a/irc/server.go b/irc/server.go index fa186b0d..d31b0eac 100644 --- a/irc/server.go +++ b/irc/server.go @@ -844,6 +844,15 @@ func (msg *KillCommand) HandleServer(server *Server) { target.Quit(quitMsg) } +func (msg *WhoWasCommand) HandleServer(server *Server) { + client := msg.Client() + for _, nickname := range msg.nicknames { + // TODO implement nick history + client.ErrWasNoSuchNick(nickname) + client.RplEndOfWhoWas(nickname) + } +} + // // keeping track of clients // From 41a6027d4e9f7764e667d448ee77ddf54d0eee6d Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Thu, 6 Mar 2014 13:10:54 -0800 Subject: [PATCH 03/11] move ClientLookupSet to its own file --- irc/client_lookup_set.go | 164 +++++++++++++++++++++++++++++++++++++++ irc/server.go | 161 -------------------------------------- 2 files changed, 164 insertions(+), 161 deletions(-) create mode 100644 irc/client_lookup_set.go diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go new file mode 100644 index 00000000..24a2d43c --- /dev/null +++ b/irc/client_lookup_set.go @@ -0,0 +1,164 @@ +package irc + +import ( + "database/sql" + "errors" + "log" + "strings" +) + +type ClientLookupSet struct { + byNick map[string]*Client + db *ClientDB +} + +func NewClientLookupSet() *ClientLookupSet { + return &ClientLookupSet{ + byNick: make(map[string]*Client), + db: NewClientDB(), + } +} + +var ( + ErrNickMissing = errors.New("nick missing") + ErrNicknameInUse = errors.New("nickname in use") + ErrNicknameMismatch = errors.New("nickname mismatch") +) + +func (clients *ClientLookupSet) Get(nick string) *Client { + return clients.byNick[strings.ToLower(nick)] +} + +func (clients *ClientLookupSet) Add(client *Client) error { + if !client.HasNick() { + return ErrNickMissing + } + if clients.Get(client.nick) != nil { + return ErrNicknameInUse + } + clients.byNick[strings.ToLower(client.nick)] = client + clients.db.Add(client) + return nil +} + +func (clients *ClientLookupSet) Remove(client *Client) error { + if !client.HasNick() { + return ErrNickMissing + } + if clients.Get(client.nick) != client { + return ErrNicknameMismatch + } + delete(clients.byNick, strings.ToLower(client.nick)) + clients.db.Remove(client) + return nil +} + +func ExpandUserHost(userhost string) (expanded string) { + expanded = userhost + // fill in missing wildcards for nicks + if !strings.Contains(expanded, "!") { + expanded += "!*" + } + if !strings.Contains(expanded, "@") { + expanded += "@*" + } + return +} + +func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { + userhost = ExpandUserHost(userhost) + set = make(ClientSet) + rows, err := clients.db.db.Query( + `SELECT nickname FROM client + WHERE userhost LIKE ? ESCAPE '\'`, + QuoteLike(userhost)) + if err != nil { + return + } + for rows.Next() { + var nickname string + err := rows.Scan(&nickname) + if err != nil { + return + } + client := clients.Get(nickname) + if client != nil { + set.Add(client) + } + } + return +} + +func (clients *ClientLookupSet) Find(userhost string) *Client { + userhost = ExpandUserHost(userhost) + row := clients.db.db.QueryRow( + `SELECT nickname FROM client + WHERE userhost LIKE ? ESCAPE \ + LIMIT 1`, + QuoteLike(userhost)) + var nickname string + err := row.Scan(&nickname) + if err != nil { + log.Println("ClientLookupSet.Find: ", err) + return nil + } + return clients.Get(nickname) +} + +// +// client db +// + +type ClientDB struct { + db *sql.DB +} + +func NewClientDB() *ClientDB { + db := &ClientDB{ + db: OpenDB(":memory:"), + } + _, err := db.db.Exec(` + CREATE TABLE client ( + nickname TEXT NOT NULL UNIQUE, + userhost TEXT NOT NULL)`) + if err != nil { + log.Fatal(err) + } + _, err = db.db.Exec(` + CREATE UNIQUE INDEX nickname_index ON client (nickname)`) + if err != nil { + log.Fatal(err) + } + return db +} + +func (db *ClientDB) Add(client *Client) { + _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, + client.Nick(), client.UserHost()) + if err != nil { + log.Println(err) + } +} + +func (db *ClientDB) Remove(client *Client) { + _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, + client.Nick()) + if err != nil { + log.Println(err) + } +} + +func QuoteLike(userhost string) (like string) { + like = userhost + // escape escape char + like = strings.Replace(like, `\`, `\\`, -1) + // escape meta-many + like = strings.Replace(like, `%`, `\%`, -1) + // escape meta-one + like = strings.Replace(like, `_`, `\_`, -1) + // swap meta-many + like = strings.Replace(like, `*`, `%`, -1) + // swap meta-one + like = strings.Replace(like, `?`, `_`, -1) + return +} diff --git a/irc/server.go b/irc/server.go index d31b0eac..16ee7b11 100644 --- a/irc/server.go +++ b/irc/server.go @@ -3,7 +3,6 @@ package irc import ( "bufio" "database/sql" - "errors" "fmt" "log" "net" @@ -852,163 +851,3 @@ func (msg *WhoWasCommand) HandleServer(server *Server) { client.RplEndOfWhoWas(nickname) } } - -// -// keeping track of clients -// - -type ClientLookupSet struct { - byNick map[string]*Client - db *ClientDB -} - -func NewClientLookupSet() *ClientLookupSet { - return &ClientLookupSet{ - byNick: make(map[string]*Client), - db: NewClientDB(), - } -} - -var ( - ErrNickMissing = errors.New("nick missing") - ErrNicknameInUse = errors.New("nickname in use") - ErrNicknameMismatch = errors.New("nickname mismatch") -) - -func (clients *ClientLookupSet) Get(nick string) *Client { - return clients.byNick[strings.ToLower(nick)] -} - -func (clients *ClientLookupSet) Add(client *Client) error { - if !client.HasNick() { - return ErrNickMissing - } - if clients.Get(client.nick) != nil { - return ErrNicknameInUse - } - clients.byNick[strings.ToLower(client.nick)] = client - clients.db.Add(client) - return nil -} - -func (clients *ClientLookupSet) Remove(client *Client) error { - if !client.HasNick() { - return ErrNickMissing - } - if clients.Get(client.nick) != client { - return ErrNicknameMismatch - } - delete(clients.byNick, strings.ToLower(client.nick)) - clients.db.Remove(client) - return nil -} - -func ExpandUserHost(userhost string) (expanded string) { - expanded = userhost - // fill in missing wildcards for nicks - if !strings.Contains(expanded, "!") { - expanded += "!*" - } - if !strings.Contains(expanded, "@") { - expanded += "@*" - } - return -} - -func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { - userhost = ExpandUserHost(userhost) - set = make(ClientSet) - rows, err := clients.db.db.Query( - `SELECT nickname FROM client - WHERE userhost LIKE ? ESCAPE '\'`, - QuoteLike(userhost)) - if err != nil { - return - } - for rows.Next() { - var nickname string - err := rows.Scan(&nickname) - if err != nil { - return - } - client := clients.Get(nickname) - if client != nil { - set.Add(client) - } - } - return -} - -func (clients *ClientLookupSet) Find(userhost string) *Client { - userhost = ExpandUserHost(userhost) - row := clients.db.db.QueryRow( - `SELECT nickname FROM client - WHERE userhost LIKE ? ESCAPE \ - LIMIT 1`, - QuoteLike(userhost)) - var nickname string - err := row.Scan(&nickname) - if err != nil { - log.Println("ClientLookupSet.Find: ", err) - return nil - } - return clients.Get(nickname) -} - -// -// client db -// - -type ClientDB struct { - db *sql.DB -} - -func NewClientDB() *ClientDB { - db := &ClientDB{ - db: OpenDB(":memory:"), - } - _, err := db.db.Exec(` - CREATE TABLE client ( - nickname TEXT NOT NULL UNIQUE, - userhost TEXT NOT NULL)`) - if err != nil { - log.Fatal(err) - } - _, err = db.db.Exec(` - CREATE UNIQUE INDEX nickname_index ON client (nickname)`) - if err != nil { - log.Fatal(err) - } - return db -} - -func (db *ClientDB) Add(client *Client) { - _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, - client.Nick(), client.UserHost()) - if err != nil { - log.Println(err) - } -} - -func (db *ClientDB) Remove(client *Client) { - _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, - client.Nick()) - if err != nil { - log.Println(err) - } -} - -func QuoteLike(userhost string) (like string) { - like = userhost - // escape escape char - like = strings.Replace(like, `\`, `\\`, -1) - // escape meta-many - like = strings.Replace(like, `%`, `\%`, -1) - // escape meta-one - like = strings.Replace(like, `_`, `\_`, -1) - // swap meta-many - like = strings.Replace(like, `*`, `%`, -1) - // swap meta-one - like = strings.Replace(like, `?`, `_`, -1) - return -} From 76852b0370182858892b11f2ba436887d685aaea Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Thu, 6 Mar 2014 13:55:25 -0800 Subject: [PATCH 04/11] implement WHOWAS with a shared ringbuffer --- irc/client.go | 3 ++- irc/reply.go | 5 ++-- irc/server.go | 12 +++++++-- irc/whowas.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 irc/whowas.go diff --git a/irc/client.go b/irc/client.go index 7047f164..07d4bc87 100644 --- a/irc/client.go +++ b/irc/client.go @@ -225,6 +225,7 @@ func (client *Client) ChangeNickname(nickname string) { // Make reply before changing nick to capture original source id. reply := RplNick(client, nickname) client.server.clients.Remove(client) + client.server.whoWas.Append(client) client.nick = nickname client.server.clients.Add(client) for friend := range client.Friends() { @@ -242,8 +243,8 @@ func (client *Client) Quit(message string) { } client.Reply(RplError("connection closed")) - client.hasQuit = true + client.server.whoWas.Append(client) friends := client.Friends() friends.Remove(client) client.destroy() diff --git a/irc/reply.go b/irc/reply.go index a32a5765..5f538df5 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -394,9 +394,10 @@ func (target *Client) RplTime() { "%s :%s", target.server.name, time.Now().Format(time.RFC1123)) } -func (target *Client) RplWhoWasUser(nickname, username, hostname, realname string) { +func (target *Client) RplWhoWasUser(whoWas *WhoWas) { target.NumericReply(RPL_WHOWASUSER, - "%s %s %s * :%s", nickname, username, hostname, realname) + "%s %s %s * :%s", + whoWas.nickname, whoWas.username, whoWas.hostname, whoWas.realname) } func (target *Client) RplEndOfWhoWas(nickname string) { diff --git a/irc/server.go b/irc/server.go index 16ee7b11..53e476fd 100644 --- a/irc/server.go +++ b/irc/server.go @@ -30,6 +30,7 @@ type Server struct { password []byte signals chan os.Signal timeout chan *Client + whoWas *WhoWasList } func NewServer(config *Config) *Server { @@ -46,6 +47,7 @@ func NewServer(config *Config) *Server { operators: config.Operators(), signals: make(chan os.Signal, 1), timeout: make(chan *Client, 16), + whoWas: NewWhoWasList(100), } if config.Server.Password != "" { @@ -846,8 +848,14 @@ func (msg *KillCommand) HandleServer(server *Server) { func (msg *WhoWasCommand) HandleServer(server *Server) { client := msg.Client() for _, nickname := range msg.nicknames { - // TODO implement nick history - client.ErrWasNoSuchNick(nickname) + results := server.whoWas.Find(nickname, msg.count) + if len(results) == 0 { + client.ErrWasNoSuchNick(nickname) + } else { + for _, whoWas := range results { + client.RplWhoWasUser(whoWas) + } + } client.RplEndOfWhoWas(nickname) } } diff --git a/irc/whowas.go b/irc/whowas.go new file mode 100644 index 00000000..008ed7f3 --- /dev/null +++ b/irc/whowas.go @@ -0,0 +1,73 @@ +package irc + +type WhoWasList struct { + buffer []*WhoWas + start uint + end uint +} + +type WhoWas struct { + nickname string + username string + hostname string + realname string +} + +func NewWhoWasList(size uint) *WhoWasList { + return &WhoWasList{ + buffer: make([]*WhoWas, size), + } +} + +func (list *WhoWasList) Append(client *Client) { + list.buffer[list.end] = &WhoWas{ + nickname: client.Nick(), + username: client.username, + hostname: client.hostname, + realname: client.realname, + } + list.end = (list.end + 1) % uint(len(list.buffer)) + if list.end == list.start { + list.start = (list.end + 1) % uint(len(list.buffer)) + } +} + +func (list *WhoWasList) Find(nickname string, limit int64) []*WhoWas { + results := make([]*WhoWas, 0) + for whoWas := range list.Each() { + if nickname != whoWas.nickname { + continue + } + results = append(results, whoWas) + if int64(len(results)) >= limit { + break + } + } + return results +} + +func (list *WhoWasList) prev(index uint) uint { + index -= 1 + if index < 0 { + index += uint(len(list.buffer)) + } + return index +} + +// Iterate the buffer in reverse. +func (list *WhoWasList) Each() <-chan *WhoWas { + ch := make(chan *WhoWas) + go func() { + defer close(ch) + if list.start == list.end { + return + } + start := list.prev(list.end) + end := list.prev(list.start) + for start != end { + ch <- list.buffer[start] + start = list.prev(start) + } + }() + return ch +} From adde42a1bfabbee06c4678f2a728dc3fda643024 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Thu, 6 Mar 2014 16:51:33 -0800 Subject: [PATCH 05/11] update ClientLookupSet when username changes --- irc/client_lookup_set.go | 123 ++++++++++++++++++++++----------------- irc/server.go | 2 + 2 files changed, 72 insertions(+), 53 deletions(-) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 24a2d43c..dee9d421 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -4,9 +4,44 @@ import ( "database/sql" "errors" "log" + "regexp" "strings" ) +var ( + ErrNickMissing = errors.New("nick missing") + ErrNicknameInUse = errors.New("nickname in use") + ErrNicknameMismatch = errors.New("nickname mismatch") + wildMaskExpr = regexp.MustCompile(`\*|\?`) + likeQuoter = strings.NewReplacer( + `\`, `\\`, + `%`, `\%`, + `_`, `\_`, + `*`, `%`, + `?`, `_`) +) + +func HasWildcards(mask string) bool { + return wildMaskExpr.MatchString(mask) +} + +func ExpandUserHost(userhost string) (expanded string) { + expanded = userhost + // fill in missing wildcards for nicks + if !strings.Contains(expanded, "!") { + expanded += "!*" + } + if !strings.Contains(expanded, "@") { + expanded += "@*" + } + return +} + +func QuoteLike(userhost string) (like string) { + like = likeQuoter.Replace(userhost) + return +} + type ClientLookupSet struct { byNick map[string]*Client db *ClientDB @@ -19,12 +54,6 @@ func NewClientLookupSet() *ClientLookupSet { } } -var ( - ErrNickMissing = errors.New("nick missing") - ErrNicknameInUse = errors.New("nickname in use") - ErrNicknameMismatch = errors.New("nickname mismatch") -) - func (clients *ClientLookupSet) Get(nick string) *Client { return clients.byNick[strings.ToLower(nick)] } @@ -53,38 +82,35 @@ func (clients *ClientLookupSet) Remove(client *Client) error { return nil } -func ExpandUserHost(userhost string) (expanded string) { - expanded = userhost - // fill in missing wildcards for nicks - if !strings.Contains(expanded, "!") { - expanded += "!*" - } - if !strings.Contains(expanded, "@") { - expanded += "@*" - } - return -} - func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { userhost = ExpandUserHost(userhost) set = make(ClientSet) rows, err := clients.db.db.Query( - `SELECT nickname FROM client - WHERE userhost LIKE ? ESCAPE '\'`, + `SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\'`, QuoteLike(userhost)) if err != nil { + if DEBUG_SERVER { + log.Println("ClientLookupSet.FindAll.Query:", err) + } return } for rows.Next() { var nickname string err := rows.Scan(&nickname) if err != nil { + if DEBUG_SERVER { + log.Println("ClientLookupSet.FindAll.Scan:", err) + } return } client := clients.Get(nickname) - if client != nil { - set.Add(client) + if client == nil { + if DEBUG_SERVER { + log.Println("ClientLookupSet.FindAll: missing client:", nickname) + } + continue } + set.Add(client) } return } @@ -92,14 +118,14 @@ func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { func (clients *ClientLookupSet) Find(userhost string) *Client { userhost = ExpandUserHost(userhost) row := clients.db.db.QueryRow( - `SELECT nickname FROM client - WHERE userhost LIKE ? ESCAPE \ - LIMIT 1`, + `SELECT nickname FROM client WHERE userhost LIKE ? ESCAPE '\' LIMIT 1`, QuoteLike(userhost)) var nickname string err := row.Scan(&nickname) if err != nil { - log.Println("ClientLookupSet.Find: ", err) + if DEBUG_SERVER { + log.Println("ClientLookupSet.Find:", err) + } return nil } return clients.Get(nickname) @@ -117,17 +143,19 @@ func NewClientDB() *ClientDB { db := &ClientDB{ db: OpenDB(":memory:"), } - _, err := db.db.Exec(` - CREATE TABLE client ( - nickname TEXT NOT NULL UNIQUE, - userhost TEXT NOT NULL)`) - if err != nil { - log.Fatal(err) + stmts := []string{ + `CREATE TABLE client ( + nickname TEXT NOT NULL COLLATE NOCASE UNIQUE, + userhost TEXT NOT NULL COLLATE NOCASE, + UNIQUE (nickname, userhost) ON CONFLICT REPLACE)`, + `CREATE UNIQUE INDEX idx_nick ON client (nickname COLLATE NOCASE)`, + `CREATE UNIQUE INDEX idx_uh ON client (userhost COLLATE NOCASE)`, } - _, err = db.db.Exec(` - CREATE UNIQUE INDEX nickname_index ON client (nickname)`) - if err != nil { - log.Fatal(err) + for _, stmt := range stmts { + _, err := db.db.Exec(stmt) + if err != nil { + log.Fatal("NewClientDB: ", stmt, err) + } } return db } @@ -136,7 +164,9 @@ func (db *ClientDB) Add(client *Client) { _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, client.Nick(), client.UserHost()) if err != nil { - log.Println(err) + if DEBUG_SERVER { + log.Println("ClientDB.Add:", err) + } } } @@ -144,21 +174,8 @@ func (db *ClientDB) Remove(client *Client) { _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, client.Nick()) if err != nil { - log.Println(err) + if DEBUG_SERVER { + log.Println("ClientDB.Remove:", err) + } } } - -func QuoteLike(userhost string) (like string) { - like = userhost - // escape escape char - like = strings.Replace(like, `\`, `\\`, -1) - // escape meta-many - like = strings.Replace(like, `%`, `\%`, -1) - // escape meta-one - like = strings.Replace(like, `_`, `\_`, -1) - // swap meta-many - like = strings.Replace(like, `*`, `%`, -1) - // swap meta-one - like = strings.Replace(like, `?`, `_`, -1) - return -} diff --git a/irc/server.go b/irc/server.go index 53e476fd..7b7bdca5 100644 --- a/irc/server.go +++ b/irc/server.go @@ -346,7 +346,9 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { func (msg *UserCommand) HandleRegServer2(server *Server) { client := msg.Client() + server.clients.Remove(client) client.username, client.realname = msg.username, msg.realname + server.clients.Add(client) server.tryRegister(client) } From 12ae89ca14c6aad1da18cc47a7fc50e955675f14 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Thu, 6 Mar 2014 17:07:23 -0800 Subject: [PATCH 06/11] minor cleanup/refactoring --- irc/client_lookup_set.go | 5 ++--- irc/server.go | 8 ++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index dee9d421..2661c2ef 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -37,9 +37,8 @@ func ExpandUserHost(userhost string) (expanded string) { return } -func QuoteLike(userhost string) (like string) { - like = likeQuoter.Replace(userhost) - return +func QuoteLike(userhost string) string { + return likeQuoter.Replace(userhost) } type ClientLookupSet struct { diff --git a/irc/server.go b/irc/server.go index 7b7bdca5..bc511617 100644 --- a/irc/server.go +++ b/irc/server.go @@ -329,7 +329,7 @@ func (m *NickCommand) HandleRegServer(s *Server) { } func (msg *RFC1459UserCommand) HandleRegServer(server *Server) { - msg.HandleRegServer2(server) + msg.setUserInfo(server) } func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { @@ -341,10 +341,10 @@ func (msg *RFC2812UserCommand) HandleRegServer(server *Server) { } client.RplUModeIs(client) } - msg.HandleRegServer2(server) + msg.setUserInfo(server) } -func (msg *UserCommand) HandleRegServer2(server *Server) { +func (msg *UserCommand) setUserInfo(server *Server) { client := msg.Client() server.clients.Remove(client) client.username, client.realname = msg.username, msg.realname @@ -497,7 +497,7 @@ func (m *ModeCommand) HandleServer(s *Server) { return } - changes := make(ModeChanges, 0) + changes := make(ModeChanges, 0, len(m.changes)) for _, change := range m.changes { switch change.mode { From d4093e7f8ba7b1df466ed944b6e59134c8e77b1f Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Fri, 7 Mar 2014 17:09:49 -0800 Subject: [PATCH 07/11] mask lists (ban, except, invite) --- irc/channel.go | 66 ++++++++++++++++++++++++++++++++-------- irc/client_lookup_set.go | 57 ++++++++++++++++++++++++++++++++++ irc/reply.go | 10 ++++++ irc/types.go | 2 -- 4 files changed, 120 insertions(+), 15 deletions(-) diff --git a/irc/channel.go b/irc/channel.go index a4510557..dd7f7eb1 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -8,7 +8,7 @@ import ( type Channel struct { flags ChannelModeSet - lists map[ChannelMode]UserMaskSet + lists map[ChannelMode]*UserMaskSet key string members MemberSet name string @@ -26,10 +26,10 @@ func IsChannel(target string) bool { func NewChannel(s *Server, name string) *Channel { channel := &Channel{ flags: make(ChannelModeSet), - lists: map[ChannelMode]UserMaskSet{ - BanMask: make(UserMaskSet), - ExceptMask: make(UserMaskSet), - InviteMask: make(UserMaskSet), + lists: map[ChannelMode]*UserMaskSet{ + BanMask: NewUserMaskSet(), + ExceptMask: NewUserMaskSet(), + InviteMask: NewUserMaskSet(), }, members: make(MemberSet), name: strings.ToLower(name), @@ -151,6 +151,19 @@ func (channel *Channel) Join(client *Client, key string) { return } + isInvited := channel.lists[InviteMask].Match(client.UserHost()) + if channel.flags[InviteOnly] && !isInvited { + client.ErrInviteOnlyChan(channel) + return + } + + if channel.lists[BanMask].Match(client.UserHost()) && + !isInvited && + !channel.lists[ExceptMask].Match(client.UserHost()) { + client.ErrBannedFromChan(channel) + return + } + client.channels.Add(channel) channel.members.Add(client) if !channel.flags[Persistent] && (len(channel.members) == 1) { @@ -213,7 +226,7 @@ func (channel *Channel) SetTopic(client *Client, topic string) { } if err := channel.Persist(); err != nil { - log.Println(err) + log.Println("Channel.Persist:", channel, err) } } @@ -310,15 +323,35 @@ func (channel *Channel) applyModeMember(client *Client, mode ChannelMode, return false } +func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeOp, mask string) bool { + if !channel.ClientIsOperator(client) { + client.ErrChanOPrivIsNeeded(channel) + return false + } + + list := channel.lists[mode] + if list == nil { + // This should never happen, but better safe than panicky. + return false + } + + if op == Add { + list.Add(mask) + } else if op == Remove { + list.Remove(mask) + } + + for lmask := range channel.lists[mode].masks { + client.RplMaskList(mode, channel, lmask) + } + client.RplEndOfMaskList(mode, channel) + return true +} + func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) bool { switch change.mode { case BanMask, ExceptMask, InviteMask: - // TODO add/remove - - for mask := range channel.lists[change.mode] { - client.RplMaskList(change.mode, channel, mask) - } - client.RplEndOfMaskList(change.mode, channel) + return channel.applyModeMask(client, change.mode, change.op, change.arg) case Moderated, NoOutside, OpOnlyTopic, Persistent, Private: return channel.applyModeFlag(client, change.mode, change.op) @@ -390,7 +423,7 @@ func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) { } if err := channel.Persist(); err != nil { - log.Println(err) + log.Println("Channel.Persist:", channel, err) } } } @@ -464,6 +497,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) { return } + if channel.flags[InviteOnly] { + channel.lists[InviteMask].Add(invitee.UserHost()) + if err := channel.Persist(); err != nil { + log.Println("Channel.Persist:", channel, err) + } + } + inviter.RplInviting(invitee, channel.name) invitee.Reply(RplInviteMsg(inviter, invitee, channel.name)) if invitee.flags[Away] { diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 2661c2ef..6c66d1da 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -178,3 +178,60 @@ func (db *ClientDB) Remove(client *Client) { } } } + +// +// usermask to regexp +// + +type UserMaskSet struct { + masks map[string]bool + regexp *regexp.Regexp +} + +func NewUserMaskSet() *UserMaskSet { + return &UserMaskSet{ + masks: make(map[string]bool), + } +} + +func (set *UserMaskSet) Add(mask string) { + set.masks[mask] = true + set.setRegexp() +} + +func (set *UserMaskSet) Remove(mask string) { + delete(set.masks, mask) + set.setRegexp() +} + +func (set *UserMaskSet) Match(userhost string) bool { + if set.regexp == nil { + return false + } + return set.regexp.MatchString(userhost) +} + +func (set *UserMaskSet) setRegexp() { + if len(set.masks) == 0 { + set.regexp = nil + return + } + + maskExprs := make([]string, len(set.masks)) + index := 0 + for mask := range set.masks { + manyParts := strings.Split(mask, "*") + manyExprs := make([]string, len(manyParts)) + for mindex, manyPart := range manyParts { + oneParts := strings.Split(manyPart, "?") + oneExprs := make([]string, len(oneParts)) + for oindex, onePart := range oneParts { + oneExprs[oindex] = regexp.QuoteMeta(onePart) + } + manyExprs[mindex] = strings.Join(oneExprs, ".") + } + maskExprs[index] = strings.Join(manyExprs, ".*") + } + expr := "^" + strings.Join(maskExprs, "|") + "$" + set.regexp, _ = regexp.Compile(expr) +} diff --git a/irc/reply.go b/irc/reply.go index 00d24143..7adb63fe 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -541,3 +541,13 @@ func (target *Client) ErrInvalidCapCmd(subCommand CapSubCommand) { target.NumericReply(ERR_INVALIDCAPCMD, "%s :Invalid CAP subcommand", subCommand) } + +func (target *Client) ErrBannedFromChan(channel *Channel) { + target.NumericReply(ERR_BANNEDFROMCHAN, + "%s :Cannot join channel (+b)", channel) +} + +func (target *Client) ErrInviteOnlyChan(channel *Channel) { + target.NumericReply(ERR_INVITEONLYCHAN, + "%s :Cannot join channel (+i)", channel) +} diff --git a/irc/types.go b/irc/types.go index 60557e8f..e9e7a98b 100644 --- a/irc/types.go +++ b/irc/types.go @@ -9,8 +9,6 @@ import ( // simple types // -type UserMaskSet map[string]bool - type CapSubCommand string type Capability string From 04c30c8c9b45e6a3ab401c94a612e63eafc7780b Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Fri, 7 Mar 2014 17:35:58 -0800 Subject: [PATCH 08/11] channel invite mode/list --- irc/channel.go | 37 ++++++++++++++++++++++++------------- irc/client_lookup_set.go | 12 ++++++++++-- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/irc/channel.go b/irc/channel.go index dd7f7eb1..c44e191d 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -323,29 +323,40 @@ func (channel *Channel) applyModeMember(client *Client, mode ChannelMode, return false } -func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeOp, mask string) bool { - if !channel.ClientIsOperator(client) { - client.ErrChanOPrivIsNeeded(channel) - return false +func (channel *Channel) ShowMaskList(client *Client, mode ChannelMode) { + for lmask := range channel.lists[mode].masks { + client.RplMaskList(mode, channel, lmask) } + client.RplEndOfMaskList(mode, channel) +} +func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeOp, + mask string) bool { list := channel.lists[mode] if list == nil { // This should never happen, but better safe than panicky. return false } - if op == Add { - list.Add(mask) - } else if op == Remove { - list.Remove(mask) + if (op == List) || (mask == "") { + channel.ShowMaskList(client, mode) + return false } - for lmask := range channel.lists[mode].masks { - client.RplMaskList(mode, channel, lmask) + if !channel.ClientIsOperator(client) { + client.ErrChanOPrivIsNeeded(channel) + return false } - client.RplEndOfMaskList(mode, channel) - return true + + if op == Add { + return list.Add(mask) + } + + if op == Remove { + return list.Remove(mask) + } + + return false } func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) bool { @@ -353,7 +364,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo case BanMask, ExceptMask, InviteMask: return channel.applyModeMask(client, change.mode, change.op, change.arg) - case Moderated, NoOutside, OpOnlyTopic, Persistent, Private: + case InviteOnly, Moderated, NoOutside, OpOnlyTopic, Persistent, Private: return channel.applyModeFlag(client, change.mode, change.op) case Key: diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 6c66d1da..44aa4bda 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -194,14 +194,22 @@ func NewUserMaskSet() *UserMaskSet { } } -func (set *UserMaskSet) Add(mask string) { +func (set *UserMaskSet) Add(mask string) bool { + if set.masks[mask] { + return false + } set.masks[mask] = true set.setRegexp() + return true } -func (set *UserMaskSet) Remove(mask string) { +func (set *UserMaskSet) Remove(mask string) bool { + if !set.masks[mask] { + return false + } delete(set.masks, mask) set.setRegexp() + return true } func (set *UserMaskSet) Match(userhost string) bool { From cf76d2bd77839cac4cc1ea1f3c98bf6092ba2de6 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Fri, 7 Mar 2014 18:14:02 -0800 Subject: [PATCH 09/11] persist and load channel mask lists --- ergonomadic.go | 9 ++++++++- irc/channel.go | 8 +++++--- irc/client_lookup_set.go | 10 ++++++++++ irc/database.go | 24 ++++++++++++++++++++---- irc/server.go | 21 ++++++++++++++++++--- 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/ergonomadic.go b/ergonomadic.go index 2c4cce76..cf91bdea 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -12,6 +12,7 @@ import ( func main() { conf := flag.String("conf", "ergonomadic.conf", "ergonomadic config file") initdb := flag.Bool("initdb", false, "initialize database") + upgradedb := flag.Bool("upgradedb", false, "update database") passwd := flag.String("genpasswd", "", "bcrypt a password") flag.Parse() @@ -35,7 +36,13 @@ func main() { if *initdb { irc.InitDB(config.Server.Database) - log.Println("database initialized: " + config.Server.Database) + log.Println("database initialized: ", config.Server.Database) + return + } + + if *upgradedb { + irc.UpgradeDB(config.Server.Database) + log.Println("database upgraded: ", config.Server.Database) return } diff --git a/irc/channel.go b/irc/channel.go index c44e191d..41f7f9a8 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -443,10 +443,12 @@ func (channel *Channel) Persist() (err error) { if channel.flags[Persistent] { _, err = channel.server.db.Exec(` INSERT OR REPLACE INTO channel - (name, flags, key, topic, user_limit) - VALUES (?, ?, ?, ?, ?)`, + (name, flags, key, topic, user_limit, ban_list, except_list, + invite_list) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, channel.name, channel.flags.String(), channel.key, channel.topic, - channel.userLimit) + channel.userLimit, channel.lists[BanMask].String(), + channel.lists[ExceptMask].String(), channel.lists[InviteMask].String()) } else { _, err = channel.server.db.Exec(` DELETE FROM channel WHERE name = ?`, channel.name) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 44aa4bda..6506741e 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -219,6 +219,16 @@ func (set *UserMaskSet) Match(userhost string) bool { return set.regexp.MatchString(userhost) } +func (set *UserMaskSet) String() string { + masks := make([]string, len(set.masks)) + index := 0 + for mask := range set.masks { + masks[index] = mask + index += 1 + } + return strings.Join(masks, " ") +} + func (set *UserMaskSet) setRegexp() { if len(set.masks) == 0 { set.regexp = nil diff --git a/irc/database.go b/irc/database.go index c7f9264a..2a482ecf 100644 --- a/irc/database.go +++ b/irc/database.go @@ -2,6 +2,7 @@ package irc import ( "database/sql" + "fmt" _ "github.com/mattn/go-sqlite3" "log" "os" @@ -14,15 +15,30 @@ func InitDB(path string) { _, err := db.Exec(` CREATE TABLE channel ( name TEXT NOT NULL UNIQUE, - flags TEXT NOT NULL, - key TEXT NOT NULL, - topic TEXT NOT NULL, - user_limit INTEGER DEFAULT 0)`) + flags TEXT DEFAULT '', + key TEXT DEFAULT '', + topic TEXT DEFAULT '', + user_limit INTEGER DEFAULT 0, + ban_list TEXT DEFAULT '', + except_list TEXT DEFAULT '', + invite_list TEXT DEFAULT '')`) if err != nil { log.Fatal("initdb error: ", err) } } +func UpgradeDB(path string) { + db := OpenDB(path) + alter := `ALTER TABLE channel ADD COLUMN %s TEXT DEFAULT ''` + cols := []string{"ban_list", "except_list", "invite_list"} + for _, col := range cols { + _, err := db.Exec(fmt.Sprintf(alter, col)) + if err != nil { + log.Fatal("updatedb error: ", err) + } + } +} + func OpenDB(path string) *sql.DB { db, err := sql.Open("sqlite3", path) if err != nil { diff --git a/irc/server.go b/irc/server.go index 5f19f773..6d83fc50 100644 --- a/irc/server.go +++ b/irc/server.go @@ -64,9 +64,19 @@ func NewServer(config *Config) *Server { return server } +func loadChannelList(channel *Channel, list string, maskMode ChannelMode) { + if list == "" { + return + } + for _, mask := range strings.Split(list, " ") { + channel.lists[maskMode].Add(mask) + } +} + func (server *Server) loadChannels() { rows, err := server.db.Query(` - SELECT name, flags, key, topic, user_limit + SELECT name, flags, key, topic, user_limit, ban_list, except_list, + invite_list FROM channel`) if err != nil { log.Fatal("error loading channels: ", err) @@ -74,9 +84,11 @@ func (server *Server) loadChannels() { for rows.Next() { var name, flags, key, topic string var userLimit uint64 - err = rows.Scan(&name, &flags, &key, &topic, &userLimit) + var banList, exceptList, inviteList string + err = rows.Scan(&name, &flags, &key, &topic, &userLimit, &banList, + &exceptList, &inviteList) if err != nil { - log.Println(err) + log.Println("Server.loadChannels:", err) continue } @@ -87,6 +99,9 @@ func (server *Server) loadChannels() { channel.key = key channel.topic = topic channel.userLimit = userLimit + loadChannelList(channel, banList, BanMask) + loadChannelList(channel, exceptList, ExceptMask) + loadChannelList(channel, inviteList, InviteMask) } } From 09cff189721f7e234b0f123e59f32361d9ae643d Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Sat, 8 Mar 2014 11:59:34 -0800 Subject: [PATCH 10/11] print message when exiting main program --- ergonomadic.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ergonomadic.go b/ergonomadic.go index cf91bdea..3d107a7c 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -52,6 +52,8 @@ func main() { irc.DEBUG_CHANNEL = config.Debug.Channel irc.DEBUG_SERVER = config.Debug.Server + server := irc.NewServer(config) log.Println(irc.SEM_VER, "running") - irc.NewServer(config).Run() + defer log.Println(irc.SEM_VER, "exiting") + server.Run() } From 81df7b4a5ca33ca873eb48c2d226e5a543ebd489 Mon Sep 17 00:00:00 2001 From: Jeremy Latt Date: Sat, 8 Mar 2014 19:22:04 -0800 Subject: [PATCH 11/11] comments and improvements for client set --- irc/client_lookup_set.go | 17 +++++++++++++++++ irc/server.go | 4 +--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 6506741e..261461d9 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -203,6 +203,17 @@ func (set *UserMaskSet) Add(mask string) bool { return true } +func (set *UserMaskSet) AddAll(masks []string) (added bool) { + for _, mask := range masks { + if !added && !set.masks[mask] { + added = true + } + set.masks[mask] = true + } + set.setRegexp() + return +} + func (set *UserMaskSet) Remove(mask string) bool { if !set.masks[mask] { return false @@ -229,6 +240,12 @@ func (set *UserMaskSet) String() string { return strings.Join(masks, " ") } +// Generate a regular expression from the set of user mask +// strings. Masks are split at the two types of wildcards, `*` and +// `?`. All the pieces are meta-escaped. `*` is replaced with `.*`, +// the regexp equivalent. Likewise, `?` is replaced with `.`. The +// parts are re-joined and finally all masks are joined into a big +// or-expression. func (set *UserMaskSet) setRegexp() { if len(set.masks) == 0 { set.regexp = nil diff --git a/irc/server.go b/irc/server.go index 6d83fc50..652d1c5e 100644 --- a/irc/server.go +++ b/irc/server.go @@ -68,9 +68,7 @@ func loadChannelList(channel *Channel, list string, maskMode ChannelMode) { if list == "" { return } - for _, mask := range strings.Split(list, " ") { - channel.lists[maskMode].Add(mask) - } + channel.lists[maskMode].AddAll(strings.Split(list, " ")) } func (server *Server) loadChannels() {