Fix dovecot with empty 334 challenge.
[exim.git] / src / src / auths / dovecot.c
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
10 /* A number of modifications have been made to the original code. Originally I
11 commented them specially, but now they are getting quite extensive, so I have
12 ceased doing that. The biggest change is to use unbuffered I/O on the socket
13 because using C buffered I/O gives problems on some operating systems. PH */
14
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
23 #include "../exim.h"
24 #include "dovecot.h"
25
26 #define VERSION_MAJOR 1
27 #define VERSION_MINOR 0
28
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.
36 AUTH req C->S sends {"AUTH", id, mechanism, service } + params, 5 defined for
37 Dovecot 1; Dovecot 2 (same protocol version) defines 9.
38
39 Master->Server sends {"USER", id, userid} + params, 6 defined.
40 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
41
42 We only define here to accept S->C; max seen is 3+<unspecified>, plus the two
43 for the command and id, where unspecified might include _at least_ user=...
44
45 So: allow for more fields than we ever expect to see, while aware that count
46 can go up without changing protocol version.
47 The cost is the length of an array of pointers on the stack.
48 */
49 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
50
51 /* Options specific to the authentication mechanism. */
52 optionlist auth_dovecot_options[] = {
53 {
54 "server_socket",
55 opt_stringptr,
56 (void *)(offsetof(auth_dovecot_options_block, server_socket))
57 },
58 };
59
60 /* Size of the options list. An extern variable has to be used so that its
61 address can appear in the tables drtables.c. */
62
63 int auth_dovecot_options_count =
64 sizeof(auth_dovecot_options) / sizeof(optionlist);
65
66 /* Default private options block for the authentication method. */
67
68 auth_dovecot_options_block auth_dovecot_option_defaults = {
69 NULL, /* server_socket */
70 };
71
72
73 /* Static variables for reading from the socket */
74
75 static uschar sbuffer[256];
76 static int socket_buffer_left;
77
78
79
80 /*************************************************
81 * Initialization entry point *
82 *************************************************/
83
84 /* Called for each instance, after its options have been read, to
85 enable consistency checks to be done, or anything else that needs
86 to be set up. */
87
88 void 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
100 /*************************************************
101 * "strcut" to split apart server lines *
102 *************************************************/
103
104 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
105 of a command-name, TAB, and then any parameters, each separated by a TAB.
106 A parameter can be param=value or a bool, just param.
107
108 This function modifies the original str in-place, inserting NUL characters.
109 It initialises ptrs entries, setting all to NULL and only setting
110 non-NULL N entries, where N is the return value, the number of fields seen
111 (one more than the number of tabs).
112
113 Note that the return value will always be at least 1, is the count of
114 actual fields (so last valid offset into ptrs is one less).
115 */
116
117 static int
118 strcut(uschar *str, uschar **ptrs, int nptrs)
119 {
120 uschar *last_sub_start = str;
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) {
130 *ptrs++ = last_sub_start;
131 last_sub_start = str + 1;
132 *str = '\0';
133 }
134 n++;
135 }
136 str++;
137 }
138
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;
144 } else {
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;
147 }
148
149 return n <= nptrs ? n : nptrs;
150 }
151
152 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
153 static void
154 debug_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");
166 }
167
168 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
169 if (strcmpic(US(str), args[0]) != 0) \
170 goto out; \
171 if (nargs - 1 < (arg_min)) \
172 goto out; \
173 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
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
182
183
184 /*************************************************
185 * "fgets" to read directly from socket *
186 *************************************************/
187
188 /* Added by PH after a suggestion by Steve Usher because the previous use of
189 C-style buffered I/O gave trouble. */
190
191 static uschar *
192 dc_gets(uschar *s, int n, int fd)
193 {
194 int p = 0;
195 int count = 0;
196
197 for (;;)
198 {
199 if (socket_buffer_left == 0)
200 {
201 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
202 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
203 p = 0;
204 }
205
206 while (p < socket_buffer_left)
207 {
208 if (count >= n - 1) break;
209 s[count++] = sbuffer[p];
210 if (sbuffer[p++] == '\n') break;
211 }
212
213 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
214 socket_buffer_left -= p;
215
216 if (s[count-1] == '\n' || count >= n - 1) break;
217 }
218
219 s[count] = '\0';
220 return s;
221 }
222
223
224
225
226 /*************************************************
227 * Server entry point *
228 *************************************************/
229
230 int 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;
235 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
236 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
237 uschar *auth_command;
238 uschar *auth_extra_data = US"";
239 uschar *p;
240 int nargs, tmp;
241 int crequid = 1, cont = 1, fd, ret = DEFER;
242 BOOL found = FALSE;
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
272 auth_defer_msg = US"authentication socket protocol error";
273
274 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
275 while (cont) {
276 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
277 OUT("authentication socket read error or premature eof");
278 p = buffer + Ustrlen(buffer) - 1;
279 if (*p != '\n') {
280 OUT("authentication socket protocol line too long");
281 }
282 *p = '\0';
283 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
284 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
285 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
286
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. */
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) {
297 CHECK_COMMAND("VERSION", 2, 2);
298 if (Uatoi(args[1]) != VERSION_MAJOR)
299 OUT("authentication socket protocol version mismatch");
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)
303 found = TRUE;
304 } else if (Ustrcmp(args[0], US"DONE") == 0) {
305 CHECK_COMMAND("DONE", 0, 0);
306 cont = 0;
307 }
308 }
309
310 if (!found) {
311 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
312 goto out;
313 }
314
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
326 if (tls_in.cipher != NULL)
327 auth_extra_data = string_sprintf("secured\t%s%s",
328 tls_in.certificate_verified? "valid-client-cert" : "",
329 tls_in.certificate_verified? "\t" : "");
330 else if (interface_address != NULL &&
331 Ustrcmp(sender_host_address, interface_address) == 0)
332 auth_extra_data = US"secured\t";
333
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
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
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 : "");
346
347 Subsequently, the command was modified to add "secured" and "valid-client-
348 cert" when relevant.
349 ****************************************************************************/
350
351 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
352 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
353 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
354 ablock->public_name, auth_extra_data, sender_host_address,
355 interface_address, data ? (char *) data : "");
356
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
361 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
362
363 while (1) {
364 uschar *temp;
365 uschar *auth_id_pre = NULL;
366 int i;
367
368 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
369 auth_defer_msg = US"authentication socket read error or premature eof";
370 goto out;
371 }
372
373 buffer[Ustrlen(buffer) - 1] = 0;
374 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
375 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
376
377 if (Uatoi(args[1]) != crequid)
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
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
398 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
399 if (write(fd, temp, Ustrlen(temp)) < 0)
400 OUT("authentication socket write error");
401 break;
402
403 case 'F':
404 CHECK_COMMAND("FAIL", 1, -1);
405
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;
411 expand_nstring[1] = auth_vars[0] =
412 string_copy(auth_id_pre); /* PH */
413 expand_nlength[1] = Ustrlen(auth_id_pre);
414 expand_nmax = 1;
415 }
416 }
417
418 ret = FAIL;
419 goto out;
420
421 case 'O':
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++)
429 {
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 }
438 }
439
440 if (auth_id_pre == NULL)
441 OUT("authentication socket protocol error, username missing");
442
443 ret = OK;
444 /* fallthrough */
445
446 default:
447 goto out;
448 }
449 }
450
451 out:
452 /* close the socket used by dovecot */
453 if (fd >= 0)
454 close(fd);
455
456 /* Expand server_condition as an authorization check */
457 return (ret == OK)? auth_check_serv_cond(ablock) : ret;
458 }