Dovecot auth: inet socket. Bug 2280
[exim.git] / src / src / auths / dovecot.c
1 /*
2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
3 * Copyright (c) 2006-2019 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 { "server_socket", opt_stringptr, OPT_OFF(auth_dovecot_options_block, server_socket) },
55 /*{ "server_tls", opt_bool, OPT_OFF(auth_dovecot_options_block, server_tls) },*/
56 };
57
58 /* Size of the options list. An extern variable has to be used so that its
59 address can appear in the tables drtables.c. */
60
61 int auth_dovecot_options_count = nelem(auth_dovecot_options);
62
63 /* Default private options block for the authentication method. */
64
65 auth_dovecot_options_block auth_dovecot_option_defaults = {
66 .server_socket = NULL,
67 /* .server_tls = FALSE,*/
68 };
69
70
71
72
73 #ifdef MACRO_PREDEF
74
75 /* Dummy values */
76 void auth_dovecot_init(auth_instance *ablock) {}
77 int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
78 int auth_dovecot_client(auth_instance *ablock, void * sx,
79 int timeout, uschar *buffer, int buffsize) {return 0;}
80
81 #else /*!MACRO_PREDEF*/
82
83
84 /* Static variables for reading from the socket */
85
86 static uschar sbuffer[256];
87 static int socket_buffer_left;
88
89
90
91 /*************************************************
92 * Initialization entry point *
93 *************************************************/
94
95 /* Called for each instance, after its options have been read, to
96 enable consistency checks to be done, or anything else that needs
97 to be set up. */
98
99 void auth_dovecot_init(auth_instance *ablock)
100 {
101 auth_dovecot_options_block *ob =
102 (auth_dovecot_options_block *)(ablock->options_block);
103
104 if (!ablock->public_name) ablock->public_name = ablock->name;
105 if (ob->server_socket) ablock->server = TRUE;
106 ablock->client = FALSE;
107 }
108
109 /*************************************************
110 * "strcut" to split apart server lines *
111 *************************************************/
112
113 /* Dovecot auth protocol uses TAB \t as delimiter; a line consists
114 of a command-name, TAB, and then any parameters, each separated by a TAB.
115 A parameter can be param=value or a bool, just param.
116
117 This function modifies the original str in-place, inserting NUL characters.
118 It initialises ptrs entries, setting all to NULL and only setting
119 non-NULL N entries, where N is the return value, the number of fields seen
120 (one more than the number of tabs).
121
122 Note that the return value will always be at least 1, is the count of
123 actual fields (so last valid offset into ptrs is one less).
124 */
125
126 static int
127 strcut(uschar *str, uschar **ptrs, int nptrs)
128 {
129 uschar *last_sub_start = str;
130 int n;
131
132 for (n = 0; n < nptrs; n++)
133 ptrs[n] = NULL;
134 n = 1;
135
136 while (*str)
137 if (*str++ == '\t')
138 if (n++ <= nptrs)
139 {
140 *ptrs++ = last_sub_start;
141 last_sub_start = str;
142 str[-1] = '\0';
143 }
144
145 /* It's acceptable for the string to end with a tab character. We see
146 this in AUTH PLAIN without an initial response from the client, which
147 causing us to send "334 " and get the data from the client. */
148 if (n <= nptrs)
149 *ptrs = last_sub_start;
150 else
151 {
152 HDEBUG(D_auth)
153 debug_printf("dovecot: warning: too many results from tab-splitting;"
154 " saw %d fields, room for %d\n", n, nptrs);
155 n = nptrs;
156 }
157
158 return n <= nptrs ? n : nptrs;
159 }
160
161 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
162 static void
163 debug_strcut(uschar **ptrs, int nlen, int alen)
164 {
165 int i;
166 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
167 socket_buffer_left, nlen);
168 for (i = 0; i < nlen; i++)
169 debug_printf(" {%s}", ptrs[i]);
170 if (nlen < alen)
171 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
172 else
173 debug_printf(" (max for capacity)\n");
174 }
175
176 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
177 if (strcmpic(US(str), args[0]) != 0) \
178 goto out; \
179 if (nargs - 1 < (arg_min)) \
180 goto out; \
181 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
182 goto out; \
183 } while (0)
184
185 #define OUT(msg) do { \
186 auth_defer_msg = (US msg); \
187 goto out; \
188 } while(0)
189
190
191
192 /*************************************************
193 * "fgets" to read directly from socket *
194 *************************************************/
195
196 /* Added by PH after a suggestion by Steve Usher because the previous use of
197 C-style buffered I/O gave trouble. */
198
199 static uschar *
200 dc_gets(uschar *s, int n, client_conn_ctx * cctx)
201 {
202 int p = 0;
203 int count = 0;
204
205 for (;;)
206 {
207 if (socket_buffer_left == 0)
208 {
209 if ((socket_buffer_left =
210 #ifndef DISABLE_TLS
211 cctx->tls_ctx ? tls_read(cctx->tls_ctx, sbuffer, sizeof(sbuffer)) :
212 #endif
213 read(cctx->sock, sbuffer, sizeof(sbuffer))) <= 0)
214 if (count == 0)
215 return NULL;
216 else
217 break;
218 p = 0;
219 }
220
221 while (p < socket_buffer_left)
222 {
223 if (count >= n - 1) break;
224 s[count++] = sbuffer[p];
225 if (sbuffer[p++] == '\n') break;
226 }
227
228 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
229 socket_buffer_left -= p;
230
231 if (s[count-1] == '\n' || count >= n - 1) break;
232 }
233
234 s[count] = '\0';
235 return s;
236 }
237
238
239
240
241 /*************************************************
242 * Server entry point *
243 *************************************************/
244
245 int
246 auth_dovecot_server(auth_instance * ablock, uschar * data)
247 {
248 auth_dovecot_options_block *ob =
249 (auth_dovecot_options_block *) ablock->options_block;
250 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
251 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
252 uschar *auth_command;
253 uschar *auth_extra_data = US"";
254 uschar *p;
255 int nargs, tmp;
256 int crequid = 1, ret = DEFER;
257 host_item host;
258 client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
259 BOOL found = FALSE, have_mech_line = FALSE;
260
261 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
262
263 if (!data)
264 {
265 ret = FAIL;
266 goto out;
267 }
268
269 /*XXX timeout? */
270 cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
271 if (cctx.sock < 0)
272 goto out;
273
274 #ifdef notdef
275 # ifndef DISABLE_TLS
276 if (ob->server_tls)
277 {
278 uschar * s;
279 smtp_connect_args conn_args = { .host = &host };
280 tls_support tls_dummy = {.sni=NULL};
281 uschar * errstr;
282
283 if (!tls_client_start(&cctx, &conn_args, NULL, &tls_dummy, &errstr))
284 {
285 auth_defer_msg = string_sprintf("TLS connect failed: %s", errstr);
286 goto out;
287 }
288 }
289 # endif
290 #endif
291
292 auth_defer_msg = US"authentication socket protocol error";
293
294 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
295 for (;;)
296 {
297 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
298 if (!dc_gets(buffer, sizeof(buffer), &cctx))
299 OUT("authentication socket read error or premature eof");
300 debug_printf("%s %d\n", __FUNCTION__, __LINE__);
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, nelem(args));
309
310 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
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 break;
354 }
355 }
356
357 if (!found)
358 {
359 auth_defer_msg = string_sprintf(
360 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
361 goto out;
362 }
363
364 /* Added by PH: data must not contain tab (as it is
365 b64 it shouldn't, but check for safety). */
366
367 if (Ustrchr(data, '\t') != NULL)
368 {
369 ret = FAIL;
370 goto out;
371 }
372
373 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
374 connection is local. */
375
376 if (tls_in.cipher)
377 auth_extra_data = string_sprintf("secured\t%s%s",
378 tls_in.certificate_verified ? "valid-client-cert" : "",
379 tls_in.certificate_verified ? "\t" : "");
380
381 else if ( interface_address
382 && Ustrcmp(sender_host_address, interface_address) == 0)
383 auth_extra_data = US"secured\t";
384
385
386 /****************************************************************************
387 The code below was the original code here. It didn't work. A reading of the
388 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
389 this was not right. Maybe something changed. I changed it to move the
390 service indication into the AUTH command, and it seems to be better. PH
391
392 fprintf(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,
396 data ? CS data : "");
397
398 Subsequently, the command was modified to add "secured" and "valid-client-
399 cert" when relevant.
400 ****************************************************************************/
401
402 auth_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);
407
408 if ((
409 #ifndef DISABLE_TLS
410 cctx.tls_ctx ? tls_write(cctx.tls_ctx, auth_command, Ustrlen(auth_command), FALSE) :
411 #endif
412 write(cctx.sock, auth_command, Ustrlen(auth_command))) < 0)
413 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
414 strerror(errno));
415
416 HDEBUG(D_auth) debug_printf("sent: '%s'\n", auth_command);
417
418 while (1)
419 {
420 uschar *temp;
421 uschar *auth_id_pre = NULL;
422
423 if (!dc_gets(buffer, sizeof(buffer), &cctx))
424 {
425 auth_defer_msg = US"authentication socket read error or premature eof";
426 goto out;
427 }
428
429 buffer[Ustrlen(buffer) - 1] = 0;
430 HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
431 nargs = strcut(buffer, args, nelem(args));
432 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
433
434 if (Uatoi(args[1]) != crequid)
435 OUT("authentication socket connection id mismatch");
436
437 switch (toupper(*args[0]))
438 {
439 case 'C':
440 CHECK_COMMAND("CONT", 1, 2);
441
442 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
443 {
444 ret = tmp;
445 goto out;
446 }
447
448 /* Added by PH: data must not contain tab (as it is
449 b64 it shouldn't, but check for safety). */
450
451 if (Ustrchr(data, '\t') != NULL)
452 {
453 ret = FAIL;
454 goto out;
455 }
456
457 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
458 if ((
459 #ifndef DISABLE_TLS
460 cctx.tls_ctx ? tls_write(cctx.tls_ctx, temp, Ustrlen(temp), FALSE) :
461 #endif
462 write(cctx.sock, temp, Ustrlen(temp))) < 0)
463 OUT("authentication socket write error");
464 break;
465
466 case 'F':
467 CHECK_COMMAND("FAIL", 1, -1);
468
469 for (int i = 2; i < nargs && !auth_id_pre; i++)
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 ret = FAIL;
478 goto out;
479
480 case 'O':
481 CHECK_COMMAND("OK", 2, -1);
482
483 /* Search for the "user=$USER" string in the args array
484 and return the proper value. */
485
486 for (int i = 2; i < nargs && !auth_id_pre; i++)
487 if (Ustrncmp(args[i], US"user=", 5) == 0)
488 {
489 auth_id_pre = args[i] + 5;
490 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
491 expand_nlength[1] = Ustrlen(auth_id_pre);
492 expand_nmax = 1;
493 }
494
495 if (!auth_id_pre)
496 OUT("authentication socket protocol error, username missing");
497
498 auth_defer_msg = NULL;
499 ret = OK;
500 /* fallthrough */
501
502 default:
503 goto out;
504 }
505 }
506
507 out:
508 /* close the socket used by dovecot */
509 #ifndef DISABLE_TLS
510 if (cctx.tls_ctx)
511 tls_close(cctx.tls_ctx, TRUE);
512 #endif
513 if (cctx.sock >= 0)
514 close(cctx.sock);
515
516 /* Expand server_condition as an authorization check */
517 return ret == OK ? auth_check_serv_cond(ablock) : ret;
518 }
519
520
521 #endif /*!MACRO_PREDEF*/