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