/*
anubisusr.c
Copyright (C) 2004-2024 The Anubis Team.
GNU Anubis is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation; either version 3 of the License, or (at your
option) any later version.
GNU Anubis 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 General Public License for more details.
You should have received a copy of the GNU General Public License along
with GNU Anubis. If not, see .
*/
#include
struct secure_struct secure;
int enable_tls = 1;
char *progname;
char *smtp_host = "localhost";
int smtp_port = 24;
char *rcfile_name = NULL;
char *netrc_name = NULL;
int verbose;
#define VDETAIL(n,s) do { if (verbose>=(n)) printf s; } while(0)
ANUBIS_SMTP_REPLY smtp_capa;
void error (const char *, ...);
int send_line (char *buf);
void smtp_get_reply (ANUBIS_SMTP_REPLY repl);
static void smtp_quit (void);
#define R_CONT 0x8000
#define R_CODEMASK 0xfff
void
error (const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
fprintf (stderr, "%s: ", progname);
vfprintf (stderr, fmt, ap);
fprintf (stderr, "\n");
va_end (ap);
}
/* Basic I/O */
NET_STREAM iostream;
void
info (int mode, const char *fmt, ...)
{
va_list ap;
if (verbose == 0)
return;
va_start (ap, fmt);
vfprintf (stderr, fmt, ap);
va_end (ap);
fprintf (stderr, "\n");
}
void
anubis_error (int exit_code, int error_code, const char *fmt, ...)
{
va_list ap;
va_start (ap, fmt);
fprintf (stderr, "%s: ", progname);
vfprintf (stderr, fmt, ap);
if (error_code)
fprintf (stderr, ": %s", strerror (error_code));
fprintf (stderr, "\n");
va_end (ap);
}
void
starttls (void)
{
ANUBIS_SMTP_REPLY reply = smtp_reply_new ();
VDETAIL (1, (_("Starting TLS negotiation\n")));
send_line ("STARTTLS");
smtp_get_reply (reply);
if (!smtp_reply_code_eq (reply, "220"))
{
error (_("Server rejected TLS negotiation"));
exit (1);
}
smtp_reply_free (reply);
iostream = start_ssl_client (iostream, verbose > 2);
if (!iostream)
{
error (_("TLS negotiation failed"));
smtp_quit ();
exit (1);
}
}
/* Auxiliary functions */
char *
skipws (char *str)
{
while (*str && isspace (*(u_char *) str))
str++;
return str;
}
char *
skipword (char *str)
{
while (*str && !isspace (*(u_char *) str))
str++;
return str;
}
/* FIXME: Move to the library and unify with hostname_error() */
const char *
h_error_string (int ec)
{
static struct h_err_tab
{
int code;
char *descr;
}
*ep, h_err_tab[] =
{
{ HOST_NOT_FOUND, N_("No such host is known in the database.") },
{ TRY_AGAIN, N_("Temporary error. Try again later.") },
{ NO_RECOVERY, N_("Non-recoverable error") },
{ NO_ADDRESS, N_("No Internet address is associated with the name") },
{ 0, 0 }
};
for (ep = h_err_tab; ep->descr; ep++)
if (ep->code == ec)
return gettext (ep->descr);
return gettext ("Unknown error");
};
/* FIXME: move to the library. Modify connect_directly_to() to use it */
int
parse_host (char *host, int port, struct sockaddr_in *addr)
{
struct hostent *hp = gethostbyname (host);
if (!hp)
{
error (_("Cannot resolve %s: %s"), host, h_error_string (h_errno));
return -1;
}
addr->sin_family = AF_INET;
addr->sin_port = htons (port);
if (hp->h_length != sizeof addr->sin_addr.s_addr)
{
error (_("Cannot resolve %s: received illegal address length (%d)"),
host, hp->h_length);
return -1;
}
memcpy (&addr->sin_addr.s_addr, hp->h_addr, sizeof addr->sin_addr.s_addr);
return 0;
}
/* GSASL mechanisms */
static ANUBIS_LIST auth_mech_list;
void
add_mech (char *arg)
{
if (!auth_mech_list)
auth_mech_list = list_create ();
list_append (auth_mech_list, arg);
}
/* Capability handling */
static int
name_cmp (void *item, void *data)
{
return strcmp (item, data);
}
char *
find_capa_v (ANUBIS_SMTP_REPLY repl, const char *name, ANUBIS_LIST list)
{
size_t n;
if (smtp_reply_has_capa (repl, name, &n))
{
const char *str = smtp_reply_line (repl, n);
size_t i;
wordsplit_t ws;
char *rv = NULL;
if (wordsplit (str, &ws,
WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_SQUEEZE_DELIMS))
{
error (_("wordsplit failed: %s"), wordsplit_strerror (&ws));
wordsplit_free (&ws);
return NULL;
}
if (!list)
{
if (ws.ws_wordv[1])
rv = ws.ws_wordv[1];
}
else
{
for (i = 0; !rv && i < ws.ws_wordc; i++)
rv = list_locate (list, ws.ws_wordv[i], name_cmp);
}
if (rv)
rv = xstrdup (rv);
wordsplit_free (&ws);
return rv;
}
return NULL;
}
/* I/O functions */
int
send_line (char *buf)
{
size_t size = strlen (buf);
size_t n;
int rc;
VDETAIL (2, ("C: %s\n", buf));
rc = stream_write (iostream, buf, size, &n);
if (rc)
{
error (_("write failed: %s"), stream_strerror (iostream, rc));
return rc;
}
rc = stream_write (iostream, CRLF, 2, &n);
if (rc)
error (_("write failed: %s"), stream_strerror (iostream, rc));
return rc;
}
static ssize_t
_usr_reader (void *data, char **sptr, size_t *psize)
{
size_t n;
int rc = stream_getline (iostream, sptr, psize, &n);
if (rc)
{
error (_("read failed: %s"), stream_strerror (iostream, rc));
exit (1);
}
VDETAIL (2, ("S: %*.*s", (int) n, (int) n, *sptr));
return n;
}
void
smtp_get_reply (ANUBIS_SMTP_REPLY repl)
{
smtp_reply_read (repl, _usr_reader, NULL);
}
void
smtp_print_reply (FILE * fp, ANUBIS_SMTP_REPLY repl)
{
size_t i;
const char *p;
for (i = 0; (p = smtp_reply_line (repl, i)); i++)
fprintf (fp, "%s\n", p);
fflush (fp);
}
void
smtp_ehlo (int xelo)
{
ANUBIS_SMTP_REPLY repl = smtp_reply_new ();
send_line (xelo ? "XELO localhost" : "EHLO localhost");
smtp_get_reply (repl);
if (!smtp_reply_code_eq (repl, "250"))
{
error (_("Server refused handshake"));
smtp_print_reply (stderr, repl);
exit (1);
}
smtp_capa = repl;
}
struct auth_args
{
char *anon_token;
char *authorization_id;
char *authentication_id;
char *password;
char *service;
char *hostname;
char *service_name;
char *passcode;
char *qop;
char *realm;
};
struct auth_args auth_args;
void
assign_string (char **pstring, const char *value)
{
if (*pstring)
free (*pstring);
*pstring = strdup (value);
}
/* Compare two hostnames. Return 0 if they have the same address type,
address length *and* at least one of the addresses of A matches
B */
int
hostcmp (const char *a, const char *b)
{
struct hostent *hp = gethostbyname (a);
char **addrlist;
char *dptr;
char **addr;
size_t i, count;
size_t entry_length;
int entry_type;
if (!hp)
return 1;
for (count = 1, addr = hp->h_addr_list; *addr; addr++)
count++;
addrlist = xmalloc (count * (sizeof *addrlist + hp->h_length)
- hp->h_length);
dptr = (char *) (addrlist + count);
for (i = 0; i < count - 1; i++)
{
memcpy (dptr, hp->h_addr_list[i], hp->h_length);
addrlist[i] = dptr;
dptr += hp->h_length;
}
addrlist[i] = NULL;
entry_length = hp->h_length;
entry_type = hp->h_addrtype;
hp = gethostbyname (b);
if (!hp || entry_length != hp->h_length || entry_type != hp->h_addrtype)
{
free (addrlist);
return 1;
}
for (addr = addrlist; *addr; addr++)
{
char **p;
for (p = hp->h_addr_list; *p; p++)
{
if (memcmp (*addr, *p, entry_length) == 0)
{
free (addrlist);
return 0;
}
}
}
free (addrlist);
return 1;
}
/* Parse traditional .netrc file. Set up auth_args fields in accordance with
it. */
void
parse_netrc (const char *filename)
{
FILE *fp;
char *buf = NULL;
size_t n = 0;
size_t def_argc = 0;
char **def_argv = NULL;
char **p_argv = NULL;
int line = 0;
wordsplit_t ws = { .ws_comment = "#" };
int wsflags = WRDSF_NOVAR | WRDSF_NOCMD | WRDSF_SQUEEZE_DELIMS | WRDSF_COMMENT;
fp = fopen (filename, "r");
if (!fp)
{
if (errno != ENOENT)
{
error (_("Cannot open configuration file %s: %s"),
filename, strerror (errno));
}
return;
}
else
VDETAIL (1, (_("Opening configuration file %s...\n"), filename));
while (xgetline (&buf, &n, fp) > 0 && n > 0)
{
int rc;
char *p;
size_t len;
line++;
len = strlen (buf);
if (len > 1 && buf[len - 1] == '\n')
buf[len - 1] = 0;
p = skipws (buf);
if (*p == 0 || *p == '#')
continue;
rc = wordsplit (p, &ws, wsflags);
wsflags |= WRDSF_REUSE;
if (rc)
{
error (_("wordsplit failed: %s"), wordsplit_strerror (&ws));
break;
}
if (strcmp (ws.ws_wordv[0], "machine") == 0)
{
if (hostcmp (ws.ws_wordv[1], smtp_host) == 0)
{
VDETAIL (1, (_("Found matching line %d\n"), line));
if (def_argc)
argv_free (def_argv);
wordsplit_get_words (&ws, &def_argc, &def_argv);
p_argv = def_argv + 2;
break;
}
}
else if (strcmp (ws.ws_wordv[0], "default") == 0)
{
VDETAIL (1, (_("Found default line %d\n"), line));
if (def_argc)
argv_free (def_argv);
wordsplit_get_words (&ws, &def_argc, &def_argv);
p_argv = def_argv + 1;
}
else
{
VDETAIL (1, (_("Ignoring unrecognized line %d\n"), line));
}
}
fclose (fp);
free (buf);
if (wsflags & WRDSF_REUSE)
wordsplit_free (&ws);
if (!p_argv)
VDETAIL (1, (_("No matching line found\n")));
else
{
while (*p_argv)
{
if (!p_argv[1])
{
error (_("%s:%d: incomplete sentence"), filename, line);
break;
}
if (strcmp (*p_argv, "login") == 0)
{
assign_string (&auth_args.authentication_id, p_argv[1]);
assign_string (&auth_args.authorization_id, p_argv[1]);
}
else if (strcmp (*p_argv, "password") == 0)
assign_string (&auth_args.password, p_argv[1]);
p_argv += 2;
}
argv_free (def_argv);
}
}
char *
get_input (const char *prompt)
{
char *buf = NULL;
size_t n;
printf ("%s", prompt);
fflush (stdout);
xgetline (&buf, &n, stdin);
n = strlen (buf);
if (n > 1 && buf[n - 1] == '\n')
buf[n - 1] = 0;
return buf;
}
static int
callback (Gsasl *ctx, Gsasl_session *sctx, Gsasl_property prop)
{
int rc = GSASL_OK;
switch (prop)
{
case GSASL_PASSWORD:
if (auth_args.password == NULL)
{
if (anubis_getpass (_("Password: "), &auth_args.password))
{
error ("anubis_getpass: %s", strerror (errno));
return GSASL_AUTHENTICATION_ERROR;
}
}
gsasl_property_set (sctx, prop, auth_args.password);
break;
case GSASL_SERVICE:
if (auth_args.service == NULL)
auth_args.service = get_input (_("GSSAPI service name: "));
if (auth_args.service == NULL)
return GSASL_AUTHENTICATION_ERROR;
gsasl_property_set (sctx, prop, auth_args.service);
break;
case GSASL_REALM:
if (auth_args.realm == NULL)
auth_args.realm = get_input (_("Client realm: "));
if (auth_args.realm == NULL)
return GSASL_AUTHENTICATION_ERROR;
gsasl_property_set (sctx, prop, auth_args.realm);
break;
case GSASL_HOSTNAME:
if (auth_args.hostname == NULL)
auth_args.hostname = get_input (_("Hostname of server: "));
if (auth_args.hostname == NULL)
return GSASL_AUTHENTICATION_ERROR;
gsasl_property_set (sctx, prop, auth_args.hostname);
break;
case GSASL_ANONYMOUS_TOKEN:
if (auth_args.anon_token == NULL)
auth_args.anon_token = get_input (_("Anonymous token: "));
if (auth_args.anon_token == NULL)
return GSASL_AUTHENTICATION_ERROR;
gsasl_property_set (sctx, prop, auth_args.anon_token);
break;
case GSASL_AUTHID:
if (auth_args.authentication_id == NULL)
auth_args.authentication_id = get_input (_("Authentication ID: "));
if (auth_args.authentication_id == NULL)
return GSASL_AUTHENTICATION_ERROR;
gsasl_property_set (sctx, prop, auth_args.authentication_id);
break;
case GSASL_AUTHZID:
if (auth_args.authorization_id == NULL)
auth_args.authorization_id = get_input (_("Authorization ID: "));
if (auth_args.authorization_id == NULL)
return GSASL_AUTHENTICATION_ERROR;
gsasl_property_set (sctx, prop, auth_args.authorization_id);
break;
case GSASL_PASSCODE:
if (auth_args.passcode == NULL)
{
if (anubis_getpass (_("Passcode: "), &auth_args.passcode))
{
error ("anubis_getpass: %s", strerror (errno));
return GSASL_AUTHENTICATION_ERROR;
}
}
gsasl_property_set (sctx, prop, auth_args.passcode);
break;
default:
rc = GSASL_NO_CALLBACK;
error (_("Unsupported callback property %d"), prop);
break;
}
return rc;
}
void
smtp_quit (void)
{
ANUBIS_SMTP_REPLY repl = smtp_reply_new ();
send_line ("QUIT");
smtp_get_reply (repl);
smtp_reply_free (repl); /* There's no use checking */
}
/* GSASL Authentication */
int
do_gsasl_auth (Gsasl *ctx, char *mech)
{
char *output;
int rc;
Gsasl_session *sess_ctx = NULL;
ANUBIS_SMTP_REPLY repl;
char buf[LINEBUFFER + 1];
snprintf (buf, sizeof buf, "AUTH %s", mech);
send_line (buf);
rc = gsasl_client_start (ctx, mech, &sess_ctx);
if (rc != GSASL_OK)
{
error (_("SASL gsasl_client_start: %s"), gsasl_strerror (rc));
exit (1);
}
output = NULL;
repl = smtp_reply_new ();
smtp_get_reply (repl);
if (!smtp_reply_code_eq (repl, "334"))
{
error (_("GSASL handshake aborted"));
smtp_print_reply (stderr, repl);
exit (1);
}
do
{
char *str;
smtp_reply_get_line (repl, 0, &str, NULL);
rc = gsasl_step64 (sess_ctx, str + 4, &output);
free (str);
if (rc != GSASL_NEEDS_MORE && rc != GSASL_OK)
break;
send_line (output);
if (rc == GSASL_OK)
break;
smtp_get_reply (repl);
if (!smtp_reply_code_eq (repl, "334"))
{
error (_("GSASL handshake aborted"));
smtp_print_reply (stderr, repl);
exit (1);
}
}
while (rc == GSASL_NEEDS_MORE);
free (output);
if (rc != GSASL_OK)
{
error (_("GSASL error: %s"), gsasl_strerror (rc));
exit (1);
}
smtp_get_reply (repl);
if (smtp_reply_code_eq (repl, "334"))
{
/* Additional data. Do we need it? */
smtp_get_reply (repl);
}
if (!smtp_reply_code_eq (repl, "235"))
{
error (_("Authentication failed"));
smtp_print_reply (stderr, repl);
smtp_quit ();
exit (1);
}
smtp_reply_free (repl);
VDETAIL (1, (_("Authentication successful\n")));
if (sess_ctx)
install_gsasl_stream (sess_ctx, &iostream);
return 0;
}
void
smtp_auth (void)
{
Gsasl *ctx;
char *mech;
int rc;
mech = find_capa_v (smtp_capa, "AUTH", auth_mech_list);
if (!mech)
{
error (_("No suitable authentication mechanism found"));
smtp_quit ();
exit (1);
}
VDETAIL (1, (_("Selected authentication mechanism: %s\n"), mech));
rc = gsasl_init (&ctx);
if (rc != GSASL_OK)
{
error (_("cannot initialize libgsasl: %s"), gsasl_strerror (rc));
smtp_quit ();
exit (1);
}
gsasl_callback_set (ctx, callback);
do_gsasl_auth (ctx, mech);
}
const char *
get_home_dir (void)
{
static char *home;
if (!home)
{
struct passwd *pwd = getpwuid (getuid ());
if (pwd)
home = pwd->pw_dir;
else
home = getenv ("HOME");
if (!home)
{
error (_("What is your home directory?"));
exit (1);
}
}
return home;
}
/* Auxiliary functions */
char *
rc_name (void)
{
char *rc;
const char *home;
if (rcfile_name)
return rcfile_name;
home = get_home_dir ();
rc = xmalloc (strlen (home) + 1 + sizeof DEFAULT_LOCAL_RCFILE);
strcpy (rc, home);
strcat (rc, "/");
strcat (rc, DEFAULT_LOCAL_RCFILE);
return rc;
}
#define CMP_UNCHANGED 0
#define CMP_CHANGED 1
#define CMP_ERROR 2
int
diff (char *file, ANUBIS_SMTP_REPLY repl)
{
const char *input = smtp_reply_line (repl, 0) + 4;
unsigned char *digest;
char const *err;
int rc;
int fd;
fd = open (file, O_RDONLY);
if (fd == -1)
{
error (_("Cannot open file %s: %s"), file, strerror (errno));
return CMP_ERROR;
}
rc = anubis_md5_file (fd, &digest, &err);
close (fd);
if (rc)
{
error (_("Can't compute file digest: %s"), err);
return CMP_ERROR;
}
rc = strcasecmp ((char*)digest, input) == 0 ? CMP_UNCHANGED : CMP_CHANGED;
free (digest);
return rc;
}
void
smtp_upload (char *rcname)
{
FILE *fp;
ANUBIS_SMTP_REPLY repl;
char *buf = NULL;
size_t n;
fp = fopen (rcname, "r");
if (!fp)
{
error (_("Cannot open file %s: %s"), rcname, strerror (errno));
return;
}
VDETAIL (1, (_("Uploading %s\n"), rcname));
repl = smtp_reply_new ();
send_line ("XDATABASE UPLOAD");
smtp_get_reply (repl);
if (!smtp_reply_code_eq (repl, "354"))
{
error (_("UPLOAD failed"));
smtp_print_reply (stderr, repl);
fclose (fp);
smtp_reply_free (repl);
return;
}
while (xgetline (&buf, &n, fp) > 0 && n > 0)
{
size_t len = strlen (buf);
if (len && buf[len - 1] == '\n')
buf[len - 1] = 0;
send_line (buf);
}
send_line (".");
fclose (fp);
smtp_get_reply (repl);
if (!smtp_reply_code_eq (repl, "250"))
{
smtp_print_reply (stderr, repl);
}
smtp_reply_free (repl);
}
/* Main entry points */
int
synch (void)
{
int fd;
int rc;
struct sockaddr_in addr;
ANUBIS_SMTP_REPLY repl;
char *rcname;
init_ssl_libs ();
VDETAIL (1, (_("Using remote SMTP %s:%d\n"), smtp_host, smtp_port));
if (parse_host (smtp_host, smtp_port, &addr))
return 1;
if ((fd = socket (AF_INET, SOCK_STREAM, 0)) == -1)
{
error (_("Cannot create socket: %s"), strerror (errno));
return 1;
}
if (connect (fd, (struct sockaddr *) &addr, sizeof (addr)) == -1)
{
error (_("Could not connect to %s:%u: %s."),
smtp_host, smtp_port, strerror (errno));
return -1;
}
stream_create (&iostream);
stream_set_io (iostream, (void *) (ptrdiff_t) fd, NULL, NULL, NULL, NULL,
NULL);
repl = smtp_reply_new ();
smtp_get_reply (repl);
if (!smtp_reply_code_eq (repl, "220"))
{
error (_("Server refused connection"));
smtp_print_reply (stderr, repl);
smtp_reply_free (repl);
return 1;
}
smtp_ehlo (1);
if (enable_tls && smtp_reply_has_capa (smtp_capa, "STARTTLS", NULL))
{
starttls ();
smtp_ehlo (0);
}
smtp_auth ();
/* Get the capabilities */
smtp_ehlo (0);
if (!smtp_reply_has_capa (smtp_capa, "XDATABASE", NULL))
{
error (_("Remote party does not reveal XDATABASE capability"));
smtp_reply_free (repl);
smtp_quit ();
return 1;
}
send_line ("XDATABASE EXAMINE");
smtp_get_reply (repl);
if (smtp_reply_code_eq (repl, "300"))
{
rcname = rc_name ();
rc = CMP_CHANGED;
}
else if (smtp_reply_code_eq (repl, "250"))
{
rcname = rc_name ();
rc = diff (rcname, repl);
}
else
{
error (_("EXAMINE failed"));
smtp_print_reply (stderr, repl);
smtp_reply_free (repl);
smtp_quit ();
return 1;
}
smtp_reply_free (repl);
if (rc == CMP_CHANGED)
{
VDETAIL (1, (_("File changed\n")));
smtp_upload (rcname);
}
else
VDETAIL (1, (_("File NOT changed\n")));
smtp_quit ();
return 0;
}
#define NETRC_NAME ".netrc"
void
read_netrc (void)
{
if (netrc_name)
parse_netrc (netrc_name);
else
{
const char *home = get_home_dir ();
char *netrc = xmalloc (strlen (home) + 1 + sizeof NETRC_NAME);
strcpy (netrc, home);
strcat (netrc, "/");
strcat (netrc, NETRC_NAME);
parse_netrc (netrc);
free (netrc);
}
}
void
xnomem (void)
{
error ("%s", _("Not enough memory"));
exit (1);
}
int
main (int argc, char **argv)
{
int index;
progname = strrchr (argv[0], '/');
if (!progname)
progname = argv[0];
else
progname++;
usr_get_options (argc, argv, &index);
argc -= optind;
argv += optind;
if (argc > 1)
{
error (_("Too many arguments. Try anubisusr --help for more info."));
exit (1);
}
if (argc == 1)
{
char *p;
smtp_host = argv[0];
p = strchr (smtp_host, ':');
if (p)
{
unsigned long n;
*p++ = 0;
n = strtoul (p, &p, 0);
if (n > USHRT_MAX)
{
error (_("Port value too big"));
exit (1);
}
smtp_port = n;
}
}
read_netrc ();
return synch ();
}
/* EOF */