ca3d1bd1cefae5d11973feb1b061ca9c178e5829
[exim.git] / src / src / auths / dovecot.c
1 /*
2  * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3  * Copyright (c) 2006-2020 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, OPT_OFF(auth_dovecot_options_block, server_socket) },
55 /*{ "server_tls", opt_bool, OPT_OFF(auth_dovecot_options_block, server_tls) },*/
56 };
57
58 /* Size of the options list. An extern variable has to be used so that its
59 address can appear in the tables drtables.c. */
60
61 int auth_dovecot_options_count = nelem(auth_dovecot_options);
62
63 /* Default private options block for the authentication method. */
64
65 auth_dovecot_options_block auth_dovecot_option_defaults = {
66         .server_socket = NULL,
67 /*      .server_tls =   FALSE,*/
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, client_conn_ctx * cctx)
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 =
210 #ifndef DISABLE_TLS
211         cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
212 #endif
213         read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
214       if (count == 0)
215         return NULL;
216       else
217         break;
218     p = 0;
219     }
220
221   while (p < socket_buffer_left)
222     {
223     if (count >= n - 1) break;
224     s[count++] = sbuffer[p];
225     if (sbuffer[p++] == '\n') break;
226     }
227
228   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
229   socket_buffer_left -= p;
230
231   if (s[count-1] == '\n' || count >= n - 1) break;
232   }
233
234 s[count] = '\0';
235 return s;
236 }
237
238
239
240
241 /*************************************************
242 *              Server entry point                *
243 *************************************************/
244
245 int
246 auth_dovecot_server(auth_instance * ablock, uschar * data)
247 {
248 auth_dovecot_options_block *ob =
249        (auth_dovecot_options_block *) ablock->options_block;
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, ret = DEFER;
257 host_item host;
258 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
259 BOOL found = FALSE, have_mech_line = FALSE;
260
261 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
262
263 if (!data)
264   {
265   ret = FAIL;
266   goto out;
267   }
268
269 /*XXX timeout? */
270 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
271 if (cctx.sock < 0)
272  goto out;
273
274 #ifdef notdef
275 # ifndef DISABLE_TLS
276 if (ob->server_tls)
277   {
278   union sockaddr_46 interface_sock;
279   EXIM_SOCKLEN_T size = sizeof(interface_sock);
280   smtp_connect_args conn_args = { .host = &host };
281   tls_support tls_dummy = { .sni = NULL };
282   uschar * errstr;
283
284   if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
285     conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
286   else
287     {
288     *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
289     goto bad;
290     }
291
292   if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
293     {
294     auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
295     goto out;
296     }
297   }
298 # endif
299 #endif
300
301 auth_defer_msg = US"authentication socket protocol error";
302
303 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
304 for (;;)
305   {
306 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
307   if (!dc_gets(buffer, sizeof(buffer), &cctx))
308     OUT("authentication socket read error or premature eof");
309 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
310   p = buffer + Ustrlen(buffer) - 1;
311   if (*p != '\n')
312     OUT("authentication socket protocol line too long");
313
314   *p = '\0';
315   HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
316
317   nargs = strcut(buffer, args, nelem(args));
318
319   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
320
321   /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
322     Exim will need. Original code also failed if Dovecot server sent unknown
323     command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
324   /* pdp: note that CUID is a per-connection identifier sent by the server,
325     which increments at server discretion.
326     By contrast, the "id" field of the protocol is a connection-specific request
327     identifier, which needs to be unique per request from the client and is not
328     connected to the CUID value, so we ignore CUID from server.  It's purely for
329     diagnostics. */
330
331   if (Ustrcmp(args[0], US"VERSION") == 0)
332     {
333     CHECK_COMMAND("VERSION", 2, 2);
334     if (Uatoi(args[1]) != VERSION_MAJOR)
335       OUT("authentication socket protocol version mismatch");
336     }
337   else if (Ustrcmp(args[0], US"MECH") == 0)
338     {
339     CHECK_COMMAND("MECH", 1, INT_MAX);
340     have_mech_line = TRUE;
341     if (strcmpic(US args[1], ablock->public_name) == 0)
342       found = TRUE;
343     }
344   else if (Ustrcmp(args[0], US"SPID") == 0)
345     {
346     /* Unfortunately the auth protocol handshake wasn't designed well
347     to differentiate between auth-client/userdb/master. auth-userdb
348     and auth-master send VERSION + SPID lines only and nothing
349     afterwards, while auth-client sends VERSION + MECH + SPID +
350     CUID + more. The simplest way that we can determine if we've
351     connected to the correct socket is to see if MECH line exists or
352     not (alternatively we'd have to have a small timeout after SPID
353     to see if CUID is sent or not). */
354
355     if (!have_mech_line)
356       OUT("authentication socket type mismatch"
357         " (connected to auth-master instead of auth-client)");
358     }
359   else if (Ustrcmp(args[0], US"DONE") == 0)
360     {
361     CHECK_COMMAND("DONE", 0, 0);
362     break;
363     }
364   }
365
366 if (!found)
367   {
368   auth_defer_msg = string_sprintf(
369     "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
370   goto out;
371   }
372
373 /* Added by PH: data must not contain tab (as it is
374 b64 it shouldn't, but check for safety). */
375
376 if (Ustrchr(data, '\t') != NULL)
377   {
378   ret = FAIL;
379   goto out;
380   }
381
382 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
383 connection is local. */
384
385 if (tls_in.cipher)
386   auth_extra_data = string_sprintf("secured\t%s%s",
387      tls_in.certificate_verified ? "valid-client-cert" : "",
388      tls_in.certificate_verified ? "\t" : "");
389
390 else if (  interface_address
391         && Ustrcmp(sender_host_address, interface_address) == 0)
392   auth_extra_data = US"secured\t";
393
394
395 /****************************************************************************
396 The code below was the original code here. It didn't work. A reading of the
397 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
398 this was not right. Maybe something changed. I changed it to move the
399 service indication into the AUTH command, and it seems to be better. PH
400
401 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
402        "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
403        VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
404        ablock->public_name, sender_host_address, interface_address,
405        data ? CS  data : "");
406
407 Subsequently, the command was modified to add "secured" and "valid-client-
408 cert" when relevant.
409 ****************************************************************************/
410
411 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
412        "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
413        VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
414        ablock->public_name, auth_extra_data, sender_host_address,
415        interface_address, data);
416
417 if ((
418 #ifndef DISABLE_TLS
419     cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
420 #endif
421     write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
422   HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
423     strerror(errno));
424
425 HDEBUG(D_auth) debug_printf("sent: '%s'\n", auth_command);
426
427 while (1)
428   {
429   uschar *temp;
430   uschar *auth_id_pre = NULL;
431
432   if (!dc_gets(buffer, sizeof(buffer), &cctx))
433     {
434     auth_defer_msg = US"authentication socket read error or premature eof";
435     goto out;
436     }
437
438   buffer[Ustrlen(buffer) - 1] = 0;
439   HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
440   nargs = strcut(buffer, args, nelem(args));
441   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
442
443   if (Uatoi(args[1]) != crequid)
444     OUT("authentication socket connection id mismatch");
445
446   switch (toupper(*args[0]))
447     {
448     case 'C':
449       CHECK_COMMAND("CONT", 1, 2);
450
451       if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
452         {
453         ret = tmp;
454         goto out;
455         }
456
457       /* Added by PH: data must not contain tab (as it is
458       b64 it shouldn't, but check for safety). */
459
460       if (Ustrchr(data, '\t') != NULL)
461         {
462         ret = FAIL;
463         goto out;
464         }
465
466       temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
467       if ((
468 #ifndef DISABLE_TLS
469           cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
470 #endif
471           write(cctx.sock, temp, Ustrlen(temp))) < 0)
472         OUT("authentication socket write error");
473       break;
474
475     case 'F':
476       CHECK_COMMAND("FAIL", 1, -1);
477
478       for (int i = 2; i < nargs && !auth_id_pre; i++)
479         if (Ustrncmp(args[i], US"user=", 5) == 0)
480           {
481           auth_id_pre = args[i] + 5;
482           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
483           expand_nlength[1] = Ustrlen(auth_id_pre);
484           expand_nmax = 1;
485           }
486       ret = FAIL;
487       goto out;
488
489     case 'O':
490       CHECK_COMMAND("OK", 2, -1);
491
492       /* Search for the "user=$USER" string in the args array
493       and return the proper value.  */
494
495       for (int i = 2; i < nargs && !auth_id_pre; i++)
496         if (Ustrncmp(args[i], US"user=", 5) == 0)
497           {
498           auth_id_pre = args[i] + 5;
499           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
500           expand_nlength[1] = Ustrlen(auth_id_pre);
501           expand_nmax = 1;
502           }
503
504       if (!auth_id_pre)
505         OUT("authentication socket protocol error, username missing");
506
507       auth_defer_msg = NULL;
508       ret = OK;
509       /* fallthrough */
510
511     default:
512       goto out;
513     }
514   }
515
516 out:
517 /* close the socket used by dovecot */
518 #ifndef DISABLE_TLS
519 if (cctx.tls_ctx)
520   tls_close(cctx.tls_ctx, TRUE);
521 #endif
522 if (cctx.sock >= 0)
523   close(cctx.sock);
524
525 /* Expand server_condition as an authorization check */
526 return ret == OK ? auth_check_serv_cond(ablock) : ret;
527 }
528
529
530 #endif   /*!MACRO_PREDEF*/