/* slimski- Simple Login Manager
   Copyright (C) 2009 Eygene Ryabinkin <rea@codelabs.ru>

   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.
*/

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include "util.h"
#include <sys/wait.h>
#include <cstring>     // expensive?
#include <string>
// # i nclude <fcntl.h>
#include <limits.h>
#include <errno.h>
#include <paths.h>
#include <sys/stat.h>
#include "app.h"   //   zeebug

extern bool testing;
extern bool zeebug;


bool Util::add_mcookie(const std::string &mcookie, const char *display,
    const std::string &xauth_cmd, const std::string &authfile)
{
    FILE *fp;
    std::string cmd = xauth_cmd + " -f " + authfile + " -q";

    fp = popen(cmd.c_str(), "w");
    if (!fp)
        return false;
    fprintf(fp, "remove %s\n", display);
    fprintf(fp, "add %s %s %s\n", display, ".", mcookie.c_str());
    fprintf(fp, "exit\n");

    pclose(fp);
    return true;
}


void Util::srandom(unsigned long seed) {
    ::srandom(seed);
}


long Util::random(void) {
    return ::random();
}


// Makes seed for the srandom() using "random" values obtained from getpid(), time(NULL) and others.
//       sole caller is App::CreateServerAuth()
long Util::makeseed(void) {
    struct timespec ts;
    long pid = getpid();
    long tm = time(NULL);
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
        ts.tv_sec = ts.tv_nsec = 0;
    }

    return pid + tm + (ts.tv_sec ^ ts.tv_nsec);
}


    //   for log timestamp         getCurrentDateTime("now")
string Util::getCurrentDateTime( string s ){
    time_t now = time(0);
    struct tm  tstruct;
    char  buf[80];
    tstruct = *localtime(&now);
    if(s=="now")
        strftime(buf, sizeof(buf), "%Y-%m-%d_%H-%M-%S", &tstruct);
    else if(s=="date")
        strftime(buf, sizeof(buf), "%Y-%m-%d", &tstruct);
    return string(buf);
}
/**
void Logger( string logMsg ){
    string filePath = "/somedir/log_"+getCurrentDateTime("date")+".txt";
    string now = getCurrentDateTime("now");
    ofstream ofs(filePath.c_str(), std::ios_base::out | std::ios_base::app );
    ofs << now << '\t' << logMsg << '\n';
    ofs.close();
}
*/

//  upon success, returns  1
int Util::dropElevatedPrivileges() {
    //   COULD check, and conditionally skip           if (getuid() == 0) || geteuid == 0)

    //    initgroups()          catch22 ~~ cannot perform initgroups() unless we retrieve the uid
    // ref:  https://www.safaribooksonline.com/library/view/secure-programming-cookbook/0596003943/ch01s03.html#secureprgckbk-CHP-1-SECT-3.3

    //   change guid + uid   ~~  nobody (uid 65534), nogroup (gid 65534), users (gid 100)
    if (setgid(65534) != 0) return 1;  //  ("Failed to set nonroot GID") so apparently NOT root
    if (setuid(65534) != 0) return 1;  //  ("Failed to set nonroot UID") so apparently NOT root
                                       // or failed due to brittle/flawed expectation that nobody and nogroup do exist

    // On systems with defined _POSIX_SAVED_IDS in the unistd.h file,
    // it should be impossible to regain elevated privs after the setuid() call, above.
    // Test, try to regain elev priv:
    if (setuid(0) != -1)  return 0;   // and the calling fn should EXIT/abort the program
    //  COULD also confirm that setregid() fails

    // change cwd, for good measure (if unable to, treat as overall failure)
    if (chdir("/tmp") != 0)   return 0;  // consider fprint()ing or logging the failure reason

    return 1;
}


//           CAREFUL CONSIDERATION
//
//    https://github.com/canonical/lightdm/blob/02fa01c67289d5dfcd2287a6cd2c46090884ac69/src/session-child.c
//         so, yeah,      ls  /proc/ourslimski/fd     0 1 2 3         a file descriptor for our logfile is open
//
//    http://www.yolinux.com/TUTORIALS/ForkExecProcesses.html
//    https://stackoverflow.com/questions/8888563/c-fork-execl-dont-wait-detach-completely  (not our exact case, here)
//    https://wiki.sei.cmu.edu/confluence/pages/viewpage.action?pageId=87152177
//    https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch01s07.html
////  https://www.oreilly.com/library/view/secure-programming-cookbook/0596003943/ch01s05.html
#ifndef OPEN_MAX
#define OPEN_MAX 256
#endif
int Util::open_devnull(int fd) {
    FILE *f = 0;
    if (!fd) f = freopen(_PATH_DEVNULL, "rb", stdin);
    else if (fd == 1)     f = freopen(_PATH_DEVNULL, "wb", stdout);
    else if (fd == 2)     f = freopen(_PATH_DEVNULL, "wb", stderr);
    return (f && fileno(f) == fd);
}
//---------------------^---v
void Util::sanitize_fds(void) {
  int         fd, fds;
  struct stat st;
  // ensure all open descriptors other than the standard ones (i.e. stdin,stdout,stderr) are closed
  if ((fds = getdtablesize(  )) == -1)        fds = OPEN_MAX;
  for (fd = 3;  fd < fds;  fd++)      close(fd);
  // Verify that the standard descriptors are open.  If they're not, attempt to open them using /dev/null.  If any are unsuccessful, abort.
  for (fd = 0;  fd < 3;  fd++)
      if (fstat(fd, &st) == -1 && (errno != EBADF || !Util::open_devnull(fd))) abort(  );
}



/**
bool goodExec(std::string& command) {
    char *firstWord = strtok(command, " \t\n\v\f\r");          //   ?other unicode whitespace chars
        if (zeebug)  {              // https://en.cppreference.com/w/cpp/string/byte/isspace
            if (stat(firstWord, &sb) == 0 && sb.st_mode & S_IXUSR)
                cout << "executable by the user" << firstWord << endl;
            else
                cout << "NOT executable by the user" << firstWord << endl;
        }
}
*/



/////   https://github.com/ar-/incron/pull/60              if (system(cmd.c_str()) != 0) // exec failed
/////                                                      if (execl("/bin/sh","sh", "-c", cmd.c_str(), (char *)0) != 0) // exec failed

//   howdy bub       not suitable
void Util::runCommand(const std::string& command, std::string saywhat) {
    if (command == "") {
        return;
    }
    if (saywhat != ""  && zeebug ) {
        //    howdy   bub     should also show the "saywhat"?
        cout << "---- exec " << saywhat.c_str() << " : " << command.c_str()  << endl;
    }

    pid_t pid;
    pid = fork();     // fork and execute program (replace the child process)
    // any file descriptors which were open in the parent are closed only if we explicitly mark them close-on-exec
    //
    if (pid == 0) {         //  0 indicates this codepoint is occurring within the child
        setsid();
        Util::sanitize_fds();

        if(!Util::dropElevatedPrivileges()) {
            exit(1);
        }

        // int execl(const char *fullExecutablePath, const char *arg, ... , NULL);         upon success, execl provides no return
        const char *commandout = command.c_str();
        if (execl("/usr/bin/sh",  "sh", "-c", commandout, (char *)0) == -1) {
            //      howdy      BASH    this detail, choice of shell, should be user-configurable?
            //
            //      howdy  bub                  for now, gonna go with generic     sh
//      std::string commandout = "'" + command + "'";              //  howdy bub     test to ensure any apostrophes in the commandstring will not BOOM
//      if (execl("/usr/bin/sh",  "/usr/bin/sh", "-c", commandout, (char *)0) == -1) {

            // fprintf(stdout, "Error when attempting to execute command '%s': %s\n", command.c_str() );
            //            fuggit                       ^--->  format ‘%s’ expects a matching ‘char*’ argument

            exit(1);
            // or  _exit(2), so that further errors are seen by the child only
        }
    } else if (pid < 0) {                 // we should NEVER reach this point
        //fprintf(stdout, "error: failed to fork a process to run command '%s': %s\n", command.c_str() );
        if (testing)
            cout << "error: failed to fork a process to run command: " << command.c_str() << endl;
    } else {
        // Ordinarily, waitpid() suspends the calling process until the system gets status information on the child.
        // (except if the system already has status information on an appropriate child when waitpid() is called, waitpid() returns immediately.)
        //
        // WNOHANG demands status information immediately. If status information is immediately available on an appropriate child
        // process, waitpid() returns this information. Otherwise, waitpid() returns immediately with an error code indicating that
        // the information was not available.  In other words, WNOHANG checks child processes without causing the caller to be suspended.
        ////waitpid(pid, NULL, WNOHANG);    // waitpid(pid, NULL, 0);     // waitpid(-1, &status, WNOHANG)
        ////
         // waitpid(pid, NULL, WUNTRACED);   //   howdy bub     We MUST wait (vs resuming mainloop and Panel winds up grabbing all keystrokes
                                 //   maybe add an arg to this fn, to make it more flexible (also consider use of more than JUST execl)
    }
}
