Merge 4.80.1 security fix in.
[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 #include "../exim.h"
16 #include "dovecot.h"
17
18 #define VERSION_MAJOR  1
19 #define VERSION_MINOR  0
20
21 /* Options specific to the authentication mechanism. */
22 optionlist auth_dovecot_options[] = {
23        {
24        "server_socket",
25        opt_stringptr,
26        (void *)(offsetof(auth_dovecot_options_block, server_socket))
27        },
28 };
29
30 /* Size of the options list. An extern variable has to be used so that its
31 address can appear in the tables drtables.c. */
32
33 int auth_dovecot_options_count =
34        sizeof(auth_dovecot_options) / sizeof(optionlist);
35
36 /* Default private options block for the authentication method. */
37
38 auth_dovecot_options_block auth_dovecot_option_defaults = {
39        NULL,                           /* server_socket */
40 };
41
42
43 /* Static variables for reading from the socket */
44
45 static uschar sbuffer[256];
46 static int sbp;
47
48
49
50 /*************************************************
51  *          Initialization entry point           *
52  *************************************************/
53
54 /* Called for each instance, after its options have been read, to
55 enable consistency checks to be done, or anything else that needs
56 to be set up. */
57
58 void auth_dovecot_init(auth_instance *ablock)
59 {
60        auth_dovecot_options_block *ob =
61                (auth_dovecot_options_block *)(ablock->options_block);
62
63        if (ablock->public_name == NULL)
64                ablock->public_name = ablock->name;
65        if (ob->server_socket != NULL)
66                ablock->server = TRUE;
67        ablock->client = FALSE;
68 }
69
70 static int strcut(uschar *str, uschar **ptrs, int nptrs)
71 {
72        uschar *tmp = str;
73        int n;
74
75        for (n = 0; n < nptrs; n++)
76                ptrs[n] = NULL;
77        n = 1;
78
79        while (*str) {
80                if (*str == '\t') {
81                        if (n <= nptrs) {
82                                *ptrs++ = tmp;
83                                tmp = str + 1;
84                                *str = 0;
85                        }
86                        n++;
87                }
88                str++;
89        }
90
91        if (n < nptrs)
92                *ptrs = tmp;
93
94        return n;
95 }
96
97 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
98        if (strcmpic(US(str), args[0]) != 0) \
99                goto out; \
100        if (nargs - 1 < (arg_min)) \
101                goto out; \
102        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
103                goto out; \
104 } while (0)
105
106 #define OUT(msg) do { \
107        auth_defer_msg = (US msg); \
108        goto out; \
109 } while(0)
110
111
112
113 /*************************************************
114 *      "fgets" to read directly from socket      *
115 *************************************************/
116
117 /* Added by PH after a suggestion by Steve Usher because the previous use of
118 C-style buffered I/O gave trouble. */
119
120 static uschar *
121 dc_gets(uschar *s, int n, int fd)
122 {
123 int p = 0;
124 int count = 0;
125
126 for (;;)
127   {
128   if (sbp == 0)
129     {
130     sbp = read(fd, sbuffer, sizeof(sbuffer));
131     if (sbp == 0) { if (count == 0) return NULL; else break; }
132     p = 0;
133     }
134
135   while (p < sbp)
136     {
137     if (count >= n - 1) break;
138     s[count++] = sbuffer[p];
139     if (sbuffer[p++] == '\n') break;
140     }
141
142   memmove(sbuffer, sbuffer + p, sbp - p);
143   sbp -= p;
144
145   if (s[count-1] == '\n' || count >= n - 1) break;
146   }
147
148 s[count] = 0;
149 return s;
150 }
151
152
153
154
155 /*************************************************
156 *              Server entry point                *
157 *************************************************/
158
159 int auth_dovecot_server(auth_instance *ablock, uschar *data)
160 {
161        auth_dovecot_options_block *ob =
162                (auth_dovecot_options_block *)(ablock->options_block);
163        struct sockaddr_un sa;
164        uschar buffer[4096];
165        uschar *args[8];
166        uschar *auth_command;
167        uschar *auth_extra_data = US"";
168        int nargs, tmp;
169        int cuid = 0, cont = 1, found = 0, fd, ret = DEFER;
170
171        HDEBUG(D_auth) debug_printf("dovecot authentication\n");
172
173        memset(&sa, 0, sizeof(sa));
174        sa.sun_family = AF_UNIX;
175
176        /* This was the original code here: it is nonsense because strncpy()
177        does not return an integer. I have converted this to use the function
178        that formats and checks length. PH */
179
180        /*
181        if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
182        */
183
184        if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
185                           ob->server_socket)) {
186                auth_defer_msg = US"authentication socket path too long";
187                return DEFER;
188        }
189
190        auth_defer_msg = US"authentication socket connection error";
191
192        fd = socket(PF_UNIX, SOCK_STREAM, 0);
193        if (fd < 0)
194                return DEFER;
195
196        if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
197                goto out;
198
199        auth_defer_msg = US"authentication socket protocol error";
200
201        sbp = 0;  /* Socket buffer pointer */
202        while (cont) {
203                if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
204                        OUT("authentication socket read error or premature eof");
205
206                buffer[Ustrlen(buffer) - 1] = 0;
207                HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
208                nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
209
210                /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
211                   Exim will need. Original code also failed if Dovecot server sent unknown
212                   command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
213                if (Ustrcmp(args[0], US"CUID") == 0) {
214                        CHECK_COMMAND("CUID", 1, 1);
215                        cuid = Uatoi(args[1]);
216                } else if (Ustrcmp(args[0], US"VERSION") == 0) {
217                        CHECK_COMMAND("VERSION", 2, 2);
218                        if (Uatoi(args[1]) != VERSION_MAJOR)
219                                OUT("authentication socket protocol version mismatch");
220                } else if (Ustrcmp(args[0], US"MECH") == 0) {
221                        CHECK_COMMAND("MECH", 1, INT_MAX);
222                        if (strcmpic(US args[1], ablock->public_name) == 0)
223                                found = 1;
224                } else if (Ustrcmp(args[0], US"DONE") == 0) {
225                        CHECK_COMMAND("DONE", 0, 0);
226                        cont = 0;
227                }
228        }
229
230        if (!found)
231                goto out;
232
233        /* Added by PH: data must not contain tab (as it is
234        b64 it shouldn't, but check for safety). */
235
236        if (Ustrchr(data, '\t') != NULL) {
237                ret = FAIL;
238                goto out;
239        }
240
241        /* Added by PH: extra fields when TLS is in use or if the TCP/IP
242        connection is local. */
243
244        if (tls_in.cipher != NULL)
245                auth_extra_data = string_sprintf("secured\t%s%s",
246                    tls_in.certificate_verified? "valid-client-cert" : "",
247                    tls_in.certificate_verified? "\t" : "");
248        else if (interface_address != NULL &&
249                 Ustrcmp(sender_host_address, interface_address) == 0)
250                auth_extra_data = US"secured\t";
251
252
253 /****************************************************************************
254    The code below was the original code here. It didn't work. A reading of the
255    file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
256    this was not right. Maybe something changed. I changed it to move the
257    service indication into the AUTH command, and it seems to be better. PH
258
259        fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
260                "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
261                VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
262                ablock->public_name, sender_host_address, interface_address,
263                data ? (char *) data : "");
264
265    Subsequently, the command was modified to add "secured" and "valid-client-
266    cert" when relevant.
267
268    The auth protocol is documented here:
269         http://wiki.dovecot.org/Authentication_Protocol
270 ****************************************************************************/
271
272        auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
273                "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
274                VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
275                ablock->public_name, auth_extra_data, sender_host_address,
276                interface_address, data ? (char *) data : "");
277
278        if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
279               HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
280                 strerror(errno));
281
282        HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
283
284        while (1) {
285                uschar *temp;
286                uschar *auth_id_pre = NULL;
287                int i;
288
289                if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
290                        auth_defer_msg = US"authentication socket read error or premature eof";
291                        goto out;
292                }
293
294                buffer[Ustrlen(buffer) - 1] = 0;
295                HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
296                nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
297
298                if (Uatoi(args[1]) != cuid)
299                        OUT("authentication socket connection id mismatch");
300
301                switch (toupper(*args[0])) {
302                case 'C':
303                        CHECK_COMMAND("CONT", 1, 2);
304
305                        tmp = auth_get_no64_data(&data, US args[2]);
306                        if (tmp != OK) {
307                                ret = tmp;
308                                goto out;
309                        }
310
311                        /* Added by PH: data must not contain tab (as it is
312                        b64 it shouldn't, but check for safety). */
313
314                        if (Ustrchr(data, '\t') != NULL) {
315                                ret = FAIL;
316                                goto out;
317                        }
318
319                        temp = string_sprintf("CONT\t%d\t%s\n", cuid, data);
320                        if (write(fd, temp, Ustrlen(temp)) < 0)
321                                OUT("authentication socket write error");
322                        break;
323
324                case 'F':
325                        CHECK_COMMAND("FAIL", 1, -1);
326
327                        for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
328                        {
329                                if ( Ustrncmp(args[i], US"user=", 5) == 0 )
330                                {
331                                        auth_id_pre = args[i]+5;
332                                        expand_nstring[1] = auth_vars[0] =
333                                                string_copy(auth_id_pre); /* PH */
334                                        expand_nlength[1] = Ustrlen(auth_id_pre);
335                                        expand_nmax = 1;
336                                }
337                        }
338
339                        ret = FAIL;
340                        goto out;
341
342                case 'O':
343                        CHECK_COMMAND("OK", 2, -1);
344
345                        /*
346                         * Search for the "user=$USER" string in the args array
347                         * and return the proper value.
348                         */
349                        for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
350                        {
351                                if ( Ustrncmp(args[i], US"user=", 5) == 0 )
352                                {
353                                        auth_id_pre = args[i]+5;
354                                        expand_nstring[1] = auth_vars[0] =
355                                                string_copy(auth_id_pre); /* PH */
356                                        expand_nlength[1] = Ustrlen(auth_id_pre);
357                                        expand_nmax = 1;
358                                }
359                        }
360
361                        if (auth_id_pre == NULL)
362                                OUT("authentication socket protocol error, username missing");
363
364                        ret = OK;
365                        /* fallthrough */
366
367                default:
368                        goto out;
369                }
370        }
371
372 out:
373        /* close the socket used by dovecot */
374        if (fd >= 0)
375               close(fd);
376
377        /* Expand server_condition as an authorization check */
378        return (ret == OK)? auth_check_serv_cond(ablock) : ret;
379 }