PageRenderTime 48ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/resource.lisp

http://github.com/3b/clws
Lisp | 255 lines | 197 code | 34 blank | 24 comment | 13 complexity | 5b114af08b69c994f987f4c308daad45 MD5 | raw file
  1. (in-package #:ws)
  2. ;;; resource stuff
  3. ;;;
  4. ;;; name ("/foo", etc)
  5. ;;;
  6. ;;; accept function
  7. ;;; args = resource name, headers, client host/port
  8. ;;; return
  9. ;;; reject connection
  10. ;;; abort connection?
  11. ;;; ? for accepted
  12. ;; fixme: make this per-server, so we can run different servers on
  13. ;; different ports?
  14. ;; fixme: add support for more complex matching than just exact match
  15. (defparameter *resources* (make-hash-table :test 'equal)
  16. "hash mapping resource name to (list of handler instance, origin
  17. validation function, ?)")
  18. (defun register-global-resource (name resource-handler origin-validation-fn)
  19. "Registers a resource instance where NAME is a path string like
  20. '/swank', resource-handler is an instance of WS-RESOURCE, and
  21. ORIGIN-VALIDATION-FN is a function that takes an origin string as
  22. input and returns T if that origin is allowed to access this
  23. resource."
  24. (setf (gethash name *resources*)
  25. (list resource-handler origin-validation-fn)))
  26. (defun find-global-resource (name)
  27. "Returns the resource registered via REGISTER-GLOBAL-RESOURCE with name NAME."
  28. (first (gethash name *resources*)))
  29. (defun unregister-global-resource (name)
  30. "Removes the resource registered via REGISTER-GLOBAL-RESOURCE with name NAME."
  31. (remhash name *resources*))
  32. (defun valid-resource-p (server resource)
  33. "Returns non-nil if there is a handler registered for the resource
  34. of the given name (a string)."
  35. (declare (type string resource)
  36. (ignore server))
  37. (when resource
  38. (gethash resource *resources*)))
  39. ;; functions for checking origins...
  40. (defun any-origin (o) (declare (ignore o)) t)
  41. (defun origin-prefix (&rest prefixes)
  42. "Returns a function that checks whether a given path matches any of
  43. the prefixes passed as arguments."
  44. (lambda (o)
  45. (loop :for p :in prefixes
  46. :for m = (mismatch o p)
  47. :when (or (not m) (= m (length p)))
  48. :return t)))
  49. (defun origin-exact (&rest origins)
  50. "Returns a function that checks whether a given path matches any of
  51. the origins passed as arguments exactly."
  52. ;; fixme: probably should use something better than a linear search
  53. (lambda (o)
  54. (member o origins :test #'string=)))
  55. (defgeneric resource-read-queue (resource)
  56. (:documentation "The concurrent mailbox used to pass messages
  57. between the server thread and resource thread."))
  58. (defclass ws-resource ()
  59. ((read-queue :initform (make-mailbox) :reader resource-read-queue))
  60. (:documentation "A server may have many resources, each associated
  61. with a particular resource path (like /echo or /chat). An single
  62. instance of a resource handles all requests on the server for that
  63. particular url, with the help of RUN-RESOURCE-LISTENER,
  64. RESOURCE-RECEIVED-FRAME, and RESOURCE-CLIENT-DISCONNECTED."))
  65. (defgeneric resource-accept-connection (res resource-name headers client)
  66. (:documentation "Decides whether to accept a connection and returns
  67. values to process the connection further. Defaults to accepting all
  68. connections and using the default mailbox and origin, so most resources
  69. shouldn't need to define a method.
  70. Passed values
  71. - RES is the instance of ws-resource
  72. - RESOURCE-NAME is the resource name requested by the client (string)
  73. - HEADERS is the hash table of headers from the client
  74. - client is the instance of client
  75. Returns values
  76. 1. NIL if the connection should be rejected, or non-nil otherwise
  77. 2. Concurrent mailbox in which to place messages received from the
  78. client, or NIL for default
  79. 3. origin from which to claim this resource is responding, or NIL
  80. for default.
  81. 4. handshake-resource or NIL for default
  82. 5. protocol or NIL for default
  83. Most of the time this function will just return true for the first
  84. value to accept the connection, and nil for the other values.
  85. Note that the connection is not fully established yet, so this
  86. function should not try to send anything to the client, see
  87. resource-client-connected for that.
  88. This function may be called from a different thread than most resource
  89. functions, so methods should be careful about accessing shared data, and
  90. should avoid blocking for extended periods.
  91. "))
  92. (defgeneric resource-client-disconnected (resource client)
  93. (:documentation "Called when a client disconnected from a WebSockets resource."))
  94. (defgeneric resource-client-connected (resource client)
  95. (:documentation "Called when a client finishes connecting to a
  96. WebSockets resource, and data can be sent to the client.
  97. Methods can return :reject to immediately close the connection and
  98. ignore any already received data from this client."))
  99. #++
  100. (defgeneric resource-received-frame (resource client message)
  101. ;;; not used for the moment, since newer ws spec combine 'frame's into
  102. ;;; 'message's, which might be binary or text...
  103. ;;; may add this back later as an interface to processing per frame
  104. ;;; instead of per message?
  105. (:documentation "Called when a client sent a frame to a WebSockets resource."))
  106. (defgeneric resource-received-text (resource client message)
  107. (:documentation "Called when a client sent a text message to a WebSockets resource."))
  108. (defgeneric resource-received-binary (resource client message)
  109. (:documentation "Called when a client sent a binary message to a WebSockets resource."))
  110. (defgeneric resource-received-custom-message (resource message)
  111. (:documentation "Called on the resource listener thread when a
  112. client is passed an arbitrary message via
  113. SEND-CUSTOM-MESSAGE-TO-RESOURCE. "))
  114. (defgeneric send-custom-message-to-resource (resource message)
  115. (:documentation "Thread-safe way to pass a message to the resource
  116. listener. Any message passed with this function will result in
  117. RESOURCE-RECEIVED-CUSTOM-MESSAGE being called on the resource thread
  118. with the second argument of this function."))
  119. (defmethod resource-accept-connection (res resource-name headers client)
  120. (declare (ignore res resource-name headers client))
  121. t)
  122. (defmethod resource-client-connected (res client)
  123. (declare (ignore res client))
  124. nil)
  125. (defmethod send-custom-message-to-resource (resource message)
  126. (mailbox-send-message (resource-read-queue resource)
  127. (list message :custom)))
  128. (defclass funcall-custom-message ()
  129. ((function :initarg :function :initform nil :reader message-function))
  130. (:documentation "A type of so-called 'custom message' used to call a
  131. function on the main resource thread."))
  132. (defmethod resource-received-custom-message (resource (message funcall-custom-message))
  133. (declare (ignore resource))
  134. (funcall (message-function message)))
  135. (defgeneric call-on-resource-thread (resource fn)
  136. (:documentation "Funcalls FN on the resource thread of RESOURCE."))
  137. (defmethod call-on-resource-thread (resource fn)
  138. (send-custom-message-to-resource
  139. resource (make-instance 'funcall-custom-message :function fn)))
  140. (defun disconnect-client (client)
  141. (when (client-resource client)
  142. (resource-client-disconnected (client-resource client) client)
  143. (setf (client-resource client) nil)))
  144. (defun run-resource-listener (resource)
  145. "Runs a resource listener in its own thread indefinitely, calling
  146. RESOURCE-CLIENT-DISCONNECTED and RESOURCE-RECEIVED-FRAME as appropriate."
  147. (macrolet
  148. ((restarts (&body body)
  149. `(handler-bind
  150. ((error
  151. (lambda (c)
  152. (cond
  153. (*debug-on-resource-errors*
  154. (invoke-debugger c))
  155. (t
  156. (lg "resource handler error ~s, dropping client~%" c)
  157. (invoke-restart 'drop-client))))))
  158. (restart-case
  159. (progn ,@body)
  160. (drop-client ()
  161. (unless (client-connection-rejected client)
  162. (ignore-errors (disconnect-client client)))
  163. ;; none of the defined status codes in draft 14 seem right for
  164. ;; 'server error'
  165. (ignore-errors (write-to-client-close client :code nil))
  166. (setf (client-connection-rejected client) t))
  167. (drop-message () #|| do nothing ||#)))))
  168. (loop :for (client data) = (mailbox-receive-message (slot-value resource 'read-queue))
  169. ;; fixme should probably call some generic function with all
  170. ;; the remaining messages
  171. :while (not (eql data :close-resource))
  172. :do
  173. (cond
  174. ((eql data :custom)
  175. ;; here we use the client place to store the custom message
  176. (handler-bind
  177. ((error
  178. (lambda (c)
  179. (cond
  180. (*debug-on-resource-errors*
  181. (invoke-debugger c))
  182. (t
  183. (lg "resource handler error ~s in custom, ignoring~%" c)
  184. (invoke-restart 'continue))))))
  185. (let ((message client))
  186. (restart-case
  187. (resource-received-custom-message resource message)
  188. (continue () :report "Continue" )))))
  189. ((and client (client-connection-rejected client))
  190. #|| ignore any further queued data from this client ||#)
  191. ((eql data :connect)
  192. (restarts
  193. (when (eq :reject (resource-client-connected resource client))
  194. (setf (client-connection-rejected client) t)
  195. (write-to-client-close client))))
  196. ((eql data :eof)
  197. (restarts
  198. (disconnect-client client))
  199. (write-to-client-close client))
  200. ((eql data :dropped)
  201. (restarts
  202. (disconnect-client client))
  203. (write-to-client-close client))
  204. ((eql data :close-resource)
  205. (restarts
  206. (disconnect-client client)))
  207. ((eql data :flow-control)
  208. (%write-to-client client :enable-read))
  209. ((symbolp data)
  210. (error "Unknown symbol in read-queue of resource: ~S " data))
  211. ((consp data)
  212. (restarts
  213. (if (eq (car data) :text)
  214. (resource-received-text resource client (cadr data))
  215. (resource-received-binary resource client (cadr data)))))
  216. (t
  217. (error "got unknown data in run-resource-listener?"))))))
  218. (defun kill-resource-listener (resource)
  219. "Terminates a RUN-RESOURCE-LISTENER from another thread."
  220. (mailbox-send-message (resource-read-queue resource)
  221. '(nil :close-resource)))