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