/*
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 */