/* This file is part of GNUnet Copyright (C) 2003-2013 GNUnet e.V. GNUnet 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. GNUnet 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 . SPDX-License-Identifier: AGPL3.0-or-later */ /** * @file transport/plugin_transport_smtp.c * @brief Implementation of the SMTP transport service * @author Christian Grothoff * @author Renaldo Ferreira */ #include "platform.h" #include "gnunet_util.h" #include "gnunet_constants.h" #include "gnunet_protocols.h" #include "gnunet_transport.h" #include "gnunet_stats_service.h" #include #include /** * The default maximum size of each outbound SMTP message. */ #define SMTP_MESSAGE_SIZE 65528 #define DEBUG_SMTP GNUNET_EXTRA_LOGGING #define FILTER_STRING_SIZE 64 /* how long can a line in base64 encoded mime text be? (in characters, excluding "\n") */ #define MAX_CHAR_PER_LINE 76 #define EBUF_LEN 128 /** * Host-Address in a SMTP network. */ typedef struct { /** * Filter line that every sender must include in the E-mails such * that the receiver can effectively filter out the GNUnet traffic * from the E-mail. */ char filter[FILTER_STRING_SIZE]; /** * Claimed E-mail address of the sender. * Format is "foo@bar.com" with null termination, padded to be * of a multiple of 8 bytes long. */ char senderAddress[0]; } EmailAddress; GNUNET_NETWORK_STRUCT_BEGIN /** * Encapsulation of a GNUnet message in the SMTP mail body (before * base64 encoding). */ typedef struct { GNUNET_MessageHeader header; /** * What is the identity of the sender (GNUNET_hash of public key) */ GNUNET_PeerIdentity sender; } SMTPMessage; GNUNET_NETWORK_STRUCT_END /* *********** globals ************* */ /** * apis (our advertised API and the core api ) */ static GNUNET_CoreAPIForTransport *core_api; static struct GNUNET_GE_Context *ectx; /** * Thread that listens for inbound messages */ static struct GNUNET_ThreadHandle *dispatchThread; /** * Flag to indicate that server has been shut down. */ static int smtp_shutdown = GNUNET_YES; /** * Set to the SMTP server hostname (and port) for outgoing messages. */ static char *smtp_server_name; static char *pipename; /** * Lock for uses of libesmtp (not thread-safe). */ static struct GNUNET_Mutex *lock; /** * Old handler for SIGPIPE (kept to be able to restore). */ static struct sigaction old_handler; static char *email; static GNUNET_TransportAPI smtpAPI; static GNUNET_Stats_ServiceAPI *stats; static int stat_bytesReceived; static int stat_bytesSent; static int stat_bytesDropped; /** * How many e-mails are we allowed to send per hour? */ static unsigned long long rate_limit; static GNUNET_CronTime last_transmission; /* ********************* the real stuff ******************* */ #define strAUTOncmp(a, b) strncmp(a, b, strlen(b)) /** * Listen to the pipe, decode messages and send to core. */ static void * listenAndDistribute(void *unused) { char *line; unsigned int linesize; SMTPMessage *mp; FILE *fdes; char *retl; char *out; unsigned int size; GNUNET_TransportPacket *coreMP; int fd; unsigned int pos; linesize = ((GNUNET_MAX_BUFFER_SIZE * 4 / 3) + 8) * (MAX_CHAR_PER_LINE + 2) / MAX_CHAR_PER_LINE; /* maximum size of a line supported */ line = GNUNET_malloc(linesize + 2); /* 2 bytes for off-by-one errors, just to be safe... */ #define READLINE(l, limit) \ do { retl = fgets(l, (limit), fdes); \ if ((retl == NULL) || (smtp_shutdown == GNUNET_YES)) { \ goto END; \ } \ if (core_api->load_monitor != NULL) \ GNUNET_network_monitor_notify_transmission (core_api->load_monitor, GNUNET_ND_DOWNLOAD, strlen(retl)); \ } while (0) while (smtp_shutdown == GNUNET_NO) { fd = OPEN(pipename, O_RDONLY | O_ASYNC); if (fd == -1) { if (smtp_shutdown == GNUNET_NO) GNUNET_thread_sleep(5 * GNUNET_CRON_SECONDS); continue; } fdes = fdopen(fd, "r"); while (smtp_shutdown == GNUNET_NO) { /* skip until end of header */ do { READLINE(line, linesize); } while ((line[0] != '\r') && (line[0] != '\n')); /* expect newline */ READLINE(line, linesize); /* read base64 encoded message; decode, process */ pos = 0; while (1) { pos = strlen(line) - 1; /* ignore new line */ READLINE(&line[pos], linesize - pos); /* read base64 encoded message; decode, process */ if ((line[pos] == '\r') || (line[pos] == '\n')) break; /* empty line => end of message! */ } size = GNUNET_STRINGS_base64_decode(line, pos, &out); if (size < sizeof(SMTPMessage)) { GNUNET_GE_BREAK(ectx, 0); GNUNET_free(out); goto END; } mp = (SMTPMessage *)&out[size - sizeof(SMTPMessage)]; if (ntohs(mp->header.size) != size) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_BULK | GNUNET_GE_USER, _("Received malformed message via %s. Ignored.\n"), "SMTP"); #if DEBUG_SMTP GNUNET_GE_LOG(ectx, GNUNET_GE_DEBUG | GNUNET_GE_REQUEST | GNUNET_GE_USER, "Size returned by base64=%d, in the msg=%d.\n", size, ntohl(mp->size)); #endif GNUNET_free(out); goto END; } if (stats != NULL) stats->change(stat_bytesReceived, size); coreMP = GNUNET_new(GNUNET_TransportPacket); coreMP->msg = out; coreMP->size = size - sizeof(SMTPMessage); coreMP->tsession = NULL; coreMP->sender = mp->sender; #if DEBUG_SMTP GNUNET_GE_LOG(ectx, GNUNET_GE_DEBUG | GNUNET_GE_REQUEST | GNUNET_GE_USER, "SMTP message passed to the core.\n"); #endif core_api->receive(coreMP); } END: #if DEBUG_SMTP GNUNET_GE_LOG(ectx, GNUNET_GE_DEBUG | GNUNET_GE_REQUEST | GNUNET_GE_USER, "SMTP message processed.\n"); #endif if (fdes != NULL) fclose(fdes); } GNUNET_free(line); return NULL; } /* *************** API implementation *************** */ /** * Verify that a hello-Message is correct (a node is reachable at that * address). Since the reply will be asynchronous, a method must be * called on success. * * @param hello the hello message to verify * (the signature/crc have been verified before) * @return GNUNET_OK on success, GNUNET_SYSERR on error */ static int api_verify_hello(const GNUNET_MessageHello * hello) { const EmailAddress *maddr; maddr = (const EmailAddress *)&hello[1]; if ((ntohs(hello->header.size) != sizeof(GNUNET_MessageHello) + ntohs(hello->senderAddressSize)) || (maddr->senderAddress [ntohs(hello->senderAddressSize) - 1 - FILTER_STRING_SIZE] != '\0')) { GNUNET_GE_BREAK(ectx, 0); return GNUNET_SYSERR; /* obviously invalid */ } if (NULL == strstr(maddr->filter, ": ")) return GNUNET_SYSERR; return GNUNET_OK; } /** * Create a hello-Message for the current node. The hello is created * without signature and without a timestamp. The GNUnet core will * GNUNET_RSA_sign the message and add an expiration time. * * @return hello on success, NULL on error */ static GNUNET_MessageHello * api_create_hello() { GNUNET_MessageHello *msg; char *filter; EmailAddress *haddr; int i; GNUNET_GC_get_configuration_value_string(core_api->cfg, "SMTP", "FILTER", "X-mailer: GNUnet", &filter); if (NULL == strstr(filter, ": ")) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_BULK | GNUNET_GE_USER, _("SMTP filter string to invalid, lacks ': '\n")); GNUNET_free(filter); return NULL; } if (strlen(filter) > FILTER_STRING_SIZE) { filter[FILTER_STRING_SIZE] = '\0'; GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_BULK | GNUNET_GE_USER, _("SMTP filter string to long, capped to `%s'\n"), filter); } i = (strlen(email) + 8) & (~7); /* make multiple of 8 */ msg = GNUNET_malloc(sizeof(GNUNET_MessageHello) + sizeof(EmailAddress) + i); memset(msg, 0, sizeof(GNUNET_MessageHello) + sizeof(EmailAddress) + i); haddr = (EmailAddress *)&msg[1]; memset(&haddr->filter[0], 0, FILTER_STRING_SIZE); strcpy(&haddr->filter[0], filter); GNUNET_memcpy(&haddr->senderAddress[0], email, strlen(email) + 1); msg->senderAddressSize = htons(strlen(email) + 1 + sizeof(EmailAddress)); msg->protocol = htons(GNUNET_TRANSPORT_PROTOCOL_NUMBER_SMTP); msg->MTU = htonl(smtpAPI.mtu); msg->header.size = htons(GNUNET_sizeof_hello(msg)); if (api_verify_hello(msg) == GNUNET_SYSERR) GNUNET_GE_ASSERT(ectx, 0); GNUNET_free(filter); return msg; } struct GetMessageClosure { unsigned int esize; unsigned int pos; char *ebody; }; static const char * get_message(void **buf, int *len, void *cls) { struct GetMessageClosure *gmc = cls; *buf = NULL; if (len == NULL) { gmc->pos = 0; return NULL; } if (gmc->pos == gmc->esize) return NULL; /* done */ *len = gmc->esize; gmc->pos = gmc->esize; return gmc->ebody; } /** * Send a message to the specified remote node. * * @param tsession the GNUNET_MessageHello identifying the remote node * @param msg what to send * @param size the size of the message * @param important is this message important enough to override typical limits? * @return GNUNET_SYSERR on error, GNUNET_OK on success */ static int api_send(GNUNET_TSession * tsession, const void *msg, const unsigned int size, int important) { const GNUNET_MessageHello *hello; const EmailAddress *haddr; char *m; char *filter; char *fvalue; SMTPMessage *mp; struct GetMessageClosure gm_cls; smtp_session_t session; smtp_message_t message; smtp_recipient_t recipient; #define EBUF_LEN 128 char ebuf[EBUF_LEN]; GNUNET_CronTime now; if (smtp_shutdown == GNUNET_YES) return GNUNET_SYSERR; if ((size == 0) || (size > smtpAPI.mtu)) { GNUNET_GE_BREAK(ectx, 0); return GNUNET_SYSERR; } now = GNUNET_get_time(); if ((important != GNUNET_YES) && ((now - last_transmission) * rate_limit) < GNUNET_CRON_HOURS) return GNUNET_NO; /* rate too high */ last_transmission = now; hello = (const GNUNET_MessageHello *)tsession->internal; if (hello == NULL) return GNUNET_SYSERR; GNUNET_mutex_lock(lock); session = smtp_create_session(); if (session == NULL) { GNUNET_GE_LOG(ectx, GNUNET_GE_ERROR | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_IMMEDIATE, _("SMTP: `%s' failed: %s.\n"), "smtp_create_session", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); GNUNET_mutex_unlock(lock); return GNUNET_SYSERR; } if (0 == smtp_set_server(session, smtp_server_name)) { GNUNET_GE_LOG(ectx, GNUNET_GE_ERROR | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_IMMEDIATE, _("SMTP: `%s' failed: %s.\n"), "smtp_set_server", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); smtp_destroy_session(session); GNUNET_mutex_unlock(lock); return GNUNET_SYSERR; } haddr = (const EmailAddress *)&hello[1]; message = smtp_add_message(session); if (message == NULL) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_BULK, _("SMTP: `%s' failed: %s.\n"), "smtp_add_message", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); smtp_destroy_session(session); GNUNET_mutex_unlock(lock); return GNUNET_SYSERR; } smtp_set_header(message, "To", NULL, haddr->senderAddress); smtp_set_header(message, "From", NULL, email); filter = GNUNET_strdup(haddr->filter); fvalue = strstr(filter, ": "); GNUNET_GE_ASSERT(NULL, NULL != fvalue); fvalue[0] = '\0'; fvalue += 2; if (0 == smtp_set_header(message, filter, fvalue)) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_BULK, _("SMTP: `%s' failed: %s.\n"), "smtp_set_header", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); smtp_destroy_session(session); GNUNET_mutex_unlock(lock); GNUNET_free(filter); return GNUNET_SYSERR; } GNUNET_free(filter); m = GNUNET_malloc(size + sizeof(SMTPMessage)); GNUNET_memcpy(m, msg, size); mp = (SMTPMessage *)&m[size]; mp->header.size = htons(size + sizeof(SMTPMessage)); mp->header.type = htons(0); mp->sender = *core_api->my_identity; gm_cls.ebody = NULL; gm_cls.pos = 0; gm_cls.esize = GNUNET_STRINGS_base64_encode(m, size + sizeof(SMTPMessage), &gm_cls.ebody); GNUNET_free(m); if (0 == smtp_size_set_estimate(message, gm_cls.esize)) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_BULK, _("SMTP: `%s' failed: %s.\n"), "smtp_size_set_estimate", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); } if (0 == smtp_set_messagecb(message, &get_message, &gm_cls)) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_BULK, _("SMTP: `%s' failed: %s.\n"), "smtp_set_messagecb", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); smtp_destroy_session(session); GNUNET_mutex_unlock(lock); GNUNET_free(gm_cls.ebody); return GNUNET_SYSERR; } recipient = smtp_add_recipient(message, haddr->senderAddress); if (recipient == NULL) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_BULK, _("SMTP: `%s' failed: %s.\n"), "smtp_add_recipient", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); smtp_destroy_session(session); GNUNET_mutex_unlock(lock); return GNUNET_SYSERR; } if (0 == smtp_start_session(session)) { GNUNET_GE_LOG(ectx, GNUNET_GE_WARNING | GNUNET_GE_ADMIN | GNUNET_GE_USER | GNUNET_GE_BULK, _("SMTP: `%s' failed: %s.\n"), "smtp_start_session", smtp_strerror(smtp_errno(), ebuf, EBUF_LEN)); smtp_destroy_session(session); GNUNET_mutex_unlock(lock); GNUNET_free(gm_cls.ebody); return GNUNET_SYSERR; } if (stats != NULL) stats->change(stat_bytesSent, size); if (core_api->load_monitor != NULL) GNUNET_network_monitor_notify_transmission(core_api->load_monitor, GNUNET_ND_UPLOAD, gm_cls.esize); smtp_message_reset_status(message); /* this is needed to plug a 28-byte/message memory leak in libesmtp */ smtp_destroy_session(session); GNUNET_mutex_unlock(lock); GNUNET_free(gm_cls.ebody); return GNUNET_OK; } /** * Establish a connection to a remote node. * @param hello the hello-Message for the target node * @param tsessionPtr the session handle that is to be set * @param may_reuse can we re-use an existing connection? * @return GNUNET_OK on success, GNUNET_SYSERR if the operation failed */ static int api_connect(const GNUNET_MessageHello * hello, GNUNET_TSession ** tsessionPtr, int may_reuse) { GNUNET_TSession *tsession; tsession = GNUNET_new(GNUNET_TSession); tsession->internal = GNUNET_malloc(GNUNET_sizeof_hello(hello)); tsession->peer = hello->senderIdentity; GNUNET_memcpy(tsession->internal, hello, GNUNET_sizeof_hello(hello)); tsession->ttype = smtpAPI.protocol_number; (*tsessionPtr) = tsession; return GNUNET_OK; } /** * Disconnect from a remote node. * * @param tsession the session that is closed * @return GNUNET_OK on success, GNUNET_SYSERR if the operation failed */ static int api_disconnect(GNUNET_TSession * tsession) { if (tsession != NULL) { if (tsession->internal != NULL) GNUNET_free(tsession->internal); GNUNET_free(tsession); } return GNUNET_OK; } /** * Start the server process to receive inbound traffic. * @return GNUNET_OK on success, GNUNET_SYSERR if the operation failed */ static int api_start_transport_server() { smtp_shutdown = GNUNET_NO; /* initialize SMTP network */ dispatchThread = GNUNET_thread_create(&listenAndDistribute, NULL, 1024 * 4); if (dispatchThread == NULL) { GNUNET_GE_DIE_STRERROR(ectx, GNUNET_GE_ADMIN | GNUNET_GE_BULK | GNUNET_GE_FATAL, "pthread_create"); return GNUNET_SYSERR; } return GNUNET_OK; } /** * Shutdown the server process (stop receiving inbound traffic). Maybe * restarted later! */ static int api_stop_transport_server() { void *unused; smtp_shutdown = GNUNET_YES; GNUNET_thread_stop_sleep(dispatchThread); GNUNET_thread_join(dispatchThread, &unused); return GNUNET_OK; } /** * Convert SMTP hello to an IP address (always fails). */ static int api_hello_to_address(const GNUNET_MessageHello * hello, void **sa, unsigned int *sa_len) { return GNUNET_SYSERR; } /** * Always fails. */ static int api_associate(GNUNET_TSession * tsession) { return GNUNET_SYSERR; /* SMTP connections can never be associated */ } /** * Always succeeds (for now; we should look at adding * frequency limits to SMTP in the future!). */ static int api_test_would_try(GNUNET_TSession * tsession, unsigned int size, int important) { return GNUNET_OK; /* we always try... */ } /** * The exported method. Makes the core api available via a global and * returns the smtp transport API. */ GNUNET_TransportAPI * inittransport_smtp(struct GNUNET_CoreAPIForTransport * core) { unsigned long long mtu; struct sigaction sa; core_api = core; ectx = core->ectx; if (!GNUNET_GC_have_configuration_value(core_api->cfg, "SMTP", "EMAIL")) { GNUNET_GE_LOG(ectx, GNUNET_GE_ERROR | GNUNET_GE_BULK | GNUNET_GE_USER, _ ("No email-address specified, can not start SMTP transport.\n")); return NULL; } GNUNET_GC_get_configuration_value_number(core_api->cfg, "SMTP", "MTU", 1200, SMTP_MESSAGE_SIZE, SMTP_MESSAGE_SIZE, &mtu); GNUNET_GC_get_configuration_value_number(core_api->cfg, "SMTP", "RATELIMIT", 0, 0, 1024 * 1024, &rate_limit); stats = core_api->service_request("stats"); if (stats != NULL) { stat_bytesReceived = stats->create(gettext_noop("# bytes received via SMTP")); stat_bytesSent = stats->create(gettext_noop("# bytes sent via SMTP")); stat_bytesDropped = stats->create(gettext_noop("# bytes dropped by SMTP (outgoing)")); } GNUNET_GC_get_configuration_value_filename(core_api->cfg, "SMTP", "PIPE", &pipename); unlink(pipename); if (0 != mkfifo(pipename, S_IWUSR | S_IRUSR | S_IWGRP | S_IWOTH)) { GNUNET_GE_LOG_STRERROR(ectx, GNUNET_GE_ADMIN | GNUNET_GE_BULK | GNUNET_GE_FATAL, "mkfifo"); GNUNET_free(pipename); core_api->service_release(stats); stats = NULL; return NULL; } /* we need to allow the mailer program to send us messages; * easiest done by giving it write permissions (see Mantis #1142) */ if (0 != chmod(pipename, S_IWUSR | S_IRUSR | S_IWGRP | S_IWOTH)) GNUNET_GE_LOG_STRERROR(ectx, GNUNET_GE_ADMIN | GNUNET_GE_BULK | GNUNET_GE_WARNING, "chmod"); GNUNET_GC_get_configuration_value_string(core_api->cfg, "SMTP", "EMAIL", NULL, &email); lock = GNUNET_mutex_create(GNUNET_NO); GNUNET_GC_get_configuration_value_string(core_api->cfg, "SMTP", "SERVER", "localhost:25", &smtp_server_name); sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE, &sa, &old_handler); smtpAPI.protocol_number = GNUNET_TRANSPORT_PROTOCOL_NUMBER_SMTP; smtpAPI.mtu = mtu - sizeof(SMTPMessage); smtpAPI.cost = 50; smtpAPI.hello_verify = &api_verify_hello; smtpAPI.hello_create = &api_create_hello; smtpAPI.connect = &api_connect; smtpAPI.send = &api_send; smtpAPI.associate = &api_associate; smtpAPI.disconnect = &api_disconnect; smtpAPI.server_start = &api_start_transport_server; smtpAPI.server_stop = &api_stop_transport_server; smtpAPI.hello_to_address = &api_hello_to_address; smtpAPI.send_now_test = &api_test_would_try; return &smtpAPI; } void donetransport_smtp() { sigaction(SIGPIPE, &old_handler, NULL); GNUNET_free(smtp_server_name); if (stats != NULL) { core_api->service_release(stats); stats = NULL; } GNUNET_mutex_destroy(lock); lock = NULL; unlink(pipename); GNUNET_free(pipename); pipename = NULL; GNUNET_free(email); email = NULL; } /* end of smtp.c */