|
@@ -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
|
|
-}
|