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