Fix dovecot with empty 334 challenge.
[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 120 uschar *last_sub_start = str;
14aa5a05
PH
121 int n;
122
123 for (n = 0; n < nptrs; n++)
124 ptrs[n] = NULL;
125 n = 1;
126
127 while (*str) {
128 if (*str == '\t') {
129 if (n <= nptrs) {
3f1df0e3
PP
130 *ptrs++ = last_sub_start;
131 last_sub_start = str + 1;
132 *str = '\0';
14aa5a05
PH
133 }
134 n++;
135 }
136 str++;
137 }
138
970ba64f
PP
139 /* It's acceptable for the string to end with a tab character. We see
140 this in AUTH PLAIN without an initial response from the client, which
141 causing us to send "334 " and get the data from the client. */
142 if (n <= nptrs) {
143 *ptrs = last_sub_start;
3f1df0e3 144 } else {
970ba64f
PP
145 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
146 n = nptrs;
3f1df0e3
PP
147 }
148
149 return n <= nptrs ? n : nptrs;
150}
14aa5a05 151
3f1df0e3
PP
152static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
153static void
154debug_strcut(uschar **ptrs, int nlen, int alen)
155{
156 int i;
157 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
158 socket_buffer_left, nlen);
159 for (i = 0; i < nlen; i++) {
160 debug_printf(" {%s}", ptrs[i]);
161 }
162 if (nlen < alen)
163 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
164 else
165 debug_printf(" (max for capacity)\n");
14aa5a05
PH
166}
167
168#define CHECK_COMMAND(str, arg_min, arg_max) do { \
57c2c631 169 if (strcmpic(US(str), args[0]) != 0) \
14aa5a05
PH
170 goto out; \
171 if (nargs - 1 < (arg_min)) \
172 goto out; \
6c588e74 173 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
14aa5a05
PH
174 goto out; \
175} while (0)
176
177#define OUT(msg) do { \
178 auth_defer_msg = (US msg); \
179 goto out; \
180} while(0)
181
7befa435
PH
182
183
14aa5a05 184/*************************************************
57c2c631
PH
185* "fgets" to read directly from socket *
186*************************************************/
187
188/* Added by PH after a suggestion by Steve Usher because the previous use of
189C-style buffered I/O gave trouble. */
190
191static uschar *
192dc_gets(uschar *s, int n, int fd)
193{
194int p = 0;
195int count = 0;
196
197for (;;)
198 {
3f1df0e3 199 if (socket_buffer_left == 0)
57c2c631 200 {
3f1df0e3
PP
201 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
202 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
51473862 203 p = 0;
57c2c631
PH
204 }
205
3f1df0e3 206 while (p < socket_buffer_left)
57c2c631
PH
207 {
208 if (count >= n - 1) break;
209 s[count++] = sbuffer[p];
210 if (sbuffer[p++] == '\n') break;
211 }
212
3f1df0e3
PP
213 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
214 socket_buffer_left -= p;
57c2c631
PH
215
216 if (s[count-1] == '\n' || count >= n - 1) break;
217 }
218
3f1df0e3 219s[count] = '\0';
57c2c631
PH
220return s;
221}
222
223
224
225
226/*************************************************
227* Server entry point *
228*************************************************/
14aa5a05
PH
229
230int auth_dovecot_server(auth_instance *ablock, uschar *data)
231{
232 auth_dovecot_options_block *ob =
233 (auth_dovecot_options_block *)(ablock->options_block);
234 struct sockaddr_un sa;
3f1df0e3
PP
235 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
236 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
7befa435
PH
237 uschar *auth_command;
238 uschar *auth_extra_data = US"";
3f1df0e3 239 uschar *p;
14aa5a05 240 int nargs, tmp;
3f1df0e3
PP
241 int crequid = 1, cont = 1, fd, ret = DEFER;
242 BOOL found = FALSE;
14aa5a05
PH
243
244 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
245
246 memset(&sa, 0, sizeof(sa));
247 sa.sun_family = AF_UNIX;
248
249 /* This was the original code here: it is nonsense because strncpy()
250 does not return an integer. I have converted this to use the function
251 that formats and checks length. PH */
252
253 /*
254 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
255 */
256
257 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
258 ob->server_socket)) {
259 auth_defer_msg = US"authentication socket path too long";
260 return DEFER;
261 }
262
263 auth_defer_msg = US"authentication socket connection error";
264
265 fd = socket(PF_UNIX, SOCK_STREAM, 0);
266 if (fd < 0)
267 return DEFER;
268
269 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
270 goto out;
271
14aa5a05
PH
272 auth_defer_msg = US"authentication socket protocol error";
273
3f1df0e3 274 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
14aa5a05 275 while (cont) {
57c2c631 276 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
14aa5a05 277 OUT("authentication socket read error or premature eof");
3f1df0e3
PP
278 p = buffer + Ustrlen(buffer) - 1;
279 if (*p != '\n') {
280 OUT("authentication socket protocol line too long");
281 }
282 *p = '\0';
14aa5a05
PH
283 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
284 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
3f1df0e3 285 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
14aa5a05 286
981a9fad
NM
287 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
288 Exim will need. Original code also failed if Dovecot server sent unknown
289 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
3f1df0e3
PP
290 /* pdp: note that CUID is a per-connection identifier sent by the server,
291 which increments at server discretion.
292 By contrast, the "id" field of the protocol is a connection-specific request
293 identifier, which needs to be unique per request from the client and is not
294 connected to the CUID value, so we ignore CUID from server. It's purely for
295 diagnostics. */
296 if (Ustrcmp(args[0], US"VERSION") == 0) {
14aa5a05 297 CHECK_COMMAND("VERSION", 2, 2);
57c2c631 298 if (Uatoi(args[1]) != VERSION_MAJOR)
14aa5a05 299 OUT("authentication socket protocol version mismatch");
981a9fad
NM
300 } else if (Ustrcmp(args[0], US"MECH") == 0) {
301 CHECK_COMMAND("MECH", 1, INT_MAX);
302 if (strcmpic(US args[1], ablock->public_name) == 0)
3f1df0e3 303 found = TRUE;
981a9fad
NM
304 } else if (Ustrcmp(args[0], US"DONE") == 0) {
305 CHECK_COMMAND("DONE", 0, 0);
306 cont = 0;
14aa5a05
PH
307 }
308 }
309
3f1df0e3
PP
310 if (!found) {
311 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
14aa5a05 312 goto out;
3f1df0e3 313 }
14aa5a05 314
7befa435
PH
315 /* Added by PH: data must not contain tab (as it is
316 b64 it shouldn't, but check for safety). */
317
318 if (Ustrchr(data, '\t') != NULL) {
319 ret = FAIL;
320 goto out;
321 }
322
323 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
324 connection is local. */
325
817d9f57 326 if (tls_in.cipher != NULL)
7befa435 327 auth_extra_data = string_sprintf("secured\t%s%s",
817d9f57
JH
328 tls_in.certificate_verified? "valid-client-cert" : "",
329 tls_in.certificate_verified? "\t" : "");
094a9ec3
PH
330 else if (interface_address != NULL &&
331 Ustrcmp(sender_host_address, interface_address) == 0)
7befa435
PH
332 auth_extra_data = US"secured\t";
333
14aa5a05
PH
334
335/****************************************************************************
336 The code below was the original code here. It didn't work. A reading of the
337 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
7befa435
PH
338 this was not right. Maybe something changed. I changed it to move the
339 service indication into the AUTH command, and it seems to be better. PH
14aa5a05
PH
340
341 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
342 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
343 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
344 ablock->public_name, sender_host_address, interface_address,
345 data ? (char *) data : "");
7befa435
PH
346
347 Subsequently, the command was modified to add "secured" and "valid-client-
348 cert" when relevant.
14aa5a05
PH
349****************************************************************************/
350
7befa435 351 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
deafd5b3 352 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
3f1df0e3 353 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
7befa435
PH
354 ablock->public_name, auth_extra_data, sender_host_address,
355 interface_address, data ? (char *) data : "");
14aa5a05 356
57c2c631
PH
357 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
358 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
359 strerror(errno));
360
7befa435 361 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
14aa5a05
PH
362
363 while (1) {
57c2c631 364 uschar *temp;
6c588e74
NM
365 uschar *auth_id_pre = NULL;
366 int i;
367
57c2c631 368 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
14aa5a05
PH
369 auth_defer_msg = US"authentication socket read error or premature eof";
370 goto out;
371 }
372
57c2c631 373 buffer[Ustrlen(buffer) - 1] = 0;
14aa5a05
PH
374 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
375 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
376
3f1df0e3 377 if (Uatoi(args[1]) != crequid)
14aa5a05
PH
378 OUT("authentication socket connection id mismatch");
379
380 switch (toupper(*args[0])) {
381 case 'C':
382 CHECK_COMMAND("CONT", 1, 2);
383
384 tmp = auth_get_no64_data(&data, US args[2]);
385 if (tmp != OK) {
386 ret = tmp;
387 goto out;
388 }
389
7befa435
PH
390 /* Added by PH: data must not contain tab (as it is
391 b64 it shouldn't, but check for safety). */
392
393 if (Ustrchr(data, '\t') != NULL) {
394 ret = FAIL;
395 goto out;
396 }
397
3f1df0e3 398 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
57c2c631 399 if (write(fd, temp, Ustrlen(temp)) < 0)
14aa5a05 400 OUT("authentication socket write error");
14aa5a05
PH
401 break;
402
403 case 'F':
6c588e74 404 CHECK_COMMAND("FAIL", 1, -1);
14aa5a05 405
6c588e74
NM
406 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
407 {
408 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
409 {
410 auth_id_pre = args[i]+5;
f0872424 411 expand_nstring[1] = auth_vars[0] =
6c588e74
NM
412 string_copy(auth_id_pre); /* PH */
413 expand_nlength[1] = Ustrlen(auth_id_pre);
14aa5a05
PH
414 expand_nmax = 1;
415 }
416 }
417
418 ret = FAIL;
419 goto out;
420
421 case 'O':
6c588e74
NM
422 CHECK_COMMAND("OK", 2, -1);
423
424 /*
425 * Search for the "user=$USER" string in the args array
426 * and return the proper value.
427 */
428 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
14aa5a05 429 {
6c588e74
NM
430 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
431 {
432 auth_id_pre = args[i]+5;
433 expand_nstring[1] = auth_vars[0] =
434 string_copy(auth_id_pre); /* PH */
435 expand_nlength[1] = Ustrlen(auth_id_pre);
436 expand_nmax = 1;
437 }
14aa5a05 438 }
6c588e74
NM
439
440 if (auth_id_pre == NULL)
441 OUT("authentication socket protocol error, username missing");
442
14aa5a05
PH
443 ret = OK;
444 /* fallthrough */
445
446 default:
447 goto out;
448 }
449 }
450
57c2c631 451out:
3f0da4d0
NM
452 /* close the socket used by dovecot */
453 if (fd >= 0)
454 close(fd);
16ff981e
PH
455
456 /* Expand server_condition as an authorization check */
457 return (ret == OK)? auth_check_serv_cond(ablock) : ret;
14aa5a05 458}