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