Dovecot: robustness; better msg on missing mech.
[exim.git] / src / src / auths / dovecot.c
1 /*
2  * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published
6  * by the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.
8  */
9
10 /* A number of modifications have been made to the original code. Originally I
11 commented them specially, but now they are getting quite extensive, so I have
12 ceased doing that. The biggest change is to use unbuffered I/O on the socket
13 because using C buffered I/O gives problems on some operating systems. PH */
14
15 /* Protocol specifications:
16  * Dovecot 1, protocol version 1.1
17  *   http://wiki.dovecot.org/Authentication%20Protocol
18  *
19  * Dovecot 2, protocol version 1.1
20  *   http://wiki2.dovecot.org/Design/AuthProtocol
21  */
22
23 #include "../exim.h"
24 #include "dovecot.h"
25
26 #define VERSION_MAJOR  1
27 #define VERSION_MINOR  0
28
29 /* http://wiki.dovecot.org/Authentication%20Protocol
30 "The maximum line length isn't defined,
31  but it's currently expected to fit into 8192 bytes"
32 */
33 #define DOVECOT_AUTH_MAXLINELEN 8192
34
35 /* This was hard-coded as 8.
36 AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
37 Dovecot 1; Dovecot 2 (same protocol version) defines 9.
38
39 Master->Server sends {"USER", id, userid} + params, 6 defined.
40 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
41
42 We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
43 for the command and id, where unspecified might include _at least_ user=...
44
45 So: allow for more fields than we ever expect to see, while aware that count
46 can go up without changing protocol version.
47 The cost is the length of an array of pointers on the stack.
48 */
49 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
50
51 /* Options specific to the authentication mechanism. */
52 optionlist auth_dovecot_options[] = {
53        {
54        "server_socket",
55        opt_stringptr,
56        (void *)(offsetof(auth_dovecot_options_block, server_socket))
57        },
58 };
59
60 /* Size of the options list. An extern variable has to be used so that its
61 address can appear in the tables drtables.c. */
62
63 int auth_dovecot_options_count =
64        sizeof(auth_dovecot_options) / sizeof(optionlist);
65
66 /* Default private options block for the authentication method. */
67
68 auth_dovecot_options_block auth_dovecot_option_defaults = {
69        NULL,                           /* server_socket */
70 };
71
72
73 /* Static variables for reading from the socket */
74
75 static uschar sbuffer[256];
76 static int socket_buffer_left;
77
78
79
80 /*************************************************
81  *          Initialization entry point           *
82  *************************************************/
83
84 /* Called for each instance, after its options have been read, to
85 enable consistency checks to be done, or anything else that needs
86 to be set up. */
87
88 void auth_dovecot_init(auth_instance *ablock)
89 {
90        auth_dovecot_options_block *ob =
91                (auth_dovecot_options_block *)(ablock->options_block);
92
93        if (ablock->public_name == NULL)
94                ablock->public_name = ablock->name;
95        if (ob->server_socket != NULL)
96                ablock->server = TRUE;
97        ablock->client = FALSE;
98 }
99
100 /*************************************************
101  *    "strcut" to split apart server lines       *
102  *************************************************/
103
104 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
105 of a command-name, TAB, and then any parameters, each separated by a TAB.
106 A parameter can be param=value or a bool, just param.
107
108 This function modifies the original str in-place, inserting NUL characters.
109 It initialises ptrs entries, setting all to NULL and only setting
110 non-NULL N entries, where N is the return value, the number of fields seen
111 (one more than the number of tabs).
112
113 Note that the return value will always be at least 1, is the count of
114 actual fields (so last valid offset into ptrs is one less).
115 */
116
117 static int
118 strcut(uschar *str, uschar **ptrs, int nptrs)
119 {
120        uschar *last_sub_start = str;
121        uschar *lastvalid = str + Ustrlen(str);
122        int n;
123
124        for (n = 0; n < nptrs; n++)
125                ptrs[n] = NULL;
126        n = 1;
127
128        while (*str) {
129                if (*str == '\t') {
130                        if (n <= nptrs) {
131                                *ptrs++ = last_sub_start;
132                                last_sub_start = str + 1;
133                                *str = '\0';
134                        }
135                        n++;
136                }
137                str++;
138        }
139
140        if (last_sub_start < lastvalid) {
141               if (n <= nptrs) {
142                        *ptrs = last_sub_start;
143                } else {
144                        HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
145                        n = nptrs;
146               }
147        } else {
148               n--;
149               HDEBUG(D_auth) debug_printf("dovecot: warning: ignoring trailing tab\n");
150        }
151
152        return n <= nptrs ? n : nptrs;
153 }
154
155 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
156 static void
157 debug_strcut(uschar **ptrs, int nlen, int alen)
158 {
159         int i;
160         debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
161                         socket_buffer_left, nlen);
162         for (i = 0; i < nlen; i++) {
163                 debug_printf(" {%s}", ptrs[i]);
164         }
165         if (nlen < alen)
166                 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
167         else
168                 debug_printf(" (max for capacity)\n");
169 }
170
171 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
172        if (strcmpic(US(str), args[0]) != 0) \
173                goto out; \
174        if (nargs - 1 < (arg_min)) \
175                goto out; \
176        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
177                goto out; \
178 } while (0)
179
180 #define OUT(msg) do { \
181        auth_defer_msg = (US msg); \
182        goto out; \
183 } while(0)
184
185
186
187 /*************************************************
188 *      "fgets" to read directly from socket      *
189 *************************************************/
190
191 /* Added by PH after a suggestion by Steve Usher because the previous use of
192 C-style buffered I/O gave trouble. */
193
194 static uschar *
195 dc_gets(uschar *s, int n, int fd)
196 {
197 int p = 0;
198 int count = 0;
199
200 for (;;)
201   {
202   if (socket_buffer_left == 0)
203     {
204     socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
205     if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
206     p = 0;
207     }
208
209   while (p < socket_buffer_left)
210     {
211     if (count >= n - 1) break;
212     s[count++] = sbuffer[p];
213     if (sbuffer[p++] == '\n') break;
214     }
215
216   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
217   socket_buffer_left -= p;
218
219   if (s[count-1] == '\n' || count >= n - 1) break;
220   }
221
222 s[count] = '\0';
223 return s;
224 }
225
226
227
228
229 /*************************************************
230 *              Server entry point                *
231 *************************************************/
232
233 int auth_dovecot_server(auth_instance *ablock, uschar *data)
234 {
235        auth_dovecot_options_block *ob =
236                (auth_dovecot_options_block *)(ablock->options_block);
237        struct sockaddr_un sa;
238        uschar buffer[DOVECOT_AUTH_MAXLINELEN];
239        uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
240        uschar *auth_command;
241        uschar *auth_extra_data = US"";
242        uschar *p;
243        int nargs, tmp;
244        int crequid = 1, cont = 1, fd, ret = DEFER;
245        BOOL found = FALSE;
246
247        HDEBUG(D_auth) debug_printf("dovecot authentication\n");
248
249        memset(&sa, 0, sizeof(sa));
250        sa.sun_family = AF_UNIX;
251
252        /* This was the original code here: it is nonsense because strncpy()
253        does not return an integer. I have converted this to use the function
254        that formats and checks length. PH */
255
256        /*
257        if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
258        */
259
260        if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
261                           ob->server_socket)) {
262                auth_defer_msg = US"authentication socket path too long";
263                return DEFER;
264        }
265
266        auth_defer_msg = US"authentication socket connection error";
267
268        fd = socket(PF_UNIX, SOCK_STREAM, 0);
269        if (fd < 0)
270                return DEFER;
271
272        if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
273                goto out;
274
275        auth_defer_msg = US"authentication socket protocol error";
276
277        socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
278        while (cont) {
279                if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
280                        OUT("authentication socket read error or premature eof");
281                p = buffer + Ustrlen(buffer) - 1;
282                if (*p != '\n') {
283                        OUT("authentication socket protocol line too long");
284                }
285                *p = '\0';
286                HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
287                nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
288                /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
289
290                /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
291                   Exim will need. Original code also failed if Dovecot server sent unknown
292                   command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
293                /* pdp: note that CUID is a per-connection identifier sent by the server,
294                   which increments at server discretion.
295                   By contrast, the "id" field of the protocol is a connection-specific request
296                   identifier, which needs to be unique per request from the client and is not
297                   connected to the CUID value, so we ignore CUID from server.  It's purely for
298                   diagnostics. */
299                if (Ustrcmp(args[0], US"VERSION") == 0) {
300                        CHECK_COMMAND("VERSION", 2, 2);
301                        if (Uatoi(args[1]) != VERSION_MAJOR)
302                                OUT("authentication socket protocol version mismatch");
303                } else if (Ustrcmp(args[0], US"MECH") == 0) {
304                        CHECK_COMMAND("MECH", 1, INT_MAX);
305                        if (strcmpic(US args[1], ablock->public_name) == 0)
306                                found = TRUE;
307                } else if (Ustrcmp(args[0], US"DONE") == 0) {
308                        CHECK_COMMAND("DONE", 0, 0);
309                        cont = 0;
310                }
311        }
312
313        if (!found) {
314                auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
315                goto out;
316        }
317
318        /* Added by PH: data must not contain tab (as it is
319        b64 it shouldn't, but check for safety). */
320
321        if (Ustrchr(data, '\t') != NULL) {
322                ret = FAIL;
323                goto out;
324        }
325
326        /* Added by PH: extra fields when TLS is in use or if the TCP/IP
327        connection is local. */
328
329        if (tls_in.cipher != NULL)
330                auth_extra_data = string_sprintf("secured\t%s%s",
331                    tls_in.certificate_verified? "valid-client-cert" : "",
332                    tls_in.certificate_verified? "\t" : "");
333        else if (interface_address != NULL &&
334                 Ustrcmp(sender_host_address, interface_address) == 0)
335                auth_extra_data = US"secured\t";
336
337
338 /****************************************************************************
339    The code below was the original code here. It didn't work. A reading of the
340    file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
341    this was not right. Maybe something changed. I changed it to move the
342    service indication into the AUTH command, and it seems to be better. PH
343
344        fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
345                "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
346                VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
347                ablock->public_name, sender_host_address, interface_address,
348                data ? (char *) data : "");
349
350    Subsequently, the command was modified to add "secured" and "valid-client-
351    cert" when relevant.
352 ****************************************************************************/
353
354        auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
355                "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
356                VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
357                ablock->public_name, auth_extra_data, sender_host_address,
358                interface_address, data ? (char *) data : "");
359
360        if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
361               HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
362                 strerror(errno));
363
364        HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
365
366        while (1) {
367                uschar *temp;
368                uschar *auth_id_pre = NULL;
369                int i;
370
371                if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
372                        auth_defer_msg = US"authentication socket read error or premature eof";
373                        goto out;
374                }
375
376                buffer[Ustrlen(buffer) - 1] = 0;
377                HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
378                nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
379
380                if (Uatoi(args[1]) != crequid)
381                        OUT("authentication socket connection id mismatch");
382
383                switch (toupper(*args[0])) {
384                case 'C':
385                        CHECK_COMMAND("CONT", 1, 2);
386
387                        tmp = auth_get_no64_data(&data, US args[2]);
388                        if (tmp != OK) {
389                                ret = tmp;
390                                goto out;
391                        }
392
393                        /* Added by PH: data must not contain tab (as it is
394                        b64 it shouldn't, but check for safety). */
395
396                        if (Ustrchr(data, '\t') != NULL) {
397                                ret = FAIL;
398                                goto out;
399                        }
400
401                        temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
402                        if (write(fd, temp, Ustrlen(temp)) < 0)
403                                OUT("authentication socket write error");
404                        break;
405
406                case 'F':
407                        CHECK_COMMAND("FAIL", 1, -1);
408
409                        for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
410                        {
411                                if ( Ustrncmp(args[i], US"user=", 5) == 0 )
412                                {
413                                        auth_id_pre = args[i]+5;
414                                        expand_nstring[1] = auth_vars[0] =
415                                                string_copy(auth_id_pre); /* PH */
416                                        expand_nlength[1] = Ustrlen(auth_id_pre);
417                                        expand_nmax = 1;
418                                }
419                        }
420
421                        ret = FAIL;
422                        goto out;
423
424                case 'O':
425                        CHECK_COMMAND("OK", 2, -1);
426
427                        /*
428                         * Search for the "user=$USER" string in the args array
429                         * and return the proper value.
430                         */
431                        for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
432                        {
433                                if ( Ustrncmp(args[i], US"user=", 5) == 0 )
434                                {
435                                        auth_id_pre = args[i]+5;
436                                        expand_nstring[1] = auth_vars[0] =
437                                                string_copy(auth_id_pre); /* PH */
438                                        expand_nlength[1] = Ustrlen(auth_id_pre);
439                                        expand_nmax = 1;
440                                }
441                        }
442
443                        if (auth_id_pre == NULL)
444                                OUT("authentication socket protocol error, username missing");
445
446                        ret = OK;
447                        /* fallthrough */
448
449                default:
450                        goto out;
451                }
452        }
453
454 out:
455        /* close the socket used by dovecot */
456        if (fd >= 0)
457               close(fd);
458
459        /* Expand server_condition as an authorization check */
460        return (ret == OK)? auth_check_serv_cond(ablock) : ret;
461 }