context.c: last_purge is local to dead_session_purge()
[citadel.git] / citadel / server / context.c
1 // Citadel context management stuff.
2 // Here's where we (hopefully) have all the code that manipulates contexts.
3 //
4 // Copyright (c) 1987-2024 by the citadel.org team
5 //
6 // This program is open source software.  Use, duplication, or disclosure
7 // is subject to the terms of the GNU General Public License, version 3.
8
9 #include "ctdl_module.h"
10 #include "serv_extensions.h"
11 #include "citserver.h"
12 #include "user_ops.h"
13 #include "locate_host.h"
14 #include "context.h"
15 #include "control.h"
16 #include "config.h"
17
18 pthread_key_t MyConKey;                         // TSD key for MyContext()
19 CitContext masterCC;
20 CitContext *ContextList = NULL;
21 int num_sessions = 0;                           // Current number of sessions
22 int next_pid = 0;
23
24 // Flag for single user mode
25 static int want_single_user = 0;
26
27 // Try to go single user
28
29 int CtdlTrySingleUser(void) {
30         int can_do = 0;
31         
32         begin_critical_section(S_SINGLE_USER);
33         if (want_single_user) {
34                 can_do = 0;
35         }
36         else {
37                 can_do = 1;
38                 want_single_user = 1;
39         }
40         end_critical_section(S_SINGLE_USER);
41         return can_do;
42 }
43
44
45 void CtdlEndSingleUser(void) {
46         begin_critical_section(S_SINGLE_USER);
47         want_single_user = 0;
48         end_critical_section(S_SINGLE_USER);
49 }
50
51
52 int CtdlWantSingleUser(void) {
53         return want_single_user;
54 }
55
56
57 int CtdlIsSingleUser(void) {
58         if (want_single_user) {
59                 // check for only one context here
60                 if (num_sessions == 1)
61                         return 1;
62         }
63         return 0;
64 }
65
66
67 // Locate a context by its session number and terminate it if the user is able.
68 // User can NOT terminate their current session.
69 // User CAN terminate any other session that has them logged in.
70 // Aide CAN terminate any session except the current one.
71 int CtdlTerminateOtherSession (int session_num) {
72         int ret = 0;
73         CitContext *ccptr;
74         int aide;
75
76         if (session_num == CC->cs_pid) return TERM_NOTALLOWED;
77
78         aide = ( (CC->user.axlevel >= AxAideU) || (CC->internal_pgm) ) ;
79
80         syslog(LOG_DEBUG, "context: locating session to kill");
81         begin_critical_section(S_SESSION_TABLE);
82         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
83                 if (session_num == ccptr->cs_pid) {
84                         ret |= TERM_FOUND;
85                         if ((ccptr->user.usernum == CC->user.usernum) || aide) {
86                                 ret |= TERM_ALLOWED;
87                         }
88                         break;
89                 }
90         }
91
92         if (((ret & TERM_FOUND) != 0) && ((ret & TERM_ALLOWED) != 0)) {
93                 if (ccptr->user.usernum == CC->user.usernum) {
94                         ccptr->kill_me = KILLME_ADMIN_TERMINATE;
95                 }
96                 else {
97                         ccptr->kill_me = KILLME_IDLE;
98                 }
99                 end_critical_section(S_SESSION_TABLE);
100         }
101         else {
102                 end_critical_section(S_SESSION_TABLE);
103         }
104
105         return ret;
106 }
107
108
109 // Check to see if the user who we just sent mail to is logged in.  If yes,
110 // bump the 'new mail' counter for their session.  That enables them to
111 // receive a new mail notification without having to hit the database.
112 void CtdlBumpNewMailCounter(long which_user) {
113         CitContext *ptr;
114
115         begin_critical_section(S_SESSION_TABLE);
116
117         for (ptr = ContextList; ptr != NULL; ptr = ptr->next) {
118                 if (ptr->user.usernum == which_user) {
119                         ptr->newmail += 1;
120                 }
121         }
122
123         end_critical_section(S_SESSION_TABLE);
124 }
125
126
127 // Check to see if a user is currently logged in
128 // Take care with what you do as a result of this test.
129 // The user may not have been logged in when this function was called BUT
130 // because of threading the user might be logged in before you test the result.
131 int CtdlIsUserLoggedIn(char *user_name) {
132         CitContext *cptr;
133         int ret = 0;
134
135         begin_critical_section (S_SESSION_TABLE);
136         for (cptr = ContextList; cptr != NULL; cptr = cptr->next) {
137                 if (!strcasecmp(cptr->user.fullname, user_name)) {
138                         ret = 1;
139                         break;
140                 }
141         }
142         end_critical_section(S_SESSION_TABLE);
143         return ret;
144 }
145
146
147 // Check to see if a user is currently logged in.
148 // Basically same as CtdlIsUserLoggedIn() but uses the user number instead.
149 // Take care with what you do as a result of this test.
150 // The user may not have been logged in when this function was called BUT
151 // because of threading the user might be logged in before you test the result.
152 int CtdlIsUserLoggedInByNum (long usernum) {
153         CitContext *cptr;
154         int ret = 0;
155
156         begin_critical_section(S_SESSION_TABLE);
157         for (cptr = ContextList; cptr != NULL; cptr = cptr->next) {
158                 if (cptr->user.usernum == usernum) {
159                         ret = 1;
160                 }
161         }
162         end_critical_section(S_SESSION_TABLE);
163         return ret;
164 }
165
166
167 // Return a pointer to the CitContext structure bound to the thread which
168 // called this function.  If there's no such binding (for example, if it's
169 // called by the housekeeper thread) then a generic 'master' CC is returned.
170 //
171 // This function is used *VERY* frequently and must be kept small.
172 CitContext *MyContext(void) {
173         register CitContext *c;
174         return ((c = (CitContext *) pthread_getspecific(MyConKey), c == NULL) ? &masterCC : c);
175 }
176
177
178 // Terminate idle sessions.  This function pounds through the session table
179 // comparing the current time to each session's time-of-last-command.  If an
180 // idle session is found it is terminated, then the search restarts at the
181 // beginning because the pointer to our place in the list becomes invalid.
182 void terminate_idle_sessions(void) {
183         CitContext *ccptr;
184         time_t now;
185         int killed = 0;
186
187         now = time(NULL);
188         begin_critical_section(S_SESSION_TABLE);
189         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
190                 if (
191                         (ccptr != CC)
192                         && (CtdlGetConfigLong("c_sleeping") > 0)
193                         && (now - (ccptr->lastcmd) > CtdlGetConfigLong("c_sleeping"))
194                 ) {
195                         ccptr->kill_me = KILLME_IDLE;
196                         ++killed;
197                 }
198         }
199         end_critical_section(S_SESSION_TABLE);
200         if (killed > 0) {
201                 syslog(LOG_INFO, "context: scheduled %d idle sessions for termination", killed);
202         }
203 }
204
205
206 // During shutdown, close the sockets of any sessions still connected.
207 void terminate_all_sessions(void) {
208         CitContext *ccptr;
209         int killed = 0;
210
211         begin_critical_section(S_SESSION_TABLE);
212         for (ccptr = ContextList; ccptr != NULL; ccptr = ccptr->next) {
213                 if (ccptr->client_socket != -1) {
214                         syslog(LOG_INFO, "context: terminate_all_sessions() is murdering %s CC[%d]", ccptr->curr_user, ccptr->cs_pid);
215                         close(ccptr->client_socket);
216                         ccptr->client_socket = -1;
217                         killed++;
218                 }
219         }
220         end_critical_section(S_SESSION_TABLE);
221         if (killed > 0) {
222                 syslog(LOG_INFO, "context: flushed %d stuck sessions", killed);
223         }
224 }
225
226
227 // Terminate a session.
228 void RemoveContext(CitContext *con) {
229         const char *c;
230         if (con == NULL) {
231                 syslog(LOG_ERR, "context: RemoveContext() called with NULL, this should not happen");
232                 return;
233         }
234         c = con->ServiceName;
235         if (c == NULL) {
236                 c = "(unknown)";
237         }
238         syslog(LOG_DEBUG, "context: RemoveContext(%s) session %d", c, con->cs_pid);
239
240         // Run any cleanup routines registered by loadable modules.
241         // Note: We have to "become_session()" because the cleanup functions might make references to "CC" assuming it's the right one.
242         become_session(con);
243         CtdlUserLogout();
244         PerformSessionHooks(EVT_STOP);          // hooks may free some data structures, close SSL, etc.
245         client_close();                         // If the client is still connected, disconnect them immediately.
246         become_session(NULL);
247         syslog(LOG_INFO, "context: session %d (%s) ended.", con->cs_pid, c);
248
249         // If using AUTHMODE_LDAP, free the DN
250         if (con->ldap_dn) {
251                 free(con->ldap_dn);
252                 con->ldap_dn = NULL;
253         }
254         FreeStrBuf(&con->StatusMessage);
255         FreeStrBuf(&con->MigrateBuf);
256         FreeStrBuf(&con->RecvBuf.Buf);
257         if (con->cached_msglist) {
258                 free(con->cached_msglist);
259         }
260
261         syslog(LOG_DEBUG, "context: done with RemoveContext()");
262 }
263
264
265 // Initialize a new context and place it in the list.  The session number
266 // used to be the PID (which is why it's called cs_pid), but that was when we
267 // had one process per session.  Now we just assign them sequentially, starting
268 // at 1 (don't change it to 0 because masterCC uses 0).
269 CitContext *CreateNewContext(void) {
270         CitContext *me;
271
272         me = (CitContext *) malloc(sizeof(CitContext));
273         if (me == NULL) {
274                 syslog(LOG_ERR, "citserver: malloc() failed: %m");
275                 return NULL;
276         }
277         memset(me, 0, sizeof(CitContext));
278
279         // Give the context a name. Hopefully makes it easier to track
280         strcpy (me->user.fullname, "SYS_notauth");
281         
282         me->state = CON_EXECUTING;      // Create new context already in CON_EXECUTING so another thread doesn't grab it.
283         me->MigrateBuf = NewStrBuf();   // Generate a unique session number
284         me->RecvBuf.Buf = NewStrBuf();
285         me->lastcmd = time(NULL);       // set lastcmd to now to prevent idle timer infanticide
286
287         begin_critical_section(S_SESSION_TABLE);
288         me->cs_pid = ++next_pid;
289         me->prev = NULL;
290         me->next = ContextList;
291         ContextList = me;
292         if (me->next != NULL) {
293                 me->next->prev = me;
294         }
295         ++num_sessions;
296         end_critical_section(S_SESSION_TABLE);
297         return (me);
298 }
299
300
301 // Initialize a new context and place it in the list.  The session number
302 // used to be the PID (which is why it's called cs_pid), but that was when we
303 // had one process per session.  Now we just assign them sequentially, starting
304 // at 1 (don't change it to 0 because masterCC uses 0).
305 CitContext *CloneContext(CitContext *CloneMe) {
306         CitContext *me;
307
308         me = (CitContext *) malloc(sizeof(CitContext));
309         if (me == NULL) {
310                 syslog(LOG_ERR, "citserver: malloc() failed: %m");
311                 return NULL;
312         }
313         memcpy(me, CloneMe, sizeof(CitContext));
314
315         memset(&me->RecvBuf, 0, sizeof(IOBuffer));
316         memset(&me->SendBuf, 0, sizeof(IOBuffer));
317         memset(&me->SBuf, 0, sizeof(IOBuffer));
318         me->MigrateBuf = NULL;
319         me->sMigrateBuf = NULL;
320         me->redirect_buffer = NULL;
321 #ifdef HAVE_OPENSSL
322         me->ssl = NULL;
323 #endif
324
325         me->download_fp = NULL;
326         me->upload_fp = NULL;
327         me->ma = NULL;
328         me->ldap_dn = NULL;
329         me->session_specific_data = NULL;
330         
331         me->CIT_ICAL = NULL;
332
333         me->cached_msglist = NULL;
334         me->download_fp = NULL;
335         me->upload_fp = NULL;
336         me->client_socket = 0;
337
338         me->MigrateBuf = NewStrBuf();
339         me->RecvBuf.Buf = NewStrBuf();
340         
341         begin_critical_section(S_SESSION_TABLE);
342
343         me->cs_pid = ++next_pid;
344         me->prev = NULL;
345         me->next = ContextList;
346         me->lastcmd = time(NULL);       // set lastcmd to now to prevent idle timer infanticide
347         ContextList = me;
348         if (me->next != NULL) {
349                 me->next->prev = me;
350         }
351         ++num_sessions;
352
353         end_critical_section(S_SESSION_TABLE);
354         return (me);
355 }
356
357
358 // Return an array containing a copy of the context list.
359 // This allows worker threads to perform "for each context" operations without
360 // having to lock and traverse the live list.
361 CitContext *CtdlGetContextArray(int *count) {
362         int nContexts, i;
363         CitContext *nptr, *cptr;
364         
365         nContexts = num_sessions;
366         nptr = malloc(sizeof(CitContext) * nContexts);
367         if (!nptr) {
368                 *count = 0;
369                 return NULL;
370         }
371         begin_critical_section(S_SESSION_TABLE);
372         for (cptr = ContextList, i=0; cptr != NULL && i < nContexts; cptr = cptr->next, i++) {
373                 memcpy(&nptr[i], cptr, sizeof (CitContext));
374         }
375         end_critical_section (S_SESSION_TABLE);
376         
377         *count = i;
378         return nptr;
379 }
380
381
382 // Back-end function for starting a session
383 void begin_session(CitContext *con) {
384
385         // Initialize some variables specific to our context.
386         con->logged_in = 0;
387         con->internal_pgm = 0;
388         con->download_fp = NULL;
389         con->upload_fp = NULL;
390         con->cached_msglist = NULL;
391         con->cached_num_msgs = 0;
392         con->FirstExpressMessage = NULL;
393         time(&con->lastcmd);
394         time(&con->lastidle);
395         strcpy(con->lastcmdname, "    ");
396         strcpy(con->cs_clientname, "(unknown)");
397         strcpy(con->curr_user, NLI);
398         *con->cs_clientinfo = '\0';
399         safestrncpy(con->cs_host, CtdlGetConfigStr("c_fqdn"), sizeof con->cs_host);
400         safestrncpy(con->cs_addr, "", sizeof con->cs_addr);
401         con->cs_UDSclientUID = -1;
402         con->cs_host[sizeof con->cs_host - 1] = 0;
403         if (!CC->is_local_client) {
404                 locate_host(con->cs_host, sizeof con->cs_host,
405                         con->cs_addr, sizeof con->cs_addr,
406                         con->client_socket
407                 );
408         }
409         else {
410                 con->cs_host[0] = 0;
411                 con->cs_addr[0] = 0;
412         }
413         con->cs_flags = 0;
414         con->nologin = 0;
415
416         if (((CtdlGetConfigInt("c_maxsessions") > 0)&&(num_sessions > CtdlGetConfigInt("c_maxsessions"))) || CtdlWantSingleUser()) {
417                 con->nologin = 1;
418         }
419
420         syslog(LOG_INFO, "context: session (%s) started from %s (%s) uid=%d",
421                 con->ServiceName, con->cs_host, con->cs_addr, con->cs_UDSclientUID
422         );
423
424         // Run any session startup routines registered by loadable modules
425         PerformSessionHooks(EVT_START);
426 }
427
428
429 // This function fills in a context and its user field correctly
430 // Then creates/loads that user
431 void CtdlFillSystemContext(CitContext *context, char *name) {
432         char sysname[SIZ];
433         long len;
434
435         memset(context, 0, sizeof(CitContext));
436         context->internal_pgm = 1;
437         context->cs_pid = 0;
438         strcpy (sysname, "SYS_");
439         strcat (sysname, name);
440         len = strlen(sysname);
441         memcpy(context->curr_user, sysname, len + 1);
442         context->client_socket = (-1);
443         context->state = CON_SYS;
444         context->ServiceName = name;
445
446         // internal_create_user has the side effect of loading the user regardless of whether they
447         // already existed or needed to be created
448         internal_create_user(sysname, &(context->user), -1) ;
449         
450         // Check to see if the system user needs upgrading
451         if (context->user.usernum == 0) {       // old system user with number 0, upgrade it
452                 context->user.usernum = get_new_user_number();
453                 syslog(LOG_INFO, "context: upgrading system user \"%s\" from user number 0 to user number %ld", context->user.fullname, context->user.usernum);
454                 // add user to the database
455                 CtdlPutUser(&(context->user));
456                 cdb_store(CDB_USERSBYNUMBER, &(context->user.usernum), sizeof(long), context->user.fullname, strlen(context->user.fullname)+1);
457         }
458 }
459
460
461 // Cleanup any contexts that are left lying around
462 void context_cleanup(void) {
463         CitContext *ptr = NULL;
464         CitContext *rem = NULL;
465
466         // Clean up the contexts.
467         // There are no threads so no critical_section stuff is needed.
468         ptr = ContextList;
469         
470         // We need to update the ContextList because some modules may want to iterate it
471         // Question is should we NULL it before iterating here or should we just keep updating it as we remove items?
472         // Answer is to NULL it first to prevent modules from doing any actions on the list at all.
473         ContextList=NULL;
474         while (ptr != NULL){
475                 // Remove the session from the active list
476                 rem = ptr->next;
477                 --num_sessions;
478
479                 syslog(LOG_DEBUG, "context: context_cleanup() purging session %d", ptr->cs_pid);
480                 RemoveContext(ptr);
481                 free (ptr);
482                 ptr = rem;
483         }
484 }
485
486
487 // Purge all sessions which have the 'kill_me' flag set.
488 // This function has code to prevent it from running more than once every
489 // few seconds, because running it after every single unbind would waste a lot
490 // of CPU time and keep the context list locked too much.  To force it to run
491 // anyway, set "force" to nonzero.
492 void dead_session_purge(int force) {
493         CitContext *ptr, *ptr2;         // general-purpose utility pointer
494         CitContext *rem = NULL;         // list of sessions to be destroyed
495         static time_t last_purge = 0;   // Last dead session purge
496
497         if (force == 0) {
498                 if ( (time(NULL) - last_purge) < 5 ) {
499                         return;         // Too soon, go away
500                 }
501         }
502         time(&last_purge);
503
504         begin_critical_section(S_SESSION_TABLE);
505         ptr = ContextList;
506         while (ptr) {
507                 ptr2 = ptr;
508                 ptr = ptr->next;
509                 
510                 if ( (ptr2->state == CON_IDLE) && (ptr2->kill_me) ) {
511                         // Remove the session from the active list
512                         if (ptr2->prev) {
513                                 ptr2->prev->next = ptr2->next;
514                         }
515                         else {
516                                 ContextList = ptr2->next;
517                         }
518                         if (ptr2->next) {
519                                 ptr2->next->prev = ptr2->prev;
520                         }
521
522                         --num_sessions;
523                         // And put it on our to-be-destroyed list
524                         ptr2->next = rem;
525                         rem = ptr2;
526                 }
527                 //else if (ptr2->kill_me) {
528                         // 2023: this was a source of segfaults but I think we fixed it
529                         //syslog(LOG_DEBUG, "context: session %d is timed out but non-idle", ptr->cs_pid);
530                 //}
531         }
532         end_critical_section(S_SESSION_TABLE);
533
534         // Now that we no longer have the session list locked, we can take
535         // our time and destroy any sessions on the to-be-killed list, which
536         // is allocated privately on this thread's stack.
537         while (rem != NULL) {
538                 syslog(LOG_DEBUG, "context: dead_session_purge() purging session %d, reason=%d", rem->cs_pid, rem->kill_me);
539                 RemoveContext(rem);
540                 ptr = rem;
541                 rem = rem->next;
542                 free(ptr);
543         }
544 }
545
546
547 // masterCC is the context we use when not attached to a session.  This function initializes it.
548 void InitializeMasterCC(void) {
549         memset(&masterCC, 0, sizeof(struct CitContext));
550         masterCC.internal_pgm = 1;
551         masterCC.cs_pid = 0;
552 }
553
554
555 // Set the "async waiting" flag for a session, if applicable
556 void set_async_waiting(struct CitContext *ccptr) {
557         syslog(LOG_DEBUG, "context: setting async_waiting flag for session %d", ccptr->cs_pid);
558         if (ccptr->is_async) {
559                 ccptr->async_waiting++;
560                 if (ccptr->state == CON_IDLE) {
561                         ccptr->state = CON_READY;
562                 }
563         }
564 }
565
566
567 CTDL_MODULE_INIT(session) {
568         return "session";
569 }