Skip to content

Commit

Permalink
Made server headers configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
josephg committed Mar 6, 2012
1 parent becb0cf commit bfc517f
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 13 deletions.
41 changes: 29 additions & 12 deletions lib/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,20 @@ defaultOptions =
# session completely. This will happen whenever a user closes their browser.
sessionTimeoutInterval: 30 * 1000

# By default, browsers don't allow access via javascript to foreign sites. You can use the
# cors: option to set the Access-Control-Allow-Origin header in responses, which tells browsers
# whether or not to allow cross domain requests to be sent.
#
# See https://developer.mozilla.org/en/http_access_control for more information.
#
# Setting cors:'*' will enable javascript from any domain to access your application. BE CAREFUL!
# If your application uses cookies to manage user sessions, javascript on a foreign site could
# make requests as if it were acting on behalf of one of your users.
cors: null

# A user can override all the headers if they want by setting the headers option to an object.
headers: null

# All server responses set some standard HTTP headers.
# To be honest, I don't know how many of these are necessary. I just copied
# them from google.
Expand All @@ -81,7 +95,6 @@ standardHeaders =
'Pragma': 'no-cache'
'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT'
'X-Content-Type-Options': 'nosniff'
'Access-Control-Allow-Origin': '*'

# Gmail also sends this, though I'm not really sure what it does...
# 'X-Xss-Protection': '1; mode=block'
Expand Down Expand Up @@ -115,7 +128,7 @@ a9fe92fedacffff48092ee693af\n"
# - The first *bind* connection a client makes. The server sends arrays there, but the
# connection is a POST and it returns immediately. So that request happens using XHR/Trident
# like regular forward channel requests.
messagingMethods = (query, res) ->
messagingMethods = (options, query, res) ->
type = query.TYPE
if type == 'html'
junkSent = false
Expand Down Expand Up @@ -163,12 +176,12 @@ messagingMethods = (query, res) ->

else
# For normal XHR requests, we send data normally.
writeHead: -> res.writeHead 200, 'OK', standardHeaders
writeHead: -> res.writeHead 200, 'OK', options.headers
write: (data) -> res.write "#{data.length}\n#{data}"
writeRaw: (data) -> res.write data
end: -> res.end()
writeError: (statusCode, message) ->
res.writeHead statusCode, standardHeaders
res.writeHead statusCode, options.headers
res.end message

# For telling the client its done bad.
Expand Down Expand Up @@ -359,6 +372,10 @@ module.exports = browserChannel = (options, onConnect) ->
options ||= {}
options[option] ?= value for option, value of defaultOptions

options.headers = {} unless options.headers
options.headers[h] ||= v for h, v of standardHeaders
options.headers['Access-Control-Allow-Origin'] = options.cors if options.cors

# Strip off a trailing slash in base.
base = options.base
base = base[... base.length - 1] if base.match /\/$/
Expand Down Expand Up @@ -475,7 +492,7 @@ module.exports = browserChannel = (options, onConnect) ->

backChannel =
res: res
methods: messagingMethods query, res
methods: messagingMethods options, query, res
chunk: query.CI == '0'
bytesSent: 0
listener: ->
Expand Down Expand Up @@ -808,7 +825,7 @@ module.exports = browserChannel = (options, onConnect) ->
# If base is /foo, we don't match /foobar. (Currently no unit tests for this)
return next() if pathname.substring(0, base.length + 1) != "#{base}/"

{writeHead, write, writeRaw, end, writeError} = messagingMethods query, res
{writeHead, write, writeRaw, end, writeError} = messagingMethods options, query, res

# # Serving the client
#
Expand Down Expand Up @@ -865,7 +882,7 @@ module.exports = browserChannel = (options, onConnect) ->
# It might be easier to put these headers in the response body or increment the
# version, but that might conflict with future browserchannel versions.
headers = {}
headers[k] = v for k, v of standardHeaders
headers[k] = v for k, v of options.headers
headers['X-Accept'] = 'application/json; application/x-www-form-urlencoded'

# This is a straight-up normal HTTP request like the forward channel requests.
Expand Down Expand Up @@ -938,7 +955,7 @@ module.exports = browserChannel = (options, onConnect) ->
# initial data (session id, etc). This connection is a little bit special - it is always
# encoded using length-prefixed json encoding and it is closed as soon as the first chunk is
# sent.
res.writeHead 200, 'OK', standardHeaders
res.writeHead 200, 'OK', options.headers
session._setBackChannel res, CI:1, TYPE:'xmlhttp', RID:'rpc'
session.flush()
else if session.state is 'closed'
Expand All @@ -950,7 +967,7 @@ module.exports = browserChannel = (options, onConnect) ->
# if our backchannel is still live and telling it how many unconfirmed
# arrays we have.
response = JSON.stringify session._backChannelStatus()
res.writeHead 200, 'OK', standardHeaders
res.writeHead 200, 'OK', options.headers
res.end "#{response.length}\n#{response}"

if req.body
Expand Down Expand Up @@ -979,17 +996,17 @@ module.exports = browserChannel = (options, onConnect) ->
#
# The client implements this using an img= appended to the page.
session?._disconnectAt query.RID
res.writeHead 200, 'OK', standardHeaders
res.writeHead 200, 'OK', options.headers
res.end()

else
res.writeHead 405, 'Method Not Allowed', standardHeaders
res.writeHead 405, 'Method Not Allowed', options.headers
res.end "Method not allowed"

else
# We'll 404 the user instead of letting another handler take care of it.
# Users shouldn't be using the specified URL prefix for anything else.
res.writeHead 404, 'Not Found', standardHeaders
res.writeHead 404, 'Not Found', options.headers
res.end "Not found"

middleware.close = -> session.close() for id, session of sessions
Expand Down
44 changes: 43 additions & 1 deletion test/server.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ module.exports = testCase
'Pragma': 'no-cache'
'Expires': 'Fri, 01 Jan 1990 00:00:00 GMT'
'X-Content-Type-Options': 'nosniff'
'Access-Control-Allow-Origin': '*'

# I'll add a couple helper methods for tests to easily message the server.
@get = (path, callback) =>
Expand Down Expand Up @@ -304,6 +303,49 @@ module.exports = testCase
test.strictEqual data, '[null,null]'
server.close()
test.done()

# You can control the CORS header ('Access-Control-Allow-Origin') using options.cors.
'CORS header is not sent if its not set in the options': (test) ->
@get '/channel/test?VER=8&MODE=init', (response) ->
test.strictEqual response.headers['access-control-allow-origin'], undefined
test.done()

'CORS header is sent during the initial phase if its set in the options': (test) ->
createServer cors:'foo.com', (->), (server, port) ->
http.get {path:'/channel/test?VER=8&MODE=init', host: 'localhost', port: port}, (response) ->
test.strictEqual response.headers['access-control-allow-origin'], 'foo.com'
server.close()
test.done()

'CORS header is set on the backchannel response': (test) ->
server = port = null

sessionCreated = (session) ->
# Make the backchannel flush as soon as its opened
session.send "flush"

req = http.get {path:"/channel/bind?VER=8&RID=rpc&SID=#{session.id}&AID=0&TYPE=xmlhttp&CI=0", host:'localhost', port:port}, (res) =>
test.strictEqual res.headers['access-control-allow-origin'], 'foo.com'
req.abort()
server.close()
test.done()

createServer cors:'foo.com', sessionCreated, (_server, _port) ->
[server, port] = [_server, _port]

req = http.request {method:'POST', path:'/channel/bind?VER=8&RID=1000&t=1', host:'localhost', port:port}, (res) =>
req.end 'count=0'

'Additional headers can be specified in the options': (test) ->
createServer headers:{'X-Foo':'bar'}, (->), (server, port) ->
http.get {path:'/channel/test?VER=8&MODE=init', host: 'localhost', port: port}, (response) ->
test.strictEqual response.headers['x-foo'], 'bar'
server.close()
test.done()

# Interestingly, the CORS header isn't required for old IE (type=html) requests because they're loaded using
# iframes anyway. (Though this should really be tested).


# node-browserchannel is only responsible for URLs with the specified (or default) prefix. If a request
# comes in for a URL outside of that path, it should be passed along to subsequent connect middleware.
Expand Down

0 comments on commit bfc517f

Please sign in to comment.