Browse Source

fix #693

tags/v2.0.0-rc1
Shivaram Lingamneni 4 years ago
parent
commit
91d6888b7e
5 changed files with 130 additions and 3 deletions
  1. 22
    0
      irc/config.go
  2. 6
    1
      irc/server.go
  3. 68
    2
      irc/strings.go
  4. 22
    0
      irc/strings_test.go
  5. 12
    0
      oragono.yaml

+ 22
- 0
irc/config.go View File

@@ -182,6 +182,27 @@ func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error
182 182
 	return err
183 183
 }
184 184
 
185
+func (cm *Casemapping) UnmarshalYAML(unmarshal func(interface{}) error) (err error) {
186
+	var orig string
187
+	if err = unmarshal(&orig); err != nil {
188
+		return err
189
+	}
190
+
191
+	var result Casemapping
192
+	switch strings.ToLower(orig) {
193
+	case "ascii":
194
+		result = CasemappingASCII
195
+	case "precis", "rfc7613", "rfc8265":
196
+		result = CasemappingPRECIS
197
+	case "permissive", "fun":
198
+		result = CasemappingPermissive
199
+	default:
200
+		return fmt.Errorf("invalid casemapping value: %s", orig)
201
+	}
202
+	*cm = result
203
+	return nil
204
+}
205
+
185 206
 type NickReservationConfig struct {
186 207
 	Enabled                bool
187 208
 	AdditionalNickLimit    int `yaml:"additional-nick-limit"`
@@ -318,6 +339,7 @@ type Config struct {
318 339
 		Cloaks        cloaks.CloakConfig              `yaml:"ip-cloaking"`
319 340
 		supportedCaps *caps.Set
320 341
 		capValues     caps.Values
342
+		Casemapping   Casemapping
321 343
 	}
322 344
 
323 345
 	Languages struct {

+ 6
- 1
irc/server.go View File

@@ -165,7 +165,9 @@ func (config *Config) generateISupport() (err error) {
165 165
 	isupport.Add("STATUSMSG", "~&@%+")
166 166
 	isupport.Add("TARGMAX", fmt.Sprintf("NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:%s,TAGMSG:%s,NOTICE:%s,MONITOR:", maxTargetsString, maxTargetsString, maxTargetsString))
167 167
 	isupport.Add("TOPICLEN", strconv.Itoa(config.Limits.TopicLen))
168
-	isupport.Add("UTF8MAPPING", casemappingName)
168
+	if globalCasemappingSetting == CasemappingPRECIS {
169
+		isupport.Add("UTF8MAPPING", precisUTF8MappingToken)
170
+	}
169 171
 
170 172
 	err = isupport.RegenerateCachedReply()
171 173
 	return
@@ -596,6 +598,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
596 598
 		server.configFilename = config.Filename
597 599
 		server.name = config.Server.Name
598 600
 		server.nameCasefolded = config.Server.nameCasefolded
601
+		globalCasemappingSetting = config.Server.Casemapping
599 602
 	} else {
600 603
 		// enforce configs that can't be changed after launch:
601 604
 		currentLimits := server.Config().Limits
@@ -605,6 +608,8 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
605 608
 			return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
606 609
 		} else if server.Config().Datastore.Path != config.Datastore.Path {
607 610
 			return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
611
+		} else if globalCasemappingSetting != config.Server.Casemapping {
612
+			return fmt.Errorf("Casemapping cannot be changed after launching the server, rehash aborted")
608 613
 		}
609 614
 	}
610 615
 

+ 68
- 2
irc/strings.go View File

@@ -8,18 +8,43 @@ package irc
8 8
 import (
9 9
 	"fmt"
10 10
 	"strings"
11
+	"unicode"
11 12
 
12 13
 	"github.com/oragono/confusables"
13 14
 	"golang.org/x/text/cases"
14 15
 	"golang.org/x/text/language"
15 16
 	"golang.org/x/text/secure/precis"
17
+	"golang.org/x/text/unicode/norm"
16 18
 	"golang.org/x/text/width"
17 19
 )
18 20
 
19 21
 const (
20
-	casemappingName = "rfc8265"
22
+	precisUTF8MappingToken = "rfc8265"
21 23
 )
22 24
 
25
+type Casemapping uint
26
+
27
+const (
28
+	// "precis" is the default / zero value:
29
+	// casefolding/validation: PRECIS + ircd restrictions (like no *)
30
+	// confusables detection: standard skeleton algorithm
31
+	CasemappingPRECIS Casemapping = iota
32
+	// "ascii" is the traditional ircd behavior:
33
+	// casefolding/validation: must be pure ASCII and follow ircd restrictions, ASCII lowercasing
34
+	// confusables detection: none
35
+	CasemappingASCII
36
+	// "permissive" is an insecure mode:
37
+	// casefolding/validation: arbitrary unicodes that follow ircd restrictions, unicode casefolding
38
+	// confusables detection: standard skeleton algorithm (which may be ineffective
39
+	// over the larger set of permitted identifiers)
40
+	CasemappingPermissive
41
+)
42
+
43
+// XXX this is a global variable without explicit synchronization.
44
+// it gets set during the initial Server.applyConfig and cannot be changed by rehash:
45
+// this happens-before all IRC connections and all casefolding operations.
46
+var globalCasemappingSetting Casemapping = CasemappingPRECIS
47
+
23 48
 // Each pass of PRECIS casefolding is a composition of idempotent operations,
24 49
 // but not idempotent itself. Therefore, the spec says "do it four times and hope
25 50
 // it converges" (lolwtf). Golang's PRECIS implementation has a "repeat" option,
@@ -46,7 +71,14 @@ func iterateFolding(profile *precis.Profile, oldStr string) (str string, err err
46 71
 
47 72
 // Casefold returns a casefolded string, without doing any name or channel character checks.
48 73
 func Casefold(str string) (string, error) {
49
-	return iterateFolding(precis.UsernameCaseMapped, str)
74
+	switch globalCasemappingSetting {
75
+	default:
76
+		return iterateFolding(precis.UsernameCaseMapped, str)
77
+	case CasemappingASCII:
78
+		return foldASCII(str)
79
+	case CasemappingPermissive:
80
+		return foldPermissive(str)
81
+	}
50 82
 }
51 83
 
52 84
 // CasefoldChannel returns a casefolded version of a channel name.
@@ -144,6 +176,16 @@ func isIdent(name string) bool {
144 176
 // from the original (unfolded) identifier and stored/tracked separately from the
145 177
 // casefolded identifier.
146 178
 func Skeleton(name string) (string, error) {
179
+	switch globalCasemappingSetting {
180
+	default:
181
+		return realSkeleton(name)
182
+	case CasemappingASCII:
183
+		// identity function is fine because we independently case-normalize in Casefold
184
+		return name, nil
185
+	}
186
+}
187
+
188
+func realSkeleton(name string) (string, error) {
147 189
 	// XXX the confusables table includes some, but not all, fullwidth->standard
148 190
 	// mappings for latin characters. do a pass of explicit width folding,
149 191
 	// same as PRECIS:
@@ -212,3 +254,27 @@ func CanonicalizeMaskWildcard(userhost string) (expanded string, err error) {
212 254
 	}
213 255
 	return fmt.Sprintf("%s!%s@%s", nick, user, host), nil
214 256
 }
257
+
258
+func foldASCII(str string) (result string, err error) {
259
+	if !IsPureASCII(str) {
260
+		return "", errInvalidCharacter
261
+	}
262
+	return strings.ToLower(str), nil
263
+}
264
+
265
+func IsPureASCII(str string) bool {
266
+	for i := 0; i < len(str); i++ {
267
+		if unicode.MaxASCII < str[i] {
268
+			return false
269
+		}
270
+	}
271
+	return true
272
+}
273
+
274
+func foldPermissive(str string) (result string, err error) {
275
+	// YOLO
276
+	str = norm.NFD.String(str)
277
+	str = cases.Fold().String(str)
278
+	str = norm.NFD.String(str)
279
+	return str, nil
280
+}

+ 22
- 0
irc/strings_test.go View File

@@ -215,3 +215,25 @@ func TestCanonicalizeMaskWildcard(t *testing.T) {
215 215
 	tester("Shivaram*", "shivaram*!*@*", nil)
216 216
 	tester("*SHIVARAM*", "*shivaram*!*@*", nil)
217 217
 }
218
+
219
+func TestFoldPermissive(t *testing.T) {
220
+	tester := func(first, second string, equal bool) {
221
+		firstFolded, err := foldPermissive(first)
222
+		if err != nil {
223
+			panic(err)
224
+		}
225
+		secondFolded, err := foldPermissive(second)
226
+		if err != nil {
227
+			panic(err)
228
+		}
229
+		foundEqual := firstFolded == secondFolded
230
+		if foundEqual != equal {
231
+			t.Errorf("%s and %s: expected equality %t, but got %t", first, second, equal, foundEqual)
232
+		}
233
+	}
234
+	tester("SHIVARAM", "shivaram", true)
235
+	tester("shIvaram", "shivaraM", true)
236
+	tester("shivaram", "DAN-", false)
237
+	tester("dolph🐬n", "DOLPH🐬n", true)
238
+	tester("dolph🐬n", "dolph💻n", false)
239
+}

+ 12
- 0
oragono.yaml View File

@@ -85,6 +85,18 @@ server:
85 85
         # should clients include this STS policy when they ship their inbuilt preload lists?
86 86
         preload: false
87 87
 
88
+    # casemapping controls what kinds of strings are permitted as identifiers (nicknames,
89
+    # channel names, account names, etc.), and how they are normalized for case.
90
+    # with the recommended default of 'precis', utf-8 identifiers that are "sane"
91
+    # (according to RFC 8265) are allowed, and the server additionally tries to protect
92
+    # against confusable characters ("homoglyph attacks").
93
+    # the other options are 'ascii' (traditional ASCII-only identifiers), and 'permissive',
94
+    # which allows identifiers to contain unusual characters like emoji, but makes users
95
+    # vulnerable to homoglyph attacks. unless you're really confident in your decision,
96
+    # we recommend leaving this value at its default (changing it once the network is
97
+    # already up and running is problematic).
98
+    casemapping: precis
99
+
88 100
     # whether to look up user hostnames with reverse DNS
89 101
     # (to suppress this for privacy purposes, use the ip-cloaking options below)
90 102
     lookup-hostnames: true

Loading…
Cancel
Save