2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3 * Copyright (c) 2006-2017 The Exim Maintainers
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[] = {
57 (void *)(offsetof(auth_dovecot_options_block, server_socket))
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 NULL, /* server_socket */
78 void auth_dovecot_init(auth_instance *ablock) {}
79 int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
80 int auth_dovecot_client(auth_instance *ablock, void * sx,
81 int timeout, uschar *buffer, int buffsize) {return 0;}
83 #else /*!MACRO_PREDEF*/
86 /* Static variables for reading from the socket */
88 static uschar sbuffer[256];
89 static int socket_buffer_left;
93 /*************************************************
94 * Initialization entry point *
95 *************************************************/
97 /* Called for each instance, after its options have been read, to
98 enable consistency checks to be done, or anything else that needs
101 void 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 ablock->client = FALSE;
111 /*************************************************
112 * "strcut" to split apart server lines *
113 *************************************************/
115 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
116 of a command-name, TAB, and then any parameters, each separated by a TAB.
117 A parameter can be param=value or a bool, just param.
119 This function modifies the original str in-place, inserting NUL characters.
120 It initialises ptrs entries, setting all to NULL and only setting
121 non-NULL N entries, where N is the return value, the number of fields seen
122 (one more than the number of tabs).
124 Note that the return value will always be at least 1, is the count of
125 actual fields (so last valid offset into ptrs is one less).
129 strcut(uschar *str, uschar **ptrs, int nptrs)
131 uschar *last_sub_start = str;
134 for (n = 0; n < nptrs; n++)
142 *ptrs++ = last_sub_start;
143 last_sub_start = str;
147 /* It's acceptable for the string to end with a tab character. We see
148 this in AUTH PLAIN without an initial response from the client, which
149 causing us to send "334 " and get the data from the client. */
151 *ptrs = last_sub_start;
155 debug_printf("dovecot: warning: too many results from tab-splitting;"
156 " saw %d fields, room for %d\n", n, nptrs);
160 return n <= nptrs ? n : nptrs;
163 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
165 debug_strcut(uschar **ptrs, int nlen, int alen)
168 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
169 socket_buffer_left, nlen);
170 for (i = 0; i < nlen; i++)
171 debug_printf(" {%s}", ptrs[i]);
173 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
175 debug_printf(" (max for capacity)\n");
178 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
179 if (strcmpic(US(str), args[0]) != 0) \
181 if (nargs - 1 < (arg_min)) \
183 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
187 #define OUT(msg) do { \
188 auth_defer_msg = (US msg); \
194 /*************************************************
195 * "fgets" to read directly from socket *
196 *************************************************/
198 /* Added by PH after a suggestion by Steve Usher because the previous use of
199 C-style buffered I/O gave trouble. */
202 dc_gets(uschar *s, int n, int fd)
209 if (socket_buffer_left == 0)
211 if ((socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer))) <= 0)
212 if (count == 0) return NULL; else break;
216 while (p < socket_buffer_left)
218 if (count >= n - 1) break;
219 s[count++] = sbuffer[p];
220 if (sbuffer[p++] == '\n') break;
223 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
224 socket_buffer_left -= p;
226 if (s[count-1] == '\n' || count >= n - 1) break;
236 /*************************************************
237 * Server entry point *
238 *************************************************/
241 auth_dovecot_server(auth_instance * ablock, uschar * data)
243 auth_dovecot_options_block *ob =
244 (auth_dovecot_options_block *) ablock->options_block;
245 struct sockaddr_un sa;
246 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
247 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
248 uschar *auth_command;
249 uschar *auth_extra_data = US"";
252 int crequid = 1, cont = 1, fd = -1, ret = DEFER;
253 BOOL found = FALSE, have_mech_line = FALSE;
255 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
263 memset(&sa, 0, sizeof(sa));
264 sa.sun_family = AF_UNIX;
266 /* This was the original code here: it is nonsense because strncpy()
267 does not return an integer. I have converted this to use the function
268 that formats and checks length. PH */
271 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
275 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
278 auth_defer_msg = US"authentication socket path too long";
282 auth_defer_msg = US"authentication socket connection error";
284 if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
287 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
290 auth_defer_msg = US"authentication socket protocol error";
292 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
295 if (!dc_gets(buffer, sizeof(buffer), fd))
296 OUT("authentication socket read error or premature eof");
297 p = buffer + Ustrlen(buffer) - 1;
299 OUT("authentication socket protocol line too long");
302 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
304 nargs = strcut(buffer, args, nelem(args));
306 /* HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args)); */
308 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
309 Exim will need. Original code also failed if Dovecot server sent unknown
310 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
311 /* pdp: note that CUID is a per-connection identifier sent by the server,
312 which increments at server discretion.
313 By contrast, the "id" field of the protocol is a connection-specific request
314 identifier, which needs to be unique per request from the client and is not
315 connected to the CUID value, so we ignore CUID from server. It's purely for
318 if (Ustrcmp(args[0], US"VERSION") == 0)
320 CHECK_COMMAND("VERSION", 2, 2);
321 if (Uatoi(args[1]) != VERSION_MAJOR)
322 OUT("authentication socket protocol version mismatch");
324 else if (Ustrcmp(args[0], US"MECH") == 0)
326 CHECK_COMMAND("MECH", 1, INT_MAX);
327 have_mech_line = TRUE;
328 if (strcmpic(US args[1], ablock->public_name) == 0)
331 else if (Ustrcmp(args[0], US"SPID") == 0)
333 /* Unfortunately the auth protocol handshake wasn't designed well
334 to differentiate between auth-client/userdb/master. auth-userdb
335 and auth-master send VERSION + SPID lines only and nothing
336 afterwards, while auth-client sends VERSION + MECH + SPID +
337 CUID + more. The simplest way that we can determine if we've
338 connected to the correct socket is to see if MECH line exists or
339 not (alternatively we'd have to have a small timeout after SPID
340 to see if CUID is sent or not). */
343 OUT("authentication socket type mismatch"
344 " (connected to auth-master instead of auth-client)");
346 else if (Ustrcmp(args[0], US"DONE") == 0)
348 CHECK_COMMAND("DONE", 0, 0);
355 auth_defer_msg = string_sprintf(
356 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
360 /* Added by PH: data must not contain tab (as it is
361 b64 it shouldn't, but check for safety). */
363 if (Ustrchr(data, '\t') != NULL)
369 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
370 connection is local. */
373 auth_extra_data = string_sprintf("secured\t%s%s",
374 tls_in.certificate_verified ? "valid-client-cert" : "",
375 tls_in.certificate_verified ? "\t" : "");
377 else if ( interface_address
378 && Ustrcmp(sender_host_address, interface_address) == 0)
379 auth_extra_data = US"secured\t";
382 /****************************************************************************
383 The code below was the original code here. It didn't work. A reading of the
384 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
385 this was not right. Maybe something changed. I changed it to move the
386 service indication into the AUTH command, and it seems to be better. PH
388 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
389 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
390 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
391 ablock->public_name, sender_host_address, interface_address,
392 data ? CS data : "");
394 Subsequently, the command was modified to add "secured" and "valid-client-
396 ****************************************************************************/
398 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
399 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
400 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
401 ablock->public_name, auth_extra_data, sender_host_address,
402 interface_address, data);
404 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
405 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
408 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
413 uschar *auth_id_pre = NULL;
415 if (!dc_gets(buffer, sizeof(buffer), fd))
417 auth_defer_msg = US"authentication socket read error or premature eof";
421 buffer[Ustrlen(buffer) - 1] = 0;
422 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
423 nargs = strcut(buffer, args, nelem(args));
425 if (Uatoi(args[1]) != crequid)
426 OUT("authentication socket connection id mismatch");
428 switch (toupper(*args[0]))
431 CHECK_COMMAND("CONT", 1, 2);
433 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
439 /* Added by PH: data must not contain tab (as it is
440 b64 it shouldn't, but check for safety). */
442 if (Ustrchr(data, '\t') != NULL)
448 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
449 if (write(fd, temp, Ustrlen(temp)) < 0)
450 OUT("authentication socket write error");
454 CHECK_COMMAND("FAIL", 1, -1);
456 for (int i = 2; i < nargs && !auth_id_pre; i++)
457 if (Ustrncmp(args[i], US"user=", 5) == 0)
459 auth_id_pre = args[i] + 5;
460 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
461 expand_nlength[1] = Ustrlen(auth_id_pre);
468 CHECK_COMMAND("OK", 2, -1);
470 /* Search for the "user=$USER" string in the args array
471 and return the proper value. */
473 for (int i = 2; i < nargs && !auth_id_pre; i++)
474 if (Ustrncmp(args[i], US"user=", 5) == 0)
476 auth_id_pre = args[i] + 5;
477 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
478 expand_nlength[1] = Ustrlen(auth_id_pre);
483 OUT("authentication socket protocol error, username missing");
494 /* close the socket used by dovecot */
498 /* Expand server_condition as an authorization check */
499 return ret == OK ? auth_check_serv_cond(ablock) : ret;
503 #endif /*!MACRO_PREDEF*/