/*
 * FCRON - periodic command scheduler 
 *
 *  Copyright 2000-2024 Thibault Godouet <fcron@free.fr>
 *
 *  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.
 */


#include "global.h"
#include "subs.h"
#include <sys/time.h>

uid_t
get_user_uid_safe(char *username)
    /* get the uid of user username, and die on error */
{
    struct passwd *pass;

    errno = 0;
    pass = getpwnam(username);
    if (pass == NULL) {
        die_e("Unable to get the uid of user %s (is user in passwd file?)",
              username);
    }

    return pass->pw_uid;

}

gid_t
get_group_gid_safe(char *groupname)
    /* get the gid of group groupname, and die on error */
{
    struct group *grp = NULL;

    errno = 0;
    grp = getgrnam(groupname);
    if (grp == NULL) {
        die_e("Unable to get the gid of group %s", groupname);
    }

    return grp->gr_gid;

}

#ifdef USE_SETE_ID
void
seteuid_safe(uid_t euid)
/* set the euid if different from the current one, and die on error */
{
    /* on BSD, one can only seteuid() to the real UID or saved UID,
     * and NOT the euid, so we don't call seteuid(geteuid()),
     * which is why we need to check if a change is needed */

    if (geteuid() != euid && seteuid(euid) != 0)
        die_e("could not change euid to %d", euid);

}

void
setegid_safe(gid_t egid)
/* set the egid if different from the current one, and die on error */
{
    /* on BSD, one can only setegid() to the real GID or saved GID,
     * and NOT the egid, so we don't call setegid(getegid()),
     * which is why we need to check if a change is needed */

    if (getegid() != egid && setegid(egid) != 0)
        die_e("could not change egid to %d", egid);

}
#endif                          /* def USE_SETE_ID */

#ifdef USE_SETE_ID
int
open_as_user(const char *pathname, uid_t openuid, gid_t opengid, int flags, ...)
/* Become user and call open(), then revert back to who we were.
 * NOTE: when flags & O_CREAT, the 5th argument is mode_t and must be set
 *       -- it is ignored otherwise */
{
    uid_t orig_euid = geteuid();
    gid_t orig_egid = getegid();
    struct stat s;
    int fd = -1;
    va_list ap;
    mode_t mode = (mode_t) 0;
    int saved_errno = 0;

    if (flags & O_CREAT) {
        va_start(ap, flags);
        mode =
            (sizeof(mode_t) < sizeof(int)) ? va_arg(ap, int) : va_arg(ap,
                                                                      mode_t);
        va_end(ap);
    }

    seteuid_safe(openuid);
    setegid_safe(opengid);

    if (flags & O_CREAT) {
        fd = open(pathname, flags, mode);
    }
    else
        fd = open(pathname, flags);

    saved_errno = errno;

    /* change the effective uid/gid back to original values */
    seteuid_safe(orig_euid);
    setegid_safe(orig_egid);

    /* if open() didn't fail make sure we opened a 'normal' file */
    if (fd >= 0) {

        if (fstat(fd, &s) < 0) {
            saved_errno = errno;
            error_e("open_as_user(): could not fstat %s", pathname);
            if (xclose(&fd) < 0)
                error_e("open_as_user: could not xclose() %s", pathname);
            fd = -1;
        }

        if (!S_ISREG(s.st_mode) || s.st_nlink != 1) {
            error_e("open_as_user(): file %s is not a regular file", pathname);
            if (xclose(&fd) < 0)
                error_e("open_as_user: could not xclose() %s", pathname);
            saved_errno = 0;
            fd = -1;
        }

    }

    errno = saved_errno;

    return fd;

}

#else                           /* def USE_SETE_ID */

int
open_as_user(const char *pathname, uid_t openuid, gid_t opengid, int flags, ...)
/* Become user and call open(), then revert back to who we were.
 * As seteuid() is not available on this system attempt to similate that behavior
 * as closely as possible.
 * NOTE: when flags & O_CREAT, the 5th argument is mode_t and must be set
 *       -- it is ignored otherwise */
{
    int fd = -1;
    struct stat s;
    va_list ap;
    mode_t mode = (mode_t) 0;
    int saved_errno = 0;

    if (flags & O_CREAT) {
        va_start(ap, flags);
        mode =
            (sizeof(mode_t) < sizeof(int)) ? va_arg(ap, int) : va_arg(ap,
                                                                      mode_t);
        va_end(ap);
    }

    /* In case a flag as O_TRUNC is set, we should test if the user
     * is allowed to open the file before we open it.
     * There will always be a risk of race-condition between the test
     * and the open but that's the best we can realistically do
     * without seteuid()... */
    if (stat(pathname, &s) == 0) {
        if (!
            (s.st_mode & S_IROTH || (s.st_uid == openuid && s.st_mode & S_IRUSR)
             || (s.st_gid == opengid && s.st_mode & S_IRGRP))) {
            error("open_as_user(): file %s does not pass the security test: "
                  "uid=%d gid=%d mode=%lo openuid=%d opengid=%d",
                  pathname, s.st_uid, s.st_gid, s.st_mode, openuid, opengid);
            errno = EACCES;
            return -1;
        }
    }
    else if (errno == ENOENT) {
        /* the file doesn't exist so no risk to truncate the wrong file! */
        ;
    }
    else {
        saved_errno = errno;
        error_e("open_as_user(): could not stat %s", pathname);
        errno = saved_errno;
        return -1;
    }

    if (flags & O_CREAT) {
        fd = open(pathname, flags, mode);
    }
    else
        fd = open(pathname, flags);

    if (fd < 0)
        /* we couldn't open the file */
        return fd;

    /* if open() didn't fail make sure we opened a 'normal' file */
    if (fstat(fd, &s) < 0) {
        saved_errno = errno;
        error_e("open_as_user(): could not fstat %s", pathname);
        goto err;
    }
    if (!S_ISREG(s.st_mode) || s.st_nlink != 1) {
        saved_errno = errno;
        error_e("open_as_user(): file %s is not a regular file", pathname);
        goto err;
    }

    /* we couldn't become openuid/opengid, so check manually if the user
     * is allowed to read that file
     * We do that again as a malicious user could have replaced the file
     * by another one (e.g. a link) between the stat() and the open() earlier */
    if (!(s.st_mode & S_IROTH || (s.st_uid == openuid && s.st_mode & S_IRUSR)
          || (s.st_gid == opengid && s.st_mode & S_IRGRP))) {
        error("open_as_user(): file %s does not pass the security test: "
              "uid=%d gid=%d mode=%lo openuid=%d opengid=%d",
              pathname, s.st_uid, s.st_gid, s.st_mode, openuid, opengid);
        saved_errno = EACCES;
        goto err;
    }

    /* if we created a new file, change the file ownership:
     * make it as it would be if we had seteuid()
     * NOTE: if O_CREAT was set without O_EXCL and the file existed before
     *       then we will end up changing the ownership even if the seteuid()
     *       version of that function wouldn't have. That shouldn't break
     *       anything though. */
    if ((flags & O_CREAT) && fchown(fd, openuid, opengid) != 0) {
        saved_errno = errno;
        error_e("Could not fchown %s to uid:%d gid:%d", pathname, openuid,
                opengid);
        goto err;
    }

    /* everything went ok: return the file descriptor */
    return fd;

 err:
    if (fd >= 0 && xclose(&fd) < 0)
        error_e("open_as_user: could not xclose() %s", pathname);
    errno = saved_errno;
    return -1;
}

#endif                          /* def USE_SETE_ID */

int
remove_as_user(const char *pathname, uid_t removeuid, gid_t removegid)
/* Become user and call remove(), then revert back to who we were */
{
    int rval = -1;
#ifdef USE_SETE_ID
    uid_t orig_euid = geteuid();
    gid_t orig_egid = getegid();

    seteuid_safe(removeuid);
    setegid_safe(removegid);
#endif                          /* def USE_SETE_ID */

    rval = remove(pathname);

#ifdef USE_SETE_ID
    seteuid_safe(orig_euid);
    setegid_safe(orig_egid);
#endif                          /* def USE_SETE_ID */

    return rval;
}

int
rename_as_user(const char *oldpath, const char *newpath, uid_t renameuid,
               gid_t renamegid)
/* Become user and call rename(), then revert back to who we were */
{
    int rval = -1;
#ifdef USE_SETE_ID
    uid_t orig_euid = geteuid();
    gid_t orig_egid = getegid();

    seteuid_safe(renameuid);
    setegid_safe(renamegid);
#endif                          /* def USE_SETE_ID */

    rval = rename(oldpath, newpath);

#ifdef USE_SETE_ID
    seteuid_safe(orig_euid);
    setegid_safe(orig_egid);
#endif                          /* def USE_SETE_ID */

    return rval;

}

int
remove_blanks(char *str)
    /* remove blanks at the the end of str */
    /* return the length of the new string */
{
    char *c = str;

    /* scan forward to the null */
    while (*c)
        c++;

    /* scan backward to the first character that is not a space */
    do {
        c--;
    }
    while (c >= str && isspace((int)*c));

    /* if last char is a '\n', we remove it */
    if (*c == '\n')
        *c = '\0';
    else
        /* one character beyond where we stopped above is where the null
         * goes. */
        *++c = '\0';

    /* return the new length */
    return (c - str);

}

int
strcmp_until(const char *left, const char *right, char until)
/* compare two strings up to a given char (copied from Vixie cron) */
/* Copyright 1988,1990,1993,1994 by Paul Vixie */
/* Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.  */
{
    while (*left != '\0' && *left != until && *left == *right) {
        left++;
        right++;
    }

    if ((*left == '\0' || *left == until)
        && (*right == '\0' || *right == until)) {
        return (0);
    }
    return (*left - *right);
}

int
get_word(char **str)
    /* make str point the next word and return word length */
{
    char *ptr;

    Skip_blanks(*str);
    ptr = *str;

    while ((isalnum((int)*ptr) || *ptr == '_' || *ptr == '-')
           && *ptr != '=' && !isspace((int)*ptr))
        ptr++;

    return (ptr - *str);
}

void
my_unsetenv(const char *name)
/* call unsetenv() if available, otherwise call putenv("var=").
 * Check for errors and log them. */
{

#ifdef HAVE_UNSETENV
    if (unsetenv(name) < 0)
        error_e("could not flush env var %s with unsetenv()", name);
#else
    char buf[PATH_LEN];
    snprintf(buf, sizeof(buf) - 1, "%s=", name);
    buf[sizeof(buf) - 1] = '\0';
    if (putenv(buf) < 0)
        error_e("could not flush env var %s with putenv()", name);
#endif

}

void
my_setenv_overwrite(const char *name, const char *value)
/* call setenv(x, x, 1) if available, otherwise call putenv() with the appropriate
 * constructed string.
 * Check for errors and log them. */
{

#ifdef HAVE_SETENV

    /* // */
    debug("Calling setenv(%s, %s, 1)", name, value);
    /* // */
    if (setenv(name, value, 1) != 0)
        error_e("setenv(%s, %s, 1) failed", name, value);

#else
    char buf[PATH_LEN];

    snprintf(buf, sizeof(buf) - 1, "%s=%s", name, value)

        /* The final \0 may not have been copied because of lack of space:
         * add it to make sure */
        buf[sizeof(buf) - 1] = '\0';

    /* // */
    debug("Calling putenv(%s)", buf);
    /* // */
    if (putenv(buf) != 0)
        error_e("putenv(%s) failed", buf);

#endif

}

time_t
tmax(time_t x, time_t y)
/* return the larger value (maximum) of x and y */
{
    if (x > y) {
        return x;
    }
    else {
        return y;
    }
}

time_t
tmin(time_t x, time_t y)
/* return the smaller value (minimum) of x and y */
{
    if (x < y) {
        return x;
    }
    else {
        return y;
    }
}

int
imax(int x, int y)
/* return the larger value (maximum) of x and y */
{
    if (x > y) {
        return x;
    }
    else {
        return y;
    }
}

int
imin(int x, int y)
/* return the smaller value (minimum) of x and y */
{
    if (x < y) {
        return x;
    }
    else {
        return y;
    }
}

time_t
my_time(void)
/* return the current time as number of seconds since the Epoch. */
{
    /* Use gettimeofday() if available as this is more accurate
     * than time() on recent Linux systems.
     * (I suspect time() sacrifies accuracy for speed in the same way as
     * clock_gettime()'s CLOCK_REALTIME_COARSE, and only updates on systems ticks) */
#ifdef HAVE_GETTIMEOFDAY
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec;
#else                           /* HAVE_GETTIMEOFDAY */
    return time(NULL);
#endif                          /* HAVE_GETTIMEOFDAY */
}
