pissircd/src/modules/authprompt.c

508 lines
14 KiB
C

/*
* Auth prompt: SASL authentication for clients that don't support SASL
* (C) Copyright 2018 Bram Matthys ("Syzop") and the UnrealIRCd team
*
* This program 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 1, 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include "unrealircd.h"
ModuleHeader MOD_HEADER
= {
"authprompt",
"1.0",
"SASL authentication for clients that don't support SASL",
"UnrealIRCd Team",
"unrealircd-6",
};
/** Configuration settings */
struct {
int enabled;
MultiLine *message;
MultiLine *fail_message;
MultiLine *unconfirmed_message;
} cfg;
/** User struct */
typedef struct APUser APUser;
struct APUser {
char *authmsg;
char *reason;
};
/* Global variables */
ModDataInfo *authprompt_md = NULL;
/* Forward declarations */
static void free_config(void);
static void init_config(void);
static void config_postdefaults(void);
int authprompt_config_test(ConfigFile *, ConfigEntry *, int, int *);
int authprompt_config_run(ConfigFile *, ConfigEntry *, int);
int authprompt_sasl_continuation(Client *client, const char *buf);
int authprompt_sasl_result(Client *client, int success);
int authprompt_take_action(Client *client, BanActionValue action, const char *reason, long duration);
int authprompt_find_tkline_match(Client *client, TKL *tk);
int authprompt_pre_local_handshake_timeout(Client *client, const char **comment);
int authprompt_pre_connect(Client *client);
CMD_FUNC(cmd_auth);
void authprompt_md_free(ModData *md);
/* Some macros */
#define SetAPUser(x, y) do { moddata_client(x, authprompt_md).ptr = y; } while(0)
#define SEUSER(x) ((APUser *)moddata_client(x, authprompt_md).ptr)
#define AGENT_SID(agent_p) (agent_p->user != NULL ? agent_p->user->server : agent_p->name)
MOD_TEST()
{
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, authprompt_config_test);
return MOD_SUCCESS;
}
MOD_INIT()
{
ModDataInfo mreq;
MARK_AS_OFFICIAL_MODULE(modinfo);
memset(&mreq, 0, sizeof(mreq));
mreq.name = "authprompt";
mreq.type = MODDATATYPE_CLIENT;
mreq.free = authprompt_md_free;
authprompt_md = ModDataAdd(modinfo->handle, mreq);
if (!authprompt_md)
{
config_error("could not register authprompt moddata");
return MOD_FAILED;
}
init_config();
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, authprompt_config_run);
HookAdd(modinfo->handle, HOOKTYPE_SASL_CONTINUATION, 0, authprompt_sasl_continuation);
HookAdd(modinfo->handle, HOOKTYPE_SASL_RESULT, 0, authprompt_sasl_result);
HookAdd(modinfo->handle, HOOKTYPE_TAKE_ACTION, 0, authprompt_take_action);
HookAdd(modinfo->handle, HOOKTYPE_FIND_TKLINE_MATCH, 0, authprompt_find_tkline_match);
HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_HANDSHAKE_TIMEOUT, 0, authprompt_pre_local_handshake_timeout);
/* For HOOKTYPE_PRE_LOCAL_CONNECT we want a low priority, so we are called last.
* This gives hooks like the one from the blacklist module (pending softban)
* a chance to be handled first.
*/
HookAdd(modinfo->handle, HOOKTYPE_PRE_LOCAL_CONNECT, -1000000, authprompt_pre_connect);
CommandAdd(modinfo->handle, "AUTH", cmd_auth, 1, CMD_UNREGISTERED);
return MOD_SUCCESS;
}
MOD_LOAD()
{
config_postdefaults();
return MOD_SUCCESS;
}
MOD_UNLOAD()
{
free_config();
return MOD_SUCCESS;
}
static void init_config(void)
{
/* This sets some default values */
memset(&cfg, 0, sizeof(cfg));
cfg.enabled = 1;
}
static void config_postdefaults(void)
{
if (!cfg.message)
{
addmultiline(&cfg.message, "The server requires clients from this IP address to authenticate with a registered nickname and password.");
addmultiline(&cfg.message, "Please reconnect using SASL, or authenticate now by typing: /QUOTE AUTH nick:password");
}
if (!cfg.fail_message)
{
addmultiline(&cfg.fail_message, "Authentication failed.");
}
if (!cfg.unconfirmed_message)
{
addmultiline(&cfg.unconfirmed_message, "You are trying to use an unconfirmed services account.");
addmultiline(&cfg.unconfirmed_message, "This services account can only be used after it has been activated/confirmed.");
}
}
static void free_config(void)
{
freemultiline(cfg.message);
freemultiline(cfg.fail_message);
freemultiline(cfg.unconfirmed_message);
memset(&cfg, 0, sizeof(cfg)); /* needed! */
}
int authprompt_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
{
int errors = 0;
ConfigEntry *cep;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::authentication-prompt... */
if (!ce || !ce->name || strcmp(ce->name, "authentication-prompt"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!cep->value)
{
config_error("%s:%i: set::authentication-prompt::%s with no value",
cep->file->filename, cep->line_number, cep->name);
errors++;
} else
if (!strcmp(cep->name, "enabled"))
{
} else
if (!strcmp(cep->name, "message"))
{
} else
if (!strcmp(cep->name, "fail-message"))
{
} else
if (!strcmp(cep->name, "unconfirmed-message"))
{
} else
{
config_error("%s:%i: unknown directive set::authentication-prompt::%s",
cep->file->filename, cep->line_number, cep->name);
errors++;
}
}
*errs = errors;
return errors ? -1 : 1;
}
int authprompt_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
{
ConfigEntry *cep;
if (type != CONFIG_SET)
return 0;
/* We are only interrested in set::authentication-prompt... */
if (!ce || !ce->name || strcmp(ce->name, "authentication-prompt"))
return 0;
for (cep = ce->items; cep; cep = cep->next)
{
if (!strcmp(cep->name, "enabled"))
{
cfg.enabled = config_checkval(cep->value, CFG_YESNO);
} else
if (!strcmp(cep->name, "message"))
{
addmultiline(&cfg.message, cep->value);
} else
if (!strcmp(cep->name, "fail-message"))
{
addmultiline(&cfg.fail_message, cep->value);
} else
if (!strcmp(cep->name, "unconfirmed-message"))
{
addmultiline(&cfg.unconfirmed_message, cep->value);
}
}
return 1;
}
void authprompt_md_free(ModData *md)
{
APUser *se = md->ptr;
if (se)
{
safe_free(se->authmsg);
safe_free(se->reason);
safe_free(se);
md->ptr = se = NULL;
}
}
/** Parse an authentication request from the user (form: <user>:<pass>).
* @param str The input string with the request.
* @param username Pointer to the username string.
* @param password Pointer to the password string.
* @retval 1 if the format is correct, 0 if not.
* @note The returned 'username' and 'password' are valid until next call to parse_nickpass().
*/
int parse_nickpass(const char *str, char **username, char **password)
{
static char buf[250];
char *p;
strlcpy(buf, str, sizeof(buf));
p = strchr(buf, ':');
if (!p)
return 0;
*p++ = '\0';
*username = buf;
*password = p;
if (!*username[0] || !*password[0])
return 0;
return 1;
}
char *make_authbuf(const char *username, const char *password)
{
char inbuf[256];
static char outbuf[512];
int size;
size = strlen(username) + 1 + strlen(username) + 1 + strlen(password);
if (size >= sizeof(inbuf)-1)
return NULL; /* too long */
/* Because size limits are already checked above, we can cut some corners here: */
memset(inbuf, 0, sizeof(inbuf));
strcpy(inbuf, username);
strcpy(inbuf+strlen(username)+1, username);
strcpy(inbuf+strlen(username)+1+strlen(username)+1, password);
/* ^ normal people use stpcpy here ;) */
if (b64_encode(inbuf, size, outbuf, sizeof(outbuf)) < 0)
return NULL; /* base64 encoding error */
return outbuf;
}
/** Send first SASL authentication request (AUTHENTICATE PLAIN).
* Among other things, this is used to discover the agent
* which will later be used for this session.
*/
void send_first_auth(Client *client)
{
Client *sasl_server;
char *addr = BadPtr(client->ip) ? "0" : client->ip;
const char *certfp = moddata_client_get(client, "certfp");
sasl_server = find_client(SASL_SERVER, NULL);
if (!sasl_server)
{
/* Services down. */
return;
}
/* Make them a user, needed for CHGHOST etc that we may receive */
if (!client->user)
make_user(client);
sendto_one(sasl_server, NULL, ":%s SASL %s %s H %s %s",
me.id, SASL_SERVER, client->id, addr, addr);
if (certfp)
sendto_one(sasl_server, NULL, ":%s SASL %s %s S %s %s",
me.id, SASL_SERVER, client->id, "PLAIN", certfp);
else
sendto_one(sasl_server, NULL, ":%s SASL %s %s S %s",
me.id, SASL_SERVER, client->id, "PLAIN");
/* The rest is sent from authprompt_sasl_continuation() */
client->local->sasl_out++;
}
CMD_FUNC(cmd_auth)
{
char *username = NULL;
char *password = NULL;
char *authbuf;
if (!SEUSER(client))
{
if (HasCapability(client, "sasl"))
sendnotice(client, "ERROR: Cannot use /AUTH when your client is doing SASL.");
else
sendnotice(client, "ERROR: /AUTH authentication request received before authentication prompt (too early!)");
return;
}
if ((parc < 2) || BadPtr(parv[1]) || !parse_nickpass(parv[1], &username, &password))
{
sendnotice(client, "ERROR: Syntax is: /AUTH <nickname>:<password>");
sendnotice(client, "Example: /AUTH mynick:secretpass");
return;
}
if (!SASL_SERVER)
{
sendnotice(client, "ERROR: SASL is not configured on this server, or services are down.");
// numeric instead? SERVICESDOWN?
return;
}
/* Presumably if the user is really fast, this could happen.. */
if (*client->local->sasl_agent || SEUSER(client)->authmsg)
{
sendnotice(client, "ERROR: Previous authentication request is still in progress. Please wait.");
return;
}
authbuf = make_authbuf(username, password);
if (!authbuf)
{
sendnotice(client, "ERROR: Internal error. Oversized username/password?");
return;
}
safe_strdup(SEUSER(client)->authmsg, authbuf);
send_first_auth(client);
}
void authprompt_tag_as_auth_required(Client *client, const char *reason)
{
/* Allocate, and therefore indicate, that we are going to handle SASL for this user */
if (!SEUSER(client))
SetAPUser(client, safe_alloc(sizeof(APUser)));
safe_strdup(SEUSER(client)->reason, reason);
}
void authprompt_send_auth_required_message(Client *client)
{
/* Send the standard-reply ACCOUNT_REQUIRED_TO_CONNECT if the client supports receiving it */
if (HasCapability(client, "standard-replies"))
{
const char *reason = SEUSER(client) && SEUSER(client)->reason ? SEUSER(client)->reason : NULL;
if (reason)
sendto_one(client, NULL, "FAIL * ACCOUNT_REQUIRED_TO_CONNECT :An account is required to connect: %s", reason);
else
sendto_one(client, NULL, "FAIL * ACCOUNT_REQUIRED_TO_CONNECT :An account is required to connect");
}
/* Display set::authentication-prompt::message */
sendnotice_multiline(client, cfg.message);
}
/* Called upon "place a host ban on this user" (eg: spamfilter, blacklist, ..) */
int authprompt_take_action(Client *client, BanActionValue action, const char *reason, long duration)
{
/* If it's a soft-xx action and the user is not logged in
* and the user is not yet online, then we will handle this user.
*/
if (IsSoftBanAction(action) && !IsLoggedIn(client) && !IsUser(client) && cfg.enabled)
{
/* And tag the user */
authprompt_tag_as_auth_required(client, reason);
authprompt_send_auth_required_message(client);
return 1; /* pretend user is killed */
}
return 99; /* no action taken, proceed normally */
}
/** Called upon "check for KLINE/GLINE" */
int authprompt_find_tkline_match(Client *client, TKL *tkl)
{
/* If it's a soft-xx action and the user is not logged in
* and the user is not yet online, then we will handle this user.
*/
if (cfg.enabled &&
TKLIsServerBan(tkl) &&
(tkl->ptr.serverban->subtype & TKL_SUBTYPE_SOFT) &&
!IsLoggedIn(client) &&
!IsUser(client))
{
/* And tag the user */
authprompt_tag_as_auth_required(client, tkl->ptr.serverban->reason);
authprompt_send_auth_required_message(client);
return 1; /* pretend user is killed */
}
return 99; /* no action taken, proceed normally */
}
int authprompt_pre_connect(Client *client)
{
/* If the user is tagged as auth required and not logged in, then.. */
if (SEUSER(client) && !IsLoggedIn(client) && cfg.enabled)
{
authprompt_send_auth_required_message(client);
return HOOK_DENY; /* do not process register_user() */
}
return HOOK_CONTINUE; /* no action taken, proceed normally */
}
int authprompt_sasl_continuation(Client *client, const char *buf)
{
/* If it's not for us (eg: user is doing real SASL) then return 0. */
if (!SEUSER(client) || !SEUSER(client)->authmsg)
return 0;
if (!strcmp(buf, "+"))
{
Client *agent = find_client(client->local->sasl_agent, NULL);
if (agent)
{
sendto_one(agent, NULL, ":%s SASL %s %s C %s",
me.id, AGENT_SID(agent), client->id, SEUSER(client)->authmsg);
}
safe_free(SEUSER(client)->authmsg);
}
return 1; /* inhibit displaying of message */
}
int authprompt_sasl_result(Client *client, int success)
{
/* If it's not for us (eg: user is doing real SASL) then return 0. */
if (!SEUSER(client))
return 0;
if (!success)
{
sendnotice_multiline(client, cfg.fail_message);
return 1;
}
if (client->user && !IsLoggedIn(client))
{
sendnotice_multiline(client, cfg.unconfirmed_message);
return 1;
}
/* Authentication was a success */
if (is_handshake_finished(client))
{
register_user(client);
/* User MAY be killed now. But since we 'return 1' below, it's safe */
}
return 1; /* inhibit success/failure message */
}
/** Override the default "Registration timeout" quit reason */
int authprompt_pre_local_handshake_timeout(Client *client, const char **comment)
{
if (SEUSER(client))
{
if (SEUSER(client)->reason)
*comment = SEUSER(client)->reason;
else
*comment = "Account required to connect";
}
return HOOK_CONTINUE;
}