#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>
#include <errno.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();
static char *helper_urlencode(char *s, char *enc);
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];
int bytes, ret;
int total = 0;
for (;;) {
bytes = read(STDIN_FILENO, buffer, BUFLEN);
if (bytes == 0) break;
if (bytes < 0) {
fprintf(stderr, "ERROR: Could not read print job from STDIN.\n");
remove(tmpfile);
return CUPS_BACKEND_FAILED;
}
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;
}
total += bytes;
}
close(fh);
fprintf(stderr, "ERROR: Read %d bytes from stdin.\n", total);
//
} 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, "ERROR: 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)
{
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
// Child
close(pipefd[0]); // close reading end
dup2(pipefd[1], 2); // send stderr to pipe
close(pipefd[1]); // no longer needed after dup
// Newer backend can read credentials from environment
if (user != NULL) setenv("AUTH_USERNAME", user, 1);
if (password != NULL) setenv("AUTH_PASSWORD", password, 1);
// For ipp14 (at least) we need to build a new URI with ://username:password@....
if (user != NULL && password != NULL && strncmp(uri, "ipp14", 5) == 0) {
char *newstr = (char*)malloc(strlen(uri) + strlen(user) * 3 + strlen(password) * 3 + 20);
char *ptr = newstr;
ptr += sprintf(ptr, "ipp14://");
ptr = helper_urlencode(user, ptr);
*ptr++ = ':';
ptr = helper_urlencode(password, ptr);
*ptr++ = '@';
ptr += sprintf(ptr, "%s", uri + 8); // TODO: If uri already contains credentials, skip over them (check if there's a @ before the first /, starting at index 8)
uri = newstr;
setenv("DEVICE_URI", uri, 1);
// Since we've already forked and execv anyways there's no need to clean up anything...
}
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...
close(pipefd[1]); // close writing end
char buffer[BUFLEN];
bool needAuth = false;
int readlen = 0;
while ((readlen = read(pipefd[0], buffer, BUFLEN-1)) > 0) {
buffer[readlen] = '\0';
if (strstr(buffer, "Unable to get printer status (Unauthorized)!") != NULL) {
needAuth = true;
if (kill(pid, SIGTERM) < 0) {
fprintf(stderr, "ERROR: Sending kill1 to backend %d failed: %d\n", (int)pid, errno);
}
break;
}
}
close(pipefd[0]);
int status;
if (waitpid(pid, &status, WNOHANG) == 0) {
if (kill(pid, SIGKILL) == 0) {
// Try to reap zombie
sleep(1);
waitpid(pid, &status, WNOHANG);
}
}
if (needAuth) {
fprintf(stderr, "ERROR: Killed backend because of 'unauthorized' message (iprint crap?), trying with auth\n");
return CUPS_BACKEND_AUTH_REQUIRED;
}
if (!WIFEXITED(status)) {
fprintf(stderr, "ERROR: Running backend %s failed!\n", backend);
return CUPS_BACKEND_FAILED;
}
status = WEXITSTATUS(status);
if (status != CUPS_BACKEND_OK) fprintf(stderr, "ERROR: Backend returned %d\n", status);
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;
}
}
/**
* Make sure enc is 3 times as large as s
*/
static char *helper_urlencode(char *source, char *enc)
{
static char table[256] = {3};
int len;
if (table[0] == 3) {
int i;
for (i = 0; i < 256; i++) {
table[i] = (isalnum(i) || i == '~' || i == '-' || i == '.' || i == '_') ? i : 0;
}
}
unsigned char *s = (unsigned char*)source;
for (; *s != '\0'; s++) {
if (table[*s] != '\0') len = sprintf(enc, "%c", table[*s]);
else len = sprintf(enc, "%%%02X", *s);
enc += len;
}
*enc = '\0';
return enc;
}