From ad66313883d3bcae084b556445ccce501a0c7918 Mon Sep 17 00:00:00 2001 From: ~keith Date: Fri, 20 May 2022 05:31:14 +0000 Subject: [PATCH] it works! --- hlogin.c | 560 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- ui.c | 258 ++++++++++++++++++++----- ui.h | 7 +- 3 files changed, 751 insertions(+), 74 deletions(-) diff --git a/hlogin.c b/hlogin.c index 7aabfc2..d547d81 100644 --- a/hlogin.c +++ b/hlogin.c @@ -17,6 +17,8 @@ * You should have received a copy of the GNU General Public License * along with hlogin. If not, see . */ +#define __USE_GNU +#define _GNU_SOURCE #include "config.h" #include @@ -26,6 +28,10 @@ #include #include "gettext.h" +#define VAR_(x) #x +#define VAR(x) VAR_(x) +#pragma message "LOCALEDIR=" VAR(LOCALEDIR) + #include #include #include @@ -33,13 +39,16 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include #include "ui.h" -#define VAR_(x) #x -#define VAR(x) VAR_(x) -#pragma message "LOCALEDIR=" VAR(LOCALEDIR) - int pam_conv_handler(int msgc, const struct pam_message *msgv[], struct pam_response **resp, void *appdata_ptr) { if (msgc <= 0) @@ -49,12 +58,21 @@ int pam_conv_handler(int msgc, const struct pam_message *msgv[], struct pam_resp if (!result) return PAM_CONV_ERR; + char *info_concat = NULL; + size_t info_len; + for (int i = 0; i < msgc; i++) { int r; char *text; switch (msgv[i]->msg_style) { case PAM_PROMPT_ECHO_OFF: case PAM_PROMPT_ECHO_ON: + if (info_concat) { + ui_setup_message(info_concat); + ui_message_run(); + free(info_concat); + info_concat = NULL; + } ui_setup_dialog((char *) msgv[i]->msg, msgv[i]->msg_style == PAM_PROMPT_ECHO_OFF); r = ui_run(); text = ui_get_text(); @@ -68,12 +86,40 @@ int pam_conv_handler(int msgc, const struct pam_message *msgv[], struct pam_resp break; case PAM_ERROR_MSG: case PAM_TEXT_INFO: - goto FAILURE; // TODO implement info/error dialog + if (!info_concat) { + info_len = strlen((char *) msgv[i]->msg); + info_concat = realloc(info_concat, sizeof(char) * (info_len + 1)); + strcpy(info_concat, (char *) msgv[i]->msg); + info_concat[info_len] = '\0'; + } else { + info_concat[info_len] = '\n'; + size_t pos = info_len + 1; + info_len = pos + strlen((char *) msgv[i]->msg); + info_concat = realloc(info_concat, sizeof(char) * (info_len + 1)); + strcpy(info_concat + pos, (char *) msgv[i]->msg); + info_concat[info_len] = '\0'; + } + result[i].resp_retcode = 0; + result[i].resp = NULL; + break; default: + if (info_concat) { + ui_setup_message(info_concat); + ui_message_run(); + free(info_concat); + info_concat = NULL; + } goto FAILURE; } } + if (info_concat) { + ui_setup_message(info_concat); + ui_message_run(); + free(info_concat); + info_concat = NULL; + } + *resp = result; return PAM_SUCCESS; @@ -91,8 +137,167 @@ FAILURE: return PAM_CONV_ERR; } -/** number of seconds to hang after a login failure or error **/ +/** number of seconds to hang after a login failure or error */ unsigned int fail_delay = 3; +/** maximum number of login attempts before a failure */ +unsigned int max_tries = 5; + +char *origin = "test_origin"; +char *tty_path = NULL; +char *tty_name = NULL; +char *tty_id = NULL; +char *remote_host = NULL; + +pid_t login_pid; + +void log_utmp(char *login_name) +{ + struct utmpx ut; + memset(&ut, 0, sizeof(ut)); + struct utmpx *ent; + + setutxent(); + while ((ent = getutxent())) + if (ent->ut_pid == login_pid && ent->ut_type >= INIT_PROCESS && ent->ut_type <= DEAD_PROCESS) + break; + + // try by tty_name + if (ent == NULL && tty_name) { + setutxent(); + ut.ut_type = LOGIN_PROCESS; + strncpy(ut.ut_line, tty_name, sizeof(ut.ut_line)); + ent = getutxline(&ut); + } + + // try by tty_id + if (ent == NULL && tty_id) { + setutxent(); + ut.ut_type = DEAD_PROCESS; + strncpy(ut.ut_id, tty_id, sizeof(ut.ut_id)); + ent = getutxid(&ut); + } + + // copy or create + if (ent) + memcpy(&ut, ent, sizeof(ut)); + else + memset(&ut, 0, sizeof(ut)); + + // update utmp entry + strncpy(ut.ut_user, login_name, sizeof(ut.ut_user)); + ut.ut_type = USER_PROCESS; + ut.ut_pid = login_pid; + if (tty_id && ut.ut_id[0] == 0) + strncpy(ut.ut_id, tty_id, sizeof(ut.ut_id)); + if (tty_name) + strncpy(ut.ut_line, tty_name, sizeof(ut.ut_line)); + if (remote_host) + strncpy(ut.ut_host, remote_host, sizeof(ut.ut_host)); + // TODO copy remote_addr + + // set time + struct timeval tv; + gettimeofday(&tv, NULL); + ut.ut_tv.tv_sec = tv.tv_sec; + ut.ut_tv.tv_usec = tv.tv_usec; + + pututxline(&ut); + endutxent(); + updwtmpx(_PATH_WTMP, &ut); +} + +const char *CANCEL_SENTINEL = "\0CANCEL\0"; + +/** try_login - prompt for login name & password and attempt to log in + * returns login_name on success or NULL on failure + */ +char *try_login(pam_handle_t *pam, unsigned int try_count) +{ + int result; + + // prompt for username + ui_setup_dialog(_("Login name:"), false); + result = ui_run(); + char *login_name = ui_get_text(); + if (result != 0) + return CANCEL_SENTINEL; + + result = pam_set_item(pam, PAM_USER, login_name); + if (result != PAM_SUCCESS) { + syslog(LOG_NOTICE, _("Couldn't set PAM_USER: %s"), pam_strerror(pam, result)); + ui_setup_message(_("Failed to begin authentication.")); + ui_message_run(); + return NULL; + } + + result = pam_authenticate(pam, 0); + if (result != PAM_SUCCESS) { + // log to syslog + if (result == PAM_MAXTRIES || try_count >= max_tries) + syslog(LOG_NOTICE, _("TOO MANY LOGIN TRIES (%u) FROM '%s' FOR '%s', %s"), + try_count, origin, login_name, pam_strerror(pam, result)); + else + syslog(LOG_NOTICE, _("FAILED LOGIN (%u) FROM '%s' FOR '%s', %s"), + try_count, origin, login_name, pam_strerror(pam, result)); + + // display error message + ui_setup_message(_("Authentication failed.")); + ui_message_run(); + + return NULL; + } + + return login_name; +} + +int get_pam_login_name(pam_handle_t *pam, char **login_name) +{ + const void *item = (const void *) *login_name; + int result = pam_get_item(pam, PAM_USER, &item); + *login_name = (char *) item; + return result; +} + +void init_env(bool no_destroy, pam_handle_t *pam, struct passwd *pwd) +{ + char *buf = getenv("TERM"); + if (buf) + buf = strdup(buf); + + if (!no_destroy) + environ = calloc(1, sizeof(char *)); + + setenv("HOME", pwd->pw_dir, 0); + setenv("USER", pwd->pw_name, 1); + setenv("LOGNAME", pwd->pw_name, 1); + setenv("SHELL", pwd->pw_shell, 1); + setenv("TERM", buf ? buf : "dumb", 1); + free(buf); + + // TODO use ENV_PATH and ENV_SUPATH from logindefs + setenv("PATH", _PATH_DEFPATH, 0); + + // apparently mailx is a bitch without this + asprintf(&buf, "%s/%s", _PATH_MAILDIR, pwd->pw_name); + setenv("MAIL", buf, 0); + free(buf); + + char **pam_env = pam_getenvlist(pam); + for (int i = 0; pam_env && pam_env[i]; i++) + putenv(pam_env[i]); +} + +static pid_t child_pid; +static volatile sig_atomic_t got_sig = 0; +static void handle_sig(int signal) +{ + if (child_pid) + kill(-child_pid, signal); + else + got_sig = 1; + if (signal == SIGTERM) + kill(-child_pid, SIGHUP); +} int main(int argc, char* argv[]) { @@ -104,10 +309,40 @@ int main(int argc, char* argv[]) openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); + login_pid = getpid(); + + if (isatty(STDIN_FILENO)) + tty_path = ttyname(STDIN_FILENO); + else if (isatty(STDOUT_FILENO)) + tty_path = ttyname(STDOUT_FILENO); + + if (tty_path) { + if (strncmp(tty_path, "/dev/", 5) == 0) + tty_name = tty_path + 5; + else + tty_name = tty_path; + + for (char *x = tty_name; x && *x; x++) { + if ((*x) >= '0' && (*x) <= '9') { + tty_id = x; + break; + } + } + } + + // TODO remote login stuff + + if (remote_host) + origin = remote_host; + else if (tty_name) + origin = tty_name; + else + syslog(LOG_NOTICE, _("Couldn't determine origin: neither remote host nor TTY")); + pam_handle_t *pam = NULL; struct pam_conv conv = { .conv = pam_conv_handler, .appdata_ptr = NULL }; - result = pam_start("login", NULL, &conv, &pam); + result = pam_start(remote_host ? "remote" : "login", NULL, &conv, &pam); if (result != PAM_SUCCESS) { syslog(LOG_ERR, _("Couldn't initialize PAM: %s"), pam_strerror(pam, result)); fprintf(stderr, _("pam_start error: %s. Abort.\n"), pam_strerror(pam, result)); @@ -115,30 +350,311 @@ int main(int argc, char* argv[]) return EXIT_FAILURE; } + result = pam_set_item(pam, PAM_RHOST, remote_host); + if (result != PAM_SUCCESS) { + syslog(LOG_ERR, _("Couldn't set PAM_RHOST: %s"), pam_strerror(pam, result)); + fprintf(stderr, _("pam_set_item error: %s. Abort.\n"), pam_strerror(pam, result)); + sleep(fail_delay); + return EXIT_FAILURE; + } + + if (tty_path) { + result = pam_set_item(pam, PAM_TTY, tty_path); + if (result != PAM_SUCCESS) { + syslog(LOG_ERR, _("Couldn't set PAM_TTY: %s"), pam_strerror(pam, result)); + fprintf(stderr, _("pam_set_item error: %s. Abort.\n"), pam_strerror(pam, result)); + sleep(fail_delay); + return EXIT_FAILURE; + } + } + ui_init(); - // prompt for username - ui_setup_dialog(_("Login name:"), false); - result = ui_run(); - char *login_name = ui_get_text(); - if (result != 0) - goto END; + char *login_name; + for (unsigned int try = 1; try <= max_tries; try++) { + login_name = try_login(pam, try); + if (login_name) + break; + } - result = pam_set_item(pam, PAM_USER, login_name); - if (result != PAM_SUCCESS) { - endwin(); - printf("pam_set_item error: %s\n", pam_strerror(pam, result)); + if (login_name == CANCEL_SENTINEL) { + syslog(LOG_NOTICE, _("Login from '%s' aborted"), origin); + ui_end(); + sleep(fail_delay); + return EXIT_SUCCESS; + } + + if (!login_name) { + char *msg; + // TRANSLATORS: default value is 5 + asprintf(&msg, _("Exceeded maximum login attempts (%u).\n"), max_tries); + ui_setup_message(msg); + ui_message_run(); + + sleep(fail_delay); + ui_end(); return EXIT_FAILURE; } - result = pam_authenticate(pam, 0); + // Is user account valid? + result = pam_acct_mgmt(pam, 0); + if (result == PAM_NEW_AUTHTOK_REQD) + result = pam_chauthtok(pam, PAM_CHANGE_EXPIRED_AUTHTOK); if (result != PAM_SUCCESS) { - endwin(); - printf("pam_authenticate error: %s\n", pam_strerror(pam, result)); + syslog(LOG_ERR, _("User validation failed for '%s': %s"), login_name, pam_strerror(pam, result)); + + switch (result) { + /* + case PAM_ACCT_EXPIRED: + asprintf(&msg, _("The user account '%s' has expired and cannot log in."), login_name); + break; + */ + case PAM_AUTHTOK_ERR: + case PAM_AUTHTOK_RECOVERY_ERR: + case PAM_AUTHTOK_LOCK_BUSY: + case PAM_AUTHTOK_DISABLE_AGING: + // asprintf(&msg, _("Failed to update login credentials for '%s'."), login_name); + ui_setup_message(_("Failed to update login credentials.")); + break; + default: + // asprintf(&msg, _("Cannot log in as '%s'. Is the user account valid?"), login_name); + ui_setup_message(_("Cannot log in.")); + } + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + // ensure we have the correct login_name + result = get_pam_login_name(pam, &login_name); + if (result != PAM_SUCCESS) { + syslog(LOG_ERR, _("Couldn't get PAM_USER: %s"), pam_strerror(pam, result)); + ui_setup_message(_("Cannot log in. The user account data is missing or invalid.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + if ((!login_name) || login_name[0] == '\0') { + syslog(LOG_ERR, _("NULL user name. Abort.")); + ui_setup_message(_("Cannot log in. The user account data is missing or invalid.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); return EXIT_FAILURE; } -END: - endwin(); + struct passwd *pwd = getpwnam(login_name); + if (!pwd) { + syslog(LOG_ERR, _("No passwd entry for '%s'. Abort."), login_name); + ui_setup_message(_("Cannot log in. The user account data is missing or invalid.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + + result = (pwd->pw_uid != 0) + ? initgroups(login_name, pwd->pw_gid) + : setgroups(0, NULL); + if (result < 0) { + syslog(LOG_ERR, _("groups initialization failed: %m")); + ui_setup_message(_("Cannot log in. Unable to establish a session.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + + // begin PAM session + result = pam_setcred(pam, PAM_ESTABLISH_CRED); + if (result != PAM_SUCCESS) { + syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result)); + ui_setup_message(_("Cannot log in. Unable to establish a session.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + result = pam_open_session(pam, 0); + if (result != PAM_SUCCESS) { + pam_setcred(pam, PAM_DELETE_CRED); + syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result)); + ui_setup_message(_("Cannot log in. Unable to establish a session.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + result = pam_setcred(pam, PAM_REINITIALIZE_CRED); + if (result != PAM_SUCCESS) { + pam_close_session(pam, 0); + syslog(LOG_ERR, _("Couldn't init PAM session: %s"), pam_strerror(pam, result)); + ui_setup_message(_("Cannot log in. Unable to establish a session.")); + ui_message_run(); + + sleep(fail_delay); + ui_end(); + return EXIT_FAILURE; + } + + endpwent(); + ui_end(); + + log_utmp(login_name); + // TODO log audit and lastlog + syslog(LOG_INFO, _("LOGIN FROM '%s' BY '%s'"), origin, login_name); + + // chown TTY + if (tty_path) { + // TODO set group as well + if (chown(tty_path, pwd->pw_uid, -1) != 0) + syslog(LOG_ERR, _("failed to chown TTY '%s': %m"), tty_path); + } + + if (setgid(pwd->pw_gid) < 0 && pwd->pw_gid) { + syslog(LOG_ALERT, _("setgid() failed: %m")); + fprintf(stderr, _("error applying user GID: %m. Abort.\n")); + sleep(fail_delay); + return EXIT_FAILURE; + } + + if ((!pwd->pw_shell) || pwd->pw_shell[0] == '\0') + pwd->pw_shell = _PATH_BSHELL; + + init_env(false, pam, pwd); + + // Get ready to fork! + + // signal handlers - i think this is in case something comes in while we're forking? + struct sigaction sa, prev_hup, prev_term; + memset(&sa, 0, sizeof(sa)); + + signal(SIGALRM, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + signal(SIGTSTP, SIG_IGN); + + sa.sa_handler = SIG_IGN; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGHUP, &sa, &prev_hup); + + if (tty_path) + ioctl(0, TIOCNOTTY, NULL); + + sa.sa_handler = handle_sig; + sigaction(SIGHUP, &sa, NULL); + sigaction(SIGTERM, &sa, &prev_term); + + closelog(); + + child_pid = fork(); + if (child_pid < 0) { + fprintf(stderr, _("error forking: %m. Abort.\n")); + openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); + syslog(LOG_ALERT, _("fork() failed: %m")); + + pam_setcred(pam, PAM_DELETE_CRED); + pam_end(pam, pam_close_session(pam, 0)); + + sleep(fail_delay); + return EXIT_FAILURE; + } + + if (child_pid) { // we're in the parent + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + sa.sa_handler = SIG_IGN; + sigaction(SIGQUIT, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + while (wait(NULL) == -1 && errno == EINTR); + openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); + + pam_setcred(pam, PAM_DELETE_CRED); + pam_end(pam, pam_close_session(pam, 0)); + return EXIT_SUCCESS; + } + + // child + + // reset signal handlers + sigaction(SIGHUP, &prev_hup, NULL); + sigaction(SIGTERM, &prev_term, NULL); + if (got_sig) + return EXIT_FAILURE; + + setsid(); + + // reopen tty + if (tty_path) { + int fd = open(tty_path, O_RDWR | O_NONBLOCK); + if (fd == -1) { + syslog(LOG_ERR, _("can't reopen tty: %m")); + return EXIT_FAILURE; + } + + if (!isatty(fd)) { + close(fd); + syslog(LOG_ERR, _("%s is not a tty"), tty_path); + return EXIT_FAILURE; + } + + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK); + + // set tty as stdin, stdout, and stderr + for (int i = 0; i < fd; i++) + close(i); + for (int i = 0; i < 3; i++) + if (fd != i) + dup2(fd, i); + if (fd >= 3) + close(fd); + } + openlog("hlogin", LOG_ODELAY, LOG_AUTHPRIV); + + if (ioctl(0, TIOCSCTTY, 1) != 0) + syslog(LOG_ERR, _("TIOCSCTTY failed: %m")); + + signal(SIGINT, SIG_DFL); + + // continue becoming the user + if (setuid(pwd->pw_uid) < 0 && pwd->pw_uid) { + syslog(LOG_ALERT, _("setuid() failed: %m")); + fprintf(stderr, _("error applying user UID: %m. Abort.\n")); + sleep(fail_delay); + return EXIT_FAILURE; + } + if (chdir(pwd->pw_dir) < 0) { + syslog(LOG_ALERT, _("failed to chdir to home (%s): %m"), pwd->pw_dir); + fprintf(stderr, _("error changing to home directory: %m. Abort.\n")); + sleep(fail_delay); + return EXIT_FAILURE; + } + + pam_end(pam, PAM_SUCCESS | PAM_DATA_SILENT); + + // execute login shell + char **shell_argv = calloc(2, sizeof(char *)); + char *shell_base = strrchr(pwd->pw_shell, '/'); + if (shell_base) + shell_base++; + else + shell_base = pwd->pw_shell; + + shell_argv[0] = malloc(sizeof(char) * (strlen(shell_base) + 2)); + shell_argv[0][0] = '-'; + strcpy(shell_argv[0] + 1, shell_base); + + execvp(pwd->pw_shell, shell_argv); + return EXIT_SUCCESS; } diff --git a/ui.c b/ui.c index 179e90e..23f0ff5 100644 --- a/ui.c +++ b/ui.c @@ -55,6 +55,9 @@ PANEL *error_shadow_p; const int login_rows = 8; const int login_cols = 48; +int error_rows, error_cols; +int error_curx; + char *input_buf; size_t input_buflen; int input_pos; @@ -120,6 +123,22 @@ FAILURE: return unknown_str; } +void safe_free(void *ptr, size_t size) +{ + memset(ptr, (int)'\0', size); + free(ptr); +} + +void *safe_realloc(void *ptr, size_t size, size_t new_size) +{ + void *new_ptr = malloc(new_size); + if (new_ptr == NULL) + return NULL; + memcpy(new_ptr, ptr, size); + safe_free(ptr, size); + return new_ptr; +} + /* char *xgetnisdomainname() { @@ -148,14 +167,20 @@ char erase_ch; void ui_setup_dialog(char *prompt, bool password) { - input_buf[0] = '\0'; + memset(input_buf, (int)'\0', sizeof(char) * input_buflen); input_pos = 0; input_scroll = 0; btn_focus = 0; + btn_count = 2; msg = prompt; hide_input = password; + hide_panel(error_p); + hide_panel(error_shadow_p); + show_panel(login_shadow_p); + top_panel(login_p); + wclear(login_w); draw_outline(login_w, login_rows, login_cols, 0, 0, false); paint_login(); @@ -163,12 +188,56 @@ void ui_setup_dialog(char *prompt, bool password) void ui_setup_message(char *text) { - msg = text; - // TODO insert word wrap logic here - int msg_len = strlen(text); + btn_focus = 0; + btn_count = 1; + int tcols = login_cols - 4; + int trows = 1; + char **msg_lines = word_wrap(text, tcols, tcols / 2, &trows); + if (trows == 1) + tcols = strlen(msg_lines[0]); + if (tcols < 16) + tcols = 16; + error_rows = trows + 4; + error_cols = tcols + 4; + + hide_panel(login_shadow_p); + + wresize(error_w, error_rows, error_cols); + wresize(error_shadow_w, error_rows, error_cols); + + replace_panel(error_shadow_p, error_shadow_w); + show_panel(error_shadow_p); + move_panel(error_shadow_p, ((term_rows - error_rows) / 2) + 1, ((term_cols - error_cols) / 2) + 1); + + replace_panel(error_p, error_w); + show_panel(error_p); + move_panel(error_p, (term_rows - error_rows) / 2, (term_cols - error_cols) / 2); + + wbkgd(error_shadow_w, ' ' | COLOR_PAIR(4)); + wclear(error_shadow_w); + touchwin(error_shadow_w); + + wclear(error_w); + draw_outline(error_w, error_rows, error_cols, 0, 0, false); + + // draw text + wattron(error_w, COLOR_PAIR(2)); + for (int i = 0; i < trows; i++) { + wmove(error_w, i + 1, 2); + waddstr(error_w, msg_lines[i]); + free(msg_lines[i]); + } + free(msg_lines); + + // draw button + wmove(error_w, error_rows - 2, 2); + wattron(error_w, A_REVERSE); + wprintw(error_w, "< %s >", _("OK")); + error_curx = error_w->_curx - 2; + wattroff(error_w, A_REVERSE); } /** ui_init - initialize the UI subsystem @@ -201,7 +270,7 @@ void ui_init() init_pair(1, COLOR_WHITE, COLOR_BLUE); init_pair(2, COLOR_BLACK, COLOR_WHITE); init_pair(3, COLOR_WHITE, COLOR_WHITE); - init_pair(4, COLOR_RED, COLOR_WHITE); + init_pair(4, COLOR_BLACK, COLOR_BLACK); // set up background window back_w = newwin(term_rows, term_cols, 0, 0); @@ -214,10 +283,10 @@ void ui_init() // set up login shadow window login_shadow_w = newwin(login_rows, login_cols, ((term_rows - login_rows) / 2) + 1, ((term_cols - login_cols) / 2) + 1); login_shadow_p = new_panel(login_shadow_w); - wbkgd(login_shadow_w, ' ' | COLOR_PAIR(0)); + wbkgd(login_shadow_w, ' ' | COLOR_PAIR(4)); wclear(login_shadow_w); - // set up dialog window + // set up login window login_w = newwin(login_rows, login_cols, (term_rows - login_rows) / 2, (term_cols - login_cols) / 2); login_p = new_panel(login_w); wattron(login_w, COLOR_PAIR(2)); @@ -226,8 +295,42 @@ void ui_init() draw_outline(login_w, login_rows, login_cols, 0, 0, false); paint_login(); + // set up error shadow window + error_shadow_w = newwin(2, 2, 1, 1); + error_shadow_p = new_panel(error_shadow_w); + wbkgd(error_shadow_w, ' ' | COLOR_PAIR(4)); + wclear(error_shadow_w); + hide_panel(error_shadow_p); + + // set up error window + error_w = newwin(1, 1, 0, 0); + error_p = new_panel(error_w); + wbkgd(error_w, ' ' | COLOR_PAIR(2)); + wclear(error_w); + hide_panel(error_p); + // enable keypad mode keypad(login_w, true); + keypad(error_w, true); +} + +void ui_end() +{ + safe_free(input_buf, input_buflen); + + del_panel(back_p); + del_panel(login_p); + del_panel(login_shadow_p); + del_panel(error_p); + del_panel(error_shadow_p); + + delwin(back_w); + delwin(login_w); + delwin(login_shadow_w); + delwin(error_w); + delwin(error_shadow_w); + + endwin(); } /** paint_back - redraw info displayed on background @@ -327,10 +430,55 @@ int ui_update() return -1; } +int ui_error_update() +{ + paint_back(); + wmove(error_w, error_rows - 2, error_curx); + + update_panels(); + doupdate(); + + int ch = wgetch(error_w); + if (ch == ERR) + return -1; + + if (ch == 0x1B) + return -2; + else if (ch == KEY_ENTER || ch == '\r' || ch == '\n') + return btn_focus; + + return -1; +} + int ui_run() { int result; while ((result = ui_update()) == -1); + btn_focus = -1; + paint_back(); + paint_login(); + update_panels(); + doupdate(); + return result; +} + +int ui_message_run() +{ + int result; + while ((result = ui_error_update()) == -1); + btn_focus = -1; + paint_back(); + wmove(error_w, error_rows - 2, 2); + wattron(error_w, COLOR_PAIR(2)); + wprintw(error_w, "< %s >", _("OK")); + wmove(error_w, error_rows - 2, error_curx); + update_panels(); + doupdate(); + hide_panel(error_p); + hide_panel(error_shadow_p); + show_panel(login_shadow_p); + top_panel(login_p); + clear(); return result; } @@ -339,6 +487,7 @@ char *ui_get_text() unsigned int len = strlen(input_buf) + 1; char *buf = malloc(len * sizeof(char)); strcpy(buf, input_buf); + memset(input_buf, (int)'\0', input_buflen * sizeof(char)); return buf; } @@ -346,8 +495,8 @@ void input_add_char(char ch) { // resize if necessary if ((strlen(input_buf) + 2) > input_buflen) { + input_buf = safe_realloc(input_buf, input_buflen * sizeof(char), input_buflen * 2 * sizeof(char)); input_buflen *= 2; - input_buf = realloc(input_buf, input_buflen * sizeof(char)); } // insert new char & shift everything over for (int i = (input_pos++); i < input_buflen; i++) { @@ -412,46 +561,37 @@ void draw_outline(WINDOW *win, int height, int width, int y, int x, bool inset) /** output_word - used in word_wrap */ void output_word(char *word, size_t len, - char **out, size_t *out_size, int *out_pos, - int *line_pos, int *lines, int width) + char ***out, int *line_pos, int *lines, int width) { int i = 0; while (i < len) { int space = width - (*line_pos); int remain = len - i; if (space > remain) { // can fit on line - if ((*out_size) < (*out_pos) + remain) { - *out_size *= 2; - *out = realloc(*out, sizeof(char) * (*out_size)); - } - // copy word - memcpy((*out) + (*out_pos), word + i, sizeof(char) * remain); + memcpy((*out)[(*lines) - 1] + (*line_pos), word + i, sizeof(char) * remain); i += remain; - *out_pos += remain; *line_pos += remain; } else { - if ((*out_size) < (*out_pos) + space + 1) { - *out_size *= 2; - *out = realloc(*out, sizeof(char) * (*out_size)); - } // copy part of word - memcpy((*out) + (*out_pos), word + i, sizeof(char) * space); + memcpy((*out)[(*lines) - 1] + (*line_pos), word + i, sizeof(char) * space); i += space; - *out_pos += space; + *line_pos += space; // add newline - (*out)[(*out_pos)++] = '\n'; + (*out)[(*lines) - 1][(*line_pos)] = '\0'; *line_pos = 0; (*lines)++; + *out = realloc(*out, sizeof(char *) * (*lines)); + (*out)[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } } } -char *word_wrap(char *text, int width, int max_word, int *lines) +char **word_wrap(char *text, int width, int max_word, int *lines) { - // output buffer - size_t out_size = 256; - char *out = malloc(sizeof(char) * out_size); - int out_pos = 0; + // output lines + *lines = 1; + char **out = malloc(sizeof(char *) * (*lines)); + out[0] = malloc(sizeof(char) * (width + 1)); // word buffer char *word = malloc(sizeof(char) * max_word); int word_len = 0; @@ -464,31 +604,28 @@ char *word_wrap(char *text, int width, int max_word, int *lines) case ' ': if (word[0] == '\0') word_len = 0; - // ensure buffer is large enough - if (out_size < out_pos + word_len + 2) { - out_size *= 2; - out = realloc(out, sizeof(char) * out_size); - } if (word_len > 0) { // do we have to wrap to a new line? if (line_pos + word_len > width) { - out[out_pos++] = '\n'; + out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; + out = realloc(out, sizeof(char *) * (*lines)); + out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } - memcpy(out + out_pos, word, sizeof(char) * word_len); - word_len = 0; - out_pos += word_len; + memcpy(out[(*lines) - 1] + line_pos, word, sizeof(char) * word_len); line_pos += word_len; + word_len = 0; } // output \n if (ch == '\n' || line_pos >= width) { - out[out_pos++] = '\n'; + out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; + out = realloc(out, sizeof(char *) * (*lines)); + out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } else { - out[out_pos++] = ' '; - line_pos++; + out[(*lines) - 1][line_pos++] = ' '; } break; default: @@ -497,26 +634,45 @@ char *word_wrap(char *text, int width, int max_word, int *lines) if (word[0] != '\0') { // write what we have output_word(word, word_len, - &out, &out_size, &out_pos, - &line_pos, lines, width); + &out, &line_pos, lines, width); word[0] = '\0'; } - // ensure buffer is large enough - if (out_size < out_pos + 2) { - out_size *= 2; - out = realloc(out, sizeof(char) * out_size); - } // write char and maybe newline - out[out_pos++] = ch; - if ((++line_pos) >= width) { - out[out_pos++] = '\n'; + out[(*lines) - 1][line_pos++] = ch; + if (line_pos >= width) { + out[(*lines) - 1][line_pos] = '\0'; line_pos = 0; (*lines)++; + out = realloc(out, sizeof(char *) * (*lines)); + out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); } } else word[word_len++] = ch; } } + if (word_len > 0 && word[0] != '\0') { + // do we have to wrap to a new line? + if (line_pos + word_len > width) { + out[(*lines) - 1][line_pos] = '\0'; + line_pos = 0; + (*lines)++; + out = realloc(out, sizeof(char *) * (*lines)); + out[(*lines) - 1] = malloc(sizeof(char) * (width + 1)); + } + memcpy(out[(*lines) - 1] + line_pos, word, sizeof(char) * word_len); + line_pos += word_len; + word_len = 0; + } + free(word); + + if (line_pos == 0) + // remove last line + free(out[--(*lines)]); + else + // non-empty line, add null terminator + out[(*lines) - 1][line_pos] = '\0'; + + return out; } diff --git a/ui.h b/ui.h index d76e8d4..678d891 100644 --- a/ui.h +++ b/ui.h @@ -27,7 +27,11 @@ extern int term_rows, term_cols; +void *safe_realloc(void *ptr, size_t size, size_t new_size); +void safe_free(void *ptr, size_t size); + void ui_init(); +void ui_end(); void paint_back(); void paint_login(); @@ -37,6 +41,7 @@ void ui_setup_message(char *text); int ui_update(); int ui_run(); +int ui_message_run(); char *ui_get_text(); void input_add_char(char ch); @@ -45,6 +50,6 @@ void input_del_char(); void draw_outline(WINDOW *win, int height, int width, int y, int x, bool inset); -char *word_wrap(char *text, int width, int max_word, int *lines); +char **word_wrap(char *text, int width, int max_word, int *lines); #endif // __UI_H