Merge branch 'debian' into hcoop_489
[hcoop/debian/exim4.git] / src / auths / dovecot.c
CommitLineData
420a0d19
CE
1/*
2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
2813c06e 3 * Copyright (c) 2006-2016 The Exim Maintainers
420a0d19
CE
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
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
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
24#include "../exim.h"
25#include "dovecot.h"
26
27#define VERSION_MAJOR 1
28#define VERSION_MINOR 0
29
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
52/* Options specific to the authentication mechanism. */
53optionlist auth_dovecot_options[] = {
54 {
55 "server_socket",
56 opt_stringptr,
57 (void *)(offsetof(auth_dovecot_options_block, server_socket))
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. */
63
64int auth_dovecot_options_count =
65 sizeof(auth_dovecot_options) / sizeof(optionlist);
66
67/* Default private options block for the authentication method. */
68
69auth_dovecot_options_block auth_dovecot_option_defaults = {
70 NULL, /* server_socket */
71};
72
73
74/* Static variables for reading from the socket */
75
76static uschar sbuffer[256];
77static int socket_buffer_left;
78
79
80
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. */
88
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
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)
120{
121 uschar *last_sub_start = str;
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) {
131 *ptrs++ = last_sub_start;
132 last_sub_start = str + 1;
133 *str = '\0';
134 }
135 n++;
136 }
137 str++;
138 }
139
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;
145 } else {
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;
148 }
149
150 return n <= nptrs ? n : nptrs;
151}
152
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");
167}
168
169#define CHECK_COMMAND(str, arg_min, arg_max) do { \
170 if (strcmpic(US(str), args[0]) != 0) \
171 goto out; \
172 if (nargs - 1 < (arg_min)) \
173 goto out; \
174 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
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
183
184
185/*************************************************
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 {
200 if (socket_buffer_left == 0)
201 {
202 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
203 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
204 p = 0;
205 }
206
207 while (p < socket_buffer_left)
208 {
209 if (count >= n - 1) break;
210 s[count++] = sbuffer[p];
211 if (sbuffer[p++] == '\n') break;
212 }
213
214 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
215 socket_buffer_left -= p;
216
217 if (s[count-1] == '\n' || count >= n - 1) break;
218 }
219
220s[count] = '\0';
221return s;
222}
223
224
225
226
227/*************************************************
228* Server entry point *
229*************************************************/
230
2813c06e
CE
231int
232auth_dovecot_server(auth_instance * ablock, uschar * data)
420a0d19 233{
2813c06e
CE
234auth_dovecot_options_block *ob =
235 (auth_dovecot_options_block *) ablock->options_block;
236struct sockaddr_un sa;
237uschar buffer[DOVECOT_AUTH_MAXLINELEN];
238uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
239uschar *auth_command;
240uschar *auth_extra_data = US"";
241uschar *p;
242int nargs, tmp;
243int crequid = 1, cont = 1, fd = -1, ret = DEFER;
244BOOL found = FALSE, have_mech_line = FALSE;
245
246HDEBUG(D_auth) debug_printf("dovecot authentication\n");
247
248if (!data)
249 {
250 ret = FAIL;
251 goto out;
252 }
420a0d19 253
2813c06e
CE
254memset(&sa, 0, sizeof(sa));
255sa.sun_family = AF_UNIX;
420a0d19 256
2813c06e
CE
257/* This was the original code here: it is nonsense because strncpy()
258does not return an integer. I have converted this to use the function
259that formats and checks length. PH */
420a0d19 260
2813c06e
CE
261/*
262if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
263}
264*/
420a0d19 265
2813c06e
CE
266if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
267 ob->server_socket))
268 {
269 auth_defer_msg = US"authentication socket path too long";
270 return DEFER;
271 }
420a0d19 272
2813c06e 273auth_defer_msg = US"authentication socket connection error";
420a0d19 274
2813c06e
CE
275if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
276 return DEFER;
420a0d19 277
2813c06e
CE
278if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
279 goto out;
420a0d19 280
2813c06e 281auth_defer_msg = US"authentication socket protocol error";
420a0d19 282
2813c06e
CE
283socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
284while (cont)
285 {
286 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
287 OUT("authentication socket read error or premature eof");
288 p = buffer + Ustrlen(buffer) - 1;
289 if (*p != '\n')
290 OUT("authentication socket protocol line too long");
291
292 *p = '\0';
293 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
294
295 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
296
297 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
298
299 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
300 Exim will need. Original code also failed if Dovecot server sent unknown
301 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
302 /* pdp: note that CUID is a per-connection identifier sent by the server,
303 which increments at server discretion.
304 By contrast, the "id" field of the protocol is a connection-specific request
305 identifier, which needs to be unique per request from the client and is not
306 connected to the CUID value, so we ignore CUID from server. It's purely for
307 diagnostics. */
308
309 if (Ustrcmp(args[0], US"VERSION") == 0)
310 {
311 CHECK_COMMAND("VERSION", 2, 2);
312 if (Uatoi(args[1]) != VERSION_MAJOR)
313 OUT("authentication socket protocol version mismatch");
314 }
315 else if (Ustrcmp(args[0], US"MECH") == 0)
316 {
317 CHECK_COMMAND("MECH", 1, INT_MAX);
318 have_mech_line = TRUE;
319 if (strcmpic(US args[1], ablock->public_name) == 0)
320 found = TRUE;
321 }
322 else if (Ustrcmp(args[0], US"SPID") == 0)
323 {
324 /* Unfortunately the auth protocol handshake wasn't designed well
325 to differentiate between auth-client/userdb/master. auth-userdb
326 and auth-master send VERSION + SPID lines only and nothing
327 afterwards, while auth-client sends VERSION + MECH + SPID +
328 CUID + more. The simplest way that we can determine if we've
329 connected to the correct socket is to see if MECH line exists or
330 not (alternatively we'd have to have a small timeout after SPID
331 to see if CUID is sent or not). */
332
333 if (!have_mech_line)
334 OUT("authentication socket type mismatch"
335 " (connected to auth-master instead of auth-client)");
336 }
337 else if (Ustrcmp(args[0], US"DONE") == 0)
338 {
339 CHECK_COMMAND("DONE", 0, 0);
340 cont = 0;
341 }
342 }
420a0d19 343
2813c06e
CE
344if (!found)
345 {
346 auth_defer_msg = string_sprintf(
347 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
348 goto out;
349 }
420a0d19 350
2813c06e
CE
351/* Added by PH: data must not contain tab (as it is
352b64 it shouldn't, but check for safety). */
420a0d19 353
2813c06e
CE
354if (Ustrchr(data, '\t') != NULL)
355 {
356 ret = FAIL;
357 goto out;
358 }
420a0d19 359
2813c06e
CE
360/* Added by PH: extra fields when TLS is in use or if the TCP/IP
361connection is local. */
420a0d19 362
2813c06e
CE
363if (tls_in.cipher != NULL)
364 auth_extra_data = string_sprintf("secured\t%s%s",
365 tls_in.certificate_verified? "valid-client-cert" : "",
366 tls_in.certificate_verified? "\t" : "");
420a0d19 367
2813c06e
CE
368else if ( interface_address != NULL
369 && Ustrcmp(sender_host_address, interface_address) == 0)
370 auth_extra_data = US"secured\t";
420a0d19 371
420a0d19 372
2813c06e
CE
373/****************************************************************************
374The code below was the original code here. It didn't work. A reading of the
375file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
376this was not right. Maybe something changed. I changed it to move the
377service indication into the AUTH command, and it seems to be better. PH
378
379fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
380 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
381 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
382 ablock->public_name, sender_host_address, interface_address,
383 data ? (char *) data : "");
384
385Subsequently, the command was modified to add "secured" and "valid-client-
386cert" when relevant.
387****************************************************************************/
420a0d19 388
2813c06e
CE
389auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
390 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
391 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
392 ablock->public_name, auth_extra_data, sender_host_address,
393 interface_address, data);
420a0d19 394
2813c06e
CE
395if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
396 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
397 strerror(errno));
420a0d19 398
2813c06e 399HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
420a0d19 400
2813c06e
CE
401while (1)
402 {
403 uschar *temp;
404 uschar *auth_id_pre = NULL;
405 int i;
420a0d19 406
2813c06e
CE
407 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
408 {
409 auth_defer_msg = US"authentication socket read error or premature eof";
410 goto out;
411 }
420a0d19 412
2813c06e
CE
413 buffer[Ustrlen(buffer) - 1] = 0;
414 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
415 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
420a0d19 416
2813c06e
CE
417 if (Uatoi(args[1]) != crequid)
418 OUT("authentication socket connection id mismatch");
420a0d19 419
2813c06e
CE
420 switch (toupper(*args[0]))
421 {
422 case 'C':
423 CHECK_COMMAND("CONT", 1, 2);
424
425 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
426 {
427 ret = tmp;
428 goto out;
429 }
430
431 /* Added by PH: data must not contain tab (as it is
432 b64 it shouldn't, but check for safety). */
433
434 if (Ustrchr(data, '\t') != NULL)
435 {
436 ret = FAIL;
437 goto out;
438 }
439
440 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
441 if (write(fd, temp, Ustrlen(temp)) < 0)
442 OUT("authentication socket write error");
443 break;
444
445 case 'F':
446 CHECK_COMMAND("FAIL", 1, -1);
447
448 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
449 {
450 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
451 {
452 auth_id_pre = args[i]+5;
453 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
454 expand_nlength[1] = Ustrlen(auth_id_pre);
455 expand_nmax = 1;
456 }
457 }
458
459 ret = FAIL;
460 goto out;
461
462 case 'O':
463 CHECK_COMMAND("OK", 2, -1);
464
465 /* Search for the "user=$USER" string in the args array
466 and return the proper value. */
467
468 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
469 {
470 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
471 {
472 auth_id_pre = args[i]+5;
473 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
474 expand_nlength[1] = Ustrlen(auth_id_pre);
475 expand_nmax = 1;
476 }
477 }
478
479 if (auth_id_pre == NULL)
480 OUT("authentication socket protocol error, username missing");
481
482 ret = OK;
483 /* fallthrough */
484
485 default:
486 goto out;
487 }
488 }
420a0d19
CE
489
490out:
2813c06e
CE
491/* close the socket used by dovecot */
492if (fd >= 0)
493 close(fd);
420a0d19 494
2813c06e
CE
495/* Expand server_condition as an authorization check */
496return ret == OK ? auth_check_serv_cond(ablock) : ret;
420a0d19 497}