2 * - by Richard W.M. Jones <rich@annexia.org>
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
9 * This library 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 * Library General Public License for more details.
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the Free
16 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 * $Id: monolith.c,v 1.27 2003/04/30 13:15:35 rich Exp $
27 #ifdef HAVE_SYS_TYPES_H
28 #include <sys/types.h>
47 #ifdef HAVE_NETINET_IN_H
48 #include <netinet/in.h>
51 #ifdef HAVE_ARPA_INET_H
52 #include <arpa/inet.h>
55 #ifdef HAVE_SYS_SOCKET_H
56 #include <sys/socket.h>
63 #include <pthr_reactor.h>
64 #include <pthr_pseudothread.h>
65 #include <pthr_iolib.h>
66 #include <pthr_mutex.h>
67 #include <pthr_http.h>
68 #include <pthr_wait_queue.h>
69 #include <rws_request.h>
71 #include "ml_window.h"
76 #define STRINGIFY(STRING) #STRING
78 #define STRINGIFY(STRING) "STRING"
80 #endif /* ! STRINGIFY */
82 /* Define some RFC-compliant dates to represent past and future. */
83 #define DISTANT_PAST "Thu, 01 Dec 1994 16:00:00 GMT"
84 #define DISTANT_FUTURE "Sun, 01 Dec 2030 16:00:00 GMT"
86 /* Headers which are sent to defeat caches. */
87 #define NO_CACHE_HEADERS "Cache-Control", "must-revalidate", \
88 "Expires", DISTANT_PAST, \
91 /* These are the default session reaping parameters, all in seconds. */
92 #define SESSION_REAP_MIN 600
93 #define SESSION_REAP_MAX 3600
94 #define SESSION_REAP_INC 600
98 const char *sessionid; /* Session ID (string of 32 hex digits). */
99 pool session_pool; /* Pool for this session. */
100 mutex lock; /* Lock for this session. */
101 int hits; /* Number of requests in this session. */
102 reactor_time_t created, last_access; /* Session created, last accessed. */
103 int reap_min; /* Time to reap, if hits == 1. */
104 int reap_max; /* Maximum reap time. */
105 int reap_inc; /* Increment in reap time, per hit. */
106 struct sockaddr_in original_ip; /* IP address of initial request. */
107 cgi args; /* Initial arguments. */
108 cgi submitted_args; /* Current arguments (short-lived). */
109 rws_request rws_rq; /* Current request (short-lived). */
110 io_handle io; /* Current IO handle (short-lived). */
111 void (*app_main) (ml_session); /* Main entry point into the application. */
112 ml_window current_window; /* "Current" window for the application. */
113 ml_window main_window; /* Nominated main window for the application.*/
114 shash windows; /* Maps window IDs -> ml_window. */
115 shash actions; /* Maps action IDs -> callback functions. */
116 const char *host_header; /* Host header. */
117 const char *canonical_path; /* Full path to the script. */
118 const char *script_name; /* Just the name of the script. */
119 const char *user_agent; /* User agent. */
120 hash dbhs; /* Hash db_handle -> pools of
121 * handles given out in current session. */
122 int userid; /* Currently logged in user (0 = none). */
123 const char *auth_cookie; /* If set, send an auth cookie at the end
124 * of the current HTTP request. */
125 const char *auth_cookie_path, *auth_cookie_expires;
130 void (*callback_fn) (ml_session, void *data);
134 static pool ml_pool; /* Monolith library's own pool. */
135 static shash sessions; /* Maps session IDs -> ml_session. */
136 static const char *auth_dbf; /* Connection used for authentication. */
138 static void run_action (ml_session, const char *);
139 static int bad_request_error (rws_request rq, const char *text);
140 static int auth_to_userid (ml_session, const char *auth);
141 static void monolith_init (void) __attribute__ ((constructor));
142 static void monolith_stop (void) __attribute__ ((destructor));
143 static void kill_session (const char *sessionid);
148 ml_pool = new_subpool (global_pool);
149 sessions = new_shash (ml_pool, ml_session);
155 /* Note that this will also free up memory used by sessions, since
156 * each session pool is a subpool of ml_pool.
158 delete_pool (ml_pool);
164 static reactor_time_t last_reap = 0;
166 /* Only reap sessions every 10s. This also ensures that only one thread
167 * will try to kill sessions.
169 if (reactor_time - last_reap > 10000LL)
174 last_reap = reactor_time;
177 = shash_values_in_pool (sessions, pth_get_pool (current_pth));
179 for (i = 0; i < vector_size (session_list); ++i)
184 vector_get (session_list, i, session);
186 /* Calculate the age before reaping. */
187 reap_age = session->reap_min +
188 (session->hits - 1) * session->reap_inc;
189 if (reap_age > session->reap_max)
190 reap_age = session->reap_max;
192 /* Session is older than this? */
193 if (reactor_time - session->last_access > reap_age * 1000LL)
197 "reaping session ID %s\n"
198 "current time = %llu, last access = %llu, diff = %llu",
201 session->last_access,
202 reactor_time - session->last_access
205 kill_session (session->sessionid);
214 return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
217 static inline const char *
218 get_sessionid_from_cookie (http_request http_request)
220 const char *sessionid;
223 sessionid = http_request_get_cookie (http_request, "ml_sessionid");
226 /* Check that the cookie has a valid form (32 hex digits). If
227 * not, just ignore it.
229 if (strlen (sessionid) == 32)
231 for (i = 0; i < 32; ++i)
232 if (!my_isxdigit (sessionid[i]))
241 /* This function is so useful, it should be part of c2lib (XXX)
242 * See also the other occurrence in ml_login_nopw.c
244 static inline const char *
245 generate_sessionid (pool pool)
248 unsigned char buffer[16];
249 char *sessionid = pmalloc (pool, 33 * sizeof (char));
251 fd = open ("/dev/urandom", O_RDONLY);
252 if (fd == -1) abort ();
253 if (read (fd, buffer, 16) != 16) abort ();
256 for (i = 0; i < 16; ++i)
257 sprintf (sessionid + i*2, "%02x", buffer[i]);
263 get_script_name (pool pool, const char *canonical_path)
265 char *t = strrchr (canonical_path, '/');
266 return t ? pstrdup (pool, t+1) : canonical_path;
270 ml_entry_point (rws_request rq, void (*app_main) (ml_session))
272 pool thread_pool = pth_get_pool (current_pth);
273 io_handle io = rws_request_io (rq);
274 http_request http_request = rws_request_http_request (rq);
275 const char *host_header = rws_request_host_header (rq);
276 const char *canonical_path = rws_request_canonical_path (rq);
277 const char *sessionid;
280 int send_sessionid = 0;
281 http_response http_response;
283 const char *actionid, *windowid, *auth;
285 /* Look for old sessions and kill them. */
286 kill_old_sessions ();
288 /* Get the sessionid, if there is one. */
289 sessionid = get_sessionid_from_cookie (http_request);
291 /* Parse the CGI parameters, and extract the monolith-specific
292 * parameters. Note that these are parsed into the thread pool,
293 * not into the session pool (we don't even know yet if we have
294 * a session pool, so that's not an option).
296 cgi = new_cgi (thread_pool, http_request, io);
298 /* If ml_reset passed, begin a new session regardless. */
299 if (cgi_param (cgi, "ml_reset"))
301 /* But if there was an existing session, delete it now. */
303 kill_session (sessionid);
308 if (sessionid && shash_get (sessions, sessionid, session))
310 /* It's an existing, valid session. */
312 /* Acquire the lock before accessing any parts of the session
315 mutex_enter (session->lock);
317 /* Update the access time. */
318 session->last_access = reactor_time;
323 /* Get the current window, from the ml_window parameter. If there
324 * is no ml_window parameter (can happen when opening new windows),
325 * then set current window to NULL and expect that the action will
326 * set the current window.
328 session->current_window = session->main_window;
329 windowid = cgi_param (cgi, "ml_window");
331 ! shash_get (session->windows, windowid, session->current_window))
333 return bad_request_error (rq,
334 psprintf (thread_pool,
335 "invalid window ID: %s",
339 /* Set the rws_rq field to the current request. */
340 session->rws_rq = rq;
342 /* Set the current IO handle. */
345 /* Get ready for sending back auth cookie. */
346 session->auth_cookie = 0;
348 /* If the userid is set, check to see if there is a "poison" cookie.
349 * If so, then we log out the user.
351 if (session->userid != 0 &&
352 (auth = http_request_get_cookie (http_request, "ml_auth")) != 0
353 && strcmp (auth, "poison") == 0)
357 else if (session->userid == 0 &&
358 (auth = http_request_get_cookie (http_request, "ml_auth")) != 0)
360 session->userid = auth_to_userid (session, auth);
363 /* If the ml_action parameter is given, invoke the appropriate
366 actionid = cgi_param (cgi, "ml_action");
368 /* Save the submitted args, for forms. */
369 session->submitted_args = cgi;
372 run_action (session, actionid);
376 /* Start a new session. */
381 /* Create a pool for this session. */
382 session_pool = new_subpool (ml_pool);
384 /* Create some state for this session. */
385 session = pmalloc (session_pool, sizeof *session);
386 session->lock = new_mutex (session_pool);
388 session->last_access = session->created = reactor_time;
389 session->reap_min = SESSION_REAP_MIN;
390 session->reap_max = SESSION_REAP_MAX;
391 session->reap_inc = SESSION_REAP_INC;
392 session->session_pool = session_pool;
393 session->app_main = app_main;
394 session->current_window = 0;
395 session->main_window = 0;
396 session->windows = new_shash (session_pool, ml_window);
397 session->sessionid = sessionid = generate_sessionid (session_pool);
398 session->actions = new_shash (session_pool, struct action);
399 session->host_header = pstrdup (session_pool, host_header);
400 session->canonical_path = pstrdup (session_pool, canonical_path);
401 session->script_name =
402 get_script_name (session_pool, session->canonical_path);
404 /* Get the User-Agent header (if any). */
405 ua = http_request_get_header (http_request, "User-Agent");
406 session->user_agent = ua ? pstrdup (session_pool, ua) : 0;
408 /* Get the IP address of the first request. */
409 namelen = sizeof (session->original_ip);
410 getpeername (io_fileno (io),
411 (struct sockaddr *) &session->original_ip, &namelen);
413 /* Take the initial arguments and copy them into the
414 * session pool, dropping the private ml_* parameters.
416 session->args = copy_cgi (session_pool, cgi);
417 cgi_erase (session->args, "ml_reset");
418 cgi_erase (session->args, "ml_window");
419 cgi_erase (session->args, "ml_action");
421 /* Set the rws_rq field to the current request. */
422 session->rws_rq = rq;
424 /* Set the current IO handle. */
427 /* Initialize the list of database handles. */
428 session->dbhs = new_hash (session_pool, db_handle, pool);
430 /* See if there's an ml_auth cookie. If so, and it's valid, then
431 * we initialize the session->userid from this. Otherwise we
432 * set session->userid to 0.
434 if ((auth = http_request_get_cookie (http_request, "ml_auth")) != 0)
435 session->userid = auth_to_userid (session, auth);
439 /* Get ready for sending back auth cookie. */
440 session->auth_cookie = 0;
442 /* Remember to send back the ml_sessionid cookie. */
445 /* Save the session. */
446 shash_insert (sessions, sessionid, session);
448 /* Acquire the lock. (Actually we don't strictly need to do this
449 * until after we have sent the cookie, but it makes the code
452 mutex_enter (session->lock);
454 /* Run the "main" program. */
458 if (! session->current_window)
460 return bad_request_error (rq, "no current window");
463 /* Begin the response. */
464 _ml_window_notify_begin_response (session->current_window);
466 http_response = new_http_response
467 (thread_pool, http_request, io,
468 _ml_window_get_response_code (session->current_window),
469 _ml_window_get_response_name (session->current_window));
471 http_response_send_headers (http_response,
472 /* Send headers to defeat caching. */
474 /* End of headers. */
477 /* Send the session cookie if necessary. */
480 const char *cookie = psprintf (thread_pool,
481 "ml_sessionid=%s; path=%s",
485 http_response_send_header (http_response, "Set-Cookie", cookie);
486 _ml_window_set_cookie_with_javascript (session->current_window, cookie);
489 /* Send the auth cookie if necessary. */
490 if (session->auth_cookie)
492 const char *cookie = psprintf (thread_pool,
493 "ml_auth=%s; path=%s; expires=%s",
494 session->auth_cookie,
495 session->auth_cookie_path
497 session->auth_cookie_expires
500 http_response_send_header (http_response, "Set-Cookie", cookie);
501 _ml_window_set_cookie_with_javascript (session->current_window, cookie);
503 session->auth_cookie = 0;
506 /* Send any additional headers required by the current window. */
507 _ml_window_send_headers (session->current_window, thread_pool,
510 close = http_response_end_headers (http_response);
512 if (!http_request_is_HEAD (http_request))
514 /* Display the main window. */
515 _ml_window_repaint (session->current_window, session, io);
518 /* XXX We might need to recover database handles here, particularly
519 * if we implement chunked encoding, and we start to process many
520 * requests over the same connection.
523 /* Free the session lock. */
524 mutex_leave (session->lock);
531 * Killing a session is a non-trivial task, because we must make sure
532 * at all costs that no other thread is using the session structure, or
533 * might be waiting on the mutex to enter the session.
535 * The procedure is as follows. If any step fails, then we need to go
536 * back round to the top and try again. Eventually this function will
537 * delete the session.
539 * - Acquire the mutex.
540 * - Check if any other threads are waiting to enter the mutex.
541 * - If none, then remove the session from the sessions hash (this ensures
542 * that no other thread will try to use the session - particularly
543 * important if the session deletion is protracted and involves I/O).
544 * - Release the mutex (no other thread will try to acquire it).
545 * - Delete the session pool, which invokes any session finalisers.
548 kill_session (const char *sessionid)
552 /* Get the session structure. */
553 if (!shash_get (sessions, sessionid, session))
554 return; /* No such session - ignore it. */
557 /* Acquire the session lock. */
558 mutex_enter (session->lock);
560 /* Any other threads waiting to enter the mutex? */
561 if (mutex_nr_sleepers (session->lock) > 0)
563 /* Release the lock and try again later. */
564 mutex_leave (session->lock);
565 pth_millisleep (100); /* To be on the safe side ... */
569 /* Remove the session from the list of sessions. After this, no
570 * other threads can find or enter this session.
572 assert (shash_erase (sessions, sessionid));
574 /* Release the lock. */
575 mutex_leave (session->lock);
577 /* Finally, we can delete the thread. */
578 delete_pool (session->session_pool);
582 ml_session_pool (ml_session session)
584 return session->session_pool;
588 ml_session_args (ml_session session)
590 return session->args;
594 ml_session_sessionid (ml_session session)
596 return session->sessionid;
600 ml_session_host_header (ml_session session)
602 return session->host_header;
606 ml_session_canonical_path (ml_session session)
608 return session->canonical_path;
612 ml_session_script_name (ml_session session)
614 return session->script_name;
618 ml_session_user_agent (ml_session session)
620 return session->user_agent;
624 ml_session_set_main_window (ml_session session, ml_window win)
626 session->main_window = win;
630 ml_session_get_main_window (ml_session session)
632 return session->main_window;
636 ml_session_get_peername (ml_session session,
637 struct sockaddr *name, socklen_t *namelen)
641 s = io_fileno (session->io);
642 return getpeername (s, name, namelen);
646 ml_session_get_peernamestr (ml_session session)
648 struct sockaddr_in addr;
649 socklen_t len = sizeof (addr);
651 if (ml_session_get_peername (session, (struct sockaddr *) &addr, &len) == -1)
653 perror ("getpeername");
657 assert (addr.sin_family == AF_INET);
658 return pstrdup (session->session_pool, inet_ntoa (addr.sin_addr));
662 _ml_get_sessions (pool pool)
664 return shash_keys_in_pool (sessions, pool);
668 _ml_get_session (const char *sessionid)
672 if (shash_get (sessions, sessionid, session))
679 _ml_session_get_hits (ml_session session)
681 return session->hits;
685 _ml_session_get_last_access (ml_session session)
687 return session->last_access;
691 _ml_session_get_created (ml_session session)
693 return session->created;
697 _ml_session_get_original_ip (ml_session session)
699 return session->original_ip;
703 _ml_session_get_app_main (ml_session session)
705 return session->app_main;
709 _ml_session_get_windows (ml_session session, pool pool)
711 return shash_keys_in_pool (session->windows, pool);
715 _ml_session_get_window (ml_session session, const char *windowid)
719 if (shash_get (session->windows, windowid, win))
726 _ml_session_get_actions (ml_session session, pool pool)
728 return shash_keys_in_pool (session->actions, pool);
732 _ml_session_get_action (ml_session session, const char *actionid,
733 void **fn_rtn, void **data_rtn)
735 struct action action;
737 if (shash_get (session->actions, actionid, action))
739 *fn_rtn = action.callback_fn;
740 *data_rtn = action.data;
748 _ml_session_get_dbhs (ml_session session, pool pool)
750 return hash_keys_in_pool (session->dbhs, pool);
754 ml_session_release_lock (ml_session session)
756 mutex_leave (session->lock);
760 ml_session_acquire_lock (ml_session session)
762 mutex_enter (session->lock);
764 /* We've probably been sleeping for a while, so update the last access
765 * time to reflect this.
767 session->last_access = reactor_time;
771 _ml_session_submitted_args (ml_session session)
773 return session->submitted_args;
776 static void get_auth_dbf (ml_session);
777 static const char *parse_expires_header (pool pool, const char *expires);
780 ml_session_login (ml_session session, int userid,
781 const char *path, const char *expires)
783 pool thread_pool = pth_get_pool (current_pth);
788 /* Parse the expires header. */
789 expires = parse_expires_header (thread_pool, expires);
791 get_auth_dbf (session);
792 dbh = get_db_handle (auth_dbf, DBI_THROW_ERRORS);
794 /* Generate a suitable cookie and insert it into the database. */
795 cookie = generate_sessionid (thread_pool);
796 sth = st_prepare_cached
798 "delete from ml_user_cookie where userid = ?",
800 st_execute (sth, userid);
801 sth = st_prepare_cached
803 "insert into ml_user_cookie (userid, cookie) values (?, ?)",
804 DBI_INT, DBI_STRING);
805 st_execute (sth, userid, cookie);
809 /* User is logged in. */
810 session->userid = userid;
812 /* Remember to send back a cookie. */
813 session->auth_cookie = cookie;
814 session->auth_cookie_path = path;
815 session->auth_cookie_expires = expires;
819 ml_session_logout (ml_session session, const char *path)
821 pool thread_pool = pth_get_pool (current_pth);
824 int old_userid = session->userid;
827 /* Set the expires header. */
828 expires = parse_expires_header (thread_pool, "+1y");
830 /* If no one is actually logged in, do nothing. */
831 if (!old_userid) return;
833 get_auth_dbf (session);
834 dbh = get_db_handle (auth_dbf, DBI_THROW_ERRORS);
836 sth = st_prepare_cached
838 "delete from ml_user_cookie where userid = ?",
840 st_execute (sth, old_userid);
844 /* User is logged out. */
847 /* Remember to send back the poison cookie. */
848 session->auth_cookie = "poison";
849 session->auth_cookie_path = path;
850 session->auth_cookie_expires = expires;
853 /* Convert auth cookie to userid, if possible. If not valid, returns 0. */
855 auth_to_userid (ml_session session, const char *auth)
861 get_auth_dbf (session);
862 dbh = get_db_handle (auth_dbf, DBI_THROW_ERRORS);
864 sth = st_prepare_cached
866 "select userid from ml_user_cookie where cookie = ?",
868 st_execute (sth, auth);
870 st_bind (sth, 0, userid, DBI_INT);
872 fetched = st_fetch (sth);
883 get_auth_dbf (ml_session session)
885 /* Check monolith is configured to handle user authentication. */
888 auth_dbf = ml_cfg_get_string (session, "monolith user database", 0);
890 pth_die ("missing 'monolith user database' key "
891 "in the rws configuration file");
895 /* XXX We should share this code with rws. */
897 parse_expires_header (pool pool, const char *expires)
902 if (expires == 0) return 0;
904 /* Is it like "+1y"? */
905 if (sscanf (expires, "%c%d%c", &pm, &length, &unit) == 3 &&
906 (pm == '+' || pm == '-') &&
908 (unit == 's' || unit == 'm' || unit == 'h' ||
909 unit == 'd' || unit == 'y'))
921 case 's': t += length; break;
922 case 'm': t += length * 60; break;
923 case 'h': t += length * (60 * 60); break;
924 case 'd': t += length * (60 * 60 * 24); break;
925 case 'y': t += length * (60 * 60 * 24 * 366); break;
932 case 's': t -= length; break;
933 case 'm': t -= length * 60; break;
934 case 'h': t -= length * (60 * 60); break;
935 case 'd': t -= length * (60 * 60 * 24); break;
936 case 'y': t -= length * (60 * 60 * 24 * 366); break;
941 strftime (header, sizeof header, "%a, %d %b %Y %H:%M:%S GMT", tm);
943 return pstrdup (pool, header);
946 /* Otherwise, assume it's in RFC 2616 format. */
951 ml_session_userid (ml_session session)
953 return session->userid;
957 ml_cfg_get_string (ml_session session,
958 const char *key, const char *default_value)
960 return rws_request_cfg_get_string (session->rws_rq, key, default_value);
964 ml_cfg_get_int (ml_session session, const char *key, int default_value)
966 return rws_request_cfg_get_int (session->rws_rq, key, default_value);
970 ml_cfg_get_bool (ml_session session, const char *key, int default_value)
972 return rws_request_cfg_get_bool (session->rws_rq, key, default_value);
976 _ml_session_set_current_window (ml_session session, ml_window window,
977 const char *windowid)
979 shash_insert (session->windows, windowid, window);
980 session->current_window = window;
984 run_action (ml_session session, const char *action_id)
988 /* Ignore unknown action IDs. */
989 if (shash_get (session->actions, action_id, a))
990 a.callback_fn (session, a.data);
994 ml_register_action (ml_session session,
995 void (*callback_fn) (ml_session, void *data), void *data)
997 static int action_id = 1;
998 const char *action_str;
1001 action_str = pitoa (session->session_pool, action_id);
1004 a.callback_fn = callback_fn;
1007 shash_insert (session->actions, action_str, a);
1013 ml_unregister_action (ml_session session, const char *action_id)
1015 shash_erase (session->actions, action_id);
1018 #define CRLF "\015\012"
1021 bad_request_error (rws_request rq, const char *text)
1023 pool thread_pool = pth_get_pool (current_pth);
1024 io_handle io = rws_request_io (rq);
1025 http_request http_request = rws_request_http_request (rq);
1026 const char *canonical_path = rws_request_canonical_path (rq);
1027 http_response http_response;
1030 http_response = new_http_response (thread_pool, http_request, io,
1031 500, "Internal server error");
1032 http_response_send_headers (http_response,
1034 "Content-Type", "text/html",
1036 /* End of headers. */
1038 close = http_response_end_headers (http_response);
1040 if (http_request_is_HEAD (http_request)) return close;
1043 "<html><head><title>Internal server error</title></head>" CRLF
1044 "<body bgcolor=\"#ffffff\">" CRLF
1045 "<h1>500 Internal server error</h1>" CRLF
1046 "<p>There was an error serving this request: %s</p>" CRLF
1048 "<p><a href=\"%s?ml_reset=1\">Restart your session</a></p>" CRLF
1050 text, canonical_path);