AmendHub

Download

jcs

/

subtext

/

session.c

 

(View History)

jcs   chat: Implement private messaging Latest amendment: 290 on 2022-11-19

1 /*
2 * Copyright (c) 2021 joshua stein <jcs@jcs.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include <stdarg.h>
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21
22 #include "ansi.h"
23 #include "board.h"
24 #include "chat.h"
25 #include "folder.h"
26 #include "logger.h"
27 #include "mail.h"
28 #include "main_menu.h"
29 #include "subtext.h"
30 #include "session.h"
31 #include "settings.h"
32 #include "signup.h"
33 #include "sysop.h"
34 #include "user.h"
35 #include "user_settings.h"
36 #include "uthread.h"
37 #include "util.h"
38
39 #include "console.h"
40
41 struct session *sessions[MAX_SESSIONS] = { 0 };
42 short nsessions = 0;
43
44 struct session_tally session_today_tally = { 0 };
45 static char session_log_tbuf[256];
46 static const char invalid_option_help[] =
47 "Invalid option (press {{B}}?{{/B}} for help)\r\n";
48
49 void session_run(struct uthread *uthread, void *arg);
50 void session_print_menu(struct session *session, bool short_menu);
51 short session_login(struct session *s);
52 size_t session_log(struct session *session, const char *str);
53 size_t session_vprintf(struct session *session, const char *format,
54 va_list ap);
55 size_t session_expand_var(struct session *session, char *ivar, char **ret,
56 bool *end_expansion);
57 void session_page_sysop(struct session *s);
58 void session_answer_page(struct session *s);
59 void sysop_edit_settings(struct session *s);
60 bool session_idled_out(struct session *session);
61
62 struct session *
63 session_create(char *node, char *via, struct node_funcs *node_funcs)
64 {
65 struct session *session;
66 size_t n;
67
68 /* reserve 1 session for the console */
69 if (nsessions >= MAX_SESSIONS - 1) {
70 if (strcmp(node, "console") != 0)
71 return NULL;
72
73 /* but only 1 */
74 if (nsessions == MAX_SESSIONS)
75 return NULL;
76 }
77
78 nsessions++;
79
80 session = xmalloczero(sizeof(struct session), "session_create");
81 session->uthread = uthread_add(session_run, session);
82 if (!session->uthread) {
83 xfree(&session);
84 return NULL;
85 }
86
87 session->node_funcs = node_funcs;
88 strlcpy(session->node, node, sizeof(session->node));
89 strlcpy(session->log.node, node, sizeof(session->log.node));
90 strlcpy(session->via, via, sizeof(session->via));
91 session->last_input_at = session->established_at = Time;
92
93 for (n = 0; n < MAX_SESSIONS; n++) {
94 if (sessions[n] == NULL) {
95 sessions[n] = session;
96 return session;
97 }
98 }
99
100 panic("session_create failed to find slot for new session (%d)",
101 nsessions);
102 }
103
104 void
105 session_run(struct uthread *uthread, void *arg)
106 {
107 struct session *s = (struct session *)arg;
108 char date[9];
109 struct tm *date_tm;
110 unsigned short c, last_c = 0;
111 bool done = false;
112 short auth, i, action;
113
114 /* until we negotiate otherwise */
115 s->terminal_columns = DEFAULT_TERMINAL_COLUMNS;
116 s->terminal_lines = DEFAULT_TERMINAL_LINES;
117
118 if (main_menu_options == NULL)
119 panic("No main menu!");
120
121 if (s->node_funcs->setup) {
122 s->node_funcs->setup(s);
123
124 if (s->ending) {
125 session_close(s);
126 return;
127 }
128 }
129
130 session_output_view_or_printf(s, DB_TEXT_ISSUE_ID,
131 "\r\nWelcome to %s (%s)\r\n\r\n", db->config.name, s->node);
132 session_flush(s);
133 uthread_yield();
134
135 auth = session_login(s);
136 if (auth == AUTH_USER_FAILED) {
137 session_close(s);
138 return;
139 }
140
141 s->logged_in = true;
142 logger_update_title();
143
144 /* update session log */
145 if (s->user) {
146 s->user->last_seen_at = Time;
147 user_save(s->user);
148
149 strlcpy(s->log.username, s->user->username,
150 sizeof(s->log.username));
151 strlcpy(s->chat_username, s->user->username,
152 sizeof(s->chat_username));
153 } else {
154 strlcpy(s->log.username, GUEST_USERNAME, sizeof(s->log.username));
155 /* guest + ttyt0 -> guest[t0] */
156 snprintf(s->chat_username, sizeof(s->chat_username), "%s[%s]",
157 GUEST_USERNAME, s->node + 3);
158 }
159
160 strlcpy(s->log.via, s->via, sizeof(s->log.via));
161 s->log.tspeed = s->tspeed;
162 s->log.logged_on_at = Time;
163 s->log.id = bile_next_id(db->sessions_bile, SL_LOG_RTYPE);
164
165 if (bile_write(db->sessions_bile, SL_LOG_RTYPE, s->log.id, &s->log,
166 sizeof(s->log)) != sizeof(s->log))
167 panic("bile_write of session log failed: %d",
168 bile_error(db->sessions_bile));
169
170 /* update call tally for today */
171 date_tm = localtime((time_t *)&Time);
172 strftime(date, sizeof(date), "%Y%m%d", date_tm);
173 if (strcmp(date, session_today_tally.date) != 0) {
174 strlcpy(session_today_tally.date, date,
175 sizeof(session_today_tally.date));
176 session_today_tally.calls = 0;
177 }
178 session_today_tally.calls++;
179
180 if (s->terminal_type[0] == '\0') {
181 /* per-node setup didn't fill this in, ask the user */
182 session_output(s, "\r\n", 2);
183 user_settings_renegotiate(s);
184 session_output(s, "\r\n", 2);
185 session_flush(s);
186 }
187
188 session_printf(s, "\r\n"
189 "Welcome, {{B}}%s{{/B}}, you are the %ld%s caller today.\r\n",
190 s->user ? s->user->username : GUEST_USERNAME,
191 session_today_tally.calls, ordinal(session_today_tally.calls));
192 session_flush(s);
193 uthread_yield();
194
195 session_print_motd(s, false);
196
197 main_menu:
198 session_output(s, "\r\n", 2);
199 session_print_menu(s, false);
200 session_flush(s);
201
202 while (!done && !s->ending) {
203 session_printf(s, "{{B}}Main Menu>{{/B}} ");
204 session_flush(s);
205
206 get_another_char:
207 action = ACTION_NONE;
208
209 if (auth == AUTH_USER_SIGNUP) {
210 action = ACTION_SIGNUP;
211 auth = AUTH_USER_GUEST;
212 } else
213 c = session_input_char(s);
214
215 if (s->ending)
216 break;
217 if (c == '\r' || c == 0 || c > 255)
218 goto get_another_char;
219
220 session_printf(s, "%c\r\n", c);
221 session_flush(s);
222
223 if (action == ACTION_NONE) {
224 /* check each menu option's all_keys array for matching key */
225 for (i = 0; ; i++) {
226 struct main_menu_option *option = &main_menu_options[i];
227 short j;
228
229 if (option->action == ACTION_NONE)
230 break;
231
232 for (j = 0; option->all_keys[j] != '\0'; j++) {
233 if (option->all_keys[j] == c) {
234 action = option->action;
235 break;
236 }
237 }
238
239 if (action != ACTION_NONE)
240 break;
241 }
242 }
243
244 /* change actions for conditionals */
245 if (action == ACTION_SETTINGS_OR_SIGNUP) {
246 if (s->user)
247 action = ACTION_SETTINGS;
248 else
249 action = ACTION_SIGNUP;
250 } else if (action == ACTION_PAGE_SEND_OR_ANSWER) {
251 if (s->user && s->user->is_sysop)
252 action = ACTION_PAGE_ANSWER;
253 else
254 action = ACTION_PAGE_SEND;
255 }
256
257 switch (action) {
258 case ACTION_BOARD_SHOW_FIRST:
259 board_show(s, 1);
260 break;
261 case ACTION_CHAT:
262 chat_start(s, NULL);
263 break;
264 case ACTION_FILES_MENU:
265 folder_list(s);
266 break;
267 case ACTION_GOODBYE:
268 session_output_view_or_printf(s, DB_TEXT_SIGNOFF_ID,
269 "Goodbye!\r\n");
270 session_flush(s);
271 done = true;
272 break;
273 case ACTION_RECENT_LOGINS:
274 session_recents(s);
275 break;
276 case ACTION_MAIL_MENU:
277 mail_menu(s);
278 break;
279 case ACTION_MAIL_COMPOSE:
280 mail_compose(s, NULL, NULL, NULL);
281 break;
282 case ACTION_MOTD:
283 session_print_motd(s, true);
284 break;
285 case ACTION_PAGE_ANSWER:
286 if (!s->user || !s->user->is_sysop) {
287 session_printf(s, invalid_option_help);
288 session_flush(s);
289 break;
290 }
291 session_answer_page(s);
292 break;
293 case ACTION_PAGE_SEND:
294 session_page_sysop(s);
295 break;
296 case ACTION_SETTINGS:
297 if (s->user)
298 user_settings_menu(s);
299 break;
300 case ACTION_SIGNUP:
301 if ((s->user = signup(s))) {
302 session_printf(s, "\r\n"
303 "Welcome, {{B}}%s{{/B}}!\r\n", s->user->username);
304 goto main_menu;
305 }
306 break;
307 case ACTION_WHOS_ONLINE:
308 session_who(s);
309 break;
310 case ACTION_BOARD_SHOW_1:
311 board_show(s, 1);
312 break;
313 case ACTION_BOARD_SHOW_2:
314 board_show(s, 2);
315 break;
316 case ACTION_BOARD_SHOW_3:
317 board_show(s, 3);
318 break;
319 case ACTION_BOARD_SHOW_4:
320 board_show(s, 4);
321 break;
322 case ACTION_BOARD_SHOW_5:
323 board_show(s, 5);
324 break;
325 case ACTION_BOARD_SHOW_6:
326 board_show(s, 6);
327 break;
328 case ACTION_BOARD_SHOW_7:
329 board_show(s, 7);
330 break;
331 case ACTION_BOARD_SHOW_8:
332 board_show(s, 8);
333 break;
334 case ACTION_BOARD_SHOW_9:
335 board_show(s, 9);
336 break;
337 case ACTION_BOARD_SHOW_10:
338 board_show(s, 10);
339 break;
340 case ACTION_SYSOP_MENU:
341 if (!s->user || !s->user->is_sysop) {
342 session_printf(s, invalid_option_help);
343 session_flush(s);
344 break;
345 }
346 sysop_menu(s);
347 break;
348 case ACTION_SHOW_MENU:
349 if (last_c == c)
350 /* asking twice in a row will print the full menu */
351 session_print_menu(s, false);
352 else
353 session_print_menu(s, true);
354 session_flush(s);
355 break;
356 default:
357 session_printf(s, invalid_option_help);
358 session_flush(s);
359 break;
360 }
361
362 last_c = c;
363 }
364
365 session_close(s);
366 }
367
368 void
369 session_print_menu(struct session *session, bool short_menu)
370 {
371 struct main_menu_option *option = NULL;
372 size_t size;
373 short i, j;
374 char *output = NULL;
375
376 if (short_menu && session_output_view_or_printf(session,
377 DB_TEXT_SHORTMENU_ID, NULL) != 0)
378 return;
379
380 if (session_output_view_or_printf(session, DB_TEXT_MENU_ID, NULL) != 0)
381 return;
382
383 for (i = 0; ; i++) {
384 option = &main_menu_options[i];
385
386 if (option->action == ACTION_NONE)
387 break;
388
389 if (option->menu_key == 0 || option->label == NULL)
390 continue;
391
392 size = session_expand_template(session, option->label, &output);
393 if (!size)
394 continue;
395
396 session_printf(session, "{{B}}%c{{/B}}: %s\r\n", option->menu_key,
397 output);
398 xfree(&output);
399 }
400 }
401
402 void
403 session_close(struct session *session)
404 {
405 unsigned long now;
406 short n;
407 bool found;
408
409 if (!session->ending) {
410 if (!session->ban_node_source) {
411 /* give 1 second to flush output */
412 now = Ticks;
413 while (session->obuflen && (Ticks - now) < 60) {
414 session->node_funcs->output(session);
415 uthread_yield();
416 }
417 }
418 session->ending = true;
419 }
420
421 /* finalize session log */
422 session->log.logged_off_at = Time;
423
424 if (bile_write(db->sessions_bile, SL_LOG_RTYPE, session->log.id,
425 &session->log, sizeof(session->log)) != sizeof(session->log))
426 panic("bile_write of session log failed: %d",
427 bile_error(db->sessions_bile));
428
429 /* close the node */
430 session->node_funcs->close(session);
431
432 /* remove session from sessions */
433 found = false;
434 for (n = 0; n < MAX_SESSIONS; n++) {
435 if (sessions[n] == session) {
436 sessions[n] = NULL;
437 found = true;
438 break;
439 }
440 }
441
442 if (!found)
443 panic("session_close failed to find session to remove");
444
445 nsessions--;
446 if (session->user)
447 xfree(&session->user);
448 xfree(&session);
449
450 logger_update_title();
451 }
452
453 void
454 session_flush(struct session *session)
455 {
456 if (session->ending || session->obuflen == 0)
457 return;
458
459 while (session->obuflen != 0) {
460 session->node_funcs->output(session);
461 if (session->obuflen != 0)
462 uthread_yield();
463 if (session->ending)
464 return;
465
466 /*
467 * The console will leave \e in the output buffer if it's the only
468 * thing in there, so don't loop forever.
469 */
470 if (session->obuflen == 1 && session->obuf[0] == '\33')
471 return;
472 }
473 }
474
475 size_t
476 session_log(struct session *session, const char *str)
477 {
478 return logger_printf("[%s] [%s] %s", session->node,
479 session->logged_in ?
480 (session->user ? session->user->username : "guest") : "-",
481 str);
482 }
483
484 size_t
485 session_logf(struct session *session, const char *format, ...)
486 {
487 va_list ap;
488
489 va_start(ap, format);
490 vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap);
491 va_end(ap);
492
493 return session_log(session, session_log_tbuf);
494 }
495
496 size_t
497 session_logf_buffered(struct session *session, const char *format, ...)
498 {
499 va_list ap;
500 size_t ret;
501
502 va_start(ap, format);
503 vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap);
504 va_end(ap);
505
506 logger->autoflush = false;
507 ret = session_log(session, session_log_tbuf);
508 logger->autoflush = true;
509
510 return ret;
511 }
512
513 size_t
514 session_output(struct session *session, const char *str, size_t len)
515 {
516 size_t chunk, olen = len, stroff = 0;
517
518 while (len && !session->ending) {
519 chunk = (sizeof(session->obuf) - session->obuflen);
520 if (chunk == 0) {
521 session_flush(session);
522 if (session->ending)
523 return 0;
524 continue;
525 }
526 if (chunk > len)
527 chunk = len;
528
529 memcpy(session->obuf + session->obuflen, str + stroff, chunk);
530 session->obuflen += chunk;
531 stroff += chunk;
532 len -= chunk;
533 }
534
535 return olen;
536 }
537
538 size_t
539 session_printf(struct session *session, const char *format, ...)
540 {
541 va_list ap;
542 size_t len;
543
544 va_start(ap, format);
545 len = session_vprintf(session, format, ap);
546 va_end(ap);
547
548 return len;
549 }
550
551 size_t
552 session_vprintf(struct session *session, const char *format, va_list ap)
553 {
554 static char session_printf_ebuf[160], session_printf_tbuf[160];
555 size_t len, n, en;
556 bool stop = false;
557
558 /* avoid a full session_expand_template for {{B}}, {{/B}}, and {{#}} */
559 session_printf_ebuf[0] = '\0';
560 for (n = 0, en = 0; format[n] != '\0'; n++) {
561 if (!stop) {
562 if (format[n] == '{' && format[n + 1] == '{' &&
563 format[n + 2] == 'B' && format[n + 3] == '}' &&
564 format[n + 4] == '}') {
565 en = strlcat(session_printf_ebuf, ansi_bold(session),
566 sizeof(session_printf_ebuf));
567 n += 4;
568 continue;
569 }
570 if (format[n] == '{' &&
571 format[n + 1] == '{' && format[n + 2] == '/' &&
572 format[n + 3] == 'B' && format[n + 4] == '}' &&
573 format[n + 5] == '}') {
574 en = strlcat(session_printf_ebuf, ansi_reset(session),
575 sizeof(session_printf_ebuf));
576 n += 5;
577 continue;
578 }
579 if (format[n] == '{' &&
580 format[n + 1] == '{' && format[n + 2] == '#' &&
581 format[n + 3] == '}' && format[n + 4] == '}') {
582 stop = true;
583 n += 4;
584 continue;
585 }
586 }
587
588 session_printf_ebuf[en++] = format[n];
589
590 if (en >= sizeof(session_printf_ebuf))
591 panic("session_printf_ebuf overflow!");
592 session_printf_ebuf[en] = '\0';
593 }
594
595 len = vsnprintf(session_printf_tbuf, sizeof(session_printf_tbuf),
596 session_printf_ebuf, ap);
597
598 if (len > sizeof(session_printf_tbuf))
599 panic("session_printf overflow! (%ld > %ld)", len,
600 sizeof(session_printf_tbuf));
601
602 return session_output(session, session_printf_tbuf, len);
603 }
604
605 size_t
606 session_output_view_or_printf(struct session *session, short id,
607 const char *format, ...)
608 {
609 size_t size;
610 struct bile_object *o;
611 char *view, *output;
612 va_list ap;
613
614 /* can't use bile_read_alloc because we need to null terminate */
615 o = bile_find(db->bile, DB_TEXT_TYPE, id);
616 if (o == NULL || o->size == 0) {
617 if (o)
618 xfree(&o);
619
620 if (format == NULL)
621 return 0;
622
623 va_start(ap, format);
624 size = session_vprintf(session, format, ap);
625 va_end(ap);
626
627 return size;
628 }
629
630 view = xmalloc(o->size + 1, "session_output_view_or_printf");
631 size = bile_read_object(db->bile, o, view, o->size);
632 view[size] = '\0';
633 xfree(&o);
634
635 size = session_expand_template(session, view, &output);
636 if (!size) {
637 xfree(&view);
638 return 0;
639 }
640
641 size = session_output(session, output, size);
642 xfree(&output);
643 xfree(&view);
644
645 return size;
646 }
647
648 bool
649 session_idled_out(struct session *session)
650 {
651 if (session->logged_in) {
652 if (Time - session->last_input_at >
653 (db->config.max_idle_minutes * 60))
654 return true;
655 } else {
656 if (Time - session->established_at >
657 db->config.max_login_seconds)
658 return true;
659 }
660
661 return false;
662 }
663
664 short
665 session_get_char(struct session *session, unsigned char *b)
666 {
667 if (session->ibuflen == 0) {
668 if (session_idled_out(session))
669 return 0;
670
671 uthread_yield();
672 session->node_funcs->input(session);
673
674 if (session->ending || session->abort_input)
675 return 0;
676 if (session->ibuflen == 0)
677 return 0;
678 }
679
680 session->last_input_at = Time;
681
682 *b = session->ibuf[session->ibufoff];
683
684 if (session->ibuflen == 1) {
685 session->ibuflen = 0;
686 session->ibufoff = 0;
687 } else {
688 session->ibuflen--;
689 session->ibufoff++;
690 }
691
692 return 1;
693 }
694
695 bool
696 session_wait_for_chars(struct session *session, unsigned short timeout_ms,
697 unsigned short num_chars)
698 {
699 unsigned long expire = 0;
700
701 if (timeout_ms)
702 expire = Ticks + (timeout_ms / ((double)1000 / (double)60));
703
704 while (session->ibuflen < num_chars) {
705 session->node_funcs->input(session);
706 if (session->ending || session->abort_input)
707 return false;
708 if (session->obuflen != 0 && session->chatting)
709 return false;
710 if (expire && Ticks > expire)
711 return false;
712 if (session_idled_out(session))
713 return false;
714 if (session->ibuflen >= num_chars)
715 break;
716 uthread_yield();
717 }
718
719 session->last_input_at = Time;
720 return true;
721 }
722
723 unsigned short
724 session_input_char(struct session *session)
725 {
726 unsigned short ret;
727 short waiting_for = 1;
728 short consumed = 0;
729
730 wait_for_char:
731 if (session->ibuflen < waiting_for &&
732 !session_wait_for_chars(session, 0, waiting_for)) {
733 if (session_idled_out(session))
734 goto idled_out;
735 return 0;
736 }
737
738 if (session->ibuf[0] == ESC) {
739 /* look for a VT100 escape sequence */
740 if (session->ibuflen == 1) {
741 waiting_for = 2;
742 goto wait_for_char;
743 } else if (session->ibuf[1] == '[') {
744 if (session->ibuflen == 2) {
745 waiting_for = 3;
746 goto wait_for_char;
747 } else {
748 consumed = 3;
749 switch (session->ibuf[2]) {
750 case 'A':
751 ret = KEY_UP;
752 break;
753 case 'B':
754 ret = KEY_DOWN;
755 break;
756 case 'C':
757 ret = KEY_RIGHT;
758 break;
759 case 'D':
760 ret = KEY_LEFT;
761 break;
762 default:
763 /* probably not good to pass it through as-is */
764 ret = KEY_OTHER;
765 }
766
767 goto done_consuming;
768 }
769 }
770 }
771
772 ret = session->ibuf[0];
773 consumed = 1;
774
775 done_consuming:
776 if (session->ibuflen == consumed)
777 session->ibuflen = 0;
778 else {
779 memmove(session->ibuf, session->ibuf + consumed,
780 session->ibuflen - consumed);
781 session->ibuflen--;
782 }
783
784 if (session->last_input == '\r' && (ret == '\n' || ret == 0)) {
785 /* we already responded to the \r, just ignore this */
786 session->last_input = ret;
787 goto wait_for_char;
788 }
789
790 session->last_input = ret;
791 return ret;
792
793 idled_out:
794 if (session->logged_in)
795 session_logf(session, "Idle too long, logging out");
796 else
797 session_logf(session, "Took too long to login, disconnecting");
798 session_printf(session,
799 "\r\n\r\nYou have been idle too long, goodbye.\r\n\r\n");
800 session_flush(session);
801 session->ending = true;
802 return 0;
803 }
804
805 void
806 session_clear_input(struct session *session)
807 {
808 uthread_yield();
809 session->node_funcs->input(session);
810 uthread_yield();
811 session->node_funcs->input(session);
812
813 session->ibuflen = 0;
814 }
815
816 /*
817 * Collect up to size-1 bytes of input with trailing null, in a field
818 * width columns wide, optionally masking echoed output with mask char.
819 * For multiline-capable input,
820 */
821 char *
822 session_field_input(struct session *session, unsigned short size,
823 unsigned short width, char *initial_input, bool multiline, char mask)
824 {
825 short ilen = 0, ipos = 0, over;
826 long n;
827 char *field;
828 unsigned short c;
829 unsigned char chc;
830 bool redraw = false;
831
832 session->abort_input = false;
833
834 field = xmalloczero(size, "session_field_input");
835
836 if (initial_input) {
837 ipos = ilen = strlcpy(field, initial_input, size);
838 /* TODO: handle initial value being longer than width */
839 session_output(session, field, ilen);
840 session_flush(session);
841 }
842
843 for (;;) {
844 c = session_input_char(session);
845 if (session->ending || session->abort_input)
846 goto field_input_bail;
847 switch (c) {
848 case 0: /* session_input_char bailed */
849 if (session->obuflen > 0) {
850 session->node_funcs->output(session);
851 uthread_yield();
852 if (session->ending)
853 goto field_input_bail;
854 }
855 break;
856 case CONTROL_D: /* ^D */
857 return field;
858 case CONTROL_C: /* ^C */
859 if (multiline)
860 return field;
861 goto field_input_bail;
862 case BACKSPACE: /* ^H */
863 case DELETE: /* backspace through telnet */
864 case KEY_LEFT:
865 if (ipos == 0)
866 continue;
867
868 if (ipos < ilen && c != KEY_LEFT) {
869 redraw = true;
870 memmove(field + ipos - 1, field + ipos, ilen - 1);
871 }
872
873 if (field[ipos - 1] == '\n') {
874 /* need to jump up a line and go over */
875 session_printf(session,
876 ansi(session, ANSI_UP_N, 1, ANSI_END));
877 over = 1;
878 for (n = ipos - 2; n >= 0; n--) {
879 if (field[n] == '\n')
880 break;
881 over++;
882 }
883 session_printf(session,
884 ansi(session, ANSI_COL_N, over, ANSI_END));
885 session_flush(session);
886 }
887
888 ipos--;
889
890 if (c != KEY_LEFT) {
891 ilen--;
892 field[ilen] = '\0';
893
894 session_printf(session,
895 ansi(session, ANSI_BACKSPACE, ANSI_END));
896 }
897
898 if (redraw) {
899 session_printf(session,
900 ansi(session, ANSI_SAVE_CURSOR, ANSI_END));
901 session_output(session, field + ipos, ilen - ipos);
902 session_output(session, " ", 1);
903 session_printf(session,
904 ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END));
905 }
906
907 session_flush(session);
908 break;
909 case '\r':
910 case '\n':
911 if (multiline)
912 goto append_char;
913 return field;
914 case KEY_RIGHT:
915 if (ipos == ilen)
916 continue;
917 ipos++;
918 session_printf(session,
919 ansi(session, ANSI_FORWARD_N, 1, ANSI_END));
920 session_flush(session);
921 break;
922 default:
923 append_char:
924 if ((c < 32 || c > 127) && (c != '\r' && c != '\n'))
925 break;
926 if (ilen >= size - 1)
927 break;
928 chc = c;
929 if (ipos < ilen)
930 memmove(field + ipos + 1, field + ipos, ilen - ipos);
931
932 field[ipos] = chc;
933 ipos++;
934 ilen++;
935 field[ilen] = '\0';
936 session_output(session, mask ? &mask : (char *)&chc, 1);
937
938 if (ipos < ilen) {
939 if (mask) {
940 /* TODO: repeat mask */
941 } else {
942 session_printf(session,
943 ansi(session, ANSI_SAVE_CURSOR, ANSI_END));
944 session_output(session, field + ipos,
945 ilen - ipos);
946 session_printf(session,
947 ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END));
948 }
949 }
950
951 if (chc == '\r') {
952 c = '\n';
953 goto append_char;
954 }
955
956 session_flush(session);
957 }
958 }
959
960 field_input_bail:
961 xfree(&field);
962 return NULL;
963 }
964
965 short
966 session_login(struct session *s)
967 {
968 struct user *user = NULL;
969 char junk[SHA256_DIGEST_STRING_LENGTH];
970 char *username = NULL, *password = NULL;
971 size_t len;
972 short n;
973
974 for (n = 1; n <= 3; n++) {
975 session_printf(s, "login: ");
976
977 if (s->autologin_username[0]) {
978 session_printf(s, "{{#}}%s\r\n", s->autologin_username);
979 session_flush(s);
980 username = xstrdup(s->autologin_username,
981 "session_login username");
982 } else {
983 session_flush(s);
984 username = session_field_input(s, DB_USERNAME_LENGTH + 1,
985 DB_USERNAME_LENGTH, NULL, false, 0);
986 session_output(s, "\r\n", 2);
987 session_flush(s);
988
989 if (username == NULL || s->ending)
990 goto login_bail;
991
992 if (username[0] == '\0') {
993 n--;
994 xfree(&username);
995 username = NULL;
996 continue;
997 }
998 }
999
1000 if (strcmp(username, GUEST_USERNAME) == 0) {
1001 session_logf(s, "Successful guest login in as %s", username);
1002 xfree(&username);
1003 return AUTH_USER_GUEST;
1004 }
1005
1006 if ((strcmp(username, "signup") == 0 ||
1007 strcmp(username, "new") == 0) && db->config.open_signup) {
1008 session_logf(s, "Successful guest signup login in as %s",
1009 username);
1010 xfree(&username);
1011 return AUTH_USER_SIGNUP;
1012 }
1013
1014 user = user_find_by_username(username);
1015
1016 if (!user && user_username_is_banned(username)) {
1017 session_logf(s, "Attempted login as banned username %s",
1018 username);
1019 s->ban_node_source = 1;
1020 goto login_bail;
1021 }
1022
1023 if (s->autologin_username[0]) {
1024 if (user) {
1025 xfree(&username);
1026 s->user = user;
1027 session_logf(s, "Automatically logged in as %s",
1028 s->autologin_username);
1029 return AUTH_USER_OK;
1030 }
1031
1032 memset(s->autologin_username, 0,
1033 sizeof(s->autologin_username));
1034 }
1035
1036 session_printf(s, "Password: ");
1037 session_flush(s);
1038 password = session_field_input(s, 64, 64, NULL, false, '*');
1039 session_output(s, "\r\n", 2);
1040 session_flush(s);
1041
1042 if (password == NULL || s->ending)
1043 goto login_bail;
1044
1045 if (user) {
1046 if (user_authenticate(user, password) == AUTH_USER_OK) {
1047 if (user->is_enabled)
1048 s->user = user;
1049 else
1050 session_logf(s, "Successful password login for %s but "
1051 "account is disabled", user->username);
1052 } else
1053 session_logf(s, "Failed password login for %s",
1054 user->username);
1055 } else {
1056 /* kill some time */
1057 SHA256Data((const u_int8_t *)password, strlen(password), junk);
1058 session_logf(s, "Failed password login for invalid user %s",
1059 username);
1060 }
1061
1062 len = strlen(username);
1063 memset(username, 0, len);
1064 xfree(&username);
1065 username = NULL;
1066
1067 len = strlen(password);
1068 memset(password, 0, len);
1069 xfree(&password);
1070 password = NULL;
1071
1072 if (s->user) {
1073 session_logf(s, "Successful password login for user %s",
1074 s->user->username);
1075 return AUTH_USER_OK;
1076 }
1077
1078 uthread_msleep(60);
1079 session_printf(s, "Login incorrect\r\n");
1080 session_flush(s);
1081
1082 if (user)
1083 xfree(&user);
1084 }
1085
1086 login_bail:
1087 if (username != NULL)
1088 xfree(&username);
1089 if (password != NULL)
1090 xfree(&password);
1091 if (!s->ban_node_source) {
1092 if (session_idled_out(s)) {
1093 session_logf(s, "Login timed out after %d seconds",
1094 db->config.max_login_seconds);
1095 session_printf(s, "\r\nLogin timed out after %d seconds\r\n",
1096 db->config.max_login_seconds);
1097 /* session_flush won't do anything since s->ending is set */
1098 s->node_funcs->output(s);
1099 } else {
1100 session_printf(s, "Thanks for playing\r\n");
1101 session_flush(s);
1102 }
1103 }
1104
1105 return AUTH_USER_FAILED;
1106 }
1107
1108 size_t
1109 session_expand_template(struct session *session, const char *tmpl,
1110 char **ret)
1111 {
1112 static char curvar[128], matchvar[128];
1113 size_t tmpllen, retsize, retpos;
1114 size_t vallen;
1115 short n, invar = 0, varlen = 0, doif, sep;
1116 char *varseek, *curvarpos, *val, *data;
1117 bool end_expansion = false;
1118
1119 retsize = 0;
1120 retpos = 0;
1121 data = NULL;
1122 tmpllen = strlen(tmpl);
1123
1124 for (n = 0; n < tmpllen; n++) {
1125 if (invar) {
1126 if (!varlen && (tmpl[n] == ' ' || tmpl[n] == '\t'))
1127 continue;
1128 if (tmpl[n] == '}' && tmpl[n + 1] == '}') {
1129 n++;
1130 goto expand_var;
1131 } else if (varlen < sizeof(curvar) - 2)
1132 curvar[varlen++] = tmpl[n];
1133 } else if (!end_expansion && tmpl[n] == '{' && tmpl[n + 1] == '{') {
1134 invar = 1;
1135 varlen = 0;
1136 n++;
1137 } else if (tmpl[n] == '\r' && tmpl[n + 1] != '\n') {
1138 EXPAND_TO_FIT(data, retsize, retpos, 2, 64);
1139 data[retpos++] = '\r';
1140 data[retpos++] = '\n';
1141 } else {
1142 EXPAND_TO_FIT(data, retsize, retpos, 1, 64);
1143 data[retpos++] = tmpl[n];
1144 }
1145
1146 continue;
1147
1148 expand_var:
1149 invar = 0;
1150 curvar[varlen] = '\0';
1151 varlen = 0;
1152
1153 if (strpos_quoted(curvar, '?') != -1 &&
1154 strpos_quoted(curvar, ':') != -1) {
1155 /* ternary: user ? "a string" : time */
1156 varseek = curvar;
1157 while (varseek[0] == ' ')
1158 varseek++;
1159 sep = strpos_quoted(varseek, '?');
1160 if (sep == -1)
1161 continue;
1162 memcpy(matchvar, varseek, sep);
1163 matchvar[sep] = '\0';
1164 curvarpos = varseek + sep + 1;
1165 while (matchvar[sep - 1] == ' ')
1166 matchvar[--sep] = '\0';
1167
1168 /* matchvar is now the condition */
1169 if (strcmp(matchvar, "user") == 0)
1170 doif = (session->user != NULL);
1171 else if (strcmp(matchvar, "sysop") == 0)
1172 doif = (session->user && session->user->is_sysop);
1173 else
1174 doif = 0;
1175
1176 while (curvarpos[0] == ' ')
1177 curvarpos++;
1178
1179 sep = strpos_quoted(curvarpos, ':');
1180 if (sep == -1)
1181 continue;
1182
1183 while (curvarpos[sep] == ' ') {
1184 curvarpos++;
1185 sep--;
1186 }
1187
1188 if (doif) {
1189 /* copy the "then" */
1190 memcpy(matchvar, curvarpos, sep);
1191 matchvar[sep] = '\0';
1192 } else {
1193 /* copy from after the "then" and : */
1194 strlcpy(matchvar, curvarpos + sep + 1, sizeof(matchvar));
1195 }
1196
1197 sep = strlen(matchvar);
1198 while (matchvar[sep - 1] == ' ')
1199 matchvar[--sep] = '\0';
1200
1201 vallen = session_expand_var(session, matchvar, &val,
1202 &end_expansion);
1203 } else {
1204 vallen = session_expand_var(session, curvar, &val,
1205 &end_expansion);
1206 }
1207
1208 if (vallen) {
1209 EXPAND_TO_FIT(data, retsize, retpos, vallen, 128);
1210 memcpy(data + retpos, val, vallen);
1211 retpos += vallen;
1212 }
1213 }
1214 data[retpos] = '\0';
1215
1216 *ret = data;
1217 return retpos;
1218 }
1219
1220 size_t
1221 session_expand_var(struct session *session, char *ivar, char **ret,
1222 bool *end_expansion)
1223 {
1224 static char var[128], retval[128];
1225 size_t retsize, retlen, varlen, mcount;
1226 struct tm *now;
1227 short count;
1228 bool pad = false;
1229
1230 *ret = (char *)&retval;
1231 retlen = 0;
1232
1233 if (sscanf(ivar, "%[^|]|%ld%n", var, &retsize, &count) == 2 &&
1234 count > 0) {
1235 /* field of fixed length, either truncated or padded */
1236 if (retsize > sizeof(retval))
1237 retsize = sizeof(retval);
1238 pad = true;
1239 varlen = strlen(var);
1240 } else {
1241 while (ivar[0] == ' ')
1242 ivar++;
1243
1244 varlen = strlcpy(var, ivar, sizeof(var));
1245 retsize = sizeof(retval);
1246 }
1247
1248 while (varlen && var[varlen - 1] == ' ') {
1249 var[varlen - 1] = '\0';
1250 varlen--;
1251 }
1252
1253 if (strcmp(var, "B") == 0) {
1254 retlen = strlcpy(retval, ansi_bold(session), retsize);
1255 } else if (strcmp(var, "/B") == 0) {
1256 retlen = strlcpy(retval, ansi_reset(session), retsize);
1257 } else if (strcmp(var, "#") == 0) {
1258 *end_expansion = true;
1259 retlen = 0;
1260 } else if (strcmp(var, "node") == 0) {
1261 retlen = strlcpy(retval, session->node, retsize);
1262 } else if (strcmp(var, "phone_number") == 0) {
1263 retlen = strlcpy(retval, db->config.phone_number, retsize);
1264 } else if (strcmp(var, "time") == 0) {
1265 now = localtime((time_t *)&Time);
1266 retlen = strftime(retval, retsize, "%H:%M", now);
1267 } else if (strcmp(var, "username") == 0) {
1268 retlen = strlcpy(retval, session->user ?
1269 session->user->username : GUEST_USERNAME, retsize);
1270 } else if (strcmp(var, "new_mail") == 0) {
1271 if (session->user) {
1272 mcount = mail_find_ids_for_user(session->user, NULL,
1273 NULL, 0, 0, true);
1274 if (mcount)
1275 retlen = sprintf(retval, "(%ld New)", mcount);
1276 }
1277 } else if (var[0] == '"') {
1278 /* a literal string, remove leading and trailing quotes */
1279 var[varlen - 1] = '\0';
1280 varlen--;
1281 if (varlen > retsize)
1282 varlen = retsize;
1283 strlcpy(retval, var + 1, varlen);
1284 retlen = varlen - 1;
1285 } else {
1286 /* shrug */
1287 strlcpy(retval, var, varlen);
1288 retlen = varlen;
1289 }
1290
1291 if (pad && retlen < retsize) {
1292 while (retlen < retsize)
1293 retval[retlen++] = ' ';
1294 retval[retlen] = '\0';
1295 }
1296
1297 return retlen;
1298 }
1299
1300 void
1301 session_pause_return(struct session *s, char enforce, char *msg)
1302 {
1303 unsigned char c;
1304
1305 session_printf(s, "{{/B}}Press ");
1306 if (enforce == 0 || enforce == '\r')
1307 session_printf(s, "{{B}}<Enter>{{/B}} ");
1308 else if (enforce == CONTROL_C)
1309 session_printf(s, "{{B}}^C{{/B}} ");
1310 else
1311 session_printf(s, "{{B}}%c{{/B}} ", enforce);
1312
1313 if (msg)
1314 session_printf(s, msg);
1315 else
1316 session_printf(s, "to return to the main menu...");
1317 session_flush(s);
1318
1319 for (;;) {
1320 c = session_input_char(s);
1321 if (s->ending)
1322 return;
1323 if (c == 0)
1324 continue;
1325 if (enforce == 0 || enforce == c)
1326 break;
1327 }
1328 session_output(s, "\r\n", 2);
1329 session_flush(s);
1330 }
1331
1332 void
1333 session_page_sysop(struct session *s)
1334 {
1335 char *message = NULL;
1336
1337 session_printf(s, "{{B}}Page Sysop{{/B}} "
1338 "(^C to cancel)\r\n"
1339 "{{B}}-------------------------{{/B}}\r\n");
1340 session_output_view_or_printf(s, DB_TEXT_PAGE_SYSOP_ID,
1341 "(Instructions missing)\r\n");
1342 session_flush(s);
1343
1344 session_printf(s, "{{B}}Message:{{/B}} ");
1345 session_flush(s);
1346 message = session_field_input(s, 64, 64, NULL, false, 0);
1347 session_output(s, "\r\n", 2);
1348 session_flush(s);
1349
1350 if (message == NULL || s->ending)
1351 goto page_done;
1352
1353 /* TODO: show message on the screen */
1354
1355 /* TODO: enter chat with sysop */
1356
1357 session_printf(s, "{{B}}TODO!{{/B}}\r\n");
1358 session_flush(s);
1359
1360 page_done:
1361 if (message != NULL)
1362 xfree(&message);
1363 }
1364
1365 void
1366 session_answer_page(struct session *s)
1367 {
1368 /* TODO */
1369 }
1370
1371 void
1372 session_print_motd(struct session *s, bool force)
1373 {
1374 struct bile_object *o;
1375 unsigned long last_seen_motd = 0, motd_id = 0;
1376 size_t size;
1377 char *view = NULL, *output = NULL;
1378
1379 if (s->user)
1380 last_seen_motd = s->user->last_motd_seen;
1381
1382 o = bile_get_nth_of_type(db->bile, 0, DB_MOTD_RTYPE);
1383 motd_id = o->id;
1384 if (o == NULL || (!force && motd_id <= last_seen_motd)) {
1385 if (o)
1386 xfree(&o);
1387 return;
1388 }
1389
1390 view = xmalloc(o->size + 1, "motd view");
1391 size = bile_read_object(db->bile, o, view, o->size);
1392 view[size] = '\0';
1393 xfree(&o);
1394
1395 size = session_expand_template(s, view, &output);
1396 xfree(&view);
1397 if (!size)
1398 return;
1399
1400 session_output(s, "\r\n", 2);
1401 session_output(s, output, size);
1402 session_output(s, "\r\n\r\n", 4);
1403 xfree(&output);
1404
1405 session_pause_return(s, 0, "to continue...");
1406
1407 if (s->user && !force) {
1408 s->user->last_motd_seen = motd_id;
1409 user_save(s->user);
1410 }
1411 }
1412
1413 void
1414 session_recents(struct session *s)
1415 {
1416 struct session_log slog;
1417 struct tm *date_tm;
1418 size_t scount, rsize, *ids;
1419 char sdate[12];
1420 short printed;
1421
1422 session_printf(s, "{{B}}Recent Logins{{/B}}\r\n");
1423 session_printf(s,
1424 "{{B}}Date Node User Via Speed{{/B}}\r\n");
1425 session_flush(s);
1426
1427 scount = bile_sorted_ids_by_type(db->sessions_bile, SL_LOG_RTYPE,
1428 &ids);
1429 for (printed = 0; printed < 10 && printed < scount; printed++) {
1430 rsize = bile_read(db->sessions_bile, SL_LOG_RTYPE,
1431 ids[scount - 1 - printed], (char *)&slog, sizeof(slog));
1432 if (rsize != sizeof(slog))
1433 panic("unexpected bile_read response %d (read %ld, "
1434 "expected %ld)", bile_error(db->sessions_bile), rsize,
1435 sizeof(slog));
1436
1437 date_tm = localtime((time_t *)&slog.logged_on_at);
1438 strftime(sdate, sizeof(sdate), "%m/%d", date_tm);
1439
1440 session_printf(s, "%-7s %-7s %-20s %-7s %-6u\r\n",
1441 sdate,
1442 slog.node,
1443 slog.username,
1444 slog.via,
1445 slog.tspeed);
1446 }
1447
1448 session_output(s, "\r\n", 2);
1449 session_flush(s);
1450 session_pause_return(s, 0, NULL);
1451
1452 xfree(&ids);
1453 }
1454
1455 void
1456 session_who(struct session *s)
1457 {
1458 char idle_s[20];
1459 char username[20];
1460 unsigned long idle;
1461 short n;
1462
1463 session_printf(s, "{{B}}Who's Online{{/B}}\r\n");
1464 session_printf(s,
1465 "{{B}}Node User Via Speed Idle{{/B}}\r\n");
1466 session_flush(s);
1467
1468 for (n = 0; n < MAX_SESSIONS; n++) {
1469 if (sessions[n] == NULL || !sessions[n]->logged_in)
1470 continue;
1471
1472 idle = Time - sessions[n]->last_input_at;
1473 if (idle < 60)
1474 sprintf(idle_s, "%lds", idle);
1475 else if (idle < (60 * 60))
1476 sprintf(idle_s, "%ldm", idle / 60);
1477 else if (idle < (60 * 60 * 24))
1478 sprintf(idle_s, "%ldh", idle / (60 * 60));
1479 else
1480 sprintf(idle_s, "%ldd", idle / (60 * 60 * 24));
1481
1482 snprintf(username, sizeof(username), "%s%s",
1483 sessions[n]->user ? sessions[n]->user->username : GUEST_USERNAME,
1484 sessions[n]->user && sessions[n]->user->is_sysop ? " (sysop)" : "");
1485
1486 session_printf(s, "%-7s %-20s %-7s %-6u %-6s\r\n",
1487 sessions[n]->node,
1488 username,
1489 sessions[n]->via,
1490 sessions[n]->tspeed,
1491 idle_s);
1492 }
1493
1494 session_output(s, "\r\n", 2);
1495 session_flush(s);
1496 session_pause_return(s, 0, NULL);
1497 }
1498
1499 char
1500 session_menu(struct session *s, char *title, char *prompt,
1501 const struct session_menu_option *opts, size_t nopts, bool show_opts)
1502 {
1503 size_t n;
1504 short j;
1505 unsigned short c;
1506 bool last_invalid = false;
1507
1508 if (show_opts) {
1509 session_printf(s, "{{B}}%s{{/B}}\r\n", title);
1510 session_flush(s);
1511 }
1512
1513 print_options:
1514 if (show_opts) {
1515 for (n = 0; n < nopts; n++) {
1516 if (opts[n].key[0] == '\0')
1517 continue;
1518 session_printf(s, "{{B}}%c{{/B}}: %s\r\n", opts[n].key[0],
1519 opts[n].title);
1520 }
1521 session_flush(s);
1522 }
1523
1524 for (;;) {
1525 session_printf(s, "{{B}}%s>{{/B}} ", prompt);
1526 session_flush(s);
1527
1528 get_menu_option:
1529 uthread_yield();
1530 c = session_input_char(s);
1531 if (s->ending)
1532 return 0;
1533 if (c == '\r' || c == 0 || c > 255)
1534 goto get_menu_option;
1535
1536 for (n = 0; n < nopts; n++) {
1537 for (j = 0; ; j++) {
1538 if (opts[n].key[j] == '\0')
1539 break;
1540 if (opts[n].ret == '#') {
1541 if (opts[n].key[j] == c && c != '#') {
1542 session_printf(s, "%c\r\n", c);
1543 session_flush(s);
1544 return c - '0'; /* return actual number */
1545 }
1546 } else if (opts[n].key[j] == c) {
1547 session_printf(s, "%c\r\n", c);
1548 session_flush(s);
1549 return opts[n].ret;
1550 }
1551 }
1552 }
1553
1554 /*
1555 * Avoid a constant back-and-forth of the user's session spewing
1556 * bogus characters and us having to print 'invalid option' over
1557 * and over.
1558 */
1559 if (last_invalid)
1560 goto get_menu_option;
1561
1562 session_printf(s, "%c\r\n", c);
1563 session_printf(s, invalid_option_help);
1564 session_flush(s);
1565 last_invalid = true;
1566 }
1567 }
1568
1569 void
1570 session_purge_logs(short days)
1571 {
1572 struct bile_object *obj;
1573 struct session_log log;
1574 size_t n, size = 0, count = 0, delta;
1575 unsigned long secs, *ids = NULL;
1576 struct tm *date_tm;
1577 char date[20];
1578
1579 secs = (60UL * 60UL * 24UL * (unsigned long)days);
1580
1581 logger_printf("[db] Purging session logs older than %d days", days);
1582
1583 for (n = 0;
1584 (obj = bile_get_nth_of_type(db->sessions_bile, n, SL_LOG_RTYPE));
1585 n++) {
1586 bile_read_object(db->sessions_bile, obj, &log, sizeof(log));
1587
1588 delta = Time - log.logged_on_at;
1589 if (delta > secs) {
1590 EXPAND_TO_FIT(ids, size, count * sizeof(unsigned long),
1591 sizeof(unsigned long), 10 * sizeof(unsigned long));
1592 ids[count++] = obj->id;
1593 }
1594
1595 xfree(&obj);
1596 }
1597
1598 if (count) {
1599 logger_printf("[db] Deleting %ld of %ld log entries", count, n);
1600
1601 for (n = 0; n < count; n++) {
1602 bile_delete(db->sessions_bile, SL_LOG_RTYPE, ids[n]);
1603 }
1604 }
1605
1606 if (ids)
1607 xfree(&ids);
1608 }