]> asedeno.scripts.mit.edu Git - PuTTY.git/blob - unix/gtkask.c
GTK3: give I/O events lower priority than window redraws.
[PuTTY.git] / unix / gtkask.c
1 /*
2  * GTK implementation of a GUI password/passphrase prompt.
3  */
4
5 #include <assert.h>
6 #include <time.h>
7 #include <stdlib.h>
8
9 #include <unistd.h>
10
11 #include <gtk/gtk.h>
12 #include <gdk/gdk.h>
13 #if !GTK_CHECK_VERSION(3,0,0)
14 #include <gdk/gdkkeysyms.h>
15 #endif
16
17 #include "gtkfont.h"
18 #include "gtkcompat.h"
19 #include "gtkmisc.h"
20
21 #include "misc.h"
22
23 #define N_DRAWING_AREAS 3
24
25 struct drawing_area_ctx {
26     GtkWidget *area;
27 #ifndef DRAW_DEFAULT_CAIRO
28     GdkColor *cols;
29 #endif
30     int width, height, current;
31 };
32
33 struct askpass_ctx {
34     GtkWidget *dialog, *promptlabel;
35     struct drawing_area_ctx drawingareas[N_DRAWING_AREAS];
36     int active_area;
37 #if GTK_CHECK_VERSION(2,0,0)
38     GtkIMContext *imc;
39 #endif
40 #ifndef DRAW_DEFAULT_CAIRO
41     GdkColormap *colmap;
42     GdkColor cols[2];
43 #endif
44     char *passphrase;
45     int passlen, passsize;
46 #if GTK_CHECK_VERSION(3,0,0)
47     GdkDevice *keyboard;               /* for gdk_device_grab */
48 #endif
49 };
50
51 static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
52 {
53     int new_active;
54     new_active = rand() % (N_DRAWING_AREAS - 1);
55     if (new_active >= ctx->active_area)
56         new_active++;
57     ctx->drawingareas[ctx->active_area].current = 0;
58     gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area);
59     ctx->drawingareas[new_active].current = 1;
60     gtk_widget_queue_draw(ctx->drawingareas[new_active].area);
61     ctx->active_area = new_active;
62 }
63
64 static int last_char_len(struct askpass_ctx *ctx)
65 {
66     /*
67      * GTK always encodes in UTF-8, so we can do this in a fixed way.
68      */
69     int i;
70     assert(ctx->passlen > 0);
71     i = ctx->passlen - 1;
72     while ((unsigned)((unsigned char)ctx->passphrase[i] - 0x80) < 0x40) {
73         if (i == 0)
74             break;
75         i--;
76     }
77     return ctx->passlen - i;
78 }
79
80 static void add_text_to_passphrase(struct askpass_ctx *ctx, gchar *str)
81 {
82     int len = strlen(str);
83     if (ctx->passlen + len >= ctx->passsize) {
84         /* Take some care with buffer expansion, because there are
85          * pieces of passphrase in the old buffer so we should ensure
86          * realloc doesn't leave a copy lying around in the address
87          * space. */
88         int oldsize = ctx->passsize;
89         char *newbuf;
90
91         ctx->passsize = (ctx->passlen + len) * 5 / 4 + 1024;
92         newbuf = snewn(ctx->passsize, char);
93         memcpy(newbuf, ctx->passphrase, oldsize);
94         smemclr(ctx->passphrase, oldsize);
95         sfree(ctx->passphrase);
96         ctx->passphrase = newbuf;
97     }
98     strcpy(ctx->passphrase + ctx->passlen, str);
99     ctx->passlen += len;
100     visually_acknowledge_keypress(ctx);
101 }
102
103 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
104 {
105     struct askpass_ctx *ctx = (struct askpass_ctx *)data;
106
107     if (event->keyval == GDK_KEY_Return &&
108         event->type == GDK_KEY_PRESS) {
109         gtk_main_quit();
110     } else if (event->keyval == GDK_KEY_Escape &&
111                event->type == GDK_KEY_PRESS) {
112         smemclr(ctx->passphrase, ctx->passsize);
113         ctx->passphrase = NULL;
114         gtk_main_quit();
115     } else {
116 #if GTK_CHECK_VERSION(2,0,0)
117         if (gtk_im_context_filter_keypress(ctx->imc, event))
118             return TRUE;
119 #endif
120
121         if (event->type == GDK_KEY_PRESS) {
122             if (!strcmp(event->string, "\x15")) {
123                 /* Ctrl-U. Wipe out the whole line */
124                 ctx->passlen = 0;
125                 visually_acknowledge_keypress(ctx);
126             } else if (!strcmp(event->string, "\x17")) {
127                 /* Ctrl-W. Delete back to the last space->nonspace
128                  * boundary. We interpret 'space' in a really simple
129                  * way (mimicking terminal drivers), and don't attempt
130                  * to second-guess exciting Unicode space
131                  * characters. */
132                 while (ctx->passlen > 0) {
133                     char deleted, prior;
134                     ctx->passlen -= last_char_len(ctx);
135                     deleted = ctx->passphrase[ctx->passlen];
136                     prior = (ctx->passlen == 0 ? ' ' :
137                              ctx->passphrase[ctx->passlen-1]);
138                     if (!g_ascii_isspace(deleted) && g_ascii_isspace(prior))
139                         break;
140                 }
141                 visually_acknowledge_keypress(ctx);
142             } else if (event->keyval == GDK_KEY_BackSpace) {
143                 /* Backspace. Delete one character. */
144                 if (ctx->passlen > 0)
145                     ctx->passlen -= last_char_len(ctx);
146                 visually_acknowledge_keypress(ctx);
147 #if !GTK_CHECK_VERSION(2,0,0)
148             } else if (event->string[0]) {
149                 add_text_to_passphrase(ctx, event->string);
150 #endif
151             }
152         }
153     }
154     return TRUE;
155 }
156
157 #if GTK_CHECK_VERSION(2,0,0)
158 static void input_method_commit_event(GtkIMContext *imc, gchar *str,
159                                       gpointer data)
160 {
161     struct askpass_ctx *ctx = (struct askpass_ctx *)data;
162     add_text_to_passphrase(ctx, str);
163 }
164 #endif
165
166 static gint configure_area(GtkWidget *widget, GdkEventConfigure *event,
167                            gpointer data)
168 {
169     struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
170     ctx->width = event->width;
171     ctx->height = event->height;
172     gtk_widget_queue_draw(widget);
173     return TRUE;
174 }
175
176 #ifdef DRAW_DEFAULT_CAIRO
177 static void askpass_redraw_cairo(cairo_t *cr, struct drawing_area_ctx *ctx)
178 {
179     cairo_set_source_rgb(cr, 1-ctx->current, 1-ctx->current, 1-ctx->current);
180     cairo_paint(cr);
181 }
182 #else
183 static void askpass_redraw_gdk(GdkWindow *win, struct drawing_area_ctx *ctx)
184 {
185     GdkGC *gc = gdk_gc_new(win);
186     gdk_gc_set_foreground(gc, &ctx->cols[ctx->current]);
187     gdk_draw_rectangle(win, gc, TRUE, 0, 0, ctx->width, ctx->height);
188     gdk_gc_unref(gc);
189 }
190 #endif
191
192 #if GTK_CHECK_VERSION(3,0,0)
193 static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
194 {
195     struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
196     askpass_redraw_cairo(cr, ctx);
197     return TRUE;
198 }
199 #else
200 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
201                         gpointer data)
202 {
203     struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
204
205 #ifdef DRAW_DEFAULT_CAIRO
206     cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(ctx->area));
207     askpass_redraw_cairo(cr, ctx);
208     cairo_destroy(cr);
209 #else
210     askpass_redraw_gdk(gtk_widget_get_window(ctx->area), ctx);
211 #endif
212
213     return TRUE;
214 }
215 #endif
216
217 static int try_grab_keyboard(struct askpass_ctx *ctx)
218 {
219     int ret;
220
221 #if GTK_CHECK_VERSION(3,0,0)
222     /*
223      * Grabbing the keyboard is quite complicated in GTK 3.
224      */
225     GdkDeviceManager *dm;
226     GdkDevice *pointer, *keyboard;
227
228     dm = gdk_display_get_device_manager
229         (gtk_widget_get_display(ctx->dialog));
230     if (!dm)
231         return FALSE;
232
233     pointer = gdk_device_manager_get_client_pointer(dm);
234     if (!pointer)
235         return FALSE;
236     keyboard = gdk_device_get_associated_device(pointer);
237     if (!keyboard)
238         return FALSE;
239     if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
240         return FALSE;
241
242     ctx->keyboard = keyboard;
243     ret = gdk_device_grab(ctx->keyboard,
244                           gtk_widget_get_window(ctx->dialog),
245                           GDK_OWNERSHIP_NONE,
246                           TRUE,
247                           GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
248                           NULL,
249                           GDK_CURRENT_TIME);
250 #else
251     /*
252      * It's much simpler in GTK 1 and 2!
253      */
254     ret = gdk_keyboard_grab(gtk_widget_get_window(ctx->dialog),
255                             FALSE, GDK_CURRENT_TIME);
256 #endif
257
258     return ret == GDK_GRAB_SUCCESS;
259 }
260
261 typedef int (try_grab_fn_t)(struct askpass_ctx *ctx);
262
263 static int repeatedly_try_grab(struct askpass_ctx *ctx, try_grab_fn_t fn)
264 {
265     /*
266      * Repeatedly try to grab some aspect of the X server. We have to
267      * do this rather than just trying once, because there is at least
268      * one important situation in which the grab may fail the first
269      * time: any user who is launching an add-key operation off some
270      * kind of window manager hotkey will almost by definition be
271      * running this script with a keyboard grab already active, namely
272      * the one-key grab that the WM (or whatever) uses to detect
273      * presses of the hotkey. So at the very least we have to give the
274      * user time to release that key.
275      */
276     const useconds_t ms_limit = 5*1000000;  /* try for 5 seconds */
277     const useconds_t ms_step = 1000000/8;   /* at 1/8 second intervals */
278     useconds_t ms;
279
280     for (ms = 0; ms < ms_limit; ms += ms_step) {
281         if (fn(ctx))
282             return TRUE;
283         usleep(ms_step);
284     }
285     return FALSE;
286 }
287
288 static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
289                                      const char *window_title,
290                                      const char *prompt_text)
291 {
292     int i;
293     GtkBox *action_area;
294
295     ctx->passlen = 0;
296     ctx->passsize = 2048;
297     ctx->passphrase = snewn(ctx->passsize, char);
298
299     /*
300      * Create widgets.
301      */
302     ctx->dialog = our_dialog_new();
303     gtk_window_set_title(GTK_WINDOW(ctx->dialog), window_title);
304     gtk_window_set_position(GTK_WINDOW(ctx->dialog), GTK_WIN_POS_CENTER);
305     ctx->promptlabel = gtk_label_new(prompt_text);
306     align_label_left(GTK_LABEL(ctx->promptlabel));
307     gtk_widget_show(ctx->promptlabel);
308     gtk_label_set_line_wrap(GTK_LABEL(ctx->promptlabel), TRUE);
309 #if GTK_CHECK_VERSION(3,0,0)
310     gtk_label_set_width_chars(GTK_LABEL(ctx->promptlabel), 48);
311 #endif
312     our_dialog_add_to_content_area(GTK_WINDOW(ctx->dialog),
313                                    ctx->promptlabel, TRUE, TRUE, 0);
314 #if GTK_CHECK_VERSION(2,0,0)
315     ctx->imc = gtk_im_multicontext_new();
316 #endif
317 #ifndef DRAW_DEFAULT_CAIRO
318     {
319         gboolean success[2];
320         ctx->colmap = gdk_colormap_get_system();
321         ctx->cols[0].red = ctx->cols[0].green = ctx->cols[0].blue = 0xFFFF;
322         ctx->cols[1].red = ctx->cols[1].green = ctx->cols[1].blue = 0;
323         gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2,
324                                   FALSE, TRUE, success);
325         if (!success[0] | !success[1])
326             return "unable to allocate colours";
327     }
328 #endif
329
330     action_area = our_dialog_make_action_hbox(GTK_WINDOW(ctx->dialog));
331
332     for (i = 0; i < N_DRAWING_AREAS; i++) {
333         ctx->drawingareas[i].area = gtk_drawing_area_new();
334 #ifndef DRAW_DEFAULT_CAIRO
335         ctx->drawingareas[i].cols = ctx->cols;
336 #endif
337         ctx->drawingareas[i].current = 0;
338         ctx->drawingareas[i].width = ctx->drawingareas[i].height = 0;
339         /* It would be nice to choose this size in some more
340          * context-sensitive way, like measuring the size of some
341          * piece of template text. */
342         gtk_widget_set_size_request(ctx->drawingareas[i].area, 32, 32);
343         gtk_box_pack_end(action_area, ctx->drawingareas[i].area,
344                          TRUE, TRUE, 5);
345         g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
346                          "configure_event",
347                          G_CALLBACK(configure_area),
348                          &ctx->drawingareas[i]);
349 #if GTK_CHECK_VERSION(3,0,0)
350         g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
351                          "draw",
352                          G_CALLBACK(draw_area),
353                          &ctx->drawingareas[i]);
354 #else
355         g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
356                          "expose_event",
357                          G_CALLBACK(expose_area),
358                          &ctx->drawingareas[i]);
359 #endif
360         gtk_widget_show(ctx->drawingareas[i].area);
361     }
362     ctx->active_area = rand() % N_DRAWING_AREAS;
363     ctx->drawingareas[ctx->active_area].current = 1;
364
365     /*
366      * Arrange to receive key events. We don't really need to worry
367      * from a UI perspective about which widget gets the events, as
368      * long as we know which it is so we can catch them. So we'll pick
369      * the prompt label at random, and we'll use gtk_grab_add to
370      * ensure key events go to it.
371      */
372     gtk_widget_set_sensitive(ctx->promptlabel, TRUE);
373
374 #if GTK_CHECK_VERSION(2,0,0)
375     gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), TRUE);
376 #endif
377
378     /*
379      * Actually show the window, and wait for it to be shown.
380      */
381     gtk_widget_show_now(ctx->dialog);
382
383     /*
384      * Now that the window is displayed, make it grab the input focus.
385      */
386     gtk_grab_add(ctx->promptlabel);
387     if (!repeatedly_try_grab(ctx, try_grab_keyboard))
388         return "unable to grab keyboard";
389
390     /*
391      * And now that we've got the keyboard grab, connect up our
392      * keyboard handlers.
393      */
394 #if GTK_CHECK_VERSION(2,0,0)
395     g_signal_connect(G_OBJECT(ctx->imc), "commit",
396                      G_CALLBACK(input_method_commit_event), ctx);
397 #endif
398     g_signal_connect(G_OBJECT(ctx->promptlabel), "key_press_event",
399                      G_CALLBACK(key_event), ctx);
400     g_signal_connect(G_OBJECT(ctx->promptlabel), "key_release_event",
401                      G_CALLBACK(key_event), ctx);
402 #if GTK_CHECK_VERSION(2,0,0)
403     gtk_im_context_set_client_window(ctx->imc,
404                                      gtk_widget_get_window(ctx->dialog));
405 #endif
406
407     return NULL;
408 }
409
410 static void gtk_askpass_cleanup(struct askpass_ctx *ctx)
411 {
412 #if GTK_CHECK_VERSION(3,0,0)
413     gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
414 #else
415     gdk_keyboard_ungrab(GDK_CURRENT_TIME);
416 #endif
417     gtk_grab_remove(ctx->promptlabel);
418
419     if (ctx->passphrase) {
420         assert(ctx->passlen < ctx->passsize);
421         ctx->passphrase[ctx->passlen] = '\0';
422     }
423
424     gtk_widget_destroy(ctx->dialog);
425 }
426
427 static int setup_gtk(const char *display)
428 {
429     static int gtk_initialised = FALSE;
430     int argc;
431     char *real_argv[3];
432     char **argv = real_argv;
433     int ret;
434
435     if (gtk_initialised)
436         return TRUE;
437
438     argc = 0;
439     argv[argc++] = dupstr("dummy");
440     argv[argc++] = dupprintf("--display=%s", display);
441     argv[argc] = NULL;
442     ret = gtk_init_check(&argc, &argv);
443     while (argc > 0)
444         sfree(argv[--argc]);
445
446     gtk_initialised = ret;
447     return ret;
448 }
449
450 char *gtk_askpass_main(const char *display, const char *wintitle,
451                        const char *prompt, int *success)
452 {
453     struct askpass_ctx actx, *ctx = &actx;
454     const char *err;
455
456     /* In case gtk_init hasn't been called yet by the program */
457     if (!setup_gtk(display)) {
458         *success = FALSE;
459         return dupstr("unable to initialise GTK");
460     }
461
462     if ((err = gtk_askpass_setup(ctx, wintitle, prompt)) != NULL) {
463         *success = FALSE;
464         return dupprintf("%s", err);
465     }
466     gtk_main();
467     gtk_askpass_cleanup(ctx);
468
469     if (ctx->passphrase) {
470         *success = TRUE;
471         return ctx->passphrase;
472     } else {
473         *success = FALSE;
474         return dupstr("passphrase input cancelled");
475     }
476 }
477
478 #ifdef TEST_ASKPASS
479 void modalfatalbox(const char *p, ...)
480 {
481     va_list ap;
482     fprintf(stderr, "FATAL ERROR: ");
483     va_start(ap, p);
484     vfprintf(stderr, p, ap);
485     va_end(ap);
486     fputc('\n', stderr);
487     exit(1);
488 }
489
490 int main(int argc, char **argv)
491 {
492     int success, exitcode;
493     char *ret;
494
495     gtk_init(&argc, &argv);
496
497     if (argc != 2) {
498         success = FALSE;
499         ret = dupprintf("usage: %s <prompt text>", argv[0]);
500     } else {
501         srand(time(NULL));
502         ret = gtk_askpass_main(NULL, "Enter passphrase", argv[1], &success);
503     }
504
505     if (!success) {
506         fputs(ret, stderr);
507         fputc('\n', stderr);
508         exitcode = 1;
509     } else {
510         fputs(ret, stdout);
511         fputc('\n', stdout);
512         exitcode = 0;
513     }
514
515     smemclr(ret, strlen(ret));
516     return exitcode;
517 }
518 #endif