2 * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
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.
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 */
15 /* Protocol specifications:
16 * Dovecot 1, protocol version 1.1
17 * http://wiki.dovecot.org/Authentication%20Protocol
19 * Dovecot 2, protocol version 1.1
20 * http://wiki2.dovecot.org/Design/AuthProtocol
26 #define VERSION_MAJOR 1
27 #define VERSION_MINOR 0
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"
33 #define DOVECOT_AUTH_MAXLINELEN 8192
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.
39 Master->Server sends {"USER", id, userid} + params, 6 defined.
40 Server->Client only gives {"OK", id} + params, unspecified, only 1 guaranteed.
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=...
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.
49 #define DOVECOT_AUTH_MAXFIELDCOUNT 16
51 /* Options specific to the authentication mechanism. */
52 optionlist auth_dovecot_options[] = {
56 (void *)(offsetof(auth_dovecot_options_block, server_socket))
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. */
63 int auth_dovecot_options_count =
64 sizeof(auth_dovecot_options) / sizeof(optionlist);
66 /* Default private options block for the authentication method. */
68 auth_dovecot_options_block auth_dovecot_option_defaults = {
69 NULL, /* server_socket */
73 /* Static variables for reading from the socket */
75 static uschar sbuffer[256];
76 static int socket_buffer_left;
80 /*************************************************
81 * Initialization entry point *
82 *************************************************/
84 /* Called for each instance, after its options have been read, to
85 enable consistency checks to be done, or anything else that needs
88 void auth_dovecot_init(auth_instance *ablock)
90 auth_dovecot_options_block *ob =
91 (auth_dovecot_options_block *)(ablock->options_block);
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;
100 /*************************************************
101 * "strcut" to split apart server lines *
102 *************************************************/
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.
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).
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).
118 strcut(uschar *str, uschar **ptrs, int nptrs)
120 uschar *last_sub_start = str;
121 uschar *lastvalid = str + Ustrlen(str);
124 for (n = 0; n < nptrs; n++)
131 *ptrs++ = last_sub_start;
132 last_sub_start = str + 1;
140 if (last_sub_start < lastvalid) {
142 *ptrs = last_sub_start;
144 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
149 HDEBUG(D_auth) debug_printf("dovecot: warning: ignoring trailing tab\n");
152 return n <= nptrs ? n : nptrs;
155 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
157 debug_strcut(uschar **ptrs, int nlen, int alen)
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]);
166 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
168 debug_printf(" (max for capacity)\n");
171 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
172 if (strcmpic(US(str), args[0]) != 0) \
174 if (nargs - 1 < (arg_min)) \
176 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
180 #define OUT(msg) do { \
181 auth_defer_msg = (US msg); \
187 /*************************************************
188 * "fgets" to read directly from socket *
189 *************************************************/
191 /* Added by PH after a suggestion by Steve Usher because the previous use of
192 C-style buffered I/O gave trouble. */
195 dc_gets(uschar *s, int n, int fd)
202 if (socket_buffer_left == 0)
204 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
205 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
209 while (p < socket_buffer_left)
211 if (count >= n - 1) break;
212 s[count++] = sbuffer[p];
213 if (sbuffer[p++] == '\n') break;
216 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
217 socket_buffer_left -= p;
219 if (s[count-1] == '\n' || count >= n - 1) break;
229 /*************************************************
230 * Server entry point *
231 *************************************************/
233 int auth_dovecot_server(auth_instance *ablock, uschar *data)
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"";
244 int crequid = 1, cont = 1, fd, ret = DEFER;
247 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
249 memset(&sa, 0, sizeof(sa));
250 sa.sun_family = AF_UNIX;
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 */
257 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
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";
266 auth_defer_msg = US"authentication socket connection error";
268 fd = socket(PF_UNIX, SOCK_STREAM, 0);
272 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
275 auth_defer_msg = US"authentication socket protocol error";
277 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
279 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
280 OUT("authentication socket read error or premature eof");
281 p = buffer + Ustrlen(buffer) - 1;
283 OUT("authentication socket protocol line too long");
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])); */
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
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)
307 } else if (Ustrcmp(args[0], US"DONE") == 0) {
308 CHECK_COMMAND("DONE", 0, 0);
314 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
318 /* Added by PH: data must not contain tab (as it is
319 b64 it shouldn't, but check for safety). */
321 if (Ustrchr(data, '\t') != NULL) {
326 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
327 connection is local. */
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";
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
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 : "");
350 Subsequently, the command was modified to add "secured" and "valid-client-
352 ****************************************************************************/
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 : "");
360 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
361 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
364 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
368 uschar *auth_id_pre = NULL;
371 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
372 auth_defer_msg = US"authentication socket read error or premature eof";
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]));
380 if (Uatoi(args[1]) != crequid)
381 OUT("authentication socket connection id mismatch");
383 switch (toupper(*args[0])) {
385 CHECK_COMMAND("CONT", 1, 2);
387 tmp = auth_get_no64_data(&data, US args[2]);
393 /* Added by PH: data must not contain tab (as it is
394 b64 it shouldn't, but check for safety). */
396 if (Ustrchr(data, '\t') != NULL) {
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");
407 CHECK_COMMAND("FAIL", 1, -1);
409 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
411 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
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);
425 CHECK_COMMAND("OK", 2, -1);
428 * Search for the "user=$USER" string in the args array
429 * and return the proper value.
431 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
433 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
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);
443 if (auth_id_pre == NULL)
444 OUT("authentication socket protocol error, username missing");
455 /* close the socket used by dovecot */
459 /* Expand server_condition as an authorization check */
460 return (ret == OK)? auth_check_serv_cond(ablock) : ret;