diff --git a/irc/client.go b/irc/client.go index 9fc041ae..462deeb8 100644 --- a/irc/client.go +++ b/irc/client.go @@ -623,12 +623,15 @@ func (client *Client) HasUsername() bool { return client.username != "" && client.username != "*" } +// SetNames sets the client's ident and realname. func (client *Client) SetNames(username, realname string) error { - usernameCasefolded, err := CasefoldName(username) - if err != nil { + // do this before casefolding to ensure these are actually ascii + if !isIdent(username) { return errInvalidUsername } + usernameCasefolded := strings.ToLower(username) // only ascii is supported in idents anyway + client.stateMutex.Lock() defer client.stateMutex.Unlock() diff --git a/irc/handlers.go b/irc/handlers.go index 7fadeffa..e89fee37 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2195,8 +2195,7 @@ func userHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp err := client.SetNames(msg.Params[0], msg.Params[3]) if err == errInvalidUsername { - rb.Add(nil, "", "ERROR", client.t("Malformed username")) - return true + rb.Add(nil, server.name, ERR_INVALIDUSERNAME, client.t("Malformed username")) } return false diff --git a/irc/numerics.go b/irc/numerics.go index ec8e83cc..05c4ff21 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -143,6 +143,7 @@ const ( ERR_YOUREBANNEDCREEP = "465" ERR_YOUWILLBEBANNED = "466" ERR_KEYSET = "467" + ERR_INVALIDUSERNAME = "468" ERR_CHANNELISFULL = "471" ERR_UNKNOWNMODE = "472" ERR_INVITEONLYCHAN = "473" diff --git a/irc/strings.go b/irc/strings.go index 0e5e50ca..5ef67e78 100644 --- a/irc/strings.go +++ b/irc/strings.go @@ -128,6 +128,32 @@ func isBoring(name string) bool { return true } +// returns true if the given name is a valid ident, using a mix of Insp and +// Chary's ident restrictions. +func isIdent(name string) bool { + if len(name) < 1 { + return false + } + + for i := 0; i < len(name); i++ { + chr := name[i] + if (chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9') { + continue // alphanumerics + } + if i == 0 { + return false // first char must be alnum + } + switch chr { + case '[', '\\', ']', '^', '_', '{', '|', '}', '-', '.', '`': + continue // allowed chars + default: + return false // disallowed chars + } + } + + return true +} + // Skeleton produces a canonicalized identifier that tries to catch // homoglyphic / confusable identifiers. It's a tweaked version of the TR39 // skeleton algorithm. We apply the skeleton algorithm first and only then casefold, diff --git a/irc/strings_test.go b/irc/strings_test.go index 6b60a0f0..757722d4 100644 --- a/irc/strings_test.go +++ b/irc/strings_test.go @@ -140,6 +140,22 @@ func TestIsBoring(t *testing.T) { assertBoring("Νικηφόρος", false) } +func TestIsIdent(t *testing.T) { + assertIdent := func(str string, expected bool) { + if isIdent(str) != expected { + t.Errorf("expected [%s] to have identness [%t], but got [%t]", str, expected, !expected) + } + } + + assertIdent("warning", true) + assertIdent("sid3225", true) + assertIdent("dan.oak25", true) + assertIdent("dan.oak[25]", true) + assertIdent("phi@#$%ip", false) + assertIdent("Νικηφόρος", false) + assertIdent("-dan56", false) +} + func TestSkeleton(t *testing.T) { skeleton := func(str string) string { skel, err := Skeleton(str)