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;
123 for (n = 0; n < nptrs; n++)
130 *ptrs++ = last_sub_start;
131 last_sub_start = str + 1;
139 /* It's acceptable for the string to end with a tab character. We see
140 this in AUTH PLAIN without an initial response from the client, which
141 causing us to send "334 " and get the data from the client. */
143 *ptrs = last_sub_start;
145 HDEBUG(D_auth) debug_printf("dovecot: warning: too many results from tab-splitting; saw %d fields, room for %d\n", n, nptrs);
149 return n <= nptrs ? n : nptrs;
152 static void debug_strcut(uschar **ptrs, int nlen, int alen) ARG_UNUSED;
154 debug_strcut(uschar **ptrs, int nlen, int alen)
157 debug_printf("%d read but unreturned bytes; strcut() gave %d results: ",
158 socket_buffer_left, nlen);
159 for (i = 0; i < nlen; i++) {
160 debug_printf(" {%s}", ptrs[i]);
163 debug_printf(" last is %s\n", ptrs[i] ? ptrs[i] : US"<null>");
165 debug_printf(" (max for capacity)\n");
168 #define CHECK_COMMAND(str, arg_min, arg_max) do { \
169 if (strcmpic(US(str), args[0]) != 0) \
171 if (nargs - 1 < (arg_min)) \
173 if ( (arg_max != -1) && (nargs - 1 > (arg_max)) ) \
177 #define OUT(msg) do { \
178 auth_defer_msg = (US msg); \
184 /*************************************************
185 * "fgets" to read directly from socket *
186 *************************************************/
188 /* Added by PH after a suggestion by Steve Usher because the previous use of
189 C-style buffered I/O gave trouble. */
192 dc_gets(uschar *s, int n, int fd)
199 if (socket_buffer_left == 0)
201 socket_buffer_left = read(fd, sbuffer, sizeof(sbuffer));
202 if (socket_buffer_left == 0) { if (count == 0) return NULL; else break; }
206 while (p < socket_buffer_left)
208 if (count >= n - 1) break;
209 s[count++] = sbuffer[p];
210 if (sbuffer[p++] == '\n') break;
213 memmove(sbuffer, sbuffer + p, socket_buffer_left - p);
214 socket_buffer_left -= p;
216 if (s[count-1] == '\n' || count >= n - 1) break;
226 /*************************************************
227 * Server entry point *
228 *************************************************/
230 int auth_dovecot_server(auth_instance *ablock, uschar *data)
232 auth_dovecot_options_block *ob =
233 (auth_dovecot_options_block *)(ablock->options_block);
234 struct sockaddr_un sa;
235 uschar buffer[DOVECOT_AUTH_MAXLINELEN];
236 uschar *args[DOVECOT_AUTH_MAXFIELDCOUNT];
237 uschar *auth_command;
238 uschar *auth_extra_data = US"";
241 int crequid = 1, cont = 1, fd, ret = DEFER;
244 HDEBUG(D_auth) debug_printf("dovecot authentication\n");
246 memset(&sa, 0, sizeof(sa));
247 sa.sun_family = AF_UNIX;
249 /* This was the original code here: it is nonsense because strncpy()
250 does not return an integer. I have converted this to use the function
251 that formats and checks length. PH */
254 if (strncpy(sa.sun_path, ob->server_socket, sizeof(sa.sun_path)) < 0) {
257 if (!string_format(US sa.sun_path, sizeof(sa.sun_path), "%s",
258 ob->server_socket)) {
259 auth_defer_msg = US"authentication socket path too long";
263 auth_defer_msg = US"authentication socket connection error";
265 fd = socket(PF_UNIX, SOCK_STREAM, 0);
269 if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0)
272 auth_defer_msg = US"authentication socket protocol error";
274 socket_buffer_left = 0; /* Global, used to read more than a line but return by line */
276 if (dc_gets(buffer, sizeof(buffer), fd) == NULL)
277 OUT("authentication socket read error or premature eof");
278 p = buffer + Ustrlen(buffer) - 1;
280 OUT("authentication socket protocol line too long");
283 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
284 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
285 /* HDEBUG(D_auth) debug_strcut(args, nargs, sizeof(args) / sizeof(args[0])); */
287 /* Code below rewritten by Kirill Miazine (km@krot.org). Only check commands that
288 Exim will need. Original code also failed if Dovecot server sent unknown
289 command. E.g. COOKIE in version 1.1 of the protocol would cause troubles. */
290 /* pdp: note that CUID is a per-connection identifier sent by the server,
291 which increments at server discretion.
292 By contrast, the "id" field of the protocol is a connection-specific request
293 identifier, which needs to be unique per request from the client and is not
294 connected to the CUID value, so we ignore CUID from server. It's purely for
296 if (Ustrcmp(args[0], US"VERSION") == 0) {
297 CHECK_COMMAND("VERSION", 2, 2);
298 if (Uatoi(args[1]) != VERSION_MAJOR)
299 OUT("authentication socket protocol version mismatch");
300 } else if (Ustrcmp(args[0], US"MECH") == 0) {
301 CHECK_COMMAND("MECH", 1, INT_MAX);
302 if (strcmpic(US args[1], ablock->public_name) == 0)
304 } else if (Ustrcmp(args[0], US"DONE") == 0) {
305 CHECK_COMMAND("DONE", 0, 0);
311 auth_defer_msg = string_sprintf("Dovecot did not advertise mechanism \"%s\" to us", ablock->public_name);
315 /* Added by PH: data must not contain tab (as it is
316 b64 it shouldn't, but check for safety). */
318 if (Ustrchr(data, '\t') != NULL) {
323 /* Added by PH: extra fields when TLS is in use or if the TCP/IP
324 connection is local. */
326 if (tls_in.cipher != NULL)
327 auth_extra_data = string_sprintf("secured\t%s%s",
328 tls_in.certificate_verified? "valid-client-cert" : "",
329 tls_in.certificate_verified? "\t" : "");
330 else if (interface_address != NULL &&
331 Ustrcmp(sender_host_address, interface_address) == 0)
332 auth_extra_data = US"secured\t";
335 /****************************************************************************
336 The code below was the original code here. It didn't work. A reading of the
337 file auth-protocol.txt.gz that came with Dovecot 1.0_beta8 indicated that
338 this was not right. Maybe something changed. I changed it to move the
339 service indication into the AUTH command, and it seems to be better. PH
341 fprintf(f, "VERSION\t%d\t%d\r\nSERVICE\tSMTP\r\nCPID\t%d\r\n"
342 "AUTH\t%d\t%s\trip=%s\tlip=%s\tresp=%s\r\n",
343 VERSION_MAJOR, VERSION_MINOR, getpid(), cuid,
344 ablock->public_name, sender_host_address, interface_address,
345 data ? (char *) data : "");
347 Subsequently, the command was modified to add "secured" and "valid-client-
349 ****************************************************************************/
351 auth_command = string_sprintf("VERSION\t%d\t%d\nCPID\t%d\n"
352 "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
353 VERSION_MAJOR, VERSION_MINOR, getpid(), crequid,
354 ablock->public_name, auth_extra_data, sender_host_address,
355 interface_address, data ? (char *) data : "");
357 if (write(fd, auth_command, Ustrlen(auth_command)) < 0)
358 HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
361 HDEBUG(D_auth) debug_printf("sent: %s", auth_command);
365 uschar *auth_id_pre = NULL;
368 if (dc_gets(buffer, sizeof(buffer), fd) == NULL) {
369 auth_defer_msg = US"authentication socket read error or premature eof";
373 buffer[Ustrlen(buffer) - 1] = 0;
374 HDEBUG(D_auth) debug_printf("received: %s\n", buffer);
375 nargs = strcut(buffer, args, sizeof(args) / sizeof(args[0]));
377 if (Uatoi(args[1]) != crequid)
378 OUT("authentication socket connection id mismatch");
380 switch (toupper(*args[0])) {
382 CHECK_COMMAND("CONT", 1, 2);
384 tmp = auth_get_no64_data(&data, US args[2]);
390 /* Added by PH: data must not contain tab (as it is
391 b64 it shouldn't, but check for safety). */
393 if (Ustrchr(data, '\t') != NULL) {
398 temp = string_sprintf("CONT\t%d\t%s\n", crequid, data);
399 if (write(fd, temp, Ustrlen(temp)) < 0)
400 OUT("authentication socket write error");
404 CHECK_COMMAND("FAIL", 1, -1);
406 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
408 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
410 auth_id_pre = args[i]+5;
411 expand_nstring[1] = auth_vars[0] =
412 string_copy(auth_id_pre); /* PH */
413 expand_nlength[1] = Ustrlen(auth_id_pre);
422 CHECK_COMMAND("OK", 2, -1);
425 * Search for the "user=$USER" string in the args array
426 * and return the proper value.
428 for (i=2; (i<nargs) && (auth_id_pre == NULL); i++)
430 if ( Ustrncmp(args[i], US"user=", 5) == 0 )
432 auth_id_pre = args[i]+5;
433 expand_nstring[1] = auth_vars[0] =
434 string_copy(auth_id_pre); /* PH */
435 expand_nlength[1] = Ustrlen(auth_id_pre);
440 if (auth_id_pre == NULL)
441 OUT("authentication socket protocol error, username missing");
452 /* close the socket used by dovecot */
456 /* Expand server_condition as an authorization check */
457 return (ret == OK)? auth_check_serv_cond(ablock) : ret;