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
100 void auth_dovecot_init(auth_instance *ablock)
102 auth_dovecot_options_block *ob =
103 (auth_dovecot_options_block *)(ablock->options_block);
105 if (!ablock->public_name) ablock->public_name = ablock->name;
106 if (ob->server_socket) ablock->server = TRUE;
107 ablock->client = FALSE;
110 /*************************************************
111 * "strcut" to split apart server lines *
112 *************************************************/
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.
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).
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).
128 strcut(uschar *str, uschar **ptrs, int nptrs)
130 uschar *last_sub_start = str;
133 for (n = 0; n < nptrs; n++)
141 *ptrs++ = last_sub_start;
142 last_sub_start = str;
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. */
150 *ptrs = last_sub_start;
154 debug_printf("dovecot: warning: too many results from tab-splitting;"
155 " saw %d fields, room for %d\n", n, nptrs);
159 return n <= nptrs ? n : nptrs;
162 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
164 debug_strcut(uschar **ptrs, int nlen, int alen)
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]);
172 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
174 debug_printf(" (max for capacity)\n");
177 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
178 if (strcmpic(US(str), args[0]) != 0) \
180 if (nargs - 1 < (arg_min)) \
182 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
186 #define OUT(msg) do { \
187 auth_defer_msg = (US msg); \
193 /*************************************************
194 * "fgets" to read directly from socket *
195 *************************************************/
197 /* Added by PH after a suggestion by Steve Usher because the previous use of
198 C-style buffered I/O gave trouble. */
201 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
208 if (socket_buffer_left == 0)
210 if ((socket_buffer_left =
212 cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
214 read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
222 while (p < socket_buffer_left)
224 if (count >= n - 1) break;
225 s[count++] = sbuffer[p];
226 if (sbuffer[p++] == '\n') break;
229 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
230 socket_buffer_left -= p;
232 if (s[count-1] == '\n' || count >= n - 1) break;
242 /*************************************************
243 * Server entry point *
244 *************************************************/
247 auth_dovecot_server(auth_instance * ablock, uschar * data)
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"";
257 int crequid = 1, ret = DEFER;
259 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
260 BOOL found = FALSE, have_mech_line = FALSE;
262 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
271 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
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 };
285 if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
286 conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
289 *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
293 if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
295 auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
302 auth_defer_msg = US"authentication socket protocol error";
304 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
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;
313 OUT("authentication socket protocol line too long");
316 HDEBUG(D_auth) debug_printf("received: '%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("sent: '%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("received: '%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");
477 CHECK_COMMAND("FAIL", 1, -1);
479 for (int i = 2; i < nargs && !auth_id_pre; i++)
480 if (Ustrncmp(args[i], US"user=", 5) == 0)
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);
491 CHECK_COMMAND("OK", 2, -1);
493 /* Search for the "user=$USER" string in the args array
494 and return the proper value. */
496 for (int i = 2; i < nargs && !auth_id_pre; i++)
497 if (Ustrncmp(args[i], US"user=", 5) == 0)
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);
506 OUT("authentication socket protocol error, username missing");
508 auth_defer_msg = NULL;
518 /* close the socket used by dovecot */
521 tls_close(cctx.tls_ctx, TRUE);
526 /* Expand server_condition as an authorization check */
527 return ret == OK ? auth_check_serv_cond(ablock) : ret;
531 #endif /*!MACRO_PREDEF*/