/* * ProFTPD - FTP server daemon * Copyright (c) 2001, 2002, 2003 The ProFTPD Project team * * 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. * * As a special exemption, The ProFTPD Project and other respective copyright * holders give permission to link this program with OpenSSL, and distribute * the resulting executable, without including the source code for OpenSSL in * the source distribution. */ /* * ProFTPD scoreboard support. * * $Id: scoreboard.c,v 1.31 2004/11/02 18:18:59 castaglia Exp $ */ #include "conf.h" #include /* From src/dirtree.c */ extern char ServerType; static pid_t scoreboard_opener = 0; static int scoreboard_fd = -1; static char scoreboard_file[PR_TUNABLE_PATH_MAX] = PR_RUN_DIR "/proftpd.scoreboard"; static off_t current_pos = 0; static pr_scoreboard_header_t header; static pr_scoreboard_entry_t entry; static struct flock entry_lock; static unsigned char scoreboard_read_locked = FALSE; static unsigned char scoreboard_write_locked = FALSE; /* Internal routines */ static char *handle_score_str(const char *fmt, va_list cmdap) { static char buf[PR_TUNABLE_SCOREBOARD_BUFFER_SIZE] = {'\0'}; memset(buf, '\0', sizeof(buf)); vsnprintf(buf, sizeof(buf), fmt, cmdap); buf[sizeof(buf)-1] = '\0'; return buf; } static int read_scoreboard_header(pr_scoreboard_header_t *sch) { int res = 0; /* No interruptions, please. */ pr_signals_block(); /* NOTE: reading a struct from a file using read(2) -- bad (in general). */ while ((res = read(scoreboard_fd, sch, sizeof(pr_scoreboard_header_t))) != sizeof(pr_scoreboard_header_t)) { int rd_errno = errno; if (res == 0) { pr_signals_unblock(); errno = EIO; return -1; } if (errno == EINTR) { pr_signals_handle(); continue; } pr_signals_unblock(); errno = rd_errno; return -1; } pr_signals_unblock(); /* Note: these errors will most likely occur only for inetd-run daemons. * Standalone daemons erase the scoreboard on startup. */ if (sch->sch_magic != PR_SCOREBOARD_MAGIC) { pr_close_scoreboard(); return PR_SCORE_ERR_BAD_MAGIC; } if (sch->sch_version < PR_SCOREBOARD_VERSION) { pr_close_scoreboard(); return PR_SCORE_ERR_OLDER_VERSION; } if (sch->sch_version > PR_SCOREBOARD_VERSION) { pr_close_scoreboard(); return PR_SCORE_ERR_NEWER_VERSION; } return 0; } static int rlock_scoreboard(void) { struct flock lock; lock.l_type = F_RDLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; while (fcntl(scoreboard_fd, F_SETLKW, &lock) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else return -1; } scoreboard_read_locked = TRUE; return 0; } static int unlock_entry(void) { entry_lock.l_type = F_UNLCK; entry_lock.l_whence = SEEK_CUR; entry_lock.l_len = sizeof(pr_scoreboard_entry_t); while (fcntl(scoreboard_fd, F_SETLKW, &entry_lock) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else return -1; } return 0; } static int unlock_scoreboard(void) { struct flock lock; lock.l_type = F_UNLCK; lock.l_whence = SEEK_SET; lock.l_start = 0; lock.l_len = 0; scoreboard_read_locked = scoreboard_write_locked = FALSE; return fcntl(scoreboard_fd, F_SETLK, &lock); } static int wlock_entry(void) { entry_lock.l_type = F_WRLCK; entry_lock.l_whence = SEEK_CUR; entry_lock.l_len = sizeof(pr_scoreboard_entry_t); while (fcntl(scoreboard_fd, F_SETLKW, &entry_lock) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else return -1; } return 0; } static int wlock_scoreboard(void) { struct flock lock; lock.l_type = F_WRLCK; lock.l_whence = 0; lock.l_start = 0; lock.l_len = 0; while (fcntl(scoreboard_fd, F_SETLKW, &lock) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else return -1; } scoreboard_write_locked = TRUE; return 0; } static int write_entry(void) { if (scoreboard_fd < 0) { errno = EINVAL; return -1; } lseek(scoreboard_fd, entry_lock.l_start, SEEK_SET); while (write(scoreboard_fd, &entry, sizeof(entry)) != sizeof(entry)) { if (errno == EINTR) { pr_signals_handle(); continue; } else return -1; } /* Rewind. */ lseek(scoreboard_fd, entry_lock.l_start, SEEK_SET); return 0; } /* Public routines */ int pr_close_scoreboard(void) { if (scoreboard_fd == -1) return 0; if (scoreboard_read_locked || scoreboard_write_locked) unlock_scoreboard(); while (close(scoreboard_fd) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else break; } scoreboard_fd = -1; scoreboard_opener = 0; return 0; } void pr_delete_scoreboard(void) { if (scoreboard_fd > -1) { while (close(scoreboard_fd) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else break; } } scoreboard_fd = -1; scoreboard_opener = 0; if (*scoreboard_file) { struct stat st; if (stat(scoreboard_file, &st) == 0) pr_log_debug(DEBUG3, "deleting existing scoreboard '%s'", scoreboard_file); unlink(scoreboard_file); } } const char *pr_get_scoreboard(void) { return scoreboard_file; } int pr_open_scoreboard(int flags) { int res; struct stat st; /* Try to prevent a file descriptor leak by only opening the scoreboard * file if the scoreboard file descriptor is not already positive, i.e. * if the scoreboard has not already been opened. */ if (scoreboard_fd >= 0 && scoreboard_opener == getpid()) { pr_log_debug(DEBUG7, "scoreboard already opened"); return 0; } pr_log_debug(DEBUG7, "opening scoreboard '%s'", scoreboard_file); /* Prevent writing to a symlink while avoiding a race condition: open * the file name O_RDWR|O_CREAT first, then check to see if it's a symlink. * If so, close the file and error out. If not, truncate as necessary, * and continue. */ while ((scoreboard_fd = open(scoreboard_file, flags|O_CREAT, PR_SCOREBOARD_MODE)) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else return -1; } /* Make certain that the scoreboard mode will be read-only for everyone * except the user owner (this allows for non-root-running daemons to * still modify the scoreboard). */ while (fchmod(scoreboard_fd, 0644) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else break; } while (fstat(scoreboard_fd, &st) < 0) { int st_errno = errno; if (errno == EINTR) { pr_signals_handle(); continue; } while (close(scoreboard_fd) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else break; } /* Either way, the scoreboard fd should be marked as closed. */ scoreboard_fd = -1; errno = st_errno; return -1; } if (S_ISLNK(st.st_mode)) { while (close(scoreboard_fd) < 0) { if (errno == EINTR) { pr_signals_handle(); continue; } else break; } errno = EPERM; scoreboard_fd = -1; return -1; } scoreboard_opener = getpid(); /* Check the header of this scoreboard file. */ if ((res = read_scoreboard_header(&header)) == -1) { /* If this file is newly created, it needs to have the header * written. */ header.sch_magic = PR_SCOREBOARD_MAGIC; header.sch_version = PR_SCOREBOARD_VERSION; if (ServerType == SERVER_STANDALONE) { header.sch_pid = getpid(); header.sch_uptime = time(NULL); } else { header.sch_pid = 0; header.sch_uptime = 0; } /* Write-lock the scoreboard file. */ if (wlock_scoreboard() < 0) return -1; while (write(scoreboard_fd, &header, sizeof(header)) != sizeof(header)) { int wr_errno = errno; if (errno == EINTR) { pr_signals_handle(); continue; } unlock_scoreboard(); errno = wr_errno; return -1; } unlock_scoreboard(); return 0; } else return res; return 0; } int pr_restore_scoreboard(void) { if (scoreboard_fd < 0) { errno = EINVAL; return -1; } /* Position the file position pointer of the scoreboard back to * where it was, prior to the last pr_rewind_scoreboard() call. */ lseek(scoreboard_fd, current_pos, SEEK_SET); return 0; } int pr_rewind_scoreboard(void) { if (scoreboard_fd < 0) { errno = EINVAL; return -1; } current_pos = lseek(scoreboard_fd, 0, SEEK_CUR); /* Position the file position pointer of the scoreboard at the * start of the scoreboard (past the header). */ lseek(scoreboard_fd, sizeof(pr_scoreboard_header_t), SEEK_SET); return 0; } int pr_set_scoreboard(const char *path) { char dir[PR_TUNABLE_PATH_MAX] = {'\0'}; struct stat st; char *tmp = NULL; sstrncpy(dir, path, sizeof(dir)); if ((tmp = strrchr(dir, '/')) == NULL) { errno = EINVAL; return -1; } *tmp = '\0'; /* Parent directory must not be world-writeable */ if (stat(dir, &st) < 0) return -1; if (!S_ISDIR(st.st_mode)) { errno = ENOTDIR; return -1; } if (st.st_mode & S_IWOTH) { errno = EPERM; return -1; } sstrncpy(scoreboard_file, path, sizeof(scoreboard_file)); return 0; } int pr_scoreboard_add_entry(void) { unsigned char found_slot = FALSE; if (scoreboard_fd < 0) { errno = EINVAL; return -1; } /* Write-lock the scoreboard file. */ if (wlock_scoreboard() < 0) return -1; /* No interruptions, please. */ pr_signals_block(); /* If the scoreboard is open, the file position is already past the * header. */ while (TRUE) { int res = 0; while ((res = read(scoreboard_fd, &entry, sizeof(entry))) == sizeof(entry)) { /* If this entry's PID is marked as zero, it means this slot can be * reused. */ if (!entry.sce_pid) { entry_lock.l_start = lseek(scoreboard_fd, 0, SEEK_CUR) - sizeof(entry); found_slot = TRUE; break; } } if (res == 0) { entry_lock.l_start = lseek(scoreboard_fd, 0, SEEK_CUR); found_slot = TRUE; } if (found_slot) break; } memset(&entry, '\0', sizeof(entry)); entry.sce_pid = getpid(); entry.sce_uid = geteuid(); entry.sce_gid = getegid(); if (write_entry() < 0) pr_log_pri(PR_LOG_NOTICE, "error writing scoreboard entry: %s", strerror(errno)); pr_signals_unblock(); /* We can unlock the scoreboard now. */ unlock_scoreboard(); return 0; } int pr_scoreboard_del_entry(unsigned char verbose) { if (scoreboard_fd < 0) { errno = EINVAL; return -1; } memset(&entry, '\0', sizeof(entry)); /* Write-lock this entry */ wlock_entry(); if (write_entry() < 0 && verbose) pr_log_pri(PR_LOG_NOTICE, "error deleting scoreboard entry: %s", strerror(errno)); unlock_entry(); return 0; } pid_t pr_scoreboard_get_daemon_pid(void) { return header.sch_pid; } time_t pr_scoreboard_get_daemon_uptime(void) { return header.sch_uptime; } pr_scoreboard_entry_t *pr_scoreboard_read_entry(void) { static pr_scoreboard_entry_t scan_entry; int res = 0; if (scoreboard_fd < 0) { errno = EINVAL; return NULL; } /* Make sure the scoreboard file is read-locked. */ if (!scoreboard_read_locked) { /* Do not proceed if we cannot lock the scoreboard. */ if (rlock_scoreboard() < 0) return NULL; } memset(&scan_entry, '\0', sizeof(scan_entry)); /* NOTE: use readv(2)? */ while (TRUE) { while ((res = read(scoreboard_fd, &scan_entry, sizeof(scan_entry))) <= 0) { if (res < 0 && errno == EINTR) { pr_signals_handle(); continue; } else { unlock_scoreboard(); return NULL; } } if (scan_entry.sce_pid) { unlock_scoreboard(); return &scan_entry; } else continue; } unlock_scoreboard(); return NULL; } /* We get clever with this function, so that it can be used to update * various entry attributes. */ int pr_scoreboard_update_entry(pid_t pid, ...) { va_list ap; char *tmp = NULL; int entry_tag = 0; if (scoreboard_fd < 0) { errno = EINVAL; return -1; } /* If updating some fields, clear the begin_idle field. */ va_start(ap, pid); while ((entry_tag = va_arg(ap, int)) != 0) { switch (entry_tag) { case PR_SCORE_USER: tmp = va_arg(ap, char *); memset(entry.sce_user, '\0', sizeof(entry.sce_user)); sstrncpy(entry.sce_user, tmp, sizeof(entry.sce_user)); break; case PR_SCORE_CLIENT_ADDR: { pr_netaddr_t *remote_addr = va_arg(ap, pr_netaddr_t *); snprintf(entry.sce_client_addr, sizeof(entry.sce_client_addr), "%s", remote_addr ? pr_netaddr_get_ipstr(remote_addr) : "(unknown)"); entry.sce_client_addr[sizeof(entry.sce_client_addr) - 1] = '\0'; } break; case PR_SCORE_CLIENT_NAME: { char *remote_name = va_arg(ap, char *); snprintf(entry.sce_client_name, sizeof(entry.sce_client_name), "%s", remote_name ? remote_name : "(unknown)"); entry.sce_client_name[sizeof(entry.sce_client_name) - 1] = '\0'; } break; case PR_SCORE_CLASS: tmp = va_arg(ap, char *); memset(entry.sce_class, '\0', sizeof(entry.sce_class)); sstrncpy(entry.sce_class, tmp, sizeof(entry.sce_class)); break; case PR_SCORE_CWD: tmp = va_arg(ap, char *); memset(entry.sce_cwd, '\0', sizeof(entry.sce_cwd)); sstrncpy(entry.sce_cwd, tmp, sizeof(entry.sce_cwd)); break; case PR_SCORE_CMD: { char *cmdstr = NULL; tmp = va_arg(ap, char *); cmdstr = handle_score_str(tmp, ap); memset(entry.sce_cmd, '\0', sizeof(entry.sce_cmd)); sstrncpy(entry.sce_cmd, cmdstr, sizeof(entry.sce_cmd)); tmp = va_arg(ap, void *); } break; case PR_SCORE_CMD_ARG: { char *argstr = NULL; tmp = va_arg(ap, char *); argstr = handle_score_str(tmp, ap); memset(entry.sce_cmd_arg, '\0', sizeof(entry.sce_cmd_arg)); sstrncpy(entry.sce_cmd_arg, argstr, sizeof(entry.sce_cmd_arg)); tmp = va_arg(ap, void *); } break; case PR_SCORE_SERVER_PORT: entry.sce_server_port = va_arg(ap, int); break; case PR_SCORE_SERVER_ADDR: { pr_netaddr_t *server_addr = va_arg(ap, pr_netaddr_t *); int server_port = va_arg(ap, int); snprintf(entry.sce_server_addr, sizeof(entry.sce_server_addr), "%s:%d", server_addr ? pr_netaddr_get_ipstr(server_addr) : "(unknown)", server_port); entry.sce_server_addr[sizeof(entry.sce_server_addr)-1] = '\0'; } break; case PR_SCORE_SERVER_LABEL: tmp = va_arg(ap, char *); memset(entry.sce_server_label, '\0', sizeof(entry.sce_server_label)); sstrncpy(entry.sce_server_label, tmp, sizeof(entry.sce_server_label)); break; case PR_SCORE_BEGIN_IDLE: /* Ignore this */ (void) va_arg(ap, time_t); time(&entry.sce_begin_idle); break; case PR_SCORE_BEGIN_SESSION: /* Ignore this */ (void) va_arg(ap, time_t); time(&entry.sce_begin_session); break; case PR_SCORE_XFER_DONE: entry.sce_xfer_done = va_arg(ap, off_t); break; case PR_SCORE_XFER_SIZE: entry.sce_xfer_size = va_arg(ap, off_t); break; case PR_SCORE_XFER_LEN: entry.sce_xfer_len = va_arg(ap, off_t); break; case PR_SCORE_XFER_ELAPSED: entry.sce_xfer_elapsed = va_arg(ap, unsigned long); break; default: errno = EINVAL; return -1; } } /* Write-lock this entry */ wlock_entry(); if (write_entry() < 0) pr_log_pri(PR_LOG_NOTICE, "error writing scoreboard entry: %s", strerror(errno)); unlock_entry(); return 0; }