Merge branch '4.next'
[exim.git] / src / src / auths / dovecot.c
1 /*
2  * Copyright (c) The Exim Maintainers 2006 - 2023
3  * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
4  * SPDX-License-Identifier: GPL-2.0-or-later
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published
8  * by the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  */
11
12 /* A number of modifications have been made to the original code. Originally I
13 commented them specially, but now they are getting quite extensive, so I have
14 ceased doing that. The biggest change is to use unbuffered I/O on the socket
15 because using C buffered I/O gives problems on some operating systems. PH */
16
17 /* Protocol specifications:
18  * Dovecot 1, protocol version 1.1
19  *   http://wiki.dovecot.org/Authentication%20Protocol
20  *
21  * Dovecot 2, protocol version 1.1
22  *   http://wiki2.dovecot.org/Design/AuthProtocol
23  */
24
25 #include "../exim.h"
26 #include "dovecot.h"
27
28 #define VERSION_MAJOR  1
29 #define VERSION_MINOR  0
30
31 /* http://wiki.dovecot.org/Authentication%20Protocol
32 "The maximum line length isn't defined,
33  but it's currently expected to fit into 8192 bytes"
34 */
35 #define DOVECOT_AUTH_MAXLINELEN 8192
36
37 /* This was hard-coded as 8.
38 AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
39 Dovecot 1; Dovecot 2 (same protocol version) defines 9.
40
41 Master->Server sends {"USER", id, userid} + params, 6 defined.
42 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
43
44 We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
45 for the command and id, where unspecified might include _at least_ user=...
46
47 So: allow for more fields than we ever expect to see, while aware that count
48 can go up without changing protocol version.
49 The cost is the length of an array of pointers on the stack.
50 */
51 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
52
53 /* Options specific to the authentication mechanism. */
54 optionlist auth_dovecot_options[] = {
55   { "server_socket", opt_stringptr, OPT_OFF(auth_dovecot_options_block, server_socket) },
56 /*{ "server_tls", opt_bool, OPT_OFF(auth_dovecot_options_block, server_tls) },*/
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         .server_socket = NULL,
68 /*      .server_tls =   FALSE,*/
69 };
70
71
72
73
74 #ifdef MACRO_PREDEF
75
76 /* Dummy values */
77 void auth_dovecot_init(auth_instance *ablock) {}
78 int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
79 int auth_dovecot_client(auth_instance *ablock, void * sx,
80   int timeout, uschar *buffer, int buffsize) {return 0;}
81
82 #else   /*!MACRO_PREDEF*/
83
84
85 /* Static variables for reading from the socket */
86
87 static uschar sbuffer[256];
88 static int socket_buffer_left;
89
90
91
92 /*************************************************
93  *          Initialization entry point           *
94  *************************************************/
95
96 /* Called for each instance, after its options have been read, to
97 enable consistency checks to be done, or anything else that needs
98 to be set up. */
99
100 void
101 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 else DEBUG(D_auth) debug_printf("Dovecot auth driver: no server_socket for %s\n", ablock->public_name);
109 ablock->client = FALSE;
110 }
111
112 /*************************************************
113  *    "strcut" to split apart server lines       *
114  *************************************************/
115
116 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
117 of a command-name, TAB, and then any parameters, each separated by a TAB.
118 A parameter can be param=value or a bool, just param.
119
120 This function modifies the original str in-place, inserting NUL characters.
121 It initialises ptrs entries, setting all to NULL and only setting
122 non-NULL N entries, where N is the return value, the number of fields seen
123 (one more than the number of tabs).
124
125 Note that the return value will always be at least 1, is the count of
126 actual fields (so last valid offset into ptrs is one less).
127 */
128
129 static int
130 strcut(uschar *str, uschar **ptrs, int nptrs)
131 {
132 uschar *last_sub_start = str;
133 int n;
134
135 for (n = 0; n < nptrs; n++)
136   ptrs[n] = NULL;
137 n = 1;
138
139 while (*str)
140   if (*str++ == '\t')
141     if (n++ <= nptrs)
142       {
143       *ptrs++ = last_sub_start;
144       last_sub_start = str;
145       str[-1] = '\0';
146       }
147
148 /* It's acceptable for the string to end with a tab character.  We see
149 this in AUTH PLAIN without an initial response from the client, which
150 causing us to send "334 " and get the data from the client. */
151 if (n <= nptrs)
152   *ptrs = last_sub_start;
153 else
154   {
155   HDEBUG(D_auth)
156     debug_printf("dovecot: warning: too many results from tab-splitting;"
157                   " saw %d fields, room for %d\n", n, nptrs);
158   n = nptrs;
159   }
160
161 return n <= nptrs ? n : nptrs;
162 }
163
164 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
165 static void
166 debug_strcut(uschar **ptrs, int nlen, int alen)
167 {
168 int i;
169 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
170                 socket_buffer_left, nlen);
171 for (i = 0; i < nlen; i++)
172   debug_printf(" {%s}", ptrs[i]);
173 if (nlen < alen)
174   debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
175 else
176   debug_printf(" (max for capacity)\n");
177 }
178
179 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
180        if (strcmpic(US(str), args[0]) != 0) \
181                goto out; \
182        if (nargs - 1 < (arg_min)) \
183                goto out; \
184        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
185                goto out; \
186 } while (0)
187
188 #define OUT(msg) do { \
189        auth_defer_msg = (US msg); \
190        goto out; \
191 } while(0)
192
193
194
195 /*************************************************
196 *      "fgets" to read directly from socket      *
197 *************************************************/
198
199 /* Added by PH after a suggestion by Steve Usher because the previous use of
200 C-style buffered I/O gave trouble. */
201
202 static uschar *
203 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
204 {
205 int p = 0;
206 int count = 0;
207
208 for (;;)
209   {
210   if (socket_buffer_left == 0)
211     {
212     if ((socket_buffer_left =
213 #ifndef DISABLE_TLS
214         cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
215 #endif
216         read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
217       if (count == 0)
218         return NULL;
219       else
220         break;
221     p = 0;
222     }
223
224   while (p < socket_buffer_left)
225     {
226     if (count >= n - 1) break;
227     s[count++] = sbuffer[p];
228     if (sbuffer[p++] == '\n') break;
229     }
230
231   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
232   socket_buffer_left -= p;
233
234   if (s[count-1] == '\n' || count >= n - 1) break;
235   }
236
237 s[count] = '\0';
238 return s;
239 }
240
241
242
243
244 /*************************************************
245 *              Server entry point                *
246 *************************************************/
247
248 int
249 auth_dovecot_server(auth_instance * ablock, uschar * data)
250 {
251 auth_dovecot_options_block *ob =
252        (auth_dovecot_options_block *) ablock->options_block;
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, ret = DEFER;
260 host_item host;
261 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
262 BOOL found = FALSE, have_mech_line = FALSE;
263
264 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
265
266 if (!data)
267   {
268   ret = FAIL;
269   goto out;
270   }
271
272 /*XXX timeout? */
273 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
274 if (cctx.sock < 0)
275  goto out;
276
277 #ifdef notdef
278 # ifndef DISABLE_TLS
279 if (ob->server_tls)
280   {
281   union sockaddr_46 interface_sock;
282   EXIM_SOCKLEN_T size = sizeof(interface_sock);
283   smtp_connect_args conn_args = { .host = &host };
284   tls_support tls_dummy = { .sni = NULL };
285   uschar * errstr;
286
287   if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
288     conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
289   else
290     {
291     *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
292     goto bad;
293     }
294
295   if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
296     {
297     auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
298     goto out;
299     }
300   }
301 # endif
302 #endif
303
304 auth_defer_msg = US"authentication socket protocol error";
305
306 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
307 for (;;)
308   {
309   if (!dc_gets(buffer, sizeof(buffer), &cctx))
310     OUT("authentication socket read error or premature eof");
311   p = buffer + Ustrlen(buffer) - 1;
312   if (*p != '\n')
313     OUT("authentication socket protocol line too long");
314
315   *p = '\0';
316   HDEBUG(D_auth) debug_printf("  DOVECOT<< '%s'\n", buffer);
317
318   nargs = strcut(buffer, args, nelem(args));
319
320   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
321
322   /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
323     Exim will need. Original code also failed if Dovecot server sent unknown
324     command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
325   /* pdp: note that CUID is a per-connection identifier sent by the server,
326     which increments at server discretion.
327     By contrast, the "id" field of the protocol is a connection-specific request
328     identifier, which needs to be unique per request from the client and is not
329     connected to the CUID value, so we ignore CUID from server.  It's purely for
330     diagnostics. */
331
332   if (Ustrcmp(args[0], US"VERSION") == 0)
333     {
334     CHECK_COMMAND("VERSION", 2, 2);
335     if (Uatoi(args[1]) != VERSION_MAJOR)
336       OUT("authentication socket protocol version mismatch");
337     }
338   else if (Ustrcmp(args[0], US"MECH") == 0)
339     {
340     CHECK_COMMAND("MECH", 1, INT_MAX);
341     have_mech_line = TRUE;
342     if (strcmpic(US args[1], ablock->public_name) == 0)
343       found = TRUE;
344     }
345   else if (Ustrcmp(args[0], US"SPID") == 0)
346     {
347     /* Unfortunately the auth protocol handshake wasn't designed well
348     to differentiate between auth-client/userdb/master. auth-userdb
349     and auth-master send VERSION + SPID lines only and nothing
350     afterwards, while auth-client sends VERSION + MECH + SPID +
351     CUID + more. The simplest way that we can determine if we've
352     connected to the correct socket is to see if MECH line exists or
353     not (alternatively we'd have to have a small timeout after SPID
354     to see if CUID is sent or not). */
355
356     if (!have_mech_line)
357       OUT("authentication socket type mismatch"
358         " (connected to auth-master instead of auth-client)");
359     }
360   else if (Ustrcmp(args[0], US"DONE") == 0)
361     {
362     CHECK_COMMAND("DONE", 0, 0);
363     break;
364     }
365   }
366
367 if (!found)
368   {
369   auth_defer_msg = string_sprintf(
370     "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
371   goto out;
372   }
373
374 /* Added by PH: data must not contain tab (as it is
375 b64 it shouldn't, but check for safety). */
376
377 if (Ustrchr(data, '\t') != NULL)
378   {
379   ret = FAIL;
380   goto out;
381   }
382
383 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
384 connection is local. */
385
386 if (tls_in.cipher)
387   auth_extra_data = string_sprintf("secured\t%s%s",
388      tls_in.certificate_verified ? "valid-client-cert" : "",
389      tls_in.certificate_verified ? "\t" : "");
390
391 else if (  interface_address
392         && Ustrcmp(sender_host_address, interface_address) == 0)
393   auth_extra_data = US"secured\t";
394
395
396 /****************************************************************************
397 The code below was the original code here. It didn't work. A reading of the
398 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
399 this was not right. Maybe something changed. I changed it to move the
400 service indication into the AUTH command, and it seems to be better. PH
401
402 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
403        "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
404        VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
405        ablock->public_name, sender_host_address, interface_address,
406        data ? CS  data : "");
407
408 Subsequently, the command was modified to add "secured" and "valid-client-
409 cert" when relevant.
410 ****************************************************************************/
411
412 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
413        "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
414        VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
415        ablock->public_name, auth_extra_data, sender_host_address,
416        interface_address, data);
417
418 if ((
419 #ifndef DISABLE_TLS
420     cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
421 #endif
422     write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
423   HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
424     strerror(errno));
425
426 HDEBUG(D_auth) debug_printf("  DOVECOT>> '%s'\n", auth_command);
427
428 while (1)
429   {
430   uschar * temp;
431   uschar * auth_id_pre = NULL;
432
433   if (!dc_gets(buffer, sizeof(buffer), &cctx))
434     {
435     auth_defer_msg = US"authentication socket read error or premature eof";
436     goto out;
437     }
438
439   buffer[Ustrlen(buffer) - 1] = 0;
440   HDEBUG(D_auth) debug_printf("  DOVECOT<< '%s'\n", buffer);
441   nargs = strcut(buffer, args, nelem(args));
442   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
443
444   if (Uatoi(args[1]) != crequid)
445     OUT("authentication socket connection id mismatch");
446
447   switch (toupper(*args[0]))
448     {
449     case 'C':
450       CHECK_COMMAND("CONT", 1, 2);
451
452       if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
453         {
454         ret = tmp;
455         goto out;
456         }
457
458       /* Added by PH: data must not contain tab (as it is
459       b64 it shouldn't, but check for safety). */
460
461       if (Ustrchr(data, '\t') != NULL)
462         {
463         ret = FAIL;
464         goto out;
465         }
466
467       temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
468       if ((
469 #ifndef DISABLE_TLS
470           cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
471 #endif
472           write(cctx.sock, temp, Ustrlen(temp))) < 0)
473         OUT("authentication socket write error");
474
475       HDEBUG(D_auth) debug_printf("  DOVECOT>> '%s'\n", temp);
476       break;
477
478     case 'F':
479       CHECK_COMMAND("FAIL", 1, -1);
480
481       for (int i = 2; i < nargs && !auth_id_pre; i++)
482         if (Ustrncmp(args[i], US"user=", 5) == 0)
483           {
484           auth_id_pre = args[i] + 5;
485           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
486           expand_nlength[1] = Ustrlen(auth_id_pre);
487           expand_nmax = 1;
488           }
489       ret = FAIL;
490       goto out;
491
492     case 'O':
493       CHECK_COMMAND("OK", 2, -1);
494
495       /* Search for the "user=$USER" string in the args array
496       and return the proper value.  */
497
498       for (int i = 2; i < nargs && !auth_id_pre; i++)
499         if (Ustrncmp(args[i], US"user=", 5) == 0)
500           {
501           auth_id_pre = args[i] + 5;
502           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
503           expand_nlength[1] = Ustrlen(auth_id_pre);
504           expand_nmax = 1;
505           }
506
507       if (!auth_id_pre)
508         OUT("authentication socket protocol error, username missing");
509
510       auth_defer_msg = NULL;
511       ret = OK;
512       /* fallthrough */
513
514     default:
515       goto out;
516     }
517   }
518
519 out:
520 /* close the socket used by dovecot */
521 #ifndef DISABLE_TLS
522 if (cctx.tls_ctx)
523   tls_close(cctx.tls_ctx, TRUE);
524 #endif
525 if (cctx.sock >= 0)
526   close(cctx.sock);
527
528 /* Expand server_condition as an authorization check */
529 if (ret == OK) ret = auth_check_serv_cond(ablock);
530
531 HDEBUG(D_auth) debug_printf("dovecot auth ret: %s\n", rc_names[ret]);
532 return ret;
533 }
534
535
536 #endif   /*!MACRO_PREDEF*/