2016-09-04 11:25:33 +02:00
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// 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"
"github.com/DanielOaks/girc-go/ircmsg"
"github.com/tidwall/buntdb"
)
2016-09-05 10:45:42 +02:00
const (
keyAccountExists = "account %s exists"
2016-09-05 14:35:13 +02:00
keyAccountVerified = "account %s verified"
2016-09-05 15:00:21 +02:00
keyAccountName = "account %s name" // stores the 'preferred name' of the account, not casemapped
2016-09-05 10:45:42 +02:00
keyAccountRegTime = "account %s registered.time"
keyAccountCredentials = "account %s credentials"
)
2016-09-04 11:25:33 +02:00
var (
errAccountCreation = errors . New ( "Account could not be created" )
)
// 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
}
// regHandler parses the REG command.
func regHandler ( server * Server , client * Client , msg ircmsg . IrcMessage ) bool {
subcommand := strings . ToLower ( msg . Params [ 0 ] )
if subcommand == "create" {
2016-09-04 13:42:19 +02:00
return regCreateHandler ( server , client , msg )
} else if subcommand == "verify" {
client . Notice ( "Parsing VERIFY" )
} else {
client . Send ( nil , server . nameString , ERR_UNKNOWNERROR , client . nickString , "REG" , msg . Params [ 0 ] , "Unknown subcommand" )
}
return false
}
2016-09-05 10:45:42 +02:00
// removeFailedRegCreateData removes the data created by REG CREATE if the account creation fails early.
func removeFailedRegCreateData ( store buntdb . DB , account string ) {
// 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
} )
}
2016-09-04 13:42:19 +02:00
// regCreateHandler parses the REG CREATE command.
func regCreateHandler ( server * Server , client * Client , msg ircmsg . IrcMessage ) bool {
client . Notice ( "Parsing CREATE" )
2016-09-04 11:25:33 +02:00
2016-09-04 13:42:19 +02:00
// get and sanitise account name
account := NewName ( msg . Params [ 1 ] )
2016-09-05 14:54:09 +02:00
//TODO(dan): probably don't need explicit check for "*" here... until we actually casemap properly as per rfc7700
2016-09-04 13:42:19 +02:00
if ! account . IsNickname ( ) || msg . Params [ 1 ] == "*" {
client . Send ( nil , server . nameString , ERR_REG_UNSPECIFIED_ERROR , client . nickString , msg . Params [ 1 ] , "Account name is not valid" )
return false
}
accountString := account . String ( )
// check whether account exists
// do it all in one write tx to prevent races
err := server . store . Update ( func ( tx * buntdb . Tx ) error {
2016-09-05 10:45:42 +02:00
accountKey := fmt . Sprintf ( keyAccountExists , accountString )
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
client . Send ( nil , server . nameString , ERR_ACCOUNT_ALREADY_EXISTS , client . nickString , msg . Params [ 1 ] , "Account already exists" )
return errAccountCreation
2016-09-04 11:25:33 +02:00
}
2016-09-05 10:45:42 +02:00
registeredTimeKey := fmt . Sprintf ( keyAccountRegTime , accountString )
2016-09-04 11:25:33 +02:00
2016-09-04 13:42:19 +02:00
tx . Set ( accountKey , "1" , nil )
2016-09-05 14:54:09 +02:00
tx . Set ( fmt . Sprintf ( keyAccountName , accountString ) , strings . TrimSpace ( msg . Params [ 1 ] ) , 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 {
client . Send ( nil , server . nameString , ERR_UNKNOWNERROR , client . nickString , "REG" , "CREATE" , "Could not register" )
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 {
client . Send ( nil , server . nameString , ERR_REG_INVALID_CALLBACK , client . nickString , msg . Params [ 1 ] , callbackNamespace , "Callback namespace is not supported" )
2016-09-05 10:45:42 +02:00
removeFailedRegCreateData ( server . store , accountString )
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 {
client . Send ( nil , server . nameString , ERR_NEEDMOREPARAMS , client . nickString , msg . Command , "Not enough parameters" )
2016-09-05 10:45:42 +02:00
removeFailedRegCreateData ( server . store , accountString )
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 == "" {
client . Send ( nil , server . nameString , ERR_REG_INVALID_CRED_TYPE , client . nickString , credentialType , callbackNamespace , "You are not using a certificiate" )
removeFailedRegCreateData ( server . store , accountString )
return false
}
2016-09-04 12:08:53 +02:00
2016-09-04 13:42:19 +02:00
if ! credentialValid {
client . Send ( nil , server . nameString , ERR_REG_INVALID_CRED_TYPE , client . nickString , credentialType , callbackNamespace , "Credential type is not supported" )
2016-09-05 10:45:42 +02:00
removeFailedRegCreateData ( server . store , accountString )
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 {
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 )
}
tx . Set ( keyAccountCredentials , string ( credText ) , nil )
return nil
} )
// details could not be stored and relevant numerics have been dispatched, abort
if err != nil {
client . Send ( nil , server . nameString , ERR_UNKNOWNERROR , client . nickString , "REG" , "CREATE" , "Could not register" )
log . Println ( "Could not save registration creds:" , err . Error ( ) )
2016-09-05 14:35:13 +02:00
removeFailedRegCreateData ( server . store , accountString )
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 {
tx . Set ( keyAccountVerified , "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
server . accounts [ accountString ] = & account
client . account = & account
client . Send ( nil , server . nameString , RPL_REGISTRATION_SUCCESS , client . nickString , accountString , "Account created" )
client . Send ( nil , server . nameString , RPL_LOGGEDIN , client . nickString , client . nickMaskString , accountString , fmt . Sprintf ( "You are now logged in as %s" , accountString ) )
client . Send ( nil , server . nameString , RPL_SASLSUCCESS , client . nickString , "Authentication successful" )
2016-09-05 14:35:13 +02:00
return nil
} )
if err != nil {
client . Send ( nil , server . nameString , ERR_UNKNOWNERROR , client . nickString , "REG" , "CREATE" , "Could not register" )
log . Println ( "Could not save verification confirmation (*):" , err . Error ( ) )
removeFailedRegCreateData ( server . store , accountString )
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
}