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 =
65 sizeof(auth_dovecot_options) / sizeof(optionlist);
67 /* Default private options block for the authentication method. */
69 auth_dovecot_options_block auth_dovecot_option_defaults = {
70 NULL, /* server_socket */
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
102 void auth_dovecot_init(auth_instance *ablock)
104 auth_dovecot_options_block *ob =
105 (auth_dovecot_options_block *)(ablock->options_block);
107 if (ablock->public_name == NULL)
108 ablock->public_name = ablock->name;
109 if (ob->server_socket != NULL)
110 ablock->server = TRUE;
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++)
147 *ptrs++ = last_sub_start;
148 last_sub_start = str + 1;
156 /* It's acceptable for the string to end with a tab character. We see
157 this in AUTH PLAIN without an initial response from the client, which
158 causing us to send "334 " and get the data from the client. */
160 *ptrs = last_sub_start;
163 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
167 return n <= nptrs ? n : nptrs;
170 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
172 debug_strcut(uschar **ptrs, int nlen, int alen)
175 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
176 socket_buffer_left, nlen);
177 for (i = 0; i < nlen; i++)
178 debug_printf(" {%s}", ptrs[i]);
180 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
182 debug_printf(" (max for capacity)\n");
185 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
186 if (strcmpic(US(str), args[0]) != 0) \
188 if (nargs - 1 < (arg_min)) \
190 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
194 #define OUT(msg) do { \
195 auth_defer_msg = (US msg); \
201 /*************************************************
202 * "fgets" to read directly from socket *
203 *************************************************/
205 /* Added by PH after a suggestion by Steve Usher because the previous use of
206 C-style buffered I/O gave trouble. */
209 dc_gets(uschar *s, int n, int fd)
216 if (socket_buffer_left == 0)
218 if ((socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer))) <= 0)
219 if (count == 0) return NULL; else break;
223 while (p < socket_buffer_left)
225 if (count >= n - 1) break;
226 s[count++] = sbuffer[p];
227 if (sbuffer[p++] == '\n') break;
230 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
231 socket_buffer_left -= p;
233 if (s[count-1] == '\n' || count >= n - 1) break;
243 /*************************************************
244 * Server entry point *
245 *************************************************/
248 auth_dovecot_server(auth_instance * ablock, uschar * data)
250 auth_dovecot_options_block *ob =
251 (auth_dovecot_options_block *) ablock->options_block;
252 struct sockaddr_un sa;
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, cont = 1, fd = -1, ret = DEFER;
260 BOOL found = FALSE, have_mech_line = FALSE;
262 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
270 memset(&sa, 0, sizeof(sa));
271 sa.sun_family = AF_UNIX;
273 /* This was the original code here: it is nonsense because strncpy()
274 does not return an integer. I have converted this to use the function
275 that formats and checks length. PH */
278 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
282 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
285 auth_defer_msg = US"authentication socket path too long";
289 auth_defer_msg = US"authentication socket connection error";
291 if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
294 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
297 auth_defer_msg = US"authentication socket protocol error";
299 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
302 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
303 OUT("authentication socket read error or premature eof");
304 p = buffer + Ustrlen(buffer) - 1;
306 OUT("authentication socket protocol line too long");
309 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
311 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
313 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
315 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
316 Exim will need. Original code also failed if Dovecot server sent unknown
317 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
318 /* pdp: note that CUID is a per-connection identifier sent by the server,
319 which increments at server discretion.
320 By contrast, the "id" field of the protocol is a connection-specific request
321 identifier, which needs to be unique per request from the client and is not
322 connected to the CUID value, so we ignore CUID from server. It's purely for
325 if (Ustrcmp(args[0], US"VERSION") == 0)
327 CHECK_COMMAND("VERSION", 2, 2);
328 if (Uatoi(args[1]) != VERSION_MAJOR)
329 OUT("authentication socket protocol version mismatch");
331 else if (Ustrcmp(args[0], US"MECH") == 0)
333 CHECK_COMMAND("MECH", 1, INT_MAX);
334 have_mech_line = TRUE;
335 if (strcmpic(US args[1], ablock->public_name) == 0)
338 else if (Ustrcmp(args[0], US"SPID") == 0)
340 /* Unfortunately the auth protocol handshake wasn't designed well
341 to differentiate between auth-client/userdb/master. auth-userdb
342 and auth-master send VERSION + SPID lines only and nothing
343 afterwards, while auth-client sends VERSION + MECH + SPID +
344 CUID + more. The simplest way that we can determine if we've
345 connected to the correct socket is to see if MECH line exists or
346 not (alternatively we'd have to have a small timeout after SPID
347 to see if CUID is sent or not). */
350 OUT("authentication socket type mismatch"
351 " (connected to auth-master instead of auth-client)");
353 else if (Ustrcmp(args[0], US"DONE") == 0)
355 CHECK_COMMAND("DONE", 0, 0);
362 auth_defer_msg = string_sprintf(
363 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
367 /* Added by PH: data must not contain tab (as it is
368 b64 it shouldn't, but check for safety). */
370 if (Ustrchr(data, '\t') != NULL)
376 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
377 connection is local. */
379 if (tls_in.cipher != NULL)
380 auth_extra_data = string_sprintf("secured\t%s%s",
381 tls_in.certificate_verified? "valid-client-cert" : "",
382 tls_in.certificate_verified? "\t" : "");
384 else if ( interface_address != NULL
385 && Ustrcmp(sender_host_address, interface_address) == 0)
386 auth_extra_data = US"secured\t";
389 /****************************************************************************
390 The code below was the original code here. It didn't work. A reading of the
391 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
392 this was not right. Maybe something changed. I changed it to move the
393 service indication into the AUTH command, and it seems to be better. PH
395 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
396 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
397 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
398 ablock->public_name, sender_host_address, interface_address,
399 data ? CS data : "");
401 Subsequently, the command was modified to add "secured" and "valid-client-
403 ****************************************************************************/
405 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
406 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
407 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
408 ablock->public_name, auth_extra_data, sender_host_address,
409 interface_address, data);
411 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
412 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
415 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
420 uschar *auth_id_pre = NULL;
423 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
425 auth_defer_msg = US"authentication socket read error or premature eof";
429 buffer[Ustrlen(buffer) - 1] = 0;
430 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
431 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
433 if (Uatoi(args[1]) != crequid)
434 OUT("authentication socket connection id mismatch");
436 switch (toupper(*args[0]))
439 CHECK_COMMAND("CONT", 1, 2);
441 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
447 /* Added by PH: data must not contain tab (as it is
448 b64 it shouldn't, but check for safety). */
450 if (Ustrchr(data, '\t') != NULL)
456 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
457 if (write(fd, temp, Ustrlen(temp)) < 0)
458 OUT("authentication socket write error");
462 CHECK_COMMAND("FAIL", 1, -1);
464 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
466 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
468 auth_id_pre = args[i]+5;
469 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
470 expand_nlength[1] = Ustrlen(auth_id_pre);
479 CHECK_COMMAND("OK", 2, -1);
481 /* Search for the "user=$USER" string in the args array
482 and return the proper value. */
484 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
486 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
488 auth_id_pre = args[i]+5;
489 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
490 expand_nlength[1] = Ustrlen(auth_id_pre);
495 if (auth_id_pre == NULL)
496 OUT("authentication socket protocol error, username missing");
507 /* close the socket used by dovecot */
511 /* Expand server_condition as an authorization check */
512 return ret == OK ? auth_check_serv_cond(ablock) : ret;
516 #endif /*!MACRO_PREDEF*/