1// Copyright (c) 2012-2014 Jeremy Latt
2// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
3// released under the MIT license
4
5package irc
6
7import (
8	"strings"
9	"sync"
10
11	"github.com/ergochat/ergo/irc/caps"
12	"github.com/ergochat/ergo/irc/modes"
13	"github.com/ergochat/ergo/irc/utils"
14)
15
16// ClientManager keeps track of clients by nick, enforcing uniqueness of casefolded nicks
17type ClientManager struct {
18	sync.RWMutex // tier 2
19	byNick       map[string]*Client
20	bySkeleton   map[string]*Client
21}
22
23// Initialize initializes a ClientManager.
24func (clients *ClientManager) Initialize() {
25	clients.byNick = make(map[string]*Client)
26	clients.bySkeleton = make(map[string]*Client)
27}
28
29// Get retrieves a client from the manager, if they exist.
30func (clients *ClientManager) Get(nick string) *Client {
31	casefoldedName, err := CasefoldName(nick)
32	if err == nil {
33		clients.RLock()
34		defer clients.RUnlock()
35		cli := clients.byNick[casefoldedName]
36		return cli
37	}
38	return nil
39}
40
41func (clients *ClientManager) removeInternal(client *Client, oldcfnick, oldskeleton string) (err error) {
42	// requires holding the writable Lock()
43	if oldcfnick == "*" || oldcfnick == "" {
44		return errNickMissing
45	}
46
47	currentEntry, present := clients.byNick[oldcfnick]
48	if present {
49		if currentEntry == client {
50			delete(clients.byNick, oldcfnick)
51		} else {
52			// this shouldn't happen, but we can ignore it
53			client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
54			err = errNickMissing
55		}
56	} else {
57		err = errNickMissing
58	}
59
60	currentEntry, present = clients.bySkeleton[oldskeleton]
61	if present {
62		if currentEntry == client {
63			delete(clients.bySkeleton, oldskeleton)
64		} else {
65			client.server.logger.Warning("internal", "clients for skeleton out of sync", oldskeleton)
66			err = errNickMissing
67		}
68	} else {
69		err = errNickMissing
70	}
71
72	return
73}
74
75// Remove removes a client from the lookup set.
76func (clients *ClientManager) Remove(client *Client) error {
77	clients.Lock()
78	defer clients.Unlock()
79
80	oldcfnick, oldskeleton := client.uniqueIdentifiers()
81	return clients.removeInternal(client, oldcfnick, oldskeleton)
82}
83
84// SetNick sets a client's nickname, validating it against nicknames in use
85// XXX: dryRun validates a client's ability to claim a nick, without
86// actually claiming it
87func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string, dryRun bool) (setNick string, err error, returnedFromAway bool) {
88	config := client.server.Config()
89
90	var newCfNick, newSkeleton string
91
92	client.stateMutex.RLock()
93	account := client.account
94	accountName := client.accountName
95	settings := client.accountSettings
96	registered := client.registered
97	realname := client.realname
98	client.stateMutex.RUnlock()
99
100	if newNick != accountName && strings.ContainsAny(newNick, disfavoredNameCharacters) {
101		return "", errNicknameInvalid, false
102	}
103
104	// recompute always-on status, because client.alwaysOn is not set for unregistered clients
105	var alwaysOn, useAccountName bool
106	if account != "" {
107		alwaysOn = persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
108		useAccountName = alwaysOn || config.Accounts.NickReservation.ForceNickEqualsAccount
109	}
110
111	if useAccountName {
112		if registered && newNick != accountName && newNick != "" {
113			return "", errNickAccountMismatch, false
114		}
115		newNick = accountName
116		newCfNick = account
117		newSkeleton, err = Skeleton(newNick)
118		if err != nil {
119			return "", errNicknameInvalid, false
120		}
121	} else {
122		newNick = strings.TrimSpace(newNick)
123		if len(newNick) == 0 {
124			return "", errNickMissing, false
125		}
126
127		if account == "" && config.Accounts.NickReservation.ForceGuestFormat && !dryRun {
128			newCfNick, err = CasefoldName(newNick)
129			if err != nil {
130				return "", errNicknameInvalid, false
131			}
132			if !config.Accounts.NickReservation.guestRegexpFolded.MatchString(newCfNick) {
133				newNick = strings.Replace(config.Accounts.NickReservation.GuestFormat, "*", newNick, 1)
134				newCfNick = "" // re-fold it below
135			}
136		}
137
138		if newCfNick == "" {
139			newCfNick, err = CasefoldName(newNick)
140		}
141		if err != nil {
142			return "", errNicknameInvalid, false
143		}
144		if len(newNick) > config.Limits.NickLen || len(newCfNick) > config.Limits.NickLen {
145			return "", errNicknameInvalid, false
146		}
147		newSkeleton, err = Skeleton(newNick)
148		if err != nil {
149			return "", errNicknameInvalid, false
150		}
151
152		if config.isRelaymsgIdentifier(newNick) {
153			return "", errNicknameInvalid, false
154		}
155
156		if restrictedCasefoldedNicks.Has(newCfNick) || restrictedSkeletons.Has(newSkeleton) {
157			return "", errNicknameInvalid, false
158		}
159
160		reservedAccount, method := client.server.accounts.EnforcementStatus(newCfNick, newSkeleton)
161		if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
162			return "", errNicknameReserved, false
163		}
164	}
165
166	var bouncerAllowed bool
167	if config.Accounts.Multiclient.Enabled {
168		if useAccountName {
169			bouncerAllowed = true
170		} else {
171			if config.Accounts.Multiclient.AllowedByDefault && settings.AllowBouncer != MulticlientDisallowedByUser {
172				bouncerAllowed = true
173			} else if settings.AllowBouncer == MulticlientAllowedByUser {
174				bouncerAllowed = true
175			}
176		}
177	}
178
179	clients.Lock()
180	defer clients.Unlock()
181
182	currentClient := clients.byNick[newCfNick]
183	// the client may just be changing case
184	if currentClient != nil && currentClient != client {
185		// these conditions forbid reattaching to an existing session:
186		if registered || !bouncerAllowed || account == "" || account != currentClient.Account() ||
187			dryRun || session == nil {
188			return "", errNicknameInUse, false
189		}
190		// check TLS modes
191		if client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
192			if useAccountName {
193				// #955: this is fatal because they can't fix it by trying a different nick
194				return "", errInsecureReattach, false
195			} else {
196				return "", errNicknameInUse, false
197			}
198		}
199		reattachSuccessful, numSessions, lastSeen, back := currentClient.AddSession(session)
200		if !reattachSuccessful {
201			return "", errNicknameInUse, false
202		}
203		if numSessions == 1 {
204			invisible := currentClient.HasMode(modes.Invisible)
205			operator := currentClient.HasMode(modes.Operator)
206			client.server.stats.AddRegistered(invisible, operator)
207		}
208		session.autoreplayMissedSince = lastSeen
209		// TODO: transition mechanism for #1065, clean this up eventually:
210		if currentClient.Realname() == "" {
211			currentClient.SetRealname(realname)
212		}
213		// successful reattach!
214		return newNick, nil, back
215	} else if currentClient == client && currentClient.Nick() == newNick {
216		return "", errNoop, false
217	}
218	// analogous checks for skeletons
219	skeletonHolder := clients.bySkeleton[newSkeleton]
220	if skeletonHolder != nil && skeletonHolder != client {
221		return "", errNicknameInUse, false
222	}
223
224	if dryRun {
225		return "", nil, false
226	}
227
228	formercfnick, formerskeleton := client.uniqueIdentifiers()
229	if changeSuccess := client.SetNick(newNick, newCfNick, newSkeleton); !changeSuccess {
230		return "", errClientDestroyed, false
231	}
232	clients.removeInternal(client, formercfnick, formerskeleton)
233	clients.byNick[newCfNick] = client
234	clients.bySkeleton[newSkeleton] = client
235	return newNick, nil, false
236}
237
238func (clients *ClientManager) AllClients() (result []*Client) {
239	clients.RLock()
240	defer clients.RUnlock()
241	result = make([]*Client, len(clients.byNick))
242	i := 0
243	for _, client := range clients.byNick {
244		result[i] = client
245		i++
246	}
247	return
248}
249
250// AllWithCapsNotify returns all clients with the given capabilities, and that support cap-notify.
251func (clients *ClientManager) AllWithCapsNotify(capabs ...caps.Capability) (sessions []*Session) {
252	capabs = append(capabs, caps.CapNotify)
253	clients.RLock()
254	defer clients.RUnlock()
255	for _, client := range clients.byNick {
256		for _, session := range client.Sessions() {
257			// cap-notify is implicit in cap version 302 and above
258			if session.capabilities.HasAll(capabs...) || 302 <= session.capVersion {
259				sessions = append(sessions, session)
260			}
261		}
262	}
263
264	return
265}
266
267// FindAll returns all clients that match the given userhost mask.
268func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
269	set = make(ClientSet)
270
271	userhost, err := CanonicalizeMaskWildcard(userhost)
272	if err != nil {
273		return set
274	}
275	matcher, err := utils.CompileGlob(userhost, false)
276	if err != nil {
277		// not much we can do here
278		return
279	}
280
281	clients.RLock()
282	defer clients.RUnlock()
283	for _, client := range clients.byNick {
284		if matcher.MatchString(client.NickMaskCasefolded()) {
285			set.Add(client)
286		}
287	}
288
289	return set
290}
291
292// Determine the canonical / unfolded form of a nick, if a client matching it
293// is present (or always-on).
294func (clients *ClientManager) UnfoldNick(cfnick string) (nick string) {
295	clients.RLock()
296	c := clients.byNick[cfnick]
297	clients.RUnlock()
298	if c != nil {
299		return c.Nick()
300	} else {
301		return cfnick
302	}
303}
304