sessions.scm 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. ;;; guile-webutils -- Web application utilities for Guile
  2. ;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
  3. ;;;
  4. ;;; This program is free software: you can redistribute it and/or
  5. ;;; modify it under the terms of the GNU General Public License
  6. ;;; as published by the Free Software Foundation, either version 3 of
  7. ;;; the License, or (at your option) any later version.
  8. ;;;
  9. ;;; This program is distributed in the hope that it will be useful,
  10. ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. ;;; General Public License for more details.
  13. ;;;
  14. ;;; You should have received a copy of the GNU General Public License
  15. ;;; along with this program. If not, see
  16. ;;; <http://www.gnu.org/licenses/>.
  17. (define-module (webutils sessions)
  18. #:use-module (ice-9 match)
  19. #:use-module (rnrs bytevectors)
  20. #:use-module (srfi srfi-9)
  21. #:use-module (srfi srfi-19)
  22. #:use-module (srfi srfi-26)
  23. #:use-module (srfi srfi-9 gnu)
  24. #:use-module (gcrypt hmac)
  25. #:use-module (gcrypt base64)
  26. #:use-module (web request)
  27. #:use-module (webutils date)
  28. #:use-module (webutils cookie)
  29. #:export (<session-manager>
  30. make-session-manager session-manager?
  31. <expire-delta>
  32. make-expire-delta expire-delta?
  33. session-data set-session delete-session))
  34. (define-record-type <session-manager>
  35. (make-session-manager-intern key expire-delta reader writer
  36. cookie-name algorithm)
  37. session-manager?
  38. (key session-manager-key)
  39. (expire-delta session-manager-expire-delta)
  40. (reader session-manager-reader)
  41. (writer session-manager-writer)
  42. (cookie-name session-manager-cookie-name)
  43. (algorithm session-manager-algorithm))
  44. ;; Intentionally opaque so as to hide the key
  45. (set-record-type-printer! <session-manager>
  46. (lambda (record port)
  47. (display "#<session-manager>")))
  48. (define (read-from-string str)
  49. (call-with-input-string str
  50. (lambda (port)
  51. (read port))))
  52. (define (write-to-string obj)
  53. (call-with-output-string
  54. (lambda (port)
  55. (write obj port))))
  56. (define-record-type <expire-delta>
  57. (make-expire-delta days hours minutes)
  58. expire-delta?
  59. (days expire-delta-days)
  60. (hours expire-delta-hours)
  61. (minutes expire-delta-minutes))
  62. ;; Defined as parameters for testing
  63. (define %current-time
  64. (make-parameter current-time))
  65. (define (get-current-time)
  66. ((%current-time)))
  67. (define* (date-in-future day hour minute)
  68. (let* ((secs-delta
  69. (+ (* 60 60 24 day)
  70. (* 60 60 hour)
  71. (* 60 minute)))
  72. (current-secs (time-second (get-current-time)))
  73. (future-time (make-time 'time-utc 0 (+ current-secs secs-delta)))
  74. (future-date (time-utc->date future-time 0)))
  75. future-date))
  76. (define* (make-session-manager key #:key
  77. ;; expire in 30 days by default
  78. (expire-delta
  79. '(30 0 0))
  80. (reader read-from-string)
  81. (writer write-to-string)
  82. (cookie-name "session")
  83. (algorithm 'sha512))
  84. (make-session-manager-intern key (apply make-expire-delta expire-delta)
  85. reader writer
  86. cookie-name algorithm))
  87. (define (expire-delta-future-date expire-delta)
  88. (date-in-future (expire-delta-days expire-delta)
  89. (expire-delta-hours expire-delta)
  90. (expire-delta-minutes expire-delta)))
  91. (define (session-manager-future-expires session-manager)
  92. (and=> (session-manager-expire-delta session-manager)
  93. expire-delta-future-date))
  94. (define (split-session-string session-string)
  95. "Split the session string into three strings: key, expire date, encoded-data.
  96. Note that the data is still base64 encoded at this point, and will not be
  97. decoded or read until later.
  98. Split on the dollar-sign character. This is safe because the key is
  99. base64 encoded, and the date uses HTTP style dates, neither of which
  100. should ever contain a dollar-sign."
  101. (let* ((first-dollar-sign (string-index session-string #\$))
  102. (second-dollar-sign (and first-dollar-sign
  103. (string-index session-string #\$
  104. (+ first-dollar-sign 1)))))
  105. (if second-dollar-sign ; no second without the first anyway
  106. (list
  107. (substring session-string 0 first-dollar-sign)
  108. (substring session-string (+ first-dollar-sign 1) second-dollar-sign)
  109. (substring session-string (+ second-dollar-sign 1)))
  110. #f)))
  111. (define (still-fresh-by-date? expires-date)
  112. "Make sure that we haven't yet passed the expiration date"
  113. (time<=? (get-current-time)
  114. (date->time-utc expires-date)))
  115. (define (still-fresh-by-date-string? expires-date-string)
  116. "Parse date string, if valid at all, and see if it's still within
  117. the expiration time"
  118. (and=> (rfc3339-string->date expires-date-string)
  119. still-fresh-by-date?))
  120. (define (session-data session-manager request)
  121. "Extract session data from REQUEST via SESSION-MANAGER, assuming it
  122. contains valid session data in its header."
  123. ;; What's a valid session cookie?
  124. ;; - First we check whether the cookie's expired... there's no sense
  125. ;; checking the signature if it is.
  126. ;; - Next, we check the signature against the date + data (as a
  127. ;; combined utf8 bytevector).
  128. ;; - If that's okay, then we return the read data using the
  129. ;; session-manager's reader method.
  130. (define session-str
  131. (and=> (assoc-ref (request-headers request) 'cookie)
  132. (cut assoc-ref <> (session-manager-cookie-name session-manager))))
  133. (define (decode-data data)
  134. (utf8->string (base64-decode data)))
  135. (match (and=> session-str split-session-string)
  136. ;; If it's false, we return false
  137. (#f #f)
  138. ((sig expires-str (= decode-data data))
  139. (cond
  140. ;; Return false if the date string is invalid
  141. ((not (still-fresh-by-date-string? expires-str))
  142. #f)
  143. ;; Otherwise, check signature against data + data
  144. (else
  145. (let* ((date-and-data (string-append expires-str "$" data))
  146. (valid-sig (verify-sig-base64
  147. (session-manager-key session-manager)
  148. (string->utf8 date-and-data) sig
  149. #:algorithm (session-manager-algorithm
  150. session-manager))))
  151. (if valid-sig
  152. ((session-manager-reader session-manager) data)
  153. #f)))))))
  154. (define (set-session session-manager obj)
  155. "Produce an HTTP cookie header containing signed OBJ, using SESSION-MANAGER."
  156. ;; Sign the date + data, joined with a dollar-sign, as a bytevector.
  157. (let* ((expires-date (session-manager-future-expires session-manager))
  158. (expires-str (date->rfc3339-string expires-date))
  159. (written-data ((session-manager-writer session-manager)
  160. obj))
  161. (date-and-data (string-append expires-str "$" written-data))
  162. (sig (sign-data-base64 (session-manager-key session-manager)
  163. date-and-data
  164. #:algorithm (session-manager-algorithm
  165. session-manager)))
  166. (signed-string
  167. (string-append sig "$" expires-str "$"
  168. (base64-encode (string->utf8 written-data)))))
  169. (set-cookie (session-manager-cookie-name session-manager)
  170. signed-string #:expires expires-date)))
  171. (define (delete-session session-manager)
  172. "Produce an HTTP header deleting the session cookie entirely.
  173. A great way to log users out!"
  174. (delete-cookie (session-manager-cookie-name session-manager)))