2017-03-27 14:15:02 +02:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2016-09-04 11:25:33 +02:00
// released under the MIT license
package irc
import (
2016-09-05 10:45:42 +02:00
"encoding/json"
2016-09-04 11:25:33 +02:00
"errors"
"fmt"
2016-09-05 10:45:42 +02:00
"log"
2016-09-04 11:25:33 +02:00
"strconv"
"strings"
"time"
2017-06-15 18:14:19 +02:00
"github.com/goshuirc/irc-go/ircfmt"
"github.com/goshuirc/irc-go/ircmsg"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/sno"
2016-09-04 11:25:33 +02:00
"github.com/tidwall/buntdb"
)
var (
2016-09-07 13:32:58 +02:00
errAccountCreation = errors . New ( "Account could not be created" )
errCertfpAlreadyExists = errors . New ( "An account already exists with your certificate" )
2016-09-04 11:25:33 +02:00
)
// AccountRegistration manages the registration of accounts.
type AccountRegistration struct {
2016-09-04 13:15:28 +02:00
Enabled bool
EnabledCallbacks [ ] string
EnabledCredentialTypes [ ] string
2016-09-04 11:25:33 +02:00
}
2016-09-05 10:45:42 +02:00
// AccountCredentials stores the various methods for verifying accounts.
type AccountCredentials struct {
PassphraseSalt [ ] byte
PassphraseHash [ ] byte
Certificate string // fingerprint
}
2016-09-04 11:25:33 +02:00
// NewAccountRegistration returns a new AccountRegistration, configured correctly.
func NewAccountRegistration ( config AccountRegistrationConfig ) ( accountReg AccountRegistration ) {
if config . Enabled {
accountReg . Enabled = true
2016-09-04 12:08:53 +02:00
for _ , name := range config . EnabledCallbacks {
// we store "none" as "*" internally
if name == "none" {
name = "*"
}
2016-09-04 13:15:28 +02:00
accountReg . EnabledCallbacks = append ( accountReg . EnabledCallbacks , name )
}
// no need to make this configurable, right now at least
accountReg . EnabledCredentialTypes = [ ] string {
"passphrase" ,
"certfp" ,
2016-09-04 12:08:53 +02:00
}
2016-09-04 11:25:33 +02:00
}
return accountReg
}
2017-03-08 12:36:13 +01:00
// accHandler parses the ACC command.
func accHandler ( server * Server , client * Client , msg ircmsg . IrcMessage ) bool {
2016-09-04 11:25:33 +02:00
subcommand := strings . ToLower ( msg . Params [ 0 ] )
2017-03-08 12:36:13 +01:00
if subcommand == "register" {
return accRegisterHandler ( server , client , msg )
2016-09-04 13:42:19 +02:00
} else if subcommand == "verify" {
2017-03-08 12:36:13 +01:00
client . Notice ( "VERIFY is not yet implemented" )
2016-09-04 13:42:19 +02:00
} else {
2017-03-08 12:36:13 +01:00
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , "ACC" , msg . Params [ 0 ] , "Unknown subcommand" )
2016-09-04 13:42:19 +02:00
}
return false
}
2017-03-08 12:36:13 +01:00
// removeFailedAccRegisterData removes the data created by ACC REGISTER if the account creation fails early.
func removeFailedAccRegisterData ( store * buntdb . DB , account string ) {
2016-09-05 10:45:42 +02:00
// error is ignored here, we can't do much about it anyways
store . Update ( func ( tx * buntdb . Tx ) error {
tx . Delete ( fmt . Sprintf ( keyAccountExists , account ) )
tx . Delete ( fmt . Sprintf ( keyAccountRegTime , account ) )
tx . Delete ( fmt . Sprintf ( keyAccountCredentials , account ) )
return nil
} )
}
2017-03-08 12:36:13 +01:00
// accRegisterHandler parses the ACC REGISTER command.
func accRegisterHandler ( server * Server , client * Client , msg ircmsg . IrcMessage ) bool {
2017-04-17 13:00:49 +02:00
// make sure reg is enabled
if ! server . accountRegistration . Enabled {
client . Send ( nil , server . name , ERR_REG_UNSPECIFIED_ERROR , client . nick , "*" , "Account registration is disabled" )
return false
}
2017-08-23 16:37:08 +02:00
// clients can't reg new accounts if they're already logged in
if client . account != nil {
client . Send ( nil , server . name , ERR_REG_UNSPECIFIED_ERROR , client . nick , "*" , "You're already logged into an account" )
return false
}
2016-09-04 13:42:19 +02:00
// get and sanitise account name
2016-10-11 15:51:46 +02:00
account := strings . TrimSpace ( msg . Params [ 1 ] )
casefoldedAccount , err := CasefoldName ( account )
// probably don't need explicit check for "*" here... but let's do it anyway just to make sure
if err != nil || msg . Params [ 1 ] == "*" {
client . Send ( nil , server . name , ERR_REG_UNSPECIFIED_ERROR , client . nick , account , "Account name is not valid" )
2016-09-04 13:42:19 +02:00
return false
}
// check whether account exists
// do it all in one write tx to prevent races
2016-10-11 15:51:46 +02:00
err = server . store . Update ( func ( tx * buntdb . Tx ) error {
accountKey := fmt . Sprintf ( keyAccountExists , casefoldedAccount )
2016-09-04 13:42:19 +02:00
_ , err := tx . Get ( accountKey )
if err != buntdb . ErrNotFound {
//TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_ACCOUNT_ALREADY_EXISTS , client . nick , account , "Account already exists" )
2016-09-04 13:42:19 +02:00
return errAccountCreation
2016-09-04 11:25:33 +02:00
}
2016-10-11 15:51:46 +02:00
registeredTimeKey := fmt . Sprintf ( keyAccountRegTime , casefoldedAccount )
2016-09-04 11:25:33 +02:00
2016-09-04 13:42:19 +02:00
tx . Set ( accountKey , "1" , nil )
2016-10-11 15:51:46 +02:00
tx . Set ( fmt . Sprintf ( keyAccountName , casefoldedAccount ) , account , nil )
2016-09-04 13:42:19 +02:00
tx . Set ( registeredTimeKey , strconv . FormatInt ( time . Now ( ) . Unix ( ) , 10 ) , nil )
return nil
} )
2016-09-04 11:25:33 +02:00
2016-09-04 13:42:19 +02:00
// account could not be created and relevant numerics have been dispatched, abort
if err != nil {
2016-09-05 11:43:32 +02:00
if err != errAccountCreation {
2017-03-08 12:36:13 +01:00
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , "ACC" , "REGISTER" , "Could not register" )
2016-09-05 11:43:32 +02:00
log . Println ( "Could not save registration initial data:" , err . Error ( ) )
}
2016-09-04 13:42:19 +02:00
return false
}
2016-09-04 11:25:33 +02:00
2016-09-04 13:42:19 +02:00
// account didn't already exist, continue with account creation and dispatching verification (if required)
callback := strings . ToLower ( msg . Params [ 2 ] )
var callbackNamespace , callbackValue string
2016-09-04 12:08:53 +02:00
2016-09-04 13:42:19 +02:00
if callback == "*" {
callbackNamespace = "*"
} else if strings . Contains ( callback , ":" ) {
callbackValues := strings . SplitN ( callback , ":" , 2 )
callbackNamespace , callbackValue = callbackValues [ 0 ] , callbackValues [ 1 ]
} else {
callbackNamespace = server . accountRegistration . EnabledCallbacks [ 0 ]
callbackValue = callback
}
2016-09-04 12:08:53 +02:00
2016-09-04 13:42:19 +02:00
// ensure the callback namespace is valid
// need to search callback list, maybe look at using a map later?
var callbackValid bool
for _ , name := range server . accountRegistration . EnabledCallbacks {
if callbackNamespace == name {
callbackValid = true
2016-09-04 12:08:53 +02:00
}
2016-09-04 13:42:19 +02:00
}
2016-09-04 12:08:53 +02:00
2016-09-04 13:42:19 +02:00
if ! callbackValid {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_REG_INVALID_CALLBACK , client . nick , account , callbackNamespace , "Callback namespace is not supported" )
2017-03-08 12:36:13 +01:00
removeFailedAccRegisterData ( server . store , casefoldedAccount )
2016-09-04 13:42:19 +02:00
return false
}
2016-09-04 12:08:53 +02:00
2016-09-04 13:42:19 +02:00
// get credential type/value
var credentialType , credentialValue string
2016-09-04 13:15:28 +02:00
2016-09-04 13:42:19 +02:00
if len ( msg . Params ) > 4 {
credentialType = strings . ToLower ( msg . Params [ 3 ] )
credentialValue = msg . Params [ 4 ]
} else if len ( msg . Params ) == 4 {
credentialType = "passphrase" // default from the spec
credentialValue = msg . Params [ 3 ]
} else {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_NEEDMOREPARAMS , client . nick , msg . Command , "Not enough parameters" )
2017-03-08 12:36:13 +01:00
removeFailedAccRegisterData ( server . store , casefoldedAccount )
2016-09-04 13:42:19 +02:00
return false
}
2016-09-04 13:15:28 +02:00
2016-09-04 13:42:19 +02:00
// ensure the credential type is valid
var credentialValid bool
for _ , name := range server . accountRegistration . EnabledCredentialTypes {
if credentialType == name {
credentialValid = true
2016-09-04 12:08:53 +02:00
}
2016-09-04 13:42:19 +02:00
}
2016-09-05 10:45:42 +02:00
if credentialType == "certfp" && client . certfp == "" {
2017-06-19 22:53:16 +02:00
client . Send ( nil , server . name , ERR_REG_INVALID_CRED_TYPE , client . nick , credentialType , callbackNamespace , "You are not using a TLS certificate" )
2017-03-08 12:36:13 +01:00
removeFailedAccRegisterData ( server . store , casefoldedAccount )
2016-09-05 10:45:42 +02:00
return false
}
2016-09-04 12:08:53 +02:00
2016-09-04 13:42:19 +02:00
if ! credentialValid {
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , ERR_REG_INVALID_CRED_TYPE , client . nick , credentialType , callbackNamespace , "Credential type is not supported" )
2017-03-08 12:36:13 +01:00
removeFailedAccRegisterData ( server . store , casefoldedAccount )
2016-09-04 13:42:19 +02:00
return false
}
2016-09-04 11:25:33 +02:00
2016-09-05 10:45:42 +02:00
// store details
err = server . store . Update ( func ( tx * buntdb . Tx ) error {
2016-09-07 13:32:58 +02:00
// certfp special lookup key
if credentialType == "certfp" {
assembledKeyCertToAccount := fmt . Sprintf ( keyCertToAccount , client . certfp )
// make sure certfp doesn't already exist because that'd be silly
_ , err := tx . Get ( assembledKeyCertToAccount )
if err != buntdb . ErrNotFound {
return errCertfpAlreadyExists
}
2016-10-11 15:51:46 +02:00
tx . Set ( assembledKeyCertToAccount , casefoldedAccount , nil )
2016-09-07 13:32:58 +02:00
}
// make creds
2016-09-05 10:45:42 +02:00
var creds AccountCredentials
// always set passphrase salt
creds . PassphraseSalt , err = NewSalt ( )
if err != nil {
return fmt . Errorf ( "Could not create passphrase salt: %s" , err . Error ( ) )
}
if credentialType == "certfp" {
creds . Certificate = client . certfp
} else if credentialType == "passphrase" {
creds . PassphraseHash , err = server . passwords . GenerateFromPassword ( creds . PassphraseSalt , credentialValue )
if err != nil {
return fmt . Errorf ( "Could not hash password: %s" , err )
}
}
credText , err := json . Marshal ( creds )
if err != nil {
return fmt . Errorf ( "Could not marshal creds: %s" , err )
}
2016-09-07 12:46:01 +02:00
tx . Set ( fmt . Sprintf ( keyAccountCredentials , account ) , string ( credText ) , nil )
2016-09-05 10:45:42 +02:00
return nil
} )
// details could not be stored and relevant numerics have been dispatched, abort
if err != nil {
2016-09-07 13:32:58 +02:00
errMsg := "Could not register"
if err == errCertfpAlreadyExists {
errMsg = "An account already exists for your certificate fingerprint"
}
2017-03-08 12:36:13 +01:00
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , "ACC" , "REGISTER" , errMsg )
2016-09-05 10:45:42 +02:00
log . Println ( "Could not save registration creds:" , err . Error ( ) )
2017-03-08 12:36:13 +01:00
removeFailedAccRegisterData ( server . store , casefoldedAccount )
2016-09-05 10:45:42 +02:00
return false
}
// automatically complete registration
2016-09-05 11:43:32 +02:00
if callbackNamespace == "*" {
2016-09-05 14:35:13 +02:00
err = server . store . Update ( func ( tx * buntdb . Tx ) error {
2016-11-06 04:47:13 +01:00
tx . Set ( fmt . Sprintf ( keyAccountVerified , casefoldedAccount ) , "1" , nil )
2016-09-05 14:54:09 +02:00
// load acct info inside store tx
account := ClientAccount {
Name : strings . TrimSpace ( msg . Params [ 1 ] ) ,
RegisteredAt : time . Now ( ) ,
Clients : [ ] * Client { client } ,
}
//TODO(dan): Consider creating ircd-wide account adding/removing/affecting lock for protecting access to these sorts of variables
2016-10-11 15:51:46 +02:00
server . accounts [ casefoldedAccount ] = & account
2016-09-05 14:54:09 +02:00
client . account = & account
2016-10-11 15:51:46 +02:00
client . Send ( nil , server . name , RPL_REGISTRATION_SUCCESS , client . nick , account . Name , "Account created" )
client . Send ( nil , server . name , RPL_LOGGEDIN , client . nick , client . nickMaskString , account . Name , fmt . Sprintf ( "You are now logged in as %s" , account . Name ) )
client . Send ( nil , server . name , RPL_SASLSUCCESS , client . nick , "Authentication successful" )
2017-05-28 20:43:09 +02:00
server . snomasks . Send ( sno . LocalAccounts , fmt . Sprintf ( ircfmt . Unescape ( "Account registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]" ) , account . Name , client . nickMaskString ) )
2016-09-05 14:35:13 +02:00
return nil
} )
if err != nil {
2017-03-08 12:36:13 +01:00
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , "ACC" , "REGISTER" , "Could not register" )
2016-09-05 14:35:13 +02:00
log . Println ( "Could not save verification confirmation (*):" , err . Error ( ) )
2017-03-08 12:36:13 +01:00
removeFailedAccRegisterData ( server . store , casefoldedAccount )
2016-09-05 14:35:13 +02:00
return false
}
2016-09-04 13:42:19 +02:00
return false
2016-09-04 11:25:33 +02:00
}
2016-09-05 10:45:42 +02:00
// dispatch callback
2016-09-05 11:43:32 +02:00
client . Notice ( fmt . Sprintf ( "We should dispatch a real callback here to %s:%s" , callbackNamespace , callbackValue ) )
2016-09-04 13:42:19 +02:00
2016-09-04 11:25:33 +02:00
return false
}