diff --git a/default.yaml b/default.yaml index cbe6960f..3c8f72da 100644 --- a/default.yaml +++ b/default.yaml @@ -134,9 +134,10 @@ server: # the recommended default is 'ascii' (traditional ASCII-only identifiers). # the other options are 'precis', which allows UTF8 identifiers that are "sane" # (according to UFC 8265), with additional mitigations for homoglyph attacks, - # and 'permissive', which allows identifiers containing unusual characters like + # 'permissive', which allows identifiers containing unusual characters like # emoji, at the cost of increased vulnerability to homoglyph attacks and potential - # client compatibility problems. we recommend leaving this value at its default; + # client compatibility problems, and the legacy mappings 'rfc1459' and + # 'rfc1459-strict'. we recommend leaving this value at its default; # however, note that changing it once the network is already up and running is # problematic. casemapping: "ascii" diff --git a/irc/config.go b/irc/config.go index 96fa4ef4..e90d9dca 100644 --- a/irc/config.go +++ b/irc/config.go @@ -453,6 +453,10 @@ func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err err result = CasemappingPRECIS case "permissive", "fun": result = CasemappingPermissive + case "rfc1459": + result = CasemappingRFC1459 + case "rfc1459-strict": + result = CasemappingRFC1459Strict default: return fmt.Errorf("invalid casemapping value: %s", orig) } @@ -1622,7 +1626,16 @@ func (config *Config) generateISupport() (err error) { isupport.Initialize() isupport.Add("AWAYLEN", strconv.Itoa(config.Limits.AwayLen)) isupport.Add("BOT", "B") - isupport.Add("CASEMAPPING", "ascii") + var casemappingToken string + switch config.Server.Casemapping { + default: + casemappingToken = "ascii" // this is published for ascii, precis, or permissive + case CasemappingRFC1459: + casemappingToken = "rfc1459" + case CasemappingRFC1459Strict: + casemappingToken = "rfc1459-strict" + } + isupport.Add("CASEMAPPING", casemappingToken) isupport.Add("CHANLIMIT", fmt.Sprintf("%s:%d", chanTypes, config.Channels.MaxChannelsPerClient)) isupport.Add("CHANMODES", chanmodesToken) if config.History.Enabled && config.History.ChathistoryMax > 0 { diff --git a/irc/strings.go b/irc/strings.go index b67bdac6..fb5c3da0 100644 --- a/irc/strings.go +++ b/irc/strings.go @@ -60,6 +60,10 @@ const ( // confusables detection: standard skeleton algorithm (which may be ineffective // over the larger set of permitted identifiers) CasemappingPermissive + // rfc1459 is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter + CasemappingRFC1459 + // rfc1459-strict is a legacy mapping as defined here: https://modern.ircdocs.horse/#casemapping-parameter + CasemappingRFC1459Strict ) // XXX this is a global variable without explicit synchronization. @@ -110,6 +114,10 @@ func casefoldWithSetting(str string, setting Casemapping) (string, error) { return foldASCII(str) case CasemappingPermissive: return foldPermissive(str) + case CasemappingRFC1459: + return foldRFC1459(str, false) + case CasemappingRFC1459Strict: + return foldRFC1459(str, true) } } @@ -214,7 +222,7 @@ func Skeleton(name string) (string, error) { switch globalCasemappingSetting { default: return realSkeleton(name) - case CasemappingASCII: + case CasemappingASCII, CasemappingRFC1459, CasemappingRFC1459Strict: // identity function is fine because we independently case-normalize in Casefold return name, nil } @@ -302,6 +310,23 @@ func foldASCII(str string) (result string, err error) { return strings.ToLower(str), nil } +var ( + rfc1459Replacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|", "~", "^") + rfc1459StrictReplacer = strings.NewReplacer("[", "{", "]", "}", "\\", "|") +) + +func foldRFC1459(str string, strict bool) (result string, err error) { + asciiFold, err := foldASCII(str) + if err != nil { + return "", err + } + replacer := rfc1459Replacer + if strict { + replacer = rfc1459StrictReplacer + } + return replacer.Replace(asciiFold), nil +} + func IsPrintableASCII(str string) bool { for i := 0; i < len(str); i++ { // allow space here because it's technically printable; diff --git a/irc/strings_test.go b/irc/strings_test.go index 4ee5d1a9..8c9d11f5 100644 --- a/irc/strings_test.go +++ b/irc/strings_test.go @@ -279,3 +279,31 @@ func TestFoldASCIIInvalid(t *testing.T) { t.Errorf("control characters should be invalid in identifiers") } } + +func TestFoldRFC1459(t *testing.T) { + folder := func(str string) (string, error) { + return foldRFC1459(str, false) + } + tester := func(first, second string, equal bool) { + validFoldTester(first, second, equal, folder, t) + } + tester("shivaram", "SHIVARAM", true) + tester("shivaram[a]", "shivaram{a}", true) + tester("shivaram\\a]", "shivaram{a}", false) + tester("shivaram\\a]", "shivaram|a}", true) + tester("shivaram~a]", "shivaram^a}", true) +} + +func TestFoldRFC1459Strict(t *testing.T) { + folder := func(str string) (string, error) { + return foldRFC1459(str, true) + } + tester := func(first, second string, equal bool) { + validFoldTester(first, second, equal, folder, t) + } + tester("shivaram", "SHIVARAM", true) + tester("shivaram[a]", "shivaram{a}", true) + tester("shivaram\\a]", "shivaram{a}", false) + tester("shivaram\\a]", "shivaram|a}", true) + tester("shivaram~a]", "shivaram^a}", false) +} diff --git a/traditional.yaml b/traditional.yaml index d26d36e9..d10b4e55 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -108,9 +108,10 @@ server: # the recommended default is 'ascii' (traditional ASCII-only identifiers). # the other options are 'precis', which allows UTF8 identifiers that are "sane" # (according to UFC 8265), with additional mitigations for homoglyph attacks, - # and 'permissive', which allows identifiers containing unusual characters like + # 'permissive', which allows identifiers containing unusual characters like # emoji, at the cost of increased vulnerability to homoglyph attacks and potential - # client compatibility problems. we recommend leaving this value at its default; + # client compatibility problems, and the legacy mappings 'rfc1459' and + # 'rfc1459-strict'. we recommend leaving this value at its default; # however, note that changing it once the network is already up and running is # problematic. casemapping: "ascii"