Browse Source

Merge pull request #324 from slingamn/misc_again.5

some more changes
tags/v1.0.0-rc1
Daniel Oaks 5 years ago
parent
commit
0e22f8d6a5
No account linked to committer's email address

+ 1
- 0
Makefile View File

@@ -20,6 +20,7 @@ test:
20 20
 	python3 ./gencapdefs.py | diff - ${capdef_file}
21 21
 	cd irc && go test . && go vet .
22 22
 	cd irc/caps && go test . && go vet .
23
+	cd irc/connection_limits && go test . && go vet .
23 24
 	cd irc/history && go test . && go vet .
24 25
 	cd irc/isupport && go test . && go vet .
25 26
 	cd irc/modes && go test . && go vet .

+ 111
- 16
irc/accounts.go View File

@@ -30,6 +30,7 @@ const (
30 30
 	keyAccountRegTime          = "account.registered.time %s"
31 31
 	keyAccountCredentials      = "account.credentials %s"
32 32
 	keyAccountAdditionalNicks  = "account.additionalnicks %s"
33
+	keyAccountEnforcement      = "account.customenforcement %s"
33 34
 	keyAccountVHost            = "account.vhost %s"
34 35
 	keyCertToAccount           = "account.creds.certfp %s"
35 36
 
@@ -53,12 +54,14 @@ type AccountManager struct {
53 54
 	// track clients logged in to accounts
54 55
 	accountToClients map[string][]*Client
55 56
 	nickToAccount    map[string]string
57
+	accountToMethod  map[string]NickReservationMethod
56 58
 }
57 59
 
58 60
 func NewAccountManager(server *Server) *AccountManager {
59 61
 	am := AccountManager{
60 62
 		accountToClients: make(map[string][]*Client),
61 63
 		nickToAccount:    make(map[string]string),
64
+		accountToMethod:  make(map[string]NickReservationMethod),
62 65
 		server:           server,
63 66
 	}
64 67
 
@@ -72,7 +75,8 @@ func (am *AccountManager) buildNickToAccountIndex() {
72 75
 		return
73 76
 	}
74 77
 
75
-	result := make(map[string]string)
78
+	nickToAccount := make(map[string]string)
79
+	accountToMethod := make(map[string]NickReservationMethod)
76 80
 	existsPrefix := fmt.Sprintf(keyAccountExists, "")
77 81
 
78 82
 	am.serialCacheUpdateMutex.Lock()
@@ -83,14 +87,22 @@ func (am *AccountManager) buildNickToAccountIndex() {
83 87
 			if !strings.HasPrefix(key, existsPrefix) {
84 88
 				return false
85 89
 			}
86
-			accountName := strings.TrimPrefix(key, existsPrefix)
87
-			if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
88
-				result[accountName] = accountName
90
+
91
+			account := strings.TrimPrefix(key, existsPrefix)
92
+			if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, account)); err == nil {
93
+				nickToAccount[account] = account
89 94
 			}
90
-			if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil {
95
+			if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, account)); err == nil {
91 96
 				additionalNicks := unmarshalReservedNicks(rawNicks)
92 97
 				for _, nick := range additionalNicks {
93
-					result[nick] = accountName
98
+					nickToAccount[nick] = account
99
+				}
100
+			}
101
+
102
+			if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
103
+				method, err := nickReservationFromString(methodStr)
104
+				if err == nil {
105
+					accountToMethod[account] = method
94 106
 				}
95 107
 			}
96 108
 			return true
@@ -99,10 +111,11 @@ func (am *AccountManager) buildNickToAccountIndex() {
99 111
 	})
100 112
 
101 113
 	if err != nil {
102
-		am.server.logger.Error("internal", fmt.Sprintf("couldn't read reserved nicks: %v", err))
114
+		am.server.logger.Error("internal", "couldn't read reserved nicks", err.Error())
103 115
 	} else {
104 116
 		am.Lock()
105
-		am.nickToAccount = result
117
+		am.nickToAccount = nickToAccount
118
+		am.accountToMethod = accountToMethod
106 119
 		am.Unlock()
107 120
 	}
108 121
 }
@@ -156,6 +169,84 @@ func (am *AccountManager) NickToAccount(nick string) string {
156 169
 	return am.nickToAccount[cfnick]
157 170
 }
158 171
 
172
+// Given a nick, looks up the account that owns it and the method (none/timeout/strict)
173
+// used to enforce ownership.
174
+func (am *AccountManager) EnforcementStatus(nick string) (account string, method NickReservationMethod) {
175
+	cfnick, err := CasefoldName(nick)
176
+	if err != nil {
177
+		return
178
+	}
179
+
180
+	config := am.server.Config()
181
+	if !config.Accounts.NickReservation.Enabled {
182
+		method = NickReservationNone
183
+		return
184
+	}
185
+
186
+	am.RLock()
187
+	defer am.RUnlock()
188
+
189
+	account = am.nickToAccount[cfnick]
190
+	method = am.accountToMethod[account]
191
+	// if they don't have a custom setting, or customization is disabled, use the default
192
+	if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
193
+		method = config.Accounts.NickReservation.Method
194
+	}
195
+	if method == NickReservationOptional {
196
+		// enforcement was explicitly enabled neither in the config or by the user
197
+		method = NickReservationNone
198
+	}
199
+	return
200
+}
201
+
202
+// Looks up the enforcement method stored in the database for an account
203
+// (typically you want EnforcementStatus instead, which respects the config)
204
+func (am *AccountManager) getStoredEnforcementStatus(account string) string {
205
+	am.RLock()
206
+	defer am.RUnlock()
207
+	return nickReservationToString(am.accountToMethod[account])
208
+}
209
+
210
+// Sets a custom enforcement method for an account and stores it in the database.
211
+func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
212
+	config := am.server.Config()
213
+	if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
214
+		return errFeatureDisabled
215
+	}
216
+
217
+	var serialized string
218
+	if method == NickReservationOptional {
219
+		serialized = "" // normally this is "default", but we're going to delete the key
220
+	} else {
221
+		serialized = nickReservationToString(method)
222
+	}
223
+
224
+	key := fmt.Sprintf(keyAccountEnforcement, account)
225
+
226
+	am.Lock()
227
+	defer am.Unlock()
228
+
229
+	currentMethod := am.accountToMethod[account]
230
+	if method != currentMethod {
231
+		if method == NickReservationOptional {
232
+			delete(am.accountToMethod, account)
233
+		} else {
234
+			am.accountToMethod[account] = method
235
+		}
236
+
237
+		return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
238
+			if serialized != "" {
239
+				_, _, err = tx.Set(key, nickReservationToString(method), nil)
240
+			} else {
241
+				_, err = tx.Delete(key)
242
+			}
243
+			return
244
+		})
245
+	}
246
+
247
+	return nil
248
+}
249
+
159 250
 func (am *AccountManager) AccountToClients(account string) (result []*Client) {
160 251
 	cfaccount, err := CasefoldName(account)
161 252
 	if err != nil {
@@ -286,14 +377,14 @@ func (am *AccountManager) serializeCredentials(passphrase string, certfp string)
286 377
 		bcryptCost := int(am.server.Config().Accounts.Registration.BcryptCost)
287 378
 		creds.PassphraseHash, err = passwd.GenerateFromPassword([]byte(passphrase), bcryptCost)
288 379
 		if err != nil {
289
-			am.server.logger.Error("internal", fmt.Sprintf("could not hash password: %v", err))
380
+			am.server.logger.Error("internal", "could not hash password", err.Error())
290 381
 			return "", errAccountCreation
291 382
 		}
292 383
 	}
293 384
 
294 385
 	credText, err := json.Marshal(creds)
295 386
 	if err != nil {
296
-		am.server.logger.Error("internal", fmt.Sprintf("could not marshal credentials: %v", err))
387
+		am.server.logger.Error("internal", "could not marshal credentials", err.Error())
297 388
 		return "", errAccountCreation
298 389
 	}
299 390
 	return string(credText), nil
@@ -367,7 +458,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
367 458
 	// config.TLS.InsecureSkipVerify
368 459
 	err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
369 460
 	if err != nil {
370
-		am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err))
461
+		am.server.logger.Error("internal", "Failed to dispatch e-mail", err.Error())
371 462
 	}
372 463
 	return
373 464
 }
@@ -576,7 +667,9 @@ func (am *AccountManager) checkPassphrase(accountName, passphrase string) (accou
576 667
 	case 0:
577 668
 		err = handleLegacyPasswordV0(am.server, accountName, account.Credentials, passphrase)
578 669
 	case 1:
579
-		err = passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase))
670
+		if passwd.CompareHashAndPassword(account.Credentials.PassphraseHash, []byte(passphrase)) != nil {
671
+			err = errAccountInvalidCredentials
672
+		}
580 673
 	default:
581 674
 		err = errAccountInvalidCredentials
582 675
 	}
@@ -619,7 +712,7 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
619 712
 	result.RegisteredAt = time.Unix(regTimeInt, 0)
620 713
 	e := json.Unmarshal([]byte(raw.Credentials), &result.Credentials)
621 714
 	if e != nil {
622
-		am.server.logger.Error("internal", fmt.Sprintf("could not unmarshal credentials: %v", e))
715
+		am.server.logger.Error("internal", "could not unmarshal credentials", e.Error())
623 716
 		err = errAccountDoesNotExist
624 717
 		return
625 718
 	}
@@ -628,7 +721,7 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
628 721
 	if raw.VHost != "" {
629 722
 		e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
630 723
 		if e != nil {
631
-			am.server.logger.Warning("internal", fmt.Sprintf("could not unmarshal vhost for account %s: %v", result.Name, e))
724
+			am.server.logger.Warning("internal", "could not unmarshal vhost for account", result.Name, e.Error())
632 725
 			// pretend they have no vhost and move on
633 726
 		}
634 727
 	}
@@ -992,10 +1085,12 @@ func (am *AccountManager) applyVhostToClients(account string, result VHostInfo)
992 1085
 
993 1086
 func (am *AccountManager) Login(client *Client, account ClientAccount) {
994 1087
 	changed := client.SetAccountName(account.Name)
995
-	if changed {
996
-		go client.nickTimer.Touch()
1088
+	if !changed {
1089
+		return
997 1090
 	}
998 1091
 
1092
+	client.nickTimer.Touch()
1093
+
999 1094
 	am.applyVHostInfo(client, account.VHost)
1000 1095
 
1001 1096
 	casefoldedAccount := client.Account()

+ 53
- 43
irc/channel.go View File

@@ -9,6 +9,7 @@ import (
9 9
 	"bytes"
10 10
 	"fmt"
11 11
 	"strconv"
12
+	"strings"
12 13
 	"time"
13 14
 
14 15
 	"sync"
@@ -48,7 +49,7 @@ type Channel struct {
48 49
 func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel {
49 50
 	casefoldedName, err := CasefoldChannel(name)
50 51
 	if err != nil {
51
-		s.logger.Error("internal", fmt.Sprintf("Bad channel name %s: %v", name, err))
52
+		s.logger.Error("internal", "Bad channel name", name, err.Error())
52 53
 		return nil
53 54
 	}
54 55
 
@@ -346,8 +347,7 @@ func (channel *Channel) IsEmpty() bool {
346 347
 
347 348
 // Join joins the given client to this channel (if they can be joined).
348 349
 func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) {
349
-	account := client.Account()
350
-	nickMaskCasefolded := client.NickMaskCasefolded()
350
+	details := client.Details()
351 351
 
352 352
 	channel.stateMutex.RLock()
353 353
 	chname := channel.name
@@ -357,7 +357,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
357 357
 	limit := channel.userLimit
358 358
 	chcount := len(channel.members)
359 359
 	_, alreadyJoined := channel.members[client]
360
-	persistentMode := channel.accountToUMode[account]
360
+	persistentMode := channel.accountToUMode[details.account]
361 361
 	channel.stateMutex.RUnlock()
362 362
 
363 363
 	if alreadyJoined {
@@ -367,7 +367,7 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
367 367
 
368 368
 	// the founder can always join (even if they disabled auto +q on join);
369 369
 	// anyone who automatically receives halfop or higher can always join
370
-	hasPrivs := isSajoin || (founder != "" && founder == account) || (persistentMode != 0 && persistentMode != modes.Voice)
370
+	hasPrivs := isSajoin || (founder != "" && founder == details.account) || (persistentMode != 0 && persistentMode != modes.Voice)
371 371
 
372 372
 	if !hasPrivs && limit != 0 && chcount >= limit {
373 373
 		rb.Add(nil, client.server.name, ERR_CHANNELISFULL, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l"))
@@ -379,20 +379,20 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
379 379
 		return
380 380
 	}
381 381
 
382
-	isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(nickMaskCasefolded)
382
+	isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded)
383 383
 	if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited {
384 384
 		rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i"))
385 385
 		return
386 386
 	}
387 387
 
388
-	if !hasPrivs && channel.lists[modes.BanMask].Match(nickMaskCasefolded) &&
388
+	if !hasPrivs && channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) &&
389 389
 		!isInvited &&
390
-		!channel.lists[modes.ExceptMask].Match(nickMaskCasefolded) {
390
+		!channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) {
391 391
 		rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b"))
392 392
 		return
393 393
 	}
394 394
 
395
-	client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", client.nick, chname))
395
+	client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname))
396 396
 
397 397
 	newChannel, givenMode := func() (newChannel bool, givenMode modes.Mode) {
398 398
 		channel.joinPartMutex.Lock()
@@ -416,15 +416,19 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
416 416
 		}()
417 417
 
418 418
 		channel.regenerateMembersCache()
419
+
420
+		channel.history.Add(history.Item{
421
+			Type:        history.Join,
422
+			Nick:        details.nickMask,
423
+			AccountName: details.accountName,
424
+			Msgid:       details.realname,
425
+		})
426
+
419 427
 		return
420 428
 	}()
421 429
 
422 430
 	client.addChannel(channel)
423 431
 
424
-	nick := client.Nick()
425
-	nickmask := client.NickMaskString()
426
-	realname := client.Realname()
427
-	accountName := client.AccountName()
428 432
 	var modestr string
429 433
 	if givenMode != 0 {
430 434
 		modestr = fmt.Sprintf("+%v", givenMode)
@@ -435,19 +439,19 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
435 439
 			continue
436 440
 		}
437 441
 		if member.capabilities.Has(caps.ExtendedJoin) {
438
-			member.Send(nil, nickmask, "JOIN", chname, accountName, realname)
442
+			member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
439 443
 		} else {
440
-			member.Send(nil, nickmask, "JOIN", chname)
444
+			member.Send(nil, details.nickMask, "JOIN", chname)
441 445
 		}
442 446
 		if givenMode != 0 {
443
-			member.Send(nil, client.server.name, "MODE", chname, modestr, nick)
447
+			member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
444 448
 		}
445 449
 	}
446 450
 
447 451
 	if client.capabilities.Has(caps.ExtendedJoin) {
448
-		rb.Add(nil, nickmask, "JOIN", chname, accountName, realname)
452
+		rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
449 453
 	} else {
450
-		rb.Add(nil, nickmask, "JOIN", chname)
454
+		rb.Add(nil, details.nickMask, "JOIN", chname)
451 455
 	}
452 456
 
453 457
 	// don't send topic when it's an entirely new channel
@@ -458,16 +462,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
458 462
 	channel.Names(client, rb)
459 463
 
460 464
 	if givenMode != 0 {
461
-		rb.Add(nil, client.server.name, "MODE", chname, modestr, nick)
465
+		rb.Add(nil, client.server.name, "MODE", chname, modestr, details.nick)
462 466
 	}
463 467
 
464
-	channel.history.Add(history.Item{
465
-		Type:        history.Join,
466
-		Nick:        nickmask,
467
-		AccountName: accountName,
468
-		Msgid:       realname,
469
-	})
470
-
471 468
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
472 469
 	rb.Flush(true)
473 470
 
@@ -489,20 +486,20 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
489 486
 
490 487
 	channel.Quit(client)
491 488
 
492
-	nickmask := client.NickMaskString()
489
+	details := client.Details()
493 490
 	for _, member := range channel.Members() {
494
-		member.Send(nil, nickmask, "PART", chname, message)
491
+		member.Send(nil, details.nickMask, "PART", chname, message)
495 492
 	}
496
-	rb.Add(nil, nickmask, "PART", chname, message)
493
+	rb.Add(nil, details.nickMask, "PART", chname, message)
497 494
 
498 495
 	channel.history.Add(history.Item{
499 496
 		Type:        history.Part,
500
-		Nick:        nickmask,
501
-		AccountName: client.AccountName(),
497
+		Nick:        details.nickMask,
498
+		AccountName: details.accountName,
502 499
 		Message:     utils.MakeSplitMessage(message, true),
503 500
 	})
504 501
 
505
-	client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", client.nick, chname))
502
+	client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname))
506 503
 }
507 504
 
508 505
 // Resume is called after a successful global resume to:
@@ -591,10 +588,17 @@ func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Tim
591 588
 	rb.Send(true)
592 589
 }
593 590
 
591
+func stripMaskFromNick(nickMask string) (nick string) {
592
+	index := strings.Index(nickMask, "!")
593
+	if index == -1 {
594
+		return
595
+	}
596
+	return nickMask[0:index]
597
+}
598
+
594 599
 func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
595 600
 	chname := channel.Name()
596 601
 	client := rb.target
597
-	extendedJoin := client.capabilities.Has(caps.ExtendedJoin)
598 602
 	serverTime := client.capabilities.Has(caps.ServerTime)
599 603
 
600 604
 	for _, item := range items {
@@ -609,21 +613,27 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
609 613
 		case history.Notice:
610 614
 			rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message)
611 615
 		case history.Join:
612
-			if extendedJoin {
613
-				// XXX Msgid is the realname in this case
614
-				rb.Add(tags, item.Nick, "JOIN", chname, item.AccountName, item.Msgid)
616
+			nick := stripMaskFromNick(item.Nick)
617
+			var message string
618
+			if item.AccountName == "*" {
619
+				message = fmt.Sprintf(client.t("%s joined the channel"), nick)
615 620
 			} else {
616
-				rb.Add(tags, item.Nick, "JOIN", chname)
621
+				message = fmt.Sprintf(client.t("%s [account: %s] joined the channel"), nick, item.AccountName)
617 622
 			}
618
-		case history.Quit:
619
-			// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
620
-			// QUIT messages across channels
621
-			fallthrough
623
+			rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
622 624
 		case history.Part:
623
-			rb.Add(tags, item.Nick, "PART", chname, item.Message.Original)
625
+			nick := stripMaskFromNick(item.Nick)
626
+			message := fmt.Sprintf(client.t("%s left the channel (%s)"), nick, item.Message.Original)
627
+			rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
628
+		case history.Quit:
629
+			nick := stripMaskFromNick(item.Nick)
630
+			message := fmt.Sprintf(client.t("%s quit (%s)"), nick, item.Message.Original)
631
+			rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
624 632
 		case history.Kick:
633
+			nick := stripMaskFromNick(item.Nick)
625 634
 			// XXX Msgid is the kick target
626
-			rb.Add(tags, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
635
+			message := fmt.Sprintf(client.t("%s kicked %s (%s)"), nick, item.Msgid, item.Message.Original)
636
+			rb.Add(tags, "HistServ", "PRIVMSG", chname, message)
627 637
 		}
628 638
 	}
629 639
 }

+ 35
- 9
irc/client.go View File

@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/goshuirc/irc-go/ircmsg"
20 20
 	ident "github.com/oragono/go-ident"
21 21
 	"github.com/oragono/oragono/irc/caps"
22
+	"github.com/oragono/oragono/irc/connection_limits"
22 23
 	"github.com/oragono/oragono/irc/history"
23 24
 	"github.com/oragono/oragono/irc/modes"
24 25
 	"github.com/oragono/oragono/irc/sno"
@@ -52,7 +53,7 @@ type ResumeDetails struct {
52 53
 // Client is an IRC client.
53 54
 type Client struct {
54 55
 	account            string
55
-	accountName        string
56
+	accountName        string // display name of the account: uncasefolded, '*' if not logged in
56 57
 	atime              time.Time
57 58
 	authorized         bool
58 59
 	awayMessage        string
@@ -73,6 +74,7 @@ type Client struct {
73 74
 	isDestroyed        bool
74 75
 	isQuitting         bool
75 76
 	languages          []string
77
+	loginThrottle      connection_limits.GenericThrottle
76 78
 	maxlenTags         uint32
77 79
 	maxlenRest         uint32
78 80
 	nick               string
@@ -100,6 +102,25 @@ type Client struct {
100 102
 	history            *history.Buffer
101 103
 }
102 104
 
105
+// WhoWas is the subset of client details needed to answer a WHOWAS query
106
+type WhoWas struct {
107
+	nick           string
108
+	nickCasefolded string
109
+	username       string
110
+	hostname       string
111
+	realname       string
112
+}
113
+
114
+// ClientDetails is a standard set of details about a client
115
+type ClientDetails struct {
116
+	WhoWas
117
+
118
+	nickMask           string
119
+	nickMaskCasefolded string
120
+	account            string
121
+	accountName        string
122
+}
123
+
103 124
 // NewClient sets up a new client and starts its goroutine.
104 125
 func NewClient(server *Server, conn net.Conn, isTLS bool) {
105 126
 	now := time.Now()
@@ -107,16 +128,21 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) {
107 128
 	fullLineLenLimit := config.Limits.LineLen.Tags + config.Limits.LineLen.Rest
108 129
 	socket := NewSocket(conn, fullLineLenLimit*2, config.Server.MaxSendQBytes)
109 130
 	client := &Client{
110
-		atime:          now,
111
-		authorized:     server.Password() == nil,
112
-		capabilities:   caps.NewSet(),
113
-		capState:       caps.NoneState,
114
-		capVersion:     caps.Cap301,
115
-		channels:       make(ChannelSet),
116
-		ctime:          now,
117
-		flags:          modes.NewModeSet(),
131
+		atime:        now,
132
+		authorized:   server.Password() == nil,
133
+		capabilities: caps.NewSet(),
134
+		capState:     caps.NoneState,
135
+		capVersion:   caps.Cap301,
136
+		channels:     make(ChannelSet),
137
+		ctime:        now,
138
+		flags:        modes.NewModeSet(),
139
+		loginThrottle: connection_limits.GenericThrottle{
140
+			Duration: config.Accounts.LoginThrottling.Duration,
141
+			Limit:    config.Accounts.LoginThrottling.MaxAttempts,
142
+		},
118 143
 		server:         server,
119 144
 		socket:         socket,
145
+		accountName:    "*",
120 146
 		nick:           "*", // * is used until actual nick is given
121 147
 		nickCasefolded: "*",
122 148
 		nickMaskString: "*", // * is used until actual nick is given

+ 3
- 8
irc/client_lookup_set.go View File

@@ -72,7 +72,7 @@ func (clients *ClientManager) removeInternal(client *Client) (err error) {
72 72
 			delete(clients.byNick, oldcfnick)
73 73
 		} else {
74 74
 			// this shouldn't happen, but we can ignore it
75
-			client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick))
75
+			client.server.logger.Warning("internal", "clients for nick out of sync", oldcfnick)
76 76
 			err = errNickMissing
77 77
 		}
78 78
 	}
@@ -119,17 +119,11 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
119 119
 		return err
120 120
 	}
121 121
 
122
-	var reservedAccount string
123
-	var method NickReservationMethod
124
-	if client.server.AccountConfig().NickReservation.Enabled {
125
-		reservedAccount = client.server.accounts.NickToAccount(newcfnick)
126
-		method = client.server.AccountConfig().NickReservation.Method
127
-	}
122
+	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick)
128 123
 
129 124
 	clients.Lock()
130 125
 	defer clients.Unlock()
131 126
 
132
-	clients.removeInternal(client)
133 127
 	currentNewEntry := clients.byNick[newcfnick]
134 128
 	// the client may just be changing case
135 129
 	if currentNewEntry != nil && currentNewEntry != client {
@@ -138,6 +132,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
138 132
 	if method == NickReservationStrict && reservedAccount != client.Account() {
139 133
 		return errNicknameReserved
140 134
 	}
135
+	clients.removeInternal(client)
141 136
 	clients.byNick[newcfnick] = client
142 137
 	client.updateNickMask(newNick)
143 138
 	return nil

+ 64
- 18
irc/config.go View File

@@ -8,7 +8,6 @@ package irc
8 8
 import (
9 9
 	"crypto/tls"
10 10
 	"encoding/json"
11
-	"errors"
12 11
 	"fmt"
13 12
 	"io/ioutil"
14 13
 	"log"
@@ -54,10 +53,15 @@ func (conf *TLSListenConfig) Config() (*tls.Config, error) {
54 53
 
55 54
 type AccountConfig struct {
56 55
 	Registration          AccountRegistrationConfig
57
-	AuthenticationEnabled bool                  `yaml:"authentication-enabled"`
58
-	SkipServerPassword    bool                  `yaml:"skip-server-password"`
59
-	NickReservation       NickReservationConfig `yaml:"nick-reservation"`
60
-	VHosts                VHostConfig
56
+	AuthenticationEnabled bool `yaml:"authentication-enabled"`
57
+	LoginThrottling       struct {
58
+		Enabled     bool
59
+		Duration    time.Duration
60
+		MaxAttempts int `yaml:"max-attempts"`
61
+	} `yaml:"login-throttling"`
62
+	SkipServerPassword bool                  `yaml:"skip-server-password"`
63
+	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
64
+	VHosts             VHostConfig
61 65
 }
62 66
 
63 67
 // AccountRegistrationConfig controls account registration.
@@ -100,10 +104,50 @@ type VHostConfig struct {
100 104
 type NickReservationMethod int
101 105
 
102 106
 const (
103
-	NickReservationWithTimeout NickReservationMethod = iota
107
+	// NickReservationOptional is the zero value; it serializes to
108
+	// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
109
+	// in both cases, it means "defer to the other source of truth", i.e.,
110
+	// in the config, defer to the user's custom setting, and as a custom setting,
111
+	// defer to the default in the config. if both are NickReservationOptional then
112
+	// there is no enforcement.
113
+	NickReservationOptional NickReservationMethod = iota
114
+	NickReservationNone
115
+	NickReservationWithTimeout
104 116
 	NickReservationStrict
105 117
 )
106 118
 
119
+func nickReservationToString(method NickReservationMethod) string {
120
+	switch method {
121
+	case NickReservationOptional:
122
+		return "default"
123
+	case NickReservationNone:
124
+		return "none"
125
+	case NickReservationWithTimeout:
126
+		return "timeout"
127
+	case NickReservationStrict:
128
+		return "strict"
129
+	default:
130
+		return ""
131
+	}
132
+}
133
+
134
+func nickReservationFromString(method string) (NickReservationMethod, error) {
135
+	switch method {
136
+	case "default":
137
+		return NickReservationOptional, nil
138
+	case "optional":
139
+		return NickReservationOptional, nil
140
+	case "none":
141
+		return NickReservationNone, nil
142
+	case "timeout":
143
+		return NickReservationWithTimeout, nil
144
+	case "strict":
145
+		return NickReservationStrict, nil
146
+	default:
147
+		return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
148
+	}
149
+}
150
+
107 151
 func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
108 152
 	var orig, raw string
109 153
 	var err error
@@ -113,22 +157,20 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
113 157
 	if raw, err = Casefold(orig); err != nil {
114 158
 		return err
115 159
 	}
116
-	if raw == "timeout" {
117
-		*nr = NickReservationWithTimeout
118
-	} else if raw == "strict" {
119
-		*nr = NickReservationStrict
120
-	} else {
121
-		return errors.New(fmt.Sprintf("invalid nick-reservation.method value: %s", orig))
160
+	method, err := nickReservationFromString(raw)
161
+	if err == nil {
162
+		*nr = method
122 163
 	}
123
-	return nil
164
+	return err
124 165
 }
125 166
 
126 167
 type NickReservationConfig struct {
127
-	Enabled             bool
128
-	AdditionalNickLimit int `yaml:"additional-nick-limit"`
129
-	Method              NickReservationMethod
130
-	RenameTimeout       time.Duration `yaml:"rename-timeout"`
131
-	RenamePrefix        string        `yaml:"rename-prefix"`
168
+	Enabled                bool
169
+	AdditionalNickLimit    int `yaml:"additional-nick-limit"`
170
+	Method                 NickReservationMethod
171
+	AllowCustomEnforcement bool          `yaml:"allow-custom-enforcement"`
172
+	RenameTimeout          time.Duration `yaml:"rename-timeout"`
173
+	RenamePrefix           string        `yaml:"rename-prefix"`
132 174
 }
133 175
 
134 176
 // ChannelRegistrationConfig controls channel registration.
@@ -558,6 +600,10 @@ func LoadConfig(filename string) (config *Config, err error) {
558 600
 		config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
559 601
 	}
560 602
 
603
+	if !config.Accounts.LoginThrottling.Enabled {
604
+		config.Accounts.LoginThrottling.MaxAttempts = 0 // limit of 0 means disabled
605
+	}
606
+
561 607
 	maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
562 608
 	if err != nil {
563 609
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

+ 50
- 13
irc/connection_limits/throttler.go View File

@@ -26,8 +26,45 @@ type ThrottlerConfig struct {
26 26
 
27 27
 // ThrottleDetails holds the connection-throttling details for a subnet/IP.
28 28
 type ThrottleDetails struct {
29
-	Start       time.Time
30
-	ClientCount int
29
+	Start time.Time
30
+	Count int
31
+}
32
+
33
+// GenericThrottle allows enforcing limits of the form
34
+// "at most X events per time window of duration Y"
35
+type GenericThrottle struct {
36
+	ThrottleDetails // variable state: what events have been seen
37
+	// these are constant after creation:
38
+	Duration time.Duration // window length to consider
39
+	Limit    int           // number of events allowed per window
40
+}
41
+
42
+// Touch checks whether an additional event is allowed:
43
+// it either denies it (by returning false) or allows it (by returning true)
44
+// and records it
45
+func (g *GenericThrottle) Touch() (throttled bool, remainingTime time.Duration) {
46
+	return g.touch(time.Now())
47
+}
48
+
49
+func (g *GenericThrottle) touch(now time.Time) (throttled bool, remainingTime time.Duration) {
50
+	if g.Limit == 0 {
51
+		return // limit of 0 disables throttling
52
+	}
53
+
54
+	elapsed := now.Sub(g.Start)
55
+	if elapsed > g.Duration {
56
+		// reset window, record the operation
57
+		g.Start = now
58
+		g.Count = 1
59
+		return false, 0
60
+	} else if g.Count >= g.Limit {
61
+		// we are throttled
62
+		return true, g.Start.Add(g.Duration).Sub(now)
63
+	} else {
64
+		// we are not throttled, record the operation
65
+		g.Count += 1
66
+		return false, 0
67
+	}
31 68
 }
32 69
 
33 70
 // Throttler manages automated client connection throttling.
@@ -102,21 +139,21 @@ func (ct *Throttler) AddClient(addr net.IP) error {
102 139
 	ct.maskAddr(addr)
103 140
 	addrString := addr.String()
104 141
 
105
-	details, exists := ct.population[addrString]
106
-	if !exists || details.Start.Add(ct.duration).Before(time.Now()) {
107
-		details = ThrottleDetails{
108
-			Start: time.Now(),
109
-		}
142
+	details := ct.population[addrString] // retrieve mutable throttle state from the map
143
+	// add in constant state to process the limiting operation
144
+	g := GenericThrottle{
145
+		ThrottleDetails: details,
146
+		Duration:        ct.duration,
147
+		Limit:           ct.subnetLimit,
110 148
 	}
149
+	throttled, _ := g.Touch()                     // actually check the limit
150
+	ct.population[addrString] = g.ThrottleDetails // store modified mutable state
111 151
 
112
-	if details.ClientCount+1 > ct.subnetLimit {
152
+	if throttled {
113 153
 		return errTooManyClients
154
+	} else {
155
+		return nil
114 156
 	}
115
-
116
-	details.ClientCount++
117
-	ct.population[addrString] = details
118
-
119
-	return nil
120 157
 }
121 158
 
122 159
 func (ct *Throttler) BanDuration() time.Duration {

+ 86
- 0
irc/connection_limits/throttler_test.go View File

@@ -0,0 +1,86 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni
2
+// released under the MIT license
3
+
4
+package connection_limits
5
+
6
+import (
7
+	"net"
8
+	"reflect"
9
+	"testing"
10
+	"time"
11
+)
12
+
13
+func assertEqual(supplied, expected interface{}, t *testing.T) {
14
+	if !reflect.DeepEqual(supplied, expected) {
15
+		t.Errorf("expected %v but got %v", expected, supplied)
16
+	}
17
+}
18
+
19
+func TestGenericThrottle(t *testing.T) {
20
+	minute, _ := time.ParseDuration("1m")
21
+	second, _ := time.ParseDuration("1s")
22
+	zero, _ := time.ParseDuration("0s")
23
+
24
+	throttler := GenericThrottle{
25
+		Duration: minute,
26
+		Limit:    2,
27
+	}
28
+
29
+	now := time.Now()
30
+	throttled, remaining := throttler.touch(now)
31
+	assertEqual(throttled, false, t)
32
+	assertEqual(remaining, zero, t)
33
+
34
+	now = now.Add(second)
35
+	throttled, remaining = throttler.touch(now)
36
+	assertEqual(throttled, false, t)
37
+	assertEqual(remaining, zero, t)
38
+
39
+	now = now.Add(second)
40
+	throttled, remaining = throttler.touch(now)
41
+	assertEqual(throttled, true, t)
42
+	assertEqual(remaining, 58*second, t)
43
+
44
+	now = now.Add(minute)
45
+	throttled, remaining = throttler.touch(now)
46
+	assertEqual(throttled, false, t)
47
+	assertEqual(remaining, zero, t)
48
+}
49
+
50
+func TestGenericThrottleDisabled(t *testing.T) {
51
+	minute, _ := time.ParseDuration("1m")
52
+	throttler := GenericThrottle{
53
+		Duration: minute,
54
+		Limit:    0,
55
+	}
56
+
57
+	for i := 0; i < 1024; i += 1 {
58
+		throttled, _ := throttler.Touch()
59
+		if throttled {
60
+			t.Error("disabled throttler should not throttle")
61
+		}
62
+	}
63
+}
64
+
65
+func TestConnectionThrottle(t *testing.T) {
66
+	minute, _ := time.ParseDuration("1m")
67
+	maxConnections := 3
68
+	config := ThrottlerConfig{
69
+		Enabled:            true,
70
+		CidrLenIPv4:        32,
71
+		CidrLenIPv6:        64,
72
+		ConnectionsPerCidr: maxConnections,
73
+		Duration:           minute,
74
+	}
75
+	throttler := NewThrottler()
76
+	throttler.ApplyConfig(config)
77
+
78
+	addr := net.ParseIP("8.8.8.8")
79
+
80
+	for i := 0; i < maxConnections; i += 1 {
81
+		err := throttler.AddClient(addr)
82
+		assertEqual(err, nil, t)
83
+	}
84
+	err := throttler.AddClient(addr)
85
+	assertEqual(err, errTooManyClients, t)
86
+}

+ 1
- 0
irc/errors.go View File

@@ -40,6 +40,7 @@ var (
40 40
 	errSaslFail                       = errors.New("SASL failed")
41 41
 	errResumeTokenAlreadySet          = errors.New("Client was already assigned a resume token")
42 42
 	errInvalidUsername                = errors.New("Invalid username")
43
+	errFeatureDisabled                = errors.New("That feature is disabled")
43 44
 )
44 45
 
45 46
 // Socket Errors

+ 11
- 19
irc/getters.go View File

@@ -147,9 +147,6 @@ func (client *Client) Account() string {
147 147
 func (client *Client) AccountName() string {
148 148
 	client.stateMutex.RLock()
149 149
 	defer client.stateMutex.RUnlock()
150
-	if client.accountName == "" {
151
-		return "*"
152
-	}
153 150
 	return client.accountName
154 151
 }
155 152
 
@@ -182,18 +179,6 @@ func (client *Client) SetAuthorized(authorized bool) {
182 179
 	client.authorized = authorized
183 180
 }
184 181
 
185
-func (client *Client) PreregNick() string {
186
-	client.stateMutex.RLock()
187
-	defer client.stateMutex.RUnlock()
188
-	return client.preregNick
189
-}
190
-
191
-func (client *Client) SetPreregNick(preregNick string) {
192
-	client.stateMutex.Lock()
193
-	defer client.stateMutex.Unlock()
194
-	client.preregNick = preregNick
195
-}
196
-
197 182
 func (client *Client) HasMode(mode modes.Mode) bool {
198 183
 	// client.flags has its own synch
199 184
 	return client.flags.HasMode(mode)
@@ -217,15 +202,22 @@ func (client *Client) Channels() (result []*Channel) {
217 202
 }
218 203
 
219 204
 func (client *Client) WhoWas() (result WhoWas) {
205
+	return client.Details().WhoWas
206
+}
207
+
208
+func (client *Client) Details() (result ClientDetails) {
220 209
 	client.stateMutex.RLock()
221 210
 	defer client.stateMutex.RUnlock()
222 211
 
223
-	result.nicknameCasefolded = client.nickCasefolded
224
-	result.nickname = client.nick
212
+	result.nick = client.nick
213
+	result.nickCasefolded = client.nickCasefolded
225 214
 	result.username = client.username
226
-	result.hostname = client.hostname
215
+	result.hostname = client.username
227 216
 	result.realname = client.realname
228
-
217
+	result.nickMask = client.nickMaskString
218
+	result.nickMaskCasefolded = client.nickMaskCasefolded
219
+	result.account = client.account
220
+	result.accountName = client.accountName
229 221
 	return
230 222
 }
231 223
 

+ 30
- 14
irc/handlers.go View File

@@ -83,9 +83,10 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
83 83
 
84 84
 // ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
85 85
 func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
86
+	nick := client.Nick()
86 87
 	// clients can't reg new accounts if they're already logged in
87 88
 	if client.LoggedIntoAccount() {
88
-		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("You're already logged into an account"))
89
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, "*", client.t("You're already logged into an account"))
89 90
 		return false
90 91
 	}
91 92
 
@@ -94,12 +95,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
94 95
 	casefoldedAccount, err := CasefoldName(account)
95 96
 	// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
96 97
 	if err != nil || msg.Params[1] == "*" {
97
-		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, account, client.t("Account name is not valid"))
98
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, account, client.t("Account name is not valid"))
98 99
 		return false
99 100
 	}
100 101
 
101 102
 	if len(msg.Params) < 4 {
102
-		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, client.t("Not enough parameters"))
103
+		rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
103 104
 		return false
104 105
 	}
105 106
 
@@ -107,7 +108,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
107 108
 	callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
108 109
 
109 110
 	if callbackNamespace == "" {
110
-		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
111
+		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, nick, account, callbackSpec, client.t("Callback namespace is not supported"))
111 112
 		return false
112 113
 	}
113 114
 
@@ -131,12 +132,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
131 132
 		}
132 133
 	}
133 134
 	if credentialType == "certfp" && client.certfp == "" {
134
-		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
135
+		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
135 136
 		return false
136 137
 	}
137 138
 
138 139
 	if !credentialValid {
139
-		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, client.nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
140
+		rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
140 141
 		return false
141 142
 	}
142 143
 
@@ -146,6 +147,13 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
146 147
 	} else if credentialType == "passphrase" {
147 148
 		passphrase = credentialValue
148 149
 	}
150
+
151
+	throttled, remainingTime := client.loginThrottle.Touch()
152
+	if throttled {
153
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
154
+		return false
155
+	}
156
+
149 157
 	err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
150 158
 	if err != nil {
151 159
 		msg := "Unknown"
@@ -161,7 +169,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
161 169
 		if err == errAccountAlreadyRegistered || err == errAccountCreation || err == errCertfpAlreadyExists {
162 170
 			msg = err.Error()
163 171
 		}
164
-		rb.Add(nil, server.name, code, client.nick, "ACC", "REGISTER", client.t(msg))
172
+		rb.Add(nil, server.name, code, nick, "ACC", "REGISTER", client.t(msg))
165 173
 		return false
166 174
 	}
167 175
 
@@ -175,7 +183,7 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
175 183
 	} else {
176 184
 		messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
177 185
 		message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
178
-		rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
186
+		rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, nick, casefoldedAccount, message)
179 187
 	}
180 188
 
181 189
 	return false
@@ -336,6 +344,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
336 344
 
337 345
 	var accountKey, authzid string
338 346
 
347
+	nick := client.Nick()
348
+
339 349
 	if len(splitValue) == 3 {
340 350
 		accountKey = string(splitValue[0])
341 351
 		authzid = string(splitValue[1])
@@ -343,11 +353,17 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
343 353
 		if accountKey == "" {
344 354
 			accountKey = authzid
345 355
 		} else if accountKey != authzid {
346
-			rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
356
+			rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: authcid and authzid should be the same"))
347 357
 			return false
348 358
 		}
349 359
 	} else {
350
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Invalid auth blob"))
360
+		rb.Add(nil, server.name, ERR_SASLFAIL, nick, client.t("SASL authentication failed: Invalid auth blob"))
361
+		return false
362
+	}
363
+
364
+	throttled, remainingTime := client.loginThrottle.Touch()
365
+	if throttled {
366
+		rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
351 367
 		return false
352 368
 	}
353 369
 
@@ -355,7 +371,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
355 371
 	err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
356 372
 	if err != nil {
357 373
 		msg := authErrorToMessage(server, err)
358
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
374
+		rb.Add(nil, server.name, ERR_SASLFAIL, nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))
359 375
 		return false
360 376
 	}
361 377
 
@@ -367,7 +383,7 @@ func authErrorToMessage(server *Server, err error) (msg string) {
367 383
 	if err == errAccountDoesNotExist || err == errAccountUnverified || err == errAccountInvalidCredentials {
368 384
 		msg = err.Error()
369 385
 	} else {
370
-		server.logger.Error("internal", fmt.Sprintf("sasl authentication failure: %v", err))
386
+		server.logger.Error("internal", "sasl authentication failure", err.Error())
371 387
 		msg = "Unknown"
372 388
 	}
373 389
 	return
@@ -1646,7 +1662,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1646 1662
 	if client.Registered() {
1647 1663
 		performNickChange(server, client, client, msg.Params[0], rb)
1648 1664
 	} else {
1649
-		client.SetPreregNick(msg.Params[0])
1665
+		client.preregNick = msg.Params[0]
1650 1666
 	}
1651 1667
 	return false
1652 1668
 }
@@ -2541,7 +2557,7 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2541 2557
 			}
2542 2558
 		} else {
2543 2559
 			for _, whoWas := range results {
2544
-				rb.Add(nil, server.name, RPL_WHOWASUSER, cnick, whoWas.nickname, whoWas.username, whoWas.hostname, "*", whoWas.realname)
2560
+				rb.Add(nil, server.name, RPL_WHOWASUSER, cnick, whoWas.nick, whoWas.username, whoWas.hostname, "*", whoWas.realname)
2545 2561
 			}
2546 2562
 		}
2547 2563
 		if len(nickname) > 0 {

+ 7
- 6
irc/idletimer.go View File

@@ -189,14 +189,14 @@ type NickTimer struct {
189 189
 // NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled)
190 190
 func NewNickTimer(client *Client) *NickTimer {
191 191
 	config := client.server.AccountConfig().NickReservation
192
-	if !(config.Enabled && config.Method == NickReservationWithTimeout) {
192
+	if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) {
193 193
 		return nil
194 194
 	}
195
-	nt := NickTimer{
195
+
196
+	return &NickTimer{
196 197
 		client:  client,
197 198
 		timeout: config.RenameTimeout,
198 199
 	}
199
-	return &nt
200 200
 }
201 201
 
202 202
 // Touch records a nick change and updates the timer as necessary
@@ -207,7 +207,8 @@ func (nt *NickTimer) Touch() {
207 207
 
208 208
 	nick := nt.client.NickCasefolded()
209 209
 	account := nt.client.Account()
210
-	accountForNick := nt.client.server.accounts.NickToAccount(nick)
210
+	accountForNick, method := nt.client.server.accounts.EnforcementStatus(nick)
211
+	enforceTimeout := method == NickReservationWithTimeout
211 212
 
212 213
 	var shouldWarn bool
213 214
 
@@ -227,11 +228,11 @@ func (nt *NickTimer) Touch() {
227 228
 		nt.accountForNick = accountForNick
228 229
 		delinquent := accountForNick != "" && accountForNick != account
229 230
 
230
-		if nt.timer != nil && (!delinquent || accountChanged) {
231
+		if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) {
231 232
 			nt.timer.Stop()
232 233
 			nt.timer = nil
233 234
 		}
234
-		if delinquent && accountChanged {
235
+		if enforceTimeout && delinquent && accountChanged {
235 236
 			nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
236 237
 			shouldWarn = true
237 238
 		}

+ 41
- 21
irc/logger/logger.go View File

@@ -5,6 +5,7 @@ package logger
5 5
 
6 6
 import (
7 7
 	"bufio"
8
+	"bytes"
8 9
 	"fmt"
9 10
 	"os"
10 11
 	"time"
@@ -32,6 +33,20 @@ const (
32 33
 	LogError
33 34
 )
34 35
 
36
+var (
37
+	colorTimeGrey = ansi.ColorFunc("243")
38
+	colorGrey     = ansi.ColorFunc("8")
39
+	colorAlert    = ansi.ColorFunc("232+b:red")
40
+	colorWarn     = ansi.ColorFunc("black:214")
41
+	colorInfo     = ansi.ColorFunc("117")
42
+	colorDebug    = ansi.ColorFunc("78")
43
+	colorSection  = ansi.ColorFunc("229")
44
+	separator     = colorGrey(":")
45
+
46
+	colorableStdout = colorable.NewColorableStdout()
47
+	colorableStderr = colorable.NewColorableStderr()
48
+)
49
+
35 50
 var (
36 51
 	// LogLevelNames takes a config name and gives the real log level.
37 52
 	LogLevelNames = map[string]Level{
@@ -230,51 +245,56 @@ func (logger *singleLogger) Log(level Level, logType string, messageParts ...str
230 245
 	}
231 246
 
232 247
 	// assemble full line
233
-	timeGrey := ansi.ColorFunc("243")
234
-	grey := ansi.ColorFunc("8")
235
-	alert := ansi.ColorFunc("232+b:red")
236
-	warn := ansi.ColorFunc("black:214")
237
-	info := ansi.ColorFunc("117")
238
-	debug := ansi.ColorFunc("78")
239
-	section := ansi.ColorFunc("229")
240 248
 
241 249
 	levelDisplay := LogLevelDisplayNames[level]
242 250
 	if level == LogError {
243
-		levelDisplay = alert(levelDisplay)
251
+		levelDisplay = colorAlert(levelDisplay)
244 252
 	} else if level == LogWarning {
245
-		levelDisplay = warn(levelDisplay)
253
+		levelDisplay = colorWarn(levelDisplay)
246 254
 	} else if level == LogInfo {
247
-		levelDisplay = info(levelDisplay)
255
+		levelDisplay = colorInfo(levelDisplay)
248 256
 	} else if level == LogDebug {
249
-		levelDisplay = debug(levelDisplay)
257
+		levelDisplay = colorDebug(levelDisplay)
250 258
 	}
251 259
 
252
-	sep := grey(":")
253
-	fullStringFormatted := fmt.Sprintf("%s %s %s %s %s %s ", timeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), sep, levelDisplay, sep, section(logType), sep)
254
-	fullStringRaw := fmt.Sprintf("%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
260
+	var formattedBuf, rawBuf bytes.Buffer
261
+	fmt.Fprintf(&formattedBuf, "%s %s %s %s %s %s ", colorTimeGrey(time.Now().UTC().Format("2006-01-02T15:04:05.000Z")), separator, levelDisplay, separator, colorSection(logType), separator)
262
+	if logger.MethodFile.Enabled {
263
+		fmt.Fprintf(&rawBuf, "%s : %s : %s : ", time.Now().UTC().Format("2006-01-02T15:04:05Z"), LogLevelDisplayNames[level], logType)
264
+	}
255 265
 	for i, p := range messageParts {
256
-		fullStringFormatted += p
257
-		fullStringRaw += p
266
+		formattedBuf.WriteString(p)
267
+		if logger.MethodFile.Enabled {
268
+			rawBuf.WriteString(p)
269
+		}
258 270
 		if i != len(messageParts)-1 {
259
-			fullStringFormatted += " " + sep + " "
260
-			fullStringRaw += " : "
271
+			formattedBuf.WriteRune(' ')
272
+			formattedBuf.WriteString(separator)
273
+			formattedBuf.WriteRune(' ')
274
+			if logger.MethodFile.Enabled {
275
+				rawBuf.WriteString(" : ")
276
+			}
261 277
 		}
262 278
 	}
279
+	formattedBuf.WriteRune('\n')
280
+	if logger.MethodFile.Enabled {
281
+		rawBuf.WriteRune('\n')
282
+	}
263 283
 
264 284
 	// output
265 285
 	if logger.MethodSTDOUT {
266 286
 		logger.stdoutWriteLock.Lock()
267
-		fmt.Fprintln(colorable.NewColorableStdout(), fullStringFormatted)
287
+		colorableStdout.Write(formattedBuf.Bytes())
268 288
 		logger.stdoutWriteLock.Unlock()
269 289
 	}
270 290
 	if logger.MethodSTDERR {
271 291
 		logger.stdoutWriteLock.Lock()
272
-		fmt.Fprintln(colorable.NewColorableStderr(), fullStringFormatted)
292
+		colorableStderr.Write(formattedBuf.Bytes())
273 293
 		logger.stdoutWriteLock.Unlock()
274 294
 	}
275 295
 	if logger.MethodFile.Enabled {
276 296
 		logger.fileWriteLock.Lock()
277
-		logger.MethodFile.Writer.WriteString(fullStringRaw + "\n")
297
+		logger.MethodFile.Writer.Write(rawBuf.Bytes())
278 298
 		logger.MethodFile.Writer.Flush()
279 299
 		logger.fileWriteLock.Unlock()
280 300
 	}

+ 1
- 1
irc/nickname.go View File

@@ -59,7 +59,7 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
59 59
 
60 60
 	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
61 61
 	if hadNick {
62
-		target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nickname, nickname))
62
+		target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
63 63
 		target.server.whoWas.Append(whowas)
64 64
 		for friend := range target.Friends() {
65 65
 			friend.Send(nil, origNickMask, "NICK", nickname)

+ 62
- 2
irc/nickserv.go View File

@@ -25,6 +25,11 @@ func nsGroupEnabled(server *Server) bool {
25 25
 	return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled
26 26
 }
27 27
 
28
+func nsEnforceEnabled(server *Server) bool {
29
+	config := server.Config()
30
+	return config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement
31
+}
32
+
28 33
 const nickservHelp = `NickServ lets you register and login to an account.
29 34
 
30 35
 To see in-depth help for a specific NickServ command, try:
@@ -44,6 +49,22 @@ DROP de-links the given (or your current) nickname from your user account.`,
44 49
 			enabled:      servCmdRequiresAccreg,
45 50
 			authRequired: true,
46 51
 		},
52
+		"enforce": {
53
+			handler: nsEnforceHandler,
54
+			help: `Syntax: $bENFORCE [method]$b
55
+
56
+ENFORCE lets you specify a custom enforcement mechanism for your registered
57
+nicknames. Your options are:
58
+1. 'none'    [no enforcement, overriding the server default]
59
+2. 'timeout' [anyone using the nick must authenticate before a deadline,
60
+              or else they will be renamed]
61
+3. 'strict'  [you must already be authenticated to use the nick]
62
+4. 'default' [use the server default]
63
+With no arguments, queries your current enforcement status.`,
64
+			helpShort:    `$bENFORCE$b lets you change how your nicknames are reserved.`,
65
+			authRequired: true,
66
+			enabled:      nsEnforceEnabled,
67
+		},
47 68
 		"ghost": {
48 69
 			handler: nsGhostHandler,
49 70
 			help: `Syntax: $bGHOST <nickname>$b
@@ -200,6 +221,15 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
200 221
 	}
201 222
 }
202 223
 
224
+func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
225
+	throttled, remainingTime := client.loginThrottle.Touch()
226
+	if throttled {
227
+		nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
228
+		return false
229
+	}
230
+	return true
231
+}
232
+
203 233
 func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
204 234
 	loginSuccessful := false
205 235
 
@@ -207,6 +237,9 @@ func nsIdentifyHandler(server *Server, client *Client, command, params string, r
207 237
 
208 238
 	// try passphrase
209 239
 	if username != "" && passphrase != "" {
240
+		if !nsLoginThrottleCheck(client, rb) {
241
+			return
242
+		}
210 243
 		err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
211 244
 		loginSuccessful = (err == nil)
212 245
 	}
@@ -407,10 +440,15 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
407 440
 	var newPassword string
408 441
 	var errorMessage string
409 442
 
443
+	hasPrivs := client.HasRoleCapabs("accreg")
444
+	if !hasPrivs && !nsLoginThrottleCheck(client, rb) {
445
+		return
446
+	}
447
+
410 448
 	fields := strings.Fields(params)
411 449
 	switch len(fields) {
412 450
 	case 2:
413
-		if !client.HasRoleCapabs("accreg") {
451
+		if !hasPrivs {
414 452
 			errorMessage = "Insufficient privileges"
415 453
 		} else {
416 454
 			target, newPassword = fields[0], fields[1]
@@ -443,7 +481,29 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb
443 481
 	if err == nil {
444 482
 		nsNotice(rb, client.t("Password changed"))
445 483
 	} else {
446
-		server.logger.Error("internal", fmt.Sprintf("could not upgrade user password: %v", err))
484
+		server.logger.Error("internal", "could not upgrade user password:", err.Error())
447 485
 		nsNotice(rb, client.t("Password could not be changed due to server error"))
448 486
 	}
449 487
 }
488
+
489
+func nsEnforceHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
490
+	arg := strings.TrimSpace(params)
491
+
492
+	if arg == "" {
493
+		status := server.accounts.getStoredEnforcementStatus(client.Account())
494
+		nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
495
+	} else {
496
+		method, err := nickReservationFromString(arg)
497
+		if err != nil {
498
+			nsNotice(rb, client.t("Invalid parameters"))
499
+			return
500
+		}
501
+		err = server.accounts.SetEnforcementStatus(client.Account(), method)
502
+		if err == nil {
503
+			nsNotice(rb, client.t("Enforcement method set"))
504
+		} else {
505
+			server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error())
506
+			nsNotice(rb, client.t("An error occurred"))
507
+		}
508
+	}
509
+}

+ 9
- 11
irc/server.go View File

@@ -386,8 +386,7 @@ func (server *Server) tryRegister(c *Client) {
386 386
 		return
387 387
 	}
388 388
 
389
-	preregNick := c.PreregNick()
390
-	if preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
389
+	if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
391 390
 		return
392 391
 	}
393 392
 
@@ -400,10 +399,10 @@ func (server *Server) tryRegister(c *Client) {
400 399
 	}
401 400
 
402 401
 	rb := NewResponseBuffer(c)
403
-	nickAssigned := performNickChange(server, c, c, preregNick, rb)
402
+	nickAssigned := performNickChange(server, c, c, c.preregNick, rb)
404 403
 	rb.Send(true)
405 404
 	if !nickAssigned {
406
-		c.SetPreregNick("")
405
+		c.preregNick = ""
407 406
 		return
408 407
 	}
409 408
 
@@ -500,9 +499,9 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
500 499
 
501 500
 func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
502 501
 	cnick := client.Nick()
503
-	targetInfo := target.WhoWas()
504
-	rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nickname, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
505
-	tnick := targetInfo.nickname
502
+	targetInfo := target.Details()
503
+	rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
504
+	tnick := targetInfo.nick
506 505
 
507 506
 	whoischannels := client.WhoisChannelsNames(target)
508 507
 	if whoischannels != nil {
@@ -518,9 +517,8 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
518 517
 	if target.HasMode(modes.TLS) {
519 518
 		rb.Add(nil, client.server.name, RPL_WHOISSECURE, cnick, tnick, client.t("is using a secure connection"))
520 519
 	}
521
-	taccount := target.AccountName()
522
-	if taccount != "*" {
523
-		rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, taccount, client.t("is logged in as"))
520
+	if targetInfo.accountName != "*" {
521
+		rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, cnick, tnick, targetInfo.accountName, client.t("is logged in as"))
524 522
 	}
525 523
 	if target.HasMode(modes.Bot) {
526 524
 		rb.Add(nil, client.server.name, RPL_WHOISBOT, cnick, tnick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.Config().Network.Name)))
@@ -836,7 +834,7 @@ func (server *Server) setupPprofListener(config *Config) {
836 834
 		}
837 835
 		go func() {
838 836
 			if err := ps.ListenAndServe(); err != nil {
839
-				server.logger.Error("rehash", fmt.Sprintf("pprof listener failed: %v", err))
837
+				server.logger.Error("rehash", "pprof listener failed", err.Error())
840 838
 			}
841 839
 		}()
842 840
 		server.pprofServer = &ps

+ 1
- 10
irc/whowas.go View File

@@ -22,15 +22,6 @@ type WhoWasList struct {
22 22
 	accessMutex sync.RWMutex // tier 1
23 23
 }
24 24
 
25
-// WhoWas is an entry in the WhoWasList.
26
-type WhoWas struct {
27
-	nicknameCasefolded string
28
-	nickname           string
29
-	username           string
30
-	hostname           string
31
-	realname           string
32
-}
33
-
34 25
 // NewWhoWasList returns a new WhoWasList
35 26
 func NewWhoWasList(size int) *WhoWasList {
36 27
 	return &WhoWasList{
@@ -82,7 +73,7 @@ func (list *WhoWasList) Find(nickname string, limit int) (results []WhoWas) {
82 73
 	// iterate backwards through the ring buffer
83 74
 	pos := list.prev(list.end)
84 75
 	for limit == 0 || len(results) < limit {
85
-		if casefoldedNickname == list.buffer[pos].nicknameCasefolded {
76
+		if casefoldedNickname == list.buffer[pos].nickCasefolded {
86 77
 			results = append(results, list.buffer[pos])
87 78
 		}
88 79
 		if pos == list.start {

+ 13
- 13
irc/whowas_test.go View File

@@ -13,11 +13,11 @@ func makeTestWhowas(nick string) WhoWas {
13 13
 		panic(err)
14 14
 	}
15 15
 	return WhoWas{
16
-		nicknameCasefolded: cfnick,
17
-		nickname:           nick,
18
-		username:           "user",
19
-		hostname:           "oragono.io",
20
-		realname:           "Real Name",
16
+		nickCasefolded: cfnick,
17
+		nick:           nick,
18
+		username:       "user",
19
+		hostname:       "oragono.io",
20
+		realname:       "Real Name",
21 21
 	}
22 22
 }
23 23
 
@@ -36,48 +36,48 @@ func TestWhoWas(t *testing.T) {
36 36
 		t.Fatalf("incorrect whowas results: %v", results)
37 37
 	}
38 38
 	results = wwl.Find("dan-", 10)
39
-	if len(results) != 1 || results[0].nickname != "dan-" {
39
+	if len(results) != 1 || results[0].nick != "dan-" {
40 40
 		t.Fatalf("incorrect whowas results: %v", results)
41 41
 	}
42 42
 
43 43
 	wwl.Append(makeTestWhowas("slingamn"))
44 44
 	results = wwl.Find("slingamN", 10)
45
-	if len(results) != 1 || results[0].nickname != "slingamn" {
45
+	if len(results) != 1 || results[0].nick != "slingamn" {
46 46
 		t.Fatalf("incorrect whowas results: %v", results)
47 47
 	}
48 48
 
49 49
 	wwl.Append(makeTestWhowas("Dan-"))
50 50
 	results = wwl.Find("dan-", 10)
51 51
 	// reverse chronological order
52
-	if len(results) != 2 || results[0].nickname != "Dan-" || results[1].nickname != "dan-" {
52
+	if len(results) != 2 || results[0].nick != "Dan-" || results[1].nick != "dan-" {
53 53
 		t.Fatalf("incorrect whowas results: %v", results)
54 54
 	}
55 55
 	// 0 means no limit
56 56
 	results = wwl.Find("dan-", 0)
57
-	if len(results) != 2 || results[0].nickname != "Dan-" || results[1].nickname != "dan-" {
57
+	if len(results) != 2 || results[0].nick != "Dan-" || results[1].nick != "dan-" {
58 58
 		t.Fatalf("incorrect whowas results: %v", results)
59 59
 	}
60 60
 	// a limit of 1 should return the most recent entry only
61 61
 	results = wwl.Find("dan-", 1)
62
-	if len(results) != 1 || results[0].nickname != "Dan-" {
62
+	if len(results) != 1 || results[0].nick != "Dan-" {
63 63
 		t.Fatalf("incorrect whowas results: %v", results)
64 64
 	}
65 65
 
66 66
 	wwl.Append(makeTestWhowas("moocow"))
67 67
 	results = wwl.Find("moocow", 10)
68
-	if len(results) != 1 || results[0].nickname != "moocow" {
68
+	if len(results) != 1 || results[0].nick != "moocow" {
69 69
 		t.Fatalf("incorrect whowas results: %v", results)
70 70
 	}
71 71
 	results = wwl.Find("dan-", 10)
72 72
 	// should have overwritten the original entry, leaving the second
73
-	if len(results) != 1 || results[0].nickname != "Dan-" {
73
+	if len(results) != 1 || results[0].nick != "Dan-" {
74 74
 		t.Fatalf("incorrect whowas results: %v", results)
75 75
 	}
76 76
 
77 77
 	// overwrite the second entry
78 78
 	wwl.Append(makeTestWhowas("enckse"))
79 79
 	results = wwl.Find("enckse", 10)
80
-	if len(results) != 1 || results[0].nickname != "enckse" {
80
+	if len(results) != 1 || results[0].nick != "enckse" {
81 81
 		t.Fatalf("incorrect whowas results: %v", results)
82 82
 	}
83 83
 	results = wwl.Find("slingamn", 10)

+ 22
- 4
oragono.yaml View File

@@ -179,6 +179,17 @@ accounts:
179 179
     # is account authentication enabled?
180 180
     authentication-enabled: true
181 181
 
182
+    # throttle account login attempts (to prevent either password guessing, or DoS
183
+    # attacks on the server aimed at forcing repeated expensive bcrypt computations)
184
+    login-throttling:
185
+        enabled: true
186
+
187
+        # window
188
+        duration:  1m
189
+
190
+        # number of attempts allowed within the window
191
+        max-attempts: 3
192
+
182 193
     # some clients (notably Pidgin and Hexchat) offer only a single password field,
183 194
     # which makes it impossible to specify a separate server password (for the PASS
184 195
     # command) and SASL password. if this option is set to true, a client that
@@ -195,12 +206,19 @@ accounts:
195 206
         additional-nick-limit: 2
196 207
 
197 208
         # method describes how nickname reservation is handled
198
-        #   timeout: let the user change to the registered nickname, give them X seconds
199
-        #            to login and then rename them if they haven't done so
200
-        #   strict:  don't let the user change to the registered nickname unless they're
201
-        #            already logged-in using SASL or NickServ
209
+        #             already logged-in using SASL or NickServ
210
+        #   timeout:  let the user change to the registered nickname, give them X seconds
211
+        #             to login and then rename them if they haven't done so
212
+        #   strict:   don't let the user change to the registered nickname unless they're
213
+        #             already logged-in using SASL or NickServ
214
+        #   optional: no enforcement by default, but allow users to opt in to
215
+        #             the enforcement level of their choice
202 216
         method: timeout
203 217
 
218
+        # allow users to set their own nickname enforcement status, e.g.,
219
+        # to opt in to strict enforcement
220
+        allow-custom-enforcement: true
221
+
204 222
         # rename-timeout - this is how long users have 'til they're renamed
205 223
         rename-timeout: 30s
206 224
 

Loading…
Cancel
Save