GnuTLS website moves
[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 uschar *lastvalid = str + Ustrlen(str);
122 int n;
123
124 for (n = 0; n < nptrs; n++)
125 ptrs[n] = NULL;
126 n = 1;
127
128 while (*str) {
129 if (*str == '\t') {
130 if (n <= nptrs) {
131 *ptrs++ = last_sub_start;
132 last_sub_start = str + 1;
133 *str = '\0';
134 }
135 n++;
136 }
137 str++;
138 }
139
140 if (last_sub_start < lastvalid) {
141 if (n <= nptrs) {
142 *ptrs = last_sub_start;
143 } else {
144 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
145 n = nptrs;
146 }
147 } else {
148 n--;
149 HDEBUG(D_auth) debug_printf("dovecot: warning: ignoring trailing tab\n");
150 }
151
152 return n <= nptrs ? n : nptrs;
153 }
154
155 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
156 static void
157 debug_strcut(uschar **ptrs, int nlen, int alen)
158 {
159 int i;
160 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
161 socket_buffer_left, nlen);
162 for (i = 0; i < nlen; i++) {
163 debug_printf(" {%s}", ptrs[i]);
164 }
165 if (nlen < alen)
166 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
167 else
168 debug_printf(" (max for capacity)\n");
169 }
170
171 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
172 if (strcmpic(US(str), args[0]) != 0) \
173 goto out; \
174 if (nargs - 1 < (arg_min)) \
175 goto out; \
176 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
177 goto out; \
178 } while (0)
179
180 #define OUT(msg) do { \
181 auth_defer_msg = (US msg); \
182 goto out; \
183 } while(0)
184
185
186
187 /*************************************************
188 * "fgets" to read directly from socket *
189 *************************************************/
190
191 /* Added by PH after a suggestion by Steve Usher because the previous use of
192 C-style buffered I/O gave trouble. */
193
194 static uschar *
195 dc_gets(uschar *s, int n, int fd)
196 {
197 int p = 0;
198 int count = 0;
199
200 for (;;)
201 {
202 if (socket_buffer_left == 0)
203 {
204 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
205 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
206 p = 0;
207 }
208
209 while (p < socket_buffer_left)
210 {
211 if (count >= n - 1) break;
212 s[count++] = sbuffer[p];
213 if (sbuffer[p++] == '\n') break;
214 }
215
216 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
217 socket_buffer_left -= p;
218
219 if (s[count-1] == '\n' || count >= n - 1) break;
220 }
221
222 s[count] = '\0';
223 return s;
224 }
225
226
227
228
229 /*************************************************
230 * Server entry point *
231 *************************************************/
232
233 int auth_dovecot_server(auth_instance *ablock, uschar *data)
234 {
235 auth_dovecot_options_block *ob =
236 (auth_dovecot_options_block *)(ablock->options_block);
237 struct sockaddr_un sa;
238 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
239 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
240 uschar *auth_command;
241 uschar *auth_extra_data = US"";
242 uschar *p;
243 int nargs, tmp;
244 int crequid = 1, cont = 1, fd, ret = DEFER;
245 BOOL found = FALSE;
246
247 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
248
249 memset(&sa, 0, sizeof(sa));
250 sa.sun_family = AF_UNIX;
251
252 /* This was the original code here: it is nonsense because strncpy()
253 does not return an integer. I have converted this to use the function
254 that formats and checks length. PH */
255
256 /*
257 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
258 */
259
260 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
261 ob->server_socket)) {
262 auth_defer_msg = US"authentication socket path too long";
263 return DEFER;
264 }
265
266 auth_defer_msg = US"authentication socket connection error";
267
268 fd = socket(PF_UNIX, SOCK_STREAM, 0);
269 if (fd < 0)
270 return DEFER;
271
272 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
273 goto out;
274
275 auth_defer_msg = US"authentication socket protocol error";
276
277 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
278 while (cont) {
279 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
280 OUT("authentication socket read error or premature eof");
281 p = buffer + Ustrlen(buffer) - 1;
282 if (*p != '\n') {
283 OUT("authentication socket protocol line too long");
284 }
285 *p = '\0';
286 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
287 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
288 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
289
290 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
291 Exim will need. Original code also failed if Dovecot server sent unknown
292 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
293 /* pdp: note that CUID is a per-connection identifier sent by the server,
294 which increments at server discretion.
295 By contrast, the "id" field of the protocol is a connection-specific request
296 identifier, which needs to be unique per request from the client and is not
297 connected to the CUID value, so we ignore CUID from server. It's purely for
298 diagnostics. */
299 if (Ustrcmp(args[0], US"VERSION") == 0) {
300 CHECK_COMMAND("VERSION", 2, 2);
301 if (Uatoi(args[1]) != VERSION_MAJOR)
302 OUT("authentication socket protocol version mismatch");
303 } else if (Ustrcmp(args[0], US"MECH") == 0) {
304 CHECK_COMMAND("MECH", 1, INT_MAX);
305 if (strcmpic(US args[1], ablock->public_name) == 0)
306 found = TRUE;
307 } else if (Ustrcmp(args[0], US"DONE") == 0) {
308 CHECK_COMMAND("DONE", 0, 0);
309 cont = 0;
310 }
311 }
312
313 if (!found) {
314 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
315 goto out;
316 }
317
318 /* Added by PH: data must not contain tab (as it is
319 b64 it shouldn't, but check for safety). */
320
321 if (Ustrchr(data, '\t') != NULL) {
322 ret = FAIL;
323 goto out;
324 }
325
326 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
327 connection is local. */
328
329 if (tls_in.cipher != NULL)
330 auth_extra_data = string_sprintf("secured\t%s%s",
331 tls_in.certificate_verified? "valid-client-cert" : "",
332 tls_in.certificate_verified? "\t" : "");
333 else if (interface_address != NULL &&
334 Ustrcmp(sender_host_address, interface_address) == 0)
335 auth_extra_data = US"secured\t";
336
337
338 /****************************************************************************
339 The code below was the original code here. It didn't work. A reading of the
340 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
341 this was not right. Maybe something changed. I changed it to move the
342 service indication into the AUTH command, and it seems to be better. PH
343
344 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
345 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
346 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
347 ablock->public_name, sender_host_address, interface_address,
348 data ? (char *) data : "");
349
350 Subsequently, the command was modified to add "secured" and "valid-client-
351 cert" when relevant.
352 ****************************************************************************/
353
354 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
355 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
356 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
357 ablock->public_name, auth_extra_data, sender_host_address,
358 interface_address, data ? (char *) data : "");
359
360 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
361 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
362 strerror(errno));
363
364 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
365
366 while (1) {
367 uschar *temp;
368 uschar *auth_id_pre = NULL;
369 int i;
370
371 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
372 auth_defer_msg = US"authentication socket read error or premature eof";
373 goto out;
374 }
375
376 buffer[Ustrlen(buffer) - 1] = 0;
377 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
378 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
379
380 if (Uatoi(args[1]) != crequid)
381 OUT("authentication socket connection id mismatch");
382
383 switch (toupper(*args[0])) {
384 case 'C':
385 CHECK_COMMAND("CONT", 1, 2);
386
387 tmp = auth_get_no64_data(&data, US args[2]);
388 if (tmp != OK) {
389 ret = tmp;
390 goto out;
391 }
392
393 /* Added by PH: data must not contain tab (as it is
394 b64 it shouldn't, but check for safety). */
395
396 if (Ustrchr(data, '\t') != NULL) {
397 ret = FAIL;
398 goto out;
399 }
400
401 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
402 if (write(fd, temp, Ustrlen(temp)) < 0)
403 OUT("authentication socket write error");
404 break;
405
406 case 'F':
407 CHECK_COMMAND("FAIL", 1, -1);
408
409 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
410 {
411 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
412 {
413 auth_id_pre = args[i]+5;
414 expand_nstring[1] = auth_vars[0] =
415 string_copy(auth_id_pre); /* PH */
416 expand_nlength[1] = Ustrlen(auth_id_pre);
417 expand_nmax = 1;
418 }
419 }
420
421 ret = FAIL;
422 goto out;
423
424 case 'O':
425 CHECK_COMMAND("OK", 2, -1);
426
427 /*
428 * Search for the "user=$USER" string in the args array
429 * and return the proper value.
430 */
431 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
432 {
433 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
434 {
435 auth_id_pre = args[i]+5;
436 expand_nstring[1] = auth_vars[0] =
437 string_copy(auth_id_pre); /* PH */
438 expand_nlength[1] = Ustrlen(auth_id_pre);
439 expand_nmax = 1;
440 }
441 }
442
443 if (auth_id_pre == NULL)
444 OUT("authentication socket protocol error, username missing");
445
446 ret = OK;
447 /* fallthrough */
448
449 default:
450 goto out;
451 }
452 }
453
454 out:
455 /* close the socket used by dovecot */
456 if (fd >= 0)
457 close(fd);
458
459 /* Expand server_condition as an authorization check */
460 return (ret == OK)? auth_check_serv_cond(ablock) : ret;
461 }