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