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