From addbdad86661170d0b694724f5382554a601d680 Mon Sep 17 00:00:00 2001 From: Logan Hanks Date: Thu, 29 Jan 2015 12:41:00 -0800 Subject: [PATCH] Add validation and normalization of nicks. --- backend/session.go | 6 ++- client/.gitignore | 1 + client/lib/stores/chat.js | 9 +++++ client/lib/ui/chatentry.js | 2 +- client/test/chat-store-test.js | 21 +++++++++++ proto/identity.go | 26 +++++++++++++ proto/identity_test.go | 69 ++++++++++++++++++++++++++++++++++ 7 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 client/.gitignore create mode 100644 proto/identity_test.go diff --git a/backend/session.go b/backend/session.go index 84cff6f6..f4c9f73a 100644 --- a/backend/session.go +++ b/backend/session.go @@ -215,8 +215,12 @@ func (s *memSession) handleCommand(cmd *proto.Packet) (interface{}, error) { } return &proto.LogReply{Log: msgs, Before: msg.Before}, nil case *proto.NickCommand: + nick, err := proto.NormalizeNick(msg.Name) + if err != nil { + return nil, err + } formerName := s.identity.Name() - s.identity.name = msg.Name + s.identity.name = nick event, err := s.room.RenameUser(s.ctx, s, formerName) if err != nil { return nil, err diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 00000000..378eac25 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1 @@ +build diff --git a/client/lib/stores/chat.js b/client/lib/stores/chat.js index 9a30edb9..6c8a64ee 100644 --- a/client/lib/stores/chat.js +++ b/client/lib/stores/chat.js @@ -48,6 +48,9 @@ module.exports.store = Reflux.createStore({ } else if (ev.body.type == 'who-reply') { this._handleWhoReply(ev.body.data) } else if (ev.body.type == 'nick-reply' || ev.body.type == 'nick-event') { + if (ev.body.type == 'nick-reply') { + this._handleNickReply(ev.body.data) + } this.state.who = this.state.who .mergeIn([ev.body.data.id], { id: ev.body.data.id, @@ -105,6 +108,12 @@ module.exports.store = Reflux.createStore({ ) }, + _handleNickReply: function(data) { + this.state.nick = data.to + this.state.nickText = data.to + storage.setRoom(this.state.roomName, 'nick', data.to) + }, + storageChange: function(data) { var roomStorage = data.room[this.state.roomName] || {} this.state.nick = roomStorage.nick diff --git a/client/lib/ui/chatentry.js b/client/lib/ui/chatentry.js index 9e5add55..c6d35eff 100644 --- a/client/lib/ui/chatentry.js +++ b/client/lib/ui/chatentry.js @@ -169,7 +169,7 @@ module.exports = React.createClass({
- + {this.state.nickText || this.state.nick}
diff --git a/client/test/chat-store-test.js b/client/test/chat-store-test.js index 6088b4d1..ef0c7035 100644 --- a/client/test/chat-store-test.js +++ b/client/test/chat-store-test.js @@ -607,6 +607,14 @@ describe('chat store', function() { }) describe('received nick changes', function() { + beforeEach(function() { + sinon.stub(storage, 'setRoom') + }) + + afterEach(function() { + storage.setRoom.restore() + }) + var nickReply = { 'id': '1', 'type': 'nick-reply', @@ -627,6 +635,19 @@ describe('chat store', function() { } } + it('should update chat and room state', function(done) { + chat.store.state.roomName = 'ezzie' + chat.store.state.nick = '' + chat.store.state.nickText = '' + handleSocket({status: 'receive', body: nickReply}, function(state) { + assert.equal(state.nick, 'tester3') + assert.equal(state.nickText, 'tester3') + sinon.assert.calledOnce(storage.setRoom) + sinon.assert.calledWithExactly(storage.setRoom, 'ezzie', 'nick', 'tester3') + done() + }) + }) + it('should update user list name', function(done) { handleSocket({status: 'receive', body: whoReply}, function() { handleSocket({status: 'receive', body: nickReply}, function(state) { diff --git a/proto/identity.go b/proto/identity.go index bcd1125a..a7875726 100644 --- a/proto/identity.go +++ b/proto/identity.go @@ -1,5 +1,13 @@ package proto +import ( + "fmt" + "strings" + "unicode/utf8" +) + +const MaxNickLength = 36 + // An Identity maps to a global persona. It may exist only in the context // of a single Room. An Identity may be anonymous. type Identity interface { @@ -12,3 +20,21 @@ type IdentityView struct { ID string `json:"id"` Name string `json:"name"` } + +// NormalizeNick validates and normalizes a proposed name from a user. +// If the proposed name is not valid, returns an error. Otherwise, returns +// the normalized form of the name. Normalization for a nick consists of: +// +// 1. Remove leading and trailing whitespace +// 2. Collapse all internal whitespace to single spaces +func NormalizeNick(name string) (string, error) { + name = strings.TrimSpace(name) + if len(name) == 0 { + return "", fmt.Errorf("invalid nick") + } + normalized := strings.Join(strings.Fields(name), " ") + if utf8.RuneCountInString(normalized) > MaxNickLength { + return "", fmt.Errorf("invalid nick") + } + return normalized, nil +} diff --git a/proto/identity_test.go b/proto/identity_test.go new file mode 100644 index 00000000..fa034408 --- /dev/null +++ b/proto/identity_test.go @@ -0,0 +1,69 @@ +package proto + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestNormalizeNick(t *testing.T) { + pass := func(name string) string { + name, err := NormalizeNick(name) + So(err, ShouldBeNil) + return name + } + + reject := func(name string) error { + name, err := NormalizeNick(name) + So(err, ShouldNotBeNil) + So(err.Error(), ShouldEqual, "invalid nick") + So(name, ShouldEqual, "") + return err + } + + Convey("Spaces are stripped", t, func() { + So(pass("test"), ShouldEqual, "test") + So(pass(" test"), ShouldEqual, "test") + So(pass("\r test"), ShouldEqual, "test") + So(pass("test "), ShouldEqual, "test") + So(pass("test\v "), ShouldEqual, "test") + So(pass(" test "), ShouldEqual, "test") + }) + + Convey("Non-spaces are required", t, func() { + reject("") + reject(" ") + reject(" \t\n\v\f ") + }) + + Convey("Internal spaces are collapsed", t, func() { + So(pass("test test"), ShouldEqual, "test test") + So(pass("test \r \n \v test"), ShouldEqual, "test test") + So(pass(" test\ntest test "), ShouldEqual, "test test test") + }) + + Convey("UTF-8 is handled", t, func() { + input := ` + ᕦ( ͡° ͜ʖ ͡°)ᕤ + + + ─=≡Σᕕ( ͡° ͜ʖ ͡°)ᕗ + ` + "\t\r\n:)" + expected := "ᕦ( ͡° ͜ʖ ͡°)ᕤ ─=≡Σᕕ( ͡° ͜ʖ ͡°)ᕗ :)" + So(pass(input), ShouldEqual, expected) + }) + + Convey("Max length is enforced", t, func() { + name := make([]byte, MaxNickLength) + for i := 0; i < MaxNickLength; i++ { + name[i] = 'a' + } + name[1] = ' ' + name[4] = ' ' + name[5] = ' ' + expected := "a aa " + string(name[6:]) + So(pass(string(name)), ShouldEqual, expected) + So(pass(string(name)+"a"), ShouldEqual, expected+"a") + reject(string(name) + "aa") + }) +}