Browse Source

refactor account registration, add nick enforcement

tags/v0.11.0-beta
Shivaram Lingamneni 6 years ago
parent
commit
ad73d68807
18 changed files with 863 additions and 600 deletions
  1. 1
    1
      DEVELOPING.md
  2. 0
    58
      irc/accountreg.go
  3. 450
    63
      irc/accounts.go
  4. 7
    5
      irc/channel.go
  5. 2
    2
      irc/chanserv.go
  6. 16
    14
      irc/client.go
  7. 9
    0
      irc/client_lookup_set.go
  8. 11
    7
      irc/commands.go
  9. 52
    7
      irc/config.go
  10. 19
    12
      irc/errors.go
  11. 27
    1
      irc/getters.go
  12. 88
    192
      irc/handlers.go
  13. 77
    0
      irc/idletimer.go
  14. 6
    0
      irc/nickname.go
  15. 21
    170
      irc/nickserv.go
  16. 3
    3
      irc/responsebuffer.go
  17. 65
    65
      irc/server.go
  18. 9
    0
      oragono.yaml

+ 1
- 1
DEVELOPING.md View File

@@ -98,7 +98,7 @@ In consequence, there is a lot of state (in particular, server and channel state
98 98
 
99 99
 There are some mutexes that are "tier 0": anything in a subpackage of `irc` (e.g., `irc/logger` or `irc/connection_limits`) shouldn't acquire mutexes defined in `irc`.
100 100
 
101
-We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. We haven't completely decided where this lock fits into the overall lock model. For now, it's probably better to err on the side of caution: if possible, don't acquire new locks inside the `buntdb` transaction, and be careful about what locks are held around the transaction as well.
101
+We are using `buntdb` for persistence; a `buntdb.DB` has an `RWMutex` inside it, with read-write transactions getting the `Lock()` and read-only transactions getting the `RLock()`. This mutex is considered tier 1. However, it's shared globally across all consumers, so if possible you should avoid acquiring it while holding ordinary application-level mutexes.
102 102
 
103 103
 ## Command handlers and ResponseBuffer
104 104
 

+ 0
- 58
irc/accountreg.go View File

@@ -1,58 +0,0 @@
1
-// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2
-// released under the MIT license
3
-
4
-package irc
5
-
6
-import (
7
-	"fmt"
8
-
9
-	"github.com/tidwall/buntdb"
10
-)
11
-
12
-// AccountRegistration manages the registration of accounts.
13
-type AccountRegistration struct {
14
-	Enabled                    bool
15
-	EnabledCallbacks           []string
16
-	EnabledCredentialTypes     []string
17
-	AllowMultiplePerConnection bool
18
-}
19
-
20
-// AccountCredentials stores the various methods for verifying accounts.
21
-type AccountCredentials struct {
22
-	PassphraseSalt []byte
23
-	PassphraseHash []byte
24
-	Certificate    string // fingerprint
25
-}
26
-
27
-// NewAccountRegistration returns a new AccountRegistration, configured correctly.
28
-func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) {
29
-	if config.Enabled {
30
-		accountReg.Enabled = true
31
-		accountReg.AllowMultiplePerConnection = config.AllowMultiplePerConnection
32
-		for _, name := range config.EnabledCallbacks {
33
-			// we store "none" as "*" internally
34
-			if name == "none" {
35
-				name = "*"
36
-			}
37
-			accountReg.EnabledCallbacks = append(accountReg.EnabledCallbacks, name)
38
-		}
39
-		// no need to make this configurable, right now at least
40
-		accountReg.EnabledCredentialTypes = []string{
41
-			"passphrase",
42
-			"certfp",
43
-		}
44
-	}
45
-	return accountReg
46
-}
47
-
48
-// removeFailedAccRegisterData removes the data created by ACC REGISTER if the account creation fails early.
49
-func removeFailedAccRegisterData(store *buntdb.DB, account string) {
50
-	// error is ignored here, we can't do much about it anyways
51
-	store.Update(func(tx *buntdb.Tx) error {
52
-		tx.Delete(fmt.Sprintf(keyAccountExists, account))
53
-		tx.Delete(fmt.Sprintf(keyAccountRegTime, account))
54
-		tx.Delete(fmt.Sprintf(keyAccountCredentials, account))
55
-
56
-		return nil
57
-	})
58
-}

+ 450
- 63
irc/accounts.go View File

@@ -7,10 +7,13 @@ import (
7 7
 	"encoding/json"
8 8
 	"fmt"
9 9
 	"strconv"
10
+	"strings"
11
+	"sync"
10 12
 	"time"
11 13
 
12 14
 	"github.com/goshuirc/irc-go/ircfmt"
13 15
 	"github.com/oragono/oragono/irc/caps"
16
+	"github.com/oragono/oragono/irc/passwd"
14 17
 	"github.com/oragono/oragono/irc/sno"
15 18
 	"github.com/tidwall/buntdb"
16 19
 )
@@ -24,6 +27,423 @@ const (
24 27
 	keyCertToAccount      = "account.creds.certfp %s"
25 28
 )
26 29
 
30
+// everything about accounts is persistent; therefore, the database is the authoritative
31
+// source of truth for all account information. anything on the heap is just a cache
32
+type AccountManager struct {
33
+	sync.RWMutex                      // tier 2
34
+	serialCacheUpdateMutex sync.Mutex // tier 3
35
+
36
+	server *Server
37
+	// track clients logged in to accounts
38
+	accountToClients map[string][]*Client
39
+	nickToAccount    map[string]string
40
+}
41
+
42
+func NewAccountManager(server *Server) *AccountManager {
43
+	am := AccountManager{
44
+		accountToClients: make(map[string][]*Client),
45
+		nickToAccount:    make(map[string]string),
46
+		server:           server,
47
+	}
48
+
49
+	am.buildNickToAccountIndex()
50
+	return &am
51
+}
52
+
53
+func (am *AccountManager) buildNickToAccountIndex() {
54
+	if am.server.AccountConfig().NickReservation == NickReservationDisabled {
55
+		return
56
+	}
57
+
58
+	result := make(map[string]string)
59
+	existsPrefix := fmt.Sprintf(keyAccountExists, "")
60
+
61
+	am.serialCacheUpdateMutex.Lock()
62
+	defer am.serialCacheUpdateMutex.Unlock()
63
+
64
+	err := am.server.store.View(func(tx *buntdb.Tx) error {
65
+		err := tx.AscendGreaterOrEqual("", existsPrefix, func(key, value string) bool {
66
+			if !strings.HasPrefix(key, existsPrefix) {
67
+				return false
68
+			}
69
+			accountName := strings.TrimPrefix(key, existsPrefix)
70
+			if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
71
+				result[accountName] = accountName
72
+			}
73
+			return true
74
+		})
75
+		return err
76
+	})
77
+
78
+	if err != nil {
79
+		am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err))
80
+	} else {
81
+		am.Lock()
82
+		am.nickToAccount = result
83
+		am.Unlock()
84
+	}
85
+
86
+	return
87
+}
88
+
89
+func (am *AccountManager) NickToAccount(cfnick string) string {
90
+	am.RLock()
91
+	defer am.RUnlock()
92
+	return am.nickToAccount[cfnick]
93
+}
94
+
95
+func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
96
+	casefoldedAccount, err := CasefoldName(account)
97
+	if err != nil || account == "" || account == "*" {
98
+		return errAccountCreation
99
+	}
100
+
101
+	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
102
+	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
103
+	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
104
+	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
105
+	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
106
+
107
+	var creds AccountCredentials
108
+	// always set passphrase salt
109
+	creds.PassphraseSalt, err = passwd.NewSalt()
110
+	if err != nil {
111
+		return errAccountCreation
112
+	}
113
+	// it's fine if this is empty, that just means no certificate is authorized
114
+	creds.Certificate = certfp
115
+	if passphrase != "" {
116
+		creds.PassphraseHash, err = am.server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
117
+		if err != nil {
118
+			am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
119
+			return errAccountCreation
120
+		}
121
+	}
122
+
123
+	credText, err := json.Marshal(creds)
124
+	if err != nil {
125
+		am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
126
+		return errAccountCreation
127
+	}
128
+	credStr := string(credText)
129
+
130
+	registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
131
+
132
+	var setOptions *buntdb.SetOptions
133
+	ttl := am.server.AccountConfig().Registration.VerifyTimeout
134
+	if ttl != 0 {
135
+		setOptions = &buntdb.SetOptions{Expires: true, TTL: ttl}
136
+	}
137
+
138
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
139
+		_, err := am.loadRawAccount(tx, casefoldedAccount)
140
+		if err != errAccountDoesNotExist {
141
+			return errAccountAlreadyRegistered
142
+		}
143
+
144
+		if certfp != "" {
145
+			// make sure certfp doesn't already exist because that'd be silly
146
+			_, err := tx.Get(certFPKey)
147
+			if err != buntdb.ErrNotFound {
148
+				return errCertfpAlreadyExists
149
+			}
150
+		}
151
+
152
+		tx.Set(accountKey, "1", setOptions)
153
+		tx.Set(accountNameKey, account, setOptions)
154
+		tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
155
+		tx.Set(credentialsKey, credStr, setOptions)
156
+		if certfp != "" {
157
+			tx.Set(certFPKey, casefoldedAccount, setOptions)
158
+		}
159
+		return nil
160
+	})
161
+
162
+	if err != nil {
163
+		return err
164
+	}
165
+
166
+	return nil
167
+}
168
+
169
+func (am *AccountManager) Verify(client *Client, account string, code string) error {
170
+	casefoldedAccount, err := CasefoldName(account)
171
+	if err != nil || account == "" || account == "*" {
172
+		return errAccountVerificationFailed
173
+	}
174
+
175
+	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
176
+	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
177
+	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
178
+	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
179
+	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
180
+
181
+	var raw rawClientAccount
182
+
183
+	func() {
184
+		am.serialCacheUpdateMutex.Lock()
185
+		defer am.serialCacheUpdateMutex.Unlock()
186
+
187
+		am.server.store.Update(func(tx *buntdb.Tx) error {
188
+			raw, err = am.loadRawAccount(tx, casefoldedAccount)
189
+			if err == errAccountDoesNotExist {
190
+				return errAccountDoesNotExist
191
+			} else if err != nil {
192
+				return errAccountVerificationFailed
193
+			} else if raw.Verified {
194
+				return errAccountAlreadyVerified
195
+			}
196
+
197
+			// TODO add code verification here
198
+			// return errAccountVerificationFailed if it fails
199
+
200
+			// verify the account
201
+			tx.Set(verifiedKey, "1", nil)
202
+			// re-set all other keys, removing the TTL
203
+			tx.Set(accountKey, "1", nil)
204
+			tx.Set(accountNameKey, raw.Name, nil)
205
+			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
206
+			tx.Set(credentialsKey, raw.Credentials, nil)
207
+
208
+			var creds AccountCredentials
209
+			// XXX we shouldn't do (de)serialization inside the txn,
210
+			// but this is like 2 usec on my system
211
+			json.Unmarshal([]byte(raw.Credentials), &creds)
212
+			if creds.Certificate != "" {
213
+				certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
214
+				tx.Set(certFPKey, casefoldedAccount, nil)
215
+			}
216
+
217
+			return nil
218
+		})
219
+
220
+		if err == nil {
221
+			am.Lock()
222
+			am.nickToAccount[casefoldedAccount] = casefoldedAccount
223
+			am.Unlock()
224
+		}
225
+	}()
226
+
227
+	if err != nil {
228
+		return err
229
+	}
230
+
231
+	am.Login(client, raw.Name)
232
+	return nil
233
+}
234
+
235
+func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
236
+	casefoldedAccount, err := CasefoldName(accountName)
237
+	if err != nil {
238
+		return errAccountDoesNotExist
239
+	}
240
+
241
+	account, err := am.LoadAccount(casefoldedAccount)
242
+	if err != nil {
243
+		return err
244
+	}
245
+
246
+	if !account.Verified {
247
+		return errAccountUnverified
248
+	}
249
+
250
+	err = am.server.passwords.CompareHashAndPassword(
251
+		account.Credentials.PassphraseHash, account.Credentials.PassphraseSalt, passphrase)
252
+	if err != nil {
253
+		return errAccountInvalidCredentials
254
+	}
255
+
256
+	am.Login(client, account.Name)
257
+	return nil
258
+}
259
+
260
+func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAccount, err error) {
261
+	var raw rawClientAccount
262
+	am.server.store.View(func(tx *buntdb.Tx) error {
263
+		raw, err = am.loadRawAccount(tx, casefoldedAccount)
264
+		if err == buntdb.ErrNotFound {
265
+			err = errAccountDoesNotExist
266
+		}
267
+		return nil
268
+	})
269
+	if err != nil {
270
+		return
271
+	}
272
+
273
+	result.Name = raw.Name
274
+	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
275
+	result.RegisteredAt = time.Unix(regTimeInt, 0)
276
+	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
277
+	if e != nil {
278
+		am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e))
279
+		err = errAccountDoesNotExist
280
+		return
281
+	}
282
+	result.Verified = raw.Verified
283
+	return
284
+}
285
+
286
+func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string) (result rawClientAccount, err error) {
287
+	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
288
+	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
289
+	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
290
+	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
291
+	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
292
+
293
+	_, e := tx.Get(accountKey)
294
+	if e == buntdb.ErrNotFound {
295
+		err = errAccountDoesNotExist
296
+		return
297
+	}
298
+
299
+	if result.Name, err = tx.Get(accountNameKey); err != nil {
300
+		return
301
+	}
302
+	if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil {
303
+		return
304
+	}
305
+	if result.Credentials, err = tx.Get(credentialsKey); err != nil {
306
+		return
307
+	}
308
+	if _, e = tx.Get(verifiedKey); e == nil {
309
+		result.Verified = true
310
+	}
311
+
312
+	return
313
+}
314
+
315
+func (am *AccountManager) Unregister(account string) error {
316
+	casefoldedAccount, err := CasefoldName(account)
317
+	if err != nil {
318
+		return errAccountDoesNotExist
319
+	}
320
+
321
+	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
322
+	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
323
+	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
324
+	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
325
+	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
326
+
327
+	var clients []*Client
328
+
329
+	func() {
330
+		var credText string
331
+
332
+		am.serialCacheUpdateMutex.Lock()
333
+		defer am.serialCacheUpdateMutex.Unlock()
334
+
335
+		am.server.store.Update(func(tx *buntdb.Tx) error {
336
+			tx.Delete(accountKey)
337
+			tx.Delete(accountNameKey)
338
+			tx.Delete(verifiedKey)
339
+			tx.Delete(registeredTimeKey)
340
+			credText, err = tx.Get(credentialsKey)
341
+			tx.Delete(credentialsKey)
342
+			return nil
343
+		})
344
+
345
+		if err == nil {
346
+			var creds AccountCredentials
347
+			if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
348
+				certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
349
+				am.server.store.Update(func(tx *buntdb.Tx) error {
350
+					if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
351
+						tx.Delete(certFPKey)
352
+					}
353
+					return nil
354
+				})
355
+			}
356
+		}
357
+
358
+		am.Lock()
359
+		defer am.Unlock()
360
+		clients = am.accountToClients[casefoldedAccount]
361
+		delete(am.accountToClients, casefoldedAccount)
362
+		// TODO when registration of multiple nicks is fully implemented,
363
+		// save the nicks that were deleted from the store and delete them here:
364
+		delete(am.nickToAccount, casefoldedAccount)
365
+	}()
366
+
367
+	for _, client := range clients {
368
+		client.LogoutOfAccount()
369
+	}
370
+
371
+	return nil
372
+}
373
+
374
+func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
375
+	if client.certfp == "" {
376
+		return errAccountInvalidCredentials
377
+	}
378
+
379
+	var account string
380
+	var rawAccount rawClientAccount
381
+	certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
382
+
383
+	err := am.server.store.Update(func(tx *buntdb.Tx) error {
384
+		var err error
385
+		account, _ = tx.Get(certFPKey)
386
+		if account == "" {
387
+			return errAccountInvalidCredentials
388
+		}
389
+		rawAccount, err = am.loadRawAccount(tx, account)
390
+		if err != nil || !rawAccount.Verified {
391
+			return errAccountUnverified
392
+		}
393
+		return nil
394
+	})
395
+
396
+	if err != nil {
397
+		return err
398
+	}
399
+
400
+	// ok, we found an account corresponding to their certificate
401
+
402
+	am.Login(client, rawAccount.Name)
403
+	return nil
404
+}
405
+
406
+func (am *AccountManager) Login(client *Client, account string) {
407
+	client.LoginToAccount(account)
408
+
409
+	casefoldedAccount, _ := CasefoldName(account)
410
+	am.Lock()
411
+	defer am.Unlock()
412
+	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
413
+}
414
+
415
+func (am *AccountManager) Logout(client *Client) {
416
+	casefoldedAccount := client.Account()
417
+	if casefoldedAccount == "" || casefoldedAccount == "*" {
418
+		return
419
+	}
420
+
421
+	client.LogoutOfAccount()
422
+
423
+	am.Lock()
424
+	defer am.Unlock()
425
+
426
+	if client.LoggedIntoAccount() {
427
+		return
428
+	}
429
+
430
+	clients := am.accountToClients[casefoldedAccount]
431
+	if len(clients) <= 1 {
432
+		delete(am.accountToClients, casefoldedAccount)
433
+		return
434
+	}
435
+	remainingClients := make([]*Client, len(clients)-1)
436
+	remainingPos := 0
437
+	for currentPos := 0; currentPos < len(clients); currentPos++ {
438
+		if clients[currentPos] != client {
439
+			remainingClients[remainingPos] = clients[currentPos]
440
+			remainingPos++
441
+		}
442
+	}
443
+	am.accountToClients[casefoldedAccount] = remainingClients
444
+	return
445
+}
446
+
27 447
 var (
28 448
 	// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
29 449
 	// This can be moved to some other data structure/place if we need to load/unload mechs later.
@@ -31,95 +451,62 @@ var (
31 451
 		"PLAIN":    authPlainHandler,
32 452
 		"EXTERNAL": authExternalHandler,
33 453
 	}
34
-
35
-	// NoAccount is a placeholder which means that the user is not logged into an account.
36
-	NoAccount = ClientAccount{
37
-		Name: "*", // * is used until actual account name is set
38
-	}
39 454
 )
40 455
 
456
+// AccountCredentials stores the various methods for verifying accounts.
457
+type AccountCredentials struct {
458
+	PassphraseSalt []byte
459
+	PassphraseHash []byte
460
+	Certificate    string // fingerprint
461
+}
462
+
41 463
 // ClientAccount represents a user account.
42 464
 type ClientAccount struct {
43 465
 	// Name of the account.
44 466
 	Name string
45 467
 	// RegisteredAt represents the time that the account was registered.
46 468
 	RegisteredAt time.Time
47
-	// Clients that are currently logged into this account (useful for notifications).
48
-	Clients []*Client
469
+	Credentials  AccountCredentials
470
+	Verified     bool
49 471
 }
50 472
 
51
-// loadAccountCredentials loads an account's credentials from the store.
52
-func loadAccountCredentials(tx *buntdb.Tx, accountKey string) (*AccountCredentials, error) {
53
-	credText, err := tx.Get(fmt.Sprintf(keyAccountCredentials, accountKey))
54
-	if err != nil {
55
-		return nil, err
56
-	}
57
-
58
-	var creds AccountCredentials
59
-	err = json.Unmarshal([]byte(credText), &creds)
60
-	if err != nil {
61
-		return nil, err
62
-	}
63
-
64
-	return &creds, nil
473
+// convenience for passing around raw serialized account data
474
+type rawClientAccount struct {
475
+	Name         string
476
+	RegisteredAt string
477
+	Credentials  string
478
+	Verified     bool
65 479
 }
66 480
 
67
-// loadAccount loads an account from the store, note that the account must actually exist.
68
-func loadAccount(server *Server, tx *buntdb.Tx, accountKey string) *ClientAccount {
69
-	name, _ := tx.Get(fmt.Sprintf(keyAccountName, accountKey))
70
-	regTime, _ := tx.Get(fmt.Sprintf(keyAccountRegTime, accountKey))
71
-	regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
72
-	accountInfo := ClientAccount{
73
-		Name:         name,
74
-		RegisteredAt: time.Unix(regTimeInt, 0),
75
-		Clients:      []*Client{},
481
+// LoginToAccount logs the client into the given account.
482
+func (client *Client) LoginToAccount(account string) {
483
+	casefoldedAccount, err := CasefoldName(account)
484
+	if err != nil {
485
+		return
76 486
 	}
77
-	server.accounts[accountKey] = &accountInfo
78
-
79
-	return &accountInfo
80
-}
81 487
 
82
-// LoginToAccount logs the client into the given account.
83
-func (client *Client) LoginToAccount(account *ClientAccount) {
84
-	if client.account == account {
488
+	if client.Account() == casefoldedAccount {
85 489
 		// already logged into this acct, no changing necessary
86 490
 		return
87
-	} else if client.LoggedIntoAccount() {
88
-		// logout of existing acct
89
-		var newClientAccounts []*Client
90
-		for _, c := range account.Clients {
91
-			if c != client {
92
-				newClientAccounts = append(newClientAccounts, c)
93
-			}
94
-		}
95
-		account.Clients = newClientAccounts
96 491
 	}
97 492
 
98
-	account.Clients = append(account.Clients, client)
99
-	client.account = account
100
-	client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, account.Name))
493
+	client.SetAccountName(casefoldedAccount)
494
+	client.nickTimer.Touch()
495
+
496
+	client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), client.nickMaskString, casefoldedAccount))
101 497
 
102 498
 	//TODO(dan): This should output the AccountNotify message instead of the sasl accepted function below.
103 499
 }
104 500
 
105 501
 // LogoutOfAccount logs the client out of their current account.
106 502
 func (client *Client) LogoutOfAccount() {
107
-	account := client.account
108
-	if account == nil {
503
+	if client.Account() == "" {
109 504
 		// already logged out
110 505
 		return
111 506
 	}
112 507
 
113
-	// logout of existing acct
114
-	var newClientAccounts []*Client
115
-	for _, c := range account.Clients {
116
-		if c != client {
117
-			newClientAccounts = append(newClientAccounts, c)
118
-		}
119
-	}
120
-	account.Clients = newClientAccounts
121
-
122
-	client.account = nil
508
+	client.SetAccountName("")
509
+	client.nickTimer.Touch()
123 510
 
124 511
 	// dispatch account-notify
125 512
 	for friend := range client.Friends(caps.AccountNotify) {
@@ -129,11 +516,11 @@ func (client *Client) LogoutOfAccount() {
129 516
 
130 517
 // successfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
131 518
 func (client *Client) successfulSaslAuth(rb *ResponseBuffer) {
132
-	rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, client.account.Name, fmt.Sprintf("You are now logged in as %s", client.account.Name))
519
+	rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, client.AccountName(), fmt.Sprintf("You are now logged in as %s", client.AccountName()))
133 520
 	rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful"))
134 521
 
135 522
 	// dispatch account-notify
136 523
 	for friend := range client.Friends(caps.AccountNotify) {
137
-		friend.Send(nil, client.nickMaskString, "ACCOUNT", client.account.Name)
524
+		friend.Send(nil, client.nickMaskString, "ACCOUNT", client.AccountName())
138 525
 	}
139 526
 }

+ 7
- 5
irc/channel.go View File

@@ -383,13 +383,13 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
383 383
 	for _, member := range channel.Members() {
384 384
 		if member == client {
385 385
 			if member.capabilities.Has(caps.ExtendedJoin) {
386
-				rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
386
+				rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
387 387
 			} else {
388 388
 				rb.Add(nil, client.nickMaskString, "JOIN", channel.name)
389 389
 			}
390 390
 		} else {
391 391
 			if member.capabilities.Has(caps.ExtendedJoin) {
392
-				member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
392
+				member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
393 393
 			} else {
394 394
 				member.Send(nil, client.nickMaskString, "JOIN", channel.name)
395 395
 			}
@@ -407,7 +407,9 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
407 407
 	// give channel mode if necessary
408 408
 	newChannel := firstJoin && !channel.IsRegistered()
409 409
 	var givenMode *modes.Mode
410
-	if client.AccountName() == channel.registeredFounder {
410
+	account := client.Account()
411
+	cffounder, _ := CasefoldName(channel.registeredFounder)
412
+	if account != "" && account == cffounder {
411 413
 		givenMode = &modes.ChannelFounder
412 414
 	} else if newChannel {
413 415
 		givenMode = &modes.ChannelOperator
@@ -419,7 +421,7 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
419 421
 	}
420 422
 
421 423
 	if client.capabilities.Has(caps.ExtendedJoin) {
422
-		rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
424
+		rb.Add(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
423 425
 	} else {
424 426
 		rb.Add(nil, client.nickMaskString, "JOIN", channel.name)
425 427
 	}
@@ -526,7 +528,7 @@ func (channel *Channel) CanSpeak(client *Client) bool {
526 528
 	if channel.flags[modes.Moderated] && !channel.ClientIsAtLeast(client, modes.Voice) {
527 529
 		return false
528 530
 	}
529
-	if channel.flags[modes.RegisteredOnly] && client.account == &NoAccount {
531
+	if channel.flags[modes.RegisteredOnly] && client.Account() == "" {
530 532
 		return false
531 533
 	}
532 534
 	return true

+ 2
- 2
irc/chanserv.go View File

@@ -70,13 +70,13 @@ func (server *Server) chanservRegisterHandler(client *Client, channelName string
70 70
 		return
71 71
 	}
72 72
 
73
-	if client.account == &NoAccount {
73
+	if client.Account() == "" {
74 74
 		rb.ChanServNotice(client.t("You must be logged in to register a channel"))
75 75
 		return
76 76
 	}
77 77
 
78 78
 	// this provides the synchronization that allows exactly one registration of the channel:
79
-	err = channelInfo.SetRegistered(client.AccountName())
79
+	err = channelInfo.SetRegistered(client.Account())
80 80
 	if err != nil {
81 81
 		rb.ChanServNotice(err.Error())
82 82
 		return

+ 16
- 14
irc/client.go View File

@@ -36,7 +36,8 @@ var (
36 36
 
37 37
 // Client is an IRC client.
38 38
 type Client struct {
39
-	account            *ClientAccount
39
+	account            string
40
+	accountName        string
40 41
 	atime              time.Time
41 42
 	authorized         bool
42 43
 	awayMessage        string
@@ -62,6 +63,7 @@ type Client struct {
62 63
 	nickCasefolded     string
63 64
 	nickMaskCasefolded string
64 65
 	nickMaskString     string // cache for nickmask string since it's used with lots of replies
66
+	nickTimer          *NickTimer
65 67
 	operName           string
66 68
 	proxiedIP          net.IP // actual remote IP if using the PROXY protocol
67 69
 	quitMessage        string
@@ -96,7 +98,6 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
96 98
 		flags:          make(map[modes.Mode]bool),
97 99
 		server:         server,
98 100
 		socket:         &socket,
99
-		account:        &NoAccount,
100 101
 		nick:           "*", // * is used until actual nick is given
101 102
 		nickCasefolded: "*",
102 103
 		nickMaskString: "*", // * is used until actual nick is given
@@ -217,6 +218,8 @@ func (client *Client) run() {
217 218
 	client.idletimer = NewIdleTimer(client)
218 219
 	client.idletimer.Start()
219 220
 
221
+	client.nickTimer = NewNickTimer(client)
222
+
220 223
 	// Set the hostname for this client
221 224
 	// (may be overridden by a later PROXY command from stunnel)
222 225
 	client.rawHostname = utils.AddrLookupHostname(client.socket.conn.RemoteAddr())
@@ -299,7 +302,6 @@ func (client *Client) Register() {
299 302
 	client.TryResume()
300 303
 
301 304
 	// finish registration
302
-	client.Touch()
303 305
 	client.updateNickMask("")
304 306
 	client.server.monitorManager.AlertAbout(client, true)
305 307
 }
@@ -338,8 +340,8 @@ func (client *Client) TryResume() {
338 340
 		return
339 341
 	}
340 342
 
341
-	oldAccountName := oldClient.AccountName()
342
-	newAccountName := client.AccountName()
343
+	oldAccountName := oldClient.Account()
344
+	newAccountName := client.Account()
343 345
 
344 346
 	if oldAccountName == "" || newAccountName == "" || oldAccountName != newAccountName {
345 347
 		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old and new clients must be logged into the same account"))
@@ -406,7 +408,7 @@ func (client *Client) TryResume() {
406 408
 			}
407 409
 
408 410
 			if member.capabilities.Has(caps.ExtendedJoin) {
409
-				member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
411
+				member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
410 412
 			} else {
411 413
 				member.Send(nil, client.nickMaskString, "JOIN", channel.name)
412 414
 			}
@@ -589,7 +591,7 @@ func (client *Client) AllNickmasks() []string {
589 591
 
590 592
 // LoggedIntoAccount returns true if this client is logged into an account.
591 593
 func (client *Client) LoggedIntoAccount() bool {
592
-	return client.account != nil && client.account != &NoAccount
594
+	return client.Account() != ""
593 595
 }
594 596
 
595 597
 // RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses.
@@ -687,6 +689,8 @@ func (client *Client) destroy(beingResumed bool) {
687 689
 		client.idletimer.Stop()
688 690
 	}
689 691
 
692
+	client.server.accounts.Logout(client)
693
+
690 694
 	client.socket.Close()
691 695
 
692 696
 	// send quit messages to friends
@@ -723,11 +727,11 @@ func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags *m
723 727
 // Adds account-tag to the line as well.
724 728
 func (client *Client) SendFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
725 729
 	// attach account-tag
726
-	if client.capabilities.Has(caps.AccountTag) && from.account != &NoAccount {
730
+	if client.capabilities.Has(caps.AccountTag) && client.LoggedIntoAccount() {
727 731
 		if tags == nil {
728
-			tags = ircmsg.MakeTags("account", from.account.Name)
732
+			tags = ircmsg.MakeTags("account", from.AccountName())
729 733
 		} else {
730
-			(*tags)["account"] = ircmsg.MakeTagValue(from.account.Name)
734
+			(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
731 735
 		}
732 736
 	}
733 737
 	// attach message-id
@@ -772,10 +776,8 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage) error {
772 776
 	maxlenTags, maxlenRest := client.maxlens()
773 777
 	line, err := message.LineMaxLen(maxlenTags, maxlenRest)
774 778
 	if err != nil {
775
-		// try not to fail quietly - especially useful when running tests, as a note to dig deeper
776
-		// log.Println("Error assembling message:")
777
-		// spew.Dump(message)
778
-		// debug.PrintStack()
779
+		logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack())
780
+		client.server.logger.Error("internal", logline)
779 781
 
780 782
 		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
781 783
 		line, _ := message.Line()

+ 9
- 0
irc/client_lookup_set.go View File

@@ -98,6 +98,12 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
98 98
 		return err
99 99
 	}
100 100
 
101
+	var reservedAccount string
102
+	reservation := client.server.AccountConfig().NickReservation
103
+	if reservation != NickReservationDisabled {
104
+		reservedAccount = client.server.accounts.NickToAccount(newcfnick)
105
+	}
106
+
101 107
 	clients.Lock()
102 108
 	defer clients.Unlock()
103 109
 
@@ -107,6 +113,9 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
107 113
 	if currentNewEntry != nil && currentNewEntry != client {
108 114
 		return errNicknameInUse
109 115
 	}
116
+	if reservation == NickReservationStrict && reservedAccount != client.Account() {
117
+		return errNicknameReserved
118
+	}
110 119
 	clients.byNick[newcfnick] = client
111 120
 	client.updateNickMask(newNick)
112 121
 	return nil

+ 11
- 7
irc/commands.go View File

@@ -39,12 +39,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
39 39
 		client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
40 40
 		return false
41 41
 	}
42
-	if !cmd.leaveClientActive {
43
-		client.Active()
44
-	}
45
-	if !cmd.leaveClientIdle {
46
-		client.Touch()
47
-	}
42
+
48 43
 	rb := NewResponseBuffer(client)
49 44
 	rb.Label = GetLabel(msg)
50 45
 
@@ -57,6 +52,14 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
57 52
 		server.tryRegister(client)
58 53
 	}
59 54
 
55
+	if !cmd.leaveClientIdle {
56
+		client.Touch()
57
+	}
58
+
59
+	if !cmd.leaveClientActive {
60
+		client.Active()
61
+	}
62
+
60 63
 	return exiting
61 64
 }
62 65
 
@@ -67,7 +70,7 @@ func init() {
67 70
 	Commands = map[string]Command{
68 71
 		"ACC": {
69 72
 			handler:   accHandler,
70
-			minParams: 3,
73
+			minParams: 2,
71 74
 		},
72 75
 		"AMBIANCE": {
73 76
 			handler:   sceneHandler,
@@ -98,6 +101,7 @@ func init() {
98 101
 		"DEBUG": {
99 102
 			handler:   debugHandler,
100 103
 			minParams: 1,
104
+			oper:      true,
101 105
 		},
102 106
 		"DLINE": {
103 107
 			handler:   dlineHandler,

+ 52
- 7
irc/config.go View File

@@ -8,6 +8,7 @@ package irc
8 8
 import (
9 9
 	"crypto/tls"
10 10
 	"encoding/json"
11
+	"errors"
11 12
 	"fmt"
12 13
 	"io/ioutil"
13 14
 	"log"
@@ -57,11 +58,49 @@ func (conf *PassConfig) PasswordBytes() []byte {
57 58
 	return bytes
58 59
 }
59 60
 
61
+type NickReservation int
62
+
63
+const (
64
+	NickReservationDisabled NickReservation = iota
65
+	NickReservationWithTimeout
66
+	NickReservationStrict
67
+)
68
+
69
+func (nr *NickReservation) UnmarshalYAML(unmarshal func(interface{}) error) error {
70
+	var orig, raw string
71
+	var err error
72
+	if err = unmarshal(&orig); err != nil {
73
+		return err
74
+	}
75
+	if raw, err = Casefold(orig); err != nil {
76
+		return err
77
+	}
78
+	if raw == "disabled" || raw == "false" || raw == "" {
79
+		*nr = NickReservationDisabled
80
+	} else if raw == "timeout" {
81
+		*nr = NickReservationWithTimeout
82
+	} else if raw == "strict" {
83
+		*nr = NickReservationStrict
84
+	} else {
85
+		return errors.New(fmt.Sprintf("invalid nick-reservation value: %s", orig))
86
+	}
87
+	return nil
88
+}
89
+
90
+type AccountConfig struct {
91
+	Registration           AccountRegistrationConfig
92
+	AuthenticationEnabled  bool            `yaml:"authentication-enabled"`
93
+	NickReservation        NickReservation `yaml:"nick-reservation"`
94
+	NickReservationTimeout time.Duration   `yaml:"nick-reservation-timeout"`
95
+}
96
+
60 97
 // AccountRegistrationConfig controls account registration.
61 98
 type AccountRegistrationConfig struct {
62
-	Enabled          bool
63
-	EnabledCallbacks []string `yaml:"enabled-callbacks"`
64
-	Callbacks        struct {
99
+	Enabled                bool
100
+	EnabledCallbacks       []string      `yaml:"enabled-callbacks"`
101
+	EnabledCredentialTypes []string      `yaml:"-"`
102
+	VerifyTimeout          time.Duration `yaml:"verify-timeout"`
103
+	Callbacks              struct {
65 104
 		Mailto struct {
66 105
 			Server string
67 106
 			Port   int
@@ -180,10 +219,7 @@ type Config struct {
180 219
 		Path string
181 220
 	}
182 221
 
183
-	Accounts struct {
184
-		Registration          AccountRegistrationConfig
185
-		AuthenticationEnabled bool `yaml:"authentication-enabled"`
186
-	}
222
+	Accounts AccountConfig
187 223
 
188 224
 	Channels struct {
189 225
 		DefaultModes *string `yaml:"default-modes"`
@@ -469,6 +505,15 @@ func LoadConfig(filename string) (config *Config, err error) {
469 505
 	}
470 506
 	config.Logging = newLogConfigs
471 507
 
508
+	// hardcode this for now
509
+	config.Accounts.Registration.EnabledCredentialTypes = []string{"passphrase", "certfp"}
510
+	for i, name := range config.Accounts.Registration.EnabledCallbacks {
511
+		if name == "none" {
512
+			// we store "none" as "*" internally
513
+			config.Accounts.Registration.EnabledCallbacks[i] = "*"
514
+		}
515
+	}
516
+
472 517
 	config.Server.MaxSendQBytes, err = bytefmt.ToBytes(config.Server.MaxSendQString)
473 518
 	if err != nil {
474 519
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

+ 19
- 12
irc/errors.go View File

@@ -9,18 +9,25 @@ import "errors"
9 9
 
10 10
 // Runtime Errors
11 11
 var (
12
-	errAccountCreation          = errors.New("Account could not be created")
13
-	errCertfpAlreadyExists      = errors.New("An account already exists with your certificate")
14
-	errChannelAlreadyRegistered = errors.New("Channel is already registered")
15
-	errChannelNameInUse         = errors.New("Channel name in use")
16
-	errInvalidChannelName       = errors.New("Invalid channel name")
17
-	errMonitorLimitExceeded     = errors.New("Monitor limit exceeded")
18
-	errNickMissing              = errors.New("nick missing")
19
-	errNicknameInUse            = errors.New("nickname in use")
20
-	errNoExistingBan            = errors.New("Ban does not exist")
21
-	errNoSuchChannel            = errors.New("No such channel")
22
-	errRenamePrivsNeeded        = errors.New("Only chanops can rename channels")
23
-	errSaslFail                 = errors.New("SASL failed")
12
+	errAccountAlreadyRegistered  = errors.New("Account already exists")
13
+	errAccountCreation           = errors.New("Account could not be created")
14
+	errAccountDoesNotExist       = errors.New("Account does not exist")
15
+	errAccountVerificationFailed = errors.New("Account verification failed")
16
+	errAccountUnverified         = errors.New("Account is not yet verified")
17
+	errAccountAlreadyVerified    = errors.New("Account is already verified")
18
+	errAccountInvalidCredentials = errors.New("Invalid account credentials")
19
+	errCertfpAlreadyExists       = errors.New("An account already exists with your certificate")
20
+	errChannelAlreadyRegistered  = errors.New("Channel is already registered")
21
+	errChannelNameInUse          = errors.New("Channel name in use")
22
+	errInvalidChannelName        = errors.New("Invalid channel name")
23
+	errMonitorLimitExceeded      = errors.New("Monitor limit exceeded")
24
+	errNickMissing               = errors.New("nick missing")
25
+	errNicknameInUse             = errors.New("nickname in use")
26
+	errNicknameReserved          = errors.New("nickname is reserved")
27
+	errNoExistingBan             = errors.New("Ban does not exist")
28
+	errNoSuchChannel             = errors.New("No such channel")
29
+	errRenamePrivsNeeded         = errors.New("Only chanops can rename channels")
30
+	errSaslFail                  = errors.New("SASL failed")
24 31
 )
25 32
 
26 33
 // Socket Errors

+ 27
- 1
irc/getters.go View File

@@ -56,6 +56,12 @@ func (server *Server) ChannelRegistrationEnabled() bool {
56 56
 	return server.channelRegistrationEnabled
57 57
 }
58 58
 
59
+func (server *Server) AccountConfig() *AccountConfig {
60
+	server.configurableStateMutex.RLock()
61
+	defer server.configurableStateMutex.RUnlock()
62
+	return server.accountConfig
63
+}
64
+
59 65
 func (client *Client) Nick() string {
60 66
 	client.stateMutex.RLock()
61 67
 	defer client.stateMutex.RUnlock()
@@ -104,10 +110,30 @@ func (client *Client) Destroyed() bool {
104 110
 	return client.isDestroyed
105 111
 }
106 112
 
113
+func (client *Client) Account() string {
114
+	client.stateMutex.RLock()
115
+	defer client.stateMutex.RUnlock()
116
+	return client.account
117
+}
118
+
107 119
 func (client *Client) AccountName() string {
108 120
 	client.stateMutex.RLock()
109 121
 	defer client.stateMutex.RUnlock()
110
-	return client.account.Name
122
+	if client.accountName == "" {
123
+		return "*"
124
+	}
125
+	return client.accountName
126
+}
127
+
128
+func (client *Client) SetAccountName(account string) {
129
+	var casefoldedAccount string
130
+	if account != "" {
131
+		casefoldedAccount, _ = CasefoldName(account)
132
+	}
133
+	client.stateMutex.Lock()
134
+	defer client.stateMutex.Unlock()
135
+	client.account = casefoldedAccount
136
+	client.accountName = account
111 137
 }
112 138
 
113 139
 func (client *Client) HasMode(mode modes.Mode) bool {

+ 88
- 192
irc/handlers.go View File

@@ -11,7 +11,6 @@ import (
11 11
 	"encoding/base64"
12 12
 	"encoding/json"
13 13
 	"fmt"
14
-	"log"
15 14
 	"net"
16 15
 	"os"
17 16
 	"runtime"
@@ -42,6 +41,8 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
42 41
 		return accRegisterHandler(server, client, msg, rb)
43 42
 	} else if subcommand == "verify" {
44 43
 		rb.Notice(client.t("VERIFY is not yet implemented"))
44
+	} else if subcommand == "unregister" {
45
+		return accUnregisterHandler(server, client, msg, rb)
45 46
 	} else {
46 47
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
47 48
 	}
@@ -49,18 +50,45 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
49 50
 	return false
50 51
 }
51 52
 
53
+// ACC UNREGISTER <accountname>
54
+func accUnregisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
55
+	// get and sanitise account name
56
+	account := strings.TrimSpace(msg.Params[1])
57
+	casefoldedAccount, err := CasefoldName(account)
58
+	// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
59
+	if err != nil || msg.Params[1] == "*" {
60
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid"))
61
+		return false
62
+	}
63
+
64
+	if !(account == client.Account() || client.HasRoleCapabs("unregister")) {
65
+		rb.Add(nil, server.name, ERR_NOPRIVS, client.Nick(), account, client.t("Insufficient oper privs"))
66
+		return false
67
+	}
68
+
69
+	err = server.accounts.Unregister(account)
70
+	// TODO better responses all around here
71
+	if err != nil {
72
+		errorMsg := fmt.Sprintf("Unknown error while unregistering account %s", casefoldedAccount)
73
+		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.Nick(), msg.Command, errorMsg)
74
+		return false
75
+	}
76
+	rb.Notice(fmt.Sprintf("Successfully unregistered account %s", casefoldedAccount))
77
+	return false
78
+}
79
+
52 80
 // ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
53 81
 func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
54 82
 	// make sure reg is enabled
55
-	if !server.accountRegistration.Enabled {
83
+	if !server.AccountConfig().Registration.Enabled {
56 84
 		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
57 85
 		return false
58 86
 	}
59 87
 
60 88
 	// clients can't reg new accounts if they're already logged in
61 89
 	if client.LoggedIntoAccount() {
62
-		if server.accountRegistration.AllowMultiplePerConnection {
63
-			client.LogoutOfAccount()
90
+		if server.AccountConfig().Registration.AllowMultiplePerConnection {
91
+			server.accounts.Logout(client)
64 92
 		} else {
65 93
 			rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account"))
66 94
 			return false
@@ -76,36 +104,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
76 104
 		return false
77 105
 	}
78 106
 
79
-	// check whether account exists
80
-	// do it all in one write tx to prevent races
81
-	err = server.store.Update(func(tx *buntdb.Tx) error {
82
-		accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
83
-
84
-		_, err := tx.Get(accountKey)
85
-		if err != buntdb.ErrNotFound {
86
-			//TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
87
-			rb.Add(nil, server.name, ERR_ACCOUNT_ALREADY_EXISTS, client.nick, account, client.t("Account already exists"))
88
-			return errAccountCreation
89
-		}
90
-
91
-		registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
92
-
93
-		tx.Set(accountKey, "1", nil)
94
-		tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil)
95
-		tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
96
-		return nil
97
-	})
98
-
99
-	// account could not be created and relevant numerics have been dispatched, abort
100
-	if err != nil {
101
-		if err != errAccountCreation {
102
-			rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", client.t("Could not register"))
103
-			log.Println("Could not save registration initial data:", err.Error())
104
-		}
107
+	if len(msg.Params) < 4 {
108
+		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
105 109
 		return false
106 110
 	}
107 111
 
108
-	// account didn't already exist, continue with account creation and dispatching verification (if required)
109 112
 	callback := strings.ToLower(msg.Params[2])
110 113
 	var callbackNamespace, callbackValue string
111 114
 
@@ -115,14 +118,14 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
115 118
 		callbackValues := strings.SplitN(callback, ":", 2)
116 119
 		callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
117 120
 	} else {
118
-		callbackNamespace = server.accountRegistration.EnabledCallbacks[0]
121
+		callbackNamespace = server.AccountConfig().Registration.EnabledCallbacks[0]
119 122
 		callbackValue = callback
120 123
 	}
121 124
 
122 125
 	// ensure the callback namespace is valid
123 126
 	// need to search callback list, maybe look at using a map later?
124 127
 	var callbackValid bool
125
-	for _, name := range server.accountRegistration.EnabledCallbacks {
128
+	for _, name := range server.AccountConfig().Registration.EnabledCallbacks {
126 129
 		if callbackNamespace == name {
127 130
 			callbackValid = true
128 131
 		}
@@ -130,7 +133,6 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
130 133
 
131 134
 	if !callbackValid {
132 135
 		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, client.t("Callback namespace is not supported"))
133
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
134 136
 		return false
135 137
 	}
136 138
 
@@ -140,116 +142,62 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
140 142
 	if len(msg.Params) > 4 {
141 143
 		credentialType = strings.ToLower(msg.Params[3])
142 144
 		credentialValue = msg.Params[4]
143
-	} else if len(msg.Params) == 4 {
145
+	} else {
146
+		// exactly 4 params
144 147
 		credentialType = "passphrase" // default from the spec
145 148
 		credentialValue = msg.Params[3]
146
-	} else {
147
-		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
148
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
149
-		return false
150 149
 	}
151 150
 
152 151
 	// ensure the credential type is valid
153 152
 	var credentialValid bool
154
-	for _, name := range server.accountRegistration.EnabledCredentialTypes {
153
+	for _, name := range server.AccountConfig().Registration.EnabledCredentialTypes {
155 154
 		if credentialType == name {
156 155
 			credentialValid = true
157 156
 		}
158 157
 	}
159 158
 	if credentialType == "certfp" && client.certfp == "" {
160 159
 		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
161
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
162 160
 		return false
163 161
 	}
164 162
 
165 163
 	if !credentialValid {
166 164
 		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
167
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
168 165
 		return false
169 166
 	}
170 167
 
171
-	// store details
172
-	err = server.store.Update(func(tx *buntdb.Tx) error {
173
-		// certfp special lookup key
174
-		if credentialType == "certfp" {
175
-			assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
176
-
177
-			// make sure certfp doesn't already exist because that'd be silly
178
-			_, err := tx.Get(assembledKeyCertToAccount)
179
-			if err != buntdb.ErrNotFound {
180
-				return errCertfpAlreadyExists
181
-			}
182
-
183
-			tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil)
184
-		}
185
-
186
-		// make creds
187
-		var creds AccountCredentials
188
-
189
-		// always set passphrase salt
190
-		creds.PassphraseSalt, err = passwd.NewSalt()
191
-		if err != nil {
192
-			return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
193
-		}
194
-
195
-		if credentialType == "certfp" {
196
-			creds.Certificate = client.certfp
197
-		} else if credentialType == "passphrase" {
198
-			creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, credentialValue)
199
-			if err != nil {
200
-				return fmt.Errorf("Could not hash password: %s", err)
201
-			}
202
-		}
203
-		credText, err := json.Marshal(creds)
204
-		if err != nil {
205
-			return fmt.Errorf("Could not marshal creds: %s", err)
206
-		}
207
-		tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil)
208
-
209
-		return nil
210
-	})
211
-
212
-	// details could not be stored and relevant numerics have been dispatched, abort
168
+	var passphrase, certfp string
169
+	if credentialType == "certfp" {
170
+		certfp = client.certfp
171
+	} else if credentialType == "passphrase" {
172
+		passphrase = credentialValue
173
+	}
174
+	err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
213 175
 	if err != nil {
214
-		errMsg := "Could not register"
176
+		msg := "Unknown"
177
+		code := ERR_UNKNOWNERROR
215 178
 		if err == errCertfpAlreadyExists {
216
-			errMsg = "An account already exists for your certificate fingerprint"
179
+			msg = "An account already exists for your certificate fingerprint"
180
+		} else if err == errAccountAlreadyRegistered {
181
+			msg = "Account already exists"
182
+			code = ERR_ACCOUNT_ALREADY_EXISTS
217 183
 		}
218
-		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", errMsg)
219
-		log.Println("Could not save registration creds:", err.Error())
220
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
184
+		if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
185
+			msg = err.Error()
186
+		}
187
+		rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg))
221 188
 		return false
222 189
 	}
223 190
 
224 191
 	// automatically complete registration
225 192
 	if callbackNamespace == "*" {
226
-		err = server.store.Update(func(tx *buntdb.Tx) error {
227
-			tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil)
228
-
229
-			// load acct info inside store tx
230
-			account := ClientAccount{
231
-				Name:         strings.TrimSpace(msg.Params[1]),
232
-				RegisteredAt: time.Now(),
233
-				Clients:      []*Client{client},
234
-			}
235
-			//TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
236
-			server.accounts[casefoldedAccount] = &account
237
-			client.account = &account
238
-
239
-			rb.Add(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, account.Name, client.t("Account created"))
240
-			rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name))
241
-			rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
242
-			server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), account.Name, client.nickMaskString))
243
-			return nil
244
-		})
193
+		err := server.accounts.Verify(client, casefoldedAccount, "")
245 194
 		if err != nil {
246
-			rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", "REGISTER", client.t("Could not register"))
247
-			log.Println("Could not save verification confirmation (*):", err.Error())
248
-			removeFailedAccRegisterData(server.store, casefoldedAccount)
249 195
 			return false
250 196
 		}
251
-
252
-		return false
197
+		client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, casefoldedAccount, client.t("Account created"))
198
+		client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
199
+		client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
200
+		server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString))
253 201
 	}
254 202
 
255 203
 	// dispatch callback
@@ -261,7 +209,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
261 209
 // AUTHENTICATE [<mechanism>|<data>|*]
262 210
 func authenticateHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
263 211
 	// sasl abort
264
-	if !server.accountAuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
212
+	if !server.AccountConfig().AuthenticationEnabled || len(msg.Params) == 1 && msg.Params[0] == "*" {
265 213
 		rb.Add(nil, server.name, ERR_SASLABORTED, client.nick, client.t("SASL authentication aborted"))
266 214
 		client.saslInProgress = false
267 215
 		client.saslMechanism = ""
@@ -374,40 +322,11 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
374 322
 		return false
375 323
 	}
376 324
 
377
-	// load and check acct data all in one update to prevent races.
378
-	// as noted elsewhere, change to proper locking for Account type later probably
379
-	err = server.store.Update(func(tx *buntdb.Tx) error {
380
-		// confirm account is verified
381
-		_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
382
-		if err != nil {
383
-			return errSaslFail
384
-		}
385
-
386
-		creds, err := loadAccountCredentials(tx, accountKey)
387
-		if err != nil {
388
-			return err
389
-		}
390
-
391
-		// ensure creds are valid
392
-		password := string(splitValue[2])
393
-		if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(password) < 1 {
394
-			return errSaslFail
395
-		}
396
-		err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, password)
397
-
398
-		// succeeded, load account info if necessary
399
-		account, exists := server.accounts[accountKey]
400
-		if !exists {
401
-			account = loadAccount(server, tx, accountKey)
402
-		}
403
-
404
-		client.LoginToAccount(account)
405
-
406
-		return err
407
-	})
408
-
325
+	password := string(splitValue[2])
326
+	err = server.accounts.AuthenticateByPassphrase(client, accountKey, password)
409 327
 	if err != nil {
410
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
328
+		msg := authErrorToMessage(server, err)
329
+		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
411 330
 		return false
412 331
 	}
413 332
 
@@ -415,6 +334,16 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
415 334
 	return false
416 335
 }
417 336
 
337
+func authErrorToMessage(server *Server, err error) (msg string) {
338
+	if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials {
339
+		msg = err.Error()
340
+	} else {
341
+		server.logger.Error("internal", fmt.Sprintf("sasl authentication failure: %v", err))
342
+		msg = "Unknown"
343
+	}
344
+	return
345
+}
346
+
418 347
 // AUTHENTICATE EXTERNAL
419 348
 func authExternalHandler(server *Server, client *Client, mechanism string, value []byte, rb *ResponseBuffer) bool {
420 349
 	if client.certfp == "" {
@@ -422,44 +351,10 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
422 351
 		return false
423 352
 	}
424 353
 
425
-	err := server.store.Update(func(tx *buntdb.Tx) error {
426
-		// certfp lookup key
427
-		accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, client.certfp))
428
-		if err != nil {
429
-			return errSaslFail
430
-		}
431
-
432
-		// confirm account exists
433
-		_, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey))
434
-		if err != nil {
435
-			return errSaslFail
436
-		}
437
-
438
-		// confirm account is verified
439
-		_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
440
-		if err != nil {
441
-			return errSaslFail
442
-		}
443
-
444
-		// confirm the certfp in that account's credentials
445
-		creds, err := loadAccountCredentials(tx, accountKey)
446
-		if err != nil || creds.Certificate != client.certfp {
447
-			return errSaslFail
448
-		}
449
-
450
-		// succeeded, load account info if necessary
451
-		account, exists := server.accounts[accountKey]
452
-		if !exists {
453
-			account = loadAccount(server, tx, accountKey)
454
-		}
455
-
456
-		client.LoginToAccount(account)
457
-
458
-		return nil
459
-	})
460
-
354
+	err := server.accounts.AuthenticateByCertFP(client)
461 355
 	if err != nil {
462
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed"))
356
+		msg := authErrorToMessage(server, err)
357
+		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
463 358
 		return false
464 359
 	}
465 360
 
@@ -582,11 +477,16 @@ func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respon
582 477
 
583 478
 // DEBUG <subcmd>
584 479
 func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
585
-	if !client.flags[modes.Operator] {
480
+	param, err := Casefold(msg.Params[0])
481
+	if err != nil {
482
+		return false
483
+	}
484
+
485
+	if !client.HasMode(modes.Operator) {
586 486
 		return false
587 487
 	}
588 488
 
589
-	switch msg.Params[0] {
489
+	switch param {
590 490
 	case "GCSTATS":
591 491
 		stats := debug.GCStats{
592 492
 			Pause:          make([]time.Duration, 10),
@@ -2107,7 +2007,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2107 2007
 	}
2108 2008
 
2109 2009
 	founder := channel.Founder()
2110
-	if founder != "" && founder != client.AccountName() {
2010
+	if founder != "" && founder != client.Account() {
2111 2011
 		//TODO(dan): Change this to ERR_CANNOTRENAME
2112 2012
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "RENAME", oldName, client.t("Only channel founders can change registered channels"))
2113 2013
 		return false
@@ -2130,11 +2030,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2130 2030
 		} else {
2131 2031
 			mcl.Send(nil, mcl.nickMaskString, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
2132 2032
 			if mcl.capabilities.Has(caps.ExtendedJoin) {
2133
-				accountName := "*"
2134
-				if mcl.account != nil {
2135
-					accountName = mcl.account.Name
2136
-				}
2137
-				mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, accountName, mcl.realname)
2033
+				mcl.Send(nil, mcl.nickMaskString, "JOIN", newName, mcl.AccountName(), mcl.realname)
2138 2034
 			} else {
2139 2035
 				mcl.Send(nil, mcl.nickMaskString, "JOIN", newName)
2140 2036
 			}

+ 77
- 0
irc/idletimer.go View File

@@ -165,3 +165,80 @@ func (it *IdleTimer) quitMessage(state TimerState) string {
165 165
 		return ""
166 166
 	}
167 167
 }
168
+
169
+// NickTimer manages timing out of clients who are squatting reserved nicks
170
+type NickTimer struct {
171
+	sync.Mutex // tier 1
172
+
173
+	// immutable after construction
174
+	timeout time.Duration
175
+	client  *Client
176
+
177
+	// mutable
178
+	nick           string
179
+	accountForNick string
180
+	account        string
181
+	timer          *time.Timer
182
+}
183
+
184
+// NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
185
+func NewNickTimer(client *Client) *NickTimer {
186
+	config := client.server.AccountConfig()
187
+	if config.NickReservation != NickReservationWithTimeout {
188
+		return nil
189
+	}
190
+	nt := NickTimer{
191
+		client:  client,
192
+		timeout: config.NickReservationTimeout,
193
+	}
194
+	return &nt
195
+}
196
+
197
+// Touch records a nick change and updates the timer as necessary
198
+func (nt *NickTimer) Touch() {
199
+	if nt == nil {
200
+		return
201
+	}
202
+
203
+	nick := nt.client.NickCasefolded()
204
+	account := nt.client.Account()
205
+	accountForNick := nt.client.server.accounts.NickToAccount(nick)
206
+
207
+	var shouldWarn bool
208
+
209
+	func() {
210
+		nt.Lock()
211
+		defer nt.Unlock()
212
+		// the timer will not reset as long as the squatter is targeting the same account
213
+		accountChanged := accountForNick != nt.accountForNick
214
+		// change state
215
+		nt.nick = nick
216
+		nt.account = account
217
+		nt.accountForNick = accountForNick
218
+		delinquent := accountForNick != "" && accountForNick != account
219
+
220
+		if nt.timer != nil && (!delinquent || accountChanged) {
221
+			nt.timer.Stop()
222
+			nt.timer = nil
223
+		}
224
+		if delinquent && accountChanged {
225
+			nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
226
+			shouldWarn = true
227
+		}
228
+	}()
229
+
230
+	if shouldWarn {
231
+		nt.sendWarning()
232
+	}
233
+}
234
+
235
+func (nt *NickTimer) sendWarning() {
236
+	baseNotice := "Nickname is reserved; you must change it or authenticate to NickServ within %v"
237
+	nt.client.Notice(fmt.Sprintf(nt.client.t(baseNotice), nt.timeout))
238
+}
239
+
240
+func (nt *NickTimer) processTimeout() {
241
+	baseMsg := "Nick is reserved and authentication timeout expired: %v"
242
+	nt.client.Quit(fmt.Sprintf(nt.client.t(baseMsg), nt.timeout))
243
+	nt.client.destroy(false)
244
+}

+ 6
- 0
irc/nickname.go View File

@@ -17,6 +17,7 @@ var (
17 17
 		"=scene=":  true, // used for rp commands
18 18
 		"chanserv": true,
19 19
 		"nickserv": true,
20
+		"hostserv": true,
20 21
 	}
21 22
 )
22 23
 
@@ -45,11 +46,16 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
45 46
 	if err == errNicknameInUse {
46 47
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, client.nick, nickname, client.t("Nickname is already in use"))
47 48
 		return false
49
+	} else if err == errNicknameReserved {
50
+		client.Send(nil, server.name, ERR_NICKNAMEINUSE, client.nick, nickname, client.t("Nickname is reserved by a different account"))
51
+		return false
48 52
 	} else if err != nil {
49 53
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "NICK", fmt.Sprintf(client.t("Could not set or change nickname: %s"), err.Error()))
50 54
 		return false
51 55
 	}
52 56
 
57
+	client.nickTimer.Touch()
58
+
53 59
 	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
54 60
 	if hadNick {
55 61
 		target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), origNick, nickname))

+ 21
- 170
irc/nickserv.go View File

@@ -4,16 +4,11 @@
4 4
 package irc
5 5
 
6 6
 import (
7
-	"encoding/json"
8 7
 	"fmt"
9
-	"strconv"
10 8
 	"strings"
11
-	"time"
12 9
 
13 10
 	"github.com/goshuirc/irc-go/ircfmt"
14
-	"github.com/oragono/oragono/irc/passwd"
15 11
 	"github.com/oragono/oragono/irc/sno"
16
-	"github.com/tidwall/buntdb"
17 12
 )
18 13
 
19 14
 const nickservHelp = `NickServ lets you register and log into a user account.
@@ -80,14 +75,14 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
80 75
 		return
81 76
 	}
82 77
 
83
-	if !server.accountRegistration.Enabled {
78
+	if !server.AccountConfig().Registration.Enabled {
84 79
 		rb.Notice(client.t("Account registration has been disabled"))
85 80
 		return
86 81
 	}
87 82
 
88 83
 	if client.LoggedIntoAccount() {
89
-		if server.accountRegistration.AllowMultiplePerConnection {
90
-			client.LogoutOfAccount()
84
+		if server.AccountConfig().Registration.AllowMultiplePerConnection {
85
+			server.accounts.Logout(client)
91 86
 		} else {
92 87
 			rb.Notice(client.t("You're already logged into an account"))
93 88
 			return
@@ -103,26 +98,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
103 98
 		return
104 99
 	}
105 100
 
106
-	// check whether account exists
107
-	// do it all in one write tx to prevent races
108
-	err = server.store.Update(func(tx *buntdb.Tx) error {
109
-		accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
110
-
111
-		_, err := tx.Get(accountKey)
112
-		if err != buntdb.ErrNotFound {
113
-			//TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
114
-			rb.Notice(client.t("Account already exists"))
115
-			return errAccountCreation
116
-		}
117
-
118
-		registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
119
-
120
-		tx.Set(accountKey, "1", nil)
121
-		tx.Set(fmt.Sprintf(keyAccountName, casefoldedAccount), account, nil)
122
-		tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil)
123
-		return nil
124
-	})
125
-
126 101
 	// account could not be created and relevant numerics have been dispatched, abort
127 102
 	if err != nil {
128 103
 		if err != errAccountCreation {
@@ -131,87 +106,32 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
131 106
 		return
132 107
 	}
133 108
 
134
-	// store details
135
-	err = server.store.Update(func(tx *buntdb.Tx) error {
136
-		// certfp special lookup key
137
-		if passphrase == "" {
138
-			assembledKeyCertToAccount := fmt.Sprintf(keyCertToAccount, client.certfp)
139
-
140
-			// make sure certfp doesn't already exist because that'd be silly
141
-			_, err := tx.Get(assembledKeyCertToAccount)
142
-			if err != buntdb.ErrNotFound {
143
-				return errCertfpAlreadyExists
144
-			}
145
-
146
-			tx.Set(assembledKeyCertToAccount, casefoldedAccount, nil)
147
-		}
148
-
149
-		// make creds
150
-		var creds AccountCredentials
151
-
152
-		// always set passphrase salt
153
-		creds.PassphraseSalt, err = passwd.NewSalt()
154
-		if err != nil {
155
-			return fmt.Errorf("Could not create passphrase salt: %s", err.Error())
156
-		}
157
-
158
-		if passphrase == "" {
159
-			creds.Certificate = client.certfp
160
-		} else {
161
-			creds.PassphraseHash, err = server.passwords.GenerateFromPassword(creds.PassphraseSalt, passphrase)
162
-			if err != nil {
163
-				return fmt.Errorf("Could not hash password: %s", err)
164
-			}
165
-		}
166
-		credText, err := json.Marshal(creds)
167
-		if err != nil {
168
-			return fmt.Errorf("Could not marshal creds: %s", err)
169
-		}
170
-		tx.Set(fmt.Sprintf(keyAccountCredentials, account), string(credText), nil)
171
-
172
-		return nil
173
-	})
109
+	err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
110
+	if err == nil {
111
+		err = server.accounts.Verify(client, casefoldedAccount, "")
112
+	}
174 113
 
175 114
 	// details could not be stored and relevant numerics have been dispatched, abort
176 115
 	if err != nil {
177 116
 		errMsg := "Could not register"
178 117
 		if err == errCertfpAlreadyExists {
179 118
 			errMsg = "An account already exists for your certificate fingerprint"
119
+		} else if err == errAccountAlreadyRegistered {
120
+			errMsg = "Account already exists"
180 121
 		}
181
-		rb.Notice(errMsg)
182
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
122
+		rb.Notice(client.t(errMsg))
183 123
 		return
184 124
 	}
185 125
 
186
-	err = server.store.Update(func(tx *buntdb.Tx) error {
187
-		tx.Set(fmt.Sprintf(keyAccountVerified, casefoldedAccount), "1", nil)
188
-
189
-		// load acct info inside store tx
190
-		account := ClientAccount{
191
-			Name:         username,
192
-			RegisteredAt: time.Now(),
193
-			Clients:      []*Client{client},
194
-		}
195
-		//TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
196
-		server.accounts[casefoldedAccount] = &account
197
-		client.account = &account
198
-
199
-		rb.Notice(client.t("Account created"))
200
-		rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account.Name, fmt.Sprintf(client.t("You are now logged in as %s"), account.Name))
201
-		rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
202
-		server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), account.Name, client.nickMaskString))
203
-		return nil
204
-	})
205
-	if err != nil {
206
-		rb.Notice(client.t("Account registration failed"))
207
-		removeFailedAccRegisterData(server.store, casefoldedAccount)
208
-		return
209
-	}
126
+	rb.Notice(client.t("Account created"))
127
+	rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
128
+	rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
129
+	server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), casefoldedAccount, client.nickMaskString))
210 130
 }
211 131
 
212 132
 func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
213 133
 	// fail out if we need to
214
-	if !server.accountAuthenticationEnabled {
134
+	if !server.AccountConfig().AuthenticationEnabled {
215 135
 		rb.Notice(client.t("Login has been disabled"))
216 136
 		return
217 137
 	}
@@ -219,45 +139,13 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
219 139
 	// try passphrase
220 140
 	if username != "" && passphrase != "" {
221 141
 		// keep it the same as in the ACC CREATE stage
222
-		accountKey, err := CasefoldName(username)
142
+		accountName, err := CasefoldName(username)
223 143
 		if err != nil {
224 144
 			rb.Notice(client.t("Could not login with your username/password"))
225 145
 			return
226 146
 		}
227 147
 
228
-		// load and check acct data all in one update to prevent races.
229
-		// as noted elsewhere, change to proper locking for Account type later probably
230
-		var accountName string
231
-		err = server.store.Update(func(tx *buntdb.Tx) error {
232
-			// confirm account is verified
233
-			_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
234
-			if err != nil {
235
-				return errSaslFail
236
-			}
237
-
238
-			creds, err := loadAccountCredentials(tx, accountKey)
239
-			if err != nil {
240
-				return err
241
-			}
242
-
243
-			// ensure creds are valid
244
-			if len(creds.PassphraseHash) < 1 || len(creds.PassphraseSalt) < 1 || len(passphrase) < 1 {
245
-				return errSaslFail
246
-			}
247
-			err = server.passwords.CompareHashAndPassword(creds.PassphraseHash, creds.PassphraseSalt, passphrase)
248
-
249
-			// succeeded, load account info if necessary
250
-			account, exists := server.accounts[accountKey]
251
-			if !exists {
252
-				account = loadAccount(server, tx, accountKey)
253
-			}
254
-
255
-			client.LoginToAccount(account)
256
-			accountName = account.Name
257
-
258
-			return err
259
-		})
260
-
148
+		err = server.accounts.AuthenticateByPassphrase(client, accountName, passphrase)
261 149
 		if err == nil {
262 150
 			rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
263 151
 			return
@@ -265,48 +153,11 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
265 153
 	}
266 154
 
267 155
 	// try certfp
268
-	certfp := client.certfp
269
-	if certfp != "" {
270
-		var accountName string
271
-		err := server.store.Update(func(tx *buntdb.Tx) error {
272
-			// certfp lookup key
273
-			accountKey, err := tx.Get(fmt.Sprintf(keyCertToAccount, certfp))
274
-			if err != nil {
275
-				return errSaslFail
276
-			}
277
-
278
-			// confirm account exists
279
-			_, err = tx.Get(fmt.Sprintf(keyAccountExists, accountKey))
280
-			if err != nil {
281
-				return errSaslFail
282
-			}
283
-
284
-			// confirm account is verified
285
-			_, err = tx.Get(fmt.Sprintf(keyAccountVerified, accountKey))
286
-			if err != nil {
287
-				return errSaslFail
288
-			}
289
-
290
-			// confirm the certfp in that account's credentials
291
-			creds, err := loadAccountCredentials(tx, accountKey)
292
-			if err != nil || creds.Certificate != client.certfp {
293
-				return errSaslFail
294
-			}
295
-
296
-			// succeeded, load account info if necessary
297
-			account, exists := server.accounts[accountKey]
298
-			if !exists {
299
-				account = loadAccount(server, tx, accountKey)
300
-			}
301
-
302
-			client.LoginToAccount(account)
303
-			accountName = account.Name
304
-
305
-			return nil
306
-		})
307
-
156
+	if client.certfp != "" {
157
+		err := server.accounts.AuthenticateByCertFP(client)
308 158
 		if err == nil {
309
-			rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
159
+			rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
160
+			// TODO more notices?
310 161
 			return
311 162
 		}
312 163
 	}

+ 3
- 3
irc/responsebuffer.go View File

@@ -43,11 +43,11 @@ func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, c
43 43
 // AddFromClient adds a new message from a specific client to our queue.
44 44
 func (rb *ResponseBuffer) AddFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) {
45 45
 	// attach account-tag
46
-	if rb.target.capabilities.Has(caps.AccountTag) && from.account != &NoAccount {
46
+	if rb.target.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
47 47
 		if tags == nil {
48
-			tags = ircmsg.MakeTags("account", from.account.Name)
48
+			tags = ircmsg.MakeTags("account", from.AccountName())
49 49
 		} else {
50
-			(*tags)["account"] = ircmsg.MakeTagValue(from.account.Name)
50
+			(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
51 51
 		}
52 52
 	}
53 53
 	// attach message-id

+ 65
- 65
irc/server.go View File

@@ -87,49 +87,48 @@ type ListenerWrapper struct {
87 87
 
88 88
 // Server is the main Oragono server.
89 89
 type Server struct {
90
-	accountAuthenticationEnabled bool
91
-	accountRegistration          *AccountRegistration
92
-	accounts                     map[string]*ClientAccount
93
-	batches                      *BatchManager
94
-	channelRegistrationEnabled   bool
95
-	channels                     *ChannelManager
96
-	channelRegistry              *ChannelRegistry
97
-	checkIdent                   bool
98
-	clients                      *ClientManager
99
-	configFilename               string
100
-	configurableStateMutex       sync.RWMutex // tier 1; generic protection for server state modified by rehash()
101
-	connectionLimiter            *connection_limits.Limiter
102
-	connectionThrottler          *connection_limits.Throttler
103
-	ctime                        time.Time
104
-	defaultChannelModes          modes.Modes
105
-	dlines                       *DLineManager
106
-	loggingRawIO                 bool
107
-	isupport                     *isupport.List
108
-	klines                       *KLineManager
109
-	languages                    *languages.Manager
110
-	limits                       Limits
111
-	listeners                    map[string]*ListenerWrapper
112
-	logger                       *logger.Manager
113
-	MaxSendQBytes                uint64
114
-	monitorManager               *MonitorManager
115
-	motdLines                    []string
116
-	name                         string
117
-	nameCasefolded               string
118
-	networkName                  string
119
-	operators                    map[string]Oper
120
-	operclasses                  map[string]OperClass
121
-	password                     []byte
122
-	passwords                    *passwd.SaltedManager
123
-	recoverFromErrors            bool
124
-	rehashMutex                  sync.Mutex // tier 3
125
-	rehashSignal                 chan os.Signal
126
-	proxyAllowedFrom             []string
127
-	signals                      chan os.Signal
128
-	snomasks                     *SnoManager
129
-	store                        *buntdb.DB
130
-	stsEnabled                   bool
131
-	webirc                       []webircConfig
132
-	whoWas                       *WhoWasList
90
+	accountConfig              *AccountConfig
91
+	accounts                   *AccountManager
92
+	batches                    *BatchManager
93
+	channelRegistrationEnabled bool
94
+	channels                   *ChannelManager
95
+	channelRegistry            *ChannelRegistry
96
+	checkIdent                 bool
97
+	clients                    *ClientManager
98
+	configFilename             string
99
+	configurableStateMutex     sync.RWMutex // tier 1; generic protection for server state modified by rehash()
100
+	connectionLimiter          *connection_limits.Limiter
101
+	connectionThrottler        *connection_limits.Throttler
102
+	ctime                      time.Time
103
+	defaultChannelModes        modes.Modes
104
+	dlines                     *DLineManager
105
+	loggingRawIO               bool
106
+	isupport                   *isupport.List
107
+	klines                     *KLineManager
108
+	languages                  *languages.Manager
109
+	limits                     Limits
110
+	listeners                  map[string]*ListenerWrapper
111
+	logger                     *logger.Manager
112
+	MaxSendQBytes              uint64
113
+	monitorManager             *MonitorManager
114
+	motdLines                  []string
115
+	name                       string
116
+	nameCasefolded             string
117
+	networkName                string
118
+	operators                  map[string]Oper
119
+	operclasses                map[string]OperClass
120
+	password                   []byte
121
+	passwords                  *passwd.SaltedManager
122
+	recoverFromErrors          bool
123
+	rehashMutex                sync.Mutex // tier 4
124
+	rehashSignal               chan os.Signal
125
+	proxyAllowedFrom           []string
126
+	signals                    chan os.Signal
127
+	snomasks                   *SnoManager
128
+	store                      *buntdb.DB
129
+	stsEnabled                 bool
130
+	webirc                     []webircConfig
131
+	whoWas                     *WhoWasList
133 132
 }
134 133
 
135 134
 var (
@@ -150,7 +149,6 @@ type clientConn struct {
150 149
 func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
151 150
 	// initialize data structures
152 151
 	server := &Server{
153
-		accounts:            make(map[string]*ClientAccount),
154 152
 		batches:             NewBatchManager(),
155 153
 		channels:            NewChannelManager(),
156 154
 		clients:             NewClientManager(),
@@ -214,10 +212,10 @@ func (server *Server) setISupport() {
214 212
 	isupport.Add("UTF8MAPPING", casemappingName)
215 213
 
216 214
 	// account registration
217
-	if server.accountRegistration.Enabled {
215
+	if server.accountConfig.Registration.Enabled {
218 216
 		// 'none' isn't shown in the REGCALLBACKS vars
219 217
 		var enabledCallbacks []string
220
-		for _, name := range server.accountRegistration.EnabledCallbacks {
218
+		for _, name := range server.accountConfig.Registration.EnabledCallbacks {
221 219
 			if name != "*" {
222 220
 				enabledCallbacks = append(enabledCallbacks, name)
223 221
 			}
@@ -348,15 +346,8 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config) *Listen
348 346
 	// make listener
349 347
 	var listener net.Listener
350 348
 	var err error
351
-	optionalUnixPrefix := "unix:"
352
-	optionalPrefixLen := len(optionalUnixPrefix)
353
-	if len(addr) >= optionalPrefixLen && strings.ToLower(addr[0:optionalPrefixLen]) == optionalUnixPrefix {
354
-		addr = addr[optionalPrefixLen:]
355
-		if len(addr) == 0 || addr[0] != '/' {
356
-			log.Fatal("Bad unix socket address", addr)
357
-		}
358
-	}
359
-	if len(addr) > 0 && addr[0] == '/' {
349
+	addr = strings.TrimPrefix(addr, "unix:")
350
+	if strings.HasPrefix(addr, "/") {
360 351
 		// https://stackoverflow.com/a/34881585
361 352
 		os.Remove(addr)
362 353
 		listener, err = net.Listen("unix", addr)
@@ -478,7 +469,7 @@ func (server *Server) tryRegister(c *Client) {
478 469
 			}
479 470
 
480 471
 			if c.capabilities.Has(caps.ExtendedJoin) {
481
-				c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.account.Name, c.realname)
472
+				c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.AccountName(), c.realname)
482 473
 			} else {
483 474
 				c.Send(nil, c.nickMaskString, "JOIN", channel.name)
484 475
 			}
@@ -630,9 +621,8 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
630 621
 	if target.flags[modes.TLS] {
631 622
 		rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
632 623
 	}
633
-	accountName := target.AccountName()
634
-	if accountName != "" {
635
-		rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as"))
624
+	if target.LoggedIntoAccount() {
625
+		rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, client.AccountName(), client.t("is logged in as"))
636 626
 	}
637 627
 	if target.flags[modes.Bot] {
638 628
 		rb.Add(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName)))
@@ -803,18 +793,28 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
803 793
 	server.languages = lm
804 794
 
805 795
 	// SASL
806
-	if config.Accounts.AuthenticationEnabled && !server.accountAuthenticationEnabled {
796
+	oldAccountConfig := server.AccountConfig()
797
+	authPreviouslyEnabled := oldAccountConfig != nil && !oldAccountConfig.AuthenticationEnabled
798
+	if config.Accounts.AuthenticationEnabled && !authPreviouslyEnabled {
807 799
 		// enabling SASL
808 800
 		SupportedCapabilities.Enable(caps.SASL)
809 801
 		CapValues.Set(caps.SASL, "PLAIN,EXTERNAL")
810 802
 		addedCaps.Add(caps.SASL)
811
-	}
812
-	if !config.Accounts.AuthenticationEnabled && server.accountAuthenticationEnabled {
803
+	} else if !config.Accounts.AuthenticationEnabled && authPreviouslyEnabled {
813 804
 		// disabling SASL
814 805
 		SupportedCapabilities.Disable(caps.SASL)
815 806
 		removedCaps.Add(caps.SASL)
816 807
 	}
817
-	server.accountAuthenticationEnabled = config.Accounts.AuthenticationEnabled
808
+
809
+	server.configurableStateMutex.Lock()
810
+	server.accountConfig = &config.Accounts
811
+	server.configurableStateMutex.Unlock()
812
+
813
+	nickReservationPreviouslyDisabled := oldAccountConfig != nil && oldAccountConfig.NickReservation == NickReservationDisabled
814
+	nickReservationNowEnabled := config.Accounts.NickReservation != NickReservationDisabled
815
+	if nickReservationPreviouslyDisabled && nickReservationNowEnabled {
816
+		server.accounts.buildNickToAccountIndex()
817
+	}
818 818
 
819 819
 	// STS
820 820
 	stsValue := config.Server.STS.Value()
@@ -902,8 +902,6 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
902 902
 	server.checkIdent = config.Server.CheckIdent
903 903
 
904 904
 	// registration
905
-	accountReg := NewAccountRegistration(config.Accounts.Registration)
906
-	server.accountRegistration = &accountReg
907 905
 	server.channelRegistrationEnabled = config.Channels.Registration.Enabled
908 906
 
909 907
 	server.defaultChannelModes = ParseDefaultChannelModes(config)
@@ -1043,6 +1041,8 @@ func (server *Server) loadDatastore(datastorePath string) error {
1043 1041
 
1044 1042
 	server.channelRegistry = NewChannelRegistry(server)
1045 1043
 
1044
+	server.accounts = NewAccountManager(server)
1045
+
1046 1046
 	return nil
1047 1047
 }
1048 1048
 

+ 9
- 0
oragono.yaml View File

@@ -159,6 +159,14 @@ accounts:
159 159
     # is account authentication enabled?
160 160
     authentication-enabled: true
161 161
 
162
+    # will the server enforce that only the account holder can use the account name as a nick?
163
+    # options:
164
+    # `disabled`: no enforcement
165
+    # `timeout` (auth to nickserv within some period of time or you're disconnected)
166
+    # `strict`: must authenticate up front with SASL
167
+    nick-reservation: disabled
168
+    nick-reservation-timeout: 30s
169
+
162 170
 # channel options
163 171
 channels:
164 172
     # modes that are set when new channels are created
@@ -210,6 +218,7 @@ oper-classes:
210 218
         capabilities:
211 219
             - "oper:rehash"
212 220
             - "oper:die"
221
+            - "unregister"
213 222
             - "samode"
214 223
 
215 224
 # ircd operators

Loading…
Cancel
Save