// Copyright (c) 2012-2014 Jeremy Latt // Copyright (c) 2016-2017 Daniel Oaks // released under the MIT license package irc import ( "fmt" "log" "regexp" "strings" "github.com/goshuirc/irc-go/ircmatch" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/modes" "sync" ) // ExpandUserHost takes a userhost, and returns an expanded version. func ExpandUserHost(userhost string) (expanded string) { expanded = userhost // fill in missing wildcards for nicks //TODO(dan): this would fail with dan@lol, fix that. if !strings.Contains(expanded, "!") { expanded += "!*" } if !strings.Contains(expanded, "@") { expanded += "@*" } return } // ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks type ClientManager struct { sync.RWMutex // tier 2 byNick map[string]*Client bySkeleton map[string]*Client } // Initialize initializes a ClientManager. func (clients *ClientManager) Initialize() { clients.byNick = make(map[string]*Client) clients.bySkeleton = make(map[string]*Client) } // Count returns how many clients are in the manager. func (clients *ClientManager) Count() int { clients.RLock() defer clients.RUnlock() count := len(clients.byNick) return count } // Get retrieves a client from the manager, if they exist. func (clients *ClientManager) Get(nick string) *Client { casefoldedName, err := CasefoldName(nick) if err == nil { clients.RLock() defer clients.RUnlock() cli := clients.byNick[casefoldedName] return cli } return nil } func (clients *ClientManager) removeInternal(client *Client) (err error) { // requires holding the writable Lock() oldcfnick, oldskeleton := client.uniqueIdentifiers() if oldcfnick == "*" || oldcfnick == "" { return errNickMissing } currentEntry, present := clients.byNick[oldcfnick] if present { if currentEntry == client { delete(clients.byNick, oldcfnick) } else { // this shouldn't happen, but we can ignore it client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick) err = errNickMissing } } else { err = errNickMissing } currentEntry, present = clients.bySkeleton[oldskeleton] if present { if currentEntry == client { delete(clients.bySkeleton, oldskeleton) } else { client.server.logger.Warning("internal", "clients for skeleton out of sync", oldskeleton) err = errNickMissing } } else { err = errNickMissing } return } // Remove removes a client from the lookup set. func (clients *ClientManager) Remove(client *Client) error { clients.Lock() defer clients.Unlock() return clients.removeInternal(client) } // Resume atomically replaces `oldClient` with `newClient`, updating // newClient's data to match. It is the caller's responsibility first // to verify that the resume is allowed, and then later to call oldClient.destroy(). func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) { clients.Lock() defer clients.Unlock() // atomically grant the new client the old nick err = clients.removeInternal(oldClient) if err != nil { // oldClient no longer owns its nick, fail out return err } // nick has been reclaimed, grant it to the new client clients.removeInternal(newClient) oldcfnick, oldskeleton := oldClient.uniqueIdentifiers() clients.byNick[oldcfnick] = newClient clients.bySkeleton[oldskeleton] = newClient newClient.copyResumeData(oldClient) return nil } // SetNick sets a client's nickname, validating it against nicknames in use func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error { newcfnick, err := CasefoldName(newNick) if err != nil { return err } newSkeleton, err := Skeleton(newNick) if err != nil { return err } reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton) account := client.Account() bouncerAllowed := client.server.accounts.BouncerAllowed(account, session) clients.Lock() defer clients.Unlock() currentClient := clients.byNick[newcfnick] // the client may just be changing case if currentClient != nil && currentClient != client { // these conditions forbid reattaching to an existing session: if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) { return errNicknameInUse } if !currentClient.AddSession(session) { return errNicknameInUse } // successful reattach. temporarily assign them the nick they'll have going forward // (the current `client` will be discarded at the end of command execution) client.updateNick(currentClient.Nick(), newcfnick, newSkeleton) return nil } // analogous checks for skeletons skeletonHolder := clients.bySkeleton[newSkeleton] if skeletonHolder != nil && skeletonHolder != client { return errNicknameInUse } if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account { return errNicknameReserved } clients.removeInternal(client) clients.byNick[newcfnick] = client clients.bySkeleton[newSkeleton] = client client.updateNick(newNick, newcfnick, newSkeleton) return nil } func (clients *ClientManager) AllClients() (result []*Client) { clients.RLock() defer clients.RUnlock() result = make([]*Client, len(clients.byNick)) i := 0 for _, client := range clients.byNick { result[i] = client i++ } return } // AllWithCaps returns all clients with the given capabilities. func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) { clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { for _, session := range client.Sessions() { if session.capabilities.HasAll(capabs...) { sessions = append(sessions, session) } } } return } // AllWithCapsNotify returns all clients with the given capabilities, and that support cap-notify. func (clients *ClientManager) AllWithCapsNotify(capabs ...caps.Capability) (sessions []*Session) { clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { for _, session := range client.Sessions() { capabs = append(capabs, caps.CapNotify) if session.capabilities.HasAll(capabs...) || 302 <= session.capVersion { sessions = append(sessions, session) } } } return } // FindAll returns all clients that match the given userhost mask. func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { set = make(ClientSet) userhost, err := Casefold(ExpandUserHost(userhost)) if err != nil { return set } matcher := ircmatch.MakeMatch(userhost) clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { if matcher.Match(client.NickMaskCasefolded()) { set.Add(client) } } return set } // Find returns the first client that matches the given userhost mask. func (clients *ClientManager) Find(userhost string) *Client { userhost, err := Casefold(ExpandUserHost(userhost)) if err != nil { return nil } matcher := ircmatch.MakeMatch(userhost) var matchedClient *Client clients.RLock() defer clients.RUnlock() for _, client := range clients.byNick { if matcher.Match(client.NickMaskCasefolded()) { matchedClient = client break } } return matchedClient } // // usermask to regexp // //TODO(dan): move this over to generally using glob syntax instead? // kinda more expected in normal ban/etc masks, though regex is useful (probably as an extban?) // UserMaskSet holds a set of client masks and lets you match hostnames to them. type UserMaskSet struct { sync.RWMutex masks map[string]bool regexp *regexp.Regexp } // NewUserMaskSet returns a new UserMaskSet. func NewUserMaskSet() *UserMaskSet { return &UserMaskSet{ masks: make(map[string]bool), } } // Add adds the given mask to this set. func (set *UserMaskSet) Add(mask string) (added bool) { casefoldedMask, err := Casefold(mask) if err != nil { log.Println(fmt.Sprintf("ERROR: Could not add mask to usermaskset: [%s]", mask)) return false } set.Lock() added = !set.masks[casefoldedMask] if added { set.masks[casefoldedMask] = true } set.Unlock() if added { set.setRegexp() } return } // AddAll adds the given masks to this set. func (set *UserMaskSet) AddAll(masks []string) (added bool) { set.Lock() defer set.Unlock() for _, mask := range masks { if !added && !set.masks[mask] { added = true } set.masks[mask] = true } if added { set.setRegexp() } return } // Remove removes the given mask from this set. func (set *UserMaskSet) Remove(mask string) (removed bool) { set.Lock() removed = set.masks[mask] if removed { delete(set.masks, mask) } set.Unlock() if removed { set.setRegexp() } return } // Match matches the given n!u@h. func (set *UserMaskSet) Match(userhost string) bool { set.RLock() regexp := set.regexp set.RUnlock() if regexp == nil { return false } return regexp.MatchString(userhost) } // String returns the masks in this set. func (set *UserMaskSet) String() string { set.RLock() masks := make([]string, len(set.masks)) index := 0 for mask := range set.masks { masks[index] = mask index++ } set.RUnlock() return strings.Join(masks, " ") } func (set *UserMaskSet) Length() int { set.RLock() defer set.RUnlock() return len(set.masks) } // setRegexp generates 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() { var re *regexp.Regexp set.RLock() 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, ".*") index++ } set.RUnlock() if index > 0 { expr := "^" + strings.Join(maskExprs, "|") + "$" re, _ = regexp.Compile(expr) } set.Lock() set.regexp = re set.Unlock() }