Skip to content

Commit

Permalink
Add validation and normalization of nicks.
Browse files Browse the repository at this point in the history
  • Loading branch information
logan authored and chromakode committed Apr 10, 2015
1 parent 42011d4 commit addbdad
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 2 deletions.
6 changes: 5 additions & 1 deletion backend/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
9 changes: 9 additions & 0 deletions client/lib/stores/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion client/lib/ui/chatentry.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ module.exports = React.createClass({
<form className="entry">
<div className="nick-box">
<div className="auto-size-container">
<input className="nick" ref="nick" defaultValue={this.state.nick} onBlur={this.setNick} onChange={this.previewNick} />
<input className="nick" ref="nick" value={this.state.nickText} onBlur={this.setNick} onChange={this.previewNick} />
<span className="nick">{this.state.nickText || this.state.nick}</span>
</div>
</div>
Expand Down
21 changes: 21 additions & 0 deletions client/test/chat-store-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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) {
Expand Down
26 changes: 26 additions & 0 deletions proto/identity.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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
}
69 changes: 69 additions & 0 deletions proto/identity_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}

0 comments on commit addbdad

Please sign in to comment.