useragent.coffee 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. # A useragent is assigned to each client when the client connects. The useragent is responsible for making
  2. # sure all requests are authorized and maintaining document metadata.
  3. #
  4. # This is used by all the client frontends to interact with the server.
  5. hat = require 'hat'
  6. types = require '../types'
  7. # This module exports a function which you can call with the model and options. Calling the function
  8. # returns _another_ function which you can call when clients connect.
  9. module.exports = (model, options) ->
  10. # By default, accept all connections + data submissions.
  11. # Don't let anyone delete documents though.
  12. auth = options.auth or (agent, action) ->
  13. if action.type in ['connect', 'read', 'create', 'update'] then action.accept() else action.reject()
  14. class UserAgent
  15. constructor: (data) ->
  16. @sessionId = hat()
  17. @connectTime = new Date
  18. @headers = data.headers
  19. @remoteAddress = data.remoteAddress
  20. # This is a map from docName -> listener function
  21. @listeners = {}
  22. # Should be manually set by the auth function.
  23. @name = null
  24. # We have access to these with socket.io, but I'm not sure we can support
  25. # these properties on the REST API or sockjs, etc.
  26. #xdomain: data.xdomain
  27. #secure: data.secure
  28. # This is a helper method which wraps auth() above. It creates the action and calls
  29. # auth. If authentication succeeds, acceptCallback() is called if it exists. otherwise
  30. # userCallback(true) is called.
  31. #
  32. # If authentication fails, userCallback('forbidden', null) is called.
  33. #
  34. # If supplied, actionData is turned into the action object passed to auth.
  35. doAuth: (actionData, name, userCallback, acceptCallback) ->
  36. action = actionData || {}
  37. action.name = name
  38. action.type = switch name
  39. when 'connect' then 'connect'
  40. when 'create' then 'create'
  41. when 'get snapshot', 'get ops', 'open' then 'read'
  42. when 'submit op' then 'update'
  43. when 'submit meta' then 'update'
  44. when 'delete' then 'delete'
  45. else throw new Error "Invalid action name #{name}"
  46. responded = false
  47. action.reject = ->
  48. throw new Error 'Multiple accept/reject calls made' if responded
  49. responded = true
  50. userCallback 'forbidden', null
  51. action.accept = ->
  52. throw new Error 'Multiple accept/reject calls made' if responded
  53. responded = true
  54. acceptCallback()
  55. auth this, action
  56. disconnect: ->
  57. model.removeListener docName, listener for docName, listener of @listeners
  58. getOps: (docName, start, end, callback) ->
  59. @doAuth {docName, start, end}, 'get ops', callback, ->
  60. model.getOps docName, start, end, callback
  61. getSnapshot: (docName, callback) ->
  62. @doAuth {docName}, 'get snapshot', callback, ->
  63. model.getSnapshot docName, callback
  64. create: (docName, type, meta, callback) ->
  65. # We don't check that types[type.name] == type. That might be important at some point.
  66. type = types[type] if typeof type == 'string'
  67. # I'm not sure what client-specified metadata should be allowed in the document metadata
  68. # object. For now, I'm going to ignore all create metadata until I know how it should work.
  69. meta = {}
  70. meta.creator = @name if @name
  71. meta.ctime = meta.mtime = Date.now()
  72. # The action object has a 'type' property already. Hence the doc type is renamed to 'docType'
  73. @doAuth {docName, docType:type, meta}, 'create', callback, =>
  74. model.create docName, type, meta, callback
  75. submitOp: (docName, opData, callback) ->
  76. opData.meta ||= {}
  77. opData.meta.source = @sessionId
  78. dupIfSource = opData.dupIfSource or []
  79. # If ops and meta get coalesced, they should be separated here.
  80. if opData.op
  81. @doAuth {docName, op:opData.op, v:opData.v, meta:opData.meta, dupIfSource}, 'submit op', callback, =>
  82. model.applyOp docName, opData, callback
  83. else
  84. @doAuth {docName, meta:opData.meta}, 'submit meta', callback, =>
  85. model.applyMetaOp docName, opData, callback
  86. # Delete the named operation.
  87. # Callback is passed (deleted?, error message)
  88. delete: (docName, callback) ->
  89. @doAuth {docName}, 'delete', callback, =>
  90. model.delete docName, callback
  91. # Open the named document for reading. Just like model.listen, version is optional.
  92. listen: (docName, version, listener, callback) ->
  93. authOps = if version?
  94. # If the specified version is older than the current version, we have to also check that the
  95. # agent is allowed to get ops from the specified version.
  96. #
  97. # We _could_ check the version number of the document and then only check getOps if
  98. # the specified version is old, but an auth check is almost certainly faster than a db roundtrip.
  99. (c) => @doAuth {docName, start:version, end:null}, 'get ops', callback, c
  100. else
  101. (c) -> c()
  102. authOps =>
  103. @doAuth {docName, v:version if version?}, 'open', callback, =>
  104. return callback? 'Document is already open' if @listeners[docName]
  105. @listeners[docName] = listener
  106. model.listen docName, version, listener, (error, v) =>
  107. if error
  108. delete @listeners[docName]
  109. callback? error, v
  110. removeListener: (docName) ->
  111. throw new Error 'Document is not open' unless @listeners[docName]
  112. model.removeListener docName, @listeners[docName]
  113. delete @listeners[docName]
  114. # Finally, return a function which takes client data and returns an authenticated useragent object
  115. # through a callback.
  116. (data, callback) ->
  117. agent = new UserAgent data
  118. agent.doAuth null, 'connect', callback, ->
  119. # Maybe store a set of agents? Is that useful?
  120. callback null, agent