Browse Source

Merge pull request #196 from slingamn/smtp.1

implement mailto callbacks
tags/v0.11.0-beta
Daniel Oaks 6 years ago
parent
commit
0ea210c28c
No account linked to committer's email address
6 changed files with 343 additions and 165 deletions
  1. 115
    47
      irc/accounts.go
  2. 21
    19
      irc/errors.go
  3. 8
    2
      irc/getters.go
  4. 100
    38
      irc/handlers.go
  5. 87
    58
      irc/nickserv.go
  6. 12
    1
      oragono.yaml

+ 115
- 47
irc/accounts.go View File

@@ -4,27 +4,32 @@
4 4
 package irc
5 5
 
6 6
 import (
7
+	"crypto/rand"
8
+	"crypto/subtle"
9
+	"encoding/hex"
7 10
 	"encoding/json"
11
+	"errors"
8 12
 	"fmt"
13
+	"net/smtp"
9 14
 	"strconv"
10 15
 	"strings"
11 16
 	"sync"
12 17
 	"time"
13 18
 
14
-	"github.com/goshuirc/irc-go/ircfmt"
15 19
 	"github.com/oragono/oragono/irc/caps"
16 20
 	"github.com/oragono/oragono/irc/passwd"
17
-	"github.com/oragono/oragono/irc/sno"
18 21
 	"github.com/tidwall/buntdb"
19 22
 )
20 23
 
21 24
 const (
22
-	keyAccountExists      = "account.exists %s"
23
-	keyAccountVerified    = "account.verified %s"
24
-	keyAccountName        = "account.name %s" // stores the 'preferred name' of the account, not casemapped
25
-	keyAccountRegTime     = "account.registered.time %s"
26
-	keyAccountCredentials = "account.credentials %s"
27
-	keyCertToAccount      = "account.creds.certfp %s"
25
+	keyAccountExists           = "account.exists %s"
26
+	keyAccountVerified         = "account.verified %s"
27
+	keyAccountCallback         = "account.callback %s"
28
+	keyAccountVerificationCode = "account.verificationcode %s"
29
+	keyAccountName             = "account.name %s" // stores the 'preferred name' of the account, not casemapped
30
+	keyAccountRegTime          = "account.registered.time %s"
31
+	keyAccountCredentials      = "account.credentials %s"
32
+	keyCertToAccount           = "account.creds.certfp %s"
28 33
 )
29 34
 
30 35
 // everything about accounts is persistent; therefore, the database is the authoritative
@@ -51,7 +56,7 @@ func NewAccountManager(server *Server) *AccountManager {
51 56
 }
52 57
 
53 58
 func (am *AccountManager) buildNickToAccountIndex() {
54
-	if am.server.AccountConfig().NickReservation.Enabled {
59
+	if !am.server.AccountConfig().NickReservation.Enabled {
55 60
 		return
56 61
 	}
57 62
 
@@ -106,8 +111,10 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
106 111
 
107 112
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
108 113
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
114
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
109 115
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
110 116
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
117
+	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
111 118
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
112 119
 
113 120
 	var creds AccountCredentials
@@ -134,6 +141,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
134 141
 	credStr := string(credText)
135 142
 
136 143
 	registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
144
+	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
137 145
 
138 146
 	var setOptions *buntdb.SetOptions
139 147
 	ttl := am.server.AccountConfig().Registration.VerifyTimeout
@@ -159,6 +167,7 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
159 167
 		tx.Set(accountNameKey, account, setOptions)
160 168
 		tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
161 169
 		tx.Set(credentialsKey, credStr, setOptions)
170
+		tx.Set(callbackKey, callbackSpec, setOptions)
162 171
 		if certfp != "" {
163 172
 			tx.Set(certFPKey, casefoldedAccount, setOptions)
164 173
 		}
@@ -169,7 +178,69 @@ func (am *AccountManager) Register(client *Client, account string, callbackNames
169 178
 		return err
170 179
 	}
171 180
 
172
-	return nil
181
+	code, err := am.dispatchCallback(client, casefoldedAccount, callbackNamespace, callbackValue)
182
+	if err != nil {
183
+		am.Unregister(casefoldedAccount)
184
+		return errCallbackFailed
185
+	} else {
186
+		return am.server.store.Update(func(tx *buntdb.Tx) error {
187
+			_, _, err = tx.Set(verificationCodeKey, code, setOptions)
188
+			return err
189
+		})
190
+	}
191
+}
192
+
193
+func (am *AccountManager) dispatchCallback(client *Client, casefoldedAccount string, callbackNamespace string, callbackValue string) (string, error) {
194
+	if callbackNamespace == "*" || callbackNamespace == "none" {
195
+		return "", nil
196
+	} else if callbackNamespace == "mailto" {
197
+		return am.dispatchMailtoCallback(client, casefoldedAccount, callbackValue)
198
+	} else {
199
+		return "", errors.New(fmt.Sprintf("Callback not implemented: %s", callbackNamespace))
200
+	}
201
+}
202
+
203
+func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
204
+	config := am.server.AccountConfig().Registration.Callbacks.Mailto
205
+	buf := make([]byte, 16)
206
+	rand.Read(buf)
207
+	code = hex.EncodeToString(buf)
208
+
209
+	subject := config.VerifyMessageSubject
210
+	if subject == "" {
211
+		subject = fmt.Sprintf(client.t("Verify your account on %s"), am.server.name)
212
+	}
213
+	messageStrings := []string{
214
+		fmt.Sprintf("From: %s\r\n", config.Sender),
215
+		fmt.Sprintf("To: %s\r\n", callbackValue),
216
+		fmt.Sprintf("Subject: %s\r\n", subject),
217
+		"\r\n", // end headers, begin message body
218
+		fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
219
+		fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
220
+		"\r\n",
221
+		client.t("To verify your account, issue one of these commands:") + "\r\n",
222
+		fmt.Sprintf("/ACC VERIFY %s %s", casefoldedAccount, code) + "\r\n",
223
+		fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
224
+	}
225
+
226
+	var message []byte
227
+	for i := 0; i < len(messageStrings); i++ {
228
+		message = append(message, []byte(messageStrings[i])...)
229
+	}
230
+	addr := fmt.Sprintf("%s:%d", config.Server, config.Port)
231
+	var auth smtp.Auth
232
+	if config.Username != "" && config.Password != "" {
233
+		auth = smtp.PlainAuth("", config.Username, config.Password, config.Server)
234
+	}
235
+
236
+	// TODO: this will never send the password in plaintext over a nonlocal link,
237
+	// but it might send the email in plaintext, regardless of the value of
238
+	// config.TLS.InsecureSkipVerify
239
+	err = smtp.SendMail(addr, auth, config.Sender, []string{callbackValue}, message)
240
+	if err != nil {
241
+		am.server.logger.Error("internal", fmt.Sprintf("Failed to dispatch e-mail: %v", err))
242
+	}
243
+	return
173 244
 }
174 245
 
175 246
 func (am *AccountManager) Verify(client *Client, account string, code string) error {
@@ -182,6 +253,8 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
182 253
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
183 254
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
184 255
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
256
+	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
257
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
185 258
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
186 259
 
187 260
 	var raw rawClientAccount
@@ -190,7 +263,7 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
190 263
 		am.serialCacheUpdateMutex.Lock()
191 264
 		defer am.serialCacheUpdateMutex.Unlock()
192 265
 
193
-		am.server.store.Update(func(tx *buntdb.Tx) error {
266
+		err = am.server.store.Update(func(tx *buntdb.Tx) error {
194 267
 			raw, err = am.loadRawAccount(tx, casefoldedAccount)
195 268
 			if err == errAccountDoesNotExist {
196 269
 				return errAccountDoesNotExist
@@ -200,15 +273,29 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
200 273
 				return errAccountAlreadyVerified
201 274
 			}
202 275
 
203
-			// TODO add code verification here
204
-			// return errAccountVerificationFailed if it fails
276
+			// actually verify the code
277
+			// a stored code of "" means a none callback / no code required
278
+			success := false
279
+			storedCode, err := tx.Get(verificationCodeKey)
280
+			if err == nil {
281
+				// this is probably unnecessary
282
+				if storedCode == "" || subtle.ConstantTimeCompare([]byte(code), []byte(storedCode)) == 1 {
283
+					success = true
284
+				}
285
+			}
286
+			if !success {
287
+				return errAccountVerificationInvalidCode
288
+			}
205 289
 
206 290
 			// verify the account
207 291
 			tx.Set(verifiedKey, "1", nil)
292
+			// don't need the code anymore
293
+			tx.Delete(verificationCodeKey)
208 294
 			// re-set all other keys, removing the TTL
209 295
 			tx.Set(accountKey, "1", nil)
210 296
 			tx.Set(accountNameKey, raw.Name, nil)
211 297
 			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
298
+			tx.Set(callbackKey, raw.Callback, nil)
212 299
 			tx.Set(credentialsKey, raw.Credentials, nil)
213 300
 
214 301
 			var creds AccountCredentials
@@ -295,6 +382,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
295 382
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
296 383
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
297 384
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
385
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
298 386
 
299 387
 	_, e := tx.Get(accountKey)
300 388
 	if e == buntdb.ErrNotFound {
@@ -302,15 +390,11 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
302 390
 		return
303 391
 	}
304 392
 
305
-	if result.Name, err = tx.Get(accountNameKey); err != nil {
306
-		return
307
-	}
308
-	if result.RegisteredAt, err = tx.Get(registeredTimeKey); err != nil {
309
-		return
310
-	}
311
-	if result.Credentials, err = tx.Get(credentialsKey); err != nil {
312
-		return
313
-	}
393
+	result.Name, _ = tx.Get(accountNameKey)
394
+	result.RegisteredAt, _ = tx.Get(registeredTimeKey)
395
+	result.Credentials, _ = tx.Get(credentialsKey)
396
+	result.Callback, _ = tx.Get(callbackKey)
397
+
314 398
 	if _, e = tx.Get(verifiedKey); e == nil {
315 399
 		result.Verified = true
316 400
 	}
@@ -328,6 +412,8 @@ func (am *AccountManager) Unregister(account string) error {
328 412
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
329 413
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
330 414
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
415
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
416
+	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
331 417
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
332 418
 
333 419
 	var clients []*Client
@@ -343,6 +429,8 @@ func (am *AccountManager) Unregister(account string) error {
343 429
 			tx.Delete(accountNameKey)
344 430
 			tx.Delete(verifiedKey)
345 431
 			tx.Delete(registeredTimeKey)
432
+			tx.Delete(callbackKey)
433
+			tx.Delete(verificationCodeKey)
346 434
 			credText, err = tx.Get(credentialsKey)
347 435
 			tx.Delete(credentialsKey)
348 436
 			return nil
@@ -484,27 +572,16 @@ type rawClientAccount struct {
484 572
 	Name         string
485 573
 	RegisteredAt string
486 574
 	Credentials  string
575
+	Callback     string
487 576
 	Verified     bool
488 577
 }
489 578
 
490 579
 // LoginToAccount logs the client into the given account.
491 580
 func (client *Client) LoginToAccount(account string) {
492
-	casefoldedAccount, err := CasefoldName(account)
493
-	if err != nil {
494
-		return
495
-	}
496
-
497
-	if client.Account() == casefoldedAccount {
498
-		// already logged into this acct, no changing necessary
499
-		return
581
+	changed := client.SetAccountName(account)
582
+	if changed {
583
+		client.nickTimer.Touch()
500 584
 	}
501
-
502
-	client.SetAccountName(casefoldedAccount)
503
-	client.nickTimer.Touch()
504
-
505
-	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))
506
-
507
-	//TODO(dan): This should output the AccountNotify message instead of the sasl accepted function below.
508 585
 }
509 586
 
510 587
 // LogoutOfAccount logs the client out of their current account.
@@ -518,18 +595,9 @@ func (client *Client) LogoutOfAccount() {
518 595
 	client.nickTimer.Touch()
519 596
 
520 597
 	// dispatch account-notify
598
+	// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else
521 599
 	for friend := range client.Friends(caps.AccountNotify) {
522 600
 		friend.Send(nil, client.nickMaskString, "ACCOUNT", "*")
523 601
 	}
524 602
 }
525 603
 
526
-// successfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
527
-func (client *Client) successfulSaslAuth(rb *ResponseBuffer) {
528
-	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()))
529
-	rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful"))
530
-
531
-	// dispatch account-notify
532
-	for friend := range client.Friends(caps.AccountNotify) {
533
-		friend.Send(nil, client.nickMaskString, "ACCOUNT", client.AccountName())
534
-	}
535
-}

+ 21
- 19
irc/errors.go View File

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

+ 8
- 2
irc/getters.go View File

@@ -125,15 +125,21 @@ func (client *Client) AccountName() string {
125 125
 	return client.accountName
126 126
 }
127 127
 
128
-func (client *Client) SetAccountName(account string) {
128
+func (client *Client) SetAccountName(account string) (changed bool) {
129 129
 	var casefoldedAccount string
130
+	var err error
130 131
 	if account != "" {
131
-		casefoldedAccount, _ = CasefoldName(account)
132
+		if casefoldedAccount, err = CasefoldName(account); err != nil {
133
+			return
134
+		}
132 135
 	}
136
+
133 137
 	client.stateMutex.Lock()
134 138
 	defer client.stateMutex.Unlock()
139
+	changed = client.account != casefoldedAccount
135 140
 	client.account = casefoldedAccount
136 141
 	client.accountName = account
142
+	return
137 143
 }
138 144
 
139 145
 func (client *Client) HasMode(mode modes.Mode) bool {

+ 100
- 38
irc/handlers.go View File

@@ -35,12 +35,18 @@ import (
35 35
 
36 36
 // ACC [REGISTER|VERIFY] ...
37 37
 func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
38
+	// make sure reg is enabled
39
+	if !server.AccountConfig().Registration.Enabled {
40
+		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
41
+		return false
42
+	}
43
+
38 44
 	subcommand := strings.ToLower(msg.Params[0])
39 45
 
40 46
 	if subcommand == "register" {
41 47
 		return accRegisterHandler(server, client, msg, rb)
42 48
 	} else if subcommand == "verify" {
43
-		rb.Notice(client.t("VERIFY is not yet implemented"))
49
+		return accVerifyHandler(server, client, msg, rb)
44 50
 	} else {
45 51
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
46 52
 	}
@@ -48,14 +54,34 @@ func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
48 54
 	return false
49 55
 }
50 56
 
51
-// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
52
-func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
53
-	// make sure reg is enabled
54
-	if !server.AccountConfig().Registration.Enabled {
55
-		rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
56
-		return false
57
+// helper function to parse ACC callbacks, e.g., mailto:person@example.com, tel:16505551234
58
+func parseCallback(spec string, config *AccountConfig) (callbackNamespace string, callbackValue string) {
59
+	callback := strings.ToLower(spec)
60
+	if callback == "*" {
61
+		callbackNamespace = "*"
62
+	} else if strings.Contains(callback, ":") {
63
+		callbackValues := strings.SplitN(callback, ":", 2)
64
+		callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
65
+	} else {
66
+		// "the IRC server MAY choose to use mailto as a default"
67
+		callbackNamespace = "mailto"
68
+		callbackValue = callback
57 69
 	}
58 70
 
71
+	// ensure the callback namespace is valid
72
+	// need to search callback list, maybe look at using a map later?
73
+	for _, name := range config.Registration.EnabledCallbacks {
74
+		if callbackNamespace == name {
75
+			return
76
+		}
77
+	}
78
+	// error value
79
+	callbackNamespace = ""
80
+	return
81
+}
82
+
83
+// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
84
+func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
59 85
 	// clients can't reg new accounts if they're already logged in
60 86
 	if client.LoggedIntoAccount() {
61 87
 		if server.AccountConfig().Registration.AllowMultiplePerConnection {
@@ -80,30 +106,11 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
80 106
 		return false
81 107
 	}
82 108
 
83
-	callback := strings.ToLower(msg.Params[2])
84
-	var callbackNamespace, callbackValue string
85
-
86
-	if callback == "*" {
87
-		callbackNamespace = "*"
88
-	} else if strings.Contains(callback, ":") {
89
-		callbackValues := strings.SplitN(callback, ":", 2)
90
-		callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
91
-	} else {
92
-		callbackNamespace = server.AccountConfig().Registration.EnabledCallbacks[0]
93
-		callbackValue = callback
94
-	}
95
-
96
-	// ensure the callback namespace is valid
97
-	// need to search callback list, maybe look at using a map later?
98
-	var callbackValid bool
99
-	for _, name := range server.AccountConfig().Registration.EnabledCallbacks {
100
-		if callbackNamespace == name {
101
-			callbackValid = true
102
-		}
103
-	}
109
+	callbackSpec := msg.Params[2]
110
+	callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())
104 111
 
105
-	if !callbackValid {
106
-		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackNamespace, client.t("Callback namespace is not supported"))
112
+	if callbackNamespace == "" {
113
+		rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, client.nick, account, callbackSpec, client.t("Callback namespace is not supported"))
107 114
 		return false
108 115
 	}
109 116
 
@@ -165,14 +172,69 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
165 172
 		if err != nil {
166 173
 			return false
167 174
 		}
168
-		client.Send(nil, server.name, RPL_REGISTRATION_SUCCESS, client.nick, casefoldedAccount, client.t("Account created"))
169
-		client.Send(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
170
-		client.Send(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
171
-		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))
175
+		sendSuccessfulRegResponse(client, rb, false)
176
+	} else {
177
+		messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
178
+		message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
179
+		rb.Add(nil, server.name, RPL_REG_VERIFICATION_REQUIRED, client.nick, casefoldedAccount, message)
180
+	}
181
+
182
+	return false
183
+}
184
+
185
+// helper function to dispatch messages when a client successfully registers
186
+func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
187
+	if forNS {
188
+		rb.Notice(client.t("Account created"))
189
+	} else {
190
+		rb.Add(nil, client.server.name, RPL_REGISTRATION_SUCCESS, client.nick, client.AccountName(), client.t("Account created"))
172 191
 	}
192
+	sendSuccessfulSaslAuth(client, rb, forNS)
193
+}
194
+
195
+// sendSuccessfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
196
+func sendSuccessfulSaslAuth(client *Client, rb *ResponseBuffer, forNS bool) {
197
+	account := client.AccountName()
198
+
199
+	if forNS {
200
+		rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
201
+	} else {
202
+		rb.Add(nil, client.server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, account, fmt.Sprintf("You are now logged in as %s", account))
203
+		rb.Add(nil, client.server.name, RPL_SASLSUCCESS, client.nick, client.t("SASL authentication successful"))
204
+	}
205
+
206
+	// dispatch account-notify
207
+	for friend := range client.Friends(caps.AccountNotify) {
208
+		friend.Send(nil, client.nickMaskString, "ACCOUNT", account)
209
+	}
210
+
211
+	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))
212
+}
173 213
 
174
-	// dispatch callback
175
-	rb.Notice(fmt.Sprintf("We should dispatch a real callback here to %s:%s", callbackNamespace, callbackValue))
214
+// ACC VERIFY <accountname> <auth_code>
215
+func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
216
+	account := strings.TrimSpace(msg.Params[1])
217
+	err := server.accounts.Verify(client, account, msg.Params[2])
218
+
219
+	var code string
220
+	var message string
221
+
222
+	if err == errAccountVerificationInvalidCode {
223
+		code = ERR_ACCOUNT_INVALID_VERIFY_CODE
224
+		message = err.Error()
225
+	} else if err == errAccountAlreadyVerified {
226
+		code = ERR_ACCOUNT_ALREADY_VERIFIED
227
+		message = err.Error()
228
+	} else if err != nil {
229
+		code = ERR_UNKNOWNERROR
230
+		message = errAccountVerificationFailed.Error()
231
+	}
232
+
233
+	if err == nil {
234
+		sendSuccessfulRegResponse(client, rb, false)
235
+	} else {
236
+		rb.Add(nil, server.name, code, client.nick, account, client.t(message))
237
+	}
176 238
 
177 239
 	return false
178 240
 }
@@ -301,7 +363,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
301 363
 		return false
302 364
 	}
303 365
 
304
-	client.successfulSaslAuth(rb)
366
+	sendSuccessfulSaslAuth(client, rb, false)
305 367
 	return false
306 368
 }
307 369
 
@@ -329,7 +391,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
329 391
 		return false
330 392
 	}
331 393
 
332
-	client.successfulSaslAuth(rb)
394
+	sendSuccessfulSaslAuth(client, rb, false)
333 395
 	return false
334 396
 }
335 397
 

+ 87
- 58
irc/nickserv.go View File

@@ -6,16 +6,20 @@ package irc
6 6
 import (
7 7
 	"fmt"
8 8
 	"strings"
9
-
10
-	"github.com/goshuirc/irc-go/ircfmt"
11
-	"github.com/oragono/oragono/irc/sno"
12 9
 )
13 10
 
11
+// TODO: "email" is an oversimplification here; it's actually any callback, e.g.,
12
+// person@example.com, mailto:person@example.com, tel:16505551234.
14 13
 const nickservHelp = `NickServ lets you register and log into a user account.
15 14
 
16 15
 To register an account:
17
-	/NS REGISTER username [password]
16
+	/NS REGISTER username email [password]
18 17
 Leave out [password] if you're registering using your client certificate fingerprint.
18
+The server may or may not allow you to register anonymously (by sending * as your
19
+email address).
20
+
21
+To verify an account (if you were sent a verification code):
22
+	/NS VERIFY username code
19 23
 
20 24
 To unregister an account:
21 25
 	/NS UNREGISTER [username]
@@ -53,19 +57,14 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb
53 57
 		}
54 58
 	} else if command == "register" {
55 59
 		// get params
56
-		username, passphrase := extractParam(params)
57
-
58
-		// fail out if we need to
59
-		if username == "" {
60
-			rb.Notice(client.t("No username supplied"))
61
-			return
62
-		}
63
-
64
-		server.nickservRegisterHandler(client, username, passphrase, rb)
60
+		username, afterUsername := extractParam(params)
61
+		email, passphrase := extractParam(afterUsername)
62
+		server.nickservRegisterHandler(client, username, email, passphrase, rb)
63
+	} else if command == "verify" {
64
+		username, code := extractParam(params)
65
+		server.nickservVerifyHandler(client, username, code, rb)
65 66
 	} else if command == "identify" {
66
-		// get params
67 67
 		username, passphrase := extractParam(params)
68
-
69 68
 		server.nickservIdentifyHandler(client, username, passphrase, rb)
70 69
 	} else if command == "unregister" {
71 70
 		username, _ := extractParam(params)
@@ -98,6 +97,10 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
98 97
 		return
99 98
 	}
100 99
 
100
+	if cfname == client.Account() {
101
+		client.server.accounts.Logout(client)
102
+	}
103
+
101 104
 	err = server.accounts.Unregister(cfname)
102 105
 	if err == errAccountDoesNotExist {
103 106
 		rb.Notice(client.t(err.Error()))
@@ -108,18 +111,41 @@ func (server *Server) nickservUnregisterHandler(client *Client, username string,
108 111
 	}
109 112
 }
110 113
 
111
-func (server *Server) nickservRegisterHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
112
-	certfp := client.certfp
113
-	if passphrase == "" && certfp == "" {
114
-		rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
114
+func (server *Server) nickservVerifyHandler(client *Client, username string, code string, rb *ResponseBuffer) {
115
+	err := server.accounts.Verify(client, username, code)
116
+
117
+	var errorMessage string
118
+	if err == errAccountVerificationInvalidCode || err == errAccountAlreadyVerified {
119
+		errorMessage = err.Error()
120
+	} else if err != nil {
121
+		errorMessage = errAccountVerificationFailed.Error()
122
+	}
123
+
124
+	if errorMessage != "" {
125
+		rb.Notice(client.t(errorMessage))
115 126
 		return
116 127
 	}
117 128
 
129
+	sendSuccessfulRegResponse(client, rb, true)
130
+}
131
+
132
+func (server *Server) nickservRegisterHandler(client *Client, username, email, passphrase string, rb *ResponseBuffer) {
118 133
 	if !server.AccountConfig().Registration.Enabled {
119 134
 		rb.Notice(client.t("Account registration has been disabled"))
120 135
 		return
121 136
 	}
122 137
 
138
+	if username == "" {
139
+		rb.Notice(client.t("No username supplied"))
140
+		return
141
+	}
142
+
143
+	certfp := client.certfp
144
+	if passphrase == "" && certfp == "" {
145
+		rb.Notice(client.t("You need to either supply a passphrase or be connected via TLS with a client cert"))
146
+		return
147
+	}
148
+
123 149
 	if client.LoggedIntoAccount() {
124 150
 		if server.AccountConfig().Registration.AllowMultiplePerConnection {
125 151
 			server.accounts.Logout(client)
@@ -129,26 +155,42 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
129 155
 		}
130 156
 	}
131 157
 
132
-	// get and sanitise account name
133
-	account := strings.TrimSpace(username)
134
-	casefoldedAccount, err := CasefoldName(account)
135
-	// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
136
-	if err != nil || username == "*" {
137
-		rb.Notice(client.t("Account name is not valid"))
138
-		return
158
+	config := server.AccountConfig()
159
+	var callbackNamespace, callbackValue string
160
+	noneCallbackAllowed := false
161
+	for _, callback := range(config.Registration.EnabledCallbacks) {
162
+		if callback == "*" {
163
+			noneCallbackAllowed = true
164
+		}
139 165
 	}
140
-
141
-	// account could not be created and relevant numerics have been dispatched, abort
142
-	if err != nil {
143
-		if err != errAccountCreation {
144
-			rb.Notice(client.t("Account registration failed"))
166
+	// XXX if ACC REGISTER allows registration with the `none` callback, then ignore
167
+	// any callback that was passed here (to avoid confusion in the case where the ircd
168
+	// has no mail server configured). otherwise, register using the provided callback:
169
+	if noneCallbackAllowed {
170
+		callbackNamespace = "*"
171
+	} else {
172
+		callbackNamespace, callbackValue = parseCallback(email, config)
173
+		if callbackNamespace == "" {
174
+			rb.Notice(client.t("Registration requires a valid e-mail address"))
175
+			return
145 176
 		}
146
-		return
147 177
 	}
148 178
 
149
-	err = server.accounts.Register(client, account, "", "", passphrase, client.certfp)
179
+	// get and sanitise account name
180
+	account := strings.TrimSpace(username)
181
+
182
+	err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, client.certfp)
150 183
 	if err == nil {
151
-		err = server.accounts.Verify(client, casefoldedAccount, "")
184
+		if callbackNamespace == "*" {
185
+			err = server.accounts.Verify(client, account, "")
186
+			if err == nil {
187
+				sendSuccessfulRegResponse(client, rb, true)
188
+			}
189
+		} else {
190
+			messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s:%s")
191
+			message := fmt.Sprintf(messageTemplate, callbackNamespace, callbackValue)
192
+			rb.Notice(message)
193
+		}
152 194
 	}
153 195
 
154 196
 	// details could not be stored and relevant numerics have been dispatched, abort
@@ -162,11 +204,6 @@ func (server *Server) nickservRegisterHandler(client *Client, username, passphra
162 204
 		rb.Notice(client.t(errMsg))
163 205
 		return
164 206
 	}
165
-
166
-	rb.Notice(client.t("Account created"))
167
-	rb.Add(nil, server.name, RPL_LOGGEDIN, client.nick, client.nickMaskString, casefoldedAccount, fmt.Sprintf(client.t("You are now logged in as %s"), casefoldedAccount))
168
-	rb.Add(nil, server.name, RPL_SASLSUCCESS, client.nick, client.t("Authentication successful"))
169
-	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))
170 207
 }
171 208
 
172 209
 func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
@@ -176,31 +213,23 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
176 213
 		return
177 214
 	}
178 215
 
216
+	loginSuccessful := false
217
+
179 218
 	// try passphrase
180 219
 	if username != "" && passphrase != "" {
181
-		// keep it the same as in the ACC CREATE stage
182
-		accountName, err := CasefoldName(username)
183
-		if err != nil {
184
-			rb.Notice(client.t("Could not login with your username/password"))
185
-			return
186
-		}
187
-
188
-		err = server.accounts.AuthenticateByPassphrase(client, accountName, passphrase)
189
-		if err == nil {
190
-			rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), accountName))
191
-			return
192
-		}
220
+		err := server.accounts.AuthenticateByPassphrase(client, username, passphrase)
221
+		loginSuccessful = (err == nil)
193 222
 	}
194 223
 
195 224
 	// try certfp
196
-	if client.certfp != "" {
225
+	if !loginSuccessful && client.certfp != "" {
197 226
 		err := server.accounts.AuthenticateByCertFP(client)
198
-		if err == nil {
199
-			rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), client.AccountName()))
200
-			// TODO more notices?
201
-			return
202
-		}
227
+		loginSuccessful = (err == nil)
203 228
 	}
204 229
 
205
-	rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
230
+	if loginSuccessful {
231
+		sendSuccessfulSaslAuth(client, rb, true)
232
+	} else {
233
+		rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
234
+	}
206 235
 }

+ 12
- 1
oragono.yaml View File

@@ -151,7 +151,18 @@ accounts:
151 151
         # callbacks to allow
152 152
         enabled-callbacks:
153 153
             - none # no verification needed, will instantly register successfully
154
-        
154
+
155
+        # example configuration for sending verification emails via a local mail relay
156
+        # callbacks:
157
+        #     mailto:
158
+        #         server: localhost
159
+        #         port: 25
160
+        #         tls:
161
+        #             enabled: false
162
+        #         username: ""
163
+        #         password: ""
164
+        #         sender: "admin@my.network"
165
+
155 166
         # allow multiple account registrations per connection
156 167
         # this is for testing purposes and shouldn't be allowed on real networks
157 168
         allow-multiple-per-connection: false

Loading…
Cancel
Save