Permalink
Browse files

Merge pull request #128 from daredevildave/master

Add (optional) authentication step when connecting to share servers
  • Loading branch information...
2 parents 4bd2896 + fdede56 commit cf5f4af49ef3098f18a34dc05dd87936d6a20b52 @daredevildave daredevildave committed Oct 23, 2012
@@ -20,7 +20,7 @@ else
Doc = require('./doc').Doc
class Connection
- constructor: (host) ->
+ constructor: (host, authentication) ->
# Map of docname -> doc
@docs = {}
@@ -37,7 +37,12 @@ class Connection
new SockJS(host)
else
new BCSocket(host, reconnect:true)
-
+
+ # Send authentication message
+ @socket.send({
+ "auth": if authentication then authentication else null
+ })
+
@socket.onmessage = (msg) =>
msg = JSON.parse(msg.data) if useSockJS?
if msg.auth is null
View
@@ -28,15 +28,15 @@ exports.open = do ->
# This is a private connection pool for implicitly created connections.
connections = {}
- getConnection = (origin) ->
+ getConnection = (origin, authentication) ->
if WEB?
location = window.location
# default to browserchannel
path = if useSockJS then 'sockjs' else 'channel'
origin ?= "#{location.protocol}//#{location.host}/#{path}"
unless connections[origin]
- c = new Connection origin
+ c = new Connection origin, authentication
del = -> delete connections[origin]
c.on 'disconnected', del
@@ -55,12 +55,20 @@ exports.open = do ->
if numDocs == 0
c.disconnect()
- (docName, type, origin, callback) ->
- if typeof origin == 'function'
- callback = origin
- origin = null
+ (docName, type, options, callback) ->
+ if typeof options == 'function'
+ callback = options
+ options = {}
- c = getConnection origin
+ if typeof options == 'string'
+ options = {
+ 'origin': options
+ }
+
+ origin = options.origin
+ authentication = options.authentication
+
+ c = getConnection origin, authentication
c.numDocs++
c.open docName, type, (error, doc) ->
if error
View
@@ -26,6 +26,9 @@ hat = require 'hat'
syncQueue = require './syncqueue'
+# Time (in ms) that the server will wait for an auth message from the client before closing the connection
+AUTH_TIMEOUT = 10000
+
# session should implement the following interface:
# headers
# address
@@ -53,6 +56,7 @@ exports.handler = (session, createAgent) ->
# Map from docName -> {queue, listener if open}
docState = {}
+
# We'll only handle one message from each client at a time.
handleMessage = (query) ->
@@ -301,26 +305,42 @@ exports.handler = (session, createAgent) ->
send msg
callback()
+ # Authentication process has failed, send error and stop session
+ failAuthentication = (error) ->
+ session.send {
+ auth: null,
+ error: error
+ }
+ session.stop()
+
+ # Wait for client to send an auth message, but don't wait forever
+ timeout = setTimeout () ->
+ failAuthentication('Timeout waiting for client auth message')
+ , AUTH_TIMEOUT
+
# We don't process any messages from the agent until they've authorized. Instead,
# they are stored in this buffer.
buffer = []
- session.on 'message', bufferMsg = (msg) -> buffer.push msg
-
- createAgent data, (error, agent_) ->
- if error
- # The client is not authorized, so they shouldn't try and reconnect.
- session.send {auth:null, error}
- session.stop()
+ session.on 'message', bufferMsg = (msg) ->
+ if typeof msg.auth != 'undefined'
+ clearTimeout timeout
+ data.authentication = msg.auth
+ createAgent data, (error, agent_) ->
+ if error
+ # The client is not authorized, so they shouldn't try and reconnect.
+ failAuthentication(error)
+ else
+ agent = agent_
+ session.send auth:agent.sessionId
+
+ # Ok. Now we can handle all the messages in the buffer. They'll go straight to
+ # handleMessage from now on.
+ session.removeListener 'message', bufferMsg
+ handleMessage msg for msg in buffer
+ buffer = null
+ session.on 'message', handleMessage
else
- agent = agent_
- session.send auth:agent.sessionId
-
- # Ok. Now we can handle all the messages in the buffer. They'll go straight to
- # handleMessage from now on.
- session.removeListener 'message', bufferMsg
- handleMessage msg for msg in buffer
- buffer = null
- session.on 'message', handleMessage
+ buffer.push msg
session.on 'close', ->
return unless agent
@@ -20,7 +20,8 @@ module.exports = (model, options) ->
@connectTime = new Date
@headers = data.headers
@remoteAddress = data.remoteAddress
-
+ @authentication = data.authentication
+
# This is a map from docName -> listener function
@listeners = {}
View
@@ -59,6 +59,8 @@ module.exports = testCase
callback()
@socket.onerror = (e) -> console.warn 'eeee', e
+
+ @socket.send({auth:null});
@expect = (data, callback) =>
expectData @socket, data, callback
@@ -350,7 +352,7 @@ module.exports = testCase
action.reject()
socket = new BCSocket "http://localhost:#{@server.address().port}/channel"
-
+ socket.send({auth:null})
expectData socket, {auth:null, error:'forbidden'}, ->
socket.onclose = ->
test.expect 7
@@ -404,3 +406,30 @@ module.exports = testCase
@expect {v:null, error:'forbidden'}, ->
test.done()
+ 'Authentication string available in auth function': (test) ->
+ @auth = (agent, action) ->
+ test.strictEqual agent.authentication, '1234'
+ action.reject()
+
+ socket = new BCSocket "http://localhost:#{@server.address().port}/channel"
+ socket.onclose = () ->
+ test.done()
+ socket.send {auth:'1234'}
+
+ 'Authentication object available in auth function': (test) ->
+ @auth = (agent, action) ->
+ test.strictEqual agent.authentication.a, 1234
+ action.reject()
+
+ socket = new BCSocket "http://localhost:#{@server.address().port}/channel"
+ socket.onclose = () ->
+ test.done()
+ socket.send {auth:{a:1234}}
+
+ 'Socket timeout if no auth message is sent': (test) ->
+ socket = new BCSocket "http://localhost:#{@server.address().port}/channel"
+ socket.onmessage = (data) ->
+ test.strictEqual data.auth, null
+ test.strictEqual data.error, 'Timeout waiting for client auth message'
+ socket.onclose = () ->
+ test.done()
View
@@ -94,6 +94,7 @@ module.exports = testCase
callback()
socket.onerror = (e) -> console.warn 'eeee', e
+ socket.send({auth:null});
@expect = (data, callback) =>
expectData socket, data, callback
@@ -388,6 +389,7 @@ module.exports = testCase
action.reject()
new WSSocket "ws://localhost:#{@server.address().port}/sockjs/websocket", (socket) ->
+ socket.send({auth:null});
expectData socket, {auth:null, error:'forbidden'}, ->
socket.onclose = ->
test.expect 7
@@ -440,3 +442,35 @@ module.exports = testCase
@socket.send {doc:@name, v:0, op:{position:0, text:'hi'}, meta:{}}
@expect {v:null, error:'forbidden'}, ->
test.done()
+
+ 'Authentication string available in auth function': (test) ->
+ @auth = (agent, action) ->
+ test.strictEqual agent.authentication, '1234'
+ test.strictEqual action.type, 'connect'
+ action.reject()
+
+ new WSSocket "ws://localhost:#{@server.address().port}/sockjs/websocket", (socket) ->
+ socket.send({auth:'1234'});
+ expectData socket, {auth:null, error:'forbidden'}, ->
+ socket.onclose = ->
+ test.done()
+
+ 'Authentication object available in auth function': (test) ->
+ @auth = (agent, action) ->
+ test.strictEqual agent.authentication.a, 1234
+ test.strictEqual action.type, 'connect'
+ action.reject()
+
+ new WSSocket "ws://localhost:#{@server.address().port}/sockjs/websocket", (socket) ->
+ socket.send({auth:{a:1234}});
+ expectData socket, {auth:null, error:'forbidden'}, ->
+ socket.onclose = ->
+ test.done()
+
+ 'Socket timeout if no auth message is sent': (test) ->
+ new WSSocket "ws://localhost:#{@server.address().port}/sockjs/websocket", (socket) ->
+ socket.onmessage = (data) ->
+ test.strictEqual data.auth, null
+ test.strictEqual data.error, 'Timeout waiting for client auth message'
+ socket.onclose = () ->
+ test.done()
Oops, something went wrong.

0 comments on commit cf5f4af

Please sign in to comment.