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
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.
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 */
17 /* Protocol specifications:
18 * Dovecot 1, protocol version 1.1
19 * http://wiki.dovecot.org/Authentication%20Protocol
21 * Dovecot 2, protocol version 1.1
22 * http://wiki2.dovecot.org/Design/AuthProtocol
27 #ifdef AUTH_DOVECOT /* Remainder of file */
30 #define VERSION_MAJOR 1
31 #define VERSION_MINOR 0
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"
37 #define DOVECOT_AUTH_MAXLINELEN 8192
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.
43 Master->Server sends {"USER", id, userid} + params, 6 defined.
44 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
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=...
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.
53 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
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) },*/
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. */
64 int auth_dovecot_options_count = nelem(auth_dovecot_options);
66 /* Default private options block for the authentication method. */
68 auth_dovecot_options_block auth_dovecot_option_defaults = {
69 .server_socket = NULL,
70 /* .server_tls = FALSE,*/
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;}
84 #else /*!MACRO_PREDEF*/
87 /* Static variables for reading from the socket */
89 static uschar sbuffer[256];
90 static int socket_buffer_left;
94 /*************************************************
95 * Initialization entry point *
96 *************************************************/
98 /* Called for each instance, after its options have been read, to
99 enable consistency checks to be done, or anything else that needs
103 auth_dovecot_init(auth_instance * ablock)
105 auth_dovecot_options_block * ob =
106 (auth_dovecot_options_block *)(ablock->options_block);
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;
114 /*************************************************
115 * "strcut" to split apart server lines *
116 *************************************************/
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.
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).
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).
132 strcut(uschar *str, uschar **ptrs, int nptrs)
134 uschar *last_sub_start = str;
137 for (n = 0; n < nptrs; n++)
145 *ptrs++ = last_sub_start;
146 last_sub_start = str;
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. */
154 *ptrs = last_sub_start;
158 debug_printf("dovecot: warning: too many results from tab-splitting;"
159 " saw %d fields, room for %d\n", n, nptrs);
163 return n <= nptrs ? n : nptrs;
166 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
168 debug_strcut(uschar **ptrs, int nlen, int alen)
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]);
176 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
178 debug_printf(" (max for capacity)\n");
181 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
182 if (strcmpic(US(str), args[0]) != 0) \
184 if (nargs - 1 < (arg_min)) \
186 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
190 #define OUT(msg) do { \
191 auth_defer_msg = (US msg); \
197 /*************************************************
198 * "fgets" to read directly from socket *
199 *************************************************/
201 /* Added by PH after a suggestion by Steve Usher because the previous use of
202 C-style buffered I/O gave trouble. */
205 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
212 if (socket_buffer_left == 0)
214 if ((socket_buffer_left =
216 cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
218 read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
226 while (p < socket_buffer_left)
228 if (count >= n - 1) break;
229 s[count++] = sbuffer[p];
230 if (sbuffer[p++] == '\n') break;
233 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
234 socket_buffer_left -= p;
236 if (s[count-1] == '\n' || count >= n - 1) break;
246 /*************************************************
247 * Server entry point *
248 *************************************************/
251 auth_dovecot_server(auth_instance * ablock, uschar * data)
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"";
261 int crequid = 1, ret = DEFER;
263 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
264 BOOL found = FALSE, have_mech_line = FALSE;
266 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
275 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
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 };
289 if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
290 conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
293 *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
297 if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
299 auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
306 auth_defer_msg = US"authentication socket protocol error";
308 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
311 if (!dc_gets(buffer, sizeof(buffer), &cctx))
312 OUT("authentication socket read error or premature eof");
313 p = buffer + Ustrlen(buffer) - 1;
315 OUT("authentication socket protocol line too long");
318 HDEBUG(D_auth) debug_printf(" DOVECOT<< '%s'\n", buffer);
320 nargs = strcut(buffer, args, nelem(args));
322 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
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
334 if (Ustrcmp(args[0], US"VERSION") == 0)
336 CHECK_COMMAND("VERSION", 2, 2);
337 if (Uatoi(args[1]) != VERSION_MAJOR)
338 OUT("authentication socket protocol version mismatch");
340 else if (Ustrcmp(args[0], US"MECH") == 0)
342 CHECK_COMMAND("MECH", 1, INT_MAX);
343 have_mech_line = TRUE;
344 if (strcmpic(US args[1], ablock->public_name) == 0)
347 else if (Ustrcmp(args[0], US"SPID") == 0)
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). */
359 OUT("authentication socket type mismatch"
360 " (connected to auth-master instead of auth-client)");
362 else if (Ustrcmp(args[0], US"DONE") == 0)
364 CHECK_COMMAND("DONE", 0, 0);
371 auth_defer_msg = string_sprintf(
372 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
376 /* Added by PH: data must not contain tab (as it is
377 b64 it shouldn't, but check for safety). */
379 if (Ustrchr(data, '\t') != NULL)
385 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
386 connection is local. */
389 auth_extra_data = string_sprintf("secured\t%s%s",
390 tls_in.certificate_verified ? "valid-client-cert" : "",
391 tls_in.certificate_verified ? "\t" : "");
393 else if ( interface_address
394 && Ustrcmp(sender_host_address, interface_address) == 0)
395 auth_extra_data = US"secured\t";
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
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 : "");
410 Subsequently, the command was modified to add "secured" and "valid-client-
412 ****************************************************************************/
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);
422 cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
424 write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
425 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
428 HDEBUG(D_auth) debug_printf(" DOVECOT>> '%s'\n", auth_command);
433 uschar * auth_id_pre = NULL;
435 if (!dc_gets(buffer, sizeof(buffer), &cctx))
437 auth_defer_msg = US"authentication socket read error or premature eof";
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));
446 if (Uatoi(args[1]) != crequid)
447 OUT("authentication socket connection id mismatch");
449 switch (toupper(*args[0]))
452 CHECK_COMMAND("CONT", 1, 2);
454 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
460 /* Added by PH: data must not contain tab (as it is
461 b64 it shouldn't, but check for safety). */
463 if (Ustrchr(data, '\t') != NULL)
469 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
472 cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
474 write(cctx.sock, temp, Ustrlen(temp))) < 0)
475 OUT("authentication socket write error");
477 HDEBUG(D_auth) debug_printf(" DOVECOT>> '%s'\n", temp);
481 CHECK_COMMAND("FAIL", 1, -1);
483 for (int i = 2; i < nargs && !auth_id_pre; i++)
484 if (Ustrncmp(args[i], US"user=", 5) == 0)
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);
495 CHECK_COMMAND("OK", 2, -1);
497 /* Search for the "user=$USER" string in the args array
498 and return the proper value. */
500 for (int i = 2; i < nargs && !auth_id_pre; i++)
501 if (Ustrncmp(args[i], US"user=", 5) == 0)
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);
510 OUT("authentication socket protocol error, username missing");
512 auth_defer_msg = NULL;
522 /* close the socket used by dovecot */
525 tls_close(cctx.tls_ctx, TRUE);
530 /* Expand server_condition as an authorization check */
531 if (ret == OK) ret = auth_check_serv_cond(ablock);
533 HDEBUG(D_auth) debug_printf("dovecot auth ret: %s\n", rc_names[ret]);
538 #endif /*!MACRO_PREDEF*/
539 #endif /*AUTH_DOVECOT*/
540 /* end of auths/dovecot.c */