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