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