Download
jcs
/subtext
/session.c
(View History)
jcs session: Avoid having to use strlen, check for null byte while walking | Latest amendment: 584 on 2024-02-13 |
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 | if (auth == AUTH_USER_SIGNUP) { |
251 | auth = AUTH_USER_GUEST; |
252 | session_output(s, "\r\n", 2); |
253 | |
254 | s->area = SESSION_AREA_SIGNUP; |
255 | s->user = signup(s); |
256 | } |
257 | |
258 | main_menu: |
259 | s->area = SESSION_AREA_MAIN_MENU; |
260 | session_output(s, "\r\n", 2); |
261 | session_print_menu(s, false); |
262 | session_flush(s); |
263 | |
264 | while (!done && !s->ending) { |
265 | s->area = SESSION_AREA_MAIN_MENU; |
266 | session_printf(s, "{{B}}Main Menu>{{/B}} "); |
267 | session_flush(s); |
268 | |
269 | get_another_char: |
270 | action = ACTION_NONE; |
271 | |
272 | c = session_input_char(s); |
273 | |
274 | if (s->ending) |
275 | break; |
276 | if (c == '\r' || c == 0 || c > 255) |
277 | goto get_another_char; |
278 | |
279 | session_printf(s, "%c\r\n", c); |
280 | session_flush(s); |
281 | |
282 | if (action == ACTION_NONE) { |
283 | /* check each menu option's all_keys array for matching key */ |
284 | for (i = 0; ; i++) { |
285 | struct main_menu_option *option = &main_menu_options[i]; |
286 | short j; |
287 | |
288 | if (option->action == ACTION_NONE) |
289 | break; |
290 | |
291 | for (j = 0; option->all_keys[j] != '\0'; j++) { |
292 | if (option->all_keys[j] == c) { |
293 | action = option->action; |
294 | break; |
295 | } |
296 | } |
297 | |
298 | if (action != ACTION_NONE) |
299 | break; |
300 | } |
301 | } |
302 | |
303 | /* change actions for conditionals */ |
304 | if (action == ACTION_SETTINGS_OR_SIGNUP) { |
305 | if (s->user) |
306 | action = ACTION_SETTINGS; |
307 | else |
308 | action = ACTION_SIGNUP; |
309 | } else if (action == ACTION_PAGE_SEND_OR_ANSWER) { |
310 | if (s->user && s->user->is_sysop) |
311 | action = ACTION_PAGE_ANSWER; |
312 | else |
313 | action = ACTION_PAGE_SEND; |
314 | } |
315 | |
316 | switch (action) { |
317 | case ACTION_BOARD_SHOW_FIRST: |
318 | s->area = SESSION_AREA_BOARDS; |
319 | board_show(s, 1, NULL); |
320 | break; |
321 | case ACTION_CHAT: |
322 | s->area = SESSION_AREA_CHAT; |
323 | chat_start(s, NULL); |
324 | break; |
325 | case ACTION_FILES_MENU: |
326 | s->area = SESSION_AREA_FILES; |
327 | folder_list(s); |
328 | break; |
329 | case ACTION_GOODBYE: |
330 | session_output_view_or_printf(s, DB_VIEW_SIGNOFF, |
331 | "Goodbye!\r\n"); |
332 | session_flush(s); |
333 | done = true; |
334 | break; |
335 | case ACTION_RECENT_LOGINS: |
336 | s->area = SESSION_AREA_RECENT_LOGINS; |
337 | session_recents(s); |
338 | break; |
339 | case ACTION_MAIL_MENU: |
340 | s->area = SESSION_AREA_MAIL; |
341 | mail_menu(s); |
342 | break; |
343 | case ACTION_MAIL_COMPOSE: |
344 | s->area = SESSION_AREA_MAIL; |
345 | mail_compose(s, NULL, NULL, NULL, NULL); |
346 | break; |
347 | case ACTION_MOTD: |
348 | s->area = SESSION_AREA_MOTD; |
349 | session_print_motd(s, true); |
350 | break; |
351 | case ACTION_PAGE_ANSWER: |
352 | if (!s->user || !s->user->is_sysop) { |
353 | session_printf(s, invalid_option_help); |
354 | session_flush(s); |
355 | break; |
356 | } |
357 | s->area = SESSION_AREA_CHAT; |
358 | session_answer_page(s); |
359 | break; |
360 | case ACTION_PAGE_SEND: |
361 | session_page_sysop(s); |
362 | break; |
363 | case ACTION_SETTINGS: |
364 | if (s->user) { |
365 | s->area = SESSION_AREA_SETTINGS; |
366 | user_settings_menu(s); |
367 | } |
368 | break; |
369 | case ACTION_SIGNUP: |
370 | s->area = SESSION_AREA_SIGNUP; |
371 | if ((s->user = signup(s))) |
372 | goto main_menu; |
373 | break; |
374 | case ACTION_WHOS_ONLINE: |
375 | s->area = SESSION_AREA_WHO; |
376 | session_who(s); |
377 | break; |
378 | case ACTION_BOARD_LIST_BOARDS: |
379 | s->area = SESSION_AREA_BOARDS; |
380 | board_list_boards(s); |
381 | break; |
382 | case ACTION_BOARD_LIST_FTN_AREAS: |
383 | s->area = SESSION_AREA_FTN; |
384 | board_list_ftn_areas(s); |
385 | break; |
386 | case ACTION_BOARD_SHOW_1: |
387 | s->area = SESSION_AREA_BOARDS; |
388 | board_show(s, 1, NULL); |
389 | break; |
390 | case ACTION_BOARD_SHOW_2: |
391 | s->area = SESSION_AREA_BOARDS; |
392 | board_show(s, 2, NULL); |
393 | break; |
394 | case ACTION_BOARD_SHOW_3: |
395 | s->area = SESSION_AREA_BOARDS; |
396 | board_show(s, 3, NULL); |
397 | break; |
398 | case ACTION_BOARD_SHOW_4: |
399 | s->area = SESSION_AREA_BOARDS; |
400 | board_show(s, 4, NULL); |
401 | break; |
402 | case ACTION_BOARD_SHOW_5: |
403 | s->area = SESSION_AREA_BOARDS; |
404 | board_show(s, 5, NULL); |
405 | break; |
406 | case ACTION_BOARD_SHOW_6: |
407 | s->area = SESSION_AREA_BOARDS; |
408 | board_show(s, 6, NULL); |
409 | break; |
410 | case ACTION_BOARD_SHOW_7: |
411 | s->area = SESSION_AREA_BOARDS; |
412 | board_show(s, 7, NULL); |
413 | break; |
414 | case ACTION_BOARD_SHOW_8: |
415 | s->area = SESSION_AREA_BOARDS; |
416 | board_show(s, 8, NULL); |
417 | break; |
418 | case ACTION_BOARD_SHOW_9: |
419 | s->area = SESSION_AREA_BOARDS; |
420 | board_show(s, 9, NULL); |
421 | break; |
422 | case ACTION_BOARD_SHOW_10: |
423 | s->area = SESSION_AREA_BOARDS; |
424 | board_show(s, 10, NULL); |
425 | break; |
426 | case ACTION_SYSOP_MENU: |
427 | if (!s->user || !s->user->is_sysop) { |
428 | session_printf(s, invalid_option_help); |
429 | session_flush(s); |
430 | break; |
431 | } |
432 | sysop_menu(s); |
433 | break; |
434 | case ACTION_SHOW_MENU: |
435 | if (last_c == c) |
436 | /* asking twice in a row will print the full menu */ |
437 | session_print_menu(s, false); |
438 | else |
439 | session_print_menu(s, true); |
440 | session_flush(s); |
441 | break; |
442 | default: |
443 | session_printf(s, invalid_option_help); |
444 | session_flush(s); |
445 | break; |
446 | } |
447 | |
448 | last_c = c; |
449 | } |
450 | |
451 | session_close(s); |
452 | } |
453 | |
454 | void |
455 | session_print_menu(struct session *session, bool short_menu) |
456 | { |
457 | struct main_menu_option *option = NULL; |
458 | size_t size; |
459 | short i; |
460 | char *output = NULL; |
461 | |
462 | if (short_menu && session_output_view_or_printf(session, |
463 | DB_VIEW_SHORTMENU, NULL) != 0) |
464 | return; |
465 | |
466 | if (session_output_view_or_printf(session, DB_VIEW_MENU, NULL) != 0) |
467 | return; |
468 | |
469 | for (i = 0; ; i++) { |
470 | option = &main_menu_options[i]; |
471 | |
472 | if (option->action == ACTION_NONE) |
473 | break; |
474 | |
475 | if (option->menu_key == 0 || option->label[0] == '\0') |
476 | continue; |
477 | |
478 | size = session_expand_template(session, option->label, &output); |
479 | if (!size) |
480 | continue; |
481 | |
482 | session_printf(session, "{{B}}%c{{/B}}: %s\r\n", option->menu_key, |
483 | output); |
484 | xfree(&output); |
485 | } |
486 | } |
487 | |
488 | void |
489 | session_close(struct session *session) |
490 | { |
491 | unsigned long now; |
492 | short n; |
493 | bool found; |
494 | |
495 | if (!session->ending) { |
496 | if (!session->ban_node_source) { |
497 | /* give 1 second to flush output */ |
498 | now = Ticks; |
499 | while (session->obuflen && (Ticks - now) < 60) { |
500 | session->node_funcs->output(session); |
501 | uthread_yield(); |
502 | } |
503 | } |
504 | session->ending = true; |
505 | } |
506 | |
507 | if (session->log.logged_on_at) { |
508 | /* finalize session log */ |
509 | session->log.logged_off_at = Time; |
510 | |
511 | if (bile_write(db->sessions_bile, SL_LOG_RTYPE, session->log.id, |
512 | &session->log, sizeof(session->log)) != sizeof(session->log)) |
513 | panic("bile_write of session log failed: %d", |
514 | bile_error(db->sessions_bile)); |
515 | } |
516 | |
517 | /* close the node */ |
518 | session->node_funcs->close(session); |
519 | |
520 | /* remove session from sessions */ |
521 | found = false; |
522 | for (n = 0; n < MAX_SESSIONS; n++) { |
523 | if (sessions[n] == session) { |
524 | sessions[n] = NULL; |
525 | found = true; |
526 | break; |
527 | } |
528 | } |
529 | |
530 | if (!found) |
531 | panic("session_close failed to find session to remove"); |
532 | |
533 | nsessions--; |
534 | if (session->user) |
535 | xfree(&session->user); |
536 | xfree(&session); |
537 | |
538 | logger_update_title(); |
539 | } |
540 | |
541 | void |
542 | session_check_buf_canaries(struct session *session) |
543 | { |
544 | if (session->obuf_canary != OBUF_CANARY) |
545 | warn("obuf canary dead"); |
546 | if (session->ibuf_canary != IBUF_CANARY) |
547 | warn("ibuf canary dead"); |
548 | if (session->obuflen_canary != OBUFLEN_CANARY) |
549 | warn("obuflen canary dead"); |
550 | if (session->ibuflen_canary != IBUFLEN_CANARY) |
551 | warn("ibuflen canary dead"); |
552 | } |
553 | |
554 | void |
555 | session_flush(struct session *session) |
556 | { |
557 | if (session->ending || session->obuflen == 0) |
558 | return; |
559 | |
560 | while (session->obuflen != 0) { |
561 | session->node_funcs->output(session); |
562 | session_check_buf_canaries(session); |
563 | if (session->obuflen != 0) |
564 | uthread_yield(); |
565 | if (session->ending) |
566 | return; |
567 | |
568 | /* |
569 | * The console will leave \e in the output buffer if it's the only |
570 | * thing in there, so don't loop forever. |
571 | */ |
572 | if (session->obuflen == 1 && session->obuf[0] == '\33') |
573 | return; |
574 | } |
575 | } |
576 | |
577 | size_t |
578 | session_log(struct session *session, const char *str) |
579 | { |
580 | return logger_printf("[%s] [%s] %s", session->node, |
581 | session->logged_in ? |
582 | (session->user ? session->user->username : GUEST_USERNAME) : "-", |
583 | str); |
584 | } |
585 | |
586 | size_t |
587 | session_logf(struct session *session, const char *format, ...) |
588 | { |
589 | va_list ap; |
590 | |
591 | va_start(ap, format); |
592 | vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap); |
593 | va_end(ap); |
594 | |
595 | return session_log(session, session_log_tbuf); |
596 | } |
597 | |
598 | size_t |
599 | session_logf_buffered(struct session *session, const char *format, ...) |
600 | { |
601 | va_list ap; |
602 | size_t ret; |
603 | |
604 | va_start(ap, format); |
605 | vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap); |
606 | va_end(ap); |
607 | |
608 | logger->autoflush = false; |
609 | ret = session_log(session, session_log_tbuf); |
610 | logger->autoflush = true; |
611 | |
612 | return ret; |
613 | } |
614 | |
615 | size_t |
616 | session_output(struct session *session, const char *str, size_t len) |
617 | { |
618 | size_t chunk, olen = len, stroff = 0; |
619 | |
620 | while (len && !session->ending) { |
621 | chunk = (sizeof(session->obuf) - session->obuflen); |
622 | if (chunk == 0) { |
623 | session_flush(session); |
624 | if (session->ending) |
625 | return 0; |
626 | continue; |
627 | } |
628 | if (chunk > len) |
629 | chunk = len; |
630 | |
631 | memcpy(session->obuf + session->obuflen, str + stroff, chunk); |
632 | session->obuflen += chunk; |
633 | if (session->obuflen > sizeof(session->obuf)) { |
634 | warn("[%s] Bogus obuflen %d > %ld", session->node, |
635 | session->obuflen, sizeof(session->obuf)); |
636 | session->ending = true; |
637 | session_close(session); |
638 | return 0; |
639 | } |
640 | stroff += chunk; |
641 | len -= chunk; |
642 | } |
643 | |
644 | session_check_buf_canaries(session); |
645 | |
646 | return olen; |
647 | } |
648 | |
649 | size_t |
650 | session_paginate(struct session *session, const char *str, size_t len, |
651 | size_t first_page_printed) |
652 | { |
653 | size_t olen = 0, n, adv; |
654 | size_t olines = first_page_printed; |
655 | size_t line_len = session->terminal_columns; |
656 | ssize_t last_space = -1; |
657 | short last_input = 0; |
658 | |
659 | while (len && !session->ending) { |
660 | for (n = 0, adv = 0; n < line_len && n < len; n++) { |
661 | if (str[n] == '\r') { |
662 | last_space = n; |
663 | adv = n + 1; |
664 | if (str[n + 1] == '\n') |
665 | adv++; |
666 | break; |
667 | } else if (str[n] == '\n') { |
668 | last_space = n; |
669 | adv = n + 1; |
670 | break; |
671 | } else if (str[n] == ' ') { |
672 | last_space = n; |
673 | adv = n + 1; |
674 | } |
675 | } |
676 | if (last_space == -1) { |
677 | /* no space, break hard */ |
678 | last_space = MIN(line_len, len); |
679 | adv = last_space; |
680 | } |
681 | if (len == n) |
682 | adv = last_space = len; |
683 | |
684 | if (last_space > 0) { |
685 | session_output(session, str, last_space); |
686 | olen += last_space; |
687 | } |
688 | str += adv; |
689 | len -= adv; |
690 | |
691 | session_output(session, "\r\n", 2); |
692 | olen += 2; |
693 | olines++; |
694 | |
695 | if (len && (olines == session->terminal_lines - 1 || |
696 | last_input == '\r' || last_input == '\n')) { |
697 | olines = 0; |
698 | |
699 | session_printf(session, "-- More --"); |
700 | session_flush(session); |
701 | |
702 | last_input = session_input_char(session); |
703 | |
704 | if (session->vt100) |
705 | session_printf(session, "\r%s", |
706 | ansi(session, ANSI_ERASE_LINE, ANSI_END)); |
707 | else |
708 | session_output(session, "\r", 1); |
709 | |
710 | if (last_input == 'q' || last_input == 'Q') |
711 | break; |
712 | } |
713 | } |
714 | |
715 | return olen; |
716 | } |
717 | |
718 | size_t |
719 | session_printf(struct session *session, const char *format, ...) |
720 | { |
721 | va_list ap; |
722 | size_t len; |
723 | |
724 | va_start(ap, format); |
725 | len = session_vprintf(session, format, ap); |
726 | va_end(ap); |
727 | |
728 | return len; |
729 | } |
730 | |
731 | size_t |
732 | session_vprintf(struct session *session, const char *format, va_list ap) |
733 | { |
734 | static char session_printf_ebuf[160], session_printf_tbuf[160]; |
735 | size_t len, n, en; |
736 | bool stop = false; |
737 | |
738 | /* avoid a full session_expand_template for {{B}}, {{/B}}, and {{#}} */ |
739 | session_printf_ebuf[0] = '\0'; |
740 | for (n = 0, en = 0; format[n] != '\0'; n++) { |
741 | if (!stop) { |
742 | if (format[n] == '{' && format[n + 1] == '{' && |
743 | format[n + 2] == 'B' && format[n + 3] == '}' && |
744 | format[n + 4] == '}') { |
745 | en = strlcat(session_printf_ebuf, ansi_bold(session), |
746 | sizeof(session_printf_ebuf)); |
747 | n += 4; |
748 | continue; |
749 | } |
750 | if (format[n] == '{' && |
751 | format[n + 1] == '{' && format[n + 2] == '/' && |
752 | format[n + 3] == 'B' && format[n + 4] == '}' && |
753 | format[n + 5] == '}') { |
754 | en = strlcat(session_printf_ebuf, ansi_reset(session), |
755 | sizeof(session_printf_ebuf)); |
756 | n += 5; |
757 | continue; |
758 | } |
759 | if (format[n] == '{' && |
760 | format[n + 1] == '{' && format[n + 2] == '#' && |
761 | format[n + 3] == '}' && format[n + 4] == '}') { |
762 | stop = true; |
763 | n += 4; |
764 | continue; |
765 | } |
766 | } |
767 | |
768 | session_printf_ebuf[en++] = format[n]; |
769 | |
770 | if (en >= sizeof(session_printf_ebuf) - 1) |
771 | panic("session_printf_ebuf overflow!"); |
772 | session_printf_ebuf[en] = '\0'; |
773 | } |
774 | |
775 | len = vsnprintf(session_printf_tbuf, sizeof(session_printf_tbuf), |
776 | session_printf_ebuf, ap); |
777 | |
778 | if (len > sizeof(session_printf_tbuf)) |
779 | panic("session_printf overflow! (%ld > %ld)", len, |
780 | sizeof(session_printf_tbuf)); |
781 | |
782 | return session_output(session, session_printf_tbuf, len); |
783 | } |
784 | |
785 | size_t |
786 | session_output_view_or_printf(struct session *session, short id, |
787 | const char *format, ...) |
788 | { |
789 | size_t size; |
790 | char *output; |
791 | va_list ap; |
792 | |
793 | /* can't use bile_read_alloc because we need to null terminate */ |
794 | if (db->views[id] == NULL || db->views[id][0] == '\0') { |
795 | if (format == NULL) |
796 | return 0; |
797 | |
798 | va_start(ap, format); |
799 | size = session_vprintf(session, format, ap); |
800 | va_end(ap); |
801 | |
802 | return size; |
803 | } |
804 | |
805 | size = session_expand_template(session, db->views[id], &output); |
806 | if (!size) |
807 | return 0; |
808 | |
809 | size = session_output(session, output, size); |
810 | xfree(&output); |
811 | |
812 | return size; |
813 | } |
814 | |
815 | bool |
816 | session_idled_out(struct session *session) |
817 | { |
818 | if (session->logged_in) { |
819 | if (session->user && session->user->is_sysop) { |
820 | if (db->config.max_sysop_idle_minutes != 0 && |
821 | Time - session->last_input_at > |
822 | (db->config.max_sysop_idle_minutes * 60)) |
823 | return true; |
824 | } else { |
825 | if (db->config.max_idle_minutes != 0 && |
826 | Time - session->last_input_at > |
827 | (db->config.max_idle_minutes * 60)) |
828 | return true; |
829 | } |
830 | } else { |
831 | if (Time - session->established_at > |
832 | db->config.max_login_seconds) |
833 | return true; |
834 | } |
835 | |
836 | return false; |
837 | } |
838 | |
839 | short |
840 | session_get_char(struct session *session, unsigned char *b) |
841 | { |
842 | return session_get_chars(session, b, 1); |
843 | } |
844 | |
845 | short |
846 | session_get_chars(struct session *session, unsigned char *b, size_t count) |
847 | { |
848 | short ret = 0; |
849 | |
850 | while (ret < count) { |
851 | if (session->ibuflen == 0) { |
852 | if (session_idled_out(session)) |
853 | return 0; |
854 | |
855 | uthread_yield(); |
856 | session->node_funcs->input(session); |
857 | |
858 | if (session->ending || session->abort_input) |
859 | return 0; |
860 | if (session->ibuflen == 0) |
861 | return 0; |
862 | } |
863 | |
864 | session->last_input_at = Time; |
865 | |
866 | *b = session->ibuf[session->ibufoff]; |
867 | b++; |
868 | |
869 | if (session->ibuflen == 1) { |
870 | session->ibuflen = 0; |
871 | session->ibufoff = 0; |
872 | } else { |
873 | session->ibuflen--; |
874 | session->ibufoff++; |
875 | } |
876 | |
877 | ret++; |
878 | } |
879 | |
880 | session_check_buf_canaries(session); |
881 | |
882 | return ret; |
883 | } |
884 | |
885 | bool |
886 | session_wait_for_chars(struct session *session, unsigned short timeout_ms, |
887 | unsigned short num_chars) |
888 | { |
889 | unsigned long expire = 0; |
890 | |
891 | if (timeout_ms) |
892 | expire = Ticks + (timeout_ms / ((double)1000 / (double)60)); |
893 | |
894 | while (session->ibuflen < num_chars) { |
895 | session->node_funcs->input(session); |
896 | if (session->ending || session->abort_input) |
897 | return false; |
898 | if (session->obuflen != 0 && session->chatting) |
899 | return false; |
900 | if (expire && Ticks > expire) |
901 | return false; |
902 | if (session_idled_out(session)) |
903 | return false; |
904 | if (session->ibuflen >= num_chars) |
905 | break; |
906 | uthread_yield(); |
907 | } |
908 | |
909 | session->last_input_at = Time; |
910 | session_check_buf_canaries(session); |
911 | return true; |
912 | } |
913 | |
914 | unsigned short |
915 | session_input_char(struct session *session) |
916 | { |
917 | unsigned short ret; |
918 | short waiting_for = 1; |
919 | short consumed = 0; |
920 | |
921 | wait_for_char: |
922 | if (session->ibuflen < waiting_for && |
923 | !session_wait_for_chars(session, 0, waiting_for)) { |
924 | if (session_idled_out(session)) |
925 | goto idled_out; |
926 | return 0; |
927 | } |
928 | |
929 | if (session->ibuf[0] == ESC) { |
930 | /* look for a VT100 escape sequence */ |
931 | if (session->ibuflen == 1) { |
932 | waiting_for = 2; |
933 | goto wait_for_char; |
934 | } else if (session->ibuf[1] == '[') { |
935 | if (session->ibuflen == 2) { |
936 | waiting_for = 3; |
937 | goto wait_for_char; |
938 | } else { |
939 | consumed = 3; |
940 | switch (session->ibuf[2]) { |
941 | case 'A': |
942 | ret = KEY_UP; |
943 | break; |
944 | case 'B': |
945 | ret = KEY_DOWN; |
946 | break; |
947 | case 'C': |
948 | ret = KEY_RIGHT; |
949 | break; |
950 | case 'D': |
951 | ret = KEY_LEFT; |
952 | break; |
953 | default: |
954 | /* probably not good to pass it through as-is */ |
955 | ret = KEY_OTHER; |
956 | } |
957 | |
958 | goto done_consuming; |
959 | } |
960 | } |
961 | } |
962 | |
963 | ret = session->ibuf[0]; |
964 | consumed = 1; |
965 | |
966 | done_consuming: |
967 | if (session->ibuflen == consumed) |
968 | session->ibuflen = 0; |
969 | else { |
970 | memmove(session->ibuf, session->ibuf + consumed, |
971 | session->ibuflen - consumed); |
972 | session->ibuflen--; |
973 | } |
974 | |
975 | if (session->last_input == '\r' && (ret == '\n' || ret == 0)) { |
976 | /* we already responded to the \r, just ignore this */ |
977 | session->last_input = ret; |
978 | goto wait_for_char; |
979 | } |
980 | |
981 | session->last_input = ret; |
982 | return ret; |
983 | |
984 | idled_out: |
985 | if (session->logged_in) |
986 | session_logf(session, "Idle too long, logging out"); |
987 | else |
988 | session_logf(session, "Took too long to login, disconnecting"); |
989 | session_printf(session, |
990 | "\r\n\r\nYou have been idle too long, goodbye.\r\n\r\n"); |
991 | session_flush(session); |
992 | session->ending = true; |
993 | return 0; |
994 | } |
995 | |
996 | void |
997 | session_clear_input(struct session *session) |
998 | { |
999 | uthread_yield(); |
1000 | session->node_funcs->input(session); |
1001 | uthread_yield(); |
1002 | session->node_funcs->input(session); |
1003 | |
1004 | session->ibuflen = 0; |
1005 | } |
1006 | |
1007 | /* |
1008 | * Collect up to size-1 bytes of input with trailing null, in a field |
1009 | * width columns wide, optionally masking echoed output with mask char. |
1010 | * For multiline-capable input, |
1011 | */ |
1012 | char * |
1013 | session_field_input(struct session *session, unsigned short size, |
1014 | unsigned short width, char *initial_input, bool multiline, char mask) |
1015 | { |
1016 | short ilen = 0, ipos = 0, over; |
1017 | long n; |
1018 | char *field; |
1019 | unsigned short c; |
1020 | unsigned char chc; |
1021 | bool redraw = false; |
1022 | |
1023 | session->abort_input = false; |
1024 | |
1025 | field = xmalloczero(size); |
1026 | if (field == NULL) { |
1027 | session->ending = true; |
1028 | return NULL; |
1029 | } |
1030 | |
1031 | if (initial_input) { |
1032 | ipos = ilen = strlcpy(field, initial_input, size); |
1033 | /* TODO: handle initial value being longer than width */ |
1034 | if (mask) { |
1035 | for (n = 0; n < ilen; n++) |
1036 | session_output(session, &mask, 1); |
1037 | } else |
1038 | session_output(session, field, ilen); |
1039 | session_flush(session); |
1040 | } |
1041 | |
1042 | for (;;) { |
1043 | c = session_input_char(session); |
1044 | if (session->ending || session->abort_input) |
1045 | goto field_input_bail; |
1046 | switch (c) { |
1047 | case 0: /* session_input_char bailed */ |
1048 | if (session->obuflen > 0) { |
1049 | session->node_funcs->output(session); |
1050 | uthread_yield(); |
1051 | if (session->ending) |
1052 | goto field_input_bail; |
1053 | } |
1054 | break; |
1055 | case CONTROL_D: /* ^D */ |
1056 | return field; |
1057 | case CONTROL_C: /* ^C */ |
1058 | if (multiline) |
1059 | return field; |
1060 | goto field_input_bail; |
1061 | case BACKSPACE: /* ^H */ |
1062 | case DELETE: /* backspace through telnet */ |
1063 | case KEY_LEFT: |
1064 | if (ipos == 0) |
1065 | continue; |
1066 | |
1067 | if (ipos < ilen && c != KEY_LEFT) { |
1068 | redraw = true; |
1069 | memmove(field + ipos - 1, field + ipos, ilen - 1); |
1070 | } |
1071 | |
1072 | if (field[ipos - 1] == '\n') { |
1073 | /* need to jump up a line and go over */ |
1074 | session_printf(session, |
1075 | ansi(session, ANSI_UP_N, 1, ANSI_END)); |
1076 | over = 1; |
1077 | for (n = ipos - 2; n >= 0; n--) { |
1078 | if (field[n] == '\n') |
1079 | break; |
1080 | over++; |
1081 | } |
1082 | session_printf(session, |
1083 | ansi(session, ANSI_COL_N, over, ANSI_END)); |
1084 | session_flush(session); |
1085 | } |
1086 | |
1087 | ipos--; |
1088 | |
1089 | if (c != KEY_LEFT) { |
1090 | ilen--; |
1091 | field[ilen] = '\0'; |
1092 | |
1093 | session_printf(session, |
1094 | ansi(session, ANSI_BACKSPACE, ANSI_END)); |
1095 | } |
1096 | |
1097 | if (redraw) { |
1098 | session_printf(session, |
1099 | ansi(session, ANSI_SAVE_CURSOR, ANSI_END)); |
1100 | session_output(session, field + ipos, ilen - ipos); |
1101 | session_output(session, " ", 1); |
1102 | session_printf(session, |
1103 | ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END)); |
1104 | } |
1105 | |
1106 | session_flush(session); |
1107 | break; |
1108 | case '\r': |
1109 | case '\n': |
1110 | if (multiline) |
1111 | goto append_char; |
1112 | return field; |
1113 | case KEY_RIGHT: |
1114 | if (ipos == ilen) |
1115 | continue; |
1116 | ipos++; |
1117 | session_printf(session, |
1118 | ansi(session, ANSI_FORWARD_N, 1, ANSI_END)); |
1119 | session_flush(session); |
1120 | break; |
1121 | default: |
1122 | append_char: |
1123 | if ((c < 32 || c > 127) && (c != '\r' && c != '\n')) |
1124 | break; |
1125 | if (ilen >= size - 1) |
1126 | break; |
1127 | chc = c; |
1128 | if (ipos < ilen) |
1129 | memmove(field + ipos + 1, field + ipos, ilen - ipos); |
1130 | |
1131 | field[ipos] = chc; |
1132 | ipos++; |
1133 | ilen++; |
1134 | field[ilen] = '\0'; |
1135 | session_output(session, mask ? &mask : (char *)&chc, 1); |
1136 | |
1137 | if (ipos < ilen) { |
1138 | if (mask) { |
1139 | /* TODO: repeat mask */ |
1140 | } else { |
1141 | session_printf(session, |
1142 | ansi(session, ANSI_SAVE_CURSOR, ANSI_END)); |
1143 | session_output(session, field + ipos, |
1144 | ilen - ipos); |
1145 | session_printf(session, |
1146 | ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END)); |
1147 | } |
1148 | } |
1149 | |
1150 | if (chc == '\r') { |
1151 | c = '\n'; |
1152 | goto append_char; |
1153 | } |
1154 | |
1155 | session_flush(session); |
1156 | } |
1157 | } |
1158 | |
1159 | field_input_bail: |
1160 | xfree(&field); |
1161 | return NULL; |
1162 | } |
1163 | |
1164 | short |
1165 | session_login(struct session *s) |
1166 | { |
1167 | struct user *user = NULL; |
1168 | char junk[SHA256_DIGEST_STRING_LENGTH]; |
1169 | char *username = NULL, *password = NULL; |
1170 | size_t len; |
1171 | short n; |
1172 | |
1173 | for (n = 1; n <= 3; n++) { |
1174 | session_printf(s, "Username: "); |
1175 | |
1176 | if (s->autologin_username[0]) { |
1177 | session_printf(s, "{{#}}%s\r\n", s->autologin_username); |
1178 | session_flush(s); |
1179 | username = xstrdup(s->autologin_username); |
1180 | if (username == NULL) |
1181 | goto login_bail; |
1182 | } else { |
1183 | session_flush(s); |
1184 | username = session_field_input(s, DB_USERNAME_LENGTH + 1, |
1185 | DB_USERNAME_LENGTH, NULL, false, 0); |
1186 | session_output(s, "\r\n", 2); |
1187 | session_flush(s); |
1188 | |
1189 | if (username == NULL || s->ending) |
1190 | goto login_bail; |
1191 | |
1192 | if (username[0] == '\0') { |
1193 | n--; |
1194 | xfree(&username); |
1195 | username = NULL; |
1196 | continue; |
1197 | } |
1198 | } |
1199 | |
1200 | if (strcasecmp(username, GUEST_USERNAME) == 0) { |
1201 | session_logf(s, "Successful guest login in as %s", username); |
1202 | xfree(&username); |
1203 | return AUTH_USER_GUEST; |
1204 | } |
1205 | |
1206 | if ((strcasecmp(username, "signup") == 0 || |
1207 | strcasecmp(username, "new") == 0) && db->config.open_signup) { |
1208 | session_logf(s, "Successful guest signup login in as %s", |
1209 | username); |
1210 | xfree(&username); |
1211 | return AUTH_USER_SIGNUP; |
1212 | } |
1213 | |
1214 | user = user_find_by_username(username); |
1215 | |
1216 | if (!user && user_username_is_banned(username)) { |
1217 | session_logf(s, "Attempted login as banned username %s", |
1218 | username); |
1219 | s->ban_node_source = 1; |
1220 | goto login_bail; |
1221 | } |
1222 | |
1223 | if (s->autologin_username[0]) { |
1224 | if (user) { |
1225 | xfree(&username); |
1226 | s->user = user; |
1227 | session_logf(s, "Automatically logged in as %s", |
1228 | s->autologin_username); |
1229 | return AUTH_USER_OK; |
1230 | } |
1231 | |
1232 | memset(s->autologin_username, 0, |
1233 | sizeof(s->autologin_username)); |
1234 | } |
1235 | |
1236 | session_printf(s, "Password: "); |
1237 | session_flush(s); |
1238 | password = session_field_input(s, 64, 64, NULL, false, '*'); |
1239 | session_output(s, "\r\n", 2); |
1240 | session_flush(s); |
1241 | |
1242 | if (password == NULL || s->ending) |
1243 | goto login_bail; |
1244 | |
1245 | if (user) { |
1246 | if (user_authenticate(user, password) == AUTH_USER_OK) { |
1247 | if (user->is_enabled) |
1248 | s->user = user; |
1249 | else |
1250 | session_logf(s, "Successful password login for %s but " |
1251 | "account is disabled", user->username); |
1252 | } else |
1253 | session_logf(s, "Failed password login for %s", |
1254 | user->username); |
1255 | } else { |
1256 | /* kill some time */ |
1257 | SHA256Data((const u_int8_t *)password, strlen(password), junk); |
1258 | session_logf(s, "Failed password login for invalid user %s", |
1259 | username); |
1260 | } |
1261 | |
1262 | len = strlen(username); |
1263 | memset(username, 0, len); |
1264 | xfree(&username); |
1265 | username = NULL; |
1266 | |
1267 | len = strlen(password); |
1268 | memset(password, 0, len); |
1269 | xfree(&password); |
1270 | password = NULL; |
1271 | |
1272 | if (s->user) { |
1273 | session_logf(s, "Successful password login for user %s", |
1274 | s->user->username); |
1275 | return AUTH_USER_OK; |
1276 | } |
1277 | |
1278 | uthread_msleep(60); |
1279 | session_printf(s, "Login incorrect\r\n"); |
1280 | session_flush(s); |
1281 | |
1282 | if (user) |
1283 | xfree(&user); |
1284 | } |
1285 | |
1286 | login_bail: |
1287 | if (username != NULL) |
1288 | xfree(&username); |
1289 | if (password != NULL) |
1290 | xfree(&password); |
1291 | if (!s->ban_node_source) { |
1292 | if (session_idled_out(s)) { |
1293 | session_logf(s, "Login timed out after %d seconds", |
1294 | db->config.max_login_seconds); |
1295 | session_printf(s, "\r\nLogin timed out after %d seconds\r\n", |
1296 | db->config.max_login_seconds); |
1297 | /* session_flush won't do anything since s->ending is set */ |
1298 | s->node_funcs->output(s); |
1299 | } else { |
1300 | session_printf(s, "Thanks for playing\r\n"); |
1301 | session_flush(s); |
1302 | } |
1303 | } |
1304 | |
1305 | return AUTH_USER_FAILED; |
1306 | } |
1307 | |
1308 | size_t |
1309 | session_expand_template(struct session *session, const char *tmpl, |
1310 | char **ret) |
1311 | { |
1312 | static char curvar[128], matchvar[128]; |
1313 | size_t retsize, retpos; |
1314 | size_t vallen; |
1315 | short n, invar = 0, varlen = 0, doif, sep; |
1316 | char *varseek, *curvarpos, *val, *data; |
1317 | bool end_expansion = false; |
1318 | |
1319 | retsize = 0; |
1320 | retpos = 0; |
1321 | data = NULL; |
1322 | |
1323 | for (n = 0; tmpl[n] != '\0'; 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 | } |