Log an error (instead of hanging) if Dovecot auth is configured to use the wrong...
[exim.git] / src / src / auths / dovecot.c
CommitLineData
14aa5a05
PH
1/*
2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
5a66c31b 3 * Copyright (c) 2006-2014 The Exim Maintainers
14aa5a05
PH
4 *
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.
9 */
10
57c2c631
PH
11/* A number of modifications have been made to the original code. Originally I
12commented them specially, but now they are getting quite extensive, so I have
13ceased doing that. The biggest change is to use unbuffered I/O on the socket
14because using C buffered I/O gives problems on some operating systems. PH */
15
3f1df0e3
PP
16/* Protocol specifications:
17 * Dovecot 1, protocol version 1.1
18 * http://wiki.dovecot.org/Authentication%20Protocol
19 *
20 * Dovecot 2, protocol version 1.1
21 * http://wiki2.dovecot.org/Design/AuthProtocol
22 */
23
14aa5a05
PH
24#include "../exim.h"
25#include "dovecot.h"
26
27#define VERSION_MAJOR 1
28#define VERSION_MINOR 0
29
3f1df0e3
PP
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"
33*/
34#define DOVECOT_AUTH_MAXLINELEN 8192
35
36/* This was hard-coded as 8.
37AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
38Dovecot 1; Dovecot 2 (same protocol version) defines 9.
39
40Master->Server sends {"USER", id, userid} + params, 6 defined.
41Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
42
43We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
44for the command and id, where unspecified might include _at least_ user=...
45
46So: allow for more fields than we ever expect to see, while aware that count
47can go up without changing protocol version.
48The cost is the length of an array of pointers on the stack.
49*/
50#define DOVECOT_AUTH_MAXFIELDCOUNT 16
51
14aa5a05
PH
52/* Options specific to the authentication mechanism. */
53optionlist auth_dovecot_options[] = {
54 {
57c2c631
PH
55 "server_socket",
56 opt_stringptr,
57 (void *)(offsetof(auth_dovecot_options_block, server_socket))
14aa5a05
PH
58 },
59};
60
61/* Size of the options list. An extern variable has to be used so that its
62address can appear in the tables drtables.c. */
57c2c631 63
14aa5a05
PH
64int auth_dovecot_options_count =
65 sizeof(auth_dovecot_options) / sizeof(optionlist);
66
67/* Default private options block for the authentication method. */
57c2c631 68
14aa5a05
PH
69auth_dovecot_options_block auth_dovecot_option_defaults = {
70 NULL, /* server_socket */
71};
72
57c2c631
PH
73
74/* Static variables for reading from the socket */
75
76static uschar sbuffer[256];
3f1df0e3 77static int socket_buffer_left;
57c2c631
PH
78
79
80
14aa5a05
PH
81/*************************************************
82 * Initialization entry point *
83 *************************************************/
84
85/* Called for each instance, after its options have been read, to
86enable consistency checks to be done, or anything else that needs
87to be set up. */
57c2c631 88
14aa5a05
PH
89void auth_dovecot_init(auth_instance *ablock)
90{
91 auth_dovecot_options_block *ob =
92 (auth_dovecot_options_block *)(ablock->options_block);
93
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;
99}
100
3f1df0e3
PP
101/*************************************************
102 * "strcut" to split apart server lines *
103 *************************************************/
104
105/* Dovecot auth protocol uses TAB \t as delimiter; a line consists
106of a command-name, TAB, and then any parameters, each separated by a TAB.
107A parameter can be param=value or a bool, just param.
108
109This function modifies the original str in-place, inserting NUL characters.
110It initialises ptrs entries, setting all to NULL and only setting
111non-NULL N entries, where N is the return value, the number of fields seen
112(one more than the number of tabs).
113
114Note that the return value will always be at least 1, is the count of
115actual fields (so last valid offset into ptrs is one less).
116*/
117
118static int
119strcut(uschar *str, uschar **ptrs, int nptrs)
14aa5a05 120{
3f1df0e3 121 uschar *last_sub_start = str;
14aa5a05
PH
122 int n;
123
124 for (n = 0; n < nptrs; n++)
125 ptrs[n] = NULL;
126 n = 1;
127
128 while (*str) {
129 if (*str == '\t') {
130 if (n <= nptrs) {
3f1df0e3
PP
131 *ptrs++ = last_sub_start;
132 last_sub_start = str + 1;
133 *str = '\0';
14aa5a05
PH
134 }
135 n++;
136 }
137 str++;
138 }
139
970ba64f
PP
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. */
143 if (n <= nptrs) {
144 *ptrs = last_sub_start;
3f1df0e3 145 } else {
970ba64f
PP
146 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
147 n = nptrs;
3f1df0e3
PP
148 }
149
150 return n <= nptrs ? n : nptrs;
151}
14aa5a05 152
3f1df0e3
PP
153static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
154static void
155debug_strcut(uschar **ptrs, int nlen, int alen)
156{
157 int i;
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]);
162 }
163 if (nlen < alen)
164 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
165 else
166 debug_printf(" (max for capacity)\n");
14aa5a05
PH
167}
168
169#define CHECK_COMMAND(str, arg_min, arg_max) do { \
57c2c631 170 if (strcmpic(US(str), args[0]) != 0) \
14aa5a05
PH
171 goto out; \
172 if (nargs - 1 < (arg_min)) \
173 goto out; \
6c588e74 174 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
14aa5a05
PH
175 goto out; \
176} while (0)
177
178#define OUT(msg) do { \
179 auth_defer_msg = (US msg); \
180 goto out; \
181} while(0)
182
7befa435
PH
183
184
14aa5a05 185/*************************************************
57c2c631
PH
186* "fgets" to read directly from socket *
187*************************************************/
188
189/* Added by PH after a suggestion by Steve Usher because the previous use of
190C-style buffered I/O gave trouble. */
191
192static uschar *
193dc_gets(uschar *s, int n, int fd)
194{
195int p = 0;
196int count = 0;
197
198for (;;)
199 {
3f1df0e3 200 if (socket_buffer_left == 0)
57c2c631 201 {
3f1df0e3
PP
202 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
203 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
51473862 204 p = 0;
57c2c631
PH
205 }
206
3f1df0e3 207 while (p < socket_buffer_left)
57c2c631
PH
208 {
209 if (count >= n - 1) break;
210 s[count++] = sbuffer[p];
211 if (sbuffer[p++] == '\n') break;
212 }
213
3f1df0e3
PP
214 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
215 socket_buffer_left -= p;
57c2c631
PH
216
217 if (s[count-1] == '\n' || count >= n - 1) break;
218 }
219
3f1df0e3 220s[count] = '\0';
57c2c631
PH
221return s;
222}
223
224
225
226
227/*************************************************
228* Server entry point *
229*************************************************/
14aa5a05
PH
230
231int auth_dovecot_server(auth_instance *ablock, uschar *data)
232{
233 auth_dovecot_options_block *ob =
234 (auth_dovecot_options_block *)(ablock->options_block);
235 struct sockaddr_un sa;
3f1df0e3
PP
236 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
237 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
7befa435
PH
238 uschar *auth_command;
239 uschar *auth_extra_data = US"";
3f1df0e3 240 uschar *p;
14aa5a05 241 int nargs, tmp;
3f1df0e3 242 int crequid = 1, cont = 1, fd, ret = DEFER;
ac650c35 243 BOOL found = FALSE, have_mech_line = FALSE;
14aa5a05
PH
244
245 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
246
247 memset(&sa, 0, sizeof(sa));
248 sa.sun_family = AF_UNIX;
249
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 */
253
254 /*
255 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
256 */
257
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";
261 return DEFER;
262 }
263
264 auth_defer_msg = US"authentication socket connection error";
265
266 fd = socket(PF_UNIX, SOCK_STREAM, 0);
267 if (fd < 0)
268 return DEFER;
269
270 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
271 goto out;
272
14aa5a05
PH
273 auth_defer_msg = US"authentication socket protocol error";
274
3f1df0e3 275 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
14aa5a05 276 while (cont) {
57c2c631 277 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
14aa5a05 278 OUT("authentication socket read error or premature eof");
3f1df0e3
PP
279 p = buffer + Ustrlen(buffer) - 1;
280 if (*p != '\n') {
281 OUT("authentication socket protocol line too long");
282 }
283 *p = '\0';
14aa5a05
PH
284 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
285 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
3f1df0e3 286 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
14aa5a05 287
981a9fad
NM
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. */
3f1df0e3
PP
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
296 diagnostics. */
297 if (Ustrcmp(args[0], US"VERSION") == 0) {
14aa5a05 298 CHECK_COMMAND("VERSION", 2, 2);
57c2c631 299 if (Uatoi(args[1]) != VERSION_MAJOR)
14aa5a05 300 OUT("authentication socket protocol version mismatch");
981a9fad
NM
301 } else if (Ustrcmp(args[0], US"MECH") == 0) {
302 CHECK_COMMAND("MECH", 1, INT_MAX);
ac650c35 303 have_mech_line = TRUE;
981a9fad 304 if (strcmpic(US args[1], ablock->public_name) == 0)
3f1df0e3 305 found = TRUE;
ac650c35
TS
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). */
315 if (!have_mech_line)
316 OUT("authentication socket type mismatch (connected to auth-master instead of auth-client)");
981a9fad
NM
317 } else if (Ustrcmp(args[0], US"DONE") == 0) {
318 CHECK_COMMAND("DONE", 0, 0);
319 cont = 0;
14aa5a05
PH
320 }
321 }
322
3f1df0e3
PP
323 if (!found) {
324 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
14aa5a05 325 goto out;
3f1df0e3 326 }
14aa5a05 327
7befa435
PH
328 /* Added by PH: data must not contain tab (as it is
329 b64 it shouldn't, but check for safety). */
330
331 if (Ustrchr(data, '\t') != NULL) {
332 ret = FAIL;
333 goto out;
334 }
335
336 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
337 connection is local. */
338
817d9f57 339 if (tls_in.cipher != NULL)
7befa435 340 auth_extra_data = string_sprintf("secured\t%s%s",
817d9f57
JH
341 tls_in.certificate_verified? "valid-client-cert" : "",
342 tls_in.certificate_verified? "\t" : "");
094a9ec3
PH
343 else if (interface_address != NULL &&
344 Ustrcmp(sender_host_address, interface_address) == 0)
7befa435
PH
345 auth_extra_data = US"secured\t";
346
14aa5a05
PH
347
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
7befa435
PH
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
14aa5a05
PH
353
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 : "");
7befa435
PH
359
360 Subsequently, the command was modified to add "secured" and "valid-client-
361 cert" when relevant.
14aa5a05
PH
362****************************************************************************/
363
7befa435 364 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
deafd5b3 365 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
3f1df0e3 366 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
7befa435
PH
367 ablock->public_name, auth_extra_data, sender_host_address,
368 interface_address, data ? (char *) data : "");
14aa5a05 369
57c2c631
PH
370 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
371 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
372 strerror(errno));
373
7befa435 374 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
14aa5a05
PH
375
376 while (1) {
57c2c631 377 uschar *temp;
6c588e74
NM
378 uschar *auth_id_pre = NULL;
379 int i;
380
57c2c631 381 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
14aa5a05
PH
382 auth_defer_msg = US"authentication socket read error or premature eof";
383 goto out;
384 }
385
57c2c631 386 buffer[Ustrlen(buffer) - 1] = 0;
14aa5a05
PH
387 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
388 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
389
3f1df0e3 390 if (Uatoi(args[1]) != crequid)
14aa5a05
PH
391 OUT("authentication socket connection id mismatch");
392
393 switch (toupper(*args[0])) {
394 case 'C':
395 CHECK_COMMAND("CONT", 1, 2);
396
397 tmp = auth_get_no64_data(&data, US args[2]);
398 if (tmp != OK) {
399 ret = tmp;
400 goto out;
401 }
402
7befa435
PH
403 /* Added by PH: data must not contain tab (as it is
404 b64 it shouldn't, but check for safety). */
405
406 if (Ustrchr(data, '\t') != NULL) {
407 ret = FAIL;
408 goto out;
409 }
410
3f1df0e3 411 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
57c2c631 412 if (write(fd, temp, Ustrlen(temp)) < 0)
14aa5a05 413 OUT("authentication socket write error");
14aa5a05
PH
414 break;
415
416 case 'F':
6c588e74 417 CHECK_COMMAND("FAIL", 1, -1);
14aa5a05 418
6c588e74
NM
419 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
420 {
421 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
422 {
423 auth_id_pre = args[i]+5;
f0872424 424 expand_nstring[1] = auth_vars[0] =
6c588e74
NM
425 string_copy(auth_id_pre); /* PH */
426 expand_nlength[1] = Ustrlen(auth_id_pre);
14aa5a05
PH
427 expand_nmax = 1;
428 }
429 }
430
431 ret = FAIL;
432 goto out;
433
434 case 'O':
6c588e74
NM
435 CHECK_COMMAND("OK", 2, -1);
436
437 /*
438 * Search for the "user=$USER" string in the args array
439 * and return the proper value.
440 */
441 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
14aa5a05 442 {
6c588e74
NM
443 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
444 {
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);
449 expand_nmax = 1;
450 }
14aa5a05 451 }
6c588e74
NM
452
453 if (auth_id_pre == NULL)
454 OUT("authentication socket protocol error, username missing");
455
14aa5a05
PH
456 ret = OK;
457 /* fallthrough */
458
459 default:
460 goto out;
461 }
462 }
463
57c2c631 464out:
3f0da4d0
NM
465 /* close the socket used by dovecot */
466 if (fd >= 0)
467 close(fd);
16ff981e
PH
468
469 /* Expand server_condition as an authorization check */
470 return (ret == OK)? auth_check_serv_cond(ablock) : ret;
14aa5a05 471}