/src/session.lisp

http://github.com/mtravers/wuwei · Lisp · 385 lines · 216 code · 86 blank · 83 comment · 0 complexity · 51f05a12ed04b386945dcc7d2d678cde MD5 · raw file

  1. (in-package :wu)
  2. ;;; +=========================================================================+
  3. ;;; | Copyright (c) 2009, 2010 Mike Travers and CollabRx, Inc |
  4. ;;; | |
  5. ;;; | Released under the MIT Open Source License |
  6. ;;; | http://www.opensource.org/licenses/mit-license.php |
  7. ;;; | |
  8. ;;; | Permission is hereby granted, free of charge, to any person obtaining |
  9. ;;; | a copy of this software and associated documentation files (the |
  10. ;;; | "Software"), to deal in the Software without restriction, including |
  11. ;;; | without limitation the rights to use, copy, modify, merge, publish, |
  12. ;;; | distribute, sublicense, and/or sell copies of the Software, and to |
  13. ;;; | permit persons to whom the Software is furnished to do so, subject to |
  14. ;;; | the following conditions: |
  15. ;;; | |
  16. ;;; | The above copyright notice and this permission notice shall be included |
  17. ;;; | in all copies or substantial portions of the Software. |
  18. ;;; | |
  19. ;;; | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
  20. ;;; | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
  21. ;;; | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
  22. ;;; | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY |
  23. ;;; | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, |
  24. ;;; | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE |
  25. ;;; | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
  26. ;;; +=========================================================================+
  27. ;;; Author: Mike Travers
  28. ;;; Session management
  29. (export '(cookie-value
  30. with-session with-session-response def-session-variable delete-session new-session-hook
  31. *aserve-request*))
  32. ;;: Variables and parameters
  33. ;;; Bound by session handler to the session name (a keyword)
  34. (defvar *session* nil)
  35. ;;; Dynamic bound to current request, makes life much easier
  36. ;;; Bound in with-session, but should be universal
  37. ;;; There is an aserve variable, but not exported so not a good idea to use: net.aserve::*worker-request*
  38. (defvar *aserve-request* nil)
  39. ;;; +++ document...and hook up or delete
  40. ;;; (defparameter *default-login-handler* nil)
  41. ;;; Session store
  42. #|
  43. Theory:
  44. - there are multiple session stores, each represented by a session store object
  45. - each handles a certain set of variables,
  46. - each is indexed by the same session id
  47. - variables are CLOS objects that point to a symbol and have additional info about reading/writing
  48. |#
  49. ;;;; :::::::::::::::::::::::::::::::: Utilities
  50. ;;; Signatures
  51. (defun string-signature (string &optional (secret *session-secret*))
  52. #+ALLEGRO (let ((*print-base* 36)) (princ-to-string (excl:hmac-sha1-string secret string)))
  53. #-ALLEGRO (hmac-sha1-string string secret))
  54. #-ALLEGRO
  55. (defun hmac-sha1-string (string &optional (secret *session-secret*) (return :hex))
  56. (let ((hmac (ironclad:make-hmac (ironclad:ascii-string-to-byte-array secret) :sha1)))
  57. (ironclad:update-hmac hmac (ironclad:ascii-string-to-byte-array string))
  58. (ecase return
  59. (:hex (ironclad:byte-array-to-hex-string (ironclad:hmac-digest hmac)))
  60. (:bytes (ironclad:hmac-digest hmac)))))
  61. ;;; Value is a list, gets written out as | separated values with the signature added
  62. (defun signed-value (v &optional (secret *session-secret*))
  63. (let* ((base-string (format nil "~{~A|~}" v))
  64. (sig (string-signature (string+ base-string secret))))
  65. (format nil "~A~A" base-string sig)))
  66. ;; Return value is a list of strings, or NIL if it doesn't verify
  67. (defun verify-signed-value (rv &optional (secret *session-secret*))
  68. (when rv
  69. (ignore-errors ;if error, just don't verify
  70. (let* ((split-pos (1+ (position #\| rv :from-end t)))
  71. (content (subseq rv 0 split-pos))
  72. (sig (subseq rv split-pos)))
  73. (when (equal sig
  74. (string-signature (string+ content secret)))
  75. (butlast #+ALLEGRO (excl:split-re "\\|" content)
  76. #-ALLEGRO (mt:string-split content #\|)
  77. ))))))
  78. ;;;; :::::::::::::::::::::::::::::::: Session Stores
  79. (defvar *session-stores* nil)
  80. (defclass session-store ()
  81. ((variables :initform nil :reader session-variables)
  82. ))
  83. (defmethod initialize-instance :after ((store session-store) &rest ignore)
  84. (declare (ignore ignore))
  85. (push store *session-stores*))
  86. ;;; dev only
  87. (defun reset-session-stores ()
  88. (setf *session-stores* nil))
  89. ;;; Assumes there will be at most one of each, seems safe
  90. (defun find-or-make-session-store (class)
  91. (or (find class *session-stores* :key #'type-of)
  92. (make-instance class)))
  93. (defun cookie-value (req name)
  94. (assocdr name (get-cookie-values req) :test #'equal))
  95. ;;;; :::::::::::::::::::::::::::::::: Session Variables
  96. (defmacro def-session-variable (name &optional initform &key (store-type :memory) reader writer)
  97. `(progn
  98. (defvar ,name ,initform)
  99. (let ((var (make-instance 'session-variable
  100. :symbol ',name
  101. :reader ',reader
  102. :writer ',writer
  103. :initform ',initform)))
  104. (add-session-variable ,store-type var)
  105. )))
  106. ;;; +++ these ought to delete var from other stores, for development purposes
  107. (defmethod add-session-variable ((type (eql :memory)) var)
  108. (add-session-variable (find-or-make-session-store 'in-memory-session-store) var))
  109. (defmethod add-session-variable ((type (eql :cookie)) var)
  110. (add-session-variable (find-or-make-session-store 'cookie-session-store) var))
  111. ;;; These stores don't exist yet +++
  112. (defmethod add-session-variable ((type (eql :file)) var)
  113. (add-session-variable (find-or-make-session-store 'file-session-store) var))
  114. (defmethod add-session-variable ((type (eql :sql)) var)
  115. (add-session-variable (find-or-make-session-store 'sql-session-store) var))
  116. ;;; This is constant once all session vars are defined, so kind of wasteful(+++)
  117. (defun all-session-variables ()
  118. (mapappend #'session-variables *session-stores*))
  119. (defun all-session-variable-symbols ()
  120. (mapappend #'session-variable-symbols *session-stores*))
  121. ;;; +++ extend so nil arg returns default values
  122. (defun all-session-variable-values (session)
  123. (mapappend #'(lambda (store) (session-values store session)) *session-stores*))
  124. (defun save-session-variables (&optional (session *session*))
  125. (dolist (store *session-stores*)
  126. (session-save-session-variables store session)))
  127. (defclass session-variable ()
  128. ((symbol :initarg :symbol :reader session-variable-symbol)
  129. (reader :initarg :reader :initform nil)
  130. (writer :initarg :writer :initform nil)
  131. (store :initarg :store)
  132. (initform :initarg :initform :initform nil :reader session-variable-initform)))
  133. (defmethod print-object ((object session-variable) stream)
  134. (with-slots (symbol) object
  135. (print-unreadable-object (object stream :type t :identity t)
  136. (princ symbol stream))))
  137. (defmethod session-variable-value ((ssv session-variable))
  138. (symbol-value (session-variable-symbol ssv)))
  139. ;;; temp theory -- all writing is in lisp syntax, reader/writer just transforms into readable if necessary
  140. (defmethod write-session-variable-value ((ssv session-variable) stream)
  141. (with-slots (writer) ssv
  142. (let ((raw (session-variable-value ssv)))
  143. (if (and writer raw)
  144. (write (funcall writer raw) :stream stream)
  145. (write raw :stream stream))
  146. (write-char #\space stream))))
  147. (defmethod read-session-variable-value ((ssv session-variable) stream)
  148. (with-slots (reader) ssv
  149. (let ((raw (read stream)))
  150. (if (and reader raw)
  151. (funcall reader raw)
  152. raw))))
  153. ;;;; :::::::::::::::::::::::::::::::: Response Generation
  154. (defmethod session-variable-symbols ((store session-store))
  155. (with-slots (variables) store
  156. (mapcar #'session-variable-symbol variables)))
  157. (defmethod add-session-variable ((store session-store) var)
  158. (with-slots (variables) store
  159. ;; +++ no, we want to update if its the same as existing
  160. (replacef var variables :key #'session-variable-symbol)))
  161. ;;;; :::::::::::::::::::::::::::::::: Memory Session Store
  162. (defclass in-memory-session-store (session-store)
  163. ((sessions :initform (make-hash-table :test #'eq))))
  164. (defmethod session-values ((store in-memory-session-store) session)
  165. (with-slots (sessions variables) store
  166. (or (gethash session sessions)
  167. (setf (gethash session sessions)
  168. (mapcar #'(lambda (var) (eval (session-variable-initform var))) variables))
  169. )))
  170. ;; +++ rename these methods, they are on session-store not session
  171. (defmethod session-save-session-variables ((store in-memory-session-store) session)
  172. (with-slots (sessions variables) store
  173. (setf (gethash session sessions)
  174. (mapcar #'session-variable-value variables))))
  175. (defmethod session-delete-session ((store in-memory-session-store) session)
  176. (with-slots (sessions variables) store
  177. (remhash session sessions)))
  178. (defmethod reset-session-store ((store in-memory-session-store))
  179. (with-slots (sessions) store
  180. (clrhash sessions)))
  181. ;;;; :::::::::::::::::::::::::::::::: Cookie Session Store
  182. ;;; +++ warning overdesign, may throw some of this out in the interests of simplifying other things
  183. ;;; +++ needs a timer and sweeper...could just make last-use-time a session variable
  184. ;;; +++ I don't quite understand how cookie store can work, since cookie sets have to be done before
  185. ;;; generating the body of a response. Possibly through some javascript, but then that will affect
  186. ;;; the page content (maybe breaking caching, argh). Of course if we are buffering responses, like
  187. ;;; we do on most cwest methods, then it could work.
  188. (defclass serialized-session-store (session-store)
  189. ((package :initform (find-package :wuwei)))) ;+++ temp
  190. (defclass cookie-session-store (serialized-session-store)
  191. ((secret)
  192. (cookie-name :initform (string+ *system-name* "-session"))
  193. ))
  194. (defmethod initialize-instance :after ((store cookie-session-store) &rest ignore)
  195. (declare (ignore ignore))
  196. (recompute-secret store))
  197. ;;; Incorporate the variables into the secret. That way, if they change, existing cookies
  198. ;;; will be invalidated, otherwise they will be mismatched.
  199. (defmethod recompute-secret ((store cookie-session-store))
  200. (with-slots (variables secret) store
  201. (setf secret
  202. (with-output-to-string (s)
  203. (write-string *session-secret* s)
  204. (dolist (v variables)
  205. (write (session-variable-symbol v) :stream s))))))
  206. (defmethod add-session-variable :after ((store cookie-session-store) var)
  207. (recompute-secret store))
  208. ;;; Encryption option would be good (see ironclad package)
  209. (defmethod session-values ((store cookie-session-store) session)
  210. (unless *aserve-request*
  211. (error "attempt to get cookie session vars without binding *aserve-request*"))
  212. (with-slots (cookie-name variables package secret) store
  213. (let ((value (verify-signed-value (cookie-value *aserve-request* cookie-name) secret))
  214. (*package* package))
  215. (if value
  216. (with-input-from-string (s (cadr value))
  217. (collecting
  218. (dolist (var variables)
  219. (collect (report-and-ignore-errors
  220. (read-session-variable-value var s))))))
  221. (mapcar #'(lambda (var) (eval (session-variable-initform var))) variables)
  222. ))))
  223. (defmethod set-cookie-session-cookie ((store cookie-session-store) req)
  224. (with-slots (cookie-name) store
  225. (set-cookie-header req :name cookie-name :value (session-state-cookie-value store) :expires :never)))
  226. (defmethod session-state-cookie-value ((store cookie-session-store))
  227. (with-slots (variables package secret) store
  228. (signed-value
  229. (list *session*
  230. (with-output-to-string (s)
  231. (let ((*print-readably* t)
  232. (*print-pretty* nil)
  233. (*package* package))
  234. ; (unless compact?
  235. ; (format s "~S " (mapcar #'session-variable-symbol variables)))
  236. (dolist (var variables)
  237. (write-session-variable-value var s)))))
  238. secret)))
  239. ;;; No-op (should make sure vars have not changed since header was written +++)
  240. (defmethod session-save-session-variables ((store cookie-session-store) session)
  241. (set-cookie-session-cookie store *aserve-request*))
  242. ;;; +++ these need to get timed out, otherwise they will accumulate ad infinitum
  243. (defmethod session-delete-session ((store cookie-session-store) session)
  244. (with-slots (cookie-name) store
  245. (set-cookie-header *aserve-request* :name cookie-name :value "")))
  246. ;;;; :::::::::::::::::::::::::::::::: Login, Session Creation, Etc
  247. (defun gensym-session-id ()
  248. (keywordize (format nil "~A-~A" (machine-instance) (get-universal-time))))
  249. (defmacro with-session-variables (&body body)
  250. `(let ((%val nil))
  251. (progv (all-session-variable-symbols) (all-session-variable-values *session*)
  252. (unwind-protect
  253. (setq %val (progn ,@body))
  254. (save-session-variables *session*)))
  255. %val))
  256. ;;; Note: has to be OUTSIDE with-http-response-and-body or equiv
  257. ;;; +++ login-handler is ignored?
  258. ;;; Assumes *session* set by with-session-vars, nil if invalid.
  259. ;;; no longer implemented, but maybe should be brought back (login-handler '*default-login-handler*)
  260. (defmacro with-session ((req ent &key) &body body)
  261. `(let* ((*aserve-request* ,req)
  262. (*session* (get-session-id (find-or-make-session-store 'cookie-session-store) ,req)) ;+++ assume this validates
  263. (new-session nil))
  264. (unless *session*
  265. (setq *session* (gensym-session-id)
  266. new-session t))
  267. (with-session-variables
  268. (when new-session (new-session-hook ,req ,ent))
  269. (save-session-variables) ;save cookie variables, especially *session*
  270. ,@body
  271. (save-session-variables) ;we also save the session variables here; let's memory state vars work more easily
  272. )))
  273. (defmethod get-session-id ((store cookie-session-store) req)
  274. (with-slots (cookie-name secret) store
  275. (let ((value (verify-signed-value (cookie-value *aserve-request* cookie-name) secret)))
  276. (when value
  277. (keywordize (first value))))))
  278. ;;; must be run inside with-session
  279. (defmacro with-session-response ((req ent &key content-type no-save?) &body body)
  280. `(progn
  281. (assert *session* nil "With-session-response in bad context")
  282. (unless ,no-save? (save-session-variables *session*))
  283. (with-http-response (,req ,ent ,@(if content-type `(:content-type ,content-type)))
  284. (with-http-body (,req ,ent)
  285. ,@body))))
  286. (defun logout (req ent)
  287. (with-session (req ent)
  288. (delete-session)))
  289. (defun delete-session (&optional (key *session*) store-class)
  290. (if store-class
  291. (session-delete-session (find-or-make-session-store store-class) key)
  292. (dolist (store *session-stores*) (session-delete-session store key))))
  293. ;;; applications can redefine this to do special actions to initialize a session
  294. (defun new-session-hook (req ent)
  295. (declare (ignore req ent))
  296. )
  297. ;;; See session-debug page in eval-server.lisp