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
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
+	"crypto/rand"
8
+	"crypto/subtle"
9
+	"encoding/hex"
7
 	"encoding/json"
10
 	"encoding/json"
11
+	"errors"
8
 	"fmt"
12
 	"fmt"
13
+	"net/smtp"
9
 	"strconv"
14
 	"strconv"
10
 	"strings"
15
 	"strings"
11
 	"sync"
16
 	"sync"
12
 	"time"
17
 	"time"
13
 
18
 
14
-	"github.com/goshuirc/irc-go/ircfmt"
15
 	"github.com/oragono/oragono/irc/caps"
19
 	"github.com/oragono/oragono/irc/caps"
16
 	"github.com/oragono/oragono/irc/passwd"
20
 	"github.com/oragono/oragono/irc/passwd"
17
-	"github.com/oragono/oragono/irc/sno"
18
 	"github.com/tidwall/buntdb"
21
 	"github.com/tidwall/buntdb"
19
 )
22
 )
20
 
23
 
21
 const (
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
 // everything about accounts is persistent; therefore, the database is the authoritative
35
 // everything about accounts is persistent; therefore, the database is the authoritative
51
 }
56
 }
52
 
57
 
53
 func (am *AccountManager) buildNickToAccountIndex() {
58
 func (am *AccountManager) buildNickToAccountIndex() {
54
-	if am.server.AccountConfig().NickReservation.Enabled {
59
+	if !am.server.AccountConfig().NickReservation.Enabled {
55
 		return
60
 		return
56
 	}
61
 	}
57
 
62
 
106
 
111
 
107
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
112
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
108
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
113
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
114
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
109
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
115
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
110
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
116
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
117
+	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
111
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
118
 	certFPKey := fmt.Sprintf(keyCertToAccount, certfp)
112
 
119
 
113
 	var creds AccountCredentials
120
 	var creds AccountCredentials
134
 	credStr := string(credText)
141
 	credStr := string(credText)
135
 
142
 
136
 	registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
143
 	registeredTimeStr := strconv.FormatInt(time.Now().Unix(), 10)
144
+	callbackSpec := fmt.Sprintf("%s:%s", callbackNamespace, callbackValue)
137
 
145
 
138
 	var setOptions *buntdb.SetOptions
146
 	var setOptions *buntdb.SetOptions
139
 	ttl := am.server.AccountConfig().Registration.VerifyTimeout
147
 	ttl := am.server.AccountConfig().Registration.VerifyTimeout
159
 		tx.Set(accountNameKey, account, setOptions)
167
 		tx.Set(accountNameKey, account, setOptions)
160
 		tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
168
 		tx.Set(registeredTimeKey, registeredTimeStr, setOptions)
161
 		tx.Set(credentialsKey, credStr, setOptions)
169
 		tx.Set(credentialsKey, credStr, setOptions)
170
+		tx.Set(callbackKey, callbackSpec, setOptions)
162
 		if certfp != "" {
171
 		if certfp != "" {
163
 			tx.Set(certFPKey, casefoldedAccount, setOptions)
172
 			tx.Set(certFPKey, casefoldedAccount, setOptions)
164
 		}
173
 		}
169
 		return err
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
 func (am *AccountManager) Verify(client *Client, account string, code string) error {
246
 func (am *AccountManager) Verify(client *Client, account string, code string) error {
182
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
253
 	accountKey := fmt.Sprintf(keyAccountExists, casefoldedAccount)
183
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
254
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
184
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
255
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
256
+	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
257
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
185
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
258
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
186
 
259
 
187
 	var raw rawClientAccount
260
 	var raw rawClientAccount
190
 		am.serialCacheUpdateMutex.Lock()
263
 		am.serialCacheUpdateMutex.Lock()
191
 		defer am.serialCacheUpdateMutex.Unlock()
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
 			raw, err = am.loadRawAccount(tx, casefoldedAccount)
267
 			raw, err = am.loadRawAccount(tx, casefoldedAccount)
195
 			if err == errAccountDoesNotExist {
268
 			if err == errAccountDoesNotExist {
196
 				return errAccountDoesNotExist
269
 				return errAccountDoesNotExist
200
 				return errAccountAlreadyVerified
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
 			// verify the account
290
 			// verify the account
207
 			tx.Set(verifiedKey, "1", nil)
291
 			tx.Set(verifiedKey, "1", nil)
292
+			// don't need the code anymore
293
+			tx.Delete(verificationCodeKey)
208
 			// re-set all other keys, removing the TTL
294
 			// re-set all other keys, removing the TTL
209
 			tx.Set(accountKey, "1", nil)
295
 			tx.Set(accountKey, "1", nil)
210
 			tx.Set(accountNameKey, raw.Name, nil)
296
 			tx.Set(accountNameKey, raw.Name, nil)
211
 			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
297
 			tx.Set(registeredTimeKey, raw.RegisteredAt, nil)
298
+			tx.Set(callbackKey, raw.Callback, nil)
212
 			tx.Set(credentialsKey, raw.Credentials, nil)
299
 			tx.Set(credentialsKey, raw.Credentials, nil)
213
 
300
 
214
 			var creds AccountCredentials
301
 			var creds AccountCredentials
295
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
382
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
296
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
383
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
297
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
384
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
385
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
298
 
386
 
299
 	_, e := tx.Get(accountKey)
387
 	_, e := tx.Get(accountKey)
300
 	if e == buntdb.ErrNotFound {
388
 	if e == buntdb.ErrNotFound {
302
 		return
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
 	if _, e = tx.Get(verifiedKey); e == nil {
398
 	if _, e = tx.Get(verifiedKey); e == nil {
315
 		result.Verified = true
399
 		result.Verified = true
316
 	}
400
 	}
328
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
412
 	accountNameKey := fmt.Sprintf(keyAccountName, casefoldedAccount)
329
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
413
 	registeredTimeKey := fmt.Sprintf(keyAccountRegTime, casefoldedAccount)
330
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
414
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
415
+	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
416
+	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
331
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
417
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
332
 
418
 
333
 	var clients []*Client
419
 	var clients []*Client
343
 			tx.Delete(accountNameKey)
429
 			tx.Delete(accountNameKey)
344
 			tx.Delete(verifiedKey)
430
 			tx.Delete(verifiedKey)
345
 			tx.Delete(registeredTimeKey)
431
 			tx.Delete(registeredTimeKey)
432
+			tx.Delete(callbackKey)
433
+			tx.Delete(verificationCodeKey)
346
 			credText, err = tx.Get(credentialsKey)
434
 			credText, err = tx.Get(credentialsKey)
347
 			tx.Delete(credentialsKey)
435
 			tx.Delete(credentialsKey)
348
 			return nil
436
 			return nil
484
 	Name         string
572
 	Name         string
485
 	RegisteredAt string
573
 	RegisteredAt string
486
 	Credentials  string
574
 	Credentials  string
575
+	Callback     string
487
 	Verified     bool
576
 	Verified     bool
488
 }
577
 }
489
 
578
 
490
 // LoginToAccount logs the client into the given account.
579
 // LoginToAccount logs the client into the given account.
491
 func (client *Client) LoginToAccount(account string) {
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
 // LogoutOfAccount logs the client out of their current account.
587
 // LogoutOfAccount logs the client out of their current account.
518
 	client.nickTimer.Touch()
595
 	client.nickTimer.Touch()
519
 
596
 
520
 	// dispatch account-notify
597
 	// dispatch account-notify
598
+	// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else
521
 	for friend := range client.Friends(caps.AccountNotify) {
599
 	for friend := range client.Friends(caps.AccountNotify) {
522
 		friend.Send(nil, client.nickMaskString, "ACCOUNT", "*")
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
 
9
 
10
 // Runtime Errors
10
 // Runtime Errors
11
 var (
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
 // Socket Errors
35
 // Socket Errors

+ 8
- 2
irc/getters.go View File

125
 	return client.accountName
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
 	var casefoldedAccount string
129
 	var casefoldedAccount string
130
+	var err error
130
 	if account != "" {
131
 	if account != "" {
131
-		casefoldedAccount, _ = CasefoldName(account)
132
+		if casefoldedAccount, err = CasefoldName(account); err != nil {
133
+			return
134
+		}
132
 	}
135
 	}
136
+
133
 	client.stateMutex.Lock()
137
 	client.stateMutex.Lock()
134
 	defer client.stateMutex.Unlock()
138
 	defer client.stateMutex.Unlock()
139
+	changed = client.account != casefoldedAccount
135
 	client.account = casefoldedAccount
140
 	client.account = casefoldedAccount
136
 	client.accountName = account
141
 	client.accountName = account
142
+	return
137
 }
143
 }
138
 
144
 
139
 func (client *Client) HasMode(mode modes.Mode) bool {
145
 func (client *Client) HasMode(mode modes.Mode) bool {

+ 100
- 38
irc/handlers.go View File

35
 
35
 
36
 // ACC [REGISTER|VERIFY] ...
36
 // ACC [REGISTER|VERIFY] ...
37
 func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
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
 	subcommand := strings.ToLower(msg.Params[0])
44
 	subcommand := strings.ToLower(msg.Params[0])
39
 
45
 
40
 	if subcommand == "register" {
46
 	if subcommand == "register" {
41
 		return accRegisterHandler(server, client, msg, rb)
47
 		return accRegisterHandler(server, client, msg, rb)
42
 	} else if subcommand == "verify" {
48
 	} else if subcommand == "verify" {
43
-		rb.Notice(client.t("VERIFY is not yet implemented"))
49
+		return accVerifyHandler(server, client, msg, rb)
44
 	} else {
50
 	} else {
45
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
51
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, "ACC", msg.Params[0], client.t("Unknown subcommand"))
46
 	}
52
 	}
48
 	return false
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
 	// clients can't reg new accounts if they're already logged in
85
 	// clients can't reg new accounts if they're already logged in
60
 	if client.LoggedIntoAccount() {
86
 	if client.LoggedIntoAccount() {
61
 		if server.AccountConfig().Registration.AllowMultiplePerConnection {
87
 		if server.AccountConfig().Registration.AllowMultiplePerConnection {
80
 		return false
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
 		return false
114
 		return false
108
 	}
115
 	}
109
 
116
 
165
 		if err != nil {
172
 		if err != nil {
166
 			return false
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
 	return false
239
 	return false
178
 }
240
 }
301
 		return false
363
 		return false
302
 	}
364
 	}
303
 
365
 
304
-	client.successfulSaslAuth(rb)
366
+	sendSuccessfulSaslAuth(client, rb, false)
305
 	return false
367
 	return false
306
 }
368
 }
307
 
369
 
329
 		return false
391
 		return false
330
 	}
392
 	}
331
 
393
 
332
-	client.successfulSaslAuth(rb)
394
+	sendSuccessfulSaslAuth(client, rb, false)
333
 	return false
395
 	return false
334
 }
396
 }
335
 
397
 

+ 87
- 58
irc/nickserv.go View File

6
 import (
6
 import (
7
 	"fmt"
7
 	"fmt"
8
 	"strings"
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
 const nickservHelp = `NickServ lets you register and log into a user account.
13
 const nickservHelp = `NickServ lets you register and log into a user account.
15
 
14
 
16
 To register an account:
15
 To register an account:
17
-	/NS REGISTER username [password]
16
+	/NS REGISTER username email [password]
18
 Leave out [password] if you're registering using your client certificate fingerprint.
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
 To unregister an account:
24
 To unregister an account:
21
 	/NS UNREGISTER [username]
25
 	/NS UNREGISTER [username]
53
 		}
57
 		}
54
 	} else if command == "register" {
58
 	} else if command == "register" {
55
 		// get params
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
 	} else if command == "identify" {
66
 	} else if command == "identify" {
66
-		// get params
67
 		username, passphrase := extractParam(params)
67
 		username, passphrase := extractParam(params)
68
-
69
 		server.nickservIdentifyHandler(client, username, passphrase, rb)
68
 		server.nickservIdentifyHandler(client, username, passphrase, rb)
70
 	} else if command == "unregister" {
69
 	} else if command == "unregister" {
71
 		username, _ := extractParam(params)
70
 		username, _ := extractParam(params)
98
 		return
97
 		return
99
 	}
98
 	}
100
 
99
 
100
+	if cfname == client.Account() {
101
+		client.server.accounts.Logout(client)
102
+	}
103
+
101
 	err = server.accounts.Unregister(cfname)
104
 	err = server.accounts.Unregister(cfname)
102
 	if err == errAccountDoesNotExist {
105
 	if err == errAccountDoesNotExist {
103
 		rb.Notice(client.t(err.Error()))
106
 		rb.Notice(client.t(err.Error()))
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
 		return
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
 	if !server.AccountConfig().Registration.Enabled {
133
 	if !server.AccountConfig().Registration.Enabled {
119
 		rb.Notice(client.t("Account registration has been disabled"))
134
 		rb.Notice(client.t("Account registration has been disabled"))
120
 		return
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
 	if client.LoggedIntoAccount() {
149
 	if client.LoggedIntoAccount() {
124
 		if server.AccountConfig().Registration.AllowMultiplePerConnection {
150
 		if server.AccountConfig().Registration.AllowMultiplePerConnection {
125
 			server.accounts.Logout(client)
151
 			server.accounts.Logout(client)
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
 	if err == nil {
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
 	// details could not be stored and relevant numerics have been dispatched, abort
196
 	// details could not be stored and relevant numerics have been dispatched, abort
162
 		rb.Notice(client.t(errMsg))
204
 		rb.Notice(client.t(errMsg))
163
 		return
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
 func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
209
 func (server *Server) nickservIdentifyHandler(client *Client, username, passphrase string, rb *ResponseBuffer) {
176
 		return
213
 		return
177
 	}
214
 	}
178
 
215
 
216
+	loginSuccessful := false
217
+
179
 	// try passphrase
218
 	// try passphrase
180
 	if username != "" && passphrase != "" {
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
 	// try certfp
224
 	// try certfp
196
-	if client.certfp != "" {
225
+	if !loginSuccessful && client.certfp != "" {
197
 		err := server.accounts.AuthenticateByCertFP(client)
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
         # callbacks to allow
151
         # callbacks to allow
152
         enabled-callbacks:
152
         enabled-callbacks:
153
             - none # no verification needed, will instantly register successfully
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
         # allow multiple account registrations per connection
166
         # allow multiple account registrations per connection
156
         # this is for testing purposes and shouldn't be allowed on real networks
167
         # this is for testing purposes and shouldn't be allowed on real networks
157
         allow-multiple-per-connection: false
168
         allow-multiple-per-connection: false

Loading…
Cancel
Save