1
0
mirror of https://github.com/unrealircd/unrealircd.git synced 2026-07-04 12:13:13 +02:00
Files
unrealircd/src/modules/authprompt.c
T
2019-02-10 10:36:20 +01:00

530 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)
= {
"authprompt",
"1.0",
"SASL authentication for clients that don't support SASL",
"3.2-b8-1",
NULL
};
typedef struct _multiline MultiLine;
struct _multiline {
MultiLine *prev, *next;
char *line;
};
/** Configuration settings */
struct {
int enabled;
MultiLine *message;
MultiLine *fail_message;
} cfg;
/** User struct */
typedef struct _apuser APUser;
struct _apuser {
char *authmsg;
};
/* 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_require_sasl(aClient *acptr, char *reason);
int authprompt_sasl_continuation(aClient *acptr, char *buf);
int authprompt_sasl_result(aClient *acptr, int success);
int authprompt_place_host_ban(aClient *sptr, int action, char *reason, long duration);
int authprompt_find_tkline_match(aClient *sptr, aTKline *tk);
int authprompt_pre_connect(aClient *sptr);
CMD_FUNC(m_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(authprompt)
{
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, authprompt_config_test);
return MOD_SUCCESS;
}
MOD_INIT(authprompt)
{
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_REQUIRE_SASL, 0, authprompt_require_sasl);
HookAdd(modinfo->handle, HOOKTYPE_SASL_CONTINUATION, 0, authprompt_sasl_continuation);
HookAdd(modinfo->handle, HOOKTYPE_SASL_RESULT, 0, authprompt_sasl_result);
HookAdd(modinfo->handle, HOOKTYPE_PLACE_HOST_BAN, 0, authprompt_place_host_ban);
HookAdd(modinfo->handle, HOOKTYPE_FIND_TKLINE_MATCH, 0, authprompt_find_tkline_match);
/* 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", m_auth, 1, M_UNREGISTERED);
return MOD_SUCCESS;
}
MOD_LOAD(authprompt)
{
config_postdefaults();
return MOD_SUCCESS;
}
MOD_UNLOAD(authprompt)
{
free_config();
return MOD_SUCCESS;
}
static void init_config(void)
{
/* This sets some default values */
memset(&cfg, 0, sizeof(cfg));
cfg.enabled = 0;
}
static void addmultiline(MultiLine **l, char *line)
{
MultiLine *m = MyMallocEx(sizeof(MultiLine));
m->line = strdup(line);
append_ListItem((ListStruct *)m, (ListStruct **)l);
}
static void freemultiline(MultiLine *l)
{
MultiLine *l_next;
for (; l; l = l_next)
{
l_next = l->next;
safefree(l->line);
MyFree(l);
}
}
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.");
}
}
static void free_config(void)
{
freemultiline(cfg.message);
freemultiline(cfg.fail_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->ce_varname || strcmp(ce->ce_varname, "authentication-prompt"))
return 0;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!cep->ce_varname)
{
config_error("%s:%i: blank set::authentication-prompt item",
cep->ce_fileptr->cf_filename, cep->ce_varlinenum);
errors++;
} else
if (!cep->ce_vardata)
{
config_error("%s:%i: set::authentication-prompt::%s with no value",
cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
errors++;
} else
if (!strcmp(cep->ce_varname, "enabled"))
{
} else
if (!strcmp(cep->ce_varname, "message"))
{
} else
if (!strcmp(cep->ce_varname, "fail-message"))
{
} else
{
config_error("%s:%i: unknown directive set::authentication-prompt::%s",
cep->ce_fileptr->cf_filename, cep->ce_varlinenum, cep->ce_varname);
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->ce_varname || strcmp(ce->ce_varname, "authentication-prompt"))
return 0;
for (cep = ce->ce_entries; cep; cep = cep->ce_next)
{
if (!strcmp(cep->ce_varname, "enabled"))
{
cfg.enabled = config_checkval(cep->ce_vardata, CFG_YESNO);
} else
if (!strcmp(cep->ce_varname, "message"))
{
addmultiline(&cfg.message, cep->ce_vardata);
} else
if (!strcmp(cep->ce_varname, "fail-message"))
{
addmultiline(&cfg.fail_message, cep->ce_vardata);
}
}
return 1;
}
void authprompt_md_free(ModData *md)
{
APUser *se = md->ptr;
if (se)
{
safefree(se->authmsg);
MyFree(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.
* @notes 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;
}
/* NOTE: This function is stolen from m_sasl. Not good. */
static const char *encode_puid(aClient *client)
{
static char buf[HOSTLEN + 20];
/* create a cookie if necessary (and in case getrandom16 returns 0, then run again) */
while (!client->local->sasl_cookie)
client->local->sasl_cookie = getrandom16();
snprintf(buf, sizeof buf, "%s!0.%d", me.name, client->local->sasl_cookie);
return buf;
}
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))
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(aClient *sptr)
{
aClient *acptr;
char *addr = BadPtr(sptr->ip) ? "0" : sptr->ip;
char *certfp = moddata_client_get(sptr, "certfp");
acptr = find_client(SASL_SERVER, NULL);
if (!acptr)
{
/* Services down. */
return;
}
sendto_one(acptr, ":%s SASL %s %s H %s %s",
me.name, SASL_SERVER, encode_puid(sptr), addr, addr);
if (certfp)
sendto_one(acptr, ":%s SASL %s %s S %s %s",
me.name, SASL_SERVER, encode_puid(sptr), "PLAIN", certfp);
else
sendto_one(acptr, ":%s SASL %s %s S %s",
me.name, SASL_SERVER, encode_puid(sptr), "PLAIN");
/* The rest is sent from authprompt_sasl_continuation() */
sptr->local->sasl_out++;
}
CMD_FUNC(m_auth)
{
char *username = NULL;
char *password = NULL;
char *authbuf;
if (!SEUSER(sptr))
{
if (CHECKPROTO(sptr, PROTO_SASL))
sendnotice(sptr, "ERROR: Cannot use /AUTH when your client is doing SASL.");
else
sendnotice(sptr, "ERROR: /AUTH authentication request received before authentication prompt (too early!)");
return 0;
}
if ((parc < 2) || BadPtr(parv[1]) || !parse_nickpass(parv[1], &username, &password))
{
sendnotice(sptr, "ERROR: Syntax is: /AUTH <nickname>:<password>");
sendnotice(sptr, "Example: /AUTH mynick:secretpass");
return 0;
}
if (!SASL_SERVER)
{
sendnotice(sptr, "ERROR: SASL is not configured on this server, or services are down.");
// numeric instead? SERVICESDOWN?
return 0;
}
/* Presumably if the user is really fast, this could happen.. */
if (*sptr->local->sasl_agent || SEUSER(sptr)->authmsg)
{
sendnotice(sptr, "ERROR: Previous authentication request is still in progress. Please wait.");
return 0;
}
authbuf = make_authbuf(username, password);
if (!authbuf)
{
sendnotice(sptr, "ERROR: Internal error. Oversized username/password?");
return 0;
}
safestrdup(SEUSER(sptr)->authmsg, authbuf);
send_first_auth(sptr);
return 0;
}
void send_multinotice(aClient *sptr, MultiLine *m)
{
for (; m; m = m->next)
sendnotice(sptr, "%s", m->line);
}
void authprompt_tag_as_auth_required(aClient *sptr)
{
/* Allocate, and therefore indicate, that we are going to handle SASL for this user */
if (!SEUSER(sptr))
SetAPUser(sptr, MyMallocEx(sizeof(APUser)));
}
void authprompt_send_auth_required_message(aClient *sptr)
{
/* Display set::authentication-prompt::message */
send_multinotice(sptr, cfg.message);
}
int authprompt_require_sasl(aClient *sptr, char *reason)
{
/* If the client did SASL then we (authprompt) will not kick in */
if (CHECKPROTO(sptr, PROTO_SASL))
return 0;
authprompt_tag_as_auth_required(sptr);
/* Display the require authentication::reason */
if (reason)
sendnotice(sptr, "%s", reason);
authprompt_send_auth_required_message(sptr);
return 1;
}
/* Called upon "place a host ban on this user" (eg: spamfilter, blacklist, ..) */
int authprompt_place_host_ban(aClient *sptr, int action, 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(sptr) && !IsPerson(sptr))
{
/* Send ban reason */
if (reason)
sendnotice(sptr, "%s", reason);
/* And tag the user */
authprompt_tag_as_auth_required(sptr);
return 0; /* pretend user is exempt */
}
return 99; /* no action taken, proceed normally */
}
/** Called upon "check for KLINE/GLINE" */
int authprompt_find_tkline_match(aClient *sptr, aTKline *tk)
{
/* 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 ((tk->subtype & TKL_SUBTYPE_SOFT) && !IsLoggedIn(sptr) && !IsPerson(sptr))
{
/* Send ban reason */
if (tk->reason)
sendnotice(sptr, "%s", tk->reason);
/* And tag the user */
authprompt_tag_as_auth_required(sptr);
return 0; /* pretend user is exempt */
}
return 99; /* no action taken, proceed normally */
}
int authprompt_pre_connect(aClient *sptr)
{
/* If the user is tagged as auth required and not logged in, then.. */
if (SEUSER(sptr) && !IsLoggedIn(sptr))
{
authprompt_send_auth_required_message(sptr);
return -1; /* do not process register_user() */
}
return 0; /* no action taken, proceed normally */
}
int authprompt_sasl_continuation(aClient *sptr, char *buf)
{
/* If it's not for us (eg: user is doing real SASL) then return 0. */
if (!SEUSER(sptr) || !SEUSER(sptr)->authmsg)
return 0;
if (!strcmp(buf, "+"))
{
aClient *agent = find_client(sptr->local->sasl_agent, NULL);
if (agent)
{
sendto_one(agent, ":%s SASL %s %s C %s",
me.name, AGENT_SID(agent), encode_puid(sptr), SEUSER(sptr)->authmsg);
}
SEUSER(sptr)->authmsg = NULL;
}
return 1; /* inhibit displaying of message */
}
int authprompt_sasl_result(aClient *sptr, int success)
{
/* If it's not for us (eg: user is doing real SASL) then return 0. */
if (!SEUSER(sptr))
return 0;
if (!success)
{
send_multinotice(sptr, cfg.fail_message);
return 1;
}
/* Authentication was a success */
if (*sptr->name && sptr->user && *sptr->user->username && IsNotSpoof(sptr))
{
register_user(sptr, sptr, sptr->name, sptr->user->username, NULL, NULL, NULL);
/* NOTE: register_user() may return FLUSH_BUFFER here, but since the caller
* won't continue processing (won't touch 'sptr') it's safe.
* That is, as long as we 'return 1'.
*/
}
return 1; /* inhibit success/failure message */
}