Copyright year bumps for substantive changes 2017
[exim.git] / src / src / auths / dovecot.c
CommitLineData
14aa5a05
PH
1/*
2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
9242a7e8 3 * Copyright (c) 2006-2017 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 73
d185889f
JH
74
75
76#ifdef MACRO_PREDEF
77
78/* Dummy values */
79void auth_dovecot_init(auth_instance *ablock) {}
80int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
81int auth_dovecot_client(auth_instance *ablock, smtp_inblock *inblock,
82 smtp_outblock *outblock, int timeout, uschar *buffer, int buffsize) {return 0;}
83
84#else /*!MACRO_PREDEF*/
85
86
57c2c631
PH
87/* Static variables for reading from the socket */
88
89static uschar sbuffer[256];
3f1df0e3 90static int socket_buffer_left;
57c2c631
PH
91
92
93
14aa5a05
PH
94/*************************************************
95 * Initialization entry point *
96 *************************************************/
97
98/* Called for each instance, after its options have been read, to
99enable consistency checks to be done, or anything else that needs
100to be set up. */
57c2c631 101
14aa5a05
PH
102void auth_dovecot_init(auth_instance *ablock)
103{
104 auth_dovecot_options_block *ob =
105 (auth_dovecot_options_block *)(ablock->options_block);
106
107 if (ablock->public_name == NULL)
108 ablock->public_name = ablock->name;
109 if (ob->server_socket != NULL)
110 ablock->server = TRUE;
111 ablock->client = FALSE;
112}
113
3f1df0e3
PP
114/*************************************************
115 * "strcut" to split apart server lines *
116 *************************************************/
117
118/* Dovecot auth protocol uses TAB \t as delimiter; a line consists
119of a command-name, TAB, and then any parameters, each separated by a TAB.
120A parameter can be param=value or a bool, just param.
121
122This function modifies the original str in-place, inserting NUL characters.
123It initialises ptrs entries, setting all to NULL and only setting
124non-NULL N entries, where N is the return value, the number of fields seen
125(one more than the number of tabs).
126
127Note that the return value will always be at least 1, is the count of
128actual fields (so last valid offset into ptrs is one less).
129*/
130
131static int
132strcut(uschar *str, uschar **ptrs, int nptrs)
14aa5a05 133{
3f1df0e3 134 uschar *last_sub_start = str;
14aa5a05
PH
135 int n;
136
137 for (n = 0; n < nptrs; n++)
138 ptrs[n] = NULL;
139 n = 1;
140
141 while (*str) {
142 if (*str == '\t') {
143 if (n <= nptrs) {
3f1df0e3
PP
144 *ptrs++ = last_sub_start;
145 last_sub_start = str + 1;
146 *str = '\0';
14aa5a05
PH
147 }
148 n++;
149 }
150 str++;
151 }
152
970ba64f
PP
153 /* It's acceptable for the string to end with a tab character. We see
154 this in AUTH PLAIN without an initial response from the client, which
155 causing us to send "334 " and get the data from the client. */
156 if (n <= nptrs) {
157 *ptrs = last_sub_start;
3f1df0e3 158 } else {
970ba64f
PP
159 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
160 n = nptrs;
3f1df0e3
PP
161 }
162
163 return n <= nptrs ? n : nptrs;
164}
14aa5a05 165
3f1df0e3
PP
166static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
167static void
168debug_strcut(uschar **ptrs, int nlen, int alen)
169{
170 int i;
171 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
172 socket_buffer_left, nlen);
173 for (i = 0; i < nlen; i++) {
174 debug_printf(" {%s}", ptrs[i]);
175 }
176 if (nlen < alen)
177 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
178 else
179 debug_printf(" (max for capacity)\n");
14aa5a05
PH
180}
181
182#define CHECK_COMMAND(str, arg_min, arg_max) do { \
57c2c631 183 if (strcmpic(US(str), args[0]) != 0) \
14aa5a05
PH
184 goto out; \
185 if (nargs - 1 < (arg_min)) \
186 goto out; \
6c588e74 187 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
14aa5a05
PH
188 goto out; \
189} while (0)
190
191#define OUT(msg) do { \
192 auth_defer_msg = (US msg); \
193 goto out; \
194} while(0)
195
7befa435
PH
196
197
14aa5a05 198/*************************************************
57c2c631
PH
199* "fgets" to read directly from socket *
200*************************************************/
201
202/* Added by PH after a suggestion by Steve Usher because the previous use of
203C-style buffered I/O gave trouble. */
204
205static uschar *
206dc_gets(uschar *s, int n, int fd)
207{
208int p = 0;
209int count = 0;
210
211for (;;)
212 {
3f1df0e3 213 if (socket_buffer_left == 0)
57c2c631 214 {
3f1df0e3
PP
215 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
216 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
51473862 217 p = 0;
57c2c631
PH
218 }
219
3f1df0e3 220 while (p < socket_buffer_left)
57c2c631
PH
221 {
222 if (count >= n - 1) break;
223 s[count++] = sbuffer[p];
224 if (sbuffer[p++] == '\n') break;
225 }
226
3f1df0e3
PP
227 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
228 socket_buffer_left -= p;
57c2c631
PH
229
230 if (s[count-1] == '\n' || count >= n - 1) break;
231 }
232
3f1df0e3 233s[count] = '\0';
57c2c631
PH
234return s;
235}
236
237
238
239
240/*************************************************
241* Server entry point *
242*************************************************/
14aa5a05 243
96f5fe4c
JH
244int
245auth_dovecot_server(auth_instance * ablock, uschar * data)
14aa5a05 246{
96f5fe4c
JH
247auth_dovecot_options_block *ob =
248 (auth_dovecot_options_block *) ablock->options_block;
249struct sockaddr_un sa;
250uschar buffer[DOVECOT_AUTH_MAXLINELEN];
251uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
252uschar *auth_command;
253uschar *auth_extra_data = US"";
254uschar *p;
255int nargs, tmp;
77560253 256int crequid = 1, cont = 1, fd = -1, ret = DEFER;
96f5fe4c
JH
257BOOL found = FALSE, have_mech_line = FALSE;
258
259HDEBUG(D_auth) debug_printf("dovecot authentication\n");
260
261if (!data)
262 {
263 ret = FAIL;
264 goto out;
265 }
14aa5a05 266
96f5fe4c
JH
267memset(&sa, 0, sizeof(sa));
268sa.sun_family = AF_UNIX;
14aa5a05 269
96f5fe4c
JH
270/* This was the original code here: it is nonsense because strncpy()
271does not return an integer. I have converted this to use the function
272that formats and checks length. PH */
14aa5a05 273
96f5fe4c
JH
274/*
275if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
276}
277*/
14aa5a05 278
96f5fe4c
JH
279if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
280 ob->server_socket))
281 {
282 auth_defer_msg = US"authentication socket path too long";
283 return DEFER;
284 }
7befa435 285
96f5fe4c 286auth_defer_msg = US"authentication socket connection error";
7befa435 287
96f5fe4c
JH
288if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
289 return DEFER;
7befa435 290
96f5fe4c
JH
291if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
292 goto out;
7befa435 293
96f5fe4c 294auth_defer_msg = US"authentication socket protocol error";
14aa5a05 295
96f5fe4c
JH
296socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
297while (cont)
298 {
299 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
300 OUT("authentication socket read error or premature eof");
301 p = buffer + Ustrlen(buffer) - 1;
302 if (*p != '\n')
303 OUT("authentication socket protocol line too long");
304
305 *p = '\0';
306 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
307
308 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
309
310 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
311
312 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
313 Exim will need. Original code also failed if Dovecot server sent unknown
314 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
315 /* pdp: note that CUID is a per-connection identifier sent by the server,
316 which increments at server discretion.
317 By contrast, the "id" field of the protocol is a connection-specific request
318 identifier, which needs to be unique per request from the client and is not
319 connected to the CUID value, so we ignore CUID from server. It's purely for
320 diagnostics. */
321
322 if (Ustrcmp(args[0], US"VERSION") == 0)
323 {
324 CHECK_COMMAND("VERSION", 2, 2);
325 if (Uatoi(args[1]) != VERSION_MAJOR)
326 OUT("authentication socket protocol version mismatch");
327 }
328 else if (Ustrcmp(args[0], US"MECH") == 0)
329 {
330 CHECK_COMMAND("MECH", 1, INT_MAX);
331 have_mech_line = TRUE;
332 if (strcmpic(US args[1], ablock->public_name) == 0)
333 found = TRUE;
334 }
335 else if (Ustrcmp(args[0], US"SPID") == 0)
336 {
337 /* Unfortunately the auth protocol handshake wasn't designed well
338 to differentiate between auth-client/userdb/master. auth-userdb
339 and auth-master send VERSION + SPID lines only and nothing
340 afterwards, while auth-client sends VERSION + MECH + SPID +
341 CUID + more. The simplest way that we can determine if we've
342 connected to the correct socket is to see if MECH line exists or
343 not (alternatively we'd have to have a small timeout after SPID
344 to see if CUID is sent or not). */
345
346 if (!have_mech_line)
347 OUT("authentication socket type mismatch"
348 " (connected to auth-master instead of auth-client)");
349 }
350 else if (Ustrcmp(args[0], US"DONE") == 0)
351 {
352 CHECK_COMMAND("DONE", 0, 0);
353 cont = 0;
354 }
355 }
14aa5a05 356
96f5fe4c
JH
357if (!found)
358 {
359 auth_defer_msg = string_sprintf(
360 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
361 goto out;
362 }
14aa5a05 363
96f5fe4c
JH
364/* Added by PH: data must not contain tab (as it is
365b64 it shouldn't, but check for safety). */
57c2c631 366
96f5fe4c
JH
367if (Ustrchr(data, '\t') != NULL)
368 {
369 ret = FAIL;
370 goto out;
371 }
14aa5a05 372
96f5fe4c
JH
373/* Added by PH: extra fields when TLS is in use or if the TCP/IP
374connection is local. */
6c588e74 375
96f5fe4c
JH
376if (tls_in.cipher != NULL)
377 auth_extra_data = string_sprintf("secured\t%s%s",
378 tls_in.certificate_verified? "valid-client-cert" : "",
379 tls_in.certificate_verified? "\t" : "");
14aa5a05 380
96f5fe4c
JH
381else if ( interface_address != NULL
382 && Ustrcmp(sender_host_address, interface_address) == 0)
383 auth_extra_data = US"secured\t";
14aa5a05 384
14aa5a05 385
96f5fe4c
JH
386/****************************************************************************
387The code below was the original code here. It didn't work. A reading of the
388file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
389this was not right. Maybe something changed. I changed it to move the
390service indication into the AUTH command, and it seems to be better. PH
391
392fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
393 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
394 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
395 ablock->public_name, sender_host_address, interface_address,
5903c6ff 396 data ? CS data : "");
96f5fe4c
JH
397
398Subsequently, the command was modified to add "secured" and "valid-client-
399cert" when relevant.
400****************************************************************************/
14aa5a05 401
96f5fe4c
JH
402auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
403 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
404 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
405 ablock->public_name, auth_extra_data, sender_host_address,
406 interface_address, data);
14aa5a05 407
96f5fe4c
JH
408if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
409 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
410 strerror(errno));
7befa435 411
96f5fe4c 412HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
7befa435 413
96f5fe4c
JH
414while (1)
415 {
416 uschar *temp;
417 uschar *auth_id_pre = NULL;
418 int i;
14aa5a05 419
96f5fe4c
JH
420 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
421 {
422 auth_defer_msg = US"authentication socket read error or premature eof";
423 goto out;
424 }
6c588e74 425
96f5fe4c
JH
426 buffer[Ustrlen(buffer) - 1] = 0;
427 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
428 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
6c588e74 429
96f5fe4c
JH
430 if (Uatoi(args[1]) != crequid)
431 OUT("authentication socket connection id mismatch");
14aa5a05 432
96f5fe4c
JH
433 switch (toupper(*args[0]))
434 {
435 case 'C':
436 CHECK_COMMAND("CONT", 1, 2);
437
438 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
439 {
440 ret = tmp;
441 goto out;
442 }
443
444 /* Added by PH: data must not contain tab (as it is
445 b64 it shouldn't, but check for safety). */
446
447 if (Ustrchr(data, '\t') != NULL)
448 {
449 ret = FAIL;
450 goto out;
451 }
452
453 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
454 if (write(fd, temp, Ustrlen(temp)) < 0)
455 OUT("authentication socket write error");
456 break;
457
458 case 'F':
459 CHECK_COMMAND("FAIL", 1, -1);
460
461 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
462 {
463 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
464 {
465 auth_id_pre = args[i]+5;
466 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
467 expand_nlength[1] = Ustrlen(auth_id_pre);
468 expand_nmax = 1;
469 }
470 }
471
472 ret = FAIL;
473 goto out;
474
475 case 'O':
476 CHECK_COMMAND("OK", 2, -1);
477
478 /* Search for the "user=$USER" string in the args array
479 and return the proper value. */
480
481 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
482 {
483 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
484 {
485 auth_id_pre = args[i]+5;
486 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
487 expand_nlength[1] = Ustrlen(auth_id_pre);
488 expand_nmax = 1;
489 }
490 }
491
492 if (auth_id_pre == NULL)
493 OUT("authentication socket protocol error, username missing");
494
495 ret = OK;
496 /* fallthrough */
497
498 default:
499 goto out;
500 }
501 }
14aa5a05 502
57c2c631 503out:
96f5fe4c
JH
504/* close the socket used by dovecot */
505if (fd >= 0)
506 close(fd);
16ff981e 507
96f5fe4c
JH
508/* Expand server_condition as an authorization check */
509return ret == OK ? auth_check_serv_cond(ablock) : ret;
14aa5a05 510}
d185889f
JH
511
512
513#endif /*!MACRO_PREDEF*/