#include "pwgui.h"
#include "config.h"
#include <QApplication>
#include <cups/backend.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <limits.h>
#include <pwd.h>
#include <grp.h>
#define NAMELEN 400
#define BUFLEN 1000
#define ENVLEN 10000
static int pid = -1;
// UID and GUI of user we should drop privileges to
static int ruid = 65534, rgid = 65534;
static char ruser[NAMELEN] = "";
// Copy of the GUIs environment, so we can access X
// Whatever you do, make sure this has at least two nullchars at the end!
static char gui_env[ENVLEN] = "\0\0\0";
static int run_backend(char *backend, char *uri, char *jobid, char *user, char *title, char *copies, char *options, char *file, char *password);
static bool helper_getpiduid(char *user, char *title);
static bool helper_loadlpuser();
static void helper_dropprivs();
static void helper_copyenv();
int main(int argc, char *argv[])
{
char tmpfile[NAMELEN];
char device[NAMELEN];
char backend[NAMELEN];
int spoolres;
// Pretty much what smbspool does, but in a generalized way
if (argc > 2 && strstr(argv[0], ":/") == NULL && strstr(argv[1], ":/") != NULL) {
argv++;
argc--;
}
// First check parameter count
if (argc != 6 && argc != 7) {
fputs("ERROR: Invalid number of arguments passed.\n", stderr);
return CUPS_BACKEND_FAILED;
}
// Determine device uri
char *env = getenv("DEVICE_URI");
if (env != NULL && strchr(env, ':') != NULL) {
snprintf(device, NAMELEN, "%s", env);
} else if (strstr(argv[0], ":/") != NULL) {
snprintf(device, NAMELEN, "%s", argv[0]);
} else {
fputs("ERROR: No device URI given.\n", stderr);
return CUPS_BACKEND_FAILED;
}
// Get backend from uri
char *colon = strchr(device, ':');
*colon = '\0';
snprintf(backend, NAMELEN, "%s/%s", BACKEND_PATH, device);
*colon = ':';
// Is valid?
if (access(backend, X_OK | R_OK) != 0) {
fprintf(stderr, "ERROR: Backend %s is not executable.\n", backend);
return CUPS_BACKEND_FAILED;
}
// argv[3] is title, get printergui pid from it
if (!helper_getpiduid(argv[2], argv[3])) {
// El cheapo validation failed. Don't enable "smart mode" (GUI etc), just exec real backend
// Mimic cups behaviour wrt dropping privs (Only 0700 or 0500 == root)
helper_loadlpuser();
struct stat st;
if (stat(backend, &st) != 0 || (st.st_mode & 0011) != 0) helper_dropprivs();
kill(pid, SIGTERM);
execv(backend, argv);
exit(127);
}
kill(pid, SIGTERM);
// Get document to print
if (argc == 6) {
// Data comes from stdin, save...
snprintf(tmpfile, NAMELEN, "/tmp/print-%s-%d-%s-%d", argv[1], (int)time(NULL), argv[2], (int)getpid());
int fh = open(tmpfile, O_CREAT | O_WRONLY | O_TRUNC, 0600);
if (fh < 0) {
fprintf(stderr, "ERROR: Could not open %s for writing..\n", tmpfile);
return CUPS_BACKEND_FAILED;
}
char buffer[BUFLEN];
size_t bytes, ret;
while (!feof(stdin)) {
bytes = fread(buffer, BUFLEN, 1, stdin);
if (bytes == 0) break;
if ((ret = write(fh, buffer, bytes)) != bytes) {
fprintf(stderr, "ERROR: Could not write %d bytes to %s (wrote %d)\n", (int)bytes, tmpfile, (int)ret);
remove(tmpfile);
return CUPS_BACKEND_FAILED;
}
}
close(fh);
//
} else {
// File given, check if file exists
snprintf(tmpfile, NAMELEN, "%s", argv[6]);
int fh = open(tmpfile, O_RDONLY);
if (fh < 0) {
fprintf(stderr, "ERROR: Could not open %s for reading..\n", tmpfile);
return CUPS_BACKEND_FAILED;
}
close(fh);
//
}
chown(tmpfile, ruid, rgid);
// Try right away with what we got
spoolres = run_backend(backend, device, argv[1], argv[2], argv[3], argv[4], argv[5], tmpfile, NULL);
if (spoolres != CUPS_BACKEND_AUTH_REQUIRED) return spoolres; // Yay
// Seems we need the dialog
int status;
char creds[NAMELEN], *pass = NULL;
snprintf(creds, NAMELEN, "%s", argv[2]);
do {
int pfd[2];
if (pipe(pfd) != 0) {
fputs("ERROR: Could not create pipe for GUI.\n", stderr);
return CUPS_BACKEND_FAILED;
}
pid_t pid = fork();
if (pid == 0) {
// Child - GUI
close(pfd[0]);
helper_dropprivs();
helper_copyenv();
QApplication a(argc, argv);
PwGui w(pfd[1], creds);
w.show();
exit(a.exec());
return CUPS_BACKEND_FAILED;
}
// Main (Parent)
close(pfd[1]);
// Read from pipe
int bytes = read(pfd[0], creds, NAMELEN - 1);
close(pfd[0]);
// Wait for child to die
waitpid(pid, NULL, 0); // Don't check status, just look at pipe data
if (bytes <= 0) {
fputs("ERROR: Could not read anything from pipe after showing GUI.\n", stderr);
remove(tmpfile);
return CUPS_BACKEND_CANCEL;
}
creds[bytes] = '\0';
int len = strlen(creds);
if (len < bytes) pass = creds + len + 1;
// Run backend with pimped user/pass
status = run_backend(backend, device, argv[1], creds, argv[3], argv[4], argv[5], tmpfile, pass);
} while (status != CUPS_BACKEND_OK);
remove(tmpfile);
fprintf(stderr, "Job submitted.\n");
return CUPS_BACKEND_OK;
}
static int run_backend(char *backend, char *uri, char *jobid, char *user, char *title, char *copies, char *options, char *file, char *password)
{
pid_t pid = fork();
if (pid == 0) {
// Child
if (user != NULL) setenv("AUTH_USERNAME", user, 1);
if (password != NULL) setenv("AUTH_PASSWORD", password, 1);
char *args[8];
args[0] = uri;
args[1] = jobid;
args[2] = user;
args[3] = title;
args[4] = copies;
args[5] = options;
args[6] = file;
args[7] = NULL;
// Priv dropping
struct stat st;
if (stat(backend, &st) != 0 || (st.st_mode & 0011) != 0) helper_dropprivs();
// Exec
execv(backend, args);
exit(127);
return 127;
}
// Main - wait for it...
int status;
waitpid(pid, &status, 0);
if (!WIFEXITED(status)) {
fprintf(stderr, "ERROR: Running backend %s failed!\n", backend);
return CUPS_BACKEND_FAILED;
}
return WEXITSTATUS(status);
}
static bool helper_getpiduid(char *user, char *title)
{
// it has to be gui-<PID>, PID has to be an instance of printergui
// and we have to be able to kill it, only then we assume we should bother the user
// with an authentication dialog
if (strncmp(title, "gui-", 4) != 0) {
fprintf(stderr, "WARNING: Job Title doesnt start with 'gui-' (Is: %s)\n", title);
return false; // Wrong job title
}
int p = atoi(title + 4);
struct stat st;
struct passwd *pw = getpwnam(user);
if (pw == NULL) {
fprintf(stderr, "WARNING: Cannot getpwnam %s\n", user);
return false;
}
char bin[PATH_MAX+1], tmp[100];
snprintf(tmp, 100, "/proc/%d/exe", p);
if (realpath(tmp, bin) == NULL) {
fprintf(stderr, "WARNING: Cannot get realpath of %s\n", tmp);
return false;
}
char *last = strrchr(bin, '/');
if (last == NULL || strcmp(last, "/printergui") != 0) {
fprintf(stderr, "WARNING: %s does not end in /printergui\n", bin);
return false; // Wrong process
}
// PID passed via job title seems to be the printergui
if (lstat(tmp, &st) < 0) {
fprintf(stderr, "WARNING: Could not lstat() %s\n", tmp);
return false;
}
if (st.st_uid != pw->pw_uid) {
fprintf(stderr, "WARNING: Owner of %s: %d, owner of job: %d (%s)\n", tmp, (int)st.st_uid, (int)pw->pw_uid, user);
return false; // Print job user doesn't match printergui process owner
}
// All checks passed, make stuff global
pid = p;
ruid = pw->pw_uid;
rgid = pw->pw_gid;
snprintf(ruser, NAMELEN, "%s", user);
// Finally, try to copy the environment of the process
snprintf(tmp, 100, "/proc/%d/environ", p);
int fh = open(tmp, O_RDONLY);
if (fh >= 0) {
char *ptr = gui_env;
int bytes = 0, ret;
while ((ret = read(fh, ptr, ENVLEN - (ptr - gui_env) - 2)) > 0) {
bytes += ret;
ptr += ret;
if (bytes + 3 >= ENVLEN) break;
}
close(fh);
if (bytes >= 0) {
gui_env[bytes+0] = '\0';
gui_env[bytes+1] = '\0';
}
}
fprintf(stderr, "DEBUG: getpiduid successful!\n");
return true;
}
static bool helper_loadlpuser()
{
struct passwd *pw = getpwnam("lp");
if (pw == NULL) return false;
ruid = pw->pw_uid;
rgid = pw->pw_gid;
return true;
}
static void helper_dropprivs()
{
if (ruid == 0) return;
initgroups(ruser, rgid);
setgid(rgid);
setuid(ruid);
chdir("/");
if (setuid(0) != -1) {
fputs("ERROR: setuid-fu!?\n", stderr);
exit(CUPS_BACKEND_FAILED);
}
}
static void helper_copyenv()
{
char *ptr = gui_env;
while (strlen(ptr) > 0) {
char *equal = strchr(ptr, '=');
if (equal != NULL) {
char *value = equal + 1;
*equal = '\0';
setenv(ptr, value, 1);
*equal = '=';
}
ptr += strlen(ptr) + 1;
}
}