612aed39a2e7e977b2cfcb7838cfbacbf36e4bb6
[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
232 auth_dovecot_server(auth_instance * ablock, uschar * data)
233 {
234 auth_dovecot_options_block *ob =
235        (auth_dovecot_options_block *) ablock->options_block;
236 struct sockaddr_un sa;
237 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
238 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
239 uschar *auth_command;
240 uschar *auth_extra_data = US"";
241 uschar *p;
242 int nargs, tmp;
243 int crequid = 1, cont = 1, fd = -1, ret = DEFER;
244 BOOL found = FALSE, have_mech_line = FALSE;
245
246 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
247
248 if (!data)
249   {
250   ret = FAIL;
251   goto out;
252   }
253
254 memset(&sa, 0, sizeof(sa));
255 sa.sun_family = AF_UNIX;
256
257 /* This was the original code here: it is nonsense because strncpy()
258 does not return an integer. I have converted this to use the function
259 that formats and checks length. PH */
260
261 /*
262 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
263 }
264 */
265
266 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
267                   ob->server_socket))
268   {
269   auth_defer_msg = US"authentication socket path too long";
270   return DEFER;
271   }
272
273 auth_defer_msg = US"authentication socket connection error";
274
275 if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
276   return DEFER;
277
278 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
279   goto out;
280
281 auth_defer_msg = US"authentication socket protocol error";
282
283 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
284 while (cont)
285   {
286   if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
287     OUT("authentication socket read error or premature eof");
288   p = buffer + Ustrlen(buffer) - 1;
289   if (*p != '\n')
290     OUT("authentication socket protocol line too long");
291
292   *p = '\0';
293   HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
294
295   nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
296
297   /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
298
299   /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
300     Exim will need. Original code also failed if Dovecot server sent unknown
301     command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
302   /* pdp: note that CUID is a per-connection identifier sent by the server,
303     which increments at server discretion.
304     By contrast, the "id" field of the protocol is a connection-specific request
305     identifier, which needs to be unique per request from the client and is not
306     connected to the CUID value, so we ignore CUID from server.  It's purely for
307     diagnostics. */
308
309   if (Ustrcmp(args[0], US"VERSION") == 0)
310     {
311     CHECK_COMMAND("VERSION", 2, 2);
312     if (Uatoi(args[1]) != VERSION_MAJOR)
313       OUT("authentication socket protocol version mismatch");
314     }
315   else if (Ustrcmp(args[0], US"MECH") == 0)
316     {
317     CHECK_COMMAND("MECH", 1, INT_MAX);
318     have_mech_line = TRUE;
319     if (strcmpic(US args[1], ablock->public_name) == 0)
320       found = TRUE;
321     }
322   else if (Ustrcmp(args[0], US"SPID") == 0)
323     {
324     /* Unfortunately the auth protocol handshake wasn't designed well
325     to differentiate between auth-client/userdb/master. auth-userdb
326     and auth-master send VERSION + SPID lines only and nothing
327     afterwards, while auth-client sends VERSION + MECH + SPID +
328     CUID + more. The simplest way that we can determine if we've
329     connected to the correct socket is to see if MECH line exists or
330     not (alternatively we'd have to have a small timeout after SPID
331     to see if CUID is sent or not). */
332
333     if (!have_mech_line)
334       OUT("authentication socket type mismatch"
335         " (connected to auth-master instead of auth-client)");
336     }
337   else if (Ustrcmp(args[0], US"DONE") == 0)
338     {
339     CHECK_COMMAND("DONE", 0, 0);
340     cont = 0;
341     }
342   }
343
344 if (!found)
345   {
346   auth_defer_msg = string_sprintf(
347     "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
348   goto out;
349   }
350
351 /* Added by PH: data must not contain tab (as it is
352 b64 it shouldn't, but check for safety). */
353
354 if (Ustrchr(data, '\t') != NULL)
355   {
356   ret = FAIL;
357   goto out;
358   }
359
360 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
361 connection is local. */
362
363 if (tls_in.cipher != NULL)
364   auth_extra_data = string_sprintf("secured\t%s%s",
365      tls_in.certificate_verified? "valid-client-cert" : "",
366      tls_in.certificate_verified? "\t" : "");
367
368 else if (  interface_address != NULL
369         && Ustrcmp(sender_host_address, interface_address) == 0)
370   auth_extra_data = US"secured\t";
371
372
373 /****************************************************************************
374 The code below was the original code here. It didn't work. A reading of the
375 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
376 this was not right. Maybe something changed. I changed it to move the
377 service indication into the AUTH command, and it seems to be better. PH
378
379 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
380        "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
381        VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
382        ablock->public_name, sender_host_address, interface_address,
383        data ? (char *) data : "");
384
385 Subsequently, the command was modified to add "secured" and "valid-client-
386 cert" when relevant.
387 ****************************************************************************/
388
389 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
390        "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
391        VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
392        ablock->public_name, auth_extra_data, sender_host_address,
393        interface_address, data);
394
395 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
396   HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
397     strerror(errno));
398
399 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
400
401 while (1)
402   {
403   uschar *temp;
404   uschar *auth_id_pre = NULL;
405   int i;
406
407   if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
408     {
409     auth_defer_msg = US"authentication socket read error or premature eof";
410     goto out;
411     }
412
413   buffer[Ustrlen(buffer) - 1] = 0;
414   HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
415   nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
416
417   if (Uatoi(args[1]) != crequid)
418     OUT("authentication socket connection id mismatch");
419
420   switch (toupper(*args[0]))
421     {
422     case 'C':
423       CHECK_COMMAND("CONT", 1, 2);
424
425       if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
426         {
427         ret = tmp;
428         goto out;
429         }
430
431       /* Added by PH: data must not contain tab (as it is
432       b64 it shouldn't, but check for safety). */
433
434       if (Ustrchr(data, '\t') != NULL)
435         {
436         ret = FAIL;
437         goto out;
438         }
439
440       temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
441       if (write(fd, temp, Ustrlen(temp)) < 0)
442         OUT("authentication socket write error");
443       break;
444
445     case 'F':
446       CHECK_COMMAND("FAIL", 1, -1);
447
448       for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
449         {
450         if ( Ustrncmp(args[i], US"user=", 5) == 0 )
451           {
452           auth_id_pre = args[i]+5;
453           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
454           expand_nlength[1] = Ustrlen(auth_id_pre);
455           expand_nmax = 1;
456           }
457         }
458
459       ret = FAIL;
460       goto out;
461
462     case 'O':
463       CHECK_COMMAND("OK", 2, -1);
464
465       /* Search for the "user=$USER" string in the args array
466       and return the proper value.  */
467
468       for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
469         {
470         if ( Ustrncmp(args[i], US"user=", 5) == 0 )
471           {
472           auth_id_pre = args[i]+5;
473           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
474           expand_nlength[1] = Ustrlen(auth_id_pre);
475           expand_nmax = 1;
476           }
477         }
478
479       if (auth_id_pre == NULL)
480         OUT("authentication socket protocol error, username missing");
481
482       ret = OK;
483       /* fallthrough */
484
485     default:
486       goto out;
487     }
488   }
489
490 out:
491 /* close the socket used by dovecot */
492 if (fd >= 0)
493   close(fd);
494
495 /* Expand server_condition as an authorization check */
496 return ret == OK ? auth_check_serv_cond(ablock) : ret;
497 }