/* * FCRON - periodic command scheduler * * Copyright 2000-2014 Thibault Godouet * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * The GNU General Public License can also be found in the file * `LICENSE' that comes with the fcron source distribution. */ /* fcrondyn : interact dynamically with running fcron process : * - list jobs, with their status, next time of execution, etc * - run, delay a job * - send a signal to a job * - etc ... */ #include "fcrondyn.h" #include "allow.h" #include "read_string.h" #include "mem.h" #ifdef HAVE_LIBREADLINE char **rl_cmpl_fcrondyn(const char *text, int start, int end); char *rl_cmpl_command_generator(const char *text, int state); #if defined(HAVE_READLINE_READLINE_H) #include #elif defined(HAVE_READLINE_H) #include #endif /* !defined(HAVE_READLINE_H) */ #if defined(HAVE_READLINE_HISTORY_H) #include #elif defined(HAVE_HISTORY_H) #include #endif /* defined(HAVE_READLINE_HISTORY_H) */ #endif /* HAVE_LIBREADLINE */ void info(void); void usage(void); void xexit(int exit_val); RETSIGTYPE sigpipe_handler(int x); int interactive_mode(int fd); /* returned by parse_cmd() and/or talk_fcron() */ #define QUIT_CMD 1 #define HELP_CMD 2 #define ZEROLEN_CMD 3 #define CMD_NOT_FOUND 4 #define INVALID_ARG 5 int talk_fcron(char *cmd_str, int fd); int parse_cmd(char *cmd_str, long int **cmd, int *cmd_len); int connect_fcron(void); int authenticate_user_password(int fd); /* command line options */ char *cmd_str = NULL; /* needed by log part : */ char *prog_name = NULL; char foreground = 1; pid_t daemon_pid = 0; /* uid/gid of user/group root * (we don't use the static UID or GID as we ask for user and group names * in the configure script) */ uid_t rootuid = 0; gid_t rootgid = 0; /* misc */ char *user_str = NULL; uid_t user_uid = 0; gid_t user_gid = 0; struct cmd_list_ent cmd_list[] = { /* name, desc, num opt, cmd code, cmd opts, cmd defaults */ {"ls", {NULL}, "List all jobs of user", 1, CMD_LIST_JOBS, {USER}, {CUR_USER}}, {"ls_lavgq", {}, "List jobs of user which are in lavg queue", 1, CMD_LIST_LAVGQ, {USER}, {CUR_USER}}, {"ls_serialq", {}, "List jobs of user which are in serial queue", 1, CMD_LIST_SERIALQ, {USER}, {CUR_USER}}, {"ls_exeq", {}, "List running jobs of user", 1, CMD_LIST_EXEQ, {USER}, {CUR_USER}}, {"detail", {}, "Print details on job", 1, CMD_DETAILS, {JOBID}, {ARG_REQUIRED}}, /* {"reschedule", {NULL}, "Reschedule next execution of job", 2, CMD_RESCHEDULE, {TIME_AND_DATE, JOBID}, {ARG_REQUIRED, ARG_REQUIRED}}, */ {"runnow", {}, "Advance next execution of job to now", 1, CMD_RUNNOW, {JOBID}, {ARG_REQUIRED}}, {"run", {}, "Run job now (without changing its current schedule)", 1, CMD_RUN, {JOBID}, {ARG_REQUIRED}}, {"kill", {}, "Send signal to running job", 2, CMD_SEND_SIGNAL, {SIGNAL, JOBID}, {ARG_REQUIRED, ARG_REQUIRED}}, {"renice", {}, "Renice running job", 2, CMD_RENICE, {NICE_VALUE, JOBID}, {ARG_REQUIRED, ARG_REQUIRED}}, {"quit", {"q", "exit"}, "Quit fcrondyn", 0, QUIT_CMD, {}, {}}, {"help", {"h"}, "Display a help message", 0, HELP_CMD, {}, {}}, }; const int cmd_list_len = sizeof(cmd_list) / sizeof(cmd_list_ent); void info(void) /* print some informations about this program : * version, license */ { fprintf(stderr, "fcrondyn " VERSION_QUOTED " - interact dynamically with daemon fcron\n" "Copyright " COPYRIGHT_QUOTED " Thibault Godouet \n" "This program is free software distributed WITHOUT ANY WARRANTY.\n" "See the GNU General Public License for more details.\n"); exit(EXIT_OK); } void usage(void) /* print a help message about command line options and exit */ { fprintf(stderr, "fcrondyn [-i]\n" "fcrondyn -x {command}\n" "fcrondyn -h\n" " -i run fcrontab in interactive mode.\n" " -x execute one command (in batch mode)\n" " -c f make fcrontab use config file f.\n" " -d set up debug mode.\n" " -h display this help message.\n" " -V display version & infos about fcrondyn.\n" "\n" "To list the available commands, run:\n" " fcrondyn -x help\n"); exit(EXIT_ERR); } RETSIGTYPE sigpipe_handler(int x) /* handle broken pipes ... */ { fprintf(stderr, "Broken pipe : fcron may have closed the connection\nThe connection " "has been idle for more than %ds, or fcron may not be running anymore.\n", MAX_IDLE_TIME); fprintf(stderr, "Exiting ...\n"); xexit(EXIT_ERR); } void xexit(int exit_val) /* clean & exit */ { Free_safe(cmd_str); exit(exit_val); } /* used in parse_cmd : */ #define Write_cmd(DATA) \ memcpy(buf + *cmd_len, &DATA, sizeof(long int)); \ *cmd_len += 1; #define Strncmp(STR1, STR2, STR1_SIZE) \ strncmp(STR1, STR2, (STR1_SIZE < strlen(STR2)) ? strlen(STR2) : STR1_SIZE) int parse_cmd(char *cmd_str, long int **cmd, int *cmd_len) /* read a command string, check if it is valid and translate it */ { long int buf[SOCKET_MSG_LEN]; int word_size = 0; int i = 0, j = 0, rank = -1; long int int_buf = 0; struct passwd *pass = NULL; #ifdef SYSFCRONTAB long int sysfcrontab_uid = SYSFCRONTAB_UID; #endif bzero(buf, sizeof(buf)); *cmd_len = 0; remove_blanks(cmd_str); /* at the end of the string */ if ((word_size = get_word(&cmd_str)) == 0) { fprintf(stderr, "Warning : Zero-length command name : line ignored.\n"); return ZEROLEN_CMD; } for (i = 0; i < cmd_list_len; i++) { int j; if (Strncmp(cmd_str, cmd_list[i].cmd_name, word_size) == 0) { rank = i; break; } for (j = 0; j < MAX_NUM_ALIAS && cmd_list[i].cmd_alias[j] != NULL; j++) { if (Strncmp(cmd_str, cmd_list[i].cmd_alias[j], word_size) == 0) { rank = i; break; } } } if (rank == (-1)) { fprintf(stderr, "Error : Unknown command.\n"); return CMD_NOT_FOUND; } else if (cmd_list[rank].cmd_code == QUIT_CMD) { if (debug_opt) fprintf(stderr, "quit command\n"); return QUIT_CMD; } else if (cmd_list[rank].cmd_code == HELP_CMD) { if (debug_opt) fprintf(stderr, "Help command\n"); return HELP_CMD; } Write_cmd(cmd_list[rank].cmd_code); if (debug_opt) fprintf(stderr, "command : %s\n", cmd_list[i].cmd_name); cmd_str += word_size; for (i = 0; i < cmd_list[rank].cmd_numopt; i++) { if ((word_size = get_word(&cmd_str)) == 0) { if (cmd_list[rank].cmd_default[i] == ARG_REQUIRED) { fprintf(stderr, "Error : arg required !\n"); return INVALID_ARG; } /* use default value : currently, works only with CUR_USER */ if (user_uid == rootuid) { /* default for root = all */ int_buf = ALL; Write_cmd(int_buf); if (debug_opt) fprintf(stderr, " uid = ALL\n"); } else { Write_cmd(user_uid); if (debug_opt) fprintf(stderr, " uid = %d\n", (int)user_uid); } } else { /* get value from line ... */ switch (cmd_list[rank].cmd_opt[i]) { case USER: int_buf = (long int)*(cmd_str + word_size); *(cmd_str + word_size) = '\0'; #ifdef SYSFCRONTAB if (strcmp(cmd_str, SYSFCRONTAB) == 0) { Write_cmd(sysfcrontab_uid); } else { #endif if ((pass = getpwnam(cmd_str)) == NULL) { fprintf(stderr, "Error : '%s' isn't a valid username.\n", cmd_str); return INVALID_ARG; } Write_cmd(pass->pw_uid); #ifdef SYSFCRONTAB } #endif *(cmd_str + word_size) = (char)int_buf; cmd_str += word_size; if (debug_opt) fprintf(stderr, " uid = %d\n", #ifdef SYSFCRONTAB (pass) ? (int)pass->pw_uid : (int)SYSFCRONTAB_UID #else (int)pass->pw_uid #endif ); break; case JOBID: /* after strtol(), cmd_str will be updated (first non-number char) */ if ((int_buf = strtol(cmd_str, &cmd_str, 10)) < 0 || int_buf >= LONG_MAX || (!isspace((int)*cmd_str) && *cmd_str != '\0')) { fprintf(stderr, "Error : invalid jobid.\n"); return INVALID_ARG; } Write_cmd(int_buf); if (debug_opt) fprintf(stderr, " jobid = %ld\n", int_buf); break; case TIME_AND_DATE: /* argghh !!! no standard function ! */ break; case NICE_VALUE: /* after strtol(), cmd_str will be updated (first non-number char) */ if ((int_buf = strtol(cmd_str, &cmd_str, 10)) > 20 || (int_buf < 0 && getuid() != rootuid) || int_buf < -20 || (!isspace((int)*cmd_str) && *cmd_str != '\0')) { fprintf(stderr, "Error : invalid nice value.\n"); return INVALID_ARG; } Write_cmd(int_buf); if (debug_opt) fprintf(stderr, " nicevalue = %ld\n", int_buf); break; case SIGNAL: if (isalpha((int)*cmd_str)) { for (j = 0; j < word_size; j++) *(cmd_str + j) = tolower(*(cmd_str + j)); if (Strncmp(cmd_str, "hup", word_size) == 0) int_buf = SIGHUP; else if (Strncmp(cmd_str, "int", word_size) == 0) int_buf = SIGINT; else if (Strncmp(cmd_str, "quit", word_size) == 0) int_buf = SIGQUIT; else if (Strncmp(cmd_str, "kill", word_size) == 0) int_buf = SIGKILL; else if (Strncmp(cmd_str, "alrm", word_size) == 0) int_buf = SIGALRM; else if (Strncmp(cmd_str, "term", word_size) == 0) int_buf = SIGTERM; else if (Strncmp(cmd_str, "usr1", word_size) == 0) int_buf = SIGUSR1; else if (Strncmp(cmd_str, "usr2", word_size) == 0) int_buf = SIGUSR2; else if (Strncmp(cmd_str, "cont", word_size) == 0) int_buf = SIGCONT; else if (Strncmp(cmd_str, "stop", word_size) == 0) int_buf = SIGSTOP; else if (Strncmp(cmd_str, "tstp", word_size) == 0) int_buf = SIGTSTP; else { fprintf(stderr, "Error : unknow signal (try integer value)\n"); return INVALID_ARG; } cmd_str += word_size; } /* after strtol(), cmd_str will be updated (first non-number char) */ else if ((int_buf = strtol(cmd_str, &cmd_str, 10)) <= 0 || int_buf >= LONG_MAX || (!isspace((int)*cmd_str) && *cmd_str != '\0')) { fprintf(stderr, "Error : invalid signal value.\n"); return INVALID_ARG; } Write_cmd(int_buf); if (debug_opt) fprintf(stderr, " signal = %ld\n", int_buf); break; default: fprintf(stderr, "Error : Unknown arg !"); return INVALID_ARG; } } } Skip_blanks(cmd_str); if (*cmd_str != '\0') fprintf(stderr, "Warning : too much arguments : '%s' ignored.\n", cmd_str); /* This is a valid command ... */ *cmd = alloc_safe(*cmd_len * sizeof(long int), "command string"); memcpy(*cmd, buf, *cmd_len * sizeof(long int)); return OK; } int authenticate_user_password(int fd) /* authenticate user */ { char *password = NULL; char buf[USER_NAME_LEN + 16]; int len = 0; fd_set read_set; /* needed to use select to check if some data is waiting */ struct timeval tv; snprintf(buf, sizeof(buf), "password for %s :", user_str); if ((password = read_string(CONV_ECHO_OFF, buf)) == NULL) return ERR; len = snprintf(buf, sizeof(buf), "%s", user_str) + 1; len += snprintf(buf + len, sizeof(buf) - len, "%s", password) + 1; send(fd, buf, len, 0); Overwrite(buf); Overwrite(password); Free_safe(password); tv.tv_sec = MAX_WAIT_TIME; tv.tv_usec = 0; FD_ZERO(&read_set); FD_SET(fd, &read_set); if (select(fd + 1, &read_set, NULL, NULL, &tv) <= 0) { error_e("Couldn't get data from socket during %d seconds.", MAX_WAIT_TIME); return ERR; } while (recv(fd, buf, sizeof(buf), 0) < 0 && errno == EINTR) if (errno == EINTR && debug_opt) fprintf(stderr, "Got EINTR ..."); if (strncmp(buf, "1", sizeof("1")) != 0) return ERR; return OK; } int connect_fcron(void) /* connect to fcron through a socket, and identify user */ { int fd = -1; struct sockaddr_un addr; int len = 0; int sun_len = 0; if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) die_e("could not create socket"); addr.sun_family = AF_UNIX; len = strlen(fifofile); if (len > sizeof(addr.sun_path) - 1) die("Error : fifo file path too long (max is %d)", sizeof(addr.sun_path) - 1); /* length(fifofile) < sizeof(add.sun_path), so strncpy will terminate * the string with at least one \0 (not necessarily required by the OS, * but still a good idea) */ strncpy(addr.sun_path, fifofile, sizeof(addr.sun_path)); addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; sun_len = (addr.sun_path - (char *)&addr) + len; #if HAVE_SA_LEN addr.sun_len = sun_len; #endif if (connect(fd, (struct sockaddr *)&addr, sun_len) < 0) die_e("Cannot connect() to fcron (check if fcron is running)"); /* Nothing to do on the client side if we use SO_PASSCRED */ #if !defined(SO_PASSCRED) && !defined(HAVE_GETPEERUCRED) && !defined(HAVE_GETPEEREID) if (authenticate_user_password(fd) == ERR) { fprintf(stderr, "Invalid password or too many authentication failures" " (try to connect later).\n(In the later case, fcron rejects all" " new authentication during %d secs)\n", AUTH_WAIT); die("Unable to authenticate user"); } #endif /* SO_PASSCRED HAVE_GETPEERUCRED HAVE_GETPEEREID */ return fd; } int talk_fcron(char *cmd_str, int fd) /* read a string command, check if it is valid and translate it, * send it to fcron, and print its answer */ { long int *cmd = NULL; int cmd_len = 0; char buf[LINE_LEN]; ssize_t read_len = 0; char existing_connection = (fd < 0) ? 0 : 1; fd_set read_set; /* needed to use select to check if some data is waiting */ struct timeval tv; switch (parse_cmd(cmd_str, &cmd, &cmd_len)) { case OK: break; case HELP_CMD: { int i, j, len; printf("Command recognized by fcrondyn :\n"); printf("------------------------------\n"); for (i = 0; i < cmd_list_len; i++) { len = printf("%s ", cmd_list[i].cmd_name); /* print args : */ for (j = 0; j < cmd_list[i].cmd_numopt; j++) { if (cmd_list[i].cmd_default[j] != ARG_REQUIRED) len += printf("["); switch (cmd_list[i].cmd_opt[j]) { case USER: len += printf("user"); break; case JOBID: len += printf("jobid"); break; case TIME_AND_DATE: len += printf("time"); break; case NICE_VALUE: len += printf("niceval"); break; case SIGNAL: len += printf("sig"); break; case BOOLEAN: len += printf("bool"); break; default: len += printf("unknown_arg!"); } if (cmd_list[i].cmd_default[j] != ARG_REQUIRED) len += printf("]"); len += printf(" "); } /* Align correctly the descriptions : */ printf("%*s%s", 24 - len, "", cmd_list[i].cmd_desc); /* print alias list (if any) */ if (cmd_list[i].cmd_alias[0] != NULL) { printf(" (aliases:"); for (j = 0; j < MAX_NUM_ALIAS && cmd_list[i].cmd_alias[j] != NULL; j++) { printf(" %s", cmd_list[i].cmd_alias[j]); } printf(")"); } printf("\n"); } } return HELP_CMD; case QUIT_CMD: return QUIT_CMD; case CMD_NOT_FOUND: return CMD_NOT_FOUND; case INVALID_ARG: return INVALID_ARG; case ZEROLEN_CMD: return ZEROLEN_CMD; default: return ERR; } /* This is a valid command (so we'll have to free() it) ... */ if (!existing_connection && (fd = connect_fcron()) == ERR) return ERR; send(fd, cmd, cmd_len * sizeof(long int), 0); Free_safe(cmd); cmd_len = 0; tv.tv_sec = MAX_WAIT_TIME; tv.tv_usec = 0; FD_ZERO(&read_set); FD_SET(fd, &read_set); if (select(fd + 1, &read_set, NULL, NULL, &tv) <= 0) { error_e("Couldn't get data from socket during %d seconds.", MAX_WAIT_TIME); return ERR; } while ((read_len = recv(fd, buf, sizeof(buf) - 1, 0)) >= 0 || errno == EINTR) { if (errno == EINTR && debug_opt) fprintf(stderr, "got EINTR ...\n"); else if (read_len > sizeof(buf)) { /* weird ... no data yet ? */ if (debug_opt) fprintf(stderr, "no data yet ?"); } else if (read_len <= 0) { if (debug_opt) fprintf(stderr, "read_len = %d\n", (int)read_len); fprintf(stderr, "connection closed by fcron\n"); shutdown(fd, SHUT_RDWR); return ERR; } else { /* ensure the string is terminated by a '\0' for when we'll printf() it */ buf[read_len] = '\0'; printf("%s", buf); /* check for the end of command output marker */ if (read_len >= sizeof(END_STR) && strncmp(&buf[read_len - sizeof(END_STR)], END_STR, sizeof(END_STR)) == 0) break; } } if (read_len < 0) { error_e("error in recv()"); } if (!existing_connection) xclose_check(&fd, "unix socket"); return OK; } #ifdef HAVE_LIBREADLINE /* Attempt to complete on the contents of TEXT. START and END bound the region of rl_line_buffer that contains the word to complete. TEXT is the word to complete. We can use the entire contents of rl_line_buffer in case we want to do some simple parsing. Return the array of matches, or NULL if there aren't any. */ char ** rl_cmpl_fcrondyn(const char *text, int start, int end) { char **matches; matches = (char **)NULL; /* If this word is at the start of the line, then it is a command * to complete. Otherwise it is an argument which we ignore for now */ if (start == 0) { matches = rl_completion_matches(text, rl_cmpl_command_generator); } return (matches); } /* Generator function for command completion. STATE lets us know whether to start from scratch; without any state (i.e. STATE == 0), then we start at the top of the list. */ char * rl_cmpl_command_generator(const char *text, int state) { static int list_index, len; char *name = NULL; /* If this is a new word to complete, initialize now. This includes * saving the length of TEXT for efficiency, and initializing the index * variable to 0. */ if (!state) { list_index = 0; len = strlen(text); } /* Return the next name which partially matches from the command list. */ while (list_index < cmd_list_len) { name = cmd_list[list_index].cmd_name; list_index++; if (strncmp(name, text, len) == 0) { return (strdup2(name)); } } /* If no names matched, then return NULL. */ return ((char *)NULL); } #endif /* HAVE_LIBREADLINE */ int interactive_mode(int fd) /* provide user a prompt, read command, send it to fcron, print its answer, * then give another prompt, etc, until user type an exit command */ { char existing_connection = (fd < 0) ? 0 : 1; int return_code = 0; #ifdef HAVE_LIBREADLINE char *line_read = NULL; #else /* HAVE_LIBREADLINE */ char buf[LINE_LEN]; #endif /* HAVE_LIBREADLINE */ if (!existing_connection && (fd = connect_fcron()) == ERR) return ERR; #ifdef HAVE_LIBREADLINE /* Allow conditional parsing of the ~/.inputrc file. */ rl_readline_name = "fcrondyn"; /* Tell the completer that we want a crack first. */ rl_attempted_completion_function = rl_cmpl_fcrondyn; while (1) { line_read = readline("fcrondyn> "); if (line_read == NULL) { /* Handle EOF gracefully and move past the prompt line */ printf("\n"); break; } return_code = talk_fcron(line_read, fd); #ifdef HAVE_READLINE_HISTORY if (line_read && *line_read) { add_history(line_read); } #endif /* HAVE_READLINE_HISTORY */ free(line_read); if (return_code == QUIT_CMD || return_code == ERR) { break; } } #else /* HAVE_LIBREADLINE */ while (fprintf(stderr, "fcrondyn> ") && fgets(buf, sizeof(buf), stdin) != NULL && (return_code = talk_fcron(buf, fd)) != QUIT_CMD && return_code != ERR) ; #endif /* HAVE_LIBREADLINE */ if (!existing_connection) xclose_check(&fd, "unix socket"); return OK; } void parseopt(int argc, char *argv[]) /* set options */ { int c, i; extern char *optarg; extern int optind, opterr, optopt; /* constants and variables defined by command line */ while (1) { c = getopt(argc, argv, "hVdc:ix:"); if (c == EOF) break; switch (c) { case 'V': info(); break; case 'h': usage(); break; case 'd': debug_opt = 1; break; case 'c': Set(fcronconf, optarg); break; case 'i': Free_safe(cmd_str); break; case 'x': Set(cmd_str, optarg); break; case ':': fprintf(stderr, "(setopt) Missing parameter.\n"); usage(); case '?': usage(); default: fprintf(stderr, "(setopt) Warning: getopt returned %c.\n", c); } } if (optind < argc) { for (i = optind; i <= argc; i++) fprintf(stderr, "Unknown argument \"%s\"", argv[i]); usage(); } } int main(int argc, char **argv) { int return_code = 0; int fd = (-1); /* fd == -1 means connection to fcron is not currently open */ struct passwd *pass = NULL; rootuid = get_user_uid_safe(ROOTNAME); rootgid = get_group_gid_safe(ROOTGROUP); if (strrchr(argv[0], '/') == NULL) prog_name = argv[0]; else prog_name = strrchr(argv[0], '/') + 1; user_uid = getuid(); user_gid = getgid(); if ((pass = getpwuid(user_uid)) == NULL) die("user \"%s\" is not in passwd file. Aborting.", USERNAME); user_str = strdup2(pass->pw_name); /* drop suid rights that we don't need, but keep the sgid rights * for now as we will need them for read_conf() and is_allowed() */ #ifdef USE_SETE_ID seteuid_safe(user_uid); #endif if (setuid(user_uid) < 0) die_e("could not setuid() to %d", user_uid); /* interpret command line options */ parseopt(argc, argv); /* read fcron.conf and update global parameters */ read_conf(); if (!is_allowed(user_str)) { die("User \"%s\" is not allowed to use %s. Aborting.", user_str, prog_name); } /* we don't need anymore special rights : drop remaining ones */ #ifdef USE_SETE_ID setegid_safe(user_gid); #endif if (setgid(user_gid) < 0) die_e("could not setgid() to %d", user_gid); /* check for broken pipes ... */ signal(SIGPIPE, sigpipe_handler); if (cmd_str == NULL) return_code = interactive_mode(fd); else return_code = talk_fcron(cmd_str, fd); xexit((return_code == OK) ? EXIT_OK : EXIT_ERR); /* never reached */ return EXIT_OK; }