/* $Id: client.c,v 1.691 2011/06/28 00:13:48 sbajic Exp $ */ /* DSPAM COPYRIGHT (C) 2002-2012 DSPAM PROJECT This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ /* * client.c - client-based functions (for operating in client/daemon mode)c * * DESCRIPTION * Client-based functions are called when --client is specified on the * commandline or by dspamc (where --client is inferred). The client * functions connect to a DSPAM server for processing (rather than the * usual behavior which is to process the message itself). Client * functions are also used when delivering via LMTP or SMTP. */ #ifdef HAVE_CONFIG_H #include #endif #ifdef DAEMON #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #include #endif #include #include #include #include "client.h" #include "dspam.h" #include "config.h" #include "util.h" #include "language.h" #include "buffer.h" /* * client_process(AGENT_CTX *, buffer *) * * DESCRIPTION * connect to a dspam daemon socket and attempt to process a message * this function is called by the dspam agent when --client is specified * * INPUT ARGUMENTS * ATX agent context * message message to be processed * * RETURN VALUES * returns 0 on success */ int client_process(AGENT_CTX *ATX, buffer *message) { char buf[1024], err[256]; struct nt_node *node_nt; struct nt_c c_nt; int exitcode = 0, msglen; THREAD_CTX TTX; int i; TTX.sockfd = client_connect(ATX, 0); if (TTX.sockfd <0) { LOG(LOG_WARNING, ERR_CLIENT_CONNECT); STATUS(ERR_CLIENT_CONNECT); return TTX.sockfd; } TTX.packet_buffer = buffer_create(NULL); if (TTX.packet_buffer == NULL) goto BAIL; /* LHLO / MAIL FROM - Authenticate on the server */ if (client_authenticate(&TTX, ATX->client_args)<0) { LOG(LOG_WARNING, ERR_CLIENT_AUTH_FAILED); STATUS(ERR_CLIENT_AUTH_FAILED); goto QUIT; } /* RCPT TO - Send recipient information */ strcpy(buf, "RCPT TO: "); node_nt = c_nt_first(ATX->users, &c_nt); while(node_nt != NULL) { const char *ptr = (const char *) node_nt->ptr; snprintf(buf, sizeof(buf), "RCPT TO: <%s>", ptr); if (send_socket(&TTX, buf)<=0) { STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } if (client_getcode(&TTX, err, sizeof(err))!=LMTP_OK) { STATUS("%s", err); goto QUIT; } node_nt = c_nt_next(ATX->users, &c_nt); } /* DATA - Send message */ if (send_socket(&TTX, "DATA")<=0) goto BAIL; if (client_getcode(&TTX, err, sizeof(err))!=LMTP_DATA) { STATUS("%s", err); goto QUIT; } i = 0; msglen = strlen(message->data); while(i 0) { if (message->data[i] == '\n') { /* only replace \n and not \r\n */ if (message->data[i - 1] != '\r') { buf[buflen] = '\r'; buflen++; } /* take care of dot stuffing \n */ if (message->data[i + 1] && message->data[i + 1] == '.') { buf[buflen] = '\n'; buflen++; buf[buflen] = '.'; buflen++; buf[buflen] = '.'; buflen++; i += 2; continue; } } } buf[buflen] = message->data[i]; buflen++; i++; } /* send buf */ t = 0; while (t < buflen) { r = send(TTX.sockfd, buf+t, buflen - t, 0); if (r <= 0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } t += r; } } if (message->data[msglen-1]!= '\n') { if (send_socket(&TTX, "")<=0) { STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } } if (send_socket(&TTX, ".")<=0) { STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } /* Server response */ if (ATX->flags & DAF_STDOUT || ATX->flags & DAF_SUMMARY || ATX->operating_mode == DSM_CLASSIFY) { char *line = NULL; int head = !(ATX->flags & DAF_STDOUT); if (ATX->flags & DAF_SUMMARY) head = 1; line = client_getline(&TTX, 300); while(line != NULL && strcmp(line, ".")) { chomp(line); if (!head) { head = 1; if (!strncmp(line, "250 ", 4)) { free(line); goto QUIT; } if (!strcmp(line, "X-Daemon-Classification: SPAM") && _ds_match_attribute(agent_config, "Broken", "returnCodes")) { exitcode = 99; } } else { /* remove dot stuffing, if needed */ if((line[0] && line[0]=='.') && (line[1] && line[1]=='.')) { size_t i, len = strlen(line); for(i=0;iusers->items;i++) { char *input = client_getline(&TTX, 300); char *x; int code = 500; if (!input) { goto BAIL; } x = strtok(input, " "); if (x) { code = atoi(x); if (code != LMTP_OK) { if (exitcode > 0) exitcode = 0; exitcode--; } else { if (_ds_match_attribute(agent_config, "Broken", "returnCodes")) { x = strtok(NULL, ":"); if (x) x = strtok(NULL, ":"); if (x && strstr(x, "SPAM") && exitcode == 0) exitcode = 99; } } } } } send_socket(&TTX, "QUIT"); client_getcode(&TTX, err, sizeof(err)); close(TTX.sockfd); buffer_destroy(TTX.packet_buffer); return exitcode; QUIT: send_socket(&TTX, "QUIT"); client_getcode(&TTX, err, sizeof(err)); BAIL: exitcode = EFAILURE; buffer_destroy(TTX.packet_buffer); close(TTX.sockfd); return exitcode; } /* * client_connect(AGENT_CTX ATX, int flags) * * DESCRIPTION * establish a connection to a server * * INPUT ARGUMENTS * ATX agent context * flags connection flags * * FLAGS * CCF_PROCESS Use ClientHost as destination * CCF_DELIVERY Use DeliveryHost as destination * * RETURN VALUES * returns 0 on success */ int client_connect(AGENT_CTX *ATX, int flags) { struct sockaddr_in addr; struct sockaddr_un saun; int sockfd; int yes = 1; int port = 24; int domain = 0; int addr_len; char *host; if (flags & CCF_DELIVERY) { host = _ds_read_attribute(agent_config, "DeliveryHost"); if (_ds_read_attribute(agent_config, "DeliveryPort")) port = atoi(_ds_read_attribute(agent_config, "DeliveryPort")); if (ATX->recipient && ATX->recipient[0]) { char *domain = strchr(ATX->recipient, '@'); if (domain) { char key[128]; char lcdomain[strlen(ATX->recipient)]; lc(lcdomain, domain+1); snprintf(key, sizeof(key), "DeliveryHost.%s", lcdomain); if (_ds_read_attribute(agent_config, key)) host = _ds_read_attribute(agent_config, key); snprintf(key, sizeof(key), "DeliveryPort.%s", lcdomain); if (_ds_read_attribute(agent_config, key)) port = atoi(_ds_read_attribute(agent_config, key)); } } if (host && host[0] == '/') domain = 1; } else { host = _ds_read_attribute(agent_config, "ClientHost"); if (_ds_read_attribute(agent_config, "ClientPort")) port = atoi(_ds_read_attribute(agent_config, "ClientPort")); if (host && host[0] == '/') domain = 1; } if (host == NULL) { LOG(LOG_CRIT, ERR_CLIENT_INVALID_CONFIG); STATUS(ERR_CLIENT_INVALID_CONFIG); return EINVAL; } /* Connect (domain socket) */ if (domain) { sockfd = socket(AF_UNIX, SOCK_STREAM, 0); saun.sun_family = AF_UNIX; strcpy(saun.sun_path, host); addr_len = sizeof(saun.sun_family) + strlen(saun.sun_path) + 1; LOGDEBUG(INFO_CLIENT_CONNECTING, host, 0); if(connect(sockfd, (struct sockaddr *)&saun, addr_len)<0) { LOG(LOG_ERR, ERR_CLIENT_CONNECT_SOCKET, host, strerror(errno)); STATUS("%s", strerror(errno)); close(sockfd); return EFAILURE; } /* Connect (TCP socket) */ } else { sockfd = socket(AF_INET, SOCK_STREAM, 0); memset(&addr, 0, sizeof(struct sockaddr_in)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(host); addr.sin_port = htons(port); addr_len = sizeof(struct sockaddr_in); LOGDEBUG(INFO_CLIENT_CONNECTING, host, port); if(connect(sockfd, (struct sockaddr *)&addr, addr_len)<0) { LOG(LOG_ERR, ERR_CLIENT_CONNECT_HOST, host, port, strerror(errno)); STATUS("%s", strerror(errno)); close(sockfd); return EFAILURE; } } LOGDEBUG(INFO_CLIENT_CONNECTED); setsockopt(sockfd,SOL_SOCKET,TCP_NODELAY,&yes,sizeof(int)); return sockfd; } /* * client_authenticate(AGENT_CTX *ATX, const char *mode) * * DESCRIPTION * greet and authenticate on a server * * INPUT ARGUMENTS * ATX agent context * mode processing mode * * NOTES * the process mode is passed using the DSPAMPROCESSMODE service tag * * RETURN VALUES * returns 0 on success */ int client_authenticate(THREAD_CTX *TTX, const char *mode) { char *ident = _ds_read_attribute(agent_config, "ClientIdent"); char buf[1024], err[128]; char *ptr; char pmode[1024]; pmode[0] = 0; if (mode) { int pos = 0, cpos = 0; for(;mode[cpos]&&(size_t)pos<(sizeof(pmode)-1);cpos++) { if (mode[cpos] == '"') { pmode[pos] = '\\'; pos++; } pmode[pos] = mode[cpos]; pos++; } pmode[pos] = 0; } if (!ident || !strchr(ident, '@')) { LOG(LOG_ERR, ERR_CLIENT_IDENT); return EINVAL; } ptr = client_expect(TTX, LMTP_GREETING, err, sizeof(err)); if (ptr == NULL) { LOG(LOG_ERR, ERR_CLIENT_WHILE_AUTH, err); return EFAILURE; } free(ptr); snprintf(buf, sizeof(buf), "LHLO %s", strchr(ident, '@')+1); if (send_socket(TTX, buf)<=0) return EFAILURE; if (client_getcode(TTX, err, sizeof(err))!=LMTP_OK) { return EFAILURE; } if (mode) { snprintf(buf, sizeof(buf), "MAIL FROM: <%s> DSPAMPROCESSMODE=\"%s\"", ident, pmode); } else { snprintf(buf, sizeof(buf), "MAIL FROM: <%s>", ident); } if (send_socket(TTX, buf)<=0) { return EFAILURE; } if (client_getcode(TTX, err, sizeof(err))!=LMTP_OK) { LOG(LOG_ERR, ERR_CLIENT_AUTHENTICATE); return EFAILURE; } return 0; } /* * client_expect(THREAD_CTX *TTX, int code, char *err, size_t len) * * DESCRIPTION * wait for the appropriate return code, then return * * INPUT ARGUMENTS * ATX agent context * code return code to wait for * err error buffer * len buffer len * * RETURN VALUES * allocated pointer to acknowledgement line, NULL on error * err buffer is populated on error */ char * client_expect(THREAD_CTX *TTX, int code, char *err, size_t len) { char *inp, *dup, *ptr, *ptrptr; int recv_code; inp = client_getline(TTX, 300); while(inp != NULL) { recv_code = 0; dup = strdup(inp); if (!dup) { free(inp); LOG(LOG_CRIT, ERR_MEM_ALLOC); strlcpy(err, ERR_MEM_ALLOC, len); return NULL; } if (strncmp(dup, "250-", 4)) { ptr = strtok_r(dup, " ", &ptrptr); if (ptr) recv_code = atoi(ptr); free(dup); if (recv_code == code) { err[0] = 0; return inp; } LOG(LOG_WARNING, ERR_CLIENT_RESPONSE_CODE, code, inp); } strlcpy(err, inp, len); free(inp); inp = client_getline(TTX, 300); } return NULL; } /* * client_parsecode(const char *err) * * DESCRIPTION * parse response code from plain text * * INPUT ARGUMENTS * err error message to parse * * RETURN VALUES * integer value of response code */ int client_parsecode(char *error) { char code[4]; code[3] = 0; strncpy(code, error, 3); return atoi(code); } /* * client_getcode(THREAD_CTX *TTX, char *err, size_t len) * * DESCRIPTION * retrieve a line of input and return response code * * INPUT ARGUMENTS * TTX thread context containing sockfd * err error buffer * len buffer len * * RETURN VALUES * integer value of response code */ int client_getcode(THREAD_CTX *TTX, char *err, size_t len) { char *inp, *ptr, *ptrptr = NULL; int i; inp = client_getline(TTX, 300); if (!inp) return EFAILURE; while(inp && !strncmp(inp, "250-", 4)) { free(inp); inp = client_getline(TTX, 300); } strlcpy(err, inp, len); ptr = strtok_r(inp, " ", &ptrptr); if (ptr == NULL) return EFAILURE; i = atoi(ptr); free(inp); return i; } /* * client_getline(THREAD_CTX *TTX, int timeout) * * DESCRIPTION * read a complete line from a socket * * INPUT ARGUMENTS * TTX thread context containing sockfd * timeout timeout (in seconds) to wait for input * * RETURN VALUES * allocated pointer to input */ char *client_getline(THREAD_CTX *TTX, int timeout) { struct timeval tv; long recv_len; char buf[1024]; char *pop; fd_set fds; int i; pop = pop_buffer(TTX); while(!pop) { tv.tv_sec = timeout; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(TTX->sockfd, &fds); i = select(TTX->sockfd+1, &fds, NULL, NULL, &tv); if (i<=0) return NULL; recv_len = recv(TTX->sockfd, buf, sizeof(buf)-1, 0); buf[recv_len] = 0; if (recv_len == 0) return NULL; buffer_cat(TTX->packet_buffer, buf); pop = pop_buffer(TTX); } #ifdef VERBOSE LOGDEBUG("RECV: %s", pop); #endif return pop; } /* * pop_buffer (THREAD_CTX *TTX) * * DESCRIPTION * pop a line off the packet buffer * * INPUT ARGUMENTS * TTX thread context containing the packet buffer * * RETURN VALUES * allocated pointer to line, NULL if complete line isn't available */ char *pop_buffer(THREAD_CTX *TTX) { char *buf, *eol; long len; if (!TTX || !TTX->packet_buffer || !TTX->packet_buffer->data) return NULL; eol = strchr(TTX->packet_buffer->data, 10); if (!eol) return NULL; len = (eol - TTX->packet_buffer->data) + 1; buf = calloc(1, len+1); if (!buf) { LOG(LOG_CRIT, ERR_MEM_ALLOC); return NULL; } memcpy(buf, TTX->packet_buffer->data, len); memmove(TTX->packet_buffer->data, eol+1, strlen(eol+1)+1); TTX->packet_buffer->used -= len; return buf; } /* * send_socket(THREAD_CTX *TTX, const char *text) * * DESCRIPTION * send a line of text to a socket * * INPUT ARGUMENTS * TTX thread context containing sockfd * text text to send * * RETURN VALUES * number of bytes sent */ int send_socket(THREAD_CTX *TTX, const char *text) { int i = 0, r, msglen; #ifdef VERBOSE LOGDEBUG("SEND: %s", text); #endif msglen = strlen(text); while(isockfd, text+i, msglen-i, 0); if (r <= 0) { return r; } i += r; } r = send(TTX->sockfd, "\r\n", 2, 0); if (r > 0) { i += r; } return i; } /* * deliver_socket(AGENT_CTX *ATX, const char *msg, int proto) * * DESCRIPTION * delivers message via LMTP or SMTP (instead of TrustedDeliveryAgent) * * If LMTP/SMTP delivery was specified in dspam.conf, this function will be * called by deliver_message(). This function connects to and delivers the * message using standard LMTP or SMTP. Depending on how DSPAM was originally * called, either the address supplied with the incoming RCPT TO or the * address supplied on the commandline with --rcpt-to will be used. If * neither are present, the username will be used. * * INPUT ARGUMENTS * ATX agent context * msg message to send * proto protocol to use * * PROTOCOLS * DDP_LMTP LMTP * DDP_SMTP SMTP * * RETURN VALUES * returns 0 on success */ int deliver_socket(AGENT_CTX *ATX, const char *msg, int proto) { THREAD_CTX TTX; char buf[1024], err[256]; char *ident = _ds_read_attribute(agent_config, "DeliveryIdent"); int exitcode = EFAILURE; int msglen, code; int buflen; char *inp; int i; int size_extension = 0; err[0] = 0; TTX.sockfd = client_connect(ATX, CCF_DELIVERY); if (TTX.sockfd <0) { STATUS(ERR_CLIENT_CONNECT); LOG(LOG_ERR, ERR_CLIENT_CONNECT); return TTX.sockfd; } TTX.packet_buffer = buffer_create(NULL); if (TTX.packet_buffer == NULL) { LOG(LOG_CRIT, ERR_MEM_ALLOC); STATUS(ERR_MEM_ALLOC); goto BAIL; } inp = client_expect(&TTX, LMTP_GREETING, err, sizeof(err)); if (inp == NULL) { LOG(LOG_ERR, ERR_CLIENT_ON_GREETING, err); STATUS("%s", err); goto BAIL; } free(inp); /* LHLO / HELO */ snprintf(buf, sizeof(buf), "%s %s", (proto == DDP_LMTP) ? "LHLO" : "HELO", (ident) ? ident : "localhost"); if (send_socket(&TTX, buf)<=0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } /* Check for SIZE extension */ if (proto == DDP_LMTP) { char *dup, *ptr, *ptrptr; inp = client_getline(&TTX, 300); while(inp != NULL) { code = 0; dup = strdup(inp); if (!dup) { free(inp); LOG(LOG_CRIT, ERR_MEM_ALLOC); LOG(LOG_ERR, ERR_CLIENT_INVALID_RESPONSE, "LHLO", ERR_MEM_ALLOC); STATUS("LHLO: %s", ERR_MEM_ALLOC); goto QUIT; } if (!strcmp(dup, "250-SIZE") || (!strncmp(dup, "250-SIZE", 8) && strlen(dup)>=8 && isspace(dup[8]))) { free(inp); free(dup); size_extension = 1; inp = client_expect(&TTX, LMTP_OK, err, sizeof(err)); break; } else if (strncmp(dup, "250-", 4)) { ptr = strtok_r(dup, " ", &ptrptr); if (ptr) code = atoi(ptr); if (code == LMTP_OK) { ptr = strtok_r(NULL, " ", &ptrptr); if (ptr && !strcmp(ptr, "SIZE")) size_extension = 1; } free(dup); if (code == LMTP_OK) { err[0] = 0; break; } LOG(LOG_WARNING, ERR_CLIENT_RESPONSE_CODE, code, inp); } strlcpy(err, inp, sizeof(err)); free(inp); inp = client_getline(&TTX, 300); } } else { inp = client_expect(&TTX, LMTP_OK, err, sizeof(err)); } if (inp == NULL) { LOG(LOG_ERR, ERR_CLIENT_INVALID_RESPONSE, (proto == DDP_LMTP) ? "LHLO" : "HELO", err); STATUS("%s: %s", (proto == DDP_LMTP) ? "LHLO" : "HELO", err); goto QUIT; } free(inp); /* MAIL FROM */ if (proto == DDP_LMTP && size_extension == 1) { snprintf(buf, sizeof(buf), "MAIL FROM:<%s> SIZE=%ld", ATX->mailfrom, (long) strlen(msg)); } else { snprintf(buf, sizeof(buf), "MAIL FROM:<%s>", ATX->mailfrom); } if (send_socket(&TTX, buf)<=0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } code = client_getcode(&TTX, err, sizeof(err)); if (code!=LMTP_OK) { LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "MAIL FROM", err); if (code >= 500) exitcode = EINVAL; chomp(err); STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err); goto QUIT; } /* RCPT TO */ snprintf(buf, sizeof(buf), "RCPT TO:<%s>", (ATX->recipient) ? ATX->recipient : ""); if (send_socket(&TTX, buf)<=0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } code = client_getcode(&TTX, err, sizeof(err)); if (code!=LMTP_OK) { LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "RCPT TO", err); if (code >= 500) exitcode = EINVAL; chomp(err); STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err); goto QUIT; } /* DATA */ if (send_socket(&TTX, "DATA")<=0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } code = client_getcode(&TTX, err, sizeof(err)); if (code!=LMTP_DATA) { LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "DATA", err); if (code >= 500) exitcode = EINVAL; chomp(err); STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err); goto QUIT; } i = 0; msglen = strlen(msg); while(i 0 && msg[i] == '\n' && msg[i - 1] != '\r') { buf[buflen] = '\r'; buflen++; } /* escape dot if first character on line */ if (msg[i] == '.' && (i == 0 || msg[i - 1] == '\n')) { buf[buflen] = '.'; buflen++; } buf[buflen] = msg[i]; buflen++; i++; } /* send buf */ t = 0; while (t < buflen) { r = send(TTX.sockfd, buf+t, buflen - t, 0); if (r <= 0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } t += r; } } if (msg[strlen(msg)-1]!= '\n') { if (send_socket(&TTX, "")<=0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } } if (send_socket(&TTX, "\r\n.")<=0) { LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED); STATUS(ERR_CLIENT_SEND_FAILED); goto BAIL; } /* server response */ code = client_getcode(&TTX, err, sizeof(err)); if (code < 200 || code >= 300) { LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "message data", err); if (code >= 400 && code < 500) exitcode = EX_TEMPFAIL; else if (code >= 500) exitcode = EINVAL; chomp(err); STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err); goto QUIT; } send_socket(&TTX, "QUIT"); client_getcode(&TTX, err, sizeof(err)); close(TTX.sockfd); buffer_destroy(TTX.packet_buffer); return 0; QUIT: send_socket(&TTX, "QUIT"); client_getcode(&TTX, err, sizeof(err)); buffer_destroy(TTX.packet_buffer); close(TTX.sockfd); return exitcode; BAIL: LOG(LOG_ERR, ERR_CLIENT_DELIVERY_FAILED); buffer_destroy(TTX.packet_buffer); close(TTX.sockfd); return exitcode; } #endif /* DAEMON */