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