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
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
28 #define VERSION_MAJOR 1
29 #define VERSION_MINOR 0
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"
35 #define DOVECOT_AUTH_MAXLINELEN 8192
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.
41 Master->Server sends {"USER", id, userid} + params, 6 defined.
42 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
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=...
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.
51 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
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) },*/
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. */
62 int auth_dovecot_options_count = nelem(auth_dovecot_options);
64 /* Default private options block for the authentication method. */
66 auth_dovecot_options_block auth_dovecot_option_defaults = {
67 .server_socket = NULL,
68 /* .server_tls = FALSE,*/
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;}
82 #else /*!MACRO_PREDEF*/
85 /* Static variables for reading from the socket */
87 static uschar sbuffer[256];
88 static int socket_buffer_left;
92 /*************************************************
93 * Initialization entry point *
94 *************************************************/
96 /* Called for each instance, after its options have been read, to
97 enable consistency checks to be done, or anything else that needs
101 auth_dovecot_init(auth_instance * ablock)
103 auth_dovecot_options_block * ob =
104 (auth_dovecot_options_block *)(ablock->options_block);
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;
112 /*************************************************
113 * "strcut" to split apart server lines *
114 *************************************************/
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.
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).
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).
130 strcut(uschar *str, uschar **ptrs, int nptrs)
132 uschar *last_sub_start = str;
135 for (n = 0; n < nptrs; n++)
143 *ptrs++ = last_sub_start;
144 last_sub_start = str;
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. */
152 *ptrs = last_sub_start;
156 debug_printf("dovecot: warning: too many results from tab-splitting;"
157 " saw %d fields, room for %d\n", n, nptrs);
161 return n <= nptrs ? n : nptrs;
164 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
166 debug_strcut(uschar **ptrs, int nlen, int alen)
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]);
174 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
176 debug_printf(" (max for capacity)\n");
179 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
180 if (strcmpic(US(str), args[0]) != 0) \
182 if (nargs - 1 < (arg_min)) \
184 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
188 #define OUT(msg) do { \
189 auth_defer_msg = (US msg); \
195 /*************************************************
196 * "fgets" to read directly from socket *
197 *************************************************/
199 /* Added by PH after a suggestion by Steve Usher because the previous use of
200 C-style buffered I/O gave trouble. */
203 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
210 if (socket_buffer_left == 0)
212 if ((socket_buffer_left =
214 cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
216 read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
224 while (p < socket_buffer_left)
226 if (count >= n - 1) break;
227 s[count++] = sbuffer[p];
228 if (sbuffer[p++] == '\n') break;
231 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
232 socket_buffer_left -= p;
234 if (s[count-1] == '\n' || count >= n - 1) break;
244 /*************************************************
245 * Server entry point *
246 *************************************************/
249 auth_dovecot_server(auth_instance * ablock, uschar * data)
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"";
259 int crequid = 1, ret = DEFER;
261 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
262 BOOL found = FALSE, have_mech_line = FALSE;
264 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
273 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
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 };
287 if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
288 conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
291 *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
295 if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
297 auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
304 auth_defer_msg = US"authentication socket protocol error";
306 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
309 if (!dc_gets(buffer, sizeof(buffer), &cctx))
310 OUT("authentication socket read error or premature eof");
311 p = buffer + Ustrlen(buffer) - 1;
313 OUT("authentication socket protocol line too long");
316 HDEBUG(D_auth) debug_printf(" DOVECOT<< '%s'\n", buffer);
318 nargs = strcut(buffer, args, nelem(args));
320 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
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
332 if (Ustrcmp(args[0], US"VERSION") == 0)
334 CHECK_COMMAND("VERSION", 2, 2);
335 if (Uatoi(args[1]) != VERSION_MAJOR)
336 OUT("authentication socket protocol version mismatch");
338 else if (Ustrcmp(args[0], US"MECH") == 0)
340 CHECK_COMMAND("MECH", 1, INT_MAX);
341 have_mech_line = TRUE;
342 if (strcmpic(US args[1], ablock->public_name) == 0)
345 else if (Ustrcmp(args[0], US"SPID") == 0)
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). */
357 OUT("authentication socket type mismatch"
358 " (connected to auth-master instead of auth-client)");
360 else if (Ustrcmp(args[0], US"DONE") == 0)
362 CHECK_COMMAND("DONE", 0, 0);
369 auth_defer_msg = string_sprintf(
370 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
374 /* Added by PH: data must not contain tab (as it is
375 b64 it shouldn't, but check for safety). */
377 if (Ustrchr(data, '\t') != NULL)
383 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
384 connection is local. */
387 auth_extra_data = string_sprintf("secured\t%s%s",
388 tls_in.certificate_verified ? "valid-client-cert" : "",
389 tls_in.certificate_verified ? "\t" : "");
391 else if ( interface_address
392 && Ustrcmp(sender_host_address, interface_address) == 0)
393 auth_extra_data = US"secured\t";
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
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 : "");
408 Subsequently, the command was modified to add "secured" and "valid-client-
410 ****************************************************************************/
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);
420 cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
422 write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
423 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
426 HDEBUG(D_auth) debug_printf(" DOVECOT>> '%s'\n", auth_command);
431 uschar * auth_id_pre = NULL;
433 if (!dc_gets(buffer, sizeof(buffer), &cctx))
435 auth_defer_msg = US"authentication socket read error or premature eof";
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));
444 if (Uatoi(args[1]) != crequid)
445 OUT("authentication socket connection id mismatch");
447 switch (toupper(*args[0]))
450 CHECK_COMMAND("CONT", 1, 2);
452 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
458 /* Added by PH: data must not contain tab (as it is
459 b64 it shouldn't, but check for safety). */
461 if (Ustrchr(data, '\t') != NULL)
467 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
470 cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
472 write(cctx.sock, temp, Ustrlen(temp))) < 0)
473 OUT("authentication socket write error");
475 HDEBUG(D_auth) debug_printf(" DOVECOT>> '%s'\n", temp);
479 CHECK_COMMAND("FAIL", 1, -1);
481 for (int i = 2; i < nargs && !auth_id_pre; i++)
482 if (Ustrncmp(args[i], US"user=", 5) == 0)
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);
493 CHECK_COMMAND("OK", 2, -1);
495 /* Search for the "user=$USER" string in the args array
496 and return the proper value. */
498 for (int i = 2; i < nargs && !auth_id_pre; i++)
499 if (Ustrncmp(args[i], US"user=", 5) == 0)
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);
508 OUT("authentication socket protocol error, username missing");
510 auth_defer_msg = NULL;
520 /* close the socket used by dovecot */
523 tls_close(cctx.tls_ctx, TRUE);
528 /* Expand server_condition as an authorization check */
529 if (ret == OK) ret = auth_check_serv_cond(ablock);
531 HDEBUG(D_auth) debug_printf("dovecot auth ret: %s\n", rc_names[ret]);
536 #endif /*!MACRO_PREDEF*/