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 }