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