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