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