2 * Copyright (c) The Exim Maintainers 2006 - 2022
3 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published
7 * by the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
11 /* A number of modifications have been made to the original code. Originally I
12 commented them specially, but now they are getting quite extensive, so I have
13 ceased doing that. The biggest change is to use unbuffered I/O on the socket
14 because using C buffered I/O gives problems on some operating systems. PH */
16 /* Protocol specifications:
17 * Dovecot 1, protocol version 1.1
18 * http://wiki.dovecot.org/Authentication%20Protocol
20 * Dovecot 2, protocol version 1.1
21 * http://wiki2.dovecot.org/Design/AuthProtocol
27 #define VERSION_MAJOR 1
28 #define VERSION_MINOR 0
30 /* http://wiki.dovecot.org/Authentication%20Protocol
31 "The maximum line length isn't defined,
32 but it's currently expected to fit into 8192 bytes"
34 #define DOVECOT_AUTH_MAXLINELEN 8192
36 /* This was hard-coded as 8.
37 AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
38 Dovecot 1; Dovecot 2 (same protocol version) defines 9.
40 Master->Server sends {"USER", id, userid} + params, 6 defined.
41 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
43 We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
44 for the command and id, where unspecified might include _at least_ user=...
46 So: allow for more fields than we ever expect to see, while aware that count
47 can go up without changing protocol version.
48 The cost is the length of an array of pointers on the stack.
50 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
52 /* Options specific to the authentication mechanism. */
53 optionlist auth_dovecot_options[] = {
54 { "server_socket", opt_stringptr, OPT_OFF(auth_dovecot_options_block, server_socket) },
55 /*{ "server_tls", opt_bool, OPT_OFF(auth_dovecot_options_block, server_tls) },*/
58 /* Size of the options list. An extern variable has to be used so that its
59 address can appear in the tables drtables.c. */
61 int auth_dovecot_options_count = nelem(auth_dovecot_options);
63 /* Default private options block for the authentication method. */
65 auth_dovecot_options_block auth_dovecot_option_defaults = {
66 .server_socket = NULL,
67 /* .server_tls = FALSE,*/
76 void auth_dovecot_init(auth_instance *ablock) {}
77 int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
78 int auth_dovecot_client(auth_instance *ablock, void * sx,
79 int timeout, uschar *buffer, int buffsize) {return 0;}
81 #else /*!MACRO_PREDEF*/
84 /* Static variables for reading from the socket */
86 static uschar sbuffer[256];
87 static int socket_buffer_left;
91 /*************************************************
92 * Initialization entry point *
93 *************************************************/
95 /* Called for each instance, after its options have been read, to
96 enable consistency checks to be done, or anything else that needs
99 void auth_dovecot_init(auth_instance *ablock)
101 auth_dovecot_options_block *ob =
102 (auth_dovecot_options_block *)(ablock->options_block);
104 if (!ablock->public_name) ablock->public_name = ablock->name;
105 if (ob->server_socket) ablock->server = TRUE;
106 ablock->client = FALSE;
109 /*************************************************
110 * "strcut" to split apart server lines *
111 *************************************************/
113 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
114 of a command-name, TAB, and then any parameters, each separated by a TAB.
115 A parameter can be param=value or a bool, just param.
117 This function modifies the original str in-place, inserting NUL characters.
118 It initialises ptrs entries, setting all to NULL and only setting
119 non-NULL N entries, where N is the return value, the number of fields seen
120 (one more than the number of tabs).
122 Note that the return value will always be at least 1, is the count of
123 actual fields (so last valid offset into ptrs is one less).
127 strcut(uschar *str, uschar **ptrs, int nptrs)
129 uschar *last_sub_start = str;
132 for (n = 0; n < nptrs; n++)
140 *ptrs++ = last_sub_start;
141 last_sub_start = str;
145 /* It's acceptable for the string to end with a tab character. We see
146 this in AUTH PLAIN without an initial response from the client, which
147 causing us to send "334 " and get the data from the client. */
149 *ptrs = last_sub_start;
153 debug_printf("dovecot: warning: too many results from tab-splitting;"
154 " saw %d fields, room for %d\n", n, nptrs);
158 return n <= nptrs ? n : nptrs;
161 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
163 debug_strcut(uschar **ptrs, int nlen, int alen)
166 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
167 socket_buffer_left, nlen);
168 for (i = 0; i < nlen; i++)
169 debug_printf(" {%s}", ptrs[i]);
171 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
173 debug_printf(" (max for capacity)\n");
176 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
177 if (strcmpic(US(str), args[0]) != 0) \
179 if (nargs - 1 < (arg_min)) \
181 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
185 #define OUT(msg) do { \
186 auth_defer_msg = (US msg); \
192 /*************************************************
193 * "fgets" to read directly from socket *
194 *************************************************/
196 /* Added by PH after a suggestion by Steve Usher because the previous use of
197 C-style buffered I/O gave trouble. */
200 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
207 if (socket_buffer_left == 0)
209 if ((socket_buffer_left =
211 cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
213 read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
221 while (p < socket_buffer_left)
223 if (count >= n - 1) break;
224 s[count++] = sbuffer[p];
225 if (sbuffer[p++] == '\n') break;
228 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
229 socket_buffer_left -= p;
231 if (s[count-1] == '\n' || count >= n - 1) break;
241 /*************************************************
242 * Server entry point *
243 *************************************************/
246 auth_dovecot_server(auth_instance * ablock, uschar * data)
248 auth_dovecot_options_block *ob =
249 (auth_dovecot_options_block *) ablock->options_block;
250 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
251 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
252 uschar *auth_command;
253 uschar *auth_extra_data = US"";
256 int crequid = 1, ret = DEFER;
258 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
259 BOOL found = FALSE, have_mech_line = FALSE;
261 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
270 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
278 union sockaddr_46 interface_sock;
279 EXIM_SOCKLEN_T size = sizeof(interface_sock);
280 smtp_connect_args conn_args = { .host = &host };
281 tls_support tls_dummy = { .sni = NULL };
284 if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
285 conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
288 *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
292 if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
294 auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
301 auth_defer_msg = US"authentication socket protocol error";
303 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
306 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
307 if (!dc_gets(buffer, sizeof(buffer), &cctx))
308 OUT("authentication socket read error or premature eof");
309 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
310 p = buffer + Ustrlen(buffer) - 1;
312 OUT("authentication socket protocol line too long");
315 HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
317 nargs = strcut(buffer, args, nelem(args));
319 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
321 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
322 Exim will need. Original code also failed if Dovecot server sent unknown
323 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
324 /* pdp: note that CUID is a per-connection identifier sent by the server,
325 which increments at server discretion.
326 By contrast, the "id" field of the protocol is a connection-specific request
327 identifier, which needs to be unique per request from the client and is not
328 connected to the CUID value, so we ignore CUID from server. It's purely for
331 if (Ustrcmp(args[0], US"VERSION") == 0)
333 CHECK_COMMAND("VERSION", 2, 2);
334 if (Uatoi(args[1]) != VERSION_MAJOR)
335 OUT("authentication socket protocol version mismatch");
337 else if (Ustrcmp(args[0], US"MECH") == 0)
339 CHECK_COMMAND("MECH", 1, INT_MAX);
340 have_mech_line = TRUE;
341 if (strcmpic(US args[1], ablock->public_name) == 0)
344 else if (Ustrcmp(args[0], US"SPID") == 0)
346 /* Unfortunately the auth protocol handshake wasn't designed well
347 to differentiate between auth-client/userdb/master. auth-userdb
348 and auth-master send VERSION + SPID lines only and nothing
349 afterwards, while auth-client sends VERSION + MECH + SPID +
350 CUID + more. The simplest way that we can determine if we've
351 connected to the correct socket is to see if MECH line exists or
352 not (alternatively we'd have to have a small timeout after SPID
353 to see if CUID is sent or not). */
356 OUT("authentication socket type mismatch"
357 " (connected to auth-master instead of auth-client)");
359 else if (Ustrcmp(args[0], US"DONE") == 0)
361 CHECK_COMMAND("DONE", 0, 0);
368 auth_defer_msg = string_sprintf(
369 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
373 /* Added by PH: data must not contain tab (as it is
374 b64 it shouldn't, but check for safety). */
376 if (Ustrchr(data, '\t') != NULL)
382 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
383 connection is local. */
386 auth_extra_data = string_sprintf("secured\t%s%s",
387 tls_in.certificate_verified ? "valid-client-cert" : "",
388 tls_in.certificate_verified ? "\t" : "");
390 else if ( interface_address
391 && Ustrcmp(sender_host_address, interface_address) == 0)
392 auth_extra_data = US"secured\t";
395 /****************************************************************************
396 The code below was the original code here. It didn't work. A reading of the
397 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
398 this was not right. Maybe something changed. I changed it to move the
399 service indication into the AUTH command, and it seems to be better. PH
401 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
402 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
403 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
404 ablock->public_name, sender_host_address, interface_address,
405 data ? CS data : "");
407 Subsequently, the command was modified to add "secured" and "valid-client-
409 ****************************************************************************/
411 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
412 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
413 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
414 ablock->public_name, auth_extra_data, sender_host_address,
415 interface_address, data);
419 cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
421 write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
422 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
425 HDEBUG(D_auth) debug_printf("sent: '%s'\n", auth_command);
430 uschar *auth_id_pre = NULL;
432 if (!dc_gets(buffer, sizeof(buffer), &cctx))
434 auth_defer_msg = US"authentication socket read error or premature eof";
438 buffer[Ustrlen(buffer) - 1] = 0;
439 HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
440 nargs = strcut(buffer, args, nelem(args));
441 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
443 if (Uatoi(args[1]) != crequid)
444 OUT("authentication socket connection id mismatch");
446 switch (toupper(*args[0]))
449 CHECK_COMMAND("CONT", 1, 2);
451 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
457 /* Added by PH: data must not contain tab (as it is
458 b64 it shouldn't, but check for safety). */
460 if (Ustrchr(data, '\t') != NULL)
466 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
469 cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
471 write(cctx.sock, temp, Ustrlen(temp))) < 0)
472 OUT("authentication socket write error");
476 CHECK_COMMAND("FAIL", 1, -1);
478 for (int i = 2; i < nargs && !auth_id_pre; i++)
479 if (Ustrncmp(args[i], US"user=", 5) == 0)
481 auth_id_pre = args[i] + 5;
482 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
483 expand_nlength[1] = Ustrlen(auth_id_pre);
490 CHECK_COMMAND("OK", 2, -1);
492 /* Search for the "user=$USER" string in the args array
493 and return the proper value. */
495 for (int i = 2; i < nargs && !auth_id_pre; i++)
496 if (Ustrncmp(args[i], US"user=", 5) == 0)
498 auth_id_pre = args[i] + 5;
499 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
500 expand_nlength[1] = Ustrlen(auth_id_pre);
505 OUT("authentication socket protocol error, username missing");
507 auth_defer_msg = NULL;
517 /* close the socket used by dovecot */
520 tls_close(cctx.tls_ctx, TRUE);
525 /* Expand server_condition as an authorization check */
526 return ret == OK ? auth_check_serv_cond(ablock) : ret;
530 #endif /*!MACRO_PREDEF*/