SPDX: Mass-update to GPL-2.0-or-later
[exim.git] / src / src / auths / dovecot.c
1 /*
2  * Copyright (c) The Exim Maintainers 2006 - 2022
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 auth_dovecot_init(auth_instance *ablock)
101 {
102 auth_dovecot_options_block *ob =
103        (auth_dovecot_options_block *)(ablock->options_block);
104
105 if (!ablock->public_name) ablock->public_name = ablock->name;
106 if (ob->server_socket) ablock->server = TRUE;
107 ablock->client = FALSE;
108 }
109
110 /*************************************************
111  *    "strcut" to split apart server lines       *
112  *************************************************/
113
114 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
115 of a command-name, TAB, and then any parameters, each separated by a TAB.
116 A parameter can be param=value or a bool, just param.
117
118 This function modifies the original str in-place, inserting NUL characters.
119 It initialises ptrs entries, setting all to NULL and only setting
120 non-NULL N entries, where N is the return value, the number of fields seen
121 (one more than the number of tabs).
122
123 Note that the return value will always be at least 1, is the count of
124 actual fields (so last valid offset into ptrs is one less).
125 */
126
127 static int
128 strcut(uschar *str, uschar **ptrs, int nptrs)
129 {
130 uschar *last_sub_start = str;
131 int n;
132
133 for (n = 0; n < nptrs; n++)
134   ptrs[n] = NULL;
135 n = 1;
136
137 while (*str)
138   if (*str++ == '\t')
139     if (n++ <= nptrs)
140       {
141       *ptrs++ = last_sub_start;
142       last_sub_start = str;
143       str[-1] = '\0';
144       }
145
146 /* It's acceptable for the string to end with a tab character.  We see
147 this in AUTH PLAIN without an initial response from the client, which
148 causing us to send "334 " and get the data from the client. */
149 if (n <= nptrs)
150   *ptrs = last_sub_start;
151 else
152   {
153   HDEBUG(D_auth)
154     debug_printf("dovecot: warning: too many results from tab-splitting;"
155                   " saw %d fields, room for %d\n", n, nptrs);
156   n = nptrs;
157   }
158
159 return n <= nptrs ? n : nptrs;
160 }
161
162 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
163 static void
164 debug_strcut(uschar **ptrs, int nlen, int alen)
165 {
166 int i;
167 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
168                 socket_buffer_left, nlen);
169 for (i = 0; i < nlen; i++)
170   debug_printf(" {%s}", ptrs[i]);
171 if (nlen < alen)
172   debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
173 else
174   debug_printf(" (max for capacity)\n");
175 }
176
177 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
178        if (strcmpic(US(str), args[0]) != 0) \
179                goto out; \
180        if (nargs - 1 < (arg_min)) \
181                goto out; \
182        if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
183                goto out; \
184 } while (0)
185
186 #define OUT(msg) do { \
187        auth_defer_msg = (US msg); \
188        goto out; \
189 } while(0)
190
191
192
193 /*************************************************
194 *      "fgets" to read directly from socket      *
195 *************************************************/
196
197 /* Added by PH after a suggestion by Steve Usher because the previous use of
198 C-style buffered I/O gave trouble. */
199
200 static uschar *
201 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
202 {
203 int p = 0;
204 int count = 0;
205
206 for (;;)
207   {
208   if (socket_buffer_left == 0)
209     {
210     if ((socket_buffer_left =
211 #ifndef DISABLE_TLS
212         cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
213 #endif
214         read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
215       if (count == 0)
216         return NULL;
217       else
218         break;
219     p = 0;
220     }
221
222   while (p < socket_buffer_left)
223     {
224     if (count >= n - 1) break;
225     s[count++] = sbuffer[p];
226     if (sbuffer[p++] == '\n') break;
227     }
228
229   memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
230   socket_buffer_left -= p;
231
232   if (s[count-1] == '\n' || count >= n - 1) break;
233   }
234
235 s[count] = '\0';
236 return s;
237 }
238
239
240
241
242 /*************************************************
243 *              Server entry point                *
244 *************************************************/
245
246 int
247 auth_dovecot_server(auth_instance * ablock, uschar * data)
248 {
249 auth_dovecot_options_block *ob =
250        (auth_dovecot_options_block *) ablock->options_block;
251 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
252 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
253 uschar *auth_command;
254 uschar *auth_extra_data = US"";
255 uschar *p;
256 int nargs, tmp;
257 int crequid = 1, ret = DEFER;
258 host_item host;
259 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
260 BOOL found = FALSE, have_mech_line = FALSE;
261
262 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
263
264 if (!data)
265   {
266   ret = FAIL;
267   goto out;
268   }
269
270 /*XXX timeout? */
271 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
272 if (cctx.sock < 0)
273  goto out;
274
275 #ifdef notdef
276 # ifndef DISABLE_TLS
277 if (ob->server_tls)
278   {
279   union sockaddr_46 interface_sock;
280   EXIM_SOCKLEN_T size = sizeof(interface_sock);
281   smtp_connect_args conn_args = { .host = &host };
282   tls_support tls_dummy = { .sni = NULL };
283   uschar * errstr;
284
285   if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
286     conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
287   else
288     {
289     *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
290     goto bad;
291     }
292
293   if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
294     {
295     auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
296     goto out;
297     }
298   }
299 # endif
300 #endif
301
302 auth_defer_msg = US"authentication socket protocol error";
303
304 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
305 for (;;)
306   {
307 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
308   if (!dc_gets(buffer, sizeof(buffer), &cctx))
309     OUT("authentication socket read error or premature eof");
310 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
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("received: '%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("sent: '%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("received: '%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       break;
475
476     case 'F':
477       CHECK_COMMAND("FAIL", 1, -1);
478
479       for (int i = 2; i < nargs && !auth_id_pre; i++)
480         if (Ustrncmp(args[i], US"user=", 5) == 0)
481           {
482           auth_id_pre = args[i] + 5;
483           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
484           expand_nlength[1] = Ustrlen(auth_id_pre);
485           expand_nmax = 1;
486           }
487       ret = FAIL;
488       goto out;
489
490     case 'O':
491       CHECK_COMMAND("OK", 2, -1);
492
493       /* Search for the "user=$USER" string in the args array
494       and return the proper value.  */
495
496       for (int i = 2; i < nargs && !auth_id_pre; i++)
497         if (Ustrncmp(args[i], US"user=", 5) == 0)
498           {
499           auth_id_pre = args[i] + 5;
500           expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
501           expand_nlength[1] = Ustrlen(auth_id_pre);
502           expand_nmax = 1;
503           }
504
505       if (!auth_id_pre)
506         OUT("authentication socket protocol error, username missing");
507
508       auth_defer_msg = NULL;
509       ret = OK;
510       /* fallthrough */
511
512     default:
513       goto out;
514     }
515   }
516
517 out:
518 /* close the socket used by dovecot */
519 #ifndef DISABLE_TLS
520 if (cctx.tls_ctx)
521   tls_close(cctx.tls_ctx, TRUE);
522 #endif
523 if (cctx.sock >= 0)
524   close(cctx.sock);
525
526 /* Expand server_condition as an authorization check */
527 return ret == OK ? auth_check_serv_cond(ablock) : ret;
528 }
529
530
531 #endif   /*!MACRO_PREDEF*/