Auth: handle socket read errors in Dovecot authenticator
[exim.git] / src / src / auths / dovecot.c
1 /*
2  * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3  * Copyright (c) 2006-2017 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
75
76 #ifdef MACRO_PREDEF
77
78 /* Dummy values */
79 void auth_dovecot_init(auth_instance *ablock) {}
80 int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
81 int auth_dovecot_client(auth_instance *ablock, void * sx,
82   int timeout, uschar *buffer, int buffsize) {return 0;}
83
84 #else   /*!MACRO_PREDEF*/
85
86
87 /* Static variables for reading from the socket */
88
89 static uschar sbuffer[256];
90 static int socket_buffer_left;
91
92
93
94 /*************************************************
95  *          Initialization entry point           *
96  *************************************************/
97
98 /* Called for each instance, after its options have been read, to
99 enable consistency checks to be done, or anything else that needs
100 to be set up. */
101
102 void auth_dovecot_init(auth_instance *ablock)
103 {
104        auth_dovecot_options_block *ob =
105                (auth_dovecot_options_block *)(ablock->options_block);
106
107        if (ablock->public_name == NULL)
108                ablock->public_name = ablock->name;
109        if (ob->server_socket != NULL)
110                ablock->server = TRUE;
111        ablock->client = FALSE;
112 }
113
114 /*************************************************
115  *    "strcut" to split apart server lines       *
116  *************************************************/
117
118 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
119 of a command-name, TAB, and then any parameters, each separated by a TAB.
120 A parameter can be param=value or a bool, just param.
121
122 This function modifies the original str in-place, inserting NUL characters.
123 It initialises ptrs entries, setting all to NULL and only setting
124 non-NULL N entries, where N is the return value, the number of fields seen
125 (one more than the number of tabs).
126
127 Note that the return value will always be at least 1, is the count of
128 actual fields (so last valid offset into ptrs is one less).
129 */
130
131 static int
132 strcut(uschar *str, uschar **ptrs, int nptrs)
133 {
134 uschar *last_sub_start = str;
135 int n;
136
137 for (n = 0; n < nptrs; n++)
138   ptrs[n] = NULL;
139 n = 1;
140
141 while (*str)
142   {
143   if (*str == '\t')
144     {
145     if (n <= nptrs)
146       {
147       *ptrs++ = last_sub_start;
148       last_sub_start = str + 1;
149       *str = '\0';
150       }
151       n++;
152     }
153     str++;
154   }
155
156 /* It's acceptable for the string to end with a tab character.  We see
157 this in AUTH PLAIN without an initial response from the client, which
158 causing us to send "334 " and get the data from the client. */
159 if (n <= nptrs)
160  *ptrs = last_sub_start;
161 else
162  {
163  HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
164  n = nptrs;
165  }
166
167 return n <= nptrs ? n : nptrs;
168 }
169
170 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
171 static void
172 debug_strcut(uschar **ptrs, int nlen, int alen)
173 {
174 int i;
175 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
176                 socket_buffer_left, nlen);
177 for (i = 0; i < nlen; i++)
178   debug_printf(" {%s}", ptrs[i]);
179 if (nlen < alen)
180   debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
181 else
182   debug_printf(" (max for capacity)\n");
183 }
184
185 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
186        if (strcmpic(US(str), args[0]) != 0) \
187                goto out; \
188        if (nargs - 1 < (arg_min)) \
189                goto out; \
190        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
191                goto out; \
192 } while (0)
193
194 #define OUT(msg) do { \
195        auth_defer_msg = (US msg); \
196        goto out; \
197 } while(0)
198
199
200
201 /*************************************************
202 *      "fgets" to read directly from socket      *
203 *************************************************/
204
205 /* Added by PH after a suggestion by Steve Usher because the previous use of
206 C-style buffered I/O gave trouble. */
207
208 static uschar *
209 dc_gets(uschar *s, int n, int fd)
210 {
211 int p = 0;
212 int count = 0;
213
214 for (;;)
215   {
216   if (socket_buffer_left == 0)
217     {
218     if ((socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer))) <= 0)
219       if (count == 0) return NULL; else break;
220     p = 0;
221     }
222
223   while (p < socket_buffer_left)
224     {
225     if (count >= n - 1) break;
226     s[count++] = sbuffer[p];
227     if (sbuffer[p++] == '\n') break;
228     }
229
230   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
231   socket_buffer_left -= p;
232
233   if (s[count-1] == '\n' || count >= n - 1) break;
234   }
235
236 s[count] = '\0';
237 return s;
238 }
239
240
241
242
243 /*************************************************
244 *              Server entry point                *
245 *************************************************/
246
247 int
248 auth_dovecot_server(auth_instance * ablock, uschar * data)
249 {
250 auth_dovecot_options_block *ob =
251        (auth_dovecot_options_block *) ablock->options_block;
252 struct sockaddr_un sa;
253 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
254 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
255 uschar *auth_command;
256 uschar *auth_extra_data = US"";
257 uschar *p;
258 int nargs, tmp;
259 int crequid = 1, cont = 1, fd = -1, ret = DEFER;
260 BOOL found = FALSE, have_mech_line = FALSE;
261
262 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
263
264 if (!data)
265   {
266   ret = FAIL;
267   goto out;
268   }
269
270 memset(&sa, 0, sizeof(sa));
271 sa.sun_family = AF_UNIX;
272
273 /* This was the original code here: it is nonsense because strncpy()
274 does not return an integer. I have converted this to use the function
275 that formats and checks length. PH */
276
277 /*
278 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
279 }
280 */
281
282 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
283                   ob->server_socket))
284   {
285   auth_defer_msg = US"authentication socket path too long";
286   return DEFER;
287   }
288
289 auth_defer_msg = US"authentication socket connection error";
290
291 if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
292   return DEFER;
293
294 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
295   goto out;
296
297 auth_defer_msg = US"authentication socket protocol error";
298
299 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
300 while (cont)
301   {
302   if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
303     OUT("authentication socket read error or premature eof");
304   p = buffer + Ustrlen(buffer) - 1;
305   if (*p != '\n')
306     OUT("authentication socket protocol line too long");
307
308   *p = '\0';
309   HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
310
311   nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
312
313   /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
314
315   /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
316     Exim will need. Original code also failed if Dovecot server sent unknown
317     command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
318   /* pdp: note that CUID is a per-connection identifier sent by the server,
319     which increments at server discretion.
320     By contrast, the "id" field of the protocol is a connection-specific request
321     identifier, which needs to be unique per request from the client and is not
322     connected to the CUID value, so we ignore CUID from server.  It's purely for
323     diagnostics. */
324
325   if (Ustrcmp(args[0], US"VERSION") == 0)
326     {
327     CHECK_COMMAND("VERSION", 2, 2);
328     if (Uatoi(args[1]) != VERSION_MAJOR)
329       OUT("authentication socket protocol version mismatch");
330     }
331   else if (Ustrcmp(args[0], US"MECH") == 0)
332     {
333     CHECK_COMMAND("MECH", 1, INT_MAX);
334     have_mech_line = TRUE;
335     if (strcmpic(US args[1], ablock->public_name) == 0)
336       found = TRUE;
337     }
338   else if (Ustrcmp(args[0], US"SPID") == 0)
339     {
340     /* Unfortunately the auth protocol handshake wasn't designed well
341     to differentiate between auth-client/userdb/master. auth-userdb
342     and auth-master send VERSION + SPID lines only and nothing
343     afterwards, while auth-client sends VERSION + MECH + SPID +
344     CUID + more. The simplest way that we can determine if we've
345     connected to the correct socket is to see if MECH line exists or
346     not (alternatively we'd have to have a small timeout after SPID
347     to see if CUID is sent or not). */
348
349     if (!have_mech_line)
350       OUT("authentication socket type mismatch"
351         " (connected to auth-master instead of auth-client)");
352     }
353   else if (Ustrcmp(args[0], US"DONE") == 0)
354     {
355     CHECK_COMMAND("DONE", 0, 0);
356     cont = 0;
357     }
358   }
359
360 if (!found)
361   {
362   auth_defer_msg = string_sprintf(
363     "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
364   goto out;
365   }
366
367 /* Added by PH: data must not contain tab (as it is
368 b64 it shouldn't, but check for safety). */
369
370 if (Ustrchr(data, '\t') != NULL)
371   {
372   ret = FAIL;
373   goto out;
374   }
375
376 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
377 connection is local. */
378
379 if (tls_in.cipher != NULL)
380   auth_extra_data = string_sprintf("secured\t%s%s",
381      tls_in.certificate_verified? "valid-client-cert" : "",
382      tls_in.certificate_verified? "\t" : "");
383
384 else if (  interface_address != NULL
385         && Ustrcmp(sender_host_address, interface_address) == 0)
386   auth_extra_data = US"secured\t";
387
388
389 /****************************************************************************
390 The code below was the original code here. It didn't work. A reading of the
391 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
392 this was not right. Maybe something changed. I changed it to move the
393 service indication into the AUTH command, and it seems to be better. PH
394
395 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
396        "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
397        VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
398        ablock->public_name, sender_host_address, interface_address,
399        data ? CS  data : "");
400
401 Subsequently, the command was modified to add "secured" and "valid-client-
402 cert" when relevant.
403 ****************************************************************************/
404
405 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
406        "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
407        VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
408        ablock->public_name, auth_extra_data, sender_host_address,
409        interface_address, data);
410
411 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
412   HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
413     strerror(errno));
414
415 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
416
417 while (1)
418   {
419   uschar *temp;
420   uschar *auth_id_pre = NULL;
421   int i;
422
423   if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
424     {
425     auth_defer_msg = US"authentication socket read error or premature eof";
426     goto out;
427     }
428
429   buffer[Ustrlen(buffer) - 1] = 0;
430   HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
431   nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
432
433   if (Uatoi(args[1]) != crequid)
434     OUT("authentication socket connection id mismatch");
435
436   switch (toupper(*args[0]))
437     {
438     case 'C':
439       CHECK_COMMAND("CONT", 1, 2);
440
441       if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
442         {
443         ret = tmp;
444         goto out;
445         }
446
447       /* Added by PH: data must not contain tab (as it is
448       b64 it shouldn't, but check for safety). */
449
450       if (Ustrchr(data, '\t') != NULL)
451         {
452         ret = FAIL;
453         goto out;
454         }
455
456       temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
457       if (write(fd, temp, Ustrlen(temp)) < 0)
458         OUT("authentication socket write error");
459       break;
460
461     case 'F':
462       CHECK_COMMAND("FAIL", 1, -1);
463
464       for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
465         {
466         if ( Ustrncmp(args[i], US"user=", 5) == 0 )
467           {
468           auth_id_pre = args[i]+5;
469           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
470           expand_nlength[1] = Ustrlen(auth_id_pre);
471           expand_nmax = 1;
472           }
473         }
474
475       ret = FAIL;
476       goto out;
477
478     case 'O':
479       CHECK_COMMAND("OK", 2, -1);
480
481       /* Search for the "user=$USER" string in the args array
482       and return the proper value.  */
483
484       for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
485         {
486         if ( Ustrncmp(args[i], US"user=", 5) == 0 )
487           {
488           auth_id_pre = args[i]+5;
489           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
490           expand_nlength[1] = Ustrlen(auth_id_pre);
491           expand_nmax = 1;
492           }
493         }
494
495       if (auth_id_pre == NULL)
496         OUT("authentication socket protocol error, username missing");
497
498       ret = OK;
499       /* fallthrough */
500
501     default:
502       goto out;
503     }
504   }
505
506 out:
507 /* close the socket used by dovecot */
508 if (fd >= 0)
509   close(fd);
510
511 /* Expand server_condition as an authorization check */
512 return ret == OK ? auth_check_serv_cond(ablock) : ret;
513 }
514
515
516 #endif   /*!MACRO_PREDEF*/