/* Copyright (C) 2000, 2001 Marcelo E. Magallon Released under the GNU LGPL version 2 or later. Gives *remote* users an xauth token on the *local* display. It uses the MIT-MAGIC-COOKIE-1 authorization schema. The list of users allowed to connect is read from /etc/security/remote_local_dpy.conf. Each line contains one user name. Everything after a dash is ignored. It places a lock file on /var/run/remote_local_dpy.lock (non configurable) The module accepts the following options on the command line: * silent the usual PAM meaning * users= (/etc/security/remote_local_dpy.conf) the file with the usernames of allowed to connect * dpy=:display.screen (:0) The display and screen numbers of the server to contact. It must include the initial semicolon. * retries=n (3) The number of times the module should try to lock the Xauth file. * timeout=n (1) The time before giving up while trying to lock the Xauth file. * dead=n (90) The time after which an Xauth lock is considered to be dead. Compile with something like: $ cc -o pam_remote_local_dpy.so pam_remote_local_dpy.c -shared -fPIC \ -lpam -L/usr/X11R6/lib -lX11 -lXext -lXau You need X11 for the Display functions, Xext for the X Security extension queries and Xau for the Xauth functions. TODO: * Merge xauth tokens. BUGS: * Removal of lock file is bogus: it should, at least, check that the username on the lockfile is the same as the current username. */ #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #define PAM_SM_SESSION #include #include char UserList[BUFSIZ] = "/etc/security/remote_local_dpy.conf"; char DisplayName[BUFSIZ] = ":0"; int retries = 3; int timeout = 1; long dead = 90L; #define MOD_LOCK_FILE "/var/run/remote_local_dpy.lock" #define AUTH_PROTOCOL "MIT-MAGIC-COOKIE-1" #define MOD_QUIET 0x01 #define MOD_USER_UNKNOWN 0x01 #define MOD_USER_OK 0x02 static void _log_err(int err, const char *format, ...) { va_list args; va_start(args, format); openlog("PAM-remote_local_dpy", LOG_CONS|LOG_PID, LOG_AUTH); vsyslog(err, format, args); va_end(args); closelog(); } static int _pam_parse(int flags, int argc, const char **argv) { int ctrl = 0; /* does the appliction require quiet? */ if ((flags & PAM_SILENT) == PAM_SILENT) ctrl |= MOD_QUIET; /* step through arguments */ for (; argc-- > 0; ++argv) { if (!strcmp(*argv, "silent")) ctrl |= MOD_QUIET; else if (!strncmp(*argv, "users=",6)) strncpy(UserList, *argv+6, BUFSIZ); else if (!strncmp(*argv, "dpy=", 4)) strncpy(DisplayName, *argv+4, BUFSIZ); else if (!strncmp(*argv, "retries=", 8)) retries = atoi(*argv+8); else if (!strncmp(*argv, "timeout=", 8)) timeout = atoi(*argv+8); else if (!strncmp(*argv, "dead=", 5)) dead = atoi(*argv+5); else _log_err(LOG_ERR, "unknown option; %s", *argv); } D(("ctrl = %o", ctrl)); return ctrl; } static int _check_user(const char *user) { FILE *list; char buf[BUFSIZ]; list = fopen(UserList, "r"); if (list == NULL) { _log_err(LOG_ERR, "can't open user list: %s", UserList); return MOD_USER_UNKNOWN; } while(!feof(list)) { char *c; fgets(buf, BUFSIZ, list); if (*buf == '#') continue; for(c = buf; c; ++c) { if (!isalpha((int)*c)) { *c = '\0'; break; } } if (!strncmp(user, buf, BUFSIZ)) return MOD_USER_OK; } return MOD_USER_UNKNOWN; } static int _cmp_auth(const Xauth *a, const Xauth *b) { return (a->family == b->family && a->address_length == b->address_length && a->number_length == b->number_length && a->name_length == b->name_length && memcmp(a->address, b->address, a->address_length) == 0 && memcmp(a->number, b->number, a->number_length) == 0 && memcmp(a->name, b->name, a->name_length)) ? 1 : 0; } /* Finds a given authorization entry on the specified file. Returns 0 if not found. It does NOT rewind the file. */ static int _find_auth(FILE *file, Xauth *auth) { int found = 0; while (!feof(file) && !found) { Xauth *auth_item = XauReadAuth(file); if (!auth_item) break; found = _cmp_auth(auth, auth_item); XauDisposeAuth(auth_item); } return found; } /* A dumbed down dpy parsing function. It just looks for a dpy number and a screen number. Skips over hostname and family. Doesn't support DECnet. */ static int _parse_dpyname(char *displayname, int *dpynum, int *scrnum) { char *d; d = strrchr(displayname, ':'); if (!d) return 0; d++; *dpynum = atoi(d); d = strchr(d, '.'); *scrnum = d ? atoi(d) : 0; return 1; } /* Cheap function to convert an integer to a string. The lenght of the string is placed in len. The returned string should be free'd. */ static char * _itos(const int i, unsigned short *len) { char buf[10]; *len = snprintf(buf, 10, "%d", i); if (*len > 10) *len = 10; // XXX: This is wrong, it should bail out return strdup(buf); } PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { int retval, ctrl; int status = PAM_SESSION_ERR; const char *user = NULL; struct passwd *pw = NULL; const char *remote_host = NULL; char localhost[BUFSIZ]; int lockfd = -1; /* The functions return int but take uid_t... */ int pfsuid = -1, pfsgid = -1; /* For the cookie */ Display *dpy = NULL; int dpynum, scrnum; int major_version, minor_version; Xauth *auth_in = NULL, *auth_out = NULL, *auth = NULL; XSecurityAuthorizationAttributes auth_attr; XSecurityAuthorization auth_id; char xauthname[BUFSIZ]; int xauthfd = -1; int xauthlocked = 0; FILE *xauthfile = NULL; /* DISPLAY variable */ char display_envvar[BUFSIZ]; ctrl = _pam_parse(flags, argc, argv); retval = pam_get_item(pamh, PAM_USER, (const void **) &user); if (retval != PAM_SUCCESS || user == NULL || *user == '\0') { _log_err(LOG_NOTICE, "user unknown"); return PAM_SESSION_ERR; } if (_check_user(user) != MOD_USER_OK) { _log_err(LOG_NOTICE, "user %s not in authorized list", user); return PAM_SESSION_ERR; } pam_get_item(pamh, PAM_RHOST, (const void **) &remote_host); if (retval != PAM_SUCCESS || remote_host == NULL || *remote_host == '\0') { _log_err(LOG_NOTICE, "remote host unknown"); return PAM_SESSION_ERR; } gethostname(localhost, BUFSIZ); if (!strncmp(remote_host, "localhost", 9) || !strncmp(remote_host, localhost, BUFSIZ)) { _log_err(LOG_NOTICE, "local users don't get cookies"); return PAM_SESSION_ERR; } if (!_parse_dpyname(DisplayName, &dpynum, &scrnum)) { _log_err(LOG_NOTICE, "dpy must specify a display number"); return PAM_SESSION_ERR; } /* We have everything, let's put a lock in place! */ lockfd = open(MOD_LOCK_FILE, O_WRONLY | O_CREAT | O_EXCL, 0600); if (lockfd == -1) { _log_err(LOG_NOTICE, "lock file %s present", MOD_LOCK_FILE); return PAM_SESSION_ERR; } write(lockfd, user, strlen(user)); close(lockfd); /* From this point on, use "goto mod_error" to signal an error */ dpy = XOpenDisplay(DisplayName); if (dpy == NULL) { _log_err(LOG_NOTICE, "can't open display %s", DisplayName); goto mod_error; } retval = XSecurityQueryExtension(dpy, &major_version, &minor_version); if (retval == 0) { _log_err(LOG_NOTICE, "couldn't query security extension on server"); goto mod_error; } auth_in = XSecurityAllocXauth(); if (auth_in == NULL) { _log_err(LOG_NOTICE, "failed to allocate an auth structure"); goto mod_error; } auth_in->name = strdup(AUTH_PROTOCOL); auth_in->name_length = strlen(auth_in->name); auth_in->data = NULL; auth_in->data_length = 0; auth_out = XSecurityGenerateAuthorization(dpy, auth_in, 0L, &auth_attr, &auth_id); XSync(dpy, False); if (auth_out == NULL) { _log_err(LOG_NOTICE, "failed to generate an authorization token"); goto mod_error; } _log_err(LOG_NOTICE, "authorization id is %ld\n", auth_id); auth = (Xauth *) malloc (sizeof (Xauth)); if (!auth) { _log_err(LOG_ERR, "Failed to allocate memory for auth token"); return PAM_SESSION_ERR; } auth->address = auth->number = auth->name = auth->data = NULL; auth->family = FamilyLocal; auth->address = strdup(localhost); auth->address_length = strlen(localhost); auth->number = _itos(dpynum, &auth->number_length); auth->name = strdup(AUTH_PROTOCOL); auth->name_length = strlen(AUTH_PROTOCOL); auth->data = malloc(auth_out->data_length * sizeof(char)); memcpy(auth->data, auth_out->data, auth_out->data_length); auth->data_length = auth_out->data_length; pw = getpwnam(user); if (pw == NULL) { _log_err(LOG_NOTICE, "failed to find user information for %s", user); goto mod_error; } /* Can't use XauFileName because we don't have the environment for the user yet */ xauthname[0] = 0; strncat(xauthname, pw->pw_dir, BUFSIZ); strncat(xauthname, "/.Xauthority", BUFSIZ); pfsuid = setfsuid(pw->pw_uid); pfsgid = setfsgid(pw->pw_gid); retval = XauLockAuth(xauthname, retries, timeout, dead); if (retval != LOCK_SUCCESS) { _log_err(LOG_NOTICE, "failed put a lock for %s in place", xauthname); goto mod_error; } xauthlocked = 1; xauthfd = open(xauthname, O_RDWR | O_EXCL, 0600); if (xauthfd == -1) { _log_err(LOG_NOTICE, "failed to open %s", xauthname); goto mod_error; } xauthfile = fdopen(xauthfd, "w+"); if (!_find_auth(xauthfile, auth)) { fseek(xauthfile, 0L, SEEK_END); XauWriteAuth(xauthfile, auth); } /* Wee! We have everything... now, be nice. */ display_envvar[0] = 0; strncat(display_envvar, "DISPLAY=", BUFSIZ); strncat(display_envvar, DisplayName, BUFSIZ); if (pam_putenv(pamh, display_envvar) != PAM_SUCCESS) { _log_err(LOG_CRIT, "Couldn't set DISPLAY enviroment variable"); } status = PAM_SUCCESS; mod_error: if (xauthlocked) { XauUnlockAuth(xauthname); } if (xauthfile != NULL) { fclose(xauthfile); } else if (xauthfd != -1) { close(xauthfd); } if (pfsuid != -1) { setfsuid(pfsuid); } if (pfsgid != -1) { setfsgid(pfsgid); } #if 0 /* Humm.. why shouldn't I free this pointer? */ if (pw) { free(pw); } #endif if (auth_in) { free(auth_in->name); XSecurityFreeXauth(auth_in); } if (auth_out) { XSecurityFreeXauth(auth_out); } if (auth) { if (auth->address) free(auth->address); if (auth->number) free(auth->number); if (auth->name) free(auth->name); if (auth->data) free(auth->data); free(auth); } if (dpy) { XCloseDisplay(dpy); } /* If we are here we *have* a lock file and it's closed */ if (status != PAM_SUCCESS) { _log_err(LOG_NOTICE, "removing lock file %s", MOD_LOCK_FILE); unlink(MOD_LOCK_FILE); } return status; } PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { /* FIXME: How to decide if the lock file should be removed or not */ unlink(MOD_LOCK_FILE); return PAM_SUCCESS; }