Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gencapdefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
CapDef = namedtuple("CapDef", ['identifier', 'name', 'url', 'standard'])

CAPDEFS = [
CapDef(
identifier="Acc",
name="draft/acc",
url="https://github.com/ircv3/ircv3-specifications/pull/276",
standard="proposed IRCv3",
),
CapDef(
identifier="AccountNotify",
name="account-notify",
Expand Down
7 changes: 6 additions & 1 deletion irc/caps/defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ package caps

const (
// number of recognized capabilities:
numCapabs = 21
numCapabs = 22
// length of the uint64 array that represents the bitset:
bitsetLen = 1
)

const (
// Acc is the proposed IRCv3 capability named "draft/acc":
// https://github.com/ircv3/ircv3-specifications/pull/276
Acc Capability = iota

// AccountNotify is the IRCv3 capability named "account-notify":
// https://ircv3.net/specs/extensions/account-notify-3.1.html
AccountNotify Capability = iota
Expand Down Expand Up @@ -101,6 +105,7 @@ const (
// `capabilityNames[capab]` is the string name of the capability `capab`
var (
capabilityNames = [numCapabs]string{
"draft/acc",
"account-notify",
"account-tag",
"away-notify",
Expand Down
5 changes: 3 additions & 2 deletions irc/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ var Commands map[string]Command
func init() {
Commands = map[string]Command{
"ACC": {
handler: accHandler,
minParams: 3,
handler: accHandler,
usablePreReg: true,
minParams: 1,
},
"AMBIANCE": {
handler: sceneHandler,
Expand Down
117 changes: 81 additions & 36 deletions irc/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,42 @@ import (
"golang.org/x/crypto/bcrypt"
)

// ACC [REGISTER|VERIFY] ...
// ACC [LS|REGISTER|VERIFY] ...
func accHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
subcommand := strings.ToLower(msg.Params[0])

if subcommand == "ls" {
config := server.Config().Accounts

rb.Add(nil, server.name, "ACC", "LS", "SUBCOMMANDS", "LS REGISTER VERIFY")

var enabledCallbacks []string
for _, name := range config.Registration.EnabledCallbacks {
enabledCallbacks = append(enabledCallbacks, name)
}
sort.Strings(enabledCallbacks)
rb.Add(nil, server.name, "ACC", "LS", "CALLBACKS", strings.Join(enabledCallbacks, " "))

rb.Add(nil, server.name, "ACC", "LS", "CREDTYPES", "passphrase certfp")

if config.NickReservation.Enabled {
rb.Add(nil, server.name, "ACC", "LS", "FLAGS", "regnick")
}
return false
}

// disallow account stuff before connection registration has completed, for now
if !client.Registered() {
client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
return false
}

// make sure reg is enabled
if !server.AccountConfig().Registration.Enabled {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, client.nick, "*", client.t("Account registration is disabled"))
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNAVAILABLE", client.t("Account registration is disabled"))
return false
}

subcommand := strings.ToLower(msg.Params[0])

if subcommand == "register" {
return accRegisterHandler(server, client, msg, rb)
} else if subcommand == "verify" {
Expand All @@ -61,7 +87,7 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
callbackValues := strings.SplitN(callback, ":", 2)
callbackNamespace, callbackValue = callbackValues[0], callbackValues[1]
} else {
// "the IRC server MAY choose to use mailto as a default"
// "If a callback namespace is not ... provided, the IRC server MUST use mailto""
callbackNamespace = "mailto"
callbackValue = callback
}
Expand All @@ -81,31 +107,42 @@ func parseCallback(spec string, config *AccountConfig) (callbackNamespace string
// ACC REGISTER <accountname> [callback_namespace:]<callback> [cred_type] :<credential>
func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
nick := client.Nick()
// clients can't reg new accounts if they're already logged in
if client.LoggedIntoAccount() {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, "*", client.t("You're already logged into an account"))

if len(msg.Params) < 4 {
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
return false
}

// get and sanitise account name
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] == "*" {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, account, client.t("Account name is not valid"))

// check for account name of *
if account == "*" {
account = nick
} else {
if server.Config().Accounts.NickReservation.Enabled {
rb.Add(nil, server.name, "FAIL", "ACC", "REG_MUST_USE_REGNICK", account, client.t("Must register with current nickname instead of separate account name"))
return false
}
}

// clients can't reg new accounts if they're already logged in
if client.LoggedIntoAccount() {
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, client.t("You're already logged into an account"))
return false
}

if len(msg.Params) < 4 {
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, nick, msg.Command, client.t("Not enough parameters"))
// sanitise account name
casefoldedAccount, err := CasefoldName(account)
if err != nil {
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_ACCOUNT_NAME", account, client.t("Account name is not valid"))
return false
}

callbackSpec := msg.Params[2]
callbackNamespace, callbackValue := parseCallback(callbackSpec, server.AccountConfig())

if callbackNamespace == "" {
rb.Add(nil, server.name, ERR_REG_INVALID_CALLBACK, nick, account, callbackSpec, client.t("Callback namespace is not supported"))
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CALLBACK", account, callbackSpec, client.t("Cannot send verification code there"))
return false
}

Expand All @@ -129,12 +166,12 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
}
}
if credentialType == "certfp" && client.certfp == "" {
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("You are not using a TLS certificate"))
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CRED_TYPE", account, credentialType, client.t("You are not using a TLS certificate"))
return false
}

if !credentialValid {
rb.Add(nil, server.name, ERR_REG_INVALID_CRED_TYPE, nick, credentialType, callbackNamespace, client.t("Credential type is not supported"))
rb.Add(nil, server.name, "FAIL", "ACC", "REG_INVALID_CRED_TYPE", account, credentialType, client.t("Credential type is not supported"))
return false
}

Expand All @@ -147,14 +184,14 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r

throttled, remainingTime := client.loginThrottle.Touch()
if throttled {
rb.Add(nil, server.name, ERR_REG_UNSPECIFIED_ERROR, nick, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
return false
}

err = server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, certfp)
if err != nil {
msg, code := registrationErrorToMessageAndCode(err)
rb.Add(nil, server.name, code, nick, "ACC", "REGISTER", client.t(msg))
msg := registrationErrorToMessageAndCode(err)
rb.Add(nil, server.name, "FAIL", "ACC", "REG_UNSPECIFIED_ERROR", account, client.t(msg))
return false
}

Expand All @@ -174,15 +211,13 @@ func accRegisterHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
return false
}

func registrationErrorToMessageAndCode(err error) (message, numeric string) {
func registrationErrorToMessageAndCode(err error) (message string) {
// default responses: let's be risk-averse about displaying internal errors
// to the clients, especially for something as sensitive as accounts
message = `Could not register`
numeric = ERR_UNKNOWNERROR
switch err {
case errAccountAlreadyRegistered, errAccountAlreadyVerified:
message = err.Error()
numeric = ERR_ACCOUNT_ALREADY_EXISTS
case errAccountCreation, errAccountMustHoldNick, errAccountBadPassphrase, errCertfpAlreadyExists, errFeatureDisabled:
message = err.Error()
}
Expand All @@ -194,20 +229,23 @@ func sendSuccessfulRegResponse(client *Client, rb *ResponseBuffer, forNS bool) {
if forNS {
rb.Notice(client.t("Account created"))
} else {
rb.Add(nil, client.server.name, RPL_REGISTRATION_SUCCESS, client.nick, client.AccountName(), client.t("Account created"))
rb.Add(nil, client.server.name, RPL_REG_SUCCESS, client.nick, client.AccountName(), client.t("Account created"))
}
sendSuccessfulSaslAuth(client, rb, forNS)
sendSuccessfulAccountAuth(client, rb, forNS, false)
}

// sendSuccessfulSaslAuth means that a SASL auth attempt completed successfully, and is used to dispatch messages.
func sendSuccessfulSaslAuth(client *Client, rb *ResponseBuffer, forNS bool) {
// sendSuccessfulAccountAuth means that an account auth attempt completed successfully, and is used to dispatch messages.
func sendSuccessfulAccountAuth(client *Client, rb *ResponseBuffer, forNS, forSASL bool) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forNS and forSASL are an XOR, right? Should this just be a single bool?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm. I have thoughts about this. forSASL is sort of a dodgy name, it's basically trying to capture the case of 'for login' vs 'for ... um, not logging in, like registration?' since those are the two places we use that function. forSASL's just the easiest way to present it, so long as we assume that NS identify is also 'for sasl'.

details := client.Details()

if forNS {
rb.Notice(fmt.Sprintf(client.t("You're now logged in as %s"), details.accountName))
} else {
//TODO(dan): some servers send this numeric even for NickServ logins iirc? to confirm and maybe do too
rb.Add(nil, client.server.name, RPL_LOGGEDIN, details.nick, details.nickMask, details.accountName, fmt.Sprintf(client.t("You are now logged in as %s"), details.accountName))
rb.Add(nil, client.server.name, RPL_SASLSUCCESS, details.nick, client.t("Authentication successful"))
if forSASL {
rb.Add(nil, client.server.name, RPL_SASLSUCCESS, details.nick, client.t("Authentication successful"))
}
}

// dispatch account-notify
Expand All @@ -223,26 +261,33 @@ func sendSuccessfulSaslAuth(client *Client, rb *ResponseBuffer, forNS bool) {
// ACC VERIFY <accountname> <auth_code>
func accVerifyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
account := strings.TrimSpace(msg.Params[1])

if len(msg.Params) < 3 {
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), msg.Command, client.t("Not enough parameters"))
return false
}

err := server.accounts.Verify(client, account, msg.Params[2])

var code string
var message string

if err == errAccountVerificationInvalidCode {
code = ERR_ACCOUNT_INVALID_VERIFY_CODE
code = "ACCOUNT_INVALID_VERIFY_CODE"
message = err.Error()
} else if err == errAccountAlreadyVerified {
code = ERR_ACCOUNT_ALREADY_VERIFIED
code = "ACCOUNT_ALREADY_VERIFIED"
message = err.Error()
} else if err != nil {
code = ERR_UNKNOWNERROR
code = "VERIFY_UNSPECIFIED_ERROR"
message = errAccountVerificationFailed.Error()
}

if err == nil {
sendSuccessfulRegResponse(client, rb, false)
rb.Add(nil, server.name, RPL_VERIFY_SUCCESS, client.Nick(), account, client.t("Account verification successful"))
sendSuccessfulAccountAuth(client, rb, false, false)
} else {
rb.Add(nil, server.name, code, client.Nick(), account, client.t(message))
rb.Add(nil, server.name, "FAIL", "ACC", code, account, client.t(message))
}

return false
Expand Down Expand Up @@ -373,7 +418,7 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
return false
}

sendSuccessfulSaslAuth(client, rb, false)
sendSuccessfulAccountAuth(client, rb, false, true)
return false
}

Expand Down Expand Up @@ -401,7 +446,7 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
return false
}

sendSuccessfulSaslAuth(client, rb, false)
sendSuccessfulAccountAuth(client, rb, false, true)
return false
}

Expand Down
5 changes: 2 additions & 3 deletions irc/nickserv.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
}

if loginSuccessful {
sendSuccessfulSaslAuth(client, rb, true)
sendSuccessfulAccountAuth(client, rb, true, true)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, false? (Or should we reduce this to a single bool?)

} else {
nsNotice(rb, client.t("Could not login with your TLS certificate or supplied username/password"))
}
Expand Down Expand Up @@ -407,8 +407,7 @@ func nsRegisterHandler(server *Server, client *Client, command string, params []

// details could not be stored and relevant numerics have been dispatched, abort
if err != nil {
errMsg, _ := registrationErrorToMessageAndCode(err)
nsNotice(rb, errMsg)
nsNotice(rb, client.t(registrationErrorToMessageAndCode(err)))
return
}
}
Expand Down
Loading