Copyright updates:
[exim.git] / src / src / auths / dovecot.c
CommitLineData
14aa5a05
PH
1/*
2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
1e1ddfac 3 * Copyright (c) 2006-2020 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[] = {
7d2f2d36
JH
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) },*/
14aa5a05
PH
56};
57
58/* Size of the options list. An extern variable has to be used so that its
59address can appear in the tables drtables.c. */
57c2c631 60
c71c454d 61int auth_dovecot_options_count = nelem(auth_dovecot_options);
14aa5a05
PH
62
63/* Default private options block for the authentication method. */
57c2c631 64
14aa5a05 65auth_dovecot_options_block auth_dovecot_option_defaults = {
7d2f2d36
JH
66 .server_socket = NULL,
67/* .server_tls = FALSE,*/
14aa5a05
PH
68};
69
57c2c631 70
d185889f
JH
71
72
73#ifdef MACRO_PREDEF
74
75/* Dummy values */
76void auth_dovecot_init(auth_instance *ablock) {}
77int auth_dovecot_server(auth_instance *ablock, uschar *data) {return 0;}
251b9eb4
JH
78int auth_dovecot_client(auth_instance *ablock, void * sx,
79 int timeout, uschar *buffer, int buffsize) {return 0;}
d185889f
JH
80
81#else /*!MACRO_PREDEF*/
82
83
57c2c631
PH
84/* Static variables for reading from the socket */
85
86static uschar sbuffer[256];
3f1df0e3 87static int socket_buffer_left;
57c2c631
PH
88
89
90
14aa5a05
PH
91/*************************************************
92 * Initialization entry point *
93 *************************************************/
94
95/* Called for each instance, after its options have been read, to
96enable consistency checks to be done, or anything else that needs
97to be set up. */
57c2c631 98
14aa5a05
PH
99void auth_dovecot_init(auth_instance *ablock)
100{
c71c454d
JH
101auth_dovecot_options_block *ob =
102 (auth_dovecot_options_block *)(ablock->options_block);
103
104if (!ablock->public_name) ablock->public_name = ablock->name;
105if (ob->server_socket) ablock->server = TRUE;
106ablock->client = FALSE;
14aa5a05
PH
107}
108
3f1df0e3
PP
109/*************************************************
110 * "strcut" to split apart server lines *
111 *************************************************/
112
113/* Dovecot auth protocol uses TAB \t as delimiter; a line consists
114of a command-name, TAB, and then any parameters, each separated by a TAB.
115A parameter can be param=value or a bool, just param.
116
117This function modifies the original str in-place, inserting NUL characters.
118It initialises ptrs entries, setting all to NULL and only setting
119non-NULL N entries, where N is the return value, the number of fields seen
120(one more than the number of tabs).
121
122Note that the return value will always be at least 1, is the count of
123actual fields (so last valid offset into ptrs is one less).
124*/
125
126static int
127strcut(uschar *str, uschar **ptrs, int nptrs)
14aa5a05 128{
d7978c0f
JH
129uschar *last_sub_start = str;
130int n;
131
132for (n = 0; n < nptrs; n++)
133 ptrs[n] = NULL;
134n = 1;
135
136while (*str)
c71c454d
JH
137 if (*str++ == '\t')
138 if (n++ <= nptrs)
d7978c0f
JH
139 {
140 *ptrs++ = last_sub_start;
c71c454d
JH
141 last_sub_start = str;
142 str[-1] = '\0';
d7978c0f 143 }
d7978c0f
JH
144
145/* It's acceptable for the string to end with a tab character. We see
146this in AUTH PLAIN without an initial response from the client, which
147causing us to send "334 " and get the data from the client. */
148if (n <= nptrs)
c71c454d 149 *ptrs = last_sub_start;
d7978c0f 150else
c71c454d
JH
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 }
d7978c0f
JH
157
158return n <= nptrs ? n : nptrs;
3f1df0e3 159}
14aa5a05 160
3f1df0e3
PP
161static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
162static void
163debug_strcut(uschar **ptrs, int nlen, int alen)
164{
d7978c0f
JH
165int i;
166debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
167 socket_buffer_left, nlen);
168for (i = 0; i < nlen; i++)
169 debug_printf(" {%s}", ptrs[i]);
170if (nlen < alen)
171 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
172else
173 debug_printf(" (max for capacity)\n");
14aa5a05
PH
174}
175
176#define CHECK_COMMAND(str, arg_min, arg_max) do { \
57c2c631 177 if (strcmpic(US(str), args[0]) != 0) \
14aa5a05
PH
178 goto out; \
179 if (nargs - 1 < (arg_min)) \
180 goto out; \
6c588e74 181 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
14aa5a05
PH
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
7befa435
PH
190
191
14aa5a05 192/*************************************************
57c2c631
PH
193* "fgets" to read directly from socket *
194*************************************************/
195
196/* Added by PH after a suggestion by Steve Usher because the previous use of
197C-style buffered I/O gave trouble. */
198
199static uschar *
7d2f2d36 200dc_gets(uschar *s, int n, client_conn_ctx * cctx)
57c2c631
PH
201{
202int p = 0;
203int count = 0;
204
205for (;;)
206 {
3f1df0e3 207 if (socket_buffer_left == 0)
57c2c631 208 {
7d2f2d36
JH
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;
51473862 218 p = 0;
57c2c631
PH
219 }
220
3f1df0e3 221 while (p < socket_buffer_left)
57c2c631
PH
222 {
223 if (count >= n - 1) break;
224 s[count++] = sbuffer[p];
225 if (sbuffer[p++] == '\n') break;
226 }
227
3f1df0e3
PP
228 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
229 socket_buffer_left -= p;
57c2c631
PH
230
231 if (s[count-1] == '\n' || count >= n - 1) break;
232 }
233
3f1df0e3 234s[count] = '\0';
57c2c631
PH
235return s;
236}
237
238
239
240
241/*************************************************
242* Server entry point *
243*************************************************/
14aa5a05 244
96f5fe4c
JH
245int
246auth_dovecot_server(auth_instance * ablock, uschar * data)
14aa5a05 247{
96f5fe4c
JH
248auth_dovecot_options_block *ob =
249 (auth_dovecot_options_block *) ablock->options_block;
96f5fe4c
JH
250uschar buffer[DOVECOT_AUTH_MAXLINELEN];
251uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
252uschar *auth_command;
253uschar *auth_extra_data = US"";
254uschar *p;
255int nargs, tmp;
7d2f2d36
JH
256int crequid = 1, ret = DEFER;
257host_item host;
258client_conn_ctx cctx = {.sock = -1, .tls_ctx = NULL};
96f5fe4c
JH
259BOOL found = FALSE, have_mech_line = FALSE;
260
261HDEBUG(D_auth) debug_printf("dovecot authentication\n");
262
263if (!data)
264 {
265 ret = FAIL;
266 goto out;
267 }
14aa5a05 268
7d2f2d36
JH
269/*XXX timeout? */
270cctx.sock = ip_streamsocket(ob->server_socket, &auth_defer_msg, 5, &host);
271if (cctx.sock < 0)
272 goto out;
14aa5a05 273
7d2f2d36
JH
274#ifdef notdef
275# ifndef DISABLE_TLS
276if (ob->server_tls)
96f5fe4c 277 {
7d2f2d36
JH
278 uschar * s;
279 smtp_connect_args conn_args = { .host = &host };
280 tls_support tls_dummy = {.sni=NULL};
281 uschar * errstr;
7befa435 282
7d2f2d36
JH
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
7befa435 291
96f5fe4c 292auth_defer_msg = US"authentication socket protocol error";
14aa5a05 293
96f5fe4c 294socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
7d2f2d36 295for (;;)
96f5fe4c 296 {
7d2f2d36
JH
297debug_printf("%s %d\n", __FUNCTION__, __LINE__);
298 if (!dc_gets(buffer, sizeof(buffer), &cctx))
96f5fe4c 299 OUT("authentication socket read error or premature eof");
7d2f2d36 300debug_printf("%s %d\n", __FUNCTION__, __LINE__);
96f5fe4c
JH
301 p = buffer + Ustrlen(buffer) - 1;
302 if (*p != '\n')
303 OUT("authentication socket protocol line too long");
304
305 *p = '\0';
7d2f2d36 306 HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
96f5fe4c 307
c71c454d 308 nargs = strcut(buffer, args, nelem(args));
96f5fe4c 309
7d2f2d36 310 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
96f5fe4c
JH
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);
7d2f2d36 353 break;
96f5fe4c
JH
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
c71c454d 376if (tls_in.cipher)
96f5fe4c 377 auth_extra_data = string_sprintf("secured\t%s%s",
c71c454d
JH
378 tls_in.certificate_verified ? "valid-client-cert" : "",
379 tls_in.certificate_verified ? "\t" : "");
14aa5a05 380
c71c454d 381else if ( interface_address
96f5fe4c
JH
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
7d2f2d36
JH
408if ((
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)
96f5fe4c
JH
413 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
414 strerror(errno));
7befa435 415
7d2f2d36 416HDEBUG(D_auth) debug_printf("sent: '%s'\n", auth_command);
7befa435 417
96f5fe4c
JH
418while (1)
419 {
420 uschar *temp;
421 uschar *auth_id_pre = NULL;
14aa5a05 422
7d2f2d36 423 if (!dc_gets(buffer, sizeof(buffer), &cctx))
96f5fe4c
JH
424 {
425 auth_defer_msg = US"authentication socket read error or premature eof";
426 goto out;
427 }
6c588e74 428
96f5fe4c 429 buffer[Ustrlen(buffer) - 1] = 0;
7d2f2d36 430 HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
c71c454d 431 nargs = strcut(buffer, args, nelem(args));
7d2f2d36 432 HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
6c588e74 433
96f5fe4c
JH
434 if (Uatoi(args[1]) != crequid)
435 OUT("authentication socket connection id mismatch");
14aa5a05 436
96f5fe4c
JH
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);
7d2f2d36
JH
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)
96f5fe4c
JH
463 OUT("authentication socket write error");
464 break;
465
466 case 'F':
467 CHECK_COMMAND("FAIL", 1, -1);
468
c71c454d
JH
469 for (int i = 2; i < nargs && !auth_id_pre; i++)
470 if (Ustrncmp(args[i], US"user=", 5) == 0)
96f5fe4c 471 {
c71c454d 472 auth_id_pre = args[i] + 5;
96f5fe4c
JH
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 }
96f5fe4c
JH
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
c71c454d
JH
486 for (int i = 2; i < nargs && !auth_id_pre; i++)
487 if (Ustrncmp(args[i], US"user=", 5) == 0)
96f5fe4c 488 {
c71c454d 489 auth_id_pre = args[i] + 5;
96f5fe4c
JH
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 }
96f5fe4c 494
c71c454d 495 if (!auth_id_pre)
96f5fe4c
JH
496 OUT("authentication socket protocol error, username missing");
497
7d2f2d36 498 auth_defer_msg = NULL;
96f5fe4c
JH
499 ret = OK;
500 /* fallthrough */
501
502 default:
503 goto out;
504 }
505 }
14aa5a05 506
57c2c631 507out:
96f5fe4c 508/* close the socket used by dovecot */
7d2f2d36
JH
509#ifndef DISABLE_TLS
510if (cctx.tls_ctx)
511 tls_close(cctx.tls_ctx, TRUE);
512#endif
513if (cctx.sock >= 0)
514 close(cctx.sock);
16ff981e 515
96f5fe4c
JH
516/* Expand server_condition as an authorization check */
517return ret == OK ? auth_check_serv_cond(ablock) : ret;
14aa5a05 518}
d185889f
JH
519
520
521#endif /*!MACRO_PREDEF*/