Merge tag 'exim-4_82_1'
[exim.git] / src / src / auths / dovecot.c
1 /*
2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3 * Copyright (c) 2006-2014 The Exim Maintainers
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
12 commented them specially, but now they are getting quite extensive, so I have
13 ceased doing that. The biggest change is to use unbuffered I/O on the socket
14 because 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.
37 AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
38 Dovecot 1; Dovecot 2 (same protocol version) defines 9.
39
40 Master->Server sends {"USER", id, userid} + params, 6 defined.
41 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
42
43 We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
44 for the command and id, where unspecified might include _at least_ user=...
45
46 So: allow for more fields than we ever expect to see, while aware that count
47 can go up without changing protocol version.
48 The 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. */
53 optionlist 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
62 address can appear in the tables drtables.c. */
63
64 int auth_dovecot_options_count =
65 sizeof(auth_dovecot_options) / sizeof(optionlist);
66
67 /* Default private options block for the authentication method. */
68
69 auth_dovecot_options_block auth_dovecot_option_defaults = {
70 NULL, /* server_socket */
71 };
72
73
74 /* Static variables for reading from the socket */
75
76 static uschar sbuffer[256];
77 static 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
86 enable consistency checks to be done, or anything else that needs
87 to be set up. */
88
89 void 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
106 of a command-name, TAB, and then any parameters, each separated by a TAB.
107 A parameter can be param=value or a bool, just param.
108
109 This function modifies the original str in-place, inserting NUL characters.
110 It initialises ptrs entries, setting all to NULL and only setting
111 non-NULL N entries, where N is the return value, the number of fields seen
112 (one more than the number of tabs).
113
114 Note that the return value will always be at least 1, is the count of
115 actual fields (so last valid offset into ptrs is one less).
116 */
117
118 static int
119 strcut(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
153 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
154 static void
155 debug_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
190 C-style buffered I/O gave trouble. */
191
192 static uschar *
193 dc_gets(uschar *s, int n, int fd)
194 {
195 int p = 0;
196 int count = 0;
197
198 for (;;)
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
220 s[count] = '\0';
221 return s;
222 }
223
224
225
226
227 /*************************************************
228 * Server entry point *
229 *************************************************/
230
231 int 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;
236 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
237 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
238 uschar *auth_command;
239 uschar *auth_extra_data = US"";
240 uschar *p;
241 int nargs, tmp;
242 int crequid = 1, cont = 1, fd, ret = DEFER;
243 BOOL found = FALSE;
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
273 auth_defer_msg = US"authentication socket protocol error";
274
275 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
276 while (cont) {
277 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
278 OUT("authentication socket read error or premature eof");
279 p = buffer + Ustrlen(buffer) - 1;
280 if (*p != '\n') {
281 OUT("authentication socket protocol line too long");
282 }
283 *p = '\0';
284 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
285 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
286 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
287
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. */
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) {
298 CHECK_COMMAND("VERSION", 2, 2);
299 if (Uatoi(args[1]) != VERSION_MAJOR)
300 OUT("authentication socket protocol version mismatch");
301 } else if (Ustrcmp(args[0], US"MECH") == 0) {
302 CHECK_COMMAND("MECH", 1, INT_MAX);
303 if (strcmpic(US args[1], ablock->public_name) == 0)
304 found = TRUE;
305 } else if (Ustrcmp(args[0], US"DONE") == 0) {
306 CHECK_COMMAND("DONE", 0, 0);
307 cont = 0;
308 }
309 }
310
311 if (!found) {
312 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
313 goto out;
314 }
315
316 /* Added by PH: data must not contain tab (as it is
317 b64 it shouldn't, but check for safety). */
318
319 if (Ustrchr(data, '\t') != NULL) {
320 ret = FAIL;
321 goto out;
322 }
323
324 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
325 connection is local. */
326
327 if (tls_in.cipher != NULL)
328 auth_extra_data = string_sprintf("secured\t%s%s",
329 tls_in.certificate_verified? "valid-client-cert" : "",
330 tls_in.certificate_verified? "\t" : "");
331 else if (interface_address != NULL &&
332 Ustrcmp(sender_host_address, interface_address) == 0)
333 auth_extra_data = US"secured\t";
334
335
336 /****************************************************************************
337 The code below was the original code here. It didn't work. A reading of the
338 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
339 this was not right. Maybe something changed. I changed it to move the
340 service indication into the AUTH command, and it seems to be better. PH
341
342 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
343 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
344 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
345 ablock->public_name, sender_host_address, interface_address,
346 data ? (char *) data : "");
347
348 Subsequently, the command was modified to add "secured" and "valid-client-
349 cert" when relevant.
350 ****************************************************************************/
351
352 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
353 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
354 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
355 ablock->public_name, auth_extra_data, sender_host_address,
356 interface_address, data ? (char *) data : "");
357
358 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
359 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
360 strerror(errno));
361
362 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
363
364 while (1) {
365 uschar *temp;
366 uschar *auth_id_pre = NULL;
367 int i;
368
369 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
370 auth_defer_msg = US"authentication socket read error or premature eof";
371 goto out;
372 }
373
374 buffer[Ustrlen(buffer) - 1] = 0;
375 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
376 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
377
378 if (Uatoi(args[1]) != crequid)
379 OUT("authentication socket connection id mismatch");
380
381 switch (toupper(*args[0])) {
382 case 'C':
383 CHECK_COMMAND("CONT", 1, 2);
384
385 tmp = auth_get_no64_data(&data, US args[2]);
386 if (tmp != OK) {
387 ret = tmp;
388 goto out;
389 }
390
391 /* Added by PH: data must not contain tab (as it is
392 b64 it shouldn't, but check for safety). */
393
394 if (Ustrchr(data, '\t') != NULL) {
395 ret = FAIL;
396 goto out;
397 }
398
399 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
400 if (write(fd, temp, Ustrlen(temp)) < 0)
401 OUT("authentication socket write error");
402 break;
403
404 case 'F':
405 CHECK_COMMAND("FAIL", 1, -1);
406
407 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
408 {
409 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
410 {
411 auth_id_pre = args[i]+5;
412 expand_nstring[1] = auth_vars[0] =
413 string_copy(auth_id_pre); /* PH */
414 expand_nlength[1] = Ustrlen(auth_id_pre);
415 expand_nmax = 1;
416 }
417 }
418
419 ret = FAIL;
420 goto out;
421
422 case 'O':
423 CHECK_COMMAND("OK", 2, -1);
424
425 /*
426 * Search for the "user=$USER" string in the args array
427 * and return the proper value.
428 */
429 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
430 {
431 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
432 {
433 auth_id_pre = args[i]+5;
434 expand_nstring[1] = auth_vars[0] =
435 string_copy(auth_id_pre); /* PH */
436 expand_nlength[1] = Ustrlen(auth_id_pre);
437 expand_nmax = 1;
438 }
439 }
440
441 if (auth_id_pre == NULL)
442 OUT("authentication socket protocol error, username missing");
443
444 ret = OK;
445 /* fallthrough */
446
447 default:
448 goto out;
449 }
450 }
451
452 out:
453 /* close the socket used by dovecot */
454 if (fd >= 0)
455 close(fd);
456
457 /* Expand server_condition as an authorization check */
458 return (ret == OK)? auth_check_serv_cond(ablock) : ret;
459 }