Merge branch 'patch-1' of https://github.com/bes-internal/exim into master-bes-rateli...
[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        int n;
122
123        for (n = 0; n < nptrs; n++)
124                ptrs[n] = NULL;
125        n = 1;
126
127        while (*str) {
128                if (*str == '\t') {
129                        if (n <= nptrs) {
130                                *ptrs++ = last_sub_start;
131                                last_sub_start = str + 1;
132                                *str = '\0';
133                        }
134                        n++;
135                }
136                str++;
137        }
138
139        /* It's acceptable for the string to end with a tab character.  We see
140        this in AUTH PLAIN without an initial response from the client, which
141        causing us to send "334 " and get the data from the client. */
142        if (n <= nptrs) {
143                *ptrs = last_sub_start;
144        } else {
145                HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
146                n = nptrs;
147        }
148
149        return n <= nptrs ? n : nptrs;
150 }
151
152 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
153 static void
154 debug_strcut(uschar **ptrs, int nlen, int alen)
155 {
156         int i;
157         debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
158                         socket_buffer_left, nlen);
159         for (i = 0; i < nlen; i++) {
160                 debug_printf(" {%s}", ptrs[i]);
161         }
162         if (nlen < alen)
163                 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
164         else
165                 debug_printf(" (max for capacity)\n");
166 }
167
168 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
169        if (strcmpic(US(str), args[0]) != 0) \
170                goto out; \
171        if (nargs - 1 < (arg_min)) \
172                goto out; \
173        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
174                goto out; \
175 } while (0)
176
177 #define OUT(msg) do { \
178        auth_defer_msg = (US msg); \
179        goto out; \
180 } while(0)
181
182
183
184 /*************************************************
185 *      "fgets" to read directly from socket      *
186 *************************************************/
187
188 /* Added by PH after a suggestion by Steve Usher because the previous use of
189 C-style buffered I/O gave trouble. */
190
191 static uschar *
192 dc_gets(uschar *s, int n, int fd)
193 {
194 int p = 0;
195 int count = 0;
196
197 for (;;)
198   {
199   if (socket_buffer_left == 0)
200     {
201     socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
202     if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
203     p = 0;
204     }
205
206   while (p < socket_buffer_left)
207     {
208     if (count >= n - 1) break;
209     s[count++] = sbuffer[p];
210     if (sbuffer[p++] == '\n') break;
211     }
212
213   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
214   socket_buffer_left -= p;
215
216   if (s[count-1] == '\n' || count >= n - 1) break;
217   }
218
219 s[count] = '\0';
220 return s;
221 }
222
223
224
225
226 /*************************************************
227 *              Server entry point                *
228 *************************************************/
229
230 int auth_dovecot_server(auth_instance *ablock, uschar *data)
231 {
232        auth_dovecot_options_block *ob =
233                (auth_dovecot_options_block *)(ablock->options_block);
234        struct sockaddr_un sa;
235        uschar buffer[DOVECOT_AUTH_MAXLINELEN];
236        uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
237        uschar *auth_command;
238        uschar *auth_extra_data = US"";
239        uschar *p;
240        int nargs, tmp;
241        int crequid = 1, cont = 1, fd, ret = DEFER;
242        BOOL found = FALSE;
243
244        HDEBUG(D_auth) debug_printf("dovecot authentication\n");
245
246        memset(&sa, 0, sizeof(sa));
247        sa.sun_family = AF_UNIX;
248
249        /* This was the original code here: it is nonsense because strncpy()
250        does not return an integer. I have converted this to use the function
251        that formats and checks length. PH */
252
253        /*
254        if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
255        */
256
257        if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
258                           ob->server_socket)) {
259                auth_defer_msg = US"authentication socket path too long";
260                return DEFER;
261        }
262
263        auth_defer_msg = US"authentication socket connection error";
264
265        fd = socket(PF_UNIX, SOCK_STREAM, 0);
266        if (fd < 0)
267                return DEFER;
268
269        if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
270                goto out;
271
272        auth_defer_msg = US"authentication socket protocol error";
273
274        socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
275        while (cont) {
276                if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
277                        OUT("authentication socket read error or premature eof");
278                p = buffer + Ustrlen(buffer) - 1;
279                if (*p != '\n') {
280                        OUT("authentication socket protocol line too long");
281                }
282                *p = '\0';
283                HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
284                nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
285                /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
286
287                /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
288                   Exim will need. Original code also failed if Dovecot server sent unknown
289                   command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
290                /* pdp: note that CUID is a per-connection identifier sent by the server,
291                   which increments at server discretion.
292                   By contrast, the "id" field of the protocol is a connection-specific request
293                   identifier, which needs to be unique per request from the client and is not
294                   connected to the CUID value, so we ignore CUID from server.  It's purely for
295                   diagnostics. */
296                if (Ustrcmp(args[0], US"VERSION") == 0) {
297                        CHECK_COMMAND("VERSION", 2, 2);
298                        if (Uatoi(args[1]) != VERSION_MAJOR)
299                                OUT("authentication socket protocol version mismatch");
300                } else if (Ustrcmp(args[0], US"MECH") == 0) {
301                        CHECK_COMMAND("MECH", 1, INT_MAX);
302                        if (strcmpic(US args[1], ablock->public_name) == 0)
303                                found = TRUE;
304                } else if (Ustrcmp(args[0], US"DONE") == 0) {
305                        CHECK_COMMAND("DONE", 0, 0);
306                        cont = 0;
307                }
308        }
309
310        if (!found) {
311                auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
312                goto out;
313        }
314
315        /* Added by PH: data must not contain tab (as it is
316        b64 it shouldn't, but check for safety). */
317
318        if (Ustrchr(data, '\t') != NULL) {
319                ret = FAIL;
320                goto out;
321        }
322
323        /* Added by PH: extra fields when TLS is in use or if the TCP/IP
324        connection is local. */
325
326        if (tls_in.cipher != NULL)
327                auth_extra_data = string_sprintf("secured\t%s%s",
328                    tls_in.certificate_verified? "valid-client-cert" : "",
329                    tls_in.certificate_verified? "\t" : "");
330        else if (interface_address != NULL &&
331                 Ustrcmp(sender_host_address, interface_address) == 0)
332                auth_extra_data = US"secured\t";
333
334
335 /****************************************************************************
336    The code below was the original code here. It didn't work. A reading of the
337    file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
338    this was not right. Maybe something changed. I changed it to move the
339    service indication into the AUTH command, and it seems to be better. PH
340
341        fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
342                "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
343                VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
344                ablock->public_name, sender_host_address, interface_address,
345                data ? (char *) data : "");
346
347    Subsequently, the command was modified to add "secured" and "valid-client-
348    cert" when relevant.
349 ****************************************************************************/
350
351        auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
352                "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
353                VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
354                ablock->public_name, auth_extra_data, sender_host_address,
355                interface_address, data ? (char *) data : "");
356
357        if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
358               HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
359                 strerror(errno));
360
361        HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
362
363        while (1) {
364                uschar *temp;
365                uschar *auth_id_pre = NULL;
366                int i;
367
368                if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
369                        auth_defer_msg = US"authentication socket read error or premature eof";
370                        goto out;
371                }
372
373                buffer[Ustrlen(buffer) - 1] = 0;
374                HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
375                nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
376
377                if (Uatoi(args[1]) != crequid)
378                        OUT("authentication socket connection id mismatch");
379
380                switch (toupper(*args[0])) {
381                case 'C':
382                        CHECK_COMMAND("CONT", 1, 2);
383
384                        tmp = auth_get_no64_data(&data, US args[2]);
385                        if (tmp != OK) {
386                                ret = tmp;
387                                goto out;
388                        }
389
390                        /* Added by PH: data must not contain tab (as it is
391                        b64 it shouldn't, but check for safety). */
392
393                        if (Ustrchr(data, '\t') != NULL) {
394                                ret = FAIL;
395                                goto out;
396                        }
397
398                        temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
399                        if (write(fd, temp, Ustrlen(temp)) < 0)
400                                OUT("authentication socket write error");
401                        break;
402
403                case 'F':
404                        CHECK_COMMAND("FAIL", 1, -1);
405
406                        for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
407                        {
408                                if ( Ustrncmp(args[i], US"user=", 5) == 0 )
409                                {
410                                        auth_id_pre = args[i]+5;
411                                        expand_nstring[1] = auth_vars[0] =
412                                                string_copy(auth_id_pre); /* PH */
413                                        expand_nlength[1] = Ustrlen(auth_id_pre);
414                                        expand_nmax = 1;
415                                }
416                        }
417
418                        ret = FAIL;
419                        goto out;
420
421                case 'O':
422                        CHECK_COMMAND("OK", 2, -1);
423
424                        /*
425                         * Search for the "user=$USER" string in the args array
426                         * and return the proper value.
427                         */
428                        for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
429                        {
430                                if ( Ustrncmp(args[i], US"user=", 5) == 0 )
431                                {
432                                        auth_id_pre = args[i]+5;
433                                        expand_nstring[1] = auth_vars[0] =
434                                                string_copy(auth_id_pre); /* PH */
435                                        expand_nlength[1] = Ustrlen(auth_id_pre);
436                                        expand_nmax = 1;
437                                }
438                        }
439
440                        if (auth_id_pre == NULL)
441                                OUT("authentication socket protocol error, username missing");
442
443                        ret = OK;
444                        /* fallthrough */
445
446                default:
447                        goto out;
448                }
449        }
450
451 out:
452        /* close the socket used by dovecot */
453        if (fd >= 0)
454               close(fd);
455
456        /* Expand server_condition as an authorization check */
457        return (ret == OK)? auth_check_serv_cond(ablock) : ret;
458 }