Use dedicated union member for option offsets
[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[] = {
13a4b4c1
JH
54 { "server_socket", opt_stringptr,
55 OPT_OFF(auth_dovecot_options_block, server_socket)
14aa5a05
PH
56 },
57};
58
59/* Size of the options list. An extern variable has to be used so that its
60address can appear in the tables drtables.c. */
57c2c631 61
c71c454d 62int auth_dovecot_options_count = nelem(auth_dovecot_options);
14aa5a05
PH
63
64/* Default private options block for the authentication method. */
57c2c631 65
14aa5a05
PH
66auth_dovecot_options_block auth_dovecot_option_defaults = {
67 NULL, /* server_socket */
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 *
200dc_gets(uschar *s, int n, int fd)
201{
202int p = 0;
203int count = 0;
204
205for (;;)
206 {
3f1df0e3 207 if (socket_buffer_left == 0)
57c2c631 208 {
c9f1be94
BL
209 if ((socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer))) <= 0)
210 if (count == 0) return NULL; else break;
51473862 211 p = 0;
57c2c631
PH
212 }
213
3f1df0e3 214 while (p < socket_buffer_left)
57c2c631
PH
215 {
216 if (count >= n - 1) break;
217 s[count++] = sbuffer[p];
218 if (sbuffer[p++] == '\n') break;
219 }
220
3f1df0e3
PP
221 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
222 socket_buffer_left -= p;
57c2c631
PH
223
224 if (s[count-1] == '\n' || count >= n - 1) break;
225 }
226
3f1df0e3 227s[count] = '\0';
57c2c631
PH
228return s;
229}
230
231
232
233
234/*************************************************
235* Server entry point *
236*************************************************/
14aa5a05 237
96f5fe4c
JH
238int
239auth_dovecot_server(auth_instance * ablock, uschar * data)
14aa5a05 240{
96f5fe4c
JH
241auth_dovecot_options_block *ob =
242 (auth_dovecot_options_block *) ablock->options_block;
243struct sockaddr_un sa;
244uschar buffer[DOVECOT_AUTH_MAXLINELEN];
245uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
246uschar *auth_command;
247uschar *auth_extra_data = US"";
248uschar *p;
249int nargs, tmp;
77560253 250int crequid = 1, cont = 1, fd = -1, ret = DEFER;
96f5fe4c
JH
251BOOL found = FALSE, have_mech_line = FALSE;
252
253HDEBUG(D_auth) debug_printf("dovecot authentication\n");
254
255if (!data)
256 {
257 ret = FAIL;
258 goto out;
259 }
14aa5a05 260
96f5fe4c
JH
261memset(&sa, 0, sizeof(sa));
262sa.sun_family = AF_UNIX;
14aa5a05 263
96f5fe4c
JH
264/* This was the original code here: it is nonsense because strncpy()
265does not return an integer. I have converted this to use the function
266that formats and checks length. PH */
14aa5a05 267
96f5fe4c
JH
268/*
269if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
270}
271*/
14aa5a05 272
96f5fe4c
JH
273if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
274 ob->server_socket))
275 {
276 auth_defer_msg = US"authentication socket path too long";
277 return DEFER;
278 }
7befa435 279
96f5fe4c 280auth_defer_msg = US"authentication socket connection error";
7befa435 281
96f5fe4c
JH
282if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0)
283 return DEFER;
7befa435 284
96f5fe4c
JH
285if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
286 goto out;
7befa435 287
96f5fe4c 288auth_defer_msg = US"authentication socket protocol error";
14aa5a05 289
96f5fe4c
JH
290socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
291while (cont)
292 {
c71c454d 293 if (!dc_gets(buffer, sizeof(buffer), fd))
96f5fe4c
JH
294 OUT("authentication socket read error or premature eof");
295 p = buffer + Ustrlen(buffer) - 1;
296 if (*p != '\n')
297 OUT("authentication socket protocol line too long");
298
299 *p = '\0';
300 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
301
c71c454d 302 nargs = strcut(buffer, args, nelem(args));
96f5fe4c 303
c71c454d 304 /* HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args)); */
96f5fe4c
JH
305
306 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
307 Exim will need. Original code also failed if Dovecot server sent unknown
308 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
309 /* pdp: note that CUID is a per-connection identifier sent by the server,
310 which increments at server discretion.
311 By contrast, the "id" field of the protocol is a connection-specific request
312 identifier, which needs to be unique per request from the client and is not
313 connected to the CUID value, so we ignore CUID from server. It's purely for
314 diagnostics. */
315
316 if (Ustrcmp(args[0], US"VERSION") == 0)
317 {
318 CHECK_COMMAND("VERSION", 2, 2);
319 if (Uatoi(args[1]) != VERSION_MAJOR)
320 OUT("authentication socket protocol version mismatch");
321 }
322 else if (Ustrcmp(args[0], US"MECH") == 0)
323 {
324 CHECK_COMMAND("MECH", 1, INT_MAX);
325 have_mech_line = TRUE;
326 if (strcmpic(US args[1], ablock->public_name) == 0)
327 found = TRUE;
328 }
329 else if (Ustrcmp(args[0], US"SPID") == 0)
330 {
331 /* Unfortunately the auth protocol handshake wasn't designed well
332 to differentiate between auth-client/userdb/master. auth-userdb
333 and auth-master send VERSION + SPID lines only and nothing
334 afterwards, while auth-client sends VERSION + MECH + SPID +
335 CUID + more. The simplest way that we can determine if we've
336 connected to the correct socket is to see if MECH line exists or
337 not (alternatively we'd have to have a small timeout after SPID
338 to see if CUID is sent or not). */
339
340 if (!have_mech_line)
341 OUT("authentication socket type mismatch"
342 " (connected to auth-master instead of auth-client)");
343 }
344 else if (Ustrcmp(args[0], US"DONE") == 0)
345 {
346 CHECK_COMMAND("DONE", 0, 0);
347 cont = 0;
348 }
349 }
14aa5a05 350
96f5fe4c
JH
351if (!found)
352 {
353 auth_defer_msg = string_sprintf(
354 "Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
355 goto out;
356 }
14aa5a05 357
96f5fe4c
JH
358/* Added by PH: data must not contain tab (as it is
359b64 it shouldn't, but check for safety). */
57c2c631 360
96f5fe4c
JH
361if (Ustrchr(data, '\t') != NULL)
362 {
363 ret = FAIL;
364 goto out;
365 }
14aa5a05 366
96f5fe4c
JH
367/* Added by PH: extra fields when TLS is in use or if the TCP/IP
368connection is local. */
6c588e74 369
c71c454d 370if (tls_in.cipher)
96f5fe4c 371 auth_extra_data = string_sprintf("secured\t%s%s",
c71c454d
JH
372 tls_in.certificate_verified ? "valid-client-cert" : "",
373 tls_in.certificate_verified ? "\t" : "");
14aa5a05 374
c71c454d 375else if ( interface_address
96f5fe4c
JH
376 && Ustrcmp(sender_host_address, interface_address) == 0)
377 auth_extra_data = US"secured\t";
14aa5a05 378
14aa5a05 379
96f5fe4c
JH
380/****************************************************************************
381The code below was the original code here. It didn't work. A reading of the
382file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
383this was not right. Maybe something changed. I changed it to move the
384service indication into the AUTH command, and it seems to be better. PH
385
386fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
387 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
388 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
389 ablock->public_name, sender_host_address, interface_address,
5903c6ff 390 data ? CS data : "");
96f5fe4c
JH
391
392Subsequently, the command was modified to add "secured" and "valid-client-
393cert" when relevant.
394****************************************************************************/
14aa5a05 395
96f5fe4c
JH
396auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
397 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
398 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
399 ablock->public_name, auth_extra_data, sender_host_address,
400 interface_address, data);
14aa5a05 401
96f5fe4c
JH
402if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
403 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
404 strerror(errno));
7befa435 405
96f5fe4c 406HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
7befa435 407
96f5fe4c
JH
408while (1)
409 {
410 uschar *temp;
411 uschar *auth_id_pre = NULL;
14aa5a05 412
c71c454d 413 if (!dc_gets(buffer, sizeof(buffer), fd))
96f5fe4c
JH
414 {
415 auth_defer_msg = US"authentication socket read error or premature eof";
416 goto out;
417 }
6c588e74 418
96f5fe4c
JH
419 buffer[Ustrlen(buffer) - 1] = 0;
420 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
c71c454d 421 nargs = strcut(buffer, args, nelem(args));
6c588e74 422
96f5fe4c
JH
423 if (Uatoi(args[1]) != crequid)
424 OUT("authentication socket connection id mismatch");
14aa5a05 425
96f5fe4c
JH
426 switch (toupper(*args[0]))
427 {
428 case 'C':
429 CHECK_COMMAND("CONT", 1, 2);
430
431 if ((tmp = auth_get_no64_data(&data, US args[2])) != OK)
432 {
433 ret = tmp;
434 goto out;
435 }
436
437 /* Added by PH: data must not contain tab (as it is
438 b64 it shouldn't, but check for safety). */
439
440 if (Ustrchr(data, '\t') != NULL)
441 {
442 ret = FAIL;
443 goto out;
444 }
445
446 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
447 if (write(fd, temp, Ustrlen(temp)) < 0)
448 OUT("authentication socket write error");
449 break;
450
451 case 'F':
452 CHECK_COMMAND("FAIL", 1, -1);
453
c71c454d
JH
454 for (int i = 2; i < nargs && !auth_id_pre; i++)
455 if (Ustrncmp(args[i], US"user=", 5) == 0)
96f5fe4c 456 {
c71c454d 457 auth_id_pre = args[i] + 5;
96f5fe4c
JH
458 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
459 expand_nlength[1] = Ustrlen(auth_id_pre);
460 expand_nmax = 1;
461 }
96f5fe4c
JH
462 ret = FAIL;
463 goto out;
464
465 case 'O':
466 CHECK_COMMAND("OK", 2, -1);
467
468 /* Search for the "user=$USER" string in the args array
469 and return the proper value. */
470
c71c454d
JH
471 for (int i = 2; i < nargs && !auth_id_pre; i++)
472 if (Ustrncmp(args[i], US"user=", 5) == 0)
96f5fe4c 473 {
c71c454d 474 auth_id_pre = args[i] + 5;
96f5fe4c
JH
475 expand_nstring[1] = auth_vars[0] = string_copy(auth_id_pre); /* PH */
476 expand_nlength[1] = Ustrlen(auth_id_pre);
477 expand_nmax = 1;
478 }
96f5fe4c 479
c71c454d 480 if (!auth_id_pre)
96f5fe4c
JH
481 OUT("authentication socket protocol error, username missing");
482
483 ret = OK;
484 /* fallthrough */
485
486 default:
487 goto out;
488 }
489 }
14aa5a05 490
57c2c631 491out:
96f5fe4c
JH
492/* close the socket used by dovecot */
493if (fd >= 0)
494 close(fd);
16ff981e 495
96f5fe4c
JH
496/* Expand server_condition as an authorization check */
497return ret == OK ? auth_check_serv_cond(ablock) : ret;
14aa5a05 498}
d185889f
JH
499
500
501#endif /*!MACRO_PREDEF*/