//////////////////////////////////////////////////////////////////////////////////////////////////////// // a working (better ??) implementation of libsudo equivalent // allows to run sudo either in sync or async mode //////////////////////////////////////////////////////////////////////////////////////////////////////// #include "SysExec.h" #include "ArgEnv.h" NAMESPACE_UPP #ifdef PLATFORM_POSIX #include #include #include // a couple of stream helpers... we can't use Upp ones here because we need non-blocking streams static void _PutLine(FILE *f, const char *line) { fprintf(f, "%s\n", line); } static String _GetLine(FILE *f) { static char buf[256]; *buf = 0; fgets(buf, 255, f); return buf; } /////////////////////////////////////////////////////////////////////////////////////////////// // executes a command via sudo; if wait is true, will wait for command end, otherwise executes it in background bool SudoExec(String user, String const &password, String const &args, VectorMap const &envir, bool w) { // root user as default if(user == "") user = "root"; // take this damn'd thing from environment // it will make some apps crash... don't know why VectorMapenv(envir, 1); if(env.Find("DBUS_SESSION_BUS_ADDRESS")) env.RemoveKey("DBUS_SESSION_BUS_ADDRESS"); // setup XAUTHORITY for new user, if different // from current one -- this will allow using X display // for spawned user if(user != GetUserName()) { String xFile = env.Get("XAUTHORITY", "/home/" + GetUserName() + "/.Xauthority"); if(FileExists(xFile)) { // create a temp location for copied xFile String tmpFile = GetTempFileName(); FileCopy(xFile, tmpFile); env.RemoveKey("XAUTHORITY"); env.Add("XAUTHORITY", tmpFile); } } // prepare args for sudo execution String sudoArgs = "-H -E -p gimmipass "; sudoArgs += "-u " + user; sudoArgs += " -- " + args; String clearArgs = "-K"; // prepare args buffers char **argv = BuildArgs("sudo", sudoArgs); char **clearArgv = BuildArgs("sudo", clearArgs); // prepare environment buffer char **envv = BuildEnv(env); // forks the process to launch sudo command inside child // providing a pseudoconsole for sudo.. it refuses to run without // beware, forkpty() don't work here... probably the setsid() // needs to happen before login_tty() int result = 0; int masterPty, slavePty; if(openpty(&masterPty, &slavePty, NULL, NULL, NULL) == -1) return false; pid_t pid = fork(); if(pid < 0) return false; if(pid == 0) { // child process // becomes the session leader if(setsid() == -1) _exit(-1); // login into pseudoconsole if(login_tty(slavePty) == -1) _exit(-1); // ignore tty hangups so we can close // the pseudotty later signal(SIGHUP, SIG_IGN); // we shall first clear the password stored, if any int cPid = fork(); if(cPid == -1) _exit(-1); else if(cPid == 0) { result = execvp("/usr/bin/sudo", clearArgv); _exit(0); } else { int status; wait(&status); } // executes sudo result = execvpe("/usr/bin/sudo", argv, envv); // just in case execvp failed _exit(result); } else { // parent process // associates an stream to child process pseudoterminal FILE *sudoFile = fdopen(masterPty, "w+"); // set input pipe to non-blocking, so we can read as we like // without hanging the program -- we need to check for 'password' input fcntl(masterPty, F_SETFL, O_NONBLOCK); // even if we can read from stream, we wait that there's somethink to read // or, 1 second if nothing there, at most, to avoid that sudo prompt // arrives too late to catch it fd_set rfds; struct timeval tv; FD_ZERO(&rfds); FD_SET(masterPty, &rfds); tv.tv_sec = 1; tv.tv_usec = 0; select(masterPty + 1, &rfds, NULL, NULL, &tv); // now we can try to read the prompt; it could be the 'gimmipass' // we choose, or some error message, garbage or even nothing // we repeat read until we find OR the passprompt required, OR // some error messages, OR... nothing String line; int answer = 0; for(int i = 0; i < 50; i++) { line = _GetLine(sudoFile); if(line.StartsWith("gimmipass")) { answer = 1; break; } else if(line.StartsWith("sudo:") || line.StartsWith("no passwd entry for")) { answer = 2; break; } } // here we have : switch(answer) { // NO PASSWORD REQUIRED case 0: // we don't need any status from sudo line = ""; break; // PASSWORD REQUIRED case 1: // send the password to sudo Sleep(100); if(password != "") _PutLine(sudoFile, ~password); else _PutLine(sudoFile, ""); // hmmmm... it hangs sometimes here reading the status // it seems that sudo sometimes ask for password AND don't put // status back to terminal, OR something very wrong happens // on forks above. As is better to not have a status than // have the app hanging, we set a timeout on status read // blocking stream again, to be sure to get the status back // (we know that must come 2 lines of status...) fcntl(masterPty, F_SETFL, fcntl(masterPty, F_GETFL) & ~O_NONBLOCK); // skip the empty line after password _GetLine(sudoFile); // stop blocking again, we must clean sudo output by removing // all chars there fcntl(masterPty, F_SETFL, fcntl(masterPty, F_GETFL) | O_NONBLOCK); // give sudo some time to setup the status line, IF ANY fd_set rfds; struct timeval tv; FD_ZERO(&rfds); FD_SET(masterPty, &rfds); tv.tv_sec = 0; tv.tv_usec = 200000; select(masterPty + 1, &rfds, NULL, NULL, &tv); // get the status line = _GetLine(sudoFile); break; // USER DOESN'T EXISTS IN SUDOERS -- ERROR case 2: // close streams and leave with error fclose(sudoFile); return(false); } // switch // here we have in 'line' the status got back from sudo // or an empty line, if password wasn't required // we check the status for password correctness if( line.StartsWith("Sorry, try again.") || line.StartsWith("Sorry, user ") ) { fclose(sudoFile); return false; } else { int spc = line.Find(' '); if(spc >= 0 && line.Find("is not in", spc+1) >= 0) { fclose(sudoFile); return false; } } // ok, now we should have checked all and the sudo cmd // should have been succesful int status; bool res = true; // if requested, wait for child process to terminate if(w) { // it shall terminate sometimes.... waitpid(pid, &status, 0); // check sudo exit status if(WIFEXITED(status)) res = (WEXITSTATUS(status) == 0); else if(WIFSIGNALED(status)) res = true; else res = false; } // otherwise, just check once if process exited, by error // for example -- maybe it couldn't spawn command else { while(true) { // get spawned process executable file name FileIn f("/proc/" + FormatInt(pid) + "/cmdline"); String procName = GetFileTitle(f.GetLine()); f.Close(); // check if process is still running // it should, if all is ok int waitRes = waitpid(pid, &status, WNOHANG); // if still running within sudo, continue waiting if(!waitRes && procName == "sudo") { Sleep(100); continue; } // otherwise, if process exited, return its status else if(waitRes) { // check sudo exit status if(WIFEXITED(status)) res = (WEXITSTATUS(status) == 0); else if(WIFSIGNALED(status)) res = true; else res = false; return res; } // otherwise, process is still running as spawned app // all is ok, just return true return true; } } // eat last sudo ouptut, if any while(_GetLine(sudoFile) != "") ; // close pseudoterminal fclose(sudoFile); return res; } } #endif END_UPP_NAMESPACE