slock

simple X display locker utility

git clone https://9o.is/git/slock.git

slock.c

(12195B)


      1 /* See LICENSE file for license details. */
      2 #define _XOPEN_SOURCE 500
      3 #if HAVE_SHADOW_H
      4 #include <shadow.h>
      5 #endif
      6 
      7 #include <ctype.h>
      8 #include <errno.h>
      9 #include <grp.h>
     10 #include <pwd.h>
     11 #include <stdarg.h>
     12 #include <stdlib.h>
     13 #include <stdio.h>
     14 #include <string.h>
     15 #include <time.h>
     16 #include <unistd.h>
     17 #include <spawn.h>
     18 #include <sys/types.h>
     19 #include <X11/extensions/Xrandr.h>
     20 #include <X11/keysym.h>
     21 #include <X11/Xlib.h>
     22 #include <X11/Xutil.h>
     23 #include <X11/Xft/Xft.h>
     24 
     25 #include "arg.h"
     26 #include "util.h"
     27 
     28 char *argv0;
     29 
     30 enum {
     31 	INIT,
     32 	INPUT,
     33 	FAILED,
     34 	NUMCOLS
     35 };
     36 
     37 struct lock {
     38 	int screen;
     39 	Window root, win;
     40 	Pixmap pmap;
     41 	XftDraw *xftdraw;
     42 	XftFont *xftfont;
     43 	XftColor colors[NUMCOLS];
     44 	unsigned long bgcolor;
     45 };
     46 
     47 struct xrandr {
     48 	int active;
     49 	int evbase;
     50 	int errbase;
     51 };
     52 
     53 #include "config.h"
     54 
     55 static void
     56 drawtext(Display *dpy, struct lock *lock, const char *text, int color)
     57 {
     58 	int w, h, x, y;
     59 	XGlyphInfo extents;
     60 
     61 	XClearWindow(dpy, lock->win);
     62 	if (!lock->xftdraw || !lock->xftfont)
     63 		return;
     64 
     65 	XftTextExtentsUtf8(dpy, lock->xftfont,
     66 			(const XftChar8 *)text, strlen(text), &extents);
     67 
     68 	w = extents.width;
     69 	h = lock->xftfont->ascent + lock->xftfont->descent;
     70 	x = (DisplayWidth(dpy, lock->screen) - w) / 2;
     71 	y = (DisplayHeight(dpy, lock->screen) - h) / 2 + lock->xftfont->ascent;
     72 
     73 	XftDrawStringUtf8(lock->xftdraw, &lock->colors[color], lock->xftfont, x, y,
     74 			(const XftChar8 *)text, strlen(text));
     75 }
     76 
     77 static void
     78 initxftfont(Display *dpy, struct lock *lock)
     79 {
     80 	lock->xftdraw = XftDrawCreate(dpy, lock->win,
     81 			DefaultVisual(dpy, lock->screen),
     82 			DefaultColormap(dpy, lock->screen));
     83 
     84 	lock->xftfont = XftFontOpenName(dpy, lock->screen, font);
     85 
     86 	if (!lock->xftfont) {
     87 		lock->xftfont = XftFontOpen(dpy, lock->screen,
     88 				XFT_FAMILY, XftTypeString, "fixed",
     89 				XFT_SIZE, XftTypeDouble, 16.0,
     90 				NULL);
     91 	}
     92 }
     93 
     94 static void
     95 locktimer(Display *dpy, struct lock **locks, int nscreens, int timeout)
     96 {
     97 	char timer_text[32];
     98 	int m, s, screen, remaining;
     99 	time_t lock_time;
    100 	XEvent ev;
    101 
    102 	lock_time = time(NULL);
    103 	remaining = timeout - (time(NULL) - lock_time);
    104 
    105 	for (screen = 0; screen < nscreens; screen++)
    106 		XRaiseWindow(dpy, locks[screen]->win);
    107 
    108 	while (XPending(dpy))
    109 		XNextEvent(dpy, &ev);
    110 
    111 	while (remaining > 0) {
    112 		for (screen = 0; screen < nscreens; screen++) {
    113 			m = remaining / 60;
    114 			s = remaining % 60;
    115 			snprintf(timer_text, sizeof(timer_text), "%s %d:%02d remaining", timer_message, m, s);
    116 			drawtext(dpy, locks[screen], timer_text, INIT);
    117 		}
    118 
    119 		while (XPending(dpy))
    120 			XNextEvent(dpy, &ev);
    121 
    122 		usleep(50000); // 50ms
    123 		remaining = timeout - (time(NULL) - lock_time);
    124 	}
    125 
    126 	for (screen = 0; screen < nscreens; screen++)
    127 		drawtext(dpy, locks[screen], message, INIT);
    128 }
    129 
    130 static void
    131 die(const char *errstr, ...)
    132 {
    133 	va_list ap;
    134 
    135 	va_start(ap, errstr);
    136 	vfprintf(stderr, errstr, ap);
    137 	va_end(ap);
    138 	exit(1);
    139 }
    140 
    141 #ifdef __linux__
    142 #include <fcntl.h>
    143 #include <linux/oom.h>
    144 
    145 static void
    146 dontkillme(void)
    147 {
    148 	FILE *f;
    149 	const char oomfile[] = "/proc/self/oom_score_adj";
    150 
    151 	if (!(f = fopen(oomfile, "w"))) {
    152 		if (errno == ENOENT)
    153 			return;
    154 		die("slock: fopen %s: %s\n", oomfile, strerror(errno));
    155 	}
    156 	fprintf(f, "%d", OOM_SCORE_ADJ_MIN);
    157 	if (fclose(f)) {
    158 		if (errno == EACCES)
    159 			die("slock: unable to disable OOM killer. "
    160 			    "Make sure to suid or sgid slock.\n");
    161 		else
    162 			die("slock: fclose %s: %s\n", oomfile, strerror(errno));
    163 	}
    164 }
    165 #endif
    166 
    167 static const char *
    168 gethash(void)
    169 {
    170 	const char *hash;
    171 	struct passwd *pw;
    172 
    173 	/* Check if the current user has a password entry */
    174 	errno = 0;
    175 	if (!(pw = getpwuid(getuid()))) {
    176 		if (errno)
    177 			die("slock: getpwuid: %s\n", strerror(errno));
    178 		else
    179 			die("slock: cannot retrieve password entry\n");
    180 	}
    181 	hash = pw->pw_passwd;
    182 
    183 #if HAVE_SHADOW_H
    184 	if (!strcmp(hash, "x")) {
    185 		struct spwd *sp;
    186 		if (!(sp = getspnam(pw->pw_name)))
    187 			die("slock: getspnam: cannot retrieve shadow entry. "
    188 			    "Make sure to suid or sgid slock.\n");
    189 		hash = sp->sp_pwdp;
    190 	}
    191 #else
    192 	if (!strcmp(hash, "*")) {
    193 #ifdef __OpenBSD__
    194 		if (!(pw = getpwuid_shadow(getuid())))
    195 			die("slock: getpwnam_shadow: cannot retrieve shadow entry. "
    196 			    "Make sure to suid or sgid slock.\n");
    197 		hash = pw->pw_passwd;
    198 #else
    199 		die("slock: getpwuid: cannot retrieve shadow entry. "
    200 		    "Make sure to suid or sgid slock.\n");
    201 #endif /* __OpenBSD__ */
    202 	}
    203 #endif /* HAVE_SHADOW_H */
    204 
    205 	return hash;
    206 }
    207 
    208 static void
    209 readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens,
    210        const char *hash)
    211 {
    212 	XRRScreenChangeNotifyEvent *rre;
    213 	char buf[32], passwd[256], *inputhash;
    214 	int num, screen, running, failure, oldc;
    215 	unsigned int len, color;
    216 	KeySym ksym;
    217 	XEvent ev;
    218 
    219 	len = 0;
    220 	running = 1;
    221 	failure = 0;
    222 	oldc = INIT;
    223 
    224 	while (running && !XNextEvent(dpy, &ev)) {
    225 		if (ev.type == KeyPress) {
    226 			explicit_bzero(&buf, sizeof(buf));
    227 			num = XLookupString(&ev.xkey, buf, sizeof(buf), &ksym, 0);
    228 			if (IsKeypadKey(ksym)) {
    229 				if (ksym == XK_KP_Enter)
    230 					ksym = XK_Return;
    231 				else if (ksym >= XK_KP_0 && ksym <= XK_KP_9)
    232 					ksym = (ksym - XK_KP_0) + XK_0;
    233 			}
    234 			if (IsFunctionKey(ksym) ||
    235 			    IsKeypadKey(ksym) ||
    236 			    IsMiscFunctionKey(ksym) ||
    237 			    IsPFKey(ksym) ||
    238 			    IsPrivateKeypadKey(ksym))
    239 				continue;
    240 			switch (ksym) {
    241 			case XK_Return:
    242 				passwd[len] = '\0';
    243 				errno = 0;
    244 				if (!(inputhash = crypt(passwd, hash)))
    245 					fprintf(stderr, "slock: crypt: %s\n", strerror(errno));
    246 				else
    247 					running = !!strcmp(inputhash, hash);
    248 				if (running) {
    249 					XBell(dpy, 100);
    250 					failure = 1;
    251 				}
    252 				explicit_bzero(&passwd, sizeof(passwd));
    253 				len = 0;
    254 				break;
    255 			case XK_Escape:
    256 				explicit_bzero(&passwd, sizeof(passwd));
    257 				len = 0;
    258 				break;
    259 			case XK_BackSpace:
    260 				if (len)
    261 					passwd[--len] = '\0';
    262 				break;
    263 			default:
    264 				if (num && !iscntrl((int)buf[0]) &&
    265 				    (len + num < sizeof(passwd))) {
    266 					memcpy(passwd + len, buf, num);
    267 					len += num;
    268 				} else if (buf[0] == '\025') { /* ctrl-u clears input */
    269 					explicit_bzero(&passwd, sizeof(passwd));
    270 					len = 0;
    271 				}
    272 				break;
    273 			}
    274 			color = len ? INPUT : ((failure || failonclear) ? FAILED : INIT);
    275 			if (running && oldc != color) {
    276 				for (screen = 0; screen < nscreens; screen++) {
    277 					drawtext(dpy, locks[screen], message, color);
    278 				}
    279 				oldc = color;
    280 			}
    281 		} else if (rr->active && ev.type == rr->evbase + RRScreenChangeNotify) {
    282 			rre = (XRRScreenChangeNotifyEvent*)&ev;
    283 			for (screen = 0; screen < nscreens; screen++) {
    284 				if (locks[screen]->win == rre->window) {
    285 					if (rre->rotation == RR_Rotate_90 ||
    286 					    rre->rotation == RR_Rotate_270)
    287 						XResizeWindow(dpy, locks[screen]->win,
    288 						              rre->height, rre->width);
    289 					else
    290 						XResizeWindow(dpy, locks[screen]->win,
    291 						              rre->width, rre->height);
    292 					drawtext(dpy, locks[screen], message, INIT);
    293 					break;
    294 				}
    295 			}
    296 		} else {
    297 			for (screen = 0; screen < nscreens; screen++)
    298 				XRaiseWindow(dpy, locks[screen]->win);
    299 		}
    300 	}
    301 }
    302 
    303 static struct lock *
    304 lockscreen(Display *dpy, struct xrandr *rr, int screen)
    305 {
    306 	char curs[] = {0, 0, 0, 0, 0, 0, 0, 0};
    307 	int i, ptgrab, kbgrab;
    308 	struct lock *lock;
    309 	XColor color, dummy;
    310 	XSetWindowAttributes wa;
    311 	Cursor invisible;
    312 
    313 	if (dpy == NULL || screen < 0 || !(lock = malloc(sizeof(struct lock))))
    314 		return NULL;
    315 
    316 	lock->screen = screen;
    317 	lock->root = RootWindow(dpy, lock->screen);
    318 
    319 	XAllocNamedColor(dpy, DefaultColormap(dpy, lock->screen),
    320 	                 bgcolor, &color, &dummy);
    321 	lock->bgcolor = color.pixel;
    322 
    323 	for (i = 0; i < NUMCOLS; i++) {
    324 		XftColorAllocName(dpy, DefaultVisual(dpy, lock->screen),
    325 				DefaultColormap(dpy, lock->screen), colorname[i], &lock->colors[i]);
    326 	}
    327 
    328 	/* init */
    329 	wa.override_redirect = 1;
    330 	wa.background_pixel = lock->bgcolor;
    331 	lock->win = XCreateWindow(dpy, lock->root, 0, 0,
    332 	                          DisplayWidth(dpy, lock->screen),
    333 	                          DisplayHeight(dpy, lock->screen),
    334 	                          0, DefaultDepth(dpy, lock->screen),
    335 	                          CopyFromParent,
    336 	                          DefaultVisual(dpy, lock->screen),
    337 	                          CWOverrideRedirect | CWBackPixel, &wa);
    338 	lock->pmap = XCreateBitmapFromData(dpy, lock->win, curs, 8, 8);
    339 	invisible = XCreatePixmapCursor(dpy, lock->pmap, lock->pmap,
    340 	                                &color, &color, 0, 0);
    341 	XDefineCursor(dpy, lock->win, invisible);
    342 
    343 	/* Try to grab mouse pointer *and* keyboard for 600ms, else fail the lock */
    344 	for (i = 0, ptgrab = kbgrab = -1; i < 6; i++) {
    345 		if (ptgrab != GrabSuccess) {
    346 			ptgrab = XGrabPointer(dpy, lock->root, False,
    347 			                      ButtonPressMask | ButtonReleaseMask |
    348 			                      PointerMotionMask, GrabModeAsync,
    349 			                      GrabModeAsync, None, invisible, CurrentTime);
    350 		}
    351 		if (kbgrab != GrabSuccess) {
    352 			kbgrab = XGrabKeyboard(dpy, lock->root, True,
    353 			                       GrabModeAsync, GrabModeAsync, CurrentTime);
    354 		}
    355 
    356 		/* input is grabbed: we can lock the screen */
    357 		if (ptgrab == GrabSuccess && kbgrab == GrabSuccess) {
    358 			XMapRaised(dpy, lock->win);
    359 			if (rr->active)
    360 				XRRSelectInput(dpy, lock->win, RRScreenChangeNotifyMask);
    361 
    362 			initxftfont(dpy, lock);
    363 			XSelectInput(dpy, lock->root, SubstructureNotifyMask);
    364 			return lock;
    365 		}
    366 
    367 		/* retry on AlreadyGrabbed but fail on other errors */
    368 		if ((ptgrab != AlreadyGrabbed && ptgrab != GrabSuccess) ||
    369 		    (kbgrab != AlreadyGrabbed && kbgrab != GrabSuccess))
    370 			break;
    371 
    372 		usleep(100000);
    373 	}
    374 
    375 	/* we couldn't grab all input: fail out */
    376 	if (ptgrab != GrabSuccess)
    377 		fprintf(stderr, "slock: unable to grab mouse pointer for screen %d\n",
    378 		        screen);
    379 	if (kbgrab != GrabSuccess)
    380 		fprintf(stderr, "slock: unable to grab keyboard for screen %d\n",
    381 		        screen);
    382 	return NULL;
    383 }
    384 
    385 static void
    386 usage(void)
    387 {
    388 	die("usage: slock [-v] [-t seconds] [cmd [arg ...]]\n");
    389 }
    390 
    391 int
    392 main(int argc, char **argv) {
    393 	struct xrandr rr;
    394 	struct lock **locks;
    395 	struct passwd *pwd;
    396 	struct group *grp;
    397 	uid_t duid;
    398 	gid_t dgid;
    399 	const char *hash;
    400 	Display *dpy;
    401 	int s, nlocks, nscreens;
    402 	int timeout = 0;
    403 
    404 	ARGBEGIN {
    405 	case 'v':
    406 		puts("slock-"VERSION);
    407 		return 0;
    408 	case 't':
    409 		timeout = atoi(EARGF(usage()));
    410 		break;
    411 	case 'm':
    412 		message = EARGF(usage());
    413 		break;
    414 	default:
    415 		usage();
    416 	} ARGEND
    417 
    418 	/* validate drop-user and -group */
    419 	errno = 0;
    420 	if (!(pwd = getpwnam(user)))
    421 		die("slock: getpwnam %s: %s\n", user,
    422 		    errno ? strerror(errno) : "user entry not found");
    423 	duid = pwd->pw_uid;
    424 	errno = 0;
    425 	if (!(grp = getgrnam(group)))
    426 		die("slock: getgrnam %s: %s\n", group,
    427 		    errno ? strerror(errno) : "group entry not found");
    428 	dgid = grp->gr_gid;
    429 
    430 #ifdef __linux__
    431 	dontkillme();
    432 #endif
    433 
    434 	hash = gethash();
    435 	errno = 0;
    436 	if (!crypt("", hash))
    437 		die("slock: crypt: %s\n", strerror(errno));
    438 
    439 	if (!(dpy = XOpenDisplay(NULL)))
    440 		die("slock: cannot open display\n");
    441 
    442 	/* drop privileges */
    443 	if (setgroups(0, NULL) < 0)
    444 		die("slock: setgroups: %s\n", strerror(errno));
    445 	if (setgid(dgid) < 0)
    446 		die("slock: setgid: %s\n", strerror(errno));
    447 	if (setuid(duid) < 0)
    448 		die("slock: setuid: %s\n", strerror(errno));
    449 
    450 	/* check for Xrandr support */
    451 	rr.active = XRRQueryExtension(dpy, &rr.evbase, &rr.errbase);
    452 
    453 	/* get number of screens in display "dpy" and blank them */
    454 	nscreens = ScreenCount(dpy);
    455 	if (!(locks = calloc(nscreens, sizeof(struct lock *))))
    456 		die("slock: out of memory\n");
    457 	for (nlocks = 0, s = 0; s < nscreens; s++) {
    458 		if ((locks[s] = lockscreen(dpy, &rr, s)) != NULL)
    459 			nlocks++;
    460 		else
    461 			break;
    462 	}
    463 	XSync(dpy, 0);
    464 
    465 	/* did we manage to lock everything? */
    466 	if (nlocks != nscreens)
    467 		return 1;
    468 
    469 	/* run post-lock command */
    470 	if (argc > 0) {
    471 		pid_t pid;
    472 		extern char **environ;
    473 		int err = posix_spawnp(&pid, argv[0], NULL, NULL, argv, environ);
    474 		if (err) {
    475 			die("slock: failed to execute post-lock command: %s: %s\n",
    476 			    argv[0], strerror(err));
    477 		}
    478 	}
    479 
    480 	if (timeout > 0)
    481 		locktimer(dpy, locks, nscreens, timeout);
    482 
    483 	/* everything is now blank. Wait for the correct password */
    484 	readpw(dpy, &rr, locks, nscreens, hash);
    485 
    486 	/* Cleanup Xft resources */
    487 	for (s = 0; s < nscreens; s++) {
    488 		if (locks[s]) {
    489 			if (locks[s]->xftfont)
    490 				XftFontClose(dpy, locks[s]->xftfont);
    491 			if (locks[s]->xftdraw)
    492 				XftDrawDestroy(locks[s]->xftdraw);
    493 		}
    494 	}
    495 
    496 	return 0;
    497 }