diff --git a/internal/web/service/inbound_node_claims.go b/internal/web/service/inbound_node_claims.go index 2ade4c32b..82d7064f4 100644 --- a/internal/web/service/inbound_node_claims.go +++ b/internal/web/service/inbound_node_claims.go @@ -84,14 +84,15 @@ func loadNodeClientClaims(tx *gorm.DB, nodeID int, snap *runtime.TrafficSnapshot return claims, nil } - lowered := make([]string, 0, len(subIDByEmail)) + reported := make([]string, 0, len(subIDByEmail)) for email := range subIDByEmail { - lowered = append(lowered, strings.ToLower(email)) + reported = append(reported, email) } + // SQLite's LOWER folds ASCII only, so a non-ASCII capital is matched by its spellings too. var records []model.ClientRecord - for _, batch := range chunkStrings(uniqueNonEmptyStrings(lowered), sqlInChunk) { + for _, batch := range chunkStrings(emailSpellings(reported), sqlInChunk) { var rows []model.ClientRecord - if err := tx.Where("LOWER(email) IN ?", batch).Find(&rows).Error; err != nil { + if err := tx.Where("LOWER(email) IN ? OR email IN ?", batch, batch).Find(&rows).Error; err != nil { return nil, err } records = append(records, rows...) diff --git a/internal/web/service/node_foreign_client_test.go b/internal/web/service/node_foreign_client_test.go index 43605617f..d581969b2 100644 --- a/internal/web/service/node_foreign_client_test.go +++ b/internal/web/service/node_foreign_client_test.go @@ -439,3 +439,23 @@ func TestNodeOversizeInboundIsNotTakenIn(t *testing.T) { t.Fatal("an inbound with an oversized remark was adopted") } } + +// SQLite's LOWER folds ASCII only, so a lookup by lowered email missed a stored non-ASCII +// capital and the node's claim on that master-only client went through untouched. +func TestNodeCannotClaimNonASCIICapitalEmail(t *testing.T) { + db := initTrafficTestDB(t) + svc := &InboundService{} + seedNodeRow(t, db, &model.Node{Id: 1, Name: "n1", Address: "127.0.0.1", Port: 2096, ApiToken: "tok", Enable: true}) + seedMasterInbound(t, db, "master-in", 40001, `{"email":"Иван@x","id":"11111111-1111-4111-8111-111111111111","subId":"ivan-sub","enable":true}`) + createNodeInbound(t, db, 1, "n1-in", 41001) + + syncNodeWithSettings(t, svc, 1, "n1-in", + `{"clients":[{"email":"Иван@x","subId":"picked-by-node","enable":true},{"email":"иван@x","subId":"lower","enable":true}]}`) + + if row := readClientRecord(t, db, "Иван@x"); row.SubID != "ivan-sub" { + t.Fatalf("node rewrote a master-only non-ASCII client: subId=%q", row.SubID) + } + if n := countClientsWhere(t, db, "email = ?", "иван@x"); n != 0 { + t.Fatal("a case variant of a non-ASCII email created a look-alike record") + } +}