7f437fa0dbc64e3cae4169925915b319990cd407
[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
27 #ifdef AUTH_DOVECOT     /* Remainder of file */
28 #include "dovecot.h"
29
30 #define VERSION_MAJOR  1
31 #define VERSION_MINOR  0
32
33 /* http://wiki.dovecot.org/Authentication%20Protocol
34 "The maximum line length isn't defined,
35  but it's currently expected to fit into 8192 bytes"
36 */
37 #define DOVECOT_AUTH_MAXLINELEN 8192
38
39 /* This was hard-coded as 8.
40 AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
41 Dovecot 1; Dovecot 2 (same protocol version) defines 9.
42
43 Master->Server sends {"USER", id, userid} + params, 6 defined.
44 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
45
46 We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
47 for the command and id, where unspecified might include _at least_ user=...
48
49 So: allow for more fields than we ever expect to see, while aware that count
50 can go up without changing protocol version.
51 The cost is the length of an array of pointers on the stack.
52 */
53 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
54
55 /* Options specific to the authentication mechanism. */
56 optionlist auth_dovecot_options[] = {
57   { "server_socket", opt_stringptr, OPT_OFF(auth_dovecot_options_block, server_socket) },
58 /*{ "server_tls", opt_bool, OPT_OFF(auth_dovecot_options_block, server_tls) },*/
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         .server_socket = NULL,
70 /*      .server_tls =   FALSE,*/
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, void * sx,
82   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
103 auth_dovecot_init(auth_instance * ablock)
104 {
105 auth_dovecot_options_block * ob =
106        (auth_dovecot_options_block *)(ablock->options_block);
107
108 if (!ablock->public_name) ablock->public_name = ablock->name;
109 if (ob->server_socket) ablock->server = TRUE;
110 else DEBUG(D_auth) debug_printf("Dovecot auth driver: no server_socket for %s\n", ablock->public_name);
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       {
145       *ptrs++ = last_sub_start;
146       last_sub_start = str;
147       str[-1] = '\0';
148       }
149
150 /* It's acceptable for the string to end with a tab character.  We see
151 this in AUTH PLAIN without an initial response from the client, which
152 causing us to send "334 " and get the data from the client. */
153 if (n <= nptrs)
154   *ptrs = last_sub_start;
155 else
156   {
157   HDEBUG(D_auth)
158     debug_printf("dovecot: warning: too many results from tab-splitting;"
159                   " 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 if (nlen < alen)
176   debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
177 else
178   debug_printf(" (max for capacity)\n");
179 }
180
181 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
182        if (strcmpic(US(str), args[0]) != 0) \
183                goto out; \
184        if (nargs - 1 < (arg_min)) \
185                goto out; \
186        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
187                goto out; \
188 } while (0)
189
190 #define OUT(msg) do { \
191        auth_defer_msg = (US msg); \
192        goto out; \
193 } while(0)
194
195
196
197 /*************************************************
198 *      "fgets" to read directly from socket      *
199 *************************************************/
200
201 /* Added by PH after a suggestion by Steve Usher because the previous use of
202 C-style buffered I/O gave trouble. */
203
204 static uschar *
205 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
206 {
207 int p = 0;
208 int count = 0;
209
210 for (;;)
211   {
212   if (socket_buffer_left == 0)
213     {
214     if ((socket_buffer_left =
215 #ifndef DISABLE_TLS
216         cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
217 #endif
218         read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
219       if (count == 0)
220         return NULL;
221       else
222         break;
223     p = 0;
224     }
225
226   while (p < socket_buffer_left)
227     {
228     if (count >= n - 1) break;
229     s[count++] = sbuffer[p];
230     if (sbuffer[p++] == '\n') break;
231     }
232
233   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
234   socket_buffer_left -= p;
235
236   if (s[count-1] == '\n' || count >= n - 1) break;
237   }
238
239 s[count] = '\0';
240 return s;
241 }
242
243
244
245
246 /*************************************************
247 *              Server entry point                *
248 *************************************************/
249
250 int
251 auth_dovecot_server(auth_instance * ablock, uschar * data)
252 {
253 auth_dovecot_options_block *ob =
254        (auth_dovecot_options_block *) ablock->options_block;
255 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
256 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
257 uschar *auth_command;
258 uschar *auth_extra_data = US"";
259 uschar *p;
260 int nargs, tmp;
261 int crequid = 1, ret = DEFER;
262 host_item host;
263 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
264 BOOL found = FALSE, have_mech_line = FALSE;
265
266 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
267
268 if (!data)
269   {
270   ret = FAIL;
271   goto out;
272   }
273
274 /*XXX timeout? */
275 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
276 if (cctx.sock < 0)
277  goto out;
278
279 #ifdef notdef
280 # ifndef DISABLE_TLS
281 if (ob->server_tls)
282   {
283   union sockaddr_46 interface_sock;
284   EXIM_SOCKLEN_T size = sizeof(interface_sock);
285   smtp_connect_args conn_args = { .host = &host };
286   tls_support tls_dummy = { .sni = NULL };
287   uschar * errstr;
288
289   if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
290     conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
291   else
292     {
293     *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
294     goto bad;
295     }
296
297   if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
298     {
299     auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
300     goto out;
301     }
302   }
303 # endif
304 #endif
305
306 auth_defer_msg = US"authentication socket protocol error";
307
308 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
309 for (;;)
310   {
311   if (!dc_gets(buffer, sizeof(buffer), &cctx))
312     OUT("authentication socket read error or premature eof");
313   p = buffer + Ustrlen(buffer) - 1;
314   if (*p != '\n')
315     OUT("authentication socket protocol line too long");
316
317   *p = '\0';
318   HDEBUG(D_auth) debug_printf("  DOVECOT<< '%s'\n", buffer);
319
320   nargs = strcut(buffer, args, nelem(args));
321
322   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
323
324   /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
325     Exim will need. Original code also failed if Dovecot server sent unknown
326     command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
327   /* pdp: note that CUID is a per-connection identifier sent by the server,
328     which increments at server discretion.
329     By contrast, the "id" field of the protocol is a connection-specific request
330     identifier, which needs to be unique per request from the client and is not
331     connected to the CUID value, so we ignore CUID from server.  It's purely for
332     diagnostics. */
333
334   if (Ustrcmp(args[0], US"VERSION") == 0)
335     {
336     CHECK_COMMAND("VERSION", 2, 2);
337     if (Uatoi(args[1]) != VERSION_MAJOR)
338       OUT("authentication socket protocol version mismatch");
339     }
340   else if (Ustrcmp(args[0], US"MECH") == 0)
341     {
342     CHECK_COMMAND("MECH", 1, INT_MAX);
343     have_mech_line = TRUE;
344     if (strcmpic(US args[1], ablock->public_name) == 0)
345       found = TRUE;
346     }
347   else if (Ustrcmp(args[0], US"SPID") == 0)
348     {
349     /* Unfortunately the auth protocol handshake wasn't designed well
350     to differentiate between auth-client/userdb/master. auth-userdb
351     and auth-master send VERSION + SPID lines only and nothing
352     afterwards, while auth-client sends VERSION + MECH + SPID +
353     CUID + more. The simplest way that we can determine if we've
354     connected to the correct socket is to see if MECH line exists or
355     not (alternatively we'd have to have a small timeout after SPID
356     to see if CUID is sent or not). */
357
358     if (!have_mech_line)
359       OUT("authentication socket type mismatch"
360         " (connected to auth-master instead of auth-client)");
361     }
362   else if (Ustrcmp(args[0], US"DONE") == 0)
363     {
364     CHECK_COMMAND("DONE", 0, 0);
365     break;
366     }
367   }
368
369 if (!found)
370   {
371   auth_defer_msg = string_sprintf(
372     "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
373   goto out;
374   }
375
376 /* Added by PH: data must not contain tab (as it is
377 b64 it shouldn't, but check for safety). */
378
379 if (Ustrchr(data, '\t') != NULL)
380   {
381   ret = FAIL;
382   goto out;
383   }
384
385 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
386 connection is local. */
387
388 if (tls_in.cipher)
389   auth_extra_data = string_sprintf("secured\t%s%s",
390      tls_in.certificate_verified ? "valid-client-cert" : "",
391      tls_in.certificate_verified ? "\t" : "");
392
393 else if (  interface_address
394         && Ustrcmp(sender_host_address, interface_address) == 0)
395   auth_extra_data = US"secured\t";
396
397
398 /****************************************************************************
399 The code below was the original code here. It didn't work. A reading of the
400 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
401 this was not right. Maybe something changed. I changed it to move the
402 service indication into the AUTH command, and it seems to be better. PH
403
404 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
405        "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
406        VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
407        ablock->public_name, sender_host_address, interface_address,
408        data ? CS  data : "");
409
410 Subsequently, the command was modified to add "secured" and "valid-client-
411 cert" when relevant.
412 ****************************************************************************/
413
414 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
415        "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
416        VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
417        ablock->public_name, auth_extra_data, sender_host_address,
418        interface_address, data);
419
420 if ((
421 #ifndef DISABLE_TLS
422     cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
423 #endif
424     write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
425   HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
426     strerror(errno));
427
428 HDEBUG(D_auth) debug_printf("  DOVECOT>> '%s'\n", auth_command);
429
430 while (1)
431   {
432   uschar * temp;
433   uschar * auth_id_pre = NULL;
434
435   if (!dc_gets(buffer, sizeof(buffer), &cctx))
436     {
437     auth_defer_msg = US"authentication socket read error or premature eof";
438     goto out;
439     }
440
441   buffer[Ustrlen(buffer) - 1] = 0;
442   HDEBUG(D_auth) debug_printf("  DOVECOT<< '%s'\n", buffer);
443   nargs = strcut(buffer, args, nelem(args));
444   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
445
446   if (Uatoi(args[1]) != crequid)
447     OUT("authentication socket connection id mismatch");
448
449   switch (toupper(*args[0]))
450     {
451     case 'C':
452       CHECK_COMMAND("CONT", 1, 2);
453
454       if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
455         {
456         ret = tmp;
457         goto out;
458         }
459
460       /* Added by PH: data must not contain tab (as it is
461       b64 it shouldn't, but check for safety). */
462
463       if (Ustrchr(data, '\t') != NULL)
464         {
465         ret = FAIL;
466         goto out;
467         }
468
469       temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
470       if ((
471 #ifndef DISABLE_TLS
472           cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
473 #endif
474           write(cctx.sock, temp, Ustrlen(temp))) < 0)
475         OUT("authentication socket write error");
476
477       HDEBUG(D_auth) debug_printf("  DOVECOT>> '%s'\n", temp);
478       break;
479
480     case 'F':
481       CHECK_COMMAND("FAIL", 1, -1);
482
483       for (int i = 2; i < nargs && !auth_id_pre; i++)
484         if (Ustrncmp(args[i], US"user=", 5) == 0)
485           {
486           auth_id_pre = args[i] + 5;
487           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
488           expand_nlength[1] = Ustrlen(auth_id_pre);
489           expand_nmax = 1;
490           }
491       ret = FAIL;
492       goto out;
493
494     case 'O':
495       CHECK_COMMAND("OK", 2, -1);
496
497       /* Search for the "user=$USER" string in the args array
498       and return the proper value.  */
499
500       for (int i = 2; i < nargs && !auth_id_pre; i++)
501         if (Ustrncmp(args[i], US"user=", 5) == 0)
502           {
503           auth_id_pre = args[i] + 5;
504           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
505           expand_nlength[1] = Ustrlen(auth_id_pre);
506           expand_nmax = 1;
507           }
508
509       if (!auth_id_pre)
510         OUT("authentication socket protocol error, username missing");
511
512       auth_defer_msg = NULL;
513       ret = OK;
514       /* fallthrough */
515
516     default:
517       goto out;
518     }
519   }
520
521 out:
522 /* close the socket used by dovecot */
523 #ifndef DISABLE_TLS
524 if (cctx.tls_ctx)
525   tls_close(cctx.tls_ctx, TRUE);
526 #endif
527 if (cctx.sock >= 0)
528   close(cctx.sock);
529
530 /* Expand server_condition as an authorization check */
531 if (ret == OK) ret = auth_check_serv_cond(ablock);
532
533 HDEBUG(D_auth) debug_printf("dovecot auth ret: %s\n", rc_names[ret]);
534 return ret;
535 }
536
537
538 #endif  /*!MACRO_PREDEF*/
539 #endif  /*AUTH_DOVECOT*/
540 /* end of auths/dovecot.c */