2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3 * Copyright (c) 2006-2014 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 */
74 /* Static variables for reading from the socket */
76 static uschar sbuffer[256];
77 static int socket_buffer_left;
81 /*************************************************
82 * Initialization entry point *
83 *************************************************/
85 /* Called for each instance, after its options have been read, to
86 enable consistency checks to be done, or anything else that needs
89 void auth_dovecot_init(auth_instance *ablock)
91 auth_dovecot_options_block *ob =
92 (auth_dovecot_options_block *)(ablock->options_block);
94 if (ablock->public_name == NULL)
95 ablock->public_name = ablock->name;
96 if (ob->server_socket != NULL)
97 ablock->server = TRUE;
98 ablock->client = FALSE;
101 /*************************************************
102 * "strcut" to split apart server lines *
103 *************************************************/
105 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
106 of a command-name, TAB, and then any parameters, each separated by a TAB.
107 A parameter can be param=value or a bool, just param.
109 This function modifies the original str in-place, inserting NUL characters.
110 It initialises ptrs entries, setting all to NULL and only setting
111 non-NULL N entries, where N is the return value, the number of fields seen
112 (one more than the number of tabs).
114 Note that the return value will always be at least 1, is the count of
115 actual fields (so last valid offset into ptrs is one less).
119 strcut(uschar *str, uschar **ptrs, int nptrs)
121 uschar *last_sub_start = str;
124 for (n = 0; n < nptrs; n++)
131 *ptrs++ = last_sub_start;
132 last_sub_start = str + 1;
140 /* It's acceptable for the string to end with a tab character. We see
141 this in AUTH PLAIN without an initial response from the client, which
142 causing us to send "334 " and get the data from the client. */
144 *ptrs = last_sub_start;
146 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
150 return n <= nptrs ? n : nptrs;
153 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
155 debug_strcut(uschar **ptrs, int nlen, int alen)
158 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
159 socket_buffer_left, nlen);
160 for (i = 0; i < nlen; i++) {
161 debug_printf(" {%s}", ptrs[i]);
164 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
166 debug_printf(" (max for capacity)\n");
169 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
170 if (strcmpic(US(str), args[0]) != 0) \
172 if (nargs - 1 < (arg_min)) \
174 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
178 #define OUT(msg) do { \
179 auth_defer_msg = (US msg); \
185 /*************************************************
186 * "fgets" to read directly from socket *
187 *************************************************/
189 /* Added by PH after a suggestion by Steve Usher because the previous use of
190 C-style buffered I/O gave trouble. */
193 dc_gets(uschar *s, int n, int fd)
200 if (socket_buffer_left == 0)
202 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
203 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
207 while (p < socket_buffer_left)
209 if (count >= n - 1) break;
210 s[count++] = sbuffer[p];
211 if (sbuffer[p++] == '\n') break;
214 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
215 socket_buffer_left -= p;
217 if (s[count-1] == '\n' || count >= n - 1) break;
227 /*************************************************
228 * Server entry point *
229 *************************************************/
231 int auth_dovecot_server(auth_instance *ablock, uschar *data)
233 auth_dovecot_options_block *ob =
234 (auth_dovecot_options_block *)(ablock->options_block);
235 struct sockaddr_un sa;
236 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
237 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
238 uschar *auth_command;
239 uschar *auth_extra_data = US"";
242 int crequid = 1, cont = 1, fd, ret = DEFER;
243 BOOL found = FALSE, have_mech_line = FALSE;
245 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
247 memset(&sa, 0, sizeof(sa));
248 sa.sun_family = AF_UNIX;
250 /* This was the original code here: it is nonsense because strncpy()
251 does not return an integer. I have converted this to use the function
252 that formats and checks length. PH */
255 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
258 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
259 ob->server_socket)) {
260 auth_defer_msg = US"authentication socket path too long";
264 auth_defer_msg = US"authentication socket connection error";
266 fd = socket(PF_UNIX, SOCK_STREAM, 0);
270 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
273 auth_defer_msg = US"authentication socket protocol error";
275 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
277 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
278 OUT("authentication socket read error or premature eof");
279 p = buffer + Ustrlen(buffer) - 1;
281 OUT("authentication socket protocol line too long");
284 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
285 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
286 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
288 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
289 Exim will need. Original code also failed if Dovecot server sent unknown
290 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
291 /* pdp: note that CUID is a per-connection identifier sent by the server,
292 which increments at server discretion.
293 By contrast, the "id" field of the protocol is a connection-specific request
294 identifier, which needs to be unique per request from the client and is not
295 connected to the CUID value, so we ignore CUID from server. It's purely for
297 if (Ustrcmp(args[0], US"VERSION") == 0) {
298 CHECK_COMMAND("VERSION", 2, 2);
299 if (Uatoi(args[1]) != VERSION_MAJOR)
300 OUT("authentication socket protocol version mismatch");
301 } else if (Ustrcmp(args[0], US"MECH") == 0) {
302 CHECK_COMMAND("MECH", 1, INT_MAX);
303 have_mech_line = TRUE;
304 if (strcmpic(US args[1], ablock->public_name) == 0)
306 } else if (Ustrcmp(args[0], US"SPID") == 0) {
307 /* Unfortunately the auth protocol handshake wasn't designed well
308 to differentiate between auth-client/userdb/master. auth-userdb
309 and auth-master send VERSION + SPID lines only and nothing
310 afterwards, while auth-client sends VERSION + MECH + SPID +
311 CUID + more. The simplest way that we can determine if we've
312 connected to the correct socket is to see if MECH line exists or
313 not (alternatively we'd have to have a small timeout after SPID
314 to see if CUID is sent or not). */
316 OUT("authentication socket type mismatch (connected to auth-master instead of auth-client)");
317 } else if (Ustrcmp(args[0], US"DONE") == 0) {
318 CHECK_COMMAND("DONE", 0, 0);
324 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
328 /* Added by PH: data must not contain tab (as it is
329 b64 it shouldn't, but check for safety). */
331 if (Ustrchr(data, '\t') != NULL) {
336 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
337 connection is local. */
339 if (tls_in.cipher != NULL)
340 auth_extra_data = string_sprintf("secured\t%s%s",
341 tls_in.certificate_verified? "valid-client-cert" : "",
342 tls_in.certificate_verified? "\t" : "");
343 else if (interface_address != NULL &&
344 Ustrcmp(sender_host_address, interface_address) == 0)
345 auth_extra_data = US"secured\t";
348 /****************************************************************************
349 The code below was the original code here. It didn't work. A reading of the
350 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
351 this was not right. Maybe something changed. I changed it to move the
352 service indication into the AUTH command, and it seems to be better. PH
354 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
355 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
356 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
357 ablock->public_name, sender_host_address, interface_address,
358 data ? (char *) data : "");
360 Subsequently, the command was modified to add "secured" and "valid-client-
362 ****************************************************************************/
364 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
365 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
366 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
367 ablock->public_name, auth_extra_data, sender_host_address,
368 interface_address, data ? (char *) data : "");
370 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
371 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
374 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
378 uschar *auth_id_pre = NULL;
381 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
382 auth_defer_msg = US"authentication socket read error or premature eof";
386 buffer[Ustrlen(buffer) - 1] = 0;
387 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
388 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
390 if (Uatoi(args[1]) != crequid)
391 OUT("authentication socket connection id mismatch");
393 switch (toupper(*args[0])) {
395 CHECK_COMMAND("CONT", 1, 2);
397 tmp = auth_get_no64_data(&data, US args[2]);
403 /* Added by PH: data must not contain tab (as it is
404 b64 it shouldn't, but check for safety). */
406 if (Ustrchr(data, '\t') != NULL) {
411 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
412 if (write(fd, temp, Ustrlen(temp)) < 0)
413 OUT("authentication socket write error");
417 CHECK_COMMAND("FAIL", 1, -1);
419 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
421 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
423 auth_id_pre = args[i]+5;
424 expand_nstring[1] = auth_vars[0] =
425 string_copy(auth_id_pre); /* PH */
426 expand_nlength[1] = Ustrlen(auth_id_pre);
435 CHECK_COMMAND("OK", 2, -1);
438 * Search for the "user=$USER" string in the args array
439 * and return the proper value.
441 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
443 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
445 auth_id_pre = args[i]+5;
446 expand_nstring[1] = auth_vars[0] =
447 string_copy(auth_id_pre); /* PH */
448 expand_nlength[1] = Ustrlen(auth_id_pre);
453 if (auth_id_pre == NULL)
454 OUT("authentication socket protocol error, username missing");
465 /* close the socket used by dovecot */
469 /* Expand server_condition as an authorization check */
470 return (ret == OK)? auth_check_serv_cond(ablock) : ret;