1c9a89d474aee1d31c6c0dd4c6f3bb70f5e3dc39
[wol.git] / project.lisp
1 ;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10; Package: wol -*-
2 ;;;; *************************************************************************
3 ;;;; FILE IDENTIFICATION
4 ;;;;
5 ;;;; Name:          project.lisp
6 ;;;; Purpose:       Project handler for wol library
7 ;;;; Programmer:    Kevin M. Rosenberg
8 ;;;; Date Started:  July 2003
9 ;;;;
10 ;;;; $Id: project.lisp,v 1.6 2003/08/05 23:00:28 kevin Exp $
11 ;;;;
12 ;;;; This file and Wol are Copyright (c) 2001-2003 by Kevin M. Rosenberg
13 ;;;; *************************************************************************
14
15 (in-package #:wol)
16
17 (defun wol-project (name &key (project-prefix "/") map index
18                     (sessions t) (session-lifetime 18000)
19                     (reap-interval 300) server
20                     (connector :modlisp))
21   (unless server
22     (setq server 
23           (ecase connector
24             (:modlisp ml:*ml-server*)
25             (:aserve net.aserve:*wserver*))))
26   
27   (unless server
28     (warn "Can't start project without server")
29     (return-from wol-project nil))
30   
31   (multiple-value-bind (project found) (gethash name *active-projects*)
32     (unless found
33       (setq project (make-instance 'wol-project))
34       (setf (gethash name *active-projects*) project))
35     
36     (setf (project-name project) name)
37     (setf (project-prefix project) project-prefix)
38     (setf (project-map project) map)
39     (setf (project-index project) index) 
40     (setf (project-server project) server)
41     (setf (project-connector project) connector)
42     (setf (lifetime (session-master project)) session-lifetime)
43     (setf (cookie-name (session-master project)) name)
44
45     (let ((hash (make-hash-table :size (length map) :test 'equal)))
46       (dolist (map-item map)
47         (setf (gethash (first map-item) hash) (second map-item)))
48       (setf (project-hash-map project) hash))
49
50     (ecase connector
51       (:modlisp
52        (setf (ml::processor server) 'wol-ml-processor))
53       (:aserve
54        (net.aserve:publish-prefix :prefix project-prefix
55                                   :server server
56                                   :function 'wol-aserve-processor)))
57   
58   (if sessions
59         (when (null (sessions (session-master project)))
60           (setf (sessions (session-master project))
61             (make-hash-table :test 'eq)))
62       (when (sessions (session-master project))
63         (setf (sessions (session-master project)) nil)))
64
65     (setq *reap-interval* reap-interval)
66     (when (and sessions (null *reaper-process*))
67       (setq *reaper-process* (start-reaper)))))
68     
69 (defun wol-ml-processor (command)
70   "Processes an incoming modlisp command"
71   (let* ((req (command->request command
72                                 :ml-server *ml-server*))
73          (ent (make-entity-for-request req)))
74     (if ent
75         (dispatch-request req ent)
76         (no-url-handler req ent))))
77
78
79 (defun wol-aserve-processor (as-req as-ent)
80   "Processes an incoming modlisp command"
81   (let* ((req (make-request-from-aserve as-req))
82          (ent (make-entity-from-aserve req as-ent)))
83     (dispatch-request req ent)))
84
85 (defun make-request-from-aserve (as-req)
86   (make-instance 'http-request
87                  :method (net.aserve:request-method as-req)
88                  ;;:host (net.aserve:request-host as-req)
89                  :raw-uri (puri:intern-uri
90                            (net.uri:render-uri
91                             (net.aserve:request-raw-uri as-req) nil))
92                  :uri (puri:intern-uri
93                        (net.uri:render-uri
94                         (net.aserve:request-uri as-req) nil))
95                  :protocol (net.aserve:request-protocol as-req)
96                  :protocol-string
97                  (net.aserve:request-protocol-string as-req)
98                  :posted-content (net.aserve::request-request-body as-req)
99                  :socket (net.aserve:request-socket as-req)
100                  :aserve-server net.aserve:*wserver*
101                  :aserve-request as-req))
102
103 (defun make-entity-from-aserve (req as-ent)
104   (make-instance 'entity
105                  :project (find-project-for-request req)
106                  :aserve-entity as-ent))
107
108
109 (defun command->request (command &key ml-server)
110   "Convert a cl-modlisp command into a wol request"
111   (let ((req
112          (make-instance 'http-request
113            :host (header-value command :host)
114            :raw-uri  (aif (ignore-errors
115                            (puri:intern-uri (header-value command :url)))
116                           it
117                           (header-value command :url))
118            :uri (aif (ignore-errors
119                       (puri:intern-uri (command->uri command)))
120                      it
121                      (command->uri command))
122            :protocol (ensure-keyword
123                       (header-value command :server-protocol))
124            :protocol-string (header-value command :server-protocol)
125            :method (ensure-keyword (header-value command :method))
126            :posted-content (header-value command :posted-content)
127            :headers command
128            :socket *modlisp-socket*
129            :ml-server ml-server)))
130     req))
131
132 (defun header-slot-value (req slot)
133   (header-value (request-headers req) slot))
134
135 (defun command->uri (command)
136   (format nil "http://~A:~D~A"
137           (header-value command :host)
138           (header-value command :server-ip-port)
139           (header-value command :url)))
140
141 (defun is-index-request (req ent)
142   (string= (puri:uri-path (request-raw-uri req)) 
143            (project-prefix (entity-project ent))))
144
145 (defun redirect-entity (page ent &optional plist)
146   (redirect-to-location (apply #'make-wol-url page ent plist)))
147
148 (defun dispatch-request (req ent)
149   (let ((proj (entity-project ent)))
150     (if (is-index-request req ent)
151         (redirect-entity (project-index proj) ent)
152         (progn
153           (request-decompile-uri req ent)
154           (compute-session req ent)
155           (dispatch-to-handler req ent)))))
156
157 (defun make-entity (&key project)
158   (make-instance 'entity :project project))
159
160 (defun make-entity-for-request (req)
161   (awhen (find-project-for-request req)
162          (make-entity :project it)))
163
164 (defun find-project-for-request (req)
165   (maphash (lambda (name project)
166              (declare (ignore name))
167              (when (request-matches-prefix req (project-prefix project))
168                (return-from find-project-for-request project)))
169            *active-projects*))
170
171 (defun request-matches-prefix (req prefix)
172   "Returns project if request matches project"
173   (string-starts-with prefix (puri:uri-path (request-raw-uri req))))
174
175
176 (defun dispatch-to-handler (req ent)
177   (let ((handler (request-find-handler req ent))
178         (*wol-stream* (request-socket req)))
179     (if handler
180         (handle-request handler req ent)
181       (no-url-handler req ent))))
182
183 (defun request-find-handler (req ent)
184   (nth-value 0 (gethash (request-page req) 
185                         (project-hash-map (entity-project ent)))))
186
187 (defun handle-request (handler req ent)
188   (typecase handler
189     (null
190      nil)
191     ((or symbol function)
192      (when (and (symbolp handler)
193                 (not (fboundp handler)))
194        (cmsg "handler given a symbol without a function ~S" handler)
195        (return-from handle-request nil))
196      (let ((next-page (funcall handler req ent)))
197        (typecase next-page
198          (string
199           (redirect-entity next-page ent))
200          (cons
201           (redirect-entity (car next-page) ent (cadr next-page)))
202          (null
203           t)
204          (t
205           (cmsg "handler should return nil or a string, not ~S" next-page))))
206      t)
207     (string
208      (cmsg "string handler not supported: ~A" handler)
209      nil)
210     (t
211      (cmsg "unknown handler type: ~S" handler)
212      nil)))
213
214
215
216 (defun wol-version-string ()
217   (format nil "~{~D~^.~}" *wol-version*))
218   
219 (defun request-query (req &key (uri t) (post t))
220   (let ((desired (cons uri post)))
221     (if (equal desired (request-desired-query req))
222         ;; Same desired as cached 
223         (request-query-alist req)
224       (progn
225         (setf (request-desired-query req) desired)
226         (setf (request-query-alist req)
227           (append
228            (when (and uri (request-uri-query req))
229              (query-to-alist (request-uri-query req)))
230            (when (and post (request-posted-content req))
231              (query-to-alist (request-posted-content req)))))))))
232
233 (defun request-query-value (key req &key (uri t) (post t))
234   (cdr (assoc key (request-query req :uri uri :post post)
235               :test 'equal)))
236     
237 (defun websession-variable (ws name)
238   (when ws
239     (gethash name (websession-variables ws))))
240
241 (defun (setf websession-variable) (value ws name)
242   (when ws
243     (setf (gethash name (websession-variables ws)) value)))
244
245
246 (defmacro with-wol-page ((req ent
247                               &key (format :html) (precompute t) headers)
248                          &body body)
249   (declare (ignore req ent))
250   (let ((fmt (gensym "FMT-"))
251         (precomp (gensym "PRE-"))
252         (result (gensym "RES-"))
253         (outstr (gensym "STR-"))
254         (stream (gensym "STRM-"))
255         (hdr (gensym "HDR-")))
256     `(let ((,fmt ,format)
257            (,precomp ,precompute)
258            ,result ,outstr ,stream)
259        (declare (ignorable ,stream))
260        (write-header-line "Status" "200 OK")
261        (write-header-line "Content-Type" (ml::format-string ,fmt))
262        (dolist (,hdr ,headers)
263          (write-header-line (car ,hdr) (cdr ,hdr)))
264        (unless ,precomp
265          (write-string "end" *wol-stream*)
266          (write-char #\NewLine *wol-stream*))
267        (setq ,outstr
268          (with-output-to-string (,stream)
269            (let ((*html-stream* (if ,precomp
270                                    ,stream
271                                    *wol-stream*))
272                  (*wol-stream* (if ,precomp
273                                    ,stream
274                                    *wol-stream*)))
275              (setq ,result (progn ,@body)))))
276        (cond
277         (,precomp
278          (write-header-line "Content-Length" 
279                             (write-to-string (length ,outstr)))
280          (write-header-line "Keep-Socket" "1")
281          (write-header-line "Connection" "Keep-Alive")
282          (write-string "end" *wol-stream*)
283          (write-char #\NewLine *wol-stream*)
284          (write-string ,outstr *wol-stream*)
285          (finish-output *wol-stream*)
286          (setq *close-modlisp-socket* nil))
287         (t
288          (finish-output *wol-stream*)
289          (setq *close-modlisp-socket* t)))
290        ,result)))
291
292
293 (defun no-url-handler (req ent)
294   (with-wol-page (req ent)
295     (html
296      (:html
297       (:head
298        (:title "404 - NotFound"))
299       (:body
300        (:h1 "Not Found")
301        (:p "The request for "
302            (:b (:write-string (request-uri req)))
303            " was not found on this server.")
304        (:hr)
305        (:div (:i "WOL "
306                  (:write-string (wol-version-string)))))))))