working
commit
e89a257a5c
@ -0,0 +1 @@
|
|||||||
|
This is the stub README.txt for the "hunchentoot-postmodern-sessions" project.
|
@ -0,0 +1,362 @@
|
|||||||
|
;;; -*- Mode: LISP; Syntax: COMMON-LISP; Package: HUNCHENTOOT; Base: 10 -*-
|
||||||
|
|
||||||
|
;;; Copyright (c) 2004-2010, Dr. Edmund Weitz. All rights reserved.
|
||||||
|
|
||||||
|
;;; Redistribution and use in source and binary forms, with or without
|
||||||
|
;;; modification, are permitted provided that the following conditions
|
||||||
|
;;; are met:
|
||||||
|
|
||||||
|
;;; * Redistributions of source code must retain the above copyright
|
||||||
|
;;; notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
;;; * Redistributions in binary form must reproduce the above
|
||||||
|
;;; copyright notice, this list of conditions and the following
|
||||||
|
;;; disclaimer in the documentation and/or other materials
|
||||||
|
;;; provided with the distribution.
|
||||||
|
|
||||||
|
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||||
|
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
(in-package :hunchentoot)
|
||||||
|
|
||||||
|
(defgeneric session-db-lock (acceptor &key whole-db-p)
|
||||||
|
(:documentation "A function which returns a lock that will be used
|
||||||
|
to prevent concurrent access to sessions. The first argument will be
|
||||||
|
the acceptor that handles the current request, the second argument is
|
||||||
|
true if the whole \(current) session database is modified. If it is
|
||||||
|
NIL, only one existing session in the database is modified.
|
||||||
|
|
||||||
|
This function can return NIL which means that sessions or session
|
||||||
|
databases will be modified without a lock held \(for example for
|
||||||
|
single-threaded environments). The default is to always return a
|
||||||
|
global lock \(ignoring the ACCEPTOR argument) for Lisps that support
|
||||||
|
threads and NIL otherwise."))
|
||||||
|
|
||||||
|
(defmethod session-db-lock ((acceptor t) &key (whole-db-p t))
|
||||||
|
(declare (ignore whole-db-p))
|
||||||
|
*global-session-db-lock*)
|
||||||
|
|
||||||
|
(defmacro with-session-lock-held ((lock) &body body)
|
||||||
|
"This is like WITH-LOCK-HELD except that it will accept NIL as a
|
||||||
|
\"lock\" and just execute BODY in this case."
|
||||||
|
(with-unique-names (thunk)
|
||||||
|
(with-rebinding (lock)
|
||||||
|
`(flet ((,thunk () ,@body))
|
||||||
|
(cond (,lock (with-lock-held (,lock) (,thunk)))
|
||||||
|
(t (,thunk)))))))
|
||||||
|
|
||||||
|
(defgeneric session-db (acceptor)
|
||||||
|
(:documentation "Returns the current session database which is an
|
||||||
|
alist where each car is a session's ID and the cdr is the
|
||||||
|
corresponding SESSION object itself. The default is to use a global
|
||||||
|
list for all acceptors."))
|
||||||
|
|
||||||
|
(defmethod session-db ((acceptor t))
|
||||||
|
*session-db*)
|
||||||
|
|
||||||
|
(defgeneric (setf session-db) (new-value acceptor)
|
||||||
|
(:documentation "Modifies the current session database. See SESSION-DB."))
|
||||||
|
|
||||||
|
(defmethod (setf session-db) (new-value (acceptor t))
|
||||||
|
(setq *session-db* new-value))
|
||||||
|
|
||||||
|
(defgeneric next-session-id (acceptor)
|
||||||
|
(:documentation "Returns the next sequential session ID, an integer,
|
||||||
|
which should be unique per session. The default method uses a simple
|
||||||
|
global counter and isn't guarded by a lock. For a high-performance
|
||||||
|
production environment you might consider using a more robust
|
||||||
|
implementation."))
|
||||||
|
|
||||||
|
(let ((session-id-counter 0))
|
||||||
|
(defmethod next-session-id ((acceptor t))
|
||||||
|
(incf session-id-counter)))
|
||||||
|
|
||||||
|
(defclass session ()
|
||||||
|
((session-id :initform (next-session-id (request-acceptor *request*))
|
||||||
|
:reader session-id
|
||||||
|
:type integer
|
||||||
|
:documentation "The unique ID \(an INTEGER) of the session.")
|
||||||
|
(session-string :reader session-string
|
||||||
|
:documentation "The session string encodes enough
|
||||||
|
data to safely retrieve this session. It is sent to the browser as a
|
||||||
|
cookie value or as a GET parameter.")
|
||||||
|
(user-agent :initform (user-agent *request*)
|
||||||
|
:reader session-user-agent
|
||||||
|
:documentation "The incoming 'User-Agent' header that
|
||||||
|
was sent when this session was created.")
|
||||||
|
(remote-addr :initform (real-remote-addr *request*)
|
||||||
|
:reader session-remote-addr
|
||||||
|
:documentation "The remote IP address of the client
|
||||||
|
when this session was started as returned by REAL-REMOTE-ADDR.")
|
||||||
|
(session-start :initform (get-universal-time)
|
||||||
|
:reader session-start
|
||||||
|
:documentation "The time this session was started.")
|
||||||
|
(last-click :initform (get-universal-time)
|
||||||
|
:reader session-last-click
|
||||||
|
:documentation "The last time this session was used.")
|
||||||
|
(session-data :initarg :session-data
|
||||||
|
:initform nil
|
||||||
|
:reader session-data
|
||||||
|
:documentation "Data associated with this session -
|
||||||
|
see SESSION-VALUE.")
|
||||||
|
(max-time :initarg :max-time
|
||||||
|
:initform *session-max-time*
|
||||||
|
:accessor session-max-time
|
||||||
|
:type fixnum
|
||||||
|
:documentation "The time \(in seconds) after which this
|
||||||
|
session expires if it's not used."))
|
||||||
|
(:documentation "SESSION objects are automatically maintained by
|
||||||
|
Hunchentoot. They should not be created explicitly with MAKE-INSTANCE
|
||||||
|
but implicitly with START-SESSION and they should be treated as opaque
|
||||||
|
objects.
|
||||||
|
|
||||||
|
You can ignore Hunchentoot's SESSION objects altogether and implement
|
||||||
|
your own sessions if you provide corresponding methods for
|
||||||
|
SESSION-COOKIE-VALUE and SESSION-VERIFY."))
|
||||||
|
|
||||||
|
(defun encode-session-string (id user-agent remote-addr start)
|
||||||
|
"Creates a uniquely encoded session string based on the values ID,
|
||||||
|
USER-AGENT, REMOTE-ADDR, and START"
|
||||||
|
(unless (boundp '*session-secret*)
|
||||||
|
(hunchentoot-warn "Session secret is unbound. Using Lisp's RANDOM function to initialize it.")
|
||||||
|
(reset-session-secret))
|
||||||
|
;; *SESSION-SECRET* is used twice due to known theoretical
|
||||||
|
;; vulnerabilities of MD5 encoding
|
||||||
|
(md5-hex (concatenate 'string
|
||||||
|
*session-secret*
|
||||||
|
(md5-hex (format nil "~A~A~@[~A~]~@[~A~]~A"
|
||||||
|
*session-secret*
|
||||||
|
id
|
||||||
|
(and *use-user-agent-for-sessions*
|
||||||
|
user-agent)
|
||||||
|
(and *use-remote-addr-for-sessions*
|
||||||
|
remote-addr)
|
||||||
|
start)))))
|
||||||
|
|
||||||
|
(defun stringify-session (session)
|
||||||
|
"Creates a string representing the SESSION object SESSION. See
|
||||||
|
ENCODE-SESSION-STRING."
|
||||||
|
(encode-session-string (session-id session)
|
||||||
|
(session-user-agent session)
|
||||||
|
(session-remote-addr session)
|
||||||
|
(session-start session)))
|
||||||
|
|
||||||
|
(defmethod initialize-instance :after ((session session) &rest init-args)
|
||||||
|
"Set SESSION-STRING slot after the session has been initialized."
|
||||||
|
(declare (ignore init-args))
|
||||||
|
(setf (slot-value session 'session-string) (stringify-session session)))
|
||||||
|
|
||||||
|
(defun session-gc ()
|
||||||
|
"Removes sessions from the current session database which are too
|
||||||
|
old - see SESSION-TOO-OLD-P."
|
||||||
|
(with-session-lock-held ((session-db-lock *acceptor*))
|
||||||
|
(setf (session-db *acceptor*)
|
||||||
|
(loop for id-session-pair in (session-db *acceptor*)
|
||||||
|
for (nil . session) = id-session-pair
|
||||||
|
when (session-too-old-p session)
|
||||||
|
do (acceptor-remove-session *acceptor* session)
|
||||||
|
else
|
||||||
|
collect id-session-pair)))
|
||||||
|
(values))
|
||||||
|
|
||||||
|
(defun session-value (symbol &optional (session *session*))
|
||||||
|
"Returns the value associated with SYMBOL from the session object
|
||||||
|
SESSION \(the default is the current session) if it exists."
|
||||||
|
(when session
|
||||||
|
(let ((found (assoc symbol (session-data session) :test #'eq)))
|
||||||
|
(values (cdr found) found))))
|
||||||
|
|
||||||
|
(defsetf session-value (symbol &optional session)
|
||||||
|
(new-value)
|
||||||
|
"Sets the value associated with SYMBOL from the session object
|
||||||
|
SESSION. If there is already a value associated with SYMBOL it will be
|
||||||
|
replaced. Will automatically start a session if none was supplied and
|
||||||
|
there's no session for the current request."
|
||||||
|
(with-rebinding (symbol)
|
||||||
|
(with-unique-names (place %session)
|
||||||
|
`(let ((,%session (or ,session (start-session))))
|
||||||
|
(with-session-lock-held ((session-db-lock *acceptor* :whole-db-p nil))
|
||||||
|
(let* ((,place (assoc ,symbol (session-data ,%session) :test #'eq)))
|
||||||
|
(cond
|
||||||
|
(,place
|
||||||
|
(setf (cdr ,place) ,new-value))
|
||||||
|
(t
|
||||||
|
(push (cons ,symbol ,new-value)
|
||||||
|
(slot-value ,%session 'session-data))
|
||||||
|
,new-value))))))))
|
||||||
|
|
||||||
|
(defun delete-session-value (symbol &optional (session *session*))
|
||||||
|
"Removes the value associated with SYMBOL from SESSION if there is
|
||||||
|
one."
|
||||||
|
(when session
|
||||||
|
(setf (slot-value session 'session-data)
|
||||||
|
(delete symbol (session-data session)
|
||||||
|
:key #'car :test #'eq)))
|
||||||
|
(values))
|
||||||
|
|
||||||
|
(defgeneric session-cookie-value (session)
|
||||||
|
(:documentation "Returns a string which can be used to safely
|
||||||
|
restore the session SESSION if as session has already been
|
||||||
|
established. This is used as the value stored in the session cookie
|
||||||
|
or in the corresponding GET parameter and verified by SESSION-VERIFY.
|
||||||
|
|
||||||
|
A default method is provided and there's no reason to change it unless
|
||||||
|
you want to use your own session objects."))
|
||||||
|
|
||||||
|
(defmethod session-cookie-value ((session session))
|
||||||
|
(and session
|
||||||
|
(format nil
|
||||||
|
"~D:~A"
|
||||||
|
(session-id session)
|
||||||
|
(session-string session))))
|
||||||
|
|
||||||
|
(defgeneric session-cookie-name (acceptor)
|
||||||
|
(:documentation "Returns the name \(a string) of the cookie \(or the
|
||||||
|
GET parameter) which is used to store a session on the client side.
|
||||||
|
The default is to use the string \"hunchentoot-session\", but you can
|
||||||
|
specialize this function if you want another name."))
|
||||||
|
|
||||||
|
(defmethod session-cookie-name ((acceptor t))
|
||||||
|
"hunchentoot-session")
|
||||||
|
|
||||||
|
(defgeneric session-created (acceptor new-session)
|
||||||
|
(:documentation "This function is called whenever a new session has
|
||||||
|
been created. There's a default method which might trigger a session
|
||||||
|
GC based on the value of *SESSION-GC-FREQUENCY*.
|
||||||
|
|
||||||
|
The return value is ignored."))
|
||||||
|
|
||||||
|
(let ((global-session-usage-counter 0))
|
||||||
|
(defmethod session-created ((acceptor t) (session t))
|
||||||
|
"Counts session usage globally and triggers session GC if
|
||||||
|
necessary."
|
||||||
|
(when (and *session-gc-frequency*
|
||||||
|
(Zerop (mod (incf global-session-usage-counter)
|
||||||
|
*session-gc-frequency*)))
|
||||||
|
(session-gc))))
|
||||||
|
|
||||||
|
(defun start-session ()
|
||||||
|
"Returns the current SESSION object. If there is no current session,
|
||||||
|
creates one and updates the corresponding data structures. In this
|
||||||
|
case the function will also send a session cookie to the browser."
|
||||||
|
(let ((session (session *request*)))
|
||||||
|
(when session
|
||||||
|
(return-from start-session session))
|
||||||
|
(setf session (make-instance 'session)
|
||||||
|
(session *request*) session)
|
||||||
|
(with-session-lock-held ((session-db-lock *acceptor*))
|
||||||
|
(setf (session-db *acceptor*)
|
||||||
|
(acons (session-id session) session (session-db *acceptor*))))
|
||||||
|
(set-cookie (session-cookie-name *acceptor*)
|
||||||
|
:value (session-cookie-value session)
|
||||||
|
:path "/")
|
||||||
|
(session-created *acceptor* session)
|
||||||
|
(setq *session* session)))
|
||||||
|
|
||||||
|
(defun remove-session (session)
|
||||||
|
"Completely removes the SESSION object SESSION from Hunchentoot's
|
||||||
|
internal session database."
|
||||||
|
(with-session-lock-held ((session-db-lock *acceptor*))
|
||||||
|
(acceptor-remove-session *acceptor* session)
|
||||||
|
(setf (session-db *acceptor*)
|
||||||
|
(delete (session-id session) (session-db *acceptor*)
|
||||||
|
:key #'car :test #'=)))
|
||||||
|
(values))
|
||||||
|
|
||||||
|
(defun session-too-old-p (session)
|
||||||
|
"Returns true if the SESSION object SESSION has not been active in
|
||||||
|
the last \(SESSION-MAX-TIME SESSION) seconds."
|
||||||
|
(< (+ (session-last-click session) (session-max-time session))
|
||||||
|
(get-universal-time)))
|
||||||
|
|
||||||
|
(defun get-stored-session (id)
|
||||||
|
"Returns the SESSION object corresponding to the number ID if the
|
||||||
|
session has not expired. Will remove the session if it has expired but
|
||||||
|
will not create a new one."
|
||||||
|
(let ((session
|
||||||
|
(cdr (assoc id (session-db *acceptor*) :test #'=))))
|
||||||
|
(when (and session
|
||||||
|
(session-too-old-p session))
|
||||||
|
(when *reply*
|
||||||
|
(log-message* :info "Session with ID ~A too old" id))
|
||||||
|
(remove-session session)
|
||||||
|
(setq session nil))
|
||||||
|
session))
|
||||||
|
|
||||||
|
(defgeneric session-verify (request)
|
||||||
|
(:documentation "Tries to get a session identifier from the cookies
|
||||||
|
\(or alternatively from the GET parameters) sent by the client (see
|
||||||
|
SESSION-COOKIE-NAME and SESSION-COOKIE-VALUE). This identifier is
|
||||||
|
then checked for validity against the REQUEST object REQUEST. On
|
||||||
|
success the corresponding session object \(if not too old) is returned
|
||||||
|
\(and updated). Otherwise NIL is returned.
|
||||||
|
|
||||||
|
A default method is provided and you only need to write your own one
|
||||||
|
if you want to maintain your own sessions."))
|
||||||
|
|
||||||
|
(defmethod session-verify ((request request))
|
||||||
|
(let ((session-identifier (or (when-let (session-cookie (cookie-in (session-cookie-name *acceptor*) request))
|
||||||
|
(url-decode session-cookie))
|
||||||
|
(get-parameter (session-cookie-name *acceptor*) request))))
|
||||||
|
(unless (and session-identifier
|
||||||
|
(stringp session-identifier)
|
||||||
|
(plusp (length session-identifier)))
|
||||||
|
(return-from session-verify nil))
|
||||||
|
(destructuring-bind (id-string session-string)
|
||||||
|
(split ":" session-identifier :limit 2)
|
||||||
|
(let* ((id (parse-integer id-string))
|
||||||
|
(session (get-stored-session id))
|
||||||
|
(user-agent (user-agent request))
|
||||||
|
(remote-addr (remote-addr request)))
|
||||||
|
(cond
|
||||||
|
((and session
|
||||||
|
(string= session-string
|
||||||
|
(session-string session))
|
||||||
|
(string= session-string
|
||||||
|
(encode-session-string id
|
||||||
|
user-agent
|
||||||
|
(real-remote-addr request)
|
||||||
|
(session-start session))))
|
||||||
|
;; the session key presented by the client is valid
|
||||||
|
(setf (slot-value session 'last-click) (get-universal-time))
|
||||||
|
session)
|
||||||
|
(session
|
||||||
|
;; the session ID pointed to an existing session, but the
|
||||||
|
;; session string did not match the expected session string
|
||||||
|
(log-message* :warning
|
||||||
|
"Fake session identifier '~A' (User-Agent: '~A', IP: '~A')"
|
||||||
|
session-identifier user-agent remote-addr)
|
||||||
|
;; remove the session to make sure that it can't be used
|
||||||
|
;; again; the original legitimate user will be required to
|
||||||
|
;; log in again
|
||||||
|
(remove-session session)
|
||||||
|
nil)
|
||||||
|
(t
|
||||||
|
;; no session was found under the ID given, presumably
|
||||||
|
;; because it has expired.
|
||||||
|
(log-message* :info
|
||||||
|
"No session for session identifier '~A' (User-Agent: '~A', IP: '~A')"
|
||||||
|
session-identifier user-agent remote-addr)
|
||||||
|
nil))))))
|
||||||
|
|
||||||
|
(defun reset-session-secret ()
|
||||||
|
"Sets *SESSION-SECRET* to a new random value. All old sessions will
|
||||||
|
cease to be valid."
|
||||||
|
(setq *session-secret* (create-random-string 10 36)))
|
||||||
|
|
||||||
|
(defun reset-sessions (&optional (acceptor *acceptor*))
|
||||||
|
"Removes ALL stored sessions of ACCEPTOR."
|
||||||
|
(with-session-lock-held ((session-db-lock acceptor))
|
||||||
|
(loop for (nil . session) in (session-db acceptor)
|
||||||
|
do (acceptor-remove-session acceptor session))
|
||||||
|
(setq *session-db* nil))
|
||||||
|
(values))
|
@ -0,0 +1,8 @@
|
|||||||
|
(defpackage #:pg-sessions
|
||||||
|
(:use #:cl #:postmodern #:anaphora #:alexandria)
|
||||||
|
(:export #:pg-session
|
||||||
|
#:pg-session-id
|
||||||
|
#:pg-session-gc
|
||||||
|
#:pg-start-session
|
||||||
|
#:save-pg-session))
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
(asdf:defsystem #:pg-sessions
|
||||||
|
:serial t
|
||||||
|
:description "Describe hunchentoot-postmodern-sessions here"
|
||||||
|
:author "Your Name <your.name@example.com>"
|
||||||
|
:license "Specify license here"
|
||||||
|
:depends-on (#:postmodern
|
||||||
|
#:anaphora
|
||||||
|
#:alexandria
|
||||||
|
#:cl-ppcre
|
||||||
|
#:hunchentoot)
|
||||||
|
:components ((:file "package")
|
||||||
|
(:file "pg-sessions")))
|
||||||
|
|
@ -0,0 +1,227 @@
|
|||||||
|
;;; Copyright (c) 2004-2010, Dr. Edmund Weitz. All rights reserved.
|
||||||
|
;;; Copyright (c) 2014, Thomas Hintz. All rights reserved.
|
||||||
|
|
||||||
|
;;; Redistribution and use in source and binary forms, with or without
|
||||||
|
;;; modification, are permitted provided that the following conditions
|
||||||
|
;;; are met:
|
||||||
|
|
||||||
|
;;; * Redistributions of source code must retain the above copyright
|
||||||
|
;;; notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
;;; * Redistributions in binary form must reproduce the above
|
||||||
|
;;; copyright notice, this list of conditions and the following
|
||||||
|
;;; disclaimer in the documentation and/or other materials
|
||||||
|
;;; provided with the distribution.
|
||||||
|
|
||||||
|
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||||
|
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||||
|
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||||
|
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
(in-package #:pg-sessions)
|
||||||
|
|
||||||
|
(defparameter *hunchentoot-postmodern-session-max-time* 3600) ; 1 hour
|
||||||
|
(defvar *pg-session-secret* "thisisasecret")
|
||||||
|
|
||||||
|
(defclass pg-session ()
|
||||||
|
((session-id :col-type serial :initarg :session-id :reader pg-session-id)
|
||||||
|
(session-string :col-type string :initform "" :accessor pg-session-string
|
||||||
|
:documentation "The session string encodes enough
|
||||||
|
data to safely retrieve this session. It is sent to the browser as a
|
||||||
|
cookie value or as a GET parameter.")
|
||||||
|
(user-agent :initarg :user-agent
|
||||||
|
:accessor pg-session-user-agent)
|
||||||
|
(remote-addr :initarg :remote-addr
|
||||||
|
:accessor pg-session-remote-addr)
|
||||||
|
(session-start :col-type bigint :initform (get-universal-time) :accessor pg-session-start)
|
||||||
|
(last-click :col-type bigint :initform (get-universal-time) :accessor pg-session-last-click
|
||||||
|
:documentation "The last time this session was used.")
|
||||||
|
(max-time :col-type bigint :initarg :max-time
|
||||||
|
:initform *hunchentoot-postmodern-session-max-time*
|
||||||
|
:accessor pg-session-max-time
|
||||||
|
:documentation "The time \(in seconds) after which this
|
||||||
|
session expires if it's not used."))
|
||||||
|
(:metaclass dao-class)
|
||||||
|
(:keys session-id)
|
||||||
|
(:documentation "An extensions of hunchentoot's sessions so that sessions can be
|
||||||
|
stored in the postmodern database."))
|
||||||
|
|
||||||
|
(defun md5-hex (string)
|
||||||
|
"Calculates the md5 sum of the string STRING and returns it as a hex string."
|
||||||
|
(with-output-to-string (s)
|
||||||
|
(loop for code across (md5:md5sum-string string)
|
||||||
|
do (format s "~2,'0x" code))))
|
||||||
|
|
||||||
|
(defun encode-session-string (id user-agent remote-addr start)
|
||||||
|
"Creates a uniquely encoded session string based on the values ID,
|
||||||
|
USER-AGENT, and START"
|
||||||
|
;; *PG-SESSION-SECRET* is used twice due to known theoretical
|
||||||
|
;; vulnerabilities of MD5 encoding
|
||||||
|
(hunchentoot:log-message* :info "UA: ~A" user-agent)
|
||||||
|
(hunchentoot:log-message* :info "addr: ~A" remote-addr)
|
||||||
|
(hunchentoot:log-message* :info "start: ~A" start)
|
||||||
|
(hunchentoot:log-message* :info "id: ~A" id)
|
||||||
|
(let ((r (md5-hex (concatenate 'string
|
||||||
|
*pg-session-secret*
|
||||||
|
(md5-hex (format nil "~A~A~@[~A~]~@[~A~]~A"
|
||||||
|
*pg-session-secret*
|
||||||
|
id
|
||||||
|
(and hunchentoot:*use-user-agent-for-sessions*
|
||||||
|
user-agent)
|
||||||
|
(and hunchentoot:*use-remote-addr-for-sessions*
|
||||||
|
remote-addr)
|
||||||
|
start))))))
|
||||||
|
(hunchentoot:log-message* :info "encoding: ~A" r)
|
||||||
|
r))
|
||||||
|
|
||||||
|
(defun stringify-session (session)
|
||||||
|
"Creates a string representing of the SESSION object SESSION. See
|
||||||
|
ENCODE-SESSION-STRING."
|
||||||
|
(hunchentoot:log-message* :info "UA: ~A" (pg-session-user-agent session))
|
||||||
|
(hunchentoot:log-message* :info "addr: ~A" (pg-session-remote-addr session))
|
||||||
|
(hunchentoot:log-message* :info "start: ~A" (pg-session-start session))
|
||||||
|
(encode-session-string (pg-session-id session)
|
||||||
|
(pg-session-user-agent session)
|
||||||
|
(pg-session-remote-addr session)
|
||||||
|
(pg-session-start session)))
|
||||||
|
|
||||||
|
(defun session-too-old-p (session)
|
||||||
|
(< (+ (pg-session-last-click session) (pg-session-max-time session))
|
||||||
|
(get-universal-time)))
|
||||||
|
|
||||||
|
(defun pg-remove-session (session)
|
||||||
|
"Completely removes the SESSION object SESSION from Hunchentoot's
|
||||||
|
internal session database."
|
||||||
|
(delete-dao session)
|
||||||
|
(values))
|
||||||
|
|
||||||
|
(defun pg-session-gc ()
|
||||||
|
"Removes sessions from the current session database which are too
|
||||||
|
old - see SESSION-TOO-OLD-P."
|
||||||
|
(loop for session in (select-dao 'pg-session)
|
||||||
|
when (session-too-old-p session)
|
||||||
|
do (remove-session session)))
|
||||||
|
|
||||||
|
(defmethod hunchentoot:session-cookie-value ((pg-session pg-session))
|
||||||
|
(hunchentoot:log-message* :warning "session verify")
|
||||||
|
(and pg-session
|
||||||
|
(format nil
|
||||||
|
"~D:~A"
|
||||||
|
(pg-session-id pg-session)
|
||||||
|
(pg-session-string pg-session))))
|
||||||
|
|
||||||
|
(defun save-pg-session (session)
|
||||||
|
(upsert-dao session))
|
||||||
|
|
||||||
|
(defun pg-start-session ()
|
||||||
|
"Returns the current SESSION object. If there is no current session,
|
||||||
|
creates one and updates the corresponding data structures. In this
|
||||||
|
case the function will also send a session cookie to the browser."
|
||||||
|
(hunchentoot:log-message* :info "in pg-start-session")
|
||||||
|
(let ((session (hunchentoot:session hunchentoot:*request*)))
|
||||||
|
(when session
|
||||||
|
(return-from pg-start-session session))
|
||||||
|
(hunchentoot:log-message* :info "session before")
|
||||||
|
(setf session (make-dao 'pg-session
|
||||||
|
:user-agent (hunchentoot:user-agent hunchentoot:*request*)
|
||||||
|
:remote-addr (hunchentoot:real-remote-addr hunchentoot:*request*)))
|
||||||
|
(hunchentoot:log-message* :info "session ~S" session)
|
||||||
|
(hunchentoot:log-message* :info "session-id: ~A" (pg-session-id session))
|
||||||
|
(setf (hunchentoot:session hunchentoot:*request*) session)
|
||||||
|
(setf (pg-session-string session) (stringify-session session))
|
||||||
|
(save-pg-session session)
|
||||||
|
(hunchentoot:set-cookie (hunchentoot:session-cookie-name hunchentoot:*acceptor*)
|
||||||
|
:value (hunchentoot:session-cookie-value session)
|
||||||
|
:path "/")
|
||||||
|
(setq hunchentoot:*session* session)))
|
||||||
|
|
||||||
|
(defun get-stored-session (id request)
|
||||||
|
"Returns the SESSION object corresponding to the number ID if the
|
||||||
|
session has not expired. Will remove the session if it has expired but
|
||||||
|
will not create a new one."
|
||||||
|
(hunchentoot:log-message* :info "get-stored-session ~A" id)
|
||||||
|
(hunchentoot:log-message* :info "request ~S" request)
|
||||||
|
(let ((session (get-dao 'pg-session id)))
|
||||||
|
(hunchentoot:log-message* :info "get-stored-session ~A recieved" id)
|
||||||
|
(hunchentoot:log-message* :info "session: ~A" session)
|
||||||
|
(when (and session
|
||||||
|
(session-too-old-p session))
|
||||||
|
(when hunchentoot:*reply*
|
||||||
|
(hunchentoot:log-message* :info "Session with ID ~A too old" id))
|
||||||
|
(pg-remove-session session)
|
||||||
|
(setq session nil))
|
||||||
|
(when session
|
||||||
|
(hunchentoot:log-message* :info "setting stuff")
|
||||||
|
(setf (pg-session-user-agent session) (hunchentoot:user-agent request)
|
||||||
|
(pg-session-remote-addr session) (hunchentoot:real-remote-addr request)))
|
||||||
|
session))
|
||||||
|
|
||||||
|
(defmethod hunchentoot:session-verify ((request hunchentoot:request))
|
||||||
|
(hunchentoot:log-message* :info "session verify")
|
||||||
|
(hunchentoot:log-message* :warning "UNCOMMENT remove session in faked")
|
||||||
|
(with-connection
|
||||||
|
'("foods" "tjhintz" "" "localhost" :pooled-p t)
|
||||||
|
(with-transaction (transaction)
|
||||||
|
(let ((session-identifier
|
||||||
|
(or (when-let (session-cookie (hunchentoot:cookie-in
|
||||||
|
(hunchentoot:session-cookie-name
|
||||||
|
hunchentoot:*acceptor*) request))
|
||||||
|
(hunchentoot:url-decode session-cookie))
|
||||||
|
(hunchentoot:get-parameter (hunchentoot:session-cookie-name
|
||||||
|
hunchentoot:*acceptor*) request))))
|
||||||
|
(unless (and session-identifier
|
||||||
|
(stringp session-identifier)
|
||||||
|
(plusp (length session-identifier)))
|
||||||
|
(return-from session-verify nil))
|
||||||
|
(destructuring-bind (id-string session-string)
|
||||||
|
(cl-ppcre:split ":" session-identifier :limit 2)
|
||||||
|
(let* ((id (parse-integer id-string))
|
||||||
|
(session (get-stored-session id request))
|
||||||
|
(user-agent (hunchentoot:user-agent request))
|
||||||
|
(remote-addr (hunchentoot:remote-addr request)))
|
||||||
|
(cond
|
||||||
|
((and session
|
||||||
|
(string= session-string
|
||||||
|
(pg-session-string session))
|
||||||
|
(string= session-string
|
||||||
|
(encode-session-string id
|
||||||
|
user-agent
|
||||||
|
(hunchentoot:real-remote-addr request)
|
||||||
|
(pg-session-start session))))
|
||||||
|
;; the session key presented by the client is valid
|
||||||
|
(setf (pg-session-last-click session) (get-universal-time))
|
||||||
|
(save-pg-session session)
|
||||||
|
session)
|
||||||
|
(session
|
||||||
|
;; the session ID pointed to an existing session, but the
|
||||||
|
;; session string did not match the expected session string
|
||||||
|
(hunchentoot:log-message*
|
||||||
|
:warning "Fake session identifier '~A' (User-Agent: '~A', IP: '~A')"
|
||||||
|
session-identifier user-agent remote-addr)
|
||||||
|
;; remove the session to make sure that it can't be used
|
||||||
|
;; again; the original legitimate user will be required to
|
||||||
|
;; log in again
|
||||||
|
;; (pg-remove-session session)
|
||||||
|
nil)
|
||||||
|
(t
|
||||||
|
;; no session was found under the ID given, presumably
|
||||||
|
;; because it has expired.
|
||||||
|
(hunchentoot:log-message*
|
||||||
|
:info "No session for session identifier '~A' (User-Agent: '~A', IP: '~A')"
|
||||||
|
session-identifier user-agent remote-addr)
|
||||||
|
nil))))))))
|
||||||
|
|
||||||
|
;; (defun reset-sessions (&optional (acceptor *acceptor*))
|
||||||
|
;; "Removes ALL stored sessions of ACCEPTOR."
|
||||||
|
;; (with-session-lock-held ((session-db-lock acceptor))
|
||||||
|
;; (loop for (nil . session) in (session-db acceptor)
|
||||||
|
;; do (acceptor-remove-session acceptor session))
|
||||||
|
;; (setq *session-db* nil))
|
||||||
|
;; (values))
|
Loading…
Reference in New Issue