/* * Copyright (c) 2021 joshua stein * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include "ansi.h" #include "board.h" #include "chat.h" #include "folder.h" #include "logger.h" #include "mail.h" #include "main_menu.h" #include "subtext.h" #include "session.h" #include "settings.h" #include "signup.h" #include "sysop.h" #include "user.h" #include "user_settings.h" #include "uthread.h" #include "util.h" #include "console.h" #define OBUF_CANARY 0xaaaaaaaa #define IBUF_CANARY 0xe38e38e3 #define OBUFLEN_CANARY 0x1c71c71c #define IBUFLEN_CANARY 0x55555555 struct session *sessions[MAX_SESSIONS] = { 0 }; short nsessions = 0; struct session_tally session_today_tally = { 0 }; static char session_log_tbuf[256]; static const char invalid_option_help[] = "Invalid option (press {{B}}?{{/B}} for help)\r\n"; /* must match session_area order */ static const char area_labels[][12] = { "Main Menu", "MOTD", "Boards", "FTN Boards", "Chat", "Mail", "Files", "Who", "Recents", "Settings", "Signup" }; void session_run(struct uthread *uthread, void *arg); void session_print_menu(struct session *session, bool short_menu); short session_login(struct session *s); size_t session_log(struct session *session, const char *str); size_t session_vprintf(struct session *session, const char *format, va_list ap); size_t session_expand_var(struct session *session, char *ivar, char **ret, bool *end_expansion); struct session * session_first_waiting_for_sysop(void); void session_page_sysop(struct session *s); void session_answer_page(struct session *s); void sysop_edit_settings(struct session *s); bool session_idled_out(struct session *session); struct session * session_create(char *node, char *via, struct node_funcs *node_funcs) { struct session *session; size_t n; /* reserve 1 session for the console */ if (nsessions >= MAX_SESSIONS - 1) { if (strcmp(node, "console") != 0) return NULL; /* but only 1 */ if (nsessions == MAX_SESSIONS) return NULL; } session = xmalloczero(sizeof(struct session)); if (session == NULL) return NULL; session->node_funcs = node_funcs; strlcpy(session->node, node, sizeof(session->node)); strlcpy(session->log.node, node, sizeof(session->log.node)); strlcpy(session->via, via, sizeof(session->via)); strlcpy(session->location, "Unknown", sizeof(session->location)); strlcpy(session->log.location, session->location, sizeof(session->log.location)); session->last_input_at = session->established_at = Time; session->obuf_canary = OBUF_CANARY; session->ibuf_canary = IBUF_CANARY; session->obuflen_canary = OBUFLEN_CANARY; session->ibuflen_canary = IBUFLEN_CANARY; for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] != NULL) continue; session->uthread = uthread_add(session_run, session); if (!session->uthread) { xfree(&session); return NULL; } sessions[n] = session; nsessions++; return session; } /* no room, but how did we get here? */ xfree(&session); return NULL; } void session_run(struct uthread *uthread, void *arg) { struct session *s = (struct session *)arg; struct session *waiting; char date[9]; struct tm *date_tm; unsigned short c, last_c = 0; bool done = false; short auth, i, action, sc; /* until we negotiate otherwise */ s->terminal_columns = DEFAULT_TERMINAL_COLUMNS; s->terminal_lines = DEFAULT_TERMINAL_LINES; s->area = SESSION_AREA_MAIN_MENU; if (main_menu_options == NULL) panic("No main menu!"); if (s->node_funcs->setup) { s->node_funcs->setup(s); if (s->ending) { session_close(s); return; } } session_output_view_or_printf(s, DB_VIEW_ISSUE, "\r\nWelcome to %s (%s)\r\n\r\n", db->config.name, s->node); session_flush(s); uthread_yield(); auth = session_login(s); if (auth == AUTH_USER_FAILED) { session_close(s); return; } s->logged_in = true; logger_update_title(); /* update session log */ if (s->user) { s->user->last_seen_at = Time; user_save(s->user); strlcpy(s->log.username, s->user->username, sizeof(s->log.username)); strlcpy(s->chat_username, s->user->username, sizeof(s->chat_username)); } else { strlcpy(s->log.username, GUEST_USERNAME, sizeof(s->log.username)); /* guest + ttyt0 -> guest[t0] */ snprintf(s->chat_username, sizeof(s->chat_username), "%s[%s]", GUEST_USERNAME, s->node + 3); } strlcpy(s->log.via, s->via, sizeof(s->log.via)); s->log.tspeed = s->tspeed; s->log.logged_on_at = Time; s->log.id = bile_next_id(db->sessions_bile, SL_LOG_RTYPE); if (bile_write(db->sessions_bile, SL_LOG_RTYPE, s->log.id, &s->log, sizeof(s->log)) != sizeof(s->log)) panic("bile_write of session log failed: %d", bile_error(db->sessions_bile)); /* update call tally for today */ date_tm = localtime((time_t *)&Time); strftime(date, sizeof(date), "%Y%m%d", date_tm); if (strcmp(date, session_today_tally.date) != 0) { strlcpy(session_today_tally.date, date, sizeof(session_today_tally.date)); session_today_tally.calls = 0; } session_today_tally.calls++; if (s->terminal_type[0] == '\0') { /* per-node setup didn't fill this in, ask the user */ session_output(s, "\r\n", 2); user_settings_renegotiate(s); session_output(s, "\r\n", 2); session_flush(s); } session_printf(s, "\r\n" "Welcome, {{B}}%s{{/B}}, you are the %ld%s caller today.\r\n", s->user ? s->user->username : GUEST_USERNAME, session_today_tally.calls, ordinal(session_today_tally.calls)); session_flush(s); uthread_yield(); if (s->user && s->user->is_sysop && (waiting = session_first_waiting_for_sysop())) { session_printf(s, "\r\n{{B}}%s{{/B}} paged sysop, answer? [Y/n] ", waiting->user ? waiting->user->username : GUEST_USERNAME); session_flush(s); sc = session_input_char(s); if (sc == 'N' || sc == 'n') { session_printf(s, "%c\r\n", sc == 'N' ? 'N' : 'n'); session_flush(s); } else { session_output(s, "\r\n", 2); session_flush(s); session_answer_page(s); } } s->area = SESSION_AREA_MOTD; session_print_motd(s, false); if (auth == AUTH_USER_SIGNUP) { auth = AUTH_USER_GUEST; session_output(s, "\r\n", 2); s->area = SESSION_AREA_SIGNUP; s->user = signup(s); } main_menu: s->area = SESSION_AREA_MAIN_MENU; session_output(s, "\r\n", 2); session_print_menu(s, false); session_flush(s); while (!done && !s->ending) { s->area = SESSION_AREA_MAIN_MENU; session_printf(s, "{{B}}Main Menu>{{/B}} "); session_flush(s); get_another_char: action = ACTION_NONE; c = session_input_char(s); if (s->ending) break; if (c == '\r' || c == 0 || c > 255) goto get_another_char; session_printf(s, "%c\r\n", c); session_flush(s); if (action == ACTION_NONE) { /* check each menu option's all_keys array for matching key */ for (i = 0; ; i++) { struct main_menu_option *option = &main_menu_options[i]; short j; if (option->action == ACTION_NONE) break; for (j = 0; option->all_keys[j] != '\0'; j++) { if (option->all_keys[j] == c) { action = option->action; break; } } if (action != ACTION_NONE) break; } } /* change actions for conditionals */ if (action == ACTION_SETTINGS_OR_SIGNUP) { if (s->user) action = ACTION_SETTINGS; else action = ACTION_SIGNUP; } else if (action == ACTION_PAGE_SEND_OR_ANSWER) { if (s->user && s->user->is_sysop) action = ACTION_PAGE_ANSWER; else action = ACTION_PAGE_SEND; } switch (action) { case ACTION_BOARD_SHOW_FIRST: s->area = SESSION_AREA_BOARDS; board_show(s, 1, NULL); break; case ACTION_CHAT: s->area = SESSION_AREA_CHAT; chat_start(s, NULL); break; case ACTION_FILES_MENU: s->area = SESSION_AREA_FILES; folder_list(s); break; case ACTION_GOODBYE: session_output_view_or_printf(s, DB_VIEW_SIGNOFF, "Goodbye!\r\n"); session_flush(s); done = true; break; case ACTION_RECENT_LOGINS: s->area = SESSION_AREA_RECENT_LOGINS; session_recents(s); break; case ACTION_MAIL_MENU: s->area = SESSION_AREA_MAIL; mail_menu(s); break; case ACTION_MAIL_COMPOSE: s->area = SESSION_AREA_MAIL; mail_compose(s, NULL, NULL, NULL, NULL); break; case ACTION_MOTD: s->area = SESSION_AREA_MOTD; session_print_motd(s, true); break; case ACTION_PAGE_ANSWER: if (!s->user || !s->user->is_sysop) { session_printf(s, invalid_option_help); session_flush(s); break; } s->area = SESSION_AREA_CHAT; session_answer_page(s); break; case ACTION_PAGE_SEND: session_page_sysop(s); break; case ACTION_SETTINGS: if (s->user) { s->area = SESSION_AREA_SETTINGS; user_settings_menu(s); } break; case ACTION_SIGNUP: s->area = SESSION_AREA_SIGNUP; if ((s->user = signup(s))) goto main_menu; break; case ACTION_WHOS_ONLINE: s->area = SESSION_AREA_WHO; session_who(s); break; case ACTION_BOARD_LIST_BOARDS: s->area = SESSION_AREA_BOARDS; board_list_boards(s); break; case ACTION_BOARD_LIST_FTN_AREAS: s->area = SESSION_AREA_FTN; board_list_ftn_areas(s); break; case ACTION_BOARD_SHOW_1: s->area = SESSION_AREA_BOARDS; board_show(s, 1, NULL); break; case ACTION_BOARD_SHOW_2: s->area = SESSION_AREA_BOARDS; board_show(s, 2, NULL); break; case ACTION_BOARD_SHOW_3: s->area = SESSION_AREA_BOARDS; board_show(s, 3, NULL); break; case ACTION_BOARD_SHOW_4: s->area = SESSION_AREA_BOARDS; board_show(s, 4, NULL); break; case ACTION_BOARD_SHOW_5: s->area = SESSION_AREA_BOARDS; board_show(s, 5, NULL); break; case ACTION_BOARD_SHOW_6: s->area = SESSION_AREA_BOARDS; board_show(s, 6, NULL); break; case ACTION_BOARD_SHOW_7: s->area = SESSION_AREA_BOARDS; board_show(s, 7, NULL); break; case ACTION_BOARD_SHOW_8: s->area = SESSION_AREA_BOARDS; board_show(s, 8, NULL); break; case ACTION_BOARD_SHOW_9: s->area = SESSION_AREA_BOARDS; board_show(s, 9, NULL); break; case ACTION_BOARD_SHOW_10: s->area = SESSION_AREA_BOARDS; board_show(s, 10, NULL); break; case ACTION_SYSOP_MENU: if (!s->user || !s->user->is_sysop) { session_printf(s, invalid_option_help); session_flush(s); break; } sysop_menu(s); break; case ACTION_SHOW_MENU: if (last_c == c) /* asking twice in a row will print the full menu */ session_print_menu(s, false); else session_print_menu(s, true); session_flush(s); break; default: session_printf(s, invalid_option_help); session_flush(s); break; } last_c = c; } session_close(s); } void session_print_menu(struct session *session, bool short_menu) { struct main_menu_option *option = NULL; size_t size; short i; char *output = NULL; if (short_menu && session_output_view_or_printf(session, DB_VIEW_SHORTMENU, NULL) != 0) return; if (session_output_view_or_printf(session, DB_VIEW_MENU, NULL) != 0) return; for (i = 0; ; i++) { option = &main_menu_options[i]; if (option->action == ACTION_NONE) break; if (option->menu_key == 0 || option->label[0] == '\0') continue; size = session_expand_template(session, option->label, &output); if (!size) continue; session_printf(session, "{{B}}%c{{/B}}: %s\r\n", option->menu_key, output); xfree(&output); } } void session_close(struct session *session) { unsigned long now; short n; bool found; if (!session->ending) { if (!session->ban_node_source) { /* give 1 second to flush output */ now = Ticks; while (session->obuflen && (Ticks - now) < 60) { session->node_funcs->output(session); uthread_yield(); } } session->ending = true; } if (session->log.logged_on_at) { /* finalize session log */ session->log.logged_off_at = Time; if (bile_write(db->sessions_bile, SL_LOG_RTYPE, session->log.id, &session->log, sizeof(session->log)) != sizeof(session->log)) panic("bile_write of session log failed: %d", bile_error(db->sessions_bile)); } /* close the node */ session->node_funcs->close(session); /* remove session from sessions */ found = false; for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == session) { sessions[n] = NULL; found = true; break; } } if (!found) panic("session_close failed to find session to remove"); nsessions--; if (session->user) xfree(&session->user); xfree(&session); logger_update_title(); } void session_check_buf_canaries(struct session *session) { if (session->obuf_canary != OBUF_CANARY) warn("obuf canary dead"); if (session->ibuf_canary != IBUF_CANARY) warn("ibuf canary dead"); if (session->obuflen_canary != OBUFLEN_CANARY) warn("obuflen canary dead"); if (session->ibuflen_canary != IBUFLEN_CANARY) warn("ibuflen canary dead"); } void session_flush(struct session *session) { if (session->ending || session->obuflen == 0) return; while (session->obuflen != 0) { session->node_funcs->output(session); session_check_buf_canaries(session); if (session->obuflen != 0) uthread_yield(); if (session->ending) return; /* * The console will leave \e in the output buffer if it's the only * thing in there, so don't loop forever. */ if (session->obuflen == 1 && session->obuf[0] == '\33') return; } } size_t session_log(struct session *session, const char *str) { return logger_printf("[%s] [%s] %s", session->node, session->logged_in ? (session->user ? session->user->username : GUEST_USERNAME) : "-", str); } size_t session_logf(struct session *session, const char *format, ...) { va_list ap; va_start(ap, format); vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap); va_end(ap); return session_log(session, session_log_tbuf); } size_t session_logf_buffered(struct session *session, const char *format, ...) { va_list ap; size_t ret; va_start(ap, format); vsnprintf(session_log_tbuf, sizeof(session_log_tbuf), format, ap); va_end(ap); logger->autoflush = false; ret = session_log(session, session_log_tbuf); logger->autoflush = true; return ret; } size_t session_output(struct session *session, const char *str, size_t len) { size_t chunk, olen = len, stroff = 0; while (len && !session->ending) { chunk = (sizeof(session->obuf) - session->obuflen); if (chunk == 0) { session_flush(session); if (session->ending) return 0; continue; } if (chunk > len) chunk = len; memcpy(session->obuf + session->obuflen, str + stroff, chunk); session->obuflen += chunk; if (session->obuflen > sizeof(session->obuf)) { warn("[%s] Bogus obuflen %d > %ld", session->node, session->obuflen, sizeof(session->obuf)); session->ending = true; session_close(session); return 0; } stroff += chunk; len -= chunk; } session_check_buf_canaries(session); return olen; } size_t session_paginate(struct session *session, const char *str, size_t len, size_t first_page_printed) { size_t olen = 0, n, adv; size_t olines = first_page_printed; size_t line_len = session->terminal_columns; ssize_t last_space = -1; short last_input = 0; while (len && !session->ending) { for (n = 0, adv = 0; n < line_len && n < len; n++) { if (str[n] == '\r') { last_space = n; adv = n + 1; if (str[n + 1] == '\n') adv++; break; } else if (str[n] == '\n') { last_space = n; adv = n + 1; break; } else if (str[n] == ' ') { last_space = n; adv = n + 1; } } if (last_space == -1) { /* no space, break hard */ last_space = MIN(line_len, len); adv = last_space; } if (len == n) adv = last_space = len; if (last_space > 0) { session_output(session, str, last_space); olen += last_space; } str += adv; len -= adv; session_output(session, "\r\n", 2); olen += 2; olines++; if (len && (olines == session->terminal_lines - 1 || last_input == '\r' || last_input == '\n')) { olines = 0; session_printf(session, "-- More --"); session_flush(session); last_input = session_input_char(session); if (session->vt100) session_printf(session, "\r%s", ansi(session, ANSI_ERASE_LINE, ANSI_END)); else session_output(session, "\r", 1); if (last_input == 'q' || last_input == 'Q') break; } } return olen; } size_t session_printf(struct session *session, const char *format, ...) { va_list ap; size_t len; va_start(ap, format); len = session_vprintf(session, format, ap); va_end(ap); return len; } size_t session_vprintf(struct session *session, const char *format, va_list ap) { static char session_printf_ebuf[160], session_printf_tbuf[160]; size_t len, n, en; bool stop = false; /* avoid a full session_expand_template for {{B}}, {{/B}}, and {{#}} */ session_printf_ebuf[0] = '\0'; for (n = 0, en = 0; format[n] != '\0'; n++) { if (!stop) { if (format[n] == '{' && format[n + 1] == '{' && format[n + 2] == 'B' && format[n + 3] == '}' && format[n + 4] == '}') { en = strlcat(session_printf_ebuf, ansi_bold(session), sizeof(session_printf_ebuf)); n += 4; continue; } if (format[n] == '{' && format[n + 1] == '{' && format[n + 2] == '/' && format[n + 3] == 'B' && format[n + 4] == '}' && format[n + 5] == '}') { en = strlcat(session_printf_ebuf, ansi_reset(session), sizeof(session_printf_ebuf)); n += 5; continue; } if (format[n] == '{' && format[n + 1] == '{' && format[n + 2] == '#' && format[n + 3] == '}' && format[n + 4] == '}') { stop = true; n += 4; continue; } } session_printf_ebuf[en++] = format[n]; if (en >= sizeof(session_printf_ebuf) - 1) panic("session_printf_ebuf overflow!"); session_printf_ebuf[en] = '\0'; } len = vsnprintf(session_printf_tbuf, sizeof(session_printf_tbuf), session_printf_ebuf, ap); if (len > sizeof(session_printf_tbuf)) panic("session_printf overflow! (%ld > %ld)", len, sizeof(session_printf_tbuf)); return session_output(session, session_printf_tbuf, len); } size_t session_output_view_or_printf(struct session *session, short id, const char *format, ...) { size_t size; char *output; va_list ap; /* can't use bile_read_alloc because we need to null terminate */ if (db->views[id] == NULL || db->views[id][0] == '\0') { if (format == NULL) return 0; va_start(ap, format); size = session_vprintf(session, format, ap); va_end(ap); return size; } size = session_expand_template(session, db->views[id], &output); if (!size) return 0; size = session_output(session, output, size); xfree(&output); return size; } bool session_idled_out(struct session *session) { if (session->logged_in) { if (session->user && session->user->is_sysop) { if (db->config.max_sysop_idle_minutes != 0 && Time - session->last_input_at > (db->config.max_sysop_idle_minutes * 60)) return true; } else { if (db->config.max_idle_minutes != 0 && Time - session->last_input_at > (db->config.max_idle_minutes * 60)) return true; } } else { if (Time - session->established_at > db->config.max_login_seconds) return true; } return false; } short session_get_char(struct session *session, unsigned char *b) { return session_get_chars(session, b, 1); } short session_get_chars(struct session *session, unsigned char *b, size_t count) { short ret = 0; while (ret < count) { if (session->ibuflen == 0) { if (session_idled_out(session)) return 0; uthread_yield(); session->node_funcs->input(session); if (session->ending || session->abort_input) return 0; if (session->ibuflen == 0) return 0; } session->last_input_at = Time; *b = session->ibuf[session->ibufoff]; b++; if (session->ibuflen == 1) { session->ibuflen = 0; session->ibufoff = 0; } else { session->ibuflen--; session->ibufoff++; } ret++; } session_check_buf_canaries(session); return ret; } bool session_wait_for_chars(struct session *session, unsigned short timeout_ms, unsigned short num_chars) { unsigned long expire = 0; if (timeout_ms) expire = Ticks + (timeout_ms / ((double)1000 / (double)60)); while (session->ibuflen < num_chars) { session->node_funcs->input(session); if (session->ending || session->abort_input) return false; if (session->obuflen != 0 && session->chatting) return false; if (expire && Ticks > expire) return false; if (session_idled_out(session)) return false; if (session->ibuflen >= num_chars) break; uthread_yield(); } session->last_input_at = Time; session_check_buf_canaries(session); return true; } unsigned short session_input_char(struct session *session) { unsigned short ret; short waiting_for = 1; short consumed = 0; wait_for_char: if (session->ibuflen < waiting_for && !session_wait_for_chars(session, 0, waiting_for)) { if (session_idled_out(session)) goto idled_out; return 0; } if (session->ibuf[0] == ESC) { /* look for a VT100 escape sequence */ if (session->ibuflen == 1) { waiting_for = 2; goto wait_for_char; } else if (session->ibuf[1] == '[') { if (session->ibuflen == 2) { waiting_for = 3; goto wait_for_char; } else { consumed = 3; switch (session->ibuf[2]) { case 'A': ret = KEY_UP; break; case 'B': ret = KEY_DOWN; break; case 'C': ret = KEY_RIGHT; break; case 'D': ret = KEY_LEFT; break; default: /* probably not good to pass it through as-is */ ret = KEY_OTHER; } goto done_consuming; } } } ret = session->ibuf[0]; consumed = 1; done_consuming: if (session->ibuflen == consumed) session->ibuflen = 0; else { memmove(session->ibuf, session->ibuf + consumed, session->ibuflen - consumed); session->ibuflen--; } if (session->last_input == '\r' && (ret == '\n' || ret == 0)) { /* we already responded to the \r, just ignore this */ session->last_input = ret; goto wait_for_char; } session->last_input = ret; return ret; idled_out: if (session->logged_in) session_logf(session, "Idle too long, logging out"); else session_logf(session, "Took too long to login, disconnecting"); session_printf(session, "\r\n\r\nYou have been idle too long, goodbye.\r\n\r\n"); session_flush(session); session->ending = true; return 0; } void session_clear_input(struct session *session) { uthread_yield(); session->node_funcs->input(session); uthread_yield(); session->node_funcs->input(session); session->ibuflen = 0; } /* * Collect up to size-1 bytes of input with trailing null, in a field * width columns wide, optionally masking echoed output with mask char. * For multiline-capable input, */ char * session_field_input(struct session *session, unsigned short size, unsigned short width, char *initial_input, bool multiline, char mask) { short ilen = 0, ipos = 0, over; long n; char *field; unsigned short c; unsigned char chc; bool redraw = false; session->abort_input = false; field = xmalloczero(size); if (field == NULL) { session->ending = true; return NULL; } if (initial_input) { ipos = ilen = strlcpy(field, initial_input, size); /* TODO: handle initial value being longer than width */ if (mask) { for (n = 0; n < ilen; n++) session_output(session, &mask, 1); } else session_output(session, field, ilen); session_flush(session); } for (;;) { c = session_input_char(session); if (session->ending || session->abort_input) goto field_input_bail; switch (c) { case 0: /* session_input_char bailed */ if (session->obuflen > 0) { session->node_funcs->output(session); uthread_yield(); if (session->ending) goto field_input_bail; } break; case CONTROL_D: /* ^D */ return field; case CONTROL_C: /* ^C */ if (multiline) return field; goto field_input_bail; case BACKSPACE: /* ^H */ case DELETE: /* backspace through telnet */ case KEY_LEFT: if (ipos == 0) continue; if (ipos < ilen && c != KEY_LEFT) { redraw = true; memmove(field + ipos - 1, field + ipos, ilen - 1); } if (field[ipos - 1] == '\n') { /* need to jump up a line and go over */ session_printf(session, ansi(session, ANSI_UP_N, 1, ANSI_END)); over = 1; for (n = ipos - 2; n >= 0; n--) { if (field[n] == '\n') break; over++; } session_printf(session, ansi(session, ANSI_COL_N, over, ANSI_END)); session_flush(session); } ipos--; if (c != KEY_LEFT) { ilen--; field[ilen] = '\0'; session_printf(session, ansi(session, ANSI_BACKSPACE, ANSI_END)); } if (redraw) { session_printf(session, ansi(session, ANSI_SAVE_CURSOR, ANSI_END)); session_output(session, field + ipos, ilen - ipos); session_output(session, " ", 1); session_printf(session, ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END)); } session_flush(session); break; case '\r': case '\n': if (multiline) goto append_char; return field; case KEY_RIGHT: if (ipos == ilen) continue; ipos++; session_printf(session, ansi(session, ANSI_FORWARD_N, 1, ANSI_END)); session_flush(session); break; default: append_char: if ((c < 32 || c > 127) && (c != '\r' && c != '\n')) break; if (ilen >= size - 1) break; chc = c; if (ipos < ilen) memmove(field + ipos + 1, field + ipos, ilen - ipos); field[ipos] = chc; ipos++; ilen++; field[ilen] = '\0'; session_output(session, mask ? &mask : (char *)&chc, 1); if (ipos < ilen) { if (mask) { /* TODO: repeat mask */ } else { session_printf(session, ansi(session, ANSI_SAVE_CURSOR, ANSI_END)); session_output(session, field + ipos, ilen - ipos); session_printf(session, ansi(session, ANSI_RESTORE_SAVED_CURSOR, ANSI_END)); } } if (chc == '\r') { c = '\n'; goto append_char; } session_flush(session); } } field_input_bail: xfree(&field); return NULL; } short session_login(struct session *s) { struct user *user = NULL; char junk[SHA256_DIGEST_STRING_LENGTH]; char *username = NULL, *password = NULL; size_t len; short n; for (n = 1; n <= 3; n++) { session_printf(s, "Username: "); if (s->autologin_username[0]) { session_printf(s, "{{#}}%s\r\n", s->autologin_username); session_flush(s); username = xstrdup(s->autologin_username); if (username == NULL) goto login_bail; } else { session_flush(s); username = session_field_input(s, DB_USERNAME_LENGTH + 1, DB_USERNAME_LENGTH, NULL, false, 0); session_output(s, "\r\n", 2); session_flush(s); if (username == NULL || s->ending) goto login_bail; if (username[0] == '\0') { n--; xfree(&username); username = NULL; continue; } } if (strcasecmp(username, GUEST_USERNAME) == 0) { session_logf(s, "Successful guest login in as %s", username); xfree(&username); return AUTH_USER_GUEST; } if ((strcasecmp(username, "signup") == 0 || strcasecmp(username, "new") == 0) && db->config.open_signup) { session_logf(s, "Successful guest signup login in as %s", username); xfree(&username); return AUTH_USER_SIGNUP; } user = user_find_by_username(username); if (!user && user_username_is_banned(username)) { session_logf(s, "Attempted login as banned username %s", username); s->ban_node_source = 1; goto login_bail; } if (s->autologin_username[0]) { if (user) { xfree(&username); s->user = user; session_logf(s, "Automatically logged in as %s", s->autologin_username); return AUTH_USER_OK; } memset(s->autologin_username, 0, sizeof(s->autologin_username)); } session_printf(s, "Password: "); session_flush(s); password = session_field_input(s, 64, 64, NULL, false, '*'); session_output(s, "\r\n", 2); session_flush(s); if (password == NULL || s->ending) goto login_bail; if (user) { if (user_authenticate(user, password) == AUTH_USER_OK) { if (user->is_enabled) s->user = user; else session_logf(s, "Successful password login for %s but " "account is disabled", user->username); } else session_logf(s, "Failed password login for %s", user->username); } else { /* kill some time */ SHA256Data((const u_int8_t *)password, strlen(password), junk); session_logf(s, "Failed password login for invalid user %s", username); } len = strlen(username); memset(username, 0, len); xfree(&username); username = NULL; len = strlen(password); memset(password, 0, len); xfree(&password); password = NULL; if (s->user) { session_logf(s, "Successful password login for user %s", s->user->username); return AUTH_USER_OK; } uthread_msleep(60); session_printf(s, "Login incorrect\r\n"); session_flush(s); if (user) xfree(&user); } login_bail: if (username != NULL) xfree(&username); if (password != NULL) xfree(&password); if (!s->ban_node_source) { if (session_idled_out(s)) { session_logf(s, "Login timed out after %d seconds", db->config.max_login_seconds); session_printf(s, "\r\nLogin timed out after %d seconds\r\n", db->config.max_login_seconds); /* session_flush won't do anything since s->ending is set */ s->node_funcs->output(s); } else { session_printf(s, "Thanks for playing\r\n"); session_flush(s); } } return AUTH_USER_FAILED; } size_t session_expand_template(struct session *session, const char *tmpl, char **ret) { static char curvar[128], matchvar[128]; size_t retsize, retpos; size_t vallen; short n, invar = 0, varlen = 0, doif, sep; char *varseek, *curvarpos, *val, *data; bool end_expansion = false; retsize = 0; retpos = 0; data = NULL; for (n = 0; tmpl[n] != '\0'; n++) { if (invar) { if (!varlen && (tmpl[n] == ' ' || tmpl[n] == '\t')) continue; if (tmpl[n] == '}' && tmpl[n + 1] == '}') { n++; goto expand_var; } else if (varlen < sizeof(curvar) - 2) curvar[varlen++] = tmpl[n]; } else if (!end_expansion && tmpl[n] == '{' && tmpl[n + 1] == '{') { invar = 1; varlen = 0; n++; } else if (tmpl[n] == '\r' && tmpl[n + 1] != '\n') { if (!grow_to_fit(&data, &retsize, retpos, 2, 64)) { xfree(&data); break; } data[retpos++] = '\r'; data[retpos++] = '\n'; } else { if (!grow_to_fit(&data, &retsize, retpos, 1, 64)) { xfree(&data); break; } data[retpos++] = tmpl[n]; } continue; expand_var: invar = 0; curvar[varlen] = '\0'; varlen = 0; if (strpos_quoted(curvar, '?') != -1 && strpos_quoted(curvar, ':') != -1) { /* ternary: user ? "a string" : time */ varseek = curvar; while (varseek[0] == ' ') varseek++; sep = strpos_quoted(varseek, '?'); if (sep == -1) continue; memcpy(matchvar, varseek, sep); matchvar[sep] = '\0'; curvarpos = varseek + sep + 1; while (matchvar[sep - 1] == ' ') matchvar[--sep] = '\0'; /* matchvar is now the condition */ if (strcmp(matchvar, "user") == 0) doif = (session->user != NULL); else if (strcmp(matchvar, "sysop") == 0) doif = (session->user && session->user->is_sysop); else doif = 0; while (curvarpos[0] == ' ') curvarpos++; sep = strpos_quoted(curvarpos, ':'); if (sep == -1) continue; while (curvarpos[sep] == ' ') { curvarpos++; sep--; } if (doif) { /* copy the "then" */ memcpy(matchvar, curvarpos, sep); matchvar[sep] = '\0'; } else { /* copy from after the "then" and : */ strlcpy(matchvar, curvarpos + sep + 1, sizeof(matchvar)); } sep = strlen(matchvar); while (matchvar[sep - 1] == ' ') matchvar[--sep] = '\0'; vallen = session_expand_var(session, matchvar, &val, &end_expansion); } else { vallen = session_expand_var(session, curvar, &val, &end_expansion); } if (vallen) { if (!grow_to_fit(&data, &retsize, retpos, vallen, 128)) { xfree(&data); break; } memcpy(data + retpos, val, vallen); retpos += vallen; } } if (data == NULL) { *ret = NULL; return 0; } data[retpos] = '\0'; *ret = data; return retpos; } size_t session_expand_var(struct session *session, char *ivar, char **ret, bool *end_expansion) { static char var[128], retval[128]; size_t retsize, retlen, varlen, unread_count; long elapsed, telapsed; struct tm *now; short count, n; bool pad = false; *ret = (char *)&retval; retlen = 0; if (sscanf(ivar, "%127[^|]|%lu%n", &var, &retsize, &count) == 2 && count > 0) { /* field of fixed length, either truncated or padded */ if (retsize > sizeof(retval)) retsize = sizeof(retval); pad = true; varlen = strlen(var); } else { while (ivar[0] == ' ') ivar++; varlen = strlcpy(var, ivar, sizeof(var)); retsize = sizeof(retval); } while (varlen && var[varlen - 1] == ' ') { var[varlen - 1] = '\0'; varlen--; } if (strcmp(var, "B") == 0) { retlen = strlcpy(retval, ansi_bold(session), retsize); } else if (strcmp(var, "/B") == 0) { retlen = strlcpy(retval, ansi_reset(session), retsize); } else if (strcmp(var, "#") == 0) { *end_expansion = true; retlen = 0; } else if (strncmp(var, "center_", 7) == 0 && sscanf(var, "center_%d", &count) == 1) { if (session->terminal_columns <= count) retlen = 0; else { retlen = MIN((session->terminal_columns - count) / 2, retsize); for (n = 0; n < retlen; n++) retval[n] = ' '; retval[retlen] = '\0'; } } else if (strcmp(var, "logged_in_time") == 0) { memset(retval, 0, retsize); retlen = 0; elapsed = Time - session->log.logged_on_at; while (elapsed > 0) { if (elapsed < 60) { telapsed = elapsed; retlen += snprintf(retval + retlen, retsize - retlen, "%lus", telapsed); } else if (elapsed < (60 * 60)) { telapsed = elapsed / 60; retlen += snprintf(retval + retlen, retsize - retlen, "%lum", telapsed); telapsed *= 60; } else if (elapsed < (60 * 60 * 24)) { telapsed = elapsed / (60 * 60); retlen += snprintf(retval + retlen, retsize - retlen, "%luh", telapsed); telapsed *= (60 * 60); } else { telapsed = elapsed / (60 * 60 * 24); retlen += snprintf(retval + retlen, retsize - retlen, "%luh", telapsed); telapsed *= (60 * 60 * 24); } elapsed -= telapsed; } } else if (strcmp(var, "node") == 0) { retlen = strlcpy(retval, session->node, retsize); } else if (strcmp(var, "phone_number") == 0) { retlen = strlcpy(retval, db->config.phone_number, retsize); } else if (strcmp(var, "time") == 0) { now = localtime((time_t *)&Time); retlen = strftime(retval, retsize, "%H:%M", now); } else if (strcmp(var, "timezone") == 0) { retlen = strlcpy(retval, db->config.timezone, retsize); } else if (strcmp(var, "username") == 0) { retlen = strlcpy(retval, session->user ? session->user->username : GUEST_USERNAME, retsize); } else if (strcmp(var, "new_mail") == 0) { if (session->user) { mail_find_ids_for_user(session->user, &unread_count, NULL, 0, 0, true); if (unread_count) retlen = sprintf(retval, "(%lu New)", unread_count); } } else if (var[0] == '"') { /* a literal string, remove leading and trailing quotes */ var[varlen - 1] = '\0'; varlen--; if (varlen > retsize) varlen = retsize; strlcpy(retval, var + 1, varlen); retlen = varlen - 1; } else { /* shrug */ strlcpy(retval, var, varlen); retlen = varlen; } if (pad && retlen < retsize) { while (retlen < retsize) retval[retlen++] = ' '; retval[retlen] = '\0'; } return retlen; } void session_pause_return(struct session *s, char enforce, char *msg) { unsigned char c; session_printf(s, "{{/B}}Press "); if (enforce == 0 || enforce == '\r') session_printf(s, "{{B}}{{/B}} "); else if (enforce == CONTROL_C) session_printf(s, "{{B}}^C{{/B}} "); else session_printf(s, "{{B}}%c{{/B}} ", enforce); if (msg) session_printf(s, msg); else session_printf(s, "to return to the main menu..."); session_flush(s); for (;;) { c = session_input_char(s); if (s->ending) return; if (c == 0) continue; if (enforce == 0 || enforce == c) break; } session_output(s, "\r\n", 2); session_flush(s); } void session_page_sysop(struct session *s) { char *message = NULL; session_printf(s, "{{B}}Page Sysop{{/B}} " "(^C to cancel)\r\n" "{{B}}-------------------------{{/B}}\r\n"); session_output_view_or_printf(s, DB_VIEW_PAGE_SYSOP, "(Instructions missing)\r\n"); session_flush(s); session_printf(s, "{{B}}Message:{{/B}} "); session_flush(s); message = session_field_input(s, 64, 64, NULL, false, 0); session_output(s, "\r\n", 2); session_flush(s); if (message == NULL || s->ending) goto page_done; session_logf(s, "Paging sysop: %s", message); progress("Page from %s: %s", s->user ? s->user->username : GUEST_USERNAME, message); SysBeep(30); uthread_yield(); chat_start(s, CHAT_WITH_SYSOP); page_done: progress(NULL); if (message != NULL) xfree(&message); } struct session * session_first_waiting_for_sysop(void) { short n; for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == NULL || !sessions[n]->logged_in || !sessions[n]->chatting) continue; if (strcmp(sessions[n]->chatting_with_node, CHAT_WITH_SYSOP) != 0) continue; return sessions[n]; } return NULL; } void session_answer_page(struct session *s) { struct session *waiting; waiting = session_first_waiting_for_sysop(); if (waiting == NULL) { session_printf(s, "No users waiting to chat.\r\n"); session_flush(s); return; } progress(NULL); session_logf(s, "Answering page from %s on %s", waiting->user ? waiting->user->username : GUEST_USERNAME, waiting->node); /* point paging user's chatting_with_node to ours */ strlcpy(waiting->chatting_with_node, s->node, sizeof(waiting->chatting_with_node)); /* and join the party */ chat_start(s, waiting->node); } void session_print_motd(struct session *s, bool force) { struct bile_object *o; unsigned long last_seen_motd = 0, motd_id = 0; size_t size; char *view = NULL, *output = NULL; if (s->user) last_seen_motd = s->user->last_motd_seen; o = bile_get_nth_of_type(db->bile, 0, DB_MOTD_RTYPE); if (o == NULL || (!force && o->id <= last_seen_motd)) { if (o) xfree(&o); return; } motd_id = o->id; view = xmalloc(o->size + 1); if (view == NULL) { s->ending = true; xfree(&o); return; } size = bile_read_object(db->bile, o, view, o->size); view[size] = '\0'; xfree(&o); size = session_expand_template(s, view, &output); xfree(&view); if (!size) return; session_output(s, "\r\n", 2); session_output(s, output, size); session_output(s, "\r\n\r\n", 4); xfree(&output); session_pause_return(s, 0, "to continue..."); if (s->user && !force) { s->user->last_motd_seen = motd_id; user_save(s->user); } } void session_recents(struct session *s) { struct session_log slog; struct tm *date_tm; size_t scount, rsize, *ids; char sdate[12]; short printed; session_printf(s, "{{B}}Recent Logins{{/B}}\r\n"); session_printf(s, "{{B}}Date User Via Speed Location{{/B}}\r\n"); session_flush(s); scount = bile_sorted_ids_by_type(db->sessions_bile, SL_LOG_RTYPE, &ids); for (printed = 0; printed < 10 && printed < scount; printed++) { rsize = bile_read(db->sessions_bile, SL_LOG_RTYPE, ids[scount - 1 - printed], (char *)&slog, sizeof(slog)); if (rsize != sizeof(slog)) panic("unexpected bile_read response %d (read %ld, " "expected %ld)", bile_error(db->sessions_bile), rsize, sizeof(slog)); date_tm = localtime((time_t *)&slog.logged_on_at); strftime(sdate, sizeof(sdate), "%m/%d", date_tm); session_printf(s, "%-5s %-20s %-7s %-6u %-32s\r\n", sdate, slog.username, slog.via, slog.tspeed, slog.location); } session_output(s, "\r\n", 2); session_flush(s); session_pause_return(s, 0, NULL); xfree(&ids); } void session_who(struct session *s) { char idle_s[20]; char username[20]; unsigned long idle; short n; session_printf(s, "{{B}}Who's Online{{/B}}\r\n"); session_printf(s, "{{B}}Node User Via Speed Cur Area Idle Location{{/B}}\r\n"); session_flush(s); for (n = 0; n < MAX_SESSIONS; n++) { if (sessions[n] == NULL || !sessions[n]->logged_in) continue; idle = Time - sessions[n]->last_input_at; if (idle < 60) snprintf(idle_s, sizeof(idle_s), "%lus", idle); else if (idle < (60 * 60)) snprintf(idle_s, sizeof(idle_s), "%lum", idle / 60); else if (idle < (60 * 60 * 24)) snprintf(idle_s, sizeof(idle_s), "%luh", idle / (60 * 60)); else snprintf(idle_s, sizeof(idle_s), "%lud", idle / (60 * 60 * 24)); snprintf(username, sizeof(username), "%s%s", sessions[n]->user ? sessions[n]->user->username : GUEST_USERNAME, sessions[n]->user && sessions[n]->user->is_sysop ? " (sysop)" : ""); session_printf(s, "%-7s %-17s %-7s %-6u %-10s %-5s %-22s\r\n", sessions[n]->node, username, sessions[n]->via, sessions[n]->tspeed, area_labels[sessions[n]->area], idle_s, sessions[n]->location); } session_output(s, "\r\n", 2); session_flush(s); session_pause_return(s, 0, NULL); } char session_menu(struct session *s, char *title, char *prompt, char *prompt_help, const struct session_menu_option *opts, size_t nopts, bool show_opts, char *number_prompt, short *ret_number) { size_t n; short j, num; unsigned short c; char cn[2], *num_input = NULL; short invalids = 0; if (show_opts && title != NULL) { session_printf(s, "{{B}}%s{{/B}}\r\n", title); session_flush(s); } print_options: if (show_opts) { for (n = 0; n < nopts; n++) { if (opts[n].key[0] == '\0') continue; session_printf(s, "{{B}}%c{{/B}}: %s\r\n", opts[n].key[0], opts[n].title); } session_flush(s); } #if 0 if (prompt_help) { session_printf(s, "{{B}}%s{{/B}}\r\n", prompt_help); session_flush(s); } #endif for (;;) { show_prompt: session_printf(s, "{{B}}%s>{{/B}} ", prompt); session_flush(s); get_menu_option: uthread_yield(); c = session_input_char(s); if (s->ending) return 0; if (c == '\r' || c == 0 || c > 255) goto get_menu_option; for (n = 0; n < nopts; n++) { if (opts[n].ret == '#' && c >= '0' && c <= '9') { session_printf(s, "%s:%s>%s ", ansi(s, ANSI_BACKSPACE, ANSI_BACKSPACE, ANSI_BOLD, ANSI_END), number_prompt, ansi(s, ANSI_RESET, ANSI_END)); session_flush(s); cn[0] = c; cn[1] = '\0'; num_input = session_field_input(s, 4, 3, (char *)&cn, false, 0); session_printf(s, "\r\n"); session_flush(s); if (num_input == NULL) goto show_prompt; if (num_input[0] == '\0') { xfree(&num_input); goto show_prompt; } num = atoi(num_input); xfree(&num_input); *ret_number = num; return opts[n].ret; } for (j = 0; ; j++) { if (opts[n].key[j] == '\0') break; if (opts[n].key[j] == c) { session_printf(s, "%c\r\n", c); session_flush(s); return opts[n].ret; } } } /* * Avoid a constant back-and-forth of the user's session spewing * bogus characters and us having to print 'invalid option' over * and over. */ if (invalids > 3) goto get_menu_option; session_printf(s, "%c\r\n", c); session_printf(s, invalid_option_help); session_flush(s); invalids++; } } void session_prune_logs(short days) { struct bile_object *obj; struct session_log log; size_t n, size = 0, count = 0, delta; unsigned long secs, *ids = NULL; if (days < 1) return; secs = (60UL * 60UL * 24UL * (unsigned long)days); logger_printf("[db] Pruning session logs older than %d days", days); for (n = 0; (obj = bile_get_nth_of_type(db->sessions_bile, n, SL_LOG_RTYPE)); n++) { bile_read_object(db->sessions_bile, obj, &log, sizeof(log)); delta = Time - log.logged_on_at; if (delta > secs) { if (!grow_to_fit(&ids, &size, count * sizeof(unsigned long), sizeof(unsigned long), 10 * sizeof(unsigned long))) break; ids[count++] = obj->id; } xfree(&obj); } uthread_yield(); if (count) { logger_printf("[db] Deleting %ld of %ld log entries", count, n); for (n = 0; n < count; n++) bile_delete(db->sessions_bile, SL_LOG_RTYPE, ids[n], 0); bile_write_map(db->sessions_bile); } if (ids) xfree(&ids); }